vertex api implemented removed oldl api setup
This commit is contained in:
parent
c269783229
commit
d432627c61
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-Ctya9TRf.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BaYtMwB9.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-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-D1TurBfA.js.map
|
//# sourceMappingURL=conclusionApi-uNxtglEr.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-D1TurBfA.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-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"}
|
||||||
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
@ -52,11 +52,11 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-Ctya9TRf.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-BaYtMwB9.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">
|
||||||
|
|||||||
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",
|
||||||
|
|||||||
@ -20,9 +20,8 @@
|
|||||||
"seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts"
|
"seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.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 });
|
||||||
|
}
|
||||||
|
|
||||||
@ -118,7 +118,8 @@ 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');
|
const m28 = require('../migrations/20250130-migrate-to-vertex-ai');
|
||||||
|
const m29 = require('../migrations/20251203-add-user-notification-preferences');
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
@ -149,7 +150,8 @@ 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 },
|
{ name: '20250130-migrate-to-vertex-ai', module: m28 },
|
||||||
|
{ name: '20251203-add-user-notification-preferences', module: m29 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
|
|||||||
@ -22,6 +22,14 @@ 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/20250123-update-request-number-format';
|
||||||
|
import * as m23 from '../migrations/20250126-add-paused-to-enum';
|
||||||
|
import * as m24 from '../migrations/20250126-add-paused-to-workflow-status-enum';
|
||||||
|
import * as m25 from '../migrations/20250126-add-pause-fields-to-workflow-requests';
|
||||||
|
import * as m26 from '../migrations/20250126-add-pause-fields-to-approval-levels';
|
||||||
|
import * as m27 from '../migrations/20250127-migrate-in-progress-to-pending';
|
||||||
|
import * as m28 from '../migrations/20250130-migrate-to-vertex-ai';
|
||||||
|
import * as m29 from '../migrations/20251203-add-user-notification-preferences';
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -58,6 +66,14 @@ 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: '20250123-update-request-number-format', module: m22 },
|
||||||
|
{ name: '20250126-add-paused-to-enum', module: m23 },
|
||||||
|
{ name: '20250126-add-paused-to-workflow-status-enum', module: m24 },
|
||||||
|
{ name: '20250126-add-pause-fields-to-workflow-requests', module: m25 },
|
||||||
|
{ name: '20250126-add-pause-fields-to-approval-levels', module: m26 },
|
||||||
|
{ name: '20250127-migrate-in-progress-to-pending', module: m27 },
|
||||||
|
{ name: '20250130-migrate-to-vertex-ai', module: m28 },
|
||||||
|
{ name: '20251203-add-user-notification-preferences', module: m29 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
switch (preferredProvider) {
|
|
||||||
case 'openai':
|
|
||||||
case 'gpt':
|
|
||||||
initialized = this.tryProvider(new OpenAIProvider(config.openaiKey, config.openaiModel));
|
|
||||||
break;
|
|
||||||
case 'gemini':
|
|
||||||
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)');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[AI Service] Failed to initialize from config:', 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
logger.info(`[AI Service] Initializing Vertex AI with project: ${PROJECT_ID}, location: ${location}, model: ${this.model}`);
|
||||||
* Fallback initialization from environment variables
|
|
||||||
*/
|
// Initialize Vertex AI client with service account credentials
|
||||||
private initializeFromEnv(): void {
|
this.vertexAI = new VertexAI({
|
||||||
try {
|
project: PROJECT_ID,
|
||||||
const preferredProvider = (process.env.AI_PROVIDER || 'claude').toLowerCase();
|
location: location,
|
||||||
|
googleAuthOptions: {
|
||||||
logger.info(`[AI Service] Using environment variable configuration`);
|
keyFilename: KEY_FILE_PATH,
|
||||||
|
},
|
||||||
switch (preferredProvider) {
|
});
|
||||||
case 'openai':
|
|
||||||
case 'gpt':
|
logger.info(`[AI Service] ✅ Vertex AI provider initialized successfully with model: ${this.model}`);
|
||||||
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) {
|
|
||||||
logger.warn('[AI Service] ⚠️ No provider available from environment variables either.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logger.error('[AI Service] Environment initialization failed:', error);
|
logger.error('[AI Service] Failed to initialize Vertex AI:', error);
|
||||||
this.isInitialized = true; // Still mark as initialized to prevent infinite loops
|
|
||||||
|
if (error.code === 'MODULE_NOT_FOUND') {
|
||||||
|
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')) {
|
||||||
|
logger.error(`[AI Service] Service account key file not found at: ${KEY_FILE_PATH}`);
|
||||||
|
logger.error('[AI Service] Please ensure the credentials file exists and GCP_KEY_FILE path is correct');
|
||||||
|
} 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.');
|
||||||
|
} else {
|
||||||
|
logger.error(`[AI Service] Initialization error: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInitialized = true; // Mark as initialized even if failed 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