Sfoglia il codice sorgente

feat: Implement block management with CRUD operations, FFB production integration, and AI-driven description generation.

Dr-Swopt 2 settimane fa
parent
commit
4f4cf902cf

+ 1 - 4
src/FFB/ffb-production.module.ts

@@ -7,7 +7,6 @@ import { SiteModule } from '../site/site.module';
 import { FFBVectorService } from './services/ffb-vector.service';
 import { GeminiEmbeddingService } from './gemini-embedding.service';
 import { FFBLangChainService } from './services/ffb-langchain.service';
-import { GeminiApiService } from './gemini.api';
 import { FFBGateway } from './ffb.gateway';
 
 @Module({
@@ -22,14 +21,12 @@ import { FFBGateway } from './ffb.gateway';
     FFBVectorService,
     GeminiEmbeddingService,
     FFBLangChainService,
-    FFBGateway,
-    GeminiApiService
+    FFBGateway
   ],
   exports: [
     FFBProductionService,
     FFBVectorService,
     FFBLangChainService,
-    GeminiApiService
   ],
 })
 

+ 28 - 1
src/FFB/services/ffb-production.service.ts

@@ -108,15 +108,42 @@ export class FFBProductionService {
   /** Generate LLM remarks for provided data or random context */
   async generateRemarks(data?: any) {
     let contextInfo = '';
+    let locationContext = '';
 
     if (data && Object.keys(data).length > 0) {
-      contextInfo = `Data: ${JSON.stringify(data)}`;
+      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 (e.g. wet weather affecting harvest, high yield block)';
     }
 
     const prompt = `Write an original, realistic field note or supervisor's remark (2–3 sentences) from an oil palm plantation regarding FFB production.
+    
+${locationContext ? `IMPORTANT: The remark should be SPECIFIC to the following location context if provided, without explicitly naming the descriptions, but using them to inform the tone or specific challenges/advantages mentioned:
+${locationContext}
+` : ''}
 
 Choose 2-3 specific aspects to focus on. Do not cover everything. Keep it grounded, practical, and observational.
 

+ 5 - 1
src/app.module.ts

@@ -11,6 +11,8 @@ import { MongoModule } from './mongo/mongo.module';
 import { FFBProductionModule } from './FFB/ffb-production.module';
 import { FaceModule } from './face/face.module';
 import { SiteModule } from './site/site.module';
+import { UsersModule } from './users/users.module';
+import { GeminiModule } from './gemini/gemini.module';
 
 @Module({
 
@@ -25,7 +27,9 @@ import { SiteModule } from './site/site.module';
     ServiceModule,
     ActivityModule,
     FaceModule,
-    SiteModule
+    SiteModule,
+    GeminiModule,
+    UsersModule
   ],
 
   controllers: [AppController],

+ 3 - 1
src/app.service.ts

@@ -1,7 +1,9 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, Logger } from '@nestjs/common';
 
 @Injectable()
 export class AppService {
+  private readonly logger = new Logger(AppService.name);
+
   getHello(): string {
     return 'Hello World!';
   }

+ 9 - 0
src/gemini/gemini.module.ts

@@ -0,0 +1,9 @@
+import { Module, Global } from '@nestjs/common';
+import { GeminiApiService } from './gemini.service';
+
+@Global()
+@Module({
+    providers: [GeminiApiService],
+    exports: [GeminiApiService],
+})
+export class GeminiModule { }

+ 1 - 1
src/FFB/gemini.api.ts → src/gemini/gemini.service.ts

@@ -33,4 +33,4 @@ export class GeminiApiService {
             throw err;
         }
     }
-}
+}

+ 1 - 9
src/site/controllers/block.controller.ts

@@ -1,24 +1,16 @@
 import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
 import { BlockService } from '../services/block.service';
 import { Block } from '../schemas/site.schema';
-import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 @Controller('blocks')
 export class BlockController {
     constructor(
         private readonly blockService: BlockService,
-        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
     @Post('generate-description')
     async generateDescription(@Body() body: { name: string, phaseName?: string, size?: number, numOfTrees?: number }) {
-        let details = '';
-        if (body.size) details += ` It is ${body.size} in size.`;
-        if (body.numOfTrees) details += ` It contains ${body.numOfTrees} trees.`;
-
-        const prompt = `Generate a professional description for a plantation block named "${body.name}"${body.phaseName ? ` within the phase "${body.phaseName}"` : ''}.${details} Keep it under 2 sentences.`;
-        const description = await this.ffbLangChainService.chatStateless(prompt);
-        return { description };
+        return this.blockService.generateDescription(body);
     }
 
     @Post()

+ 1 - 5
src/site/controllers/phase.controller.ts

@@ -2,7 +2,6 @@ import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/
 import { PhaseService } from '../services/phase.service';
 import { BlockService } from '../services/block.service';
 import { Phase } from '../schemas/site.schema';
-import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 
 @Controller('phases')
@@ -10,14 +9,11 @@ export class PhaseController {
     constructor(
         private readonly phaseService: PhaseService,
         private readonly blockService: BlockService,
-        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
     @Post('generate-description')
     async generateDescription(@Body() body: { name: string, siteName?: string }) {
-        const prompt = `Generate a professional description for a plantation phase named "${body.name}"${body.siteName ? ` within the site "${body.siteName}"` : ''}. Keep it under 2 sentences.`;
-        const description = await this.ffbLangChainService.chatStateless(prompt);
-        return { description };
+        return this.phaseService.generateDescription(body);
     }
 
     @Post()

+ 1 - 5
src/site/controllers/site.controller.ts

@@ -2,7 +2,6 @@ import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/
 import { SiteService } from '../services/site.service';
 import { PhaseService } from '../services/phase.service';
 import { Site } from '../schemas/site.schema';
-import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 
 @Controller('sites')
@@ -10,14 +9,11 @@ export class SiteController {
     constructor(
         private readonly siteService: SiteService,
         private readonly phaseService: PhaseService,
-        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
     @Post('generate-description')
     async generateDescription(@Body() body: { name: string, address?: string }) {
-        const prompt = `Generate a professional description for a plantation site named "${body.name}" located at "${body.address || 'unspecified location'}". Keep it under 2 sentences.`;
-        const description = await this.ffbLangChainService.chatStateless(prompt);
-        return { description };
+        return this.siteService.generateDescription(body);
     }
 
     @Post()

+ 21 - 0
src/site/services/block.service.ts

@@ -1,4 +1,5 @@
 import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
+import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { BlockRepository } from '../repositories/block.repository';
@@ -17,6 +18,7 @@ export class BlockService {
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
+        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
 
@@ -92,4 +94,23 @@ export class BlockService {
         // 2. Delete the block itself
         await repo.delete(id);
     }
+
+    async generateDescription(data: { name: string, phaseName?: string, size?: number, numOfTrees?: number }): Promise<{ description: string }> {
+        const prompt = `Write a realistic, professional description (2–4 sentences) for an oil palm plantation block named "${data.name}"${data.phaseName ? ` within phase "${data.phaseName}"` : ''}.${data.size ? ` It has a size of ${data.size} units` : ''}${data.numOfTrees ? ` and contains ${data.numOfTrees} trees` : ''}.
+
+Choose 1-2 aspects from the following to incorporate naturally:
+- Block health and maturity (e.g., trees at peak production age, healthy canopy development, consistent fruit set)
+- Field accessibility (e.g., easily accessible by tractor paths, slightly sloped area requiring specialized harvesting tools)
+- Environmental features (e.g., bordering primary forest, presence of natural irrigation channels, rich nutrient-holding soil)
+- Monitoring and care (e.g., subject to intensive pest monitoring, recently fertilized, under strict irrigation schedule)
+
+Guidelines:
+- Maintain a grounded, operational tone.
+- Do not mention the bullet points explicitly; weave them into the narrative.
+- Ensure the length is exactly 2-4 sentences.
+
+Description:`;
+        const description = await this.ffbLangChainService.chatStateless(prompt);
+        return { description: description.replace(/^Description:\s*/i, '').trim() };
+    }
 }

+ 21 - 0
src/site/services/phase.service.ts

@@ -1,4 +1,5 @@
 import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
+import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { PhaseRepository } from '../repositories/phase.repository';
@@ -17,6 +18,7 @@ export class PhaseService {
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
+        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
 
@@ -90,4 +92,23 @@ export class PhaseService {
         // 3. Delete the phase itself
         await repo.delete(id);
     }
+
+    async generateDescription(data: { name: string, siteName?: string }): Promise<{ description: string }> {
+        const prompt = `Write a realistic, professional description (2–4 sentences) for an oil palm plantation phase named "${data.name}"${data.siteName ? ` belonging to the site "${data.siteName}"` : ''}.
+
+Choose 1-2 aspects from the following to incorporate naturally:
+- Operational status (e.g., peak harvest season, recently completed pruning, ongoing field maintenance)
+- Terrain and conditions (e.g., undulating landscape with laterite paths, well-draining soil, distinct drainage networks)
+- Workforce activities (e.g., supervised harvesting teams on site, morning muster activities, regular perimeter checks)
+- Vegetation health (e.g., vibrant green fronds, uniform bunch development, minimal disease incidence)
+
+Guidelines:
+- Maintain a grounded, operational tone.
+- Do not mention the bullet points explicitly; weave them into the narrative.
+- Ensure the length is exactly 2-4 sentences.
+
+Description:`;
+        const description = await this.ffbLangChainService.chatStateless(prompt);
+        return { description: description.replace(/^Description:\s*/i, '').trim() };
+    }
 }

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

@@ -1,4 +1,5 @@
 import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
+import { FFBLangChainService } from '../../FFB/services/ffb-langchain.service';
 
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { SiteRepository } from '../repositories/site.repository';
@@ -17,6 +18,7 @@ export class SiteService {
         private readonly mongoCore: MongoCoreService,
         @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
+        private readonly ffbLangChainService: FFBLangChainService,
     ) { }
 
 
@@ -86,4 +88,23 @@ export class SiteService {
         // 5. Delete the site itself
         await repo.delete(id);
     }
+
+    async generateDescription(data: { name: string, address?: string }): Promise<{ description: string }> {
+        const prompt = `Write a realistic, professional description (2–4 sentences) for an oil palm plantation site named "${data.name}" located at "${data.address || 'Unknown address'}".
+
+Choose 1-2 aspects from the following to incorporate naturally:
+- General infrastructure (e.g., well-maintained access roads, proximity to main transport hubs, presence of staff housing)
+- Environmental setting (e.g., rolling hills, river boundaries, dense canopy coverage)
+- Workforce and activity (e.g., active harvesting teams, daily briefings at the muster point, recent replanting efforts)
+- Strategic importance (e.g., central processing site, high-yield territory, pivotal logistical junction)
+
+Guidelines:
+- Maintain a grounded, operational tone.
+- Do not mention the bullet points explicitly; weave them into the narrative.
+- Ensure the length is exactly 2-4 sentences.
+
+Description:`;
+        const description = await this.ffbLangChainService.chatStateless(prompt);
+        return { description: description.replace(/^Description:\s*/i, '').trim() };
+    }
 }

+ 26 - 3
src/users/user.service.ts

@@ -1,15 +1,38 @@
-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
 import { isoBase64URL } from '@simplewebauthn/server/helpers';
 import * as bcrypt from 'bcrypt';
-import { LoginPayload, RegisteredUser, User } from 'src/interface/interface';
+import { LoginPayload, RegisteredUser, User } from '../interface/interface';
 
 
 @Injectable()
-export class UsersService {
+export class UsersService implements OnModuleInit {
     private logger: Logger = new Logger(`UsersService`);
     private users: RegisteredUser[] = [];
     private idCounter = 1;
 
+    async onModuleInit() {
+        // Defer seeding to avoid initialization bottlenecks
+        setTimeout(() => {
+            this.seedDefaultUser().catch(err => {
+                this.logger.error(`Failed to seed default user: ${err.message || err}`);
+            });
+        }, 5000);
+    }
+
+    private async seedDefaultUser() {
+        const email = 'user@user.com';
+        const existing = await this.findByEmail(email);
+        if (!existing) {
+            this.logger.log(`Seeding default user: ${email}`);
+            await this.createUser({
+                name: 'Default User',
+                email: email,
+                password: 'user123'
+            });
+            this.logger.log(`User ${email} seeded successfully.`);
+        }
+    }
+
     public async createUser(user: User): Promise<RegisteredUser> {
         return new Promise(async (resolve, reject) => {
             const existing = this.users.find(u => u.email === user.email);

+ 3 - 3
src/users/users.controller.ts

@@ -1,10 +1,10 @@
 import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
-import { UsersService } from 'src/users/user.service';
-import { RegisteredUser } from 'src/interface/interface';
+import { UsersService } from './user.service';
+import { RegisteredUser } from '../interface/interface';
 
 @Controller('users')
 export class UsersController {
-  constructor(private readonly usersService: UsersService) {}
+  constructor(private readonly usersService: UsersService) { }
 
   // GET /users/:id → Fetch user by ID
   @Get(':id')

+ 3 - 3
src/users/users.module.ts

@@ -1,11 +1,11 @@
 // src/services/users.module.ts
 import { Module } from '@nestjs/common';
-import { UsersService } from 'src/users/user.service';
-import { UsersController } from 'src/users/users.controller';
+import { UsersService } from './user.service';
+import { UsersController } from './users.controller';
 
 @Module({
   providers: [UsersService],
   exports: [UsersService],
   controllers: [UsersController],
 })
-export class UsersModule {}
+export class UsersModule { }