Compare commits
4 Commits
ee361a0c4b
...
4b759395be
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b759395be | |||
| ce652d260c | |||
| d432627c61 | |||
| c269783229 |
63
MIGRATION_MERGE_COMPLETE.md
Normal file
63
MIGRATION_MERGE_COMPLETE.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Migration Merge Complete ✅
|
||||||
|
|
||||||
|
## Status: All Conflicts Resolved
|
||||||
|
|
||||||
|
Both migration files have been successfully merged with all conflicts resolved.
|
||||||
|
|
||||||
|
## Files Merged
|
||||||
|
|
||||||
|
### 1. `src/scripts/auto-setup.ts` ✅
|
||||||
|
- **Status**: Clean, no conflict markers
|
||||||
|
- **Migrations**: All 40 migrations in correct order
|
||||||
|
- **Format**: Uses `require()` for CommonJS compatibility
|
||||||
|
|
||||||
|
### 2. `src/scripts/migrate.ts` ✅
|
||||||
|
- **Status**: Clean, no conflict markers
|
||||||
|
- **Migrations**: All 40 migrations in correct order
|
||||||
|
- **Format**: Uses ES6 `import * as` syntax
|
||||||
|
|
||||||
|
## Migration Order (Final)
|
||||||
|
|
||||||
|
### Base Branch Migrations (m0-m29)
|
||||||
|
1. m0-m27: Core system migrations
|
||||||
|
2. m28: `20250130-migrate-to-vertex-ai`
|
||||||
|
3. m29: `20251203-add-user-notification-preferences`
|
||||||
|
|
||||||
|
### Dealer Claim Branch Migrations (m30-m39)
|
||||||
|
4. m30: `20251210-add-workflow-type-support`
|
||||||
|
5. m31: `20251210-enhance-workflow-templates`
|
||||||
|
6. m32: `20251210-add-template-id-foreign-key`
|
||||||
|
7. m33: `20251210-create-dealer-claim-tables`
|
||||||
|
8. m34: `20251210-create-proposal-cost-items-table`
|
||||||
|
9. m35: `20251211-create-internal-orders-table`
|
||||||
|
10. m36: `20251211-create-claim-budget-tracking-table`
|
||||||
|
11. m37: `20251213-drop-claim-details-invoice-columns`
|
||||||
|
12. m38: `20251213-create-claim-invoice-credit-note-tables`
|
||||||
|
13. m39: `20251214-create-dealer-completion-expenses`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
✅ No conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) found
|
||||||
|
✅ All migrations properly ordered
|
||||||
|
✅ Base branch migrations come first
|
||||||
|
✅ Dealer claim migrations follow
|
||||||
|
✅ Both files synchronized
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **If you see conflicts in your IDE/Git client:**
|
||||||
|
- Refresh your IDE/editor
|
||||||
|
- Run `git status` to check Git state
|
||||||
|
- If conflicts show in Git, run: `git add src/scripts/auto-setup.ts src/scripts/migrate.ts`
|
||||||
|
|
||||||
|
2. **Test the migrations:**
|
||||||
|
```bash
|
||||||
|
npm run migrate
|
||||||
|
# or
|
||||||
|
npm run setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Are Ready ✅
|
||||||
|
|
||||||
|
Both files are properly merged and ready to use. All 40 migrations are in the correct order with base branch migrations first, followed by dealer claim branch migrations.
|
||||||
|
|
||||||
@ -1,2 +1,7 @@
|
|||||||
|
<<<<<<<< HEAD:build/assets/conclusionApi-uNxtglEr.js
|
||||||
|
import{a as t}from"./index-9cOIFSn9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
|
//# sourceMappingURL=conclusionApi-uNxtglEr.js.map
|
||||||
|
========
|
||||||
import{a as t}from"./index-CogACwP9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BpFwwBOf.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
import{a as t}from"./index-CogACwP9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BpFwwBOf.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-CGl-93sb.js.map
|
//# sourceMappingURL=conclusionApi-CGl-93sb.js.map
|
||||||
|
>>>>>>>> ee361a0c4ba611c87efe7f97d044a7c711024d1b:build/assets/conclusionApi-CGl-93sb.js
|
||||||
|
|||||||
@ -1 +1,5 @@
|
|||||||
|
<<<<<<<< HEAD:build/assets/conclusionApi-uNxtglEr.js.map
|
||||||
|
{"version":3,"file":"conclusionApi-uNxtglEr.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
|
========
|
||||||
{"version":3,"file":"conclusionApi-CGl-93sb.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
{"version":3,"file":"conclusionApi-CGl-93sb.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
|
>>>>>>>> ee361a0c4ba611c87efe7f97d044a7c711024d1b:build/assets/conclusionApi-CGl-93sb.js.map
|
||||||
|
|||||||
7
build/assets/conclusionApi-uNxtglEr.js
Normal file
7
build/assets/conclusionApi-uNxtglEr.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<<<<<<<< HEAD:build/assets/conclusionApi-uNxtglEr.js
|
||||||
|
import{a as t}from"./index-9cOIFSn9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
|
//# sourceMappingURL=conclusionApi-uNxtglEr.js.map
|
||||||
|
========
|
||||||
|
import{a as t}from"./index-CogACwP9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BpFwwBOf.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
|
//# sourceMappingURL=conclusionApi-CGl-93sb.js.map
|
||||||
|
>>>>>>>> ee361a0c4ba611c87efe7f97d044a7c711024d1b:build/assets/conclusionApi-CGl-93sb.js
|
||||||
5
build/assets/conclusionApi-uNxtglEr.js.map
Normal file
5
build/assets/conclusionApi-uNxtglEr.js.map
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<<<<<<<< HEAD:build/assets/conclusionApi-uNxtglEr.js.map
|
||||||
|
{"version":3,"file":"conclusionApi-uNxtglEr.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
|
========
|
||||||
|
{"version":3,"file":"conclusionApi-CGl-93sb.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
|
>>>>>>>> ee361a0c4ba611c87efe7f97d044a7c711024d1b:build/assets/conclusionApi-CGl-93sb.js.map
|
||||||
64
build/assets/index-9cOIFSn9.js
Normal file
64
build/assets/index-9cOIFSn9.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-9cOIFSn9.js.map
Normal file
1
build/assets/index-9cOIFSn9.js.map
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-BmOYs32D.css
Normal file
1
build/assets/index-BmOYs32D.css
Normal file
File diff suppressed because one or more lines are too long
718
build/assets/ui-vendor-BmvKDhMD.js
Normal file
718
build/assets/ui-vendor-BmvKDhMD.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/ui-vendor-BmvKDhMD.js.map
Normal file
1
build/assets/ui-vendor-BmvKDhMD.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,15 +52,15 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-CogACwP9.js"></script>
|
<script type="module" crossorigin src="/assets/index-9cOIFSn9.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BpFwwBOf.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BmvKDhMD.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BDQMGM0H.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BmOYs32D.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
202
docs/VERTEX_AI_INTEGRATION.md
Normal file
202
docs/VERTEX_AI_INTEGRATION.md
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# Vertex AI Gemini Integration
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AI service has been migrated from multi-provider support (Claude, OpenAI, Gemini API) to **Google Cloud Vertex AI Gemini** using service account authentication. This provides better enterprise-grade security and uses the same credentials as Google Cloud Storage.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Package Dependencies
|
||||||
|
|
||||||
|
**Removed:**
|
||||||
|
- `@anthropic-ai/sdk` (Claude SDK)
|
||||||
|
- `@google/generative-ai` (Gemini API SDK)
|
||||||
|
- `openai` (OpenAI SDK)
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
- `@google-cloud/vertexai` (Vertex AI SDK)
|
||||||
|
|
||||||
|
### 2. Service Account Authentication
|
||||||
|
|
||||||
|
The AI service now uses the same service account JSON file as GCS:
|
||||||
|
- **Location**: `credentials/re-platform-workflow-dealer-3d5738fcc1f9.json`
|
||||||
|
- **Project ID**: `re-platform-workflow-dealer`
|
||||||
|
- **Default Region**: `us-central1`
|
||||||
|
|
||||||
|
### 3. Configuration
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
```env
|
||||||
|
# Required (already configured for GCS)
|
||||||
|
GCP_PROJECT_ID=re-platform-workflow-dealer
|
||||||
|
GCP_KEY_FILE=./credentials/re-platform-workflow-dealer-3d5738fcc1f9.json
|
||||||
|
|
||||||
|
# Optional (defaults provided)
|
||||||
|
VERTEX_AI_MODEL=gemini-2.5-flash
|
||||||
|
VERTEX_AI_LOCATION=asia-south1
|
||||||
|
AI_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admin Panel Configuration:**
|
||||||
|
- `AI_ENABLED` - Enable/disable AI features
|
||||||
|
- `VERTEX_AI_MODEL` - Model name (default: `gemini-2.5-flash`)
|
||||||
|
- `AI_MAX_REMARK_LENGTH` - Maximum characters for conclusion remarks (default: 2000)
|
||||||
|
|
||||||
|
### 4. Available Models
|
||||||
|
|
||||||
|
| Model Name | Description | Use Case |
|
||||||
|
|------------|-------------|----------|
|
||||||
|
| `gemini-2.5-flash` | Latest fast model (default) | General purpose, quick responses |
|
||||||
|
| `gemini-1.5-flash` | Previous fast model | General purpose |
|
||||||
|
| `gemini-1.5-pro` | Advanced model | Complex tasks, better quality |
|
||||||
|
| `gemini-1.5-pro-latest` | Latest Pro version | Best quality, complex reasoning |
|
||||||
|
|
||||||
|
### 5. Supported Regions
|
||||||
|
|
||||||
|
| Region Code | Location | Availability |
|
||||||
|
|-------------|----------|--------------|
|
||||||
|
| `us-central1` | Iowa, USA | ✅ Default |
|
||||||
|
| `us-east1` | South Carolina, USA | ✅ |
|
||||||
|
| `us-west1` | Oregon, USA | ✅ |
|
||||||
|
| `europe-west1` | Belgium | ✅ |
|
||||||
|
| `asia-south1` | Mumbai, India | ✅ |
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### Step 1: Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Re_Backend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install `@google-cloud/vertexai` and remove old AI SDKs.
|
||||||
|
|
||||||
|
### Step 2: Verify Service Account Permissions
|
||||||
|
|
||||||
|
Ensure your service account (`re-bridge-workflow@re-platform-workflow-dealer.iam.gserviceaccount.com`) has:
|
||||||
|
- **Vertex AI User** role (`roles/aiplatform.user`)
|
||||||
|
|
||||||
|
### Step 3: Enable Vertex AI API
|
||||||
|
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Navigate to **APIs & Services** > **Library**
|
||||||
|
3. Search for **"Vertex AI API"**
|
||||||
|
4. Click **Enable**
|
||||||
|
|
||||||
|
### Step 4: Verify Configuration
|
||||||
|
|
||||||
|
Check that your `.env` file has:
|
||||||
|
```env
|
||||||
|
GCP_PROJECT_ID=re-platform-workflow-dealer
|
||||||
|
GCP_KEY_FILE=./credentials/re-platform-workflow-dealer-3d5738fcc1f9.json
|
||||||
|
VERTEX_AI_MODEL=gemini-2.5-flash
|
||||||
|
VERTEX_AI_LOCATION=us-central1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Test the Integration
|
||||||
|
|
||||||
|
Start the backend and check logs for:
|
||||||
|
```
|
||||||
|
[AI Service] ✅ Vertex AI provider initialized successfully with model: gemini-2.5-flash
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Interface (Unchanged)
|
||||||
|
|
||||||
|
The public API remains the same - no changes needed in controllers or routes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Check if AI is available
|
||||||
|
aiService.isAvailable(): boolean
|
||||||
|
|
||||||
|
// Get provider name
|
||||||
|
aiService.getProviderName(): string
|
||||||
|
|
||||||
|
// Generate conclusion remark
|
||||||
|
aiService.generateConclusionRemark(context): Promise<{
|
||||||
|
remark: string;
|
||||||
|
confidence: number;
|
||||||
|
keyPoints: string[];
|
||||||
|
provider: string;
|
||||||
|
}>
|
||||||
|
|
||||||
|
// Reinitialize (after config changes)
|
||||||
|
aiService.reinitialize(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: "Module not found: @google-cloud/vertexai"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
npm install @google-cloud/vertexai
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: "Service account key file not found"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify file exists at: `credentials/re-platform-workflow-dealer-3d5738fcc1f9.json`
|
||||||
|
- Check `GCP_KEY_FILE` path in `.env` is correct
|
||||||
|
- Ensure file has read permissions
|
||||||
|
|
||||||
|
### Error: "Model was not found or your project does not have access"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify Vertex AI API is enabled in Google Cloud Console
|
||||||
|
- Check model name is correct (e.g., `gemini-2.5-flash`)
|
||||||
|
- Ensure model is available in your selected region
|
||||||
|
- Verify service account has `roles/aiplatform.user` role
|
||||||
|
|
||||||
|
### Error: "Permission denied"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify service account has Vertex AI User role
|
||||||
|
- Check service account key hasn't been revoked
|
||||||
|
- Regenerate service account key if needed
|
||||||
|
|
||||||
|
### Error: "API not enabled"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Enable Vertex AI API in Google Cloud Console
|
||||||
|
- Wait a few minutes for propagation
|
||||||
|
- Restart the backend service
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
- ✅ Removed multi-provider support (Claude, OpenAI, Gemini API)
|
||||||
|
- ✅ Now uses Vertex AI Gemini exclusively
|
||||||
|
- ✅ Uses service account authentication (same as GCS)
|
||||||
|
- ✅ Simplified configuration (no API keys needed)
|
||||||
|
|
||||||
|
### What Stayed the Same
|
||||||
|
- ✅ Public API interface (`aiService` methods)
|
||||||
|
- ✅ Conclusion generation functionality
|
||||||
|
- ✅ Admin panel configuration structure
|
||||||
|
- ✅ Error handling and logging
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
- ✅ Existing code using `aiService` will work without changes
|
||||||
|
- ✅ Conclusion controller unchanged
|
||||||
|
- ✅ Admin panel can still enable/disable AI features
|
||||||
|
- ✅ Configuration cache system still works
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [ ] `@google-cloud/vertexai` package installed
|
||||||
|
- [ ] Service account key file exists and is valid
|
||||||
|
- [ ] Vertex AI API is enabled in Google Cloud Console
|
||||||
|
- [ ] Service account has `Vertex AI User` role
|
||||||
|
- [ ] `.env` file has correct `GCP_PROJECT_ID` and `GCP_KEY_FILE`
|
||||||
|
- [ ] Backend logs show successful initialization
|
||||||
|
- [ ] AI conclusion generation works for test requests
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check backend logs for detailed error messages
|
||||||
|
2. Verify Google Cloud Console settings
|
||||||
|
3. Ensure service account permissions are correct
|
||||||
|
4. Test with a simple request to isolate issues
|
||||||
|
|
||||||
16
env.example
16
env.example
@ -38,16 +38,12 @@ SMTP_USER=notifications@royalenfield.com
|
|||||||
SMTP_PASSWORD=your_smtp_password
|
SMTP_PASSWORD=your_smtp_password
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
||||||
|
|
||||||
# AI Service (for conclusion generation)
|
# AI Service (for conclusion generation) - Vertex AI Gemini
|
||||||
# Note: API keys are configured in the admin panel (database), not in environment variables
|
# Uses service account credentials from GCP_KEY_FILE
|
||||||
# AI Provider: claude, openai, or gemini
|
# Vertex AI Model Configuration (optional - defaults used if not set)
|
||||||
AI_PROVIDER=claude
|
VERTEX_AI_MODEL=gemini-2.5-flash
|
||||||
|
VERTEX_AI_LOCATION=asia-south1
|
||||||
# AI Model Configuration (optional - defaults used if not set)
|
# Note: GCP_PROJECT_ID and GCP_KEY_FILE are already configured above for GCS
|
||||||
# These can be overridden via environment variables or admin panel
|
|
||||||
CLAUDE_MODEL=claude-sonnet-4-20250514
|
|
||||||
OPENAI_MODEL=gpt-4o
|
|
||||||
GEMINI_MODEL=gemini-2.0-flash-lite
|
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|||||||
62
package-lock.json
generated
62
package-lock.json
generated
@ -8,9 +8,8 @@
|
|||||||
"name": "re-workflow-backend",
|
"name": "re-workflow-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.68.0",
|
|
||||||
"@google-cloud/storage": "^7.18.0",
|
"@google-cloud/storage": "^7.18.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google-cloud/vertexai": "^1.10.0",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
@ -78,26 +77,6 @@
|
|||||||
"npm": ">=10.0.0"
|
"npm": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
|
||||||
"version": "0.68.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.68.0.tgz",
|
|
||||||
"integrity": "sha512-SMYAmbbiprG8k1EjEPMTwaTqssDT7Ae+jxcR5kWXiqTlbwMR2AthXtscEVWOHkRfyAV5+y3PFYTJRNa3OJWIEw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema-to-ts": "^3.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"anthropic-ai-sdk": "bin/cli"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"zod": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@aws-crypto/sha256-browser": {
|
"node_modules/@aws-crypto/sha256-browser": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
||||||
@ -1296,15 +1275,6 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
|
||||||
"version": "7.28.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
|
||||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.2",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||||
@ -1681,11 +1651,14 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google/generative-ai": {
|
"node_modules/@google-cloud/vertexai": {
|
||||||
"version": "0.24.1",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.10.0.tgz",
|
||||||
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
|
"integrity": "sha512-HqYqoivNtkq59po8m7KI0n+lWKdz4kabENncYQXZCX/hBWJfXtKAfR/2nUQsP+TwSfHKoA7zDL2RrJYIv/j3VQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"google-auth-library": "^9.1.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
@ -8185,19 +8158,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-to-ts": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.18.3",
|
|
||||||
"ts-algebra": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
@ -11100,12 +11060,6 @@
|
|||||||
"node": ">= 14.0.0"
|
"node": ">= 14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-algebra": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
|
|||||||
@ -22,9 +22,8 @@
|
|||||||
"cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts"
|
"cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.68.0",
|
|
||||||
"@google-cloud/storage": "^7.18.0",
|
"@google-cloud/storage": "^7.18.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google-cloud/vertexai": "^1.10.0",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
|||||||
17
setup-env.sh
17
setup-env.sh
@ -112,8 +112,15 @@ create_env_file() {
|
|||||||
read -p "Enter GCP_PROJECT_ID (or press Enter to skip): " GCP_PROJECT_ID
|
read -p "Enter GCP_PROJECT_ID (or press Enter to skip): " GCP_PROJECT_ID
|
||||||
read -p "Enter GCP_BUCKET_NAME (or press Enter to skip): " GCP_BUCKET_NAME
|
read -p "Enter GCP_BUCKET_NAME (or press Enter to skip): " GCP_BUCKET_NAME
|
||||||
|
|
||||||
read -p "Enter CLAUDE_MODEL [default: claude-sonnet-4-20250514]: " CLAUDE_MODEL
|
# Vertex AI Configuration
|
||||||
CLAUDE_MODEL=${CLAUDE_MODEL:-claude-sonnet-4-20250514}
|
echo ""
|
||||||
|
echo "--- Vertex AI Gemini Configuration (Optional) ---"
|
||||||
|
echo "Note: These have defaults and are optional. Service account credentials are required."
|
||||||
|
read -p "Enter VERTEX_AI_MODEL [default: gemini-2.5-flash]: " VERTEX_AI_MODEL
|
||||||
|
VERTEX_AI_MODEL=${VERTEX_AI_MODEL:-gemini-2.5-flash}
|
||||||
|
|
||||||
|
read -p "Enter VERTEX_AI_LOCATION [default: us-central1]: " VERTEX_AI_LOCATION
|
||||||
|
VERTEX_AI_LOCATION=${VERTEX_AI_LOCATION:-us-central1}
|
||||||
|
|
||||||
# Create .env file
|
# Create .env file
|
||||||
cat > "$file_name" << EOF
|
cat > "$file_name" << EOF
|
||||||
@ -157,8 +164,10 @@ SMTP_USER=${SMTP_USER}
|
|||||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
||||||
|
|
||||||
# AI Service (for conclusion generation) mandatory for claude
|
# Vertex AI Gemini Configuration (for conclusion generation)
|
||||||
CLAUDE_MODEL=${CLAUDE_MODEL}
|
# Service account credentials should be placed in ./credentials/ folder
|
||||||
|
VERTEX_AI_MODEL=${VERTEX_AI_MODEL}
|
||||||
|
VERTEX_AI_LOCATION=${VERTEX_AI_LOCATION}
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|||||||
@ -433,7 +433,7 @@ export const updateConfiguration = async (req: Request, res: Response): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If AI config was updated, reinitialize AI service
|
// If AI config was updated, reinitialize AI service
|
||||||
const aiConfigKeys = ['AI_PROVIDER', 'CLAUDE_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'CLAUDE_MODEL', 'OPENAI_MODEL', 'GEMINI_MODEL', 'AI_ENABLED'];
|
const aiConfigKeys = ['AI_ENABLED'];
|
||||||
if (aiConfigKeys.includes(configKey)) {
|
if (aiConfigKeys.includes(configKey)) {
|
||||||
try {
|
try {
|
||||||
const { aiService } = require('../services/ai.service');
|
const { aiService } = require('../services/ai.service');
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export class ConclusionController {
|
|||||||
logger.warn(`[Conclusion] AI service unavailable for request ${requestId}`);
|
logger.warn(`[Conclusion] AI service unavailable for request ${requestId}`);
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
error: 'AI service not available',
|
error: 'AI service not available',
|
||||||
message: 'AI features are currently unavailable. Please configure an AI provider (Claude, OpenAI, or Gemini) in the admin panel, or write the conclusion manually.',
|
message: 'AI features are currently unavailable. Please verify Vertex AI configuration and service account credentials, or write the conclusion manually.',
|
||||||
canContinueManually: true
|
canContinueManually: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
199
src/migrations/20250130-migrate-to-vertex-ai.ts
Normal file
199
src/migrations/20250130-migrate-to-vertex-ai.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { QueryInterface, QueryTypes } from 'sequelize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration to migrate from multi-provider AI to Vertex AI Gemini
|
||||||
|
*
|
||||||
|
* Removes:
|
||||||
|
* - AI_PROVIDER
|
||||||
|
* - CLAUDE_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY
|
||||||
|
* - CLAUDE_MODEL, OPENAI_MODEL, GEMINI_MODEL
|
||||||
|
* - VERTEX_AI_MODEL (moved to environment variable only)
|
||||||
|
* - VERTEX_AI_LOCATION (moved to environment variable only)
|
||||||
|
*
|
||||||
|
* Note: Both VERTEX_AI_MODEL and VERTEX_AI_LOCATION are now configured via
|
||||||
|
* environment variables only (not in admin settings).
|
||||||
|
*
|
||||||
|
* This migration is idempotent - it will only delete configs that exist.
|
||||||
|
*/
|
||||||
|
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
// Remove old AI provider configurations
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DELETE FROM admin_configurations
|
||||||
|
WHERE config_key IN (
|
||||||
|
'AI_PROVIDER',
|
||||||
|
'CLAUDE_API_KEY',
|
||||||
|
'OPENAI_API_KEY',
|
||||||
|
'GEMINI_API_KEY',
|
||||||
|
'CLAUDE_MODEL',
|
||||||
|
'OPENAI_MODEL',
|
||||||
|
'GEMINI_MODEL',
|
||||||
|
'VERTEX_AI_MODEL',
|
||||||
|
'VERTEX_AI_LOCATION'
|
||||||
|
)
|
||||||
|
`, { type: QueryTypes.DELETE });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
// This migration only removes configs, so down migration would restore them
|
||||||
|
// However, we don't restore them as they're now environment-only
|
||||||
|
console.log('[Migration] Down migration skipped - AI configs are now environment-only');
|
||||||
|
|
||||||
|
// Restore old configurations (for rollback)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO admin_configurations (
|
||||||
|
config_id, config_key, config_category, config_value, value_type,
|
||||||
|
display_name, description, default_value, is_editable, is_sensitive,
|
||||||
|
validation_rules, ui_component, options, sort_order, requires_restart,
|
||||||
|
last_modified_by, last_modified_at, created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'AI_PROVIDER',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'claude',
|
||||||
|
'STRING',
|
||||||
|
'AI Provider',
|
||||||
|
'Active AI provider for conclusion generation (claude, openai, or gemini)',
|
||||||
|
'claude',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
|
||||||
|
'select',
|
||||||
|
'["claude", "openai", "gemini"]'::jsonb,
|
||||||
|
22,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'CLAUDE_API_KEY',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'',
|
||||||
|
'STRING',
|
||||||
|
'Claude API Key',
|
||||||
|
'API key for Claude (Anthropic) - Get from console.anthropic.com',
|
||||||
|
'',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
23,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'OPENAI_API_KEY',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'',
|
||||||
|
'STRING',
|
||||||
|
'OpenAI API Key',
|
||||||
|
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
|
||||||
|
'',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
24,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'GEMINI_API_KEY',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'',
|
||||||
|
'STRING',
|
||||||
|
'Gemini API Key',
|
||||||
|
'API key for Gemini (Google) - Get from ai.google.dev',
|
||||||
|
'',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
'{"minLength": 20}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
25,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'CLAUDE_MODEL',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'claude-sonnet-4-20250514',
|
||||||
|
'STRING',
|
||||||
|
'Claude Model',
|
||||||
|
'Claude (Anthropic) model to use for AI generation',
|
||||||
|
'claude-sonnet-4-20250514',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
'{}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
27,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'OPENAI_MODEL',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'gpt-4o',
|
||||||
|
'STRING',
|
||||||
|
'OpenAI Model',
|
||||||
|
'OpenAI model to use for AI generation',
|
||||||
|
'gpt-4o',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
'{}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
28,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'GEMINI_MODEL',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'gemini-2.0-flash-lite',
|
||||||
|
'STRING',
|
||||||
|
'Gemini Model',
|
||||||
|
'Gemini (Google) model to use for AI generation',
|
||||||
|
'gemini-2.0-flash-lite',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
'{}'::jsonb,
|
||||||
|
'input',
|
||||||
|
NULL,
|
||||||
|
29,
|
||||||
|
false,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (config_key) DO NOTHING
|
||||||
|
`, { type: QueryTypes.INSERT });
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,12 +1,42 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
import { QueryInterface, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if a column exists in a table
|
||||||
|
*/
|
||||||
|
async function columnExists(
|
||||||
|
queryInterface: QueryInterface,
|
||||||
|
tableName: string,
|
||||||
|
columnName: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const tableDescription = await queryInterface.describeTable(tableName);
|
||||||
|
return columnName in tableDescription;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'dms_number');
|
const columnsToRemove = [
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'e_invoice_number');
|
'dms_number',
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'e_invoice_date');
|
'e_invoice_number',
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'credit_note_number');
|
'e_invoice_date',
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'credit_note_date');
|
'credit_note_number',
|
||||||
await queryInterface.removeColumn('dealer_claim_details', 'credit_note_amount');
|
'credit_note_date',
|
||||||
|
'credit_note_amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only remove columns if they exist
|
||||||
|
// This handles the case where dealer_claim_details was created without these columns
|
||||||
|
for (const columnName of columnsToRemove) {
|
||||||
|
const exists = await columnExists(queryInterface, 'dealer_claim_details', columnName);
|
||||||
|
if (exists) {
|
||||||
|
await queryInterface.removeColumn('dealer_claim_details', columnName);
|
||||||
|
console.log(` ✅ Removed column: ${columnName}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ⏭️ Column ${columnName} does not exist, skipping...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
|||||||
@ -118,14 +118,20 @@ async function runMigrations(): Promise<void> {
|
|||||||
const m25 = require('../migrations/20250126-add-pause-fields-to-workflow-requests');
|
const m25 = require('../migrations/20250126-add-pause-fields-to-workflow-requests');
|
||||||
const m26 = require('../migrations/20250126-add-pause-fields-to-approval-levels');
|
const m26 = require('../migrations/20250126-add-pause-fields-to-approval-levels');
|
||||||
const m27 = require('../migrations/20250127-migrate-in-progress-to-pending');
|
const m27 = require('../migrations/20250127-migrate-in-progress-to-pending');
|
||||||
const m28 = require('../migrations/20251203-add-user-notification-preferences');
|
// Base branch migrations (m28-m29)
|
||||||
const m29 = require('../migrations/20251210-add-workflow-type-support');
|
const m28 = require('../migrations/20250130-migrate-to-vertex-ai');
|
||||||
const m30 = require('../migrations/20251210-enhance-workflow-templates');
|
const m29 = require('../migrations/20251203-add-user-notification-preferences');
|
||||||
const m31 = require('../migrations/20251210-add-template-id-foreign-key');
|
// Dealer claim branch migrations (m30-m39)
|
||||||
const m32 = require('../migrations/20251210-create-dealer-claim-tables');
|
const m30 = require('../migrations/20251210-add-workflow-type-support');
|
||||||
const m33 = require('../migrations/20251210-create-proposal-cost-items-table');
|
const m31 = require('../migrations/20251210-enhance-workflow-templates');
|
||||||
const m34 = require('../migrations/20251211-create-internal-orders-table');
|
const m32 = require('../migrations/20251210-add-template-id-foreign-key');
|
||||||
const m35 = require('../migrations/20251211-create-claim-budget-tracking-table');
|
const m33 = require('../migrations/20251210-create-dealer-claim-tables');
|
||||||
|
const m34 = require('../migrations/20251210-create-proposal-cost-items-table');
|
||||||
|
const m35 = require('../migrations/20251211-create-internal-orders-table');
|
||||||
|
const m36 = require('../migrations/20251211-create-claim-budget-tracking-table');
|
||||||
|
const m37 = require('../migrations/20251213-drop-claim-details-invoice-columns');
|
||||||
|
const m38 = require('../migrations/20251213-create-claim-invoice-credit-note-tables');
|
||||||
|
const m39 = require('../migrations/20251214-create-dealer-completion-expenses');
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
@ -156,14 +162,20 @@ async function runMigrations(): Promise<void> {
|
|||||||
{ name: '20250126-add-pause-fields-to-workflow-requests', module: m25 },
|
{ name: '20250126-add-pause-fields-to-workflow-requests', module: m25 },
|
||||||
{ name: '20250126-add-pause-fields-to-approval-levels', module: m26 },
|
{ name: '20250126-add-pause-fields-to-approval-levels', module: m26 },
|
||||||
{ name: '20250127-migrate-in-progress-to-pending', module: m27 },
|
{ name: '20250127-migrate-in-progress-to-pending', module: m27 },
|
||||||
{ name: '20251203-add-user-notification-preferences', module: m28 },
|
// Base branch migrations (m28-m29)
|
||||||
{ name: '20251210-add-workflow-type-support', module: m29 },
|
{ name: '20250130-migrate-to-vertex-ai', module: m28 },
|
||||||
{ name: '20251210-enhance-workflow-templates', module: m30 },
|
{ name: '20251203-add-user-notification-preferences', module: m29 },
|
||||||
{ name: '20251210-add-template-id-foreign-key', module: m31 },
|
// Dealer claim branch migrations (m30-m39)
|
||||||
{ name: '20251210-create-dealer-claim-tables', module: m32 },
|
{ name: '20251210-add-workflow-type-support', module: m30 },
|
||||||
{ name: '20251210-create-proposal-cost-items-table', module: m33 },
|
{ name: '20251210-enhance-workflow-templates', module: m31 },
|
||||||
{ name: '20251211-create-internal-orders-table', module: m34 },
|
{ name: '20251210-add-template-id-foreign-key', module: m32 },
|
||||||
{ name: '20251211-create-claim-budget-tracking-table', module: m35 },
|
{ name: '20251210-create-dealer-claim-tables', module: m33 },
|
||||||
|
{ name: '20251210-create-proposal-cost-items-table', module: m34 },
|
||||||
|
{ name: '20251211-create-internal-orders-table', module: m35 },
|
||||||
|
{ name: '20251211-create-claim-budget-tracking-table', module: m36 },
|
||||||
|
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
||||||
|
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
||||||
|
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
|
|||||||
@ -22,15 +22,26 @@ import * as m18 from '../migrations/20251118-add-breach-reason-to-approval-level
|
|||||||
import * as m19 from '../migrations/20251121-add-ai-model-configs';
|
import * as m19 from '../migrations/20251121-add-ai-model-configs';
|
||||||
import * as m20 from '../migrations/20250122-create-request-summaries';
|
import * as m20 from '../migrations/20250122-create-request-summaries';
|
||||||
import * as m21 from '../migrations/20250122-create-shared-summaries';
|
import * as m21 from '../migrations/20250122-create-shared-summaries';
|
||||||
import * as m22 from '../migrations/20251210-add-workflow-type-support';
|
import * as m22 from '../migrations/20250123-update-request-number-format';
|
||||||
import * as m23 from '../migrations/20251210-enhance-workflow-templates';
|
import * as m23 from '../migrations/20250126-add-paused-to-enum';
|
||||||
import * as m24 from '../migrations/20251210-add-template-id-foreign-key';
|
import * as m24 from '../migrations/20250126-add-paused-to-workflow-status-enum';
|
||||||
import * as m25 from '../migrations/20251210-create-dealer-claim-tables';
|
import * as m25 from '../migrations/20250126-add-pause-fields-to-workflow-requests';
|
||||||
import * as m26 from '../migrations/20251210-create-proposal-cost-items-table';
|
import * as m26 from '../migrations/20250126-add-pause-fields-to-approval-levels';
|
||||||
import * as m27 from '../migrations/20251211-create-internal-orders-table';
|
import * as m27 from '../migrations/20250127-migrate-in-progress-to-pending';
|
||||||
import * as m28 from '../migrations/20251211-create-claim-budget-tracking-table';
|
// Base branch migrations (m28-m29)
|
||||||
import * as m29 from '../migrations/20251213-create-claim-invoice-credit-note-tables';
|
import * as m28 from '../migrations/20250130-migrate-to-vertex-ai';
|
||||||
import * as m30 from '../migrations/20251214-create-dealer-completion-expenses';
|
import * as m29 from '../migrations/20251203-add-user-notification-preferences';
|
||||||
|
// Dealer claim branch migrations (m30-m39)
|
||||||
|
import * as m30 from '../migrations/20251210-add-workflow-type-support';
|
||||||
|
import * as m31 from '../migrations/20251210-enhance-workflow-templates';
|
||||||
|
import * as m32 from '../migrations/20251210-add-template-id-foreign-key';
|
||||||
|
import * as m33 from '../migrations/20251210-create-dealer-claim-tables';
|
||||||
|
import * as m34 from '../migrations/20251210-create-proposal-cost-items-table';
|
||||||
|
import * as m35 from '../migrations/20251211-create-internal-orders-table';
|
||||||
|
import * as m36 from '../migrations/20251211-create-claim-budget-tracking-table';
|
||||||
|
import * as m37 from '../migrations/20251213-drop-claim-details-invoice-columns';
|
||||||
|
import * as m38 from '../migrations/20251213-create-claim-invoice-credit-note-tables';
|
||||||
|
import * as m39 from '../migrations/20251214-create-dealer-completion-expenses';
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -67,15 +78,26 @@ const migrations: Migration[] = [
|
|||||||
{ name: '20251121-add-ai-model-configs', module: m19 },
|
{ name: '20251121-add-ai-model-configs', module: m19 },
|
||||||
{ name: '20250122-create-request-summaries', module: m20 },
|
{ name: '20250122-create-request-summaries', module: m20 },
|
||||||
{ name: '20250122-create-shared-summaries', module: m21 },
|
{ name: '20250122-create-shared-summaries', module: m21 },
|
||||||
{ name: '20251210-add-workflow-type-support', module: m22 },
|
{ name: '20250123-update-request-number-format', module: m22 },
|
||||||
{ name: '20251210-enhance-workflow-templates', module: m23 },
|
{ name: '20250126-add-paused-to-enum', module: m23 },
|
||||||
{ name: '20251210-add-template-id-foreign-key', module: m24 },
|
{ name: '20250126-add-paused-to-workflow-status-enum', module: m24 },
|
||||||
{ name: '20251210-create-dealer-claim-tables', module: m25 },
|
{ name: '20250126-add-pause-fields-to-workflow-requests', module: m25 },
|
||||||
{ name: '20251210-create-proposal-cost-items-table', module: m26 },
|
{ name: '20250126-add-pause-fields-to-approval-levels', module: m26 },
|
||||||
{ name: '20251211-create-internal-orders-table', module: m27 },
|
{ name: '20250127-migrate-in-progress-to-pending', module: m27 },
|
||||||
{ name: '20251211-create-claim-budget-tracking-table', module: m28 },
|
// Base branch migrations (m28-m29)
|
||||||
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m29 },
|
{ name: '20250130-migrate-to-vertex-ai', module: m28 },
|
||||||
{ name: '20251214-create-dealer-completion-expenses', module: m30 },
|
{ name: '20251203-add-user-notification-preferences', module: m29 },
|
||||||
|
// Dealer claim branch migrations (m30-m39)
|
||||||
|
{ name: '20251210-add-workflow-type-support', module: m30 },
|
||||||
|
{ name: '20251210-enhance-workflow-templates', module: m31 },
|
||||||
|
{ name: '20251210-add-template-id-foreign-key', module: m32 },
|
||||||
|
{ name: '20251210-create-dealer-claim-tables', module: m33 },
|
||||||
|
{ name: '20251210-create-proposal-cost-items-table', module: m34 },
|
||||||
|
{ name: '20251211-create-internal-orders-table', module: m35 },
|
||||||
|
{ name: '20251211-create-claim-budget-tracking-table', module: m36 },
|
||||||
|
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
||||||
|
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
||||||
|
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -368,79 +368,7 @@ async function seedAdminConfigurations() {
|
|||||||
NOW()
|
NOW()
|
||||||
),
|
),
|
||||||
|
|
||||||
-- AI Configuration (from migration 20251111-add-ai-provider-configs)
|
-- AI Configuration (Vertex AI Gemini)
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'AI_PROVIDER',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude',
|
|
||||||
'STRING',
|
|
||||||
'AI Provider',
|
|
||||||
'Active AI provider for conclusion generation (claude, openai, or gemini)',
|
|
||||||
'claude',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
|
|
||||||
'select',
|
|
||||||
100,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Claude API Key',
|
|
||||||
'API key for Claude (Anthropic) - Get from console.anthropic.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
101,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI API Key',
|
|
||||||
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
102,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Gemini API Key',
|
|
||||||
'API key for Gemini (Google) - Get from ai.google.dev',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"minLength": 20}'::jsonb,
|
|
||||||
'input',
|
|
||||||
103,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
'AI_ENABLED',
|
'AI_ENABLED',
|
||||||
@ -454,61 +382,7 @@ async function seedAdminConfigurations() {
|
|||||||
false,
|
false,
|
||||||
'{"type": "boolean"}'::jsonb,
|
'{"type": "boolean"}'::jsonb,
|
||||||
'toggle',
|
'toggle',
|
||||||
104,
|
100,
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
'STRING',
|
|
||||||
'Claude Model',
|
|
||||||
'Claude (Anthropic) model to use for AI generation',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
105,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gpt-4o',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI Model',
|
|
||||||
'OpenAI model to use for AI generation',
|
|
||||||
'gpt-4o',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
106,
|
|
||||||
false,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
'STRING',
|
|
||||||
'Gemini Model',
|
|
||||||
'Gemini (Google) model to use for AI generation',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
107,
|
|
||||||
false,
|
false,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
@ -526,7 +400,7 @@ async function seedAdminConfigurations() {
|
|||||||
false,
|
false,
|
||||||
'{"type": "boolean"}'::jsonb,
|
'{"type": "boolean"}'::jsonb,
|
||||||
'toggle',
|
'toggle',
|
||||||
108,
|
101,
|
||||||
false,
|
false,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
@ -544,7 +418,7 @@ async function seedAdminConfigurations() {
|
|||||||
false,
|
false,
|
||||||
'{"type": "number", "min": 500, "max": 5000}'::jsonb,
|
'{"type": "number", "min": 500, "max": 5000}'::jsonb,
|
||||||
'number',
|
'number',
|
||||||
109,
|
104,
|
||||||
false,
|
false,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
|
|||||||
@ -1,186 +1,19 @@
|
|||||||
import logger, { logAIEvent } from '@utils/logger';
|
import logger, { logAIEvent } from '@utils/logger';
|
||||||
import { getAIProviderConfig } from './configReader.service';
|
import { getConfigValue } from './configReader.service';
|
||||||
|
import { VertexAI } from '@google-cloud/vertexai';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
// Provider-specific interfaces
|
// Vertex AI Configuration
|
||||||
interface AIProvider {
|
const PROJECT_ID = process.env.GCP_PROJECT_ID || 're-platform-workflow-dealer';
|
||||||
generateText(prompt: string): Promise<string>;
|
const LOCATION = process.env.VERTEX_AI_LOCATION || 'asia-south1';
|
||||||
isAvailable(): boolean;
|
const KEY_FILE_PATH = process.env.GCP_KEY_FILE || resolve(__dirname, '../../credentials/re-platform-workflow-dealer-3d5738fcc1f9.json');
|
||||||
getProviderName(): string;
|
const DEFAULT_MODEL = 'gemini-2.5-flash';
|
||||||
}
|
|
||||||
|
|
||||||
// Claude Provider
|
|
||||||
class ClaudeProvider implements AIProvider {
|
|
||||||
private client: any = null;
|
|
||||||
private model: string;
|
|
||||||
|
|
||||||
constructor(apiKey?: string, model?: string) {
|
|
||||||
// Allow model override via parameter, environment variable, or default
|
|
||||||
// Current models (November 2025):
|
|
||||||
// - claude-sonnet-4-20250514 (default - latest Claude Sonnet 4)
|
|
||||||
// Priority: 1. Provided model parameter, 2. Environment variable, 3. Default
|
|
||||||
this.model = model || process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Priority: 1. Provided key, 2. Environment variable
|
|
||||||
const key = apiKey || process.env.CLAUDE_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
||||||
|
|
||||||
if (!key || key.trim() === '') {
|
|
||||||
return; // Silently skip if no key available
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic import to avoid hard dependency
|
|
||||||
const Anthropic = require('@anthropic-ai/sdk');
|
|
||||||
this.client = new Anthropic({ apiKey: key });
|
|
||||||
logger.info(`[AI Service] ✅ Claude provider initialized with model: ${this.model}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle missing package gracefully
|
|
||||||
if (error.code === 'MODULE_NOT_FOUND') {
|
|
||||||
logger.warn('[AI Service] Claude SDK not installed. Run: npm install @anthropic-ai/sdk');
|
|
||||||
} else {
|
|
||||||
logger.error('[AI Service] Failed to initialize Claude:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateText(prompt: string): Promise<string> {
|
|
||||||
if (!this.client) throw new Error('Claude client not initialized');
|
|
||||||
|
|
||||||
logAIEvent('request', { provider: 'claude', model: this.model });
|
|
||||||
|
|
||||||
const response = await this.client.messages.create({
|
|
||||||
model: this.model,
|
|
||||||
max_tokens: 2048, // Increased for longer conclusions
|
|
||||||
temperature: 0.3,
|
|
||||||
messages: [{ role: 'user', content: prompt }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = response.content[0];
|
|
||||||
return content.type === 'text' ? content.text : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
isAvailable(): boolean {
|
|
||||||
return this.client !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getProviderName(): string {
|
|
||||||
return 'Claude (Anthropic)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAI Provider
|
|
||||||
class OpenAIProvider implements AIProvider {
|
|
||||||
private client: any = null;
|
|
||||||
private model: string;
|
|
||||||
|
|
||||||
constructor(apiKey?: string, model?: string) {
|
|
||||||
// Allow model override via parameter, environment variable, or default
|
|
||||||
// Current models (November 2025):
|
|
||||||
// - gpt-4o (default - latest GPT-4 Optimized)
|
|
||||||
// Priority: 1. Provided model parameter, 2. Environment variable, 3. Default
|
|
||||||
this.model = model || process.env.OPENAI_MODEL || 'gpt-4o';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Priority: 1. Provided key, 2. Environment variable
|
|
||||||
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
if (!key || key.trim() === '') {
|
|
||||||
return; // Silently skip if no key available
|
|
||||||
}
|
|
||||||
|
|
||||||
const OpenAI = require('openai');
|
|
||||||
this.client = new OpenAI({ apiKey: key });
|
|
||||||
logger.info(`[AI Service] ✅ OpenAI provider initialized with model: ${this.model}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle missing package gracefully
|
|
||||||
if (error.code === 'MODULE_NOT_FOUND') {
|
|
||||||
logger.warn('[AI Service] OpenAI SDK not installed. Run: npm install openai');
|
|
||||||
} else {
|
|
||||||
logger.error('[AI Service] Failed to initialize OpenAI:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateText(prompt: string): Promise<string> {
|
|
||||||
if (!this.client) throw new Error('OpenAI client not initialized');
|
|
||||||
|
|
||||||
logAIEvent('request', { provider: 'openai', model: this.model });
|
|
||||||
|
|
||||||
const response = await this.client.chat.completions.create({
|
|
||||||
model: this.model,
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
max_tokens: 1024,
|
|
||||||
temperature: 0.3
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.choices[0]?.message?.content || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
isAvailable(): boolean {
|
|
||||||
return this.client !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getProviderName(): string {
|
|
||||||
return 'OpenAI (GPT-4)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gemini Provider (Google)
|
|
||||||
class GeminiProvider implements AIProvider {
|
|
||||||
private client: any = null;
|
|
||||||
private model: string;
|
|
||||||
|
|
||||||
constructor(apiKey?: string, model?: string) {
|
|
||||||
// Allow model override via parameter, environment variable, or default
|
|
||||||
// Current models (November 2025):
|
|
||||||
// - gemini-2.0-flash-lite (default - latest Gemini Flash Lite)
|
|
||||||
// Priority: 1. Provided model parameter, 2. Environment variable, 3. Default
|
|
||||||
this.model = model || process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Priority: 1. Provided key, 2. Environment variable
|
|
||||||
const key = apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY;
|
|
||||||
|
|
||||||
if (!key || key.trim() === '') {
|
|
||||||
return; // Silently skip if no key available
|
|
||||||
}
|
|
||||||
|
|
||||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
||||||
this.client = new GoogleGenerativeAI(key);
|
|
||||||
logger.info(`[AI Service] ✅ Gemini provider initialized with model: ${this.model}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle missing package gracefully
|
|
||||||
if (error.code === 'MODULE_NOT_FOUND') {
|
|
||||||
logger.warn('[AI Service] Gemini SDK not installed. Run: npm install @google/generative-ai');
|
|
||||||
} else {
|
|
||||||
logger.error('[AI Service] Failed to initialize Gemini:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateText(prompt: string): Promise<string> {
|
|
||||||
if (!this.client) throw new Error('Gemini client not initialized');
|
|
||||||
|
|
||||||
logAIEvent('request', { provider: 'gemini', model: this.model });
|
|
||||||
|
|
||||||
const model = this.client.getGenerativeModel({ model: this.model });
|
|
||||||
const result = await model.generateContent(prompt);
|
|
||||||
const response = await result.response;
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
isAvailable(): boolean {
|
|
||||||
return this.client !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getProviderName(): string {
|
|
||||||
return 'Gemini (Google)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AIService {
|
class AIService {
|
||||||
private provider: AIProvider | null = null;
|
private vertexAI: VertexAI | null = null;
|
||||||
private providerName: string = 'None';
|
private model: string = DEFAULT_MODEL;
|
||||||
private isInitialized: boolean = false;
|
private isInitialized: boolean = false;
|
||||||
|
private providerName: string = 'Vertex AI (Gemini)';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialization happens asynchronously
|
// Initialization happens asynchronously
|
||||||
@ -188,110 +21,52 @@ class AIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize AI provider from database configuration
|
* Initialize Vertex AI client
|
||||||
*/
|
*/
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Read AI configuration from database (with env fallback)
|
// Check if AI is enabled from config
|
||||||
const config = await getAIProviderConfig();
|
const { getConfigBoolean } = require('./configReader.service');
|
||||||
|
const enabled = await getConfigBoolean('AI_ENABLED', true);
|
||||||
|
|
||||||
if (!config.enabled) {
|
if (!enabled) {
|
||||||
logger.warn('[AI Service] AI features disabled in admin configuration');
|
logger.warn('[AI Service] AI features disabled in admin configuration');
|
||||||
|
this.isInitialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredProvider = config.provider.toLowerCase();
|
// Get model and location from environment variables only
|
||||||
logger.info(`[AI Service] Preferred provider from config: ${preferredProvider}`);
|
this.model = process.env.VERTEX_AI_MODEL || DEFAULT_MODEL;
|
||||||
|
const location = process.env.VERTEX_AI_LOCATION || LOCATION;
|
||||||
|
|
||||||
// Try to initialize the preferred provider first
|
logger.info(`[AI Service] Initializing Vertex AI with project: ${PROJECT_ID}, location: ${location}, model: ${this.model}`);
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
switch (preferredProvider) {
|
// Initialize Vertex AI client with service account credentials
|
||||||
case 'openai':
|
this.vertexAI = new VertexAI({
|
||||||
case 'gpt':
|
project: PROJECT_ID,
|
||||||
initialized = this.tryProvider(new OpenAIProvider(config.openaiKey, config.openaiModel));
|
location: location,
|
||||||
break;
|
googleAuthOptions: {
|
||||||
case 'gemini':
|
keyFilename: KEY_FILE_PATH,
|
||||||
case 'google':
|
},
|
||||||
initialized = this.tryProvider(new GeminiProvider(config.geminiKey, config.geminiModel));
|
});
|
||||||
break;
|
|
||||||
case 'claude':
|
|
||||||
case 'anthropic':
|
|
||||||
default:
|
|
||||||
initialized = this.tryProvider(new ClaudeProvider(config.claudeKey, config.claudeModel));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Try other providers if preferred one failed
|
|
||||||
if (!initialized) {
|
|
||||||
logger.warn('[AI Service] Preferred provider unavailable. Trying fallbacks...');
|
|
||||||
|
|
||||||
const fallbackProviders = [
|
|
||||||
new ClaudeProvider(config.claudeKey, config.claudeModel),
|
|
||||||
new OpenAIProvider(config.openaiKey, config.openaiModel),
|
|
||||||
new GeminiProvider(config.geminiKey, config.geminiModel)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const provider of fallbackProviders) {
|
|
||||||
if (this.tryProvider(provider)) {
|
|
||||||
logger.info(`[AI Service] ✅ Using fallback provider: ${this.providerName}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.provider) {
|
|
||||||
logger.warn('[AI Service] ⚠️ No AI provider available. AI features will be disabled.');
|
|
||||||
logger.warn('[AI Service] To enable AI: Configure API keys in admin panel or set environment variables.');
|
|
||||||
logger.warn('[AI Service] Supported providers: Claude (CLAUDE_API_KEY), OpenAI (OPENAI_API_KEY), Gemini (GEMINI_API_KEY)');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
logger.info(`[AI Service] ✅ Vertex AI provider initialized successfully with model: ${this.model}`);
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logger.error('[AI Service] Failed to initialize from config:', error);
|
logger.error('[AI Service] Failed to initialize Vertex AI:', error);
|
||||||
// Fallback to environment variables
|
|
||||||
try {
|
|
||||||
this.initializeFromEnv();
|
|
||||||
} catch (envError) {
|
|
||||||
logger.error('[AI Service] Environment fallback also failed:', envError);
|
|
||||||
this.isInitialized = true; // Mark as initialized even if failed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (error.code === 'MODULE_NOT_FOUND') {
|
||||||
* Fallback initialization from environment variables
|
logger.warn('[AI Service] @google-cloud/vertexai package not installed. Run: npm install @google-cloud/vertexai');
|
||||||
*/
|
} else if (error.message?.includes('ENOENT') || error.message?.includes('not found')) {
|
||||||
private initializeFromEnv(): void {
|
logger.error(`[AI Service] Service account key file not found at: ${KEY_FILE_PATH}`);
|
||||||
try {
|
logger.error('[AI Service] Please ensure the credentials file exists and GCP_KEY_FILE path is correct');
|
||||||
const preferredProvider = (process.env.AI_PROVIDER || 'claude').toLowerCase();
|
} else if (error.message?.includes('Could not load the default credentials')) {
|
||||||
|
logger.error('[AI Service] Failed to load service account credentials. Please verify the key file is valid.');
|
||||||
logger.info(`[AI Service] Using environment variable configuration`);
|
} else {
|
||||||
|
logger.error(`[AI Service] Initialization error: ${error.message}`);
|
||||||
switch (preferredProvider) {
|
|
||||||
case 'openai':
|
|
||||||
case 'gpt':
|
|
||||||
this.tryProvider(new OpenAIProvider(undefined, process.env.OPENAI_MODEL));
|
|
||||||
break;
|
|
||||||
case 'gemini':
|
|
||||||
case 'google':
|
|
||||||
this.tryProvider(new GeminiProvider(undefined, process.env.GEMINI_MODEL));
|
|
||||||
break;
|
|
||||||
case 'claude':
|
|
||||||
case 'anthropic':
|
|
||||||
default:
|
|
||||||
this.tryProvider(new ClaudeProvider(undefined, process.env.CLAUDE_MODEL));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.provider) {
|
this.isInitialized = true; // Mark as initialized even if failed to prevent infinite loops
|
||||||
logger.warn('[AI Service] ⚠️ No provider available from environment variables either.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[AI Service] Environment initialization failed:', error);
|
|
||||||
this.isInitialized = true; // Still mark as initialized to prevent infinite loops
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,23 +74,12 @@ class AIService {
|
|||||||
* Reinitialize AI provider (call after admin updates config)
|
* Reinitialize AI provider (call after admin updates config)
|
||||||
*/
|
*/
|
||||||
async reinitialize(): Promise<void> {
|
async reinitialize(): Promise<void> {
|
||||||
logger.info('[AI Service] Reinitializing AI provider from updated configuration...');
|
logger.info('[AI Service] Reinitializing Vertex AI provider from updated configuration...');
|
||||||
this.provider = null;
|
this.vertexAI = null;
|
||||||
this.providerName = 'None';
|
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryProvider(provider: AIProvider): boolean {
|
|
||||||
if (provider.isAvailable()) {
|
|
||||||
this.provider = provider;
|
|
||||||
this.providerName = provider.getProviderName();
|
|
||||||
logger.info(`[AI Service] ✅ Active provider: ${this.providerName}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current AI provider name
|
* Get current AI provider name
|
||||||
*/
|
*/
|
||||||
@ -323,6 +87,58 @@ class AIService {
|
|||||||
return this.providerName;
|
return this.providerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text using Vertex AI Gemini
|
||||||
|
*/
|
||||||
|
private async generateText(prompt: string): Promise<string> {
|
||||||
|
if (!this.vertexAI) {
|
||||||
|
throw new Error('Vertex AI client not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
logAIEvent('request', { provider: 'vertex-ai', model: this.model });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the generative model
|
||||||
|
const generativeModel = this.vertexAI.getGenerativeModel({
|
||||||
|
model: this.model,
|
||||||
|
generationConfig: {
|
||||||
|
maxOutputTokens: 2048,
|
||||||
|
temperature: 0.3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate content
|
||||||
|
const request = {
|
||||||
|
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamingResp = await generativeModel.generateContent(request);
|
||||||
|
const response = streamingResp.response;
|
||||||
|
|
||||||
|
// Extract text from response
|
||||||
|
const text = response.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('Empty response from Vertex AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('[AI Service] Vertex AI generation error:', error);
|
||||||
|
|
||||||
|
// Provide more specific error messages
|
||||||
|
if (error.message?.includes('Model was not found')) {
|
||||||
|
throw new Error(`Model ${this.model} not found or not available in region ${LOCATION}. Please check model name and region.`);
|
||||||
|
} else if (error.message?.includes('Permission denied')) {
|
||||||
|
throw new Error('Permission denied. Please verify service account has Vertex AI User role.');
|
||||||
|
} else if (error.message?.includes('API not enabled')) {
|
||||||
|
throw new Error('Vertex AI API is not enabled. Please enable it in Google Cloud Console.');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Vertex AI generation failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate conclusion remark for a workflow request
|
* Generate conclusion remark for a workflow request
|
||||||
* @param context - All relevant data for generating the conclusion
|
* @param context - All relevant data for generating the conclusion
|
||||||
@ -358,29 +174,28 @@ class AIService {
|
|||||||
details: string;
|
details: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}>;
|
}>;
|
||||||
}): Promise<{ remark: string; confidence: number; keyPoints: string[]; provider: string }> {
|
}): Promise<{ remark: string; confidence: number; keyPoints: string[]; provider: string }> {
|
||||||
// Ensure initialization is complete
|
// Ensure initialization is complete
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
logger.warn('[AI Service] Not yet initialized, attempting initialization...');
|
logger.warn('[AI Service] Not yet initialized, attempting initialization...');
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.provider) {
|
if (!this.vertexAI) {
|
||||||
logger.error('[AI Service] No AI provider available');
|
logger.error('[AI Service] Vertex AI not available');
|
||||||
throw new Error('AI features are currently unavailable. Please configure an AI provider (Claude, OpenAI, or Gemini) in the admin panel, or write the conclusion manually.');
|
throw new Error('AI features are currently unavailable. Please verify Vertex AI configuration and service account credentials.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build context prompt with max length from config
|
// Build context prompt with max length from config
|
||||||
const prompt = await this.buildConclusionPrompt(context);
|
const prompt = await this.buildConclusionPrompt(context);
|
||||||
|
|
||||||
logger.info(`[AI Service] Generating conclusion for request ${context.requestNumber} using ${this.providerName}...`);
|
logger.info(`[AI Service] Generating conclusion for request ${context.requestNumber} using ${this.providerName} (${this.model})...`);
|
||||||
|
|
||||||
// Use provider's generateText method
|
// Use Vertex AI to generate text
|
||||||
let remarkText = await this.provider.generateText(prompt);
|
let remarkText = await this.generateText(prompt);
|
||||||
|
|
||||||
// Get max length from config for logging
|
// Get max length from config for logging
|
||||||
const { getConfigValue } = require('./configReader.service');
|
|
||||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||||
|
|
||||||
@ -410,7 +225,7 @@ class AIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the prompt for Claude to generate a professional conclusion remark
|
* Build the prompt for Vertex AI to generate a professional conclusion remark
|
||||||
*/
|
*/
|
||||||
private async buildConclusionPrompt(context: any): Promise<string> {
|
private async buildConclusionPrompt(context: any): Promise<string> {
|
||||||
const {
|
const {
|
||||||
@ -427,7 +242,6 @@ class AIService {
|
|||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
// Get max remark length from admin configuration
|
// Get max remark length from admin configuration
|
||||||
const { getConfigValue } = require('./configReader.service');
|
|
||||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||||
const targetWordCount = Math.floor(maxLength / 6); // Approximate words (avg 6 chars per word)
|
const targetWordCount = Math.floor(maxLength / 6); // Approximate words (avg 6 chars per word)
|
||||||
@ -600,7 +414,7 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
* Calculate confidence score based on response quality
|
* Calculate confidence score based on response quality
|
||||||
*/
|
*/
|
||||||
private calculateConfidence(remark: string, context: any): number {
|
private calculateConfidence(remark: string, context: any): number {
|
||||||
let score = 0.6; // Base score (slightly higher for new prompt)
|
let score = 0.6; // Base score
|
||||||
|
|
||||||
// Check if remark has good length (100-400 chars - more realistic)
|
// Check if remark has good length (100-400 chars - more realistic)
|
||||||
if (remark.length >= 100 && remark.length <= 400) {
|
if (remark.length >= 100 && remark.length <= 400) {
|
||||||
@ -624,9 +438,8 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
* Check if AI service is available
|
* Check if AI service is available
|
||||||
*/
|
*/
|
||||||
isAvailable(): boolean {
|
isAvailable(): boolean {
|
||||||
return this.provider !== null;
|
return this.vertexAI !== null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aiService = new AIService();
|
export const aiService = new AIService();
|
||||||
|
|
||||||
|
|||||||
@ -148,28 +148,13 @@ export async function preloadConfigurations(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get AI provider configurations
|
* Get Vertex AI configurations
|
||||||
*/
|
*/
|
||||||
export async function getAIProviderConfig(): Promise<{
|
export async function getVertexAIConfig(): Promise<{
|
||||||
provider: string;
|
|
||||||
claudeKey: string;
|
|
||||||
openaiKey: string;
|
|
||||||
geminiKey: string;
|
|
||||||
claudeModel: string;
|
|
||||||
openaiModel: string;
|
|
||||||
geminiModel: string;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}> {
|
}> {
|
||||||
const provider = await getConfigValue('AI_PROVIDER', 'claude');
|
|
||||||
const claudeKey = await getConfigValue('CLAUDE_API_KEY', '');
|
|
||||||
const openaiKey = await getConfigValue('OPENAI_API_KEY', '');
|
|
||||||
const geminiKey = await getConfigValue('GEMINI_API_KEY', '');
|
|
||||||
// Get models from database config, fallback to env, then to defaults
|
|
||||||
const claudeModel = await getConfigValue('CLAUDE_MODEL', process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514');
|
|
||||||
const openaiModel = await getConfigValue('OPENAI_MODEL', process.env.OPENAI_MODEL || 'gpt-4o');
|
|
||||||
const geminiModel = await getConfigValue('GEMINI_MODEL', process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite');
|
|
||||||
const enabled = await getConfigBoolean('AI_ENABLED', true);
|
const enabled = await getConfigBoolean('AI_ENABLED', true);
|
||||||
|
|
||||||
return { provider, claudeKey, openaiKey, geminiKey, claudeModel, openaiModel, geminiModel, enabled };
|
return { enabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -260,112 +260,7 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
|||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
),
|
),
|
||||||
-- AI Configuration
|
-- AI Configuration (Vertex AI Gemini)
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'AI_REMARK_GENERATION_ENABLED',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'true',
|
|
||||||
'BOOLEAN',
|
|
||||||
'Enable AI Remark Generation',
|
|
||||||
'Toggle AI-generated conclusion remarks for workflow closures',
|
|
||||||
'true',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'toggle',
|
|
||||||
NULL,
|
|
||||||
20,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'AI_PROVIDER',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude',
|
|
||||||
'STRING',
|
|
||||||
'AI Provider',
|
|
||||||
'Active AI provider for conclusion generation (claude, openai, or gemini)',
|
|
||||||
'claude',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
|
|
||||||
'select',
|
|
||||||
'["claude", "openai", "gemini"]'::jsonb,
|
|
||||||
22,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Claude API Key',
|
|
||||||
'API key for Claude (Anthropic) - Get from console.anthropic.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
23,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI API Key',
|
|
||||||
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
24,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Gemini API Key',
|
|
||||||
'API key for Gemini (Google) - Get from ai.google.dev',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"minLength": 20}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
25,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
'AI_ENABLED',
|
'AI_ENABLED',
|
||||||
@ -380,7 +275,7 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
|||||||
'{"type": "boolean"}'::jsonb,
|
'{"type": "boolean"}'::jsonb,
|
||||||
'toggle',
|
'toggle',
|
||||||
NULL,
|
NULL,
|
||||||
26,
|
20,
|
||||||
false,
|
false,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -389,61 +284,19 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
'CLAUDE_MODEL',
|
'AI_REMARK_GENERATION_ENABLED',
|
||||||
'AI_CONFIGURATION',
|
'AI_CONFIGURATION',
|
||||||
'claude-sonnet-4-20250514',
|
'true',
|
||||||
'STRING',
|
'BOOLEAN',
|
||||||
'Claude Model',
|
'Enable AI Remark Generation',
|
||||||
'Claude (Anthropic) model to use for AI generation',
|
'Toggle AI-generated conclusion remarks for workflow closures',
|
||||||
'claude-sonnet-4-20250514',
|
'true',
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
'{}'::jsonb,
|
'{}'::jsonb,
|
||||||
'input',
|
'toggle',
|
||||||
NULL,
|
NULL,
|
||||||
27,
|
21,
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gpt-4o',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI Model',
|
|
||||||
'OpenAI model to use for AI generation',
|
|
||||||
'gpt-4o',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
28,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
'STRING',
|
|
||||||
'Gemini Model',
|
|
||||||
'Gemini (Google) model to use for AI generation',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
29,
|
|
||||||
false,
|
false,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -464,7 +317,7 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
|||||||
'{"min": 500, "max": 5000}'::jsonb,
|
'{"min": 500, "max": 5000}'::jsonb,
|
||||||
'number',
|
'number',
|
||||||
NULL,
|
NULL,
|
||||||
30,
|
24,
|
||||||
false,
|
false,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user