added email reinitaialize and api token limit
This commit is contained in:
parent
9285c97d4b
commit
d1ae0ffaec
@ -1,2 +1,2 @@
|
||||
import{a as t}from"./index-XMUlTorM.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-muWiQD3D.js.map
|
||||
import{a as t}from"./index-D5U31xpx.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-xBwvOJP0.js.map
|
||||
@ -1 +1 @@
|
||||
{"version":3,"file":"conclusionApi-muWiQD3D.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-xBwvOJP0.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-D5U31xpx.js.map
Normal file
1
build/assets/index-D5U31xpx.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,7 +52,7 @@
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-XMUlTorM.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D5U31xpx.js"></script>
|
||||
<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/utils-vendor-DHm03ykU.js">
|
||||
|
||||
@ -10,6 +10,7 @@ import { seedDefaultConfigurations } from './services/configSeed.service';
|
||||
import { startPauseResumeJob } from './jobs/pauseResumeJob';
|
||||
import './queues/pauseResumeWorker'; // Initialize pause resume worker
|
||||
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
|
||||
import { emailService } from './services/email.service';
|
||||
|
||||
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
||||
|
||||
@ -20,6 +21,15 @@ const startServer = async (): Promise<void> => {
|
||||
// This will merge secrets from GCS into process.env if enabled
|
||||
await initializeSecrets();
|
||||
|
||||
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded)
|
||||
// This ensures the email service uses production SMTP if credentials are available
|
||||
try {
|
||||
await emailService.initialize();
|
||||
console.log('📧 Email service re-initialized after secrets loaded');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', error);
|
||||
}
|
||||
|
||||
const server = http.createServer(app);
|
||||
initSocket(server);
|
||||
|
||||
|
||||
@ -99,10 +99,11 @@ class AIService {
|
||||
|
||||
try {
|
||||
// Get the generative model
|
||||
// Increase maxOutputTokens to handle longer conclusions (up to ~4000 tokens ≈ 3000 words)
|
||||
const generativeModel = this.vertexAI.getGenerativeModel({
|
||||
model: this.model,
|
||||
generationConfig: {
|
||||
maxOutputTokens: 2048,
|
||||
maxOutputTokens: 4096, // Increased from 2048 to handle longer conclusions
|
||||
temperature: 0.3,
|
||||
},
|
||||
});
|
||||
@ -154,6 +155,19 @@ class AIService {
|
||||
// Extract text from response
|
||||
const text = candidate.content?.parts?.[0]?.text || '';
|
||||
|
||||
// Handle MAX_TOKENS finish reason - accept whatever response we got
|
||||
// We trust the AI's response - no truncation on our side
|
||||
if (candidate.finishReason === 'MAX_TOKENS' && text) {
|
||||
// Accept the response as-is - AI was instructed to stay within limits
|
||||
// If it hit the limit, we still use what we got (no truncation on our side)
|
||||
logger.info('[AI Service] Vertex AI response hit token limit, but content received is preserved as-is:', {
|
||||
textLength: text.length,
|
||||
finishReason: candidate.finishReason
|
||||
});
|
||||
// Return the response without any truncation - trust what AI generated
|
||||
return text;
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
// Log detailed response structure for debugging
|
||||
logger.error('[AI Service] Empty text in Vertex AI response:', {
|
||||
@ -169,7 +183,7 @@ class AIService {
|
||||
if (candidate.finishReason === 'SAFETY') {
|
||||
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
|
||||
} else if (candidate.finishReason === 'MAX_TOKENS') {
|
||||
throw new Error('Vertex AI response was truncated due to token limit.');
|
||||
throw new Error('Vertex AI response was truncated due to token limit. The prompt may be too long or the response limit was exceeded.');
|
||||
} else if (candidate.finishReason === 'RECITATION') {
|
||||
throw new Error('Vertex AI blocked the response due to recitation concerns.');
|
||||
} else {
|
||||
@ -254,9 +268,10 @@ class AIService {
|
||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||
|
||||
// Log length (no trimming - preserve complete AI-generated content)
|
||||
// Trust AI's response - do not truncate anything
|
||||
// AI is instructed to stay within limit, but we accept whatever it generates
|
||||
if (remarkText.length > maxLength) {
|
||||
logger.warn(`[AI Service] ⚠️ AI exceeded suggested limit (${remarkText.length} > ${maxLength}). Content preserved to avoid incomplete information.`);
|
||||
logger.info(`[AI Service] AI generated ${remarkText.length} characters (suggested limit: ${maxLength}). Full content preserved as-is.`);
|
||||
}
|
||||
|
||||
// Extract key points (look for bullet points or numbered items)
|
||||
@ -336,8 +351,9 @@ class AIService {
|
||||
.map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`)
|
||||
.join('\n');
|
||||
|
||||
// Summarize documents
|
||||
// Summarize documents (limit to reduce token usage)
|
||||
const documentSummary = documents
|
||||
.slice(0, 10) // Limit to first 10 documents
|
||||
.map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`)
|
||||
.join('\n');
|
||||
|
||||
@ -382,15 +398,17 @@ ${isRejected
|
||||
- Sounds natural and human-written (not AI-generated)`}
|
||||
|
||||
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
|
||||
- Your response MUST be EXACTLY within ${maxLength} characters (not words, CHARACTERS including spaces)
|
||||
- Count your characters carefully before responding
|
||||
- Your response MUST stay within ${maxLength} characters (not words, CHARACTERS including spaces including HTML tags)
|
||||
- This is a HARD LIMIT - you must count your characters and ensure your complete response fits within ${maxLength} characters
|
||||
- Count your characters carefully before responding - include all HTML tags in your count
|
||||
- If you have too much content, PRIORITIZE the most important information:
|
||||
1. Final decision (approved/rejected)
|
||||
2. Key approvers and their decisions
|
||||
3. Critical TAT breaches (if any)
|
||||
4. Brief summary of the request
|
||||
- OMIT less important details to fit within the limit rather than exceeding it
|
||||
- Better to be concise than to exceed the limit
|
||||
- Better to be concise and complete within the limit than to exceed it
|
||||
- IMPORTANT: Generate your complete response within this limit - do not generate partial content that exceeds the limit
|
||||
|
||||
**WRITING GUIDELINES:**
|
||||
- Be concise and direct - every word must add value
|
||||
|
||||
@ -100,6 +100,18 @@ export class EmailService {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// If using test account, check if SMTP credentials are now available and re-initialize
|
||||
if (this.useTestAccount) {
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPassword = process.env.SMTP_PASSWORD;
|
||||
|
||||
if (smtpHost && smtpUser && smtpPassword) {
|
||||
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
||||
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
|
||||
|
||||
@ -233,6 +245,8 @@ export class EmailService {
|
||||
export const emailService = new EmailService();
|
||||
|
||||
// Initialize on import (will use test account if SMTP not configured)
|
||||
// Note: If secrets are loaded later, the service will re-initialize automatically
|
||||
// when sendEmail is called (if SMTP credentials become available)
|
||||
emailService.initialize().catch(error => {
|
||||
logger.error('Failed to initialize email service:', error);
|
||||
});
|
||||
|
||||
@ -1831,14 +1831,16 @@ export class WorkflowService {
|
||||
// Include PAUSED status so paused requests where user is the current approver are shown
|
||||
const pendingLevels = await ApprovalLevel.findAll({
|
||||
where: {
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
ApprovalStatus.PENDING as any,
|
||||
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
|
||||
ApprovalStatus.PAUSED as any,
|
||||
'PENDING',
|
||||
'IN_PROGRESS',
|
||||
'PAUSED'
|
||||
] as any },
|
||||
] as any
|
||||
},
|
||||
},
|
||||
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
|
||||
attributes: ['requestId', 'levelNumber', 'approverId'],
|
||||
@ -1907,7 +1909,8 @@ export class WorkflowService {
|
||||
baseConditions.push({
|
||||
[Op.or]: [
|
||||
{
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
WorkflowStatus.PENDING as any,
|
||||
WorkflowStatus.APPROVED as any,
|
||||
WorkflowStatus.PAUSED as any,
|
||||
@ -1915,7 +1918,8 @@ export class WorkflowService {
|
||||
'IN_PROGRESS', // Legacy support - will be migrated to PENDING
|
||||
'APPROVED',
|
||||
'PAUSED'
|
||||
] as any }
|
||||
] as any
|
||||
}
|
||||
},
|
||||
// Also include requests with isPaused = true (even if status is PENDING)
|
||||
{
|
||||
@ -1942,10 +1946,12 @@ export class WorkflowService {
|
||||
baseConditions.push({
|
||||
[Op.and]: [
|
||||
{ status: statusUpper },
|
||||
{ [Op.or]: [
|
||||
{
|
||||
[Op.or]: [
|
||||
{ isPaused: { [Op.is]: null } },
|
||||
{ isPaused: false }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
@ -2067,12 +2073,14 @@ export class WorkflowService {
|
||||
const levelRows = await ApprovalLevel.findAll({
|
||||
where: {
|
||||
approverId: userId,
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
ApprovalStatus.APPROVED as any,
|
||||
(ApprovalStatus as any).REJECTED ?? 'REJECTED',
|
||||
'APPROVED',
|
||||
'REJECTED'
|
||||
] as any },
|
||||
] as any
|
||||
},
|
||||
},
|
||||
attributes: ['requestId'],
|
||||
});
|
||||
@ -2378,40 +2386,6 @@ export class WorkflowService {
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
|
||||
// Send notification to INITIATOR confirming submission
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
title: 'Request Submitted Successfully',
|
||||
body: `Your request "${workflowData.title}" has been submitted and is now with the first approver.`,
|
||||
requestNumber: requestNumber,
|
||||
requestId: (workflow as any).requestId,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'request_submitted',
|
||||
priority: 'MEDIUM'
|
||||
});
|
||||
|
||||
// Send notification to FIRST APPROVER for assignment
|
||||
const firstLevel = await ApprovalLevel.findOne({ where: { requestId: (workflow as any).requestId, levelNumber: 1 } });
|
||||
if (firstLevel) {
|
||||
await notificationService.sendToUsers([(firstLevel as any).approverId], {
|
||||
title: 'New Request Assigned',
|
||||
body: `${workflowData.title}`,
|
||||
requestNumber: requestNumber,
|
||||
requestId: (workflow as any).requestId,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'assignment',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
|
||||
activityService.log({
|
||||
requestId: (workflow as any).requestId,
|
||||
type: 'assignment',
|
||||
user: { userId: initiatorId, name: initiatorName },
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'Assigned to approver',
|
||||
details: `Request assigned to ${(firstLevel as any).approverName || (firstLevel as any).approverEmail || 'approver'} for review`
|
||||
});
|
||||
}
|
||||
|
||||
return workflow;
|
||||
} catch (error) {
|
||||
@ -3192,13 +3166,28 @@ export class WorkflowService {
|
||||
// Don't fail the submission if TAT scheduling fails
|
||||
}
|
||||
|
||||
// NOTE: Notifications are already sent in createWorkflow() when the workflow is created
|
||||
// We should NOT send "Request submitted" to the approver here - that's incorrect
|
||||
// The approver should only receive "New Request Assigned" notification (sent in createWorkflow)
|
||||
// The initiator receives "Request Submitted Successfully" notification (sent in createWorkflow)
|
||||
//
|
||||
// If this is a draft being submitted, notifications were already sent during creation,
|
||||
// so we don't need to send them again here to avoid duplicates
|
||||
// Send "Request Submitted" notification to INITIATOR
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
title: 'Request Submitted Successfully',
|
||||
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
||||
requestNumber: (updated as any).requestNumber,
|
||||
requestId: (updated as any).requestId,
|
||||
url: `/request/${(updated as any).requestNumber}`,
|
||||
type: 'request_submitted',
|
||||
priority: 'MEDIUM'
|
||||
});
|
||||
|
||||
// Send "New Request Assigned" notification to FIRST APPROVER
|
||||
await notificationService.sendToUsers([(current as any).approverId], {
|
||||
title: 'New Request Assigned',
|
||||
body: `${workflowTitle}`,
|
||||
requestNumber: (updated as any).requestNumber,
|
||||
requestId: (updated as any).requestId,
|
||||
url: `/request/${(updated as any).requestNumber}`,
|
||||
type: 'assignment',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
}
|
||||
return updated;
|
||||
} catch (error) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user