Compare commits
3 Commits
4cf7288857
...
be220bbb0c
| Author | SHA1 | Date | |
|---|---|---|---|
| be220bbb0c | |||
| d1ae0ffaec | |||
| 9285c97d4b |
@ -1,2 +1,2 @@
|
||||
import{a as t}from"./index-9cOIFSn9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-uNxtglEr.js.map
|
||||
import{a as t}from"./index-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-uNxtglEr.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||
{"version":3,"file":"conclusionApi-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
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/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
1
build/assets/index-DwXE9Ynd.css
Normal file
1
build/assets/index-DwXE9Ynd.css
Normal file
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-9cOIFSn9.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">
|
||||
@ -60,7 +60,7 @@
|
||||
<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/router-vendor-CRr9x_Jp.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BmOYs32D.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwXE9Ynd.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
56
src/controllers/workflowTemplate.controller.ts
Normal file
56
src/controllers/workflowTemplate.controller.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { WorkflowTemplate } from '../models';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
export const createTemplate = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { name, description, category, priority, estimatedTime, approvers, suggestedSLA } = req.body;
|
||||
const userId = (req as any).user?.userId;
|
||||
|
||||
const template = await WorkflowTemplate.create({
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
priority,
|
||||
estimatedTime,
|
||||
approvers,
|
||||
suggestedSLA,
|
||||
createdBy: userId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Workflow template created successfully',
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error creating workflow template:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create workflow template',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getTemplates = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const templates = await WorkflowTemplate.findAll({
|
||||
where: { isActive: true },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: templates
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching workflow templates:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch workflow templates',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
84
src/migrations/20260122-create-workflow-templates.ts
Normal file
84
src/migrations/20260122-create-workflow-templates.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { QueryInterface, DataTypes } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
// Drop table if exists to ensure schema is correct (CASCADE to remove FKs)
|
||||
await queryInterface.dropTable('workflow_templates', { cascade: true });
|
||||
|
||||
// Create Enum for Template Priority
|
||||
await queryInterface.sequelize.query(`DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_template_priority') THEN
|
||||
CREATE TYPE enum_template_priority AS ENUM ('low', 'medium', 'high');
|
||||
END IF;
|
||||
END$$;`);
|
||||
|
||||
await queryInterface.createTable('workflow_templates', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'General'
|
||||
},
|
||||
priority: {
|
||||
type: 'enum_template_priority',
|
||||
defaultValue: 'medium'
|
||||
},
|
||||
estimated_time: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'Variable'
|
||||
},
|
||||
approvers: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
suggested_sla: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 24
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'user_id'
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
});
|
||||
|
||||
// Add index on created_by
|
||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "workflow_templates_created_by" ON "workflow_templates" ("created_by");');
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.dropTable('workflow_templates');
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_template_priority;');
|
||||
}
|
||||
99
src/models/WorkflowTemplate.ts
Normal file
99
src/models/WorkflowTemplate.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '@config/database';
|
||||
import { User } from './User';
|
||||
|
||||
export class WorkflowTemplate extends Model {
|
||||
public id!: string;
|
||||
public name!: string;
|
||||
public description!: string;
|
||||
public category!: string;
|
||||
public priority!: 'low' | 'medium' | 'high';
|
||||
public estimatedTime!: string;
|
||||
public approvers!: any[];
|
||||
public suggestedSLA!: number;
|
||||
public isActive!: boolean;
|
||||
public createdBy!: string;
|
||||
public fields!: any;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
}
|
||||
|
||||
WorkflowTemplate.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'General'
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.ENUM('low', 'medium', 'high'),
|
||||
defaultValue: 'medium'
|
||||
},
|
||||
estimatedTime: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'Variable',
|
||||
field: 'estimated_time'
|
||||
},
|
||||
approvers: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
suggestedSLA: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 24,
|
||||
comment: 'In hours',
|
||||
field: 'suggested_sla'
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
field: 'is_active'
|
||||
},
|
||||
createdBy: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
field: 'created_by',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'user_id'
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'updated_at'
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: 'workflow_templates',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
);
|
||||
@ -16,6 +16,7 @@ import { Notification } from './Notification';
|
||||
import ConclusionRemark from './ConclusionRemark';
|
||||
import RequestSummary from './RequestSummary';
|
||||
import SharedSummary from './SharedSummary';
|
||||
import { WorkflowTemplate } from './WorkflowTemplate';
|
||||
|
||||
// Define associations
|
||||
const defineAssociations = () => {
|
||||
@ -138,7 +139,8 @@ export {
|
||||
Notification,
|
||||
ConclusionRemark,
|
||||
RequestSummary,
|
||||
SharedSummary
|
||||
SharedSummary,
|
||||
WorkflowTemplate
|
||||
};
|
||||
|
||||
// Export default sequelize instance
|
||||
|
||||
@ -13,6 +13,7 @@ import dashboardRoutes from './dashboard.routes';
|
||||
import notificationRoutes from './notification.routes';
|
||||
import conclusionRoutes from './conclusion.routes';
|
||||
import aiRoutes from './ai.routes';
|
||||
import workflowTemplateRoutes from './workflowTemplate.routes';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -40,6 +41,7 @@ router.use('/notifications', notificationRoutes);
|
||||
router.use('/conclusions', conclusionRoutes);
|
||||
router.use('/ai', aiRoutes);
|
||||
router.use('/summaries', summaryRoutes);
|
||||
router.use('/templates', workflowTemplateRoutes);
|
||||
|
||||
// TODO: Add other route modules as they are implemented
|
||||
// router.use('/approvals', approvalRoutes);
|
||||
|
||||
14
src/routes/workflowTemplate.routes.ts
Normal file
14
src/routes/workflowTemplate.routes.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { createTemplate, getTemplates } from '../controllers/workflowTemplate.controller';
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Public route to get templates (authenticated users)
|
||||
router.get('/', authenticateToken, getTemplates);
|
||||
|
||||
// Admin only route to create templates
|
||||
router.post('/', authenticateToken, requireAdmin, createTemplate);
|
||||
|
||||
export default router;
|
||||
@ -120,6 +120,7 @@ async function runMigrations(): Promise<void> {
|
||||
const m27 = require('../migrations/20250127-migrate-in-progress-to-pending');
|
||||
const m28 = require('../migrations/20250130-migrate-to-vertex-ai');
|
||||
const m29 = require('../migrations/20251203-add-user-notification-preferences');
|
||||
const m30 = require('../migrations/20260122-create-workflow-templates');
|
||||
|
||||
const migrations = [
|
||||
{ name: '2025103000-create-users', module: m0 },
|
||||
@ -152,6 +153,7 @@ async function runMigrations(): Promise<void> {
|
||||
{ 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 },
|
||||
{ name: '20260122-create-workflow-templates', module: m30 },
|
||||
];
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -101,7 +101,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRequestCreatedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Created Successfully`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Created Successfully`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -170,7 +170,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getMultiApproverRequestEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Multi-Level Approval Request - Your Turn`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Multi-Level Approval Request - Your Turn`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -198,7 +198,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApprovalRequestEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Approval Request - Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Approval Request - Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -252,7 +252,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApprovalConfirmationEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -303,7 +303,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRejectionNotificationEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Rejected`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -379,7 +379,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getTATReminderEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -449,7 +449,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getTATBreachedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] TAT BREACHED - Immediate Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT BREACHED - Immediate Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -518,7 +518,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowResumedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Resumed - Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed - Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -587,7 +587,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowResumedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Resumed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -665,7 +665,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRequestClosedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Closed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Closed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: recipientData.email,
|
||||
@ -734,7 +734,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApproverSkippedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Approver Skipped`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Approver Skipped`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: skippedApproverData.email,
|
||||
@ -794,7 +794,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowPausedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Paused`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Paused`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: recipientData.email,
|
||||
|
||||
@ -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) {
|
||||
@ -2549,7 +2523,7 @@ export class WorkflowService {
|
||||
|
||||
// Reload with associations
|
||||
const workflow = await WorkflowRequest.findByPk(actualRequestId, {
|
||||
include: [ { association: 'initiator' } ]
|
||||
include: [{ association: 'initiator' }]
|
||||
});
|
||||
if (!workflow) return null;
|
||||
|
||||
@ -2645,7 +2619,7 @@ export class WorkflowService {
|
||||
// Use the actual UUID requestId for all queries
|
||||
const approvals = await ApprovalLevel.findAll({
|
||||
where: { requestId: actualRequestId },
|
||||
order: [['levelNumber','ASC']]
|
||||
order: [['levelNumber', 'ASC']]
|
||||
}) as any[];
|
||||
|
||||
const participants = await Participant.findAll({
|
||||
@ -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