ffb-query-planner.service.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { Injectable, OnModuleInit } from '@nestjs/common';
  2. import * as fs from 'fs';
  3. import * as path from 'path';
  4. import axios from 'axios';
  5. import { AgentQueryPlan } from './ffb-agent.types';
  6. @Injectable()
  7. export class FFBQueryPlannerService implements OnModuleInit {
  8. private systemPrompt: any;
  9. async onModuleInit() {
  10. const filePath = path.join(process.cwd(), 'AgentQueryPlan.json'); // updated file
  11. const data = fs.readFileSync(filePath, 'utf-8');
  12. this.systemPrompt = JSON.parse(data);
  13. }
  14. private buildPrompt(userMessage: string): string {
  15. const examplesText = (this.systemPrompt.examples || [])
  16. .map(
  17. (ex: any) =>
  18. `Q: "${ex.question}"\nA: ${JSON.stringify(ex.plan, null, 2)}`
  19. )
  20. .join('\n\n');
  21. return `
  22. ${this.systemPrompt.instructions}
  23. Always include the minimal "fields" needed for computation to reduce bandwidth.
  24. ${examplesText}
  25. Now, given the following user question, output the JSON only:
  26. Q: "${userMessage}"
  27. `;
  28. }
  29. async plan(userMessage: string): Promise<AgentQueryPlan> {
  30. const promptText = this.buildPrompt(userMessage);
  31. const responseText = await this.callGemini(promptText);
  32. const sanitized = this.sanitizeLLMOutput(responseText);
  33. try {
  34. return JSON.parse(sanitized);
  35. } catch (err) {
  36. console.error('Failed to parse Gemini output:', sanitized);
  37. throw new Error('LLM returned invalid JSON');
  38. }
  39. }
  40. private async callGemini(prompt: string): Promise<string> {
  41. const apiKey = process.env.GOOGLE_API_KEY;
  42. if (!apiKey) throw new Error('Missing GOOGLE_API_KEY');
  43. const url =
  44. 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
  45. const body = {
  46. contents: [{ role: 'user', parts: [{ text: prompt }] }],
  47. };
  48. try {
  49. const response = await axios.post(url, body, {
  50. headers: {
  51. 'Content-Type': 'application/json',
  52. 'x-goog-api-key': apiKey,
  53. },
  54. });
  55. const text =
  56. response.data?.candidates?.[0]?.content?.parts
  57. ?.map((p: any) => p.text)
  58. .join(' ') ?? '';
  59. if (!text) throw new Error('No text generated by Gemini');
  60. return text;
  61. } catch (err: any) {
  62. console.error('Failed to call Gemini:', err.response?.data || err.message);
  63. throw err;
  64. }
  65. }
  66. private sanitizeLLMOutput(text: string): string {
  67. return text
  68. .trim()
  69. .replace(/^```json\s*/, '') // remove opening ```json
  70. .replace(/^```\s*/, '') // remove opening ```
  71. .replace(/```$/, '') // remove closing ```
  72. .trim();
  73. }
  74. }