Преглед изворни кода

feat: Implement FFB production and site management with RAG capabilities using LangChain and vector embeddings.

Dr-Swopt пре 2 недеља
родитељ
комит
9ad0e884db

+ 7 - 0
src/FFB/ffb-production.controller.ts

@@ -74,4 +74,11 @@ export class FFBProductionController {
     return { remark };
     return { remark };
   }
   }
 
 
+  @Post('generate-issues')
+  async generateIssues(@Body() body: any) {
+    console.log(`POST /ffb-production/generate-issues`);
+    const issues = await this.ffbService.generateIssues(body);
+    return { issues };
+  }
+
 }
 }

+ 1 - 0
src/FFB/ffb-production.schema.ts

@@ -18,6 +18,7 @@ export interface FFBProduction {
   quantity: number;
   quantity: number;
   quantityUom: string;
   quantityUom: string;
   remarks: string;
   remarks: string;
+  issues?: string;
 }
 }
 
 
 export interface ThoughtPayload {
 export interface ThoughtPayload {

+ 1 - 0
src/FFB/mongo-ffb-production.repository.ts

@@ -99,6 +99,7 @@ export class FFBProductionRepository {
             weight: 1,
             weight: 1,
             weightUom: 1,
             weightUom: 1,
             remarks: 1,
             remarks: 1,
+            issues: 1,
             score: { "$meta": "vectorSearchScore" }  // correctly get the score
             score: { "$meta": "vectorSearchScore" }  // correctly get the score
           }
           }
         }
         }

+ 1 - 0
src/FFB/services/ffb-langchain.service.ts

@@ -30,6 +30,7 @@ export class FFBLangChainService {
     private sessionManager: SessionManager;
     private sessionManager: SessionManager;
 
 
     constructor(
     constructor(
+        @Inject(forwardRef(() => FFBVectorService))
         private readonly vectorService: FFBVectorService,
         private readonly vectorService: FFBVectorService,
         @Inject(forwardRef(() => FFBGateway))
         @Inject(forwardRef(() => FFBGateway))
         private readonly gateway: FFBGateway
         private readonly gateway: FFBGateway

+ 69 - 14
src/FFB/services/ffb-production.service.ts

@@ -148,20 +148,11 @@ ${locationContext}
 Choose 2-3 specific aspects to focus on. Do not cover everything. Keep it grounded, practical, and observational.
 Choose 2-3 specific aspects to focus on. Do not cover everything. Keep it grounded, practical, and observational.
 
 
 You may choose from (but are not limited to) the following aspects:
 You may choose from (but are not limited to) the following aspects:
-- Harvest conditions (e.g., slippery laterite paths, loose fronds, uneven ground)
-- Crop quality or ripeness (e.g., mixed ripeness, loose fruit levels, overripe bunches)
-- Weather or micro-climate effects (e.g., morning mist, short showers, heat buildup)
-- Equipment or tools (e.g., tractor performance, bin condition, cutter sharpness)
-- Manpower or team dynamics (e.g., fast harvesters, fatigue setting in, new worker adapting)
-- Field layout or block characteristics (e.g., long walking distance, soft patches, slope)
-- Collection point or evacuation flow (e.g., bin queue forming, delayed pickup, smooth turnaround)
-- Timing or pacing (e.g., late start, catching up by midday, slowing toward afternoon)
-- Small operational adjustments (e.g., rerouting harvest path, spacing out bin movement)
-- Minor issues or near-misses (e.g., short hold-up, brief stoppage, quick fix on site)
-- Sensory details (e.g., smell of fresh fruit, sound of rustling leaves, sight of sun glare)
-- Human factors (e.g., harvester morale, supervisor vigilance, teamwork spirit)
-- Environmental observations (e.g., presence of wildlife, condition of surrounding vegetation)
-- Any other specific, tangible detail relevant to FFB production
+- Harvest conditions (e.g., favorable weather, good ground conditions)
+- Crop quality (e.g., ripe fruit, good bunch weight)
+- Team dynamics (e.g., good attendance, high morale, efficient teamwork)
+- Operational flow (e.g., smooth evacuation, timely transport)
+- General observations (e.g., completed scheduled harvesting, routine maintenance done)
 
 
 Do not mention any of the above aspects explicitly in the remark. Instead, weave them naturally into the observation.
 Do not mention any of the above aspects explicitly in the remark. Instead, weave them naturally into the observation.
 
 
@@ -170,6 +161,7 @@ Guidelines:
 - Include at least one concrete, physical detail.
 - Include at least one concrete, physical detail.
 - Avoid generic phrases like “overall performance was good” or “operations ran smoothly”.
 - Avoid generic phrases like “overall performance was good” or “operations ran smoothly”.
 - Do not explain or summarize the entire day.
 - Do not explain or summarize the entire day.
+- FOCUS ON GENERAL WORK DONE AND POSITIVE/NEUTRAL OBSERVATIONS. DO NOT MENTION ISSUES OR PROBLEMS (Use generate-issues for that).
 
 
 ${contextInfo ? `Context Reference (use lightly, do not restate verbatim): ${contextInfo}` : ''}
 ${contextInfo ? `Context Reference (use lightly, do not restate verbatim): ${contextInfo}` : ''}
 
 
@@ -181,4 +173,67 @@ Remark:`;
     return remark.replace(/^Remark:\s*/i, '').replace(/^"|"$/g, '').trim();
     return remark.replace(/^Remark:\s*/i, '').replace(/^"|"$/g, '').trim();
   }
   }
 
 
+  /** Generate LLM issues for provided data */
+  async generateIssues(data?: any) {
+    let contextInfo = '';
+    let locationContext = '';
+
+    if (data && Object.keys(data).length > 0) {
+      const { siteId, phaseId, blockId } = data;
+
+      // Fetch additional context if IDs are provided
+      const [site, phase, block] = await Promise.all([
+        siteId ? this.siteService.findById(siteId) : null,
+        phaseId ? this.phaseService.findById(phaseId) : null,
+        blockId ? this.blockService.findById(blockId) : null,
+      ]);
+
+      if (site) {
+        locationContext += `Site "${site.name}": ${site.description || 'No description available.'} Location: ${site.address}. `;
+      }
+      if (phase) {
+        locationContext += `Phase "${phase.name}": ${phase.description || 'No description available.'} Status: ${phase.status}. `;
+      }
+      if (block) {
+        locationContext += `Block "${block.name}": ${block.description || 'No description available.'} Size: ${block.size} ${block.sizeUom || 'units'}, Number of trees: ${block.numOfTrees}. `;
+      }
+
+      contextInfo = `Production Data: ${JSON.stringify(data)}. `;
+      if (locationContext) {
+        contextInfo += `Location Context: ${locationContext}`;
+      }
+    } else {
+      contextInfo = 'Data: Random FFB production context.';
+    }
+
+    const prompt = `Write a specific issue report (1-2 sentences) from an oil palm plantation regarding FFB production.
+
+${locationContext ? `IMPORTANT: The issue should be RELEVANT to the following location context if provided:
+${locationContext}
+` : ''}
+
+Choose 1 specific problem to focus on. Keep it realistic and problematic.
+
+You may choose from (but are not limited to):
+- Weather issues (e.g., heavy rain halting work, flooded paths)
+- Equipment breakdown (e.g., tractor stuck, engine failure, broken ramp)
+- Manpower shortage (e.g., absentees, medical leave, slow progress)
+- Crop issues (e.g., high rate of uncollected loose fruit, unripe bunches harvested)
+- Access issues (e.g., collapsed bridge, road washed out, blocked drain)
+- Pests/Wildlife (e.g., elephant intrusion damage, rat damage)
+
+Guidelines:
+- Direct and to the point.
+- Clearly state the problem.
+- FOCUS ON ISSUES ONLY.
+
+${contextInfo ? `Context Reference: ${contextInfo}` : ''}
+
+Issue Report:`;
+
+    const remark = await this.ffbLangChainService.chatStateless(prompt, 'gemini');
+
+    // Clean up remark
+    return remark.replace(/^Issue Report:\s*/i, '').replace(/^"|"$/g, '').trim();
+  }
 }
 }

+ 53 - 11
src/FFB/services/ffb-vector.service.ts

@@ -1,16 +1,25 @@
-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { Injectable, OnModuleInit, Inject, forwardRef } from '@nestjs/common';
 import { MongoCoreService } from 'src/mongo/mongo-core.service';
 import { MongoCoreService } from 'src/mongo/mongo-core.service';
 import { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
 import { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
 import { FFBProduction } from '../ffb-production.schema';
 import { FFBProduction } from '../ffb-production.schema';
-import { GeminiEmbeddingService } from '../gemini-embedding.service';
+import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
+import { SiteService } from 'src/site/services/site.service';
+import { PhaseService } from 'src/site/services/phase.service';
+import { BlockService } from 'src/site/services/block.service';
 
 
 @Injectable()
 @Injectable()
 export class FFBVectorService implements OnModuleInit {
 export class FFBVectorService implements OnModuleInit {
   private repo: FFBProductionRepository;
   private repo: FFBProductionRepository;
+  private embeddings: GoogleGenerativeAIEmbeddings;
 
 
   constructor(
   constructor(
     private readonly mongoCore: MongoCoreService,
     private readonly mongoCore: MongoCoreService,
-    private readonly embeddingService: GeminiEmbeddingService
+    @Inject(forwardRef(() => SiteService))
+    private readonly siteService: SiteService,
+    @Inject(forwardRef(() => PhaseService))
+    private readonly phaseService: PhaseService,
+    @Inject(forwardRef(() => BlockService))
+    private readonly blockService: BlockService,
   ) { }
   ) { }
 
 
   async onModuleInit() {
   async onModuleInit() {
@@ -19,7 +28,13 @@ export class FFBVectorService implements OnModuleInit {
     this.repo = new FFBProductionRepository(db);
     this.repo = new FFBProductionRepository(db);
     await this.repo.init();
     await this.repo.init();
 
 
-    console.log('✅ Gemini embedding service ready. Repository initialized.');
+    // Initialize LangChain embeddings
+    this.embeddings = new GoogleGenerativeAIEmbeddings({
+      apiKey: process.env.GOOGLE_API_KEY,
+      modelName: process.env.EMBEDDING_MODEL || 'text-embedding-004', // Modern model
+    });
+
+    console.log('✅ FFB Vector Service initialized with LangChain embeddings.');
   }
   }
 
 
   /** Get a string representation of the schema based on a sample document */
   /** Get a string representation of the schema based on a sample document */
@@ -36,17 +51,42 @@ export class FFBVectorService implements OnModuleInit {
     return this.repo.distinct(field, filter);
     return this.repo.distinct(field, filter);
   }
   }
 
 
-  /** Convert a record to a string suitable for embedding */
-  private recordToText(record: FFBProduction): string {
-    return `Production on ${new Date(record.productionDate).toISOString()} at ${record.site} in ${record.phase} ${record.block} produced ${record.quantity} ${record.quantityUom} with a total weight of ${record.weight} ${record.weightUom}.`;
+  /** Convert a record to a string suitable for embedding with enriched context */
+  private async recordToTextEnriched(record: FFBProduction): Promise<string> {
+    const siteId = record.site.id;
+    const phaseId = record.phase.id;
+    const blockId = record.block.id;
+
+    // Fetch descriptions for enrichment
+    const [site, phase, block] = await Promise.all([
+      this.siteService.findById(siteId),
+      this.phaseService.findById(phaseId),
+      this.blockService.findById(blockId),
+    ]);
+
+    let text = `FFB Production Record:
+Date: ${new Date(record.productionDate).toLocaleDateString()}
+Location: Site "${record.site.name}", Phase "${record.phase.name}", Block "${record.block.name}".
+Metrics: Produced ${record.quantity} ${record.quantityUom} with a total weight of ${record.weight} ${record.weightUom}.
+Remarks: ${record.remarks || 'No remarks provided.'}
+Issues: ${record.issues || 'No issues reported.'}
+`;
+
+    if (site?.description) text += `Site Context: ${site.description}\n`;
+    if (phase?.description) text += `Phase Context: ${phase.description}\n`;
+    if (block?.description) text += `Block Context: ${block.description}\n`;
+
+    return text.trim();
   }
   }
 
 
   /** Insert a single record with embedding vector */
   /** Insert a single record with embedding vector */
   async insertWithVector(record: FFBProduction) {
   async insertWithVector(record: FFBProduction) {
-    const text = this.recordToText(record);
-    const vector = await this.embeddingService.embedText(text);
+    const text = await this.recordToTextEnriched(record);
+
+    // Use LangChain embeddings with RETRIEVAL_DOCUMENT task type
+    const vector = await this.embeddings.embedDocuments([text]);
 
 
-    const data: FFBProduction & { vector: number[] } = { ...record, vector };
+    const data: FFBProduction & { vector: number[] } = { ...record, vector: vector[0] };
     return this.repo.create(data);
     return this.repo.create(data);
   }
   }
 
 
@@ -54,7 +94,9 @@ export class FFBVectorService implements OnModuleInit {
   async vectorSearch(query: string, k = 5, filter: Record<string, any> = {}) {
   async vectorSearch(query: string, k = 5, filter: Record<string, any> = {}) {
     if (!query) throw new Error('Query string cannot be empty');
     if (!query) throw new Error('Query string cannot be empty');
 
 
-    const vector = await this.embeddingService.embedText(query);
+    // Use LangChain embeddings with RETRIEVAL_QUERY task type (internally handled or explicit)
+    // LangChain embedQuery uses query task type by default for Google Generative AI
+    const vector = await this.embeddings.embedQuery(query);
     const results = await this.repo.vectorSearch(vector, k, 50, filter);
     const results = await this.repo.vectorSearch(vector, k, 50, filter);
 
 
     return results.map((r) => ({
     return results.map((r) => ({

+ 1 - 0
src/site/services/site.service.ts

@@ -18,6 +18,7 @@ export class SiteService {
         private readonly mongoCore: MongoCoreService,
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
         private readonly ffbService: FFBProductionService,
+        @Inject(forwardRef(() => FFBLangChainService))
         private readonly ffbLangChainService: FFBLangChainService,
         private readonly ffbLangChainService: FFBLangChainService,
     ) { }
     ) { }