Ver código fonte

added some new capabitlies

Dr-Swopt 1 semana atrás
pai
commit
12136bef3f

+ 4 - 0
src/FFB/services/config/agent-state.ts

@@ -28,5 +28,9 @@ export const AgentState = Annotation.Root({
     socketId: Annotation<string>({
         reducer: (x, y) => y ?? x,
         default: () => "default",
+    }),
+    language: Annotation<string>({
+        reducer: (x, y) => y ?? x,
+        default: () => "English",
     })
 });

+ 13 - 4
src/FFB/services/config/langchain-config.ts

@@ -3,6 +3,7 @@ import { z } from "zod";
 export const SCHEMAS = {
     ENTRY: z.object({
         category: z.enum(['InScope-Actionable', 'InScope-NeedsGuidance', 'InScope-Meta', 'OutOfScope']),
+        language: z.string().describe("The language of the user input (e.g., English, Spanish, Indonesian)"),
         reasoning: z.string()
     }),
     ROUTER: z.object({
@@ -20,14 +21,15 @@ export const SCHEMAS = {
             endDate: z.string().nullable(),
         }),
         aggregationType: z.enum(["sum", "avg", "count", "list", "count_distinct"]),
-        fieldToAggregate: z.enum(["quantity", "weight", "site", "block", "phase"])
+        fieldToAggregate: z.enum(["quantity", "weight", "site", "block", "phase"]),
+        groupBy: z.enum(["site", "block", "phase"]).nullable().describe("Field to group by (e.g., 'per site')")
     })
 };
 
 export const PROMPTS = {
     ENTRY: (lastMessage: string) => `
 You are the Entry Node for a strictly defined FFB Production Data Agent.
-Your job is to CLASSIFY the user's message into one of four categories.
+Your job is to CLASSIFY the user's message into one of four categories AND DETECT the language.
 
 IDENTITY:
 - You retrieve, summarize, and aggregate records from a vector database.
@@ -91,11 +93,18 @@ ${context}
     REFUSAL: `I'm sorry, but I can only assist with FFB production data querying and aggregation.\n\n` +
         `Please ask me about production weights, site activities, or specific dates.`,
 
-    SYNTHESIS: (lastMessage: string, payload: any) => `
+    SYNTHESIS: (lastMessage: string, payload: any, language: string = 'English') => `
 User Question: "${lastMessage}"
 Data Context: ${JSON.stringify(payload)}
+Target Language: ${language}
 
 Synthesize a natural language answer based STRICTLY on the Data Context.
-Cite the source (e.g., "Based on aggregation results...").
+
+FORMATTING RULES:
+- Respond in ${language}.
+- If the result is a LIST, use bullet points.
+- If the result is a COUNT, say "There are X [entities]." (e.g., "There are 5 sites.").
+- If the result is a SUM/AVG, say "The total/average [field] for [context] is X [unit]."
+- Cite the source (e.g., "Based on aggregation results...").
 `
 };

+ 70 - 18
src/FFB/services/nodes/aggregation.node.ts

@@ -13,25 +13,49 @@ export const aggregationNode = async (
 ): Promise<Partial<typeof AgentState.State>> => {
     const lastMessage = state.messages[state.messages.length - 1].content as string;
 
+    const context = state.messages.map(m => `${m._getType()}: ${m.content}`).join('\n');
+    console.log("Aggregation Context:", context);
+
     const structuredLlm = model.withStructuredOutput(SCHEMAS.AGGREGATION);
-    const params = await structuredLlm.invoke(`Extract aggregation parameters for: "${lastMessage}". Context: ${JSON.stringify(state.entityStore)}`);
+    const params = await structuredLlm.invoke(`
+    Extract aggregation parameters for: "${lastMessage}".
+    
+    Conversation History:
+    ${context}
+    
+    Entity State:
+    ${JSON.stringify(state.entityStore)}
+    
+    INSTRUCTIONS:
+    - Use the History and Entity State to resolve implicit references (e.g., "that site").
+    - If a specific site/date is mentioned in history/state and NOT overridden, use it.
+    - If user asks for "per site", "by phase", or "for each block", use the 'groupBy' field.
+    - IMPORTANT: If NO specific date or range is mentioned by the user or found in history, DEFAULT the start date to "2024-01-01" and end date to "2026-12-31".
+    `);
+
+    // Update entity store with new parameters
+    const updatedEntityStore = { ...state.entityStore };
+    if (params.matchStage.site) updatedEntityStore.site = params.matchStage.site;
+    if (params.matchStage.startDate) updatedEntityStore.startDate = params.matchStage.startDate;
+    if (params.matchStage.endDate) updatedEntityStore.endDate = params.matchStage.endDate;
 
     const pipeline: any[] = [];
     const match: any = {};
 
-    // Check for null instead of undefined
-    if (params.matchStage.site !== null) {
-        match.site = params.matchStage.site;
+    // Use params or fallback to entity store
+    const siteToUse = params.matchStage.site || updatedEntityStore.site;
+    if (siteToUse) {
+        match.site = siteToUse;
     }
 
-    if (params.matchStage.startDate !== null || params.matchStage.endDate !== null) {
+    // Date handling
+    const startDate = params.matchStage.startDate || updatedEntityStore.startDate;
+    const endDate = params.matchStage.endDate || updatedEntityStore.endDate;
+
+    if (startDate || endDate) {
         match.productionDate = {};
-        if (params.matchStage.startDate !== null) {
-            match.productionDate.$gte = new Date(params.matchStage.startDate);
-        }
-        if (params.matchStage.endDate !== null) {
-            match.productionDate.$lte = new Date(params.matchStage.endDate);
-        }
+        if (startDate) match.productionDate.$gte = new Date(startDate);
+        if (endDate) match.productionDate.$lte = new Date(endDate);
     }
 
     if (Object.keys(match).length > 0) {
@@ -48,14 +72,40 @@ export const aggregationNode = async (
             results = results.map(val => ({ [params.fieldToAggregate]: val }));
         } else {
             // Count distinct
-            const distinctValues = await vectorService.getDistinct(params.fieldToAggregate, match);
-            results = [{ totalValue: distinctValues.length }];
+            if (params.groupBy) {
+                // Remove duplicate match push (it was already done above if needed)
+
+                // Count distinct per group (e.g. how many blocks per site)
+                pipeline.push({
+                    $group: {
+                        _id: `$${params.groupBy}`,
+                        distinctValues: { $addToSet: `$${params.fieldToAggregate}` }
+                    }
+                });
+                pipeline.push({
+                    $project: {
+                        _id: 1,
+                        totalValue: { $size: "$distinctValues" }
+                    }
+                });
+                results = await vectorService.aggregate(pipeline);
+            } else {
+                const distinctValues = await vectorService.getDistinct(params.fieldToAggregate, match);
+                results = [{ totalValue: distinctValues.length }];
+            }
         }
     } else {
-        // Standard aggregation
-        const group: any = { _id: null };
-        const operator = `$${params.aggregationType}`;
-        group.totalValue = { [operator]: `$${params.fieldToAggregate}` };
+        // Standard aggregation (Sum, Avg, Count)
+        // If groupBy is null, _id is null -> one global result
+        const group: any = { _id: params.groupBy ? `$${params.groupBy}` : null };
+
+        let operator = `$${params.aggregationType}`;
+        if (params.aggregationType === 'count') {
+            group.totalValue = { $sum: 1 };
+        } else {
+            group.totalValue = { [operator]: `$${params.fieldToAggregate}` };
+        }
+
         pipeline.push({ $group: group });
         results = await vectorService.aggregate(pipeline);
     }
@@ -63,12 +113,14 @@ export const aggregationNode = async (
     let payload: ThoughtPayload = {
         node: `aggregation_node`,
         status: 'completed',
+        message: `Aggregated ${params.aggregationType} of ${params.fieldToAggregate}`,
         pipeline: pipeline,
         results: results
     }
     gateway.emitThought(state.socketId, payload);
 
     return {
-        actionPayload: { type: 'aggregate', pipeline, results }
+        entityStore: updatedEntityStore,
+        actionPayload: { type: 'aggregate', pipeline, results, params }
     };
 };

+ 2 - 1
src/FFB/services/nodes/entry.node.ts

@@ -28,6 +28,7 @@ export const entryNode = async (
 
     return {
         entryCategory: result.category,
-        socketId: state.socketId
+        socketId: state.socketId,
+        language: result.language || "English"
     };
 };

+ 15 - 1
src/FFB/services/nodes/synthesis.node.ts

@@ -12,6 +12,20 @@ export const synthesisNode = async (
     const lastMessage = state.messages[state.messages.length - 1].content as string;
     const payload = state.actionPayload;
 
+    // Detect empty or zero results to give better context
+    let isEmpty = false;
+    if (payload.results && Array.isArray(payload.results)) {
+        if (payload.results.length === 0) isEmpty = true;
+        // If it's a count operation returning 0
+        else if (payload.results.length === 1 && typeof payload.results[0].totalValue === 'number' && payload.results[0].totalValue === 0) {
+            isEmpty = true;
+        }
+    }
+
+    if (isEmpty) {
+        payload.humanContext = "System Note: The aggregation returned 0 matches. Ensure the response clearly states no data was found for the specific criteria (Site/Date/Phase).";
+    }
+
     let thoughtPayload: ThoughtPayload = {
         node: 'synthesis_node',
         status: 'processing',
@@ -20,7 +34,7 @@ export const synthesisNode = async (
     }
     gateway.emitThought(state.socketId, thoughtPayload);
 
-    const response = await model.invoke(PROMPTS.SYNTHESIS(lastMessage, payload));
+    const response = await model.invoke(PROMPTS.SYNTHESIS(lastMessage, payload, state.language));
     return {
         messages: [response]
     };