Browse Source

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

Dr-Swopt 2 weeks ago
parent
commit
9ad0e884db

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

@@ -74,4 +74,11 @@ export class FFBProductionController {
     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;
   quantityUom: string;
   remarks: string;
+  issues?: string;
 }
 
 export interface ThoughtPayload {

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

@@ -99,6 +99,7 @@ export class FFBProductionRepository {
             weight: 1,
             weightUom: 1,
             remarks: 1,
+            issues: 1,
             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;
 
     constructor(
+        @Inject(forwardRef(() => FFBVectorService))
         private readonly vectorService: FFBVectorService,
         @Inject(forwardRef(() => 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.
 
 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.
 
@@ -170,6 +161,7 @@ Guidelines:
 - Include at least one concrete, physical detail.
 - Avoid generic phrases like “overall performance was good” or “operations ran smoothly”.
 - 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}` : ''}
 
@@ -181,4 +173,67 @@ Remark:`;
     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 { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
 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()
 export class FFBVectorService implements OnModuleInit {
   private repo: FFBProductionRepository;
+  private embeddings: GoogleGenerativeAIEmbeddings;
 
   constructor(
     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() {
@@ -19,7 +28,13 @@ export class FFBVectorService implements OnModuleInit {
     this.repo = new FFBProductionRepository(db);
     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 */
@@ -36,17 +51,42 @@ export class FFBVectorService implements OnModuleInit {
     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 */
   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);
   }
 
@@ -54,7 +94,9 @@ export class FFBVectorService implements OnModuleInit {
   async vectorSearch(query: string, k = 5, filter: Record<string, any> = {}) {
     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);
 
     return results.map((r) => ({

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

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