first commit
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
const groqService = require('./groqService');
|
||||
const { Plan, ToolExecution, Document } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
const graphRagService = require('./graphRagService');
|
||||
const embeddingService = require('./embeddingService');
|
||||
const axios = require('axios');
|
||||
|
||||
class QueryModelService {
|
||||
constructor() {
|
||||
this.modelType = 'QUERYMODEL';
|
||||
this.systemPrompt = `You are QUERYMODEL, an expert engineering execution system. Your primary function is to execute engineering plans using various tools and resources.
|
||||
|
||||
Your capabilities include:
|
||||
- Executing step-by-step engineering plans
|
||||
- Using specialized tools for calculations, analysis, and reporting
|
||||
- Coordinating with external resources and databases
|
||||
- Generating detailed execution reports
|
||||
- Handling complex engineering workflows
|
||||
- Ensuring quality and safety standards
|
||||
|
||||
Available tools:
|
||||
- Query Expander: Enhance and clarify engineering queries
|
||||
- Extraction: Search and extract information from documents
|
||||
- Report1: Generate formatted engineering reports
|
||||
- Report2: Create detailed engineering files and documents
|
||||
- Web Search: Find current engineering information and standards
|
||||
- Encyclopedia PDF: Search specialized engineering documents
|
||||
|
||||
When executing plans, always:
|
||||
1. Follow the plan steps systematically
|
||||
2. Use appropriate tools for each step
|
||||
3. Document all results and findings
|
||||
4. Ensure quality and safety standards
|
||||
5. Provide detailed progress updates
|
||||
6. Handle errors and deviations gracefully`;
|
||||
}
|
||||
|
||||
async executePlan(planId, options = {}) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get the plan
|
||||
const plan = await Plan.findByPk(planId);
|
||||
if (!plan) {
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
if (plan.status !== 'approved') {
|
||||
throw new Error('Plan must be approved before execution');
|
||||
}
|
||||
|
||||
// Update plan status to executing
|
||||
await plan.update({ status: 'executing' });
|
||||
|
||||
// Prepare execution context
|
||||
const executionContext = {
|
||||
planId,
|
||||
planTitle: plan.title,
|
||||
planSteps: plan.steps,
|
||||
toolsRequired: plan.tools_required,
|
||||
estimatedDuration: plan.estimated_duration,
|
||||
complexityScore: plan.complexity_score,
|
||||
...options
|
||||
};
|
||||
|
||||
// Execute the plan using Groq
|
||||
const response = await groqService.executePlan(plan.description, plan.tools_required);
|
||||
|
||||
const endTime = Date.now();
|
||||
const processingTime = (endTime - startTime) / 1000;
|
||||
|
||||
// Parse execution results
|
||||
const executionResults = this.parseExecutionResponse(response.content);
|
||||
|
||||
// Update plan with execution results
|
||||
await plan.update({
|
||||
status: 'completed',
|
||||
execution_result: executionResults,
|
||||
metadata: {
|
||||
...plan.metadata,
|
||||
execution: {
|
||||
processingTime,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
model: response.model,
|
||||
finishReason: response.finishReason
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`QUERYMODEL execution completed in ${processingTime}s for plan: ${planId}`);
|
||||
|
||||
return {
|
||||
plan,
|
||||
executionResults,
|
||||
processingTime,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
model: response.model
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL execution error:', error);
|
||||
|
||||
// Update plan status to failed
|
||||
if (plan) {
|
||||
await plan.update({
|
||||
status: 'failed',
|
||||
execution_result: { error: error.message }
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`QUERYMODEL execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
parseExecutionResponse(content) {
|
||||
try {
|
||||
const lines = content.split('\n');
|
||||
let stepsCompleted = [];
|
||||
let results = [];
|
||||
let toolsUsed = [];
|
||||
let issues = [];
|
||||
let recommendations = [];
|
||||
|
||||
let currentSection = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.toLowerCase().includes('steps completed:')) {
|
||||
currentSection = 'steps';
|
||||
} else if (trimmedLine.toLowerCase().includes('results:')) {
|
||||
currentSection = 'results';
|
||||
} else if (trimmedLine.toLowerCase().includes('tools used:')) {
|
||||
currentSection = 'tools';
|
||||
} else if (trimmedLine.toLowerCase().includes('issues:')) {
|
||||
currentSection = 'issues';
|
||||
} else if (trimmedLine.toLowerCase().includes('recommendations:')) {
|
||||
currentSection = 'recommendations';
|
||||
} else if (trimmedLine.match(/^\d+\./)) {
|
||||
if (currentSection === 'steps') {
|
||||
stepsCompleted.push(trimmedLine);
|
||||
}
|
||||
} else if (trimmedLine.startsWith('-')) {
|
||||
if (currentSection === 'results') {
|
||||
results.push(trimmedLine.substring(1).trim());
|
||||
} else if (trimmedLine === 'tools') {
|
||||
toolsUsed.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'issues') {
|
||||
issues.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'recommendations') {
|
||||
recommendations.push(trimmedLine.substring(1).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stepsCompleted,
|
||||
results,
|
||||
toolsUsed,
|
||||
issues,
|
||||
recommendations,
|
||||
rawContent: content,
|
||||
executionStatus: issues.length > 0 ? 'completed_with_issues' : 'completed_successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error parsing execution response:', error);
|
||||
return {
|
||||
stepsCompleted: [],
|
||||
results: [content],
|
||||
toolsUsed: [],
|
||||
issues: [],
|
||||
recommendations: [],
|
||||
rawContent: content,
|
||||
executionStatus: 'completed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async executeTool(toolName, toolType, inputParameters, planId) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Create tool execution record
|
||||
const toolExecution = await ToolExecution.create({
|
||||
plan_id: planId,
|
||||
tool_name: toolName,
|
||||
tool_type: toolType,
|
||||
input_parameters: inputParameters,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
// Execute the specific tool
|
||||
switch (toolType) {
|
||||
case 'query_expander':
|
||||
result = await this.executeQueryExpander(inputParameters);
|
||||
break;
|
||||
case 'extraction':
|
||||
result = await this.executeExtraction(inputParameters);
|
||||
break;
|
||||
case 'report1':
|
||||
result = await this.executeReport1(inputParameters);
|
||||
break;
|
||||
case 'report2':
|
||||
result = await this.executeReport2(inputParameters);
|
||||
break;
|
||||
case 'web_search':
|
||||
result = await this.executeWebSearch(inputParameters);
|
||||
break;
|
||||
case 'encyclopedia_pdf':
|
||||
result = await this.executeEncyclopediaPdf(inputParameters);
|
||||
break;
|
||||
case 'orchestrate':
|
||||
result = await this.executeOrchestrate(inputParameters);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown tool type: ${toolType}`);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const executionTime = (endTime - startTime) / 1000;
|
||||
|
||||
// Update tool execution record
|
||||
await toolExecution.update({
|
||||
output_result: result,
|
||||
status: 'completed',
|
||||
execution_time: executionTime,
|
||||
tokens_used: result.tokensUsed || 0
|
||||
});
|
||||
|
||||
logger.info(`Tool executed: ${toolName} in ${executionTime}s`);
|
||||
|
||||
return toolExecution;
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution error: ${toolName}`, error);
|
||||
|
||||
// Update tool execution record with error
|
||||
if (toolExecution) {
|
||||
await toolExecution.update({
|
||||
status: 'failed',
|
||||
error_message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Tool execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async executeQueryExpander(inputParameters) {
|
||||
const { query, context } = inputParameters;
|
||||
const response = await groqService.expandQuery(query, context);
|
||||
|
||||
return {
|
||||
expandedQuery: response.content,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeOrchestrate(inputParameters) {
|
||||
const { query, category, topK = 5, generateReport = true } = inputParameters;
|
||||
|
||||
// 1) Expand query
|
||||
const expanded = await this.executeQueryExpander({ query, context: { category } });
|
||||
const expandedQuery = (expanded.expandedQuery || '').trim() || query;
|
||||
|
||||
// 2) Extract from RAG using original query (use 'general' category for now)
|
||||
const extraction = await this.executeExtraction({ query: query, category: 'general', topK });
|
||||
|
||||
// 3) If low confidence, augment with web search using original query
|
||||
let web = null;
|
||||
if (extraction.confidence < 0.7) {
|
||||
try {
|
||||
web = await this.executeWebSearch({ query: query, maxResults: 5, searchDepth: 'basic', includeAnswer: true });
|
||||
} catch (e) {
|
||||
// continue without web
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Optionally generate a brief report
|
||||
let report = null;
|
||||
if (generateReport) {
|
||||
// Get full document content for better report generation
|
||||
const documentDetails = await Promise.all(
|
||||
extraction.results.slice(0, 3).map(async (result) => {
|
||||
try {
|
||||
const doc = await Document.findByPk(result.id, {
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'category']
|
||||
});
|
||||
return {
|
||||
filename: result.original_filename,
|
||||
content: doc?.extracted_text || result.snippet,
|
||||
score: result.score,
|
||||
category: result.category
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
filename: result.original_filename,
|
||||
content: result.snippet,
|
||||
score: result.score,
|
||||
category: result.category
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const reportData = {
|
||||
query,
|
||||
expandedQuery,
|
||||
relevantDocuments: documentDetails,
|
||||
webAnswer: web?.answer || null,
|
||||
webCount: web?.totalResults || 0
|
||||
};
|
||||
const reportResp = await groqService.generateReport(reportData, 'summary');
|
||||
report = {
|
||||
content: reportResp.content,
|
||||
tokensUsed: reportResp.usage?.total_tokens,
|
||||
processingTime: reportResp.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
expandedQuery,
|
||||
extraction,
|
||||
web,
|
||||
report,
|
||||
decision: {
|
||||
usedWeb: !!web,
|
||||
confidence: extraction.confidence
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async executeExtraction(inputParameters) {
|
||||
const { query, topK = 5, category } = inputParameters;
|
||||
|
||||
// 1) Try Graph RAG first
|
||||
const graph = await graphRagService.graphSearch({ query, category });
|
||||
let results = graph.results.map((r) => ({
|
||||
id: r.id,
|
||||
original_filename: r.original_filename,
|
||||
snippet: r.snippet,
|
||||
category: r.category,
|
||||
score: r.score,
|
||||
source: 'graph'
|
||||
}));
|
||||
|
||||
// 2) If not enough, fallback to semantic search
|
||||
if (results.length < topK) {
|
||||
const queryEmbedding = await embeddingService.embedText(query);
|
||||
const where = { is_indexed: true };
|
||||
if (category) where.category = category;
|
||||
const docs = await Document.findAll({
|
||||
where,
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'embeddings', 'category']
|
||||
});
|
||||
|
||||
const scored = [];
|
||||
for (const d of docs) {
|
||||
const emb = d.embeddings || [];
|
||||
if (!Array.isArray(emb) || emb.length === 0) continue;
|
||||
const score = embeddingService.cosineSimilarity(queryEmbedding, emb);
|
||||
scored.push({
|
||||
id: d.id,
|
||||
original_filename: d.original_filename,
|
||||
snippet: (d.extracted_text || '').slice(0, 400),
|
||||
category: d.category,
|
||||
score,
|
||||
source: 'semantic'
|
||||
});
|
||||
}
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
const need = topK - results.length;
|
||||
results = results.concat(scored.slice(0, Math.max(0, need)));
|
||||
}
|
||||
|
||||
// Trim to topK and return
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
results = results.slice(0, topK);
|
||||
|
||||
// Confidence heuristic
|
||||
const confidence = results.length > 0 ? Math.min(0.99, Math.max(0.5, results[0].score)) : 0;
|
||||
|
||||
return {
|
||||
query,
|
||||
topK,
|
||||
results,
|
||||
confidence
|
||||
};
|
||||
}
|
||||
|
||||
async executeReport1(inputParameters) {
|
||||
const { data, format, context } = inputParameters;
|
||||
const response = await groqService.generateReport(data, 'technical');
|
||||
|
||||
return {
|
||||
report: response.content,
|
||||
format: format || 'technical',
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeReport2(inputParameters) {
|
||||
const { data, format, filename } = inputParameters;
|
||||
const response = await groqService.generateReport(data, format);
|
||||
|
||||
return {
|
||||
report: response.content,
|
||||
filename: filename || `report_${Date.now()}.txt`,
|
||||
format: format || 'general',
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeWebSearch(inputParameters) {
|
||||
const { query, maxResults = 5, searchDepth = 'basic', includeAnswer = true } = inputParameters;
|
||||
|
||||
const apiKey = process.env.TAVILY_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('TAVILY_API_KEY is not set');
|
||||
}
|
||||
|
||||
const url = 'https://api.tavily.com/search';
|
||||
const payload = {
|
||||
query,
|
||||
search_depth: searchDepth,
|
||||
include_answer: includeAnswer,
|
||||
max_results: Math.min(10, Math.max(1, maxResults))
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await axios.post(url, payload, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }, // ✅ Correct way to pass API key
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
const data = resp.data || {};
|
||||
|
||||
const results = (data.results || []).map((r) => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
snippet: r.content || r.snippet || '',
|
||||
score: r.score ?? undefined,
|
||||
published: r.published_date || undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
query,
|
||||
answer: data.answer || null,
|
||||
results,
|
||||
totalResults: results.length,
|
||||
source: 'tavily',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Tavily web search error:', error?.response?.data || error.message);
|
||||
throw new Error('Web search failed');
|
||||
}
|
||||
}
|
||||
|
||||
async executeEncyclopediaPdf(inputParameters) {
|
||||
const { query, documents, context } = inputParameters;
|
||||
|
||||
// Use our RAG for offline PDF search (category filter could be 'encyclopedia')
|
||||
const graph = await graphRagService.graphSearch({ query, category: 'encyclopedia' });
|
||||
return {
|
||||
query,
|
||||
results: graph.results,
|
||||
totalResults: graph.results.length,
|
||||
source: 'offline_rag'
|
||||
};
|
||||
}
|
||||
|
||||
async getModelStatus() {
|
||||
try {
|
||||
const groqInfo = await groqService.getModelInfo();
|
||||
const connectionTest = await groqService.testConnection();
|
||||
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: connectionTest.success ? 'active' : 'error',
|
||||
groqInfo,
|
||||
connectionTest,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL status check error:', error);
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new QueryModelService();
|
||||
Reference in New Issue
Block a user