|
|
@@ -5,18 +5,12 @@ import { FFBGateway } from "./ffb.gateway";
|
|
|
import path from "path";
|
|
|
import fs from "fs";
|
|
|
import axios from "axios";
|
|
|
-import jwt from "jsonwebtoken";
|
|
|
import { GeminiEmbeddingService } from "./gemini-embedding.service";
|
|
|
|
|
|
@Injectable()
|
|
|
export class FFBQueryAgentService implements OnModuleInit {
|
|
|
private systemPrompt: any;
|
|
|
private repo: FFBProductionRepository;
|
|
|
- private readonly VECTOR_DIM = parseInt(process.env.VECTOR_DIM || '3072'); // Gemini default
|
|
|
-
|
|
|
- private serviceAccount: any; // parsed service account JSON
|
|
|
- private tokenExpiry: number = 0;
|
|
|
- private accessToken: string = '';
|
|
|
|
|
|
constructor(
|
|
|
private readonly mongoCore: MongoCoreService,
|
|
|
@@ -28,35 +22,32 @@ export class FFBQueryAgentService implements OnModuleInit {
|
|
|
const filePath = path.join(process.cwd(), 'QueryAgent.json');
|
|
|
const data = fs.readFileSync(filePath, 'utf-8');
|
|
|
this.systemPrompt = JSON.parse(data);
|
|
|
-
|
|
|
- // Load service account for OAuth
|
|
|
- const credsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
|
- if (!credsPath) throw new Error('Missing GOOGLE_APPLICATION_CREDENTIALS');
|
|
|
- const credsData = fs.readFileSync(credsPath, 'utf-8');
|
|
|
- this.serviceAccount = JSON.parse(credsData);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
- private buildPrompt(userMessage: string): string {
|
|
|
+ private buildPrompt(userMessage: string, schemaFields: string[]): string {
|
|
|
const examplesText = (this.systemPrompt.examples || [])
|
|
|
- .map((ex: any) => `Q: "${ex.question}"\nA: ${JSON.stringify(ex.plan, null, 2)}`)
|
|
|
+ .map(
|
|
|
+ (ex: any) => `Q: "${ex.question}"\nA: ${JSON.stringify(ex.plan, null, 2)}`
|
|
|
+ )
|
|
|
.join('\n\n');
|
|
|
|
|
|
return `
|
|
|
${this.systemPrompt.instructions}
|
|
|
|
|
|
+Document fields: ${schemaFields.join(", ")}
|
|
|
+
|
|
|
Always include the minimal "fields" needed for computation to reduce bandwidth.
|
|
|
|
|
|
${examplesText}
|
|
|
|
|
|
Now, given the following user question, output JSON only in the format:
|
|
|
-{ "textToBeEmbedded": string, "pipeline": [ ... ] }
|
|
|
+{ "textToBeEmbedded": string, "pipeline": [ ... ], "reasoning": string }
|
|
|
|
|
|
Q: "${userMessage}"
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
private async callGemini(prompt: string): Promise<string> {
|
|
|
const apiKey = process.env.GOOGLE_API_KEY;
|
|
|
if (!apiKey) throw new Error('Missing GOOGLE_API_KEY');
|
|
|
@@ -93,7 +84,7 @@ Q: "${userMessage}"
|
|
|
return sanitized;
|
|
|
}
|
|
|
|
|
|
- private parseLLMOutput(sanitized: string): { textToBeEmbedded: string; pipeline: any[] } {
|
|
|
+ private parseLLMOutput(sanitized: string): { textToBeEmbedded: string; pipeline: any[], reasoning: string } {
|
|
|
try {
|
|
|
const parsed = JSON.parse(sanitized);
|
|
|
if (!('pipeline' in parsed) || !Array.isArray(parsed.pipeline)) {
|
|
|
@@ -121,37 +112,75 @@ Q: "${userMessage}"
|
|
|
|
|
|
/** Main entry point: plan + conditional vector search execution */
|
|
|
async query(userMessage: string): Promise<any[]> {
|
|
|
- const promptText = this.buildPrompt(userMessage);
|
|
|
+ // 0. Get repository first
|
|
|
+ const repo = await this.getRepo();
|
|
|
+
|
|
|
+ // 1. Get sample doc to dynamically inject schema fields
|
|
|
+ const sampleDoc = await repo.findAll();
|
|
|
+ const ffbFields = sampleDoc.length > 0 ? Object.keys(sampleDoc[0]) : [
|
|
|
+ "productionDate",
|
|
|
+ "site",
|
|
|
+ "phase",
|
|
|
+ "block",
|
|
|
+ "weight",
|
|
|
+ "weightUom",
|
|
|
+ "quantity",
|
|
|
+ "quantityUom"
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 2. Build prompt with dynamic schema
|
|
|
+ const promptText = this.buildPrompt(userMessage, ffbFields);
|
|
|
+
|
|
|
+ // 3. Call LLM
|
|
|
const llmResponse = await this.callGemini(promptText);
|
|
|
|
|
|
+ // 4. Sanitize + parse
|
|
|
const sanitized = this.sanitizeLLMOutput(llmResponse);
|
|
|
- const { textToBeEmbedded, pipeline } = this.parseLLMOutput(sanitized);
|
|
|
+ const { textToBeEmbedded, pipeline, reasoning } = this.parseLLMOutput(sanitized);
|
|
|
|
|
|
+ // 5. If vector search is needed, generate embedding and inject into $vectorSearch
|
|
|
if (textToBeEmbedded && textToBeEmbedded.trim()) {
|
|
|
const embedding = await this.embeddingService.embedText(textToBeEmbedded.trim());
|
|
|
- for (const stage of pipeline) {
|
|
|
- if ('$vectorSearch' in stage) stage.$vectorSearch.queryVector = embedding;
|
|
|
+
|
|
|
+ // Ensure $vectorSearch is the first stage in the pipeline
|
|
|
+ const vectorStageIndex = pipeline.findIndex(stage => '$vectorSearch' in stage);
|
|
|
+ if (vectorStageIndex > -1) {
|
|
|
+ pipeline[vectorStageIndex].$vectorSearch.queryVector = embedding;
|
|
|
+ if (vectorStageIndex !== 0) {
|
|
|
+ // Move $vectorSearch to first stage
|
|
|
+ const [vsStage] = pipeline.splice(vectorStageIndex, 1);
|
|
|
+ pipeline.unshift(vsStage);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const pipelineForSocket = pipeline.map(stage => ('$vectorSearch' in stage ? { ...stage, $vectorSearch: { ...stage.$vectorSearch, queryVector: '[VECTOR]' } } : stage));
|
|
|
+ // 6. Prepare pipeline for frontend display (mask actual vectors)
|
|
|
+ const pipelineForSocket = pipeline.map(stage =>
|
|
|
+ ('$vectorSearch' in stage
|
|
|
+ ? { ...stage, $vectorSearch: { ...stage.$vectorSearch, queryVector: '[VECTOR]' } }
|
|
|
+ : stage)
|
|
|
+ );
|
|
|
|
|
|
+ // 7. Emit pipeline + reasoning
|
|
|
this.gateway.emitAgentOutput({
|
|
|
stage: 'pipeline_generated',
|
|
|
rawLLMOutput: llmResponse,
|
|
|
pipeline: pipelineForSocket,
|
|
|
prettyPipeline: JSON.stringify(pipelineForSocket, null, 2),
|
|
|
textToBeEmbedded,
|
|
|
+ reasoning
|
|
|
});
|
|
|
|
|
|
- const repo = await this.getRepo();
|
|
|
+ // 8. Execute aggregation
|
|
|
const results = await repo.aggregate(pipeline);
|
|
|
|
|
|
+ // 9. Emit execution results
|
|
|
this.gateway.emitAgentOutput({
|
|
|
stage: 'pipeline_executed',
|
|
|
pipeline: pipelineForSocket,
|
|
|
count: results.length,
|
|
|
results,
|
|
|
+ reasoning
|
|
|
});
|
|
|
|
|
|
return results;
|