浏览代码

feat: Implement initial backend for site, phase, and block management, session handling, and AI-powered description generation.

Dr-Swopt 2 周之前
父节点
当前提交
33bd17c993

+ 10 - 2
src/FFB/ffb-production.module.ts

@@ -7,6 +7,7 @@ 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({
@@ -21,8 +22,15 @@ import { FFBGateway } from './ffb.gateway';
     FFBVectorService,
     GeminiEmbeddingService,
     FFBLangChainService,
-    FFBGateway
+    FFBGateway,
+    GeminiApiService
+  ],
+  exports: [
+    FFBProductionService,
+    FFBVectorService,
+    FFBLangChainService,
+    GeminiApiService
   ],
-  exports: [],
 })
+
 export class FFBProductionModule { }

+ 27 - 23
src/FFB/gemini.api.ts

@@ -1,32 +1,36 @@
+import { Injectable } from "@nestjs/common";
 import axios from "axios";
 
-export async function callGemini(prompt: string): Promise<string> {
-    const apiKey = process.env.GOOGLE_API_KEY;
-    if (!apiKey) throw new Error("Missing GOOGLE_API_KEY");
+@Injectable()
+export class GeminiApiService {
+    async callGemini(prompt: string): Promise<string> {
+        const apiKey = process.env.GOOGLE_API_KEY;
+        if (!apiKey) throw new Error("Missing GOOGLE_API_KEY");
 
-    const url =
-        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent";
+        const url =
+            "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
 
-    const body = {
-        contents: [{ role: 'user', parts: [{ text: prompt }] }],
-    };
+        const body = {
+            contents: [{ role: 'user', parts: [{ text: prompt }] }],
+        };
 
-    try {
-        const response = await axios.post(url, body, {
-            headers: {
-                'Content-Type': 'application/json',
-                'x-goog-api-key': apiKey,
-            },
-        });
+        try {
+            const response = await axios.post(url, body, {
+                headers: {
+                    'Content-Type': 'application/json',
+                    'x-goog-api-key': apiKey,
+                },
+            });
 
-        const text = response.data?.candidates?.[0]?.content?.parts
-            ?.map((p: any) => p.text)
-            .join(' ') ?? '';
+            const text = response.data?.candidates?.[0]?.content?.parts
+                ?.map((p: any) => p.text)
+                .join(' ') ?? '';
 
-        if (!text) throw new Error("No text generated by Gemini");
-        return text;
-    } catch (err: any) {
-        console.error("Failed to call Gemini:", err.response?.data || err.message);
-        throw err;
+            if (!text) throw new Error("No text generated by Gemini");
+            return text;
+        } catch (err: any) {
+            console.error("Failed to call Gemini:", err.response?.data || err.message);
+            throw err;
+        }
     }
 }

+ 2 - 2
src/FFB/services/ffb-langchain.service.ts

@@ -52,7 +52,7 @@ export class FFBLangChainService {
 
     private getProvider(socketId: string): 'openai' | 'gemini' {
         if (socketId?.startsWith('stateless:')) {
-            return (socketId.split(':')[1] as 'openai' | 'gemini') || 'openai';
+            return (socketId.split(':')[1] as 'openai' | 'gemini') || 'gemini';
         }
         return this.sessionManager.getModelProvider(socketId);
     }
@@ -177,7 +177,7 @@ export class FFBLangChainService {
         }
     }
 
-    async chatStateless(message: string, provider: 'openai' | 'gemini' = 'openai'): Promise<string> {
+    async chatStateless(message: string, provider: 'openai' | 'gemini' = 'gemini'): Promise<string> {
         try {
             const userMsg = new HumanMessage(message);
             const socketId = `stateless:${provider}`;

+ 1 - 1
src/FFB/services/utils/session-manager.ts

@@ -18,7 +18,7 @@ export class SessionManager {
         this.sessions.set(socketId, {
             items: [],
             entityStore: {},
-            modelProvider: 'openai' // Default
+            modelProvider: 'gemini' // Default
         });
         console.log(`Session created for ${socketId}`);
     }

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

@@ -1,10 +1,25 @@
 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) { }
+    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 };
+    }
 
     @Post()
     async create(@Body() block: Block) {

+ 9 - 0
src/site/controllers/phase.controller.ts

@@ -2,6 +2,7 @@ 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')
@@ -9,8 +10,16 @@ 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 };
+    }
+
     @Post()
 
     async create(@Body() phase: Phase) {

+ 9 - 0
src/site/controllers/site.controller.ts

@@ -2,6 +2,7 @@ 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')
@@ -9,8 +10,16 @@ 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 };
+    }
+
     @Post()
 
     async create(@Body() site: Site) {

+ 23 - 5
src/site/services/block.service.ts

@@ -1,40 +1,53 @@
-import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
+
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { BlockRepository } from '../repositories/block.repository';
 import { Block } from '../schemas/site.schema';
 import { PhaseRepository } from '../repositories/phase.repository';
+import { SiteRepository } from '../repositories/site.repository';
 import { FFBProductionService } from '../../FFB/services/ffb-production.service';
 
 @Injectable()
 export class BlockService {
     private repo: BlockRepository;
     private phaseRepo: PhaseRepository;
+    private siteRepo: SiteRepository;
 
     constructor(
         private readonly mongoCore: MongoCoreService,
+        @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
     ) { }
 
+
     private async getRepos() {
         if (!this.repo) {
             const db = await this.mongoCore.getDb();
             this.repo = new BlockRepository(db);
             this.phaseRepo = new PhaseRepository(db);
+            this.siteRepo = new SiteRepository(db);
             await this.repo.init();
             await this.phaseRepo.init();
+            await this.siteRepo.init();
         }
-        return { repo: this.repo, phaseRepo: this.phaseRepo };
+        return { repo: this.repo, phaseRepo: this.phaseRepo, siteRepo: this.siteRepo };
     }
 
     async create(block: Block): Promise<Block> {
-        const { repo, phaseRepo } = await this.getRepos();
+        const { repo, phaseRepo, siteRepo } = await this.getRepos();
 
-        // Parent Validation: Check if Phase exists
+        // 1. Validate Phase exists
         const phase = await phaseRepo.findById(block.phaseId);
         if (!phase) {
             throw new BadRequestException(`Phase with ID ${block.phaseId} does not exist.`);
         }
 
+        // 2. Validate Site exists
+        const site = await siteRepo.findById(phase.siteId);
+        if (!site) {
+            throw new BadRequestException(`Site with ID ${phase.siteId} (from Phase ${block.phaseId}) does not exist.`);
+        }
+
         return repo.create(block);
     }
 
@@ -49,14 +62,19 @@ export class BlockService {
     }
 
     async update(id: string, update: Partial<Block>): Promise<void> {
-        const { repo, phaseRepo } = await this.getRepos();
+        const { repo, phaseRepo, siteRepo } = await this.getRepos();
 
         const block = await repo.findById(id);
         if (!block) throw new NotFoundException('Block not found');
 
         if (update.phaseId) {
+            // 1. Validate Phase exists
             const phase = await phaseRepo.findById(update.phaseId);
             if (!phase) throw new BadRequestException(`Phase with ID ${update.phaseId} does not exist.`);
+
+            // 2. Validate Site exists
+            const site = await siteRepo.findById(phase.siteId);
+            if (!site) throw new BadRequestException(`Site with ID ${phase.siteId} (from Phase ${update.phaseId}) does not exist.`);
         }
 
         await repo.update(id, update);

+ 4 - 1
src/site/services/phase.service.ts

@@ -1,4 +1,5 @@
-import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { Injectable, NotFoundException, BadRequestException, Inject, forwardRef } from '@nestjs/common';
+
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { PhaseRepository } from '../repositories/phase.repository';
 import { Phase } from '../schemas/site.schema';
@@ -14,9 +15,11 @@ export class PhaseService {
 
     constructor(
         private readonly mongoCore: MongoCoreService,
+        @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
     ) { }
 
+
     private async getRepos() {
         if (!this.repo) {
             const db = await this.mongoCore.getDb();

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

@@ -1,4 +1,5 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
+
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { SiteRepository } from '../repositories/site.repository';
 import { Site } from '../schemas/site.schema';
@@ -14,9 +15,11 @@ export class SiteService {
 
     constructor(
         private readonly mongoCore: MongoCoreService,
+        @Inject(forwardRef(() => FFBProductionService))
         private readonly ffbService: FFBProductionService,
     ) { }
 
+
     private async getRepos() {
         if (!this.repo) {
             const db = await this.mongoCore.getDb();