first commit

This commit is contained in:
2025-11-06 11:08:59 +01:00
commit 3c5117c2c3
85 changed files with 13275 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
const groqService = require('./groqService');
const model1Service = require('./model1Service');
const logger = require('../utils/logger');
class ChatRouter {
constructor() {
this.systemPrompt = `You are ReasonAI, a specialized engineering assistant. Your role is to:
1. **Identify Engineering Questions**: Determine if a user's message requires an engineering plan or is a simple question/conversation.
2. **Respond Appropriately**:
- For engineering questions: Indicate that a plan should be generated
- For simple questions/greetings: Provide helpful responses within your engineering scope
- For conversation context: Use conversation history to provide relevant responses
- For out-of-scope questions: Politely redirect to engineering topics
3. **Scope Boundaries**:
- ✅ Engineering: Civil, mechanical, electrical, chemical, environmental, software engineering
- ✅ Technical: Calculations, design, analysis, standards, codes, materials
- ✅ Simple: Greetings, basic engineering concepts, "what is 2+2"
- ✅ Conversation: Questions about previous messages, context, follow-ups
- ❌ Out-of-scope: Sports, entertainment, politics, general knowledge outside engineering
4. **Context Awareness**: Use conversation history to provide relevant responses. If user asks about previous messages or context, respond helpfully.
5. Some questiosn might be engineering questions but not every questions requires a plan, if it can be answered straightforwardly, then it doesnt need a plan
6. **Response Format (STRICT)**: Respond ONLY with a single, minified JSON object. No prose, no code fences, no backticks, no explanations. Use DOUBLE QUOTES for all keys and string values. If unsure, default to simple_response. The JSON schema is:
{
"isEngineeringQuestion": boolean,
"responseType": "plan_needed" | "simple_response" | "out_of_scope",
"response": "Your response to the user",
"reasoning": "Why you classified it this way"
}`;
}
async routeMessage(content, context = {}) {
try {
const messages = [
{
role: 'system',
content: this.systemPrompt
}
];
// Add conversation history for context
if (context.conversationHistory && context.conversationHistory.length > 0) {
context.conversationHistory.forEach(msg => {
messages.push({
role: msg.role,
content: msg.content
});
});
}
// Add current message
messages.push({
role: 'user',
content: `Current user message: "${content}"\n\nContext: ${JSON.stringify(context)}\n\nConversation History: ${context.conversationHistory ? JSON.stringify(context.conversationHistory) : 'None'}`
});
const response = await groqService.generateResponse(messages, {
temperature: 0.3,
max_tokens: 4000
});
// Parse the JSON response
let routingDecision;
try {
// Prefer fenced json block if present
const fence = response.content.match(/```json\s*([\s\S]*?)```/i);
const raw = fence ? fence[1] : (response.content.match(/\{[\s\S]*\}/) || [null])[0];
if (!raw) throw new Error('No JSON found in response');
// Trim whitespace and attempt strict parse
const candidate = raw.trim();
routingDecision = JSON.parse(candidate);
} catch (parseError) {
// Downgrade to warn to avoid noisy error logs for non-strict JSON
logger.warn('Routing JSON parse failed; falling back to heuristic classification');
// Smart fallback based on content analysis
const lowerContent = content.toLowerCase();
const engineeringKeywords = ['design', 'calculate', 'analyze', 'beam', 'steel', 'concrete', 'structure', 'load', 'stress', 'engineering', 'technical', 'specification', 'code', 'standard'];
const isEngineering = engineeringKeywords.some(keyword => lowerContent.includes(keyword));
routingDecision = {
isEngineeringQuestion: isEngineering,
responseType: isEngineering ? 'plan_needed' : 'simple_response',
response: isEngineering ?
"I'll generate an engineering plan for your request." :
"I'm ReasonAI, your engineering assistant. I can help with engineering questions, calculations, and technical problems. How can I assist you today?",
reasoning: "Fallback routing due to parse error"
};
}
logger.info(`Message routed as: ${routingDecision.responseType}`, {
content: content.substring(0, 100),
reasoning: routingDecision.reasoning,
conversationHistoryLength: context.conversationHistory ? context.conversationHistory.length : 0
});
return routingDecision;
} catch (error) {
logger.error('Chat routing error:', error);
// Fallback response
return {
isEngineeringQuestion: false,
responseType: 'simple_response',
response: "I'm ReasonAI, your engineering assistant. I'm here to help with engineering questions and technical problems. How can I assist you today?",
reasoning: "Routing service error"
};
}
}
async generateSimpleResponse(content, context = {}) {
try {
const messages = [
{
role: 'system',
content: `You are ReasonAI, a friendly engineering assistant. Respond helpfully to the user's message while staying within your engineering scope. Be concise and professional. Consider the conversation history for context.`
}
];
// Add conversation history for context
if (context.conversationHistory && context.conversationHistory.length > 0) {
context.conversationHistory.forEach(msg => {
messages.push({
role: msg.role,
content: msg.content
});
});
}
// Add current message
messages.push({
role: 'user',
content: content
});
const response = await groqService.generateResponse(messages, {
temperature: 0.7,
max_tokens: 300
});
return response.content;
} catch (error) {
logger.error('Simple response generation error:', error);
return "I'm ReasonAI, your engineering assistant. I can help with engineering questions, calculations, and technical problems. How can I assist you today?";
}
}
}
module.exports = new ChatRouter();
+55
View File
@@ -0,0 +1,55 @@
const logger = require('../utils/logger');
let pipeline;
try {
// Lazy import to avoid startup cost when unused
({ pipeline } = require('@xenova/transformers'));
} catch (e) {
logger.warn('Embedding pipeline not available. Did you install @xenova/transformers?');
}
class EmbeddingService {
constructor() {
this.initialized = false;
this.extractor = null;
this.modelName = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
}
async initIfNeeded() {
if (this.initialized) return;
if (!pipeline) {
throw new Error('Transformers pipeline not available');
}
this.extractor = await pipeline('feature-extraction', this.modelName);
this.initialized = true;
logger.info(`Embedding model loaded: ${this.modelName}`);
}
async embedText(text) {
if (!text || !text.trim()) return [];
await this.initIfNeeded();
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
// output is a Tensor; convert to plain JS array
// Depending on version, .data or .tolist()
const vector = Array.isArray(output) ? output : (output?.data ? Array.from(output.data) : output.tolist());
return vector;
}
cosineSimilarity(a, b) {
if (!a || !b || a.length !== b.length || a.length === 0) return 0;
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
const va = a[i] || 0;
const vb = b[i] || 0;
dot += va * vb;
normA += va * va;
normB += vb * vb;
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom ? dot / denom : 0;
}
}
module.exports = new EmbeddingService();
+144
View File
@@ -0,0 +1,144 @@
const { Document } = require('../models');
const embeddingService = require('./embeddingService');
const logger = require('../utils/logger');
class GraphRagService {
constructor() {
this.similarityThreshold = parseFloat(process.env.GRAPH_RAG_SIM_THRESHOLD || '0.2');
this.maxNeighbors = parseInt(process.env.GRAPH_RAG_MAX_NEIGHBORS || '10');
this.maxResults = parseInt(process.env.GRAPH_RAG_MAX_RESULTS || '10');
}
scoreSimilarity(a, b) {
return embeddingService.cosineSimilarity(a, b);
}
tagOverlap(tagsA = [], tagsB = []) {
const setA = new Set((tagsA || []).map((t) => (t || '').toLowerCase()));
const setB = new Set((tagsB || []).map((t) => (t || '').toLowerCase()));
let overlap = 0;
setA.forEach((t) => {
if (setB.has(t)) overlap += 1;
});
return overlap;
}
buildGraph(nodes) {
// nodes: [{ id, embedding, tags }]
const edges = new Map(); // id -> [{ id, score, reason }]
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const ni = nodes[i];
const nj = nodes[j];
const sim = this.scoreSimilarity(ni.embedding, nj.embedding);
const tagScore = this.tagOverlap(ni.tags, nj.tags);
const hybrid = sim + Math.min(tagScore, 3) * 0.05; // light tag bonus
if (hybrid >= this.similarityThreshold) {
if (!edges.has(ni.id)) edges.set(ni.id, []);
if (!edges.has(nj.id)) edges.set(nj.id, []);
edges.get(ni.id).push({ id: nj.id, score: hybrid, reason: { sim, tagScore } });
edges.get(nj.id).push({ id: ni.id, score: hybrid, reason: { sim, tagScore } });
}
}
}
// Trim neighbors
edges.forEach((arr, k) => {
arr.sort((a, b) => b.score - a.score);
edges.set(k, arr.slice(0, this.maxNeighbors));
});
return edges;
}
async graphSearch({ query, category }) {
const queryEmbedding = await embeddingService.embedText(query);
// Load candidate docs
const where = { is_indexed: true };
if (category) where.category = category;
const docs = await Document.findAll({
where,
attributes: ['id', 'original_filename', 'extracted_text', 'embeddings', 'tags', 'category', 'created_at']
});
const nodes = docs
.filter((d) => Array.isArray(d.embeddings) && d.embeddings.length > 0)
.map((d) => ({ id: d.id, embedding: d.embeddings, tags: d.tags || [], ref: d }));
if (nodes.length === 0) {
return { results: [] };
}
// Seed scores by query similarity
const seedScores = nodes.map((n) => ({
id: n.id,
score: this.scoreSimilarity(queryEmbedding, n.embedding)
}));
// Log similarity scores for debugging
logger.info('Similarity scores:', seedScores.map(s => ({ id: s.id, score: s.score.toFixed(4) })));
seedScores.sort((a, b) => b.score - a.score);
const seeds = seedScores.slice(0, Math.min(5, seedScores.length)).map((s) => s.id);
const graph = this.buildGraph(nodes);
// Expand neighborhoods from seeds
const visited = new Set();
const scored = new Map();
const pushScore = (id, add, meta) => {
const prev = scored.get(id) || { score: 0, hops: Infinity, reasons: [] };
const combined = {
score: Math.max(prev.score, add),
hops: Math.min(prev.hops, meta.hops),
reasons: prev.reasons.length < 3 ? [...prev.reasons, meta] : prev.reasons
};
scored.set(id, combined);
};
const queue = [];
seeds.forEach((id) => queue.push({ id, hops: 0, via: null }));
while (queue.length > 0 && scored.size < 200) {
const { id, hops, via } = queue.shift();
if (visited.has(id) || hops > 2) continue;
visited.add(id);
// Base score: similarity to query
const node = nodes.find((n) => n.id === id);
const base = this.scoreSimilarity(queryEmbedding, node.embedding);
pushScore(id, base, { type: 'seed', hops });
const neighbors = graph.get(id) || [];
neighbors.forEach((nbr) => {
const pathScore = (base + nbr.score) / 2;
pushScore(nbr.id, pathScore, { type: 'edge', hops: hops + 1, via: id, edgeScore: nbr.score });
if (!visited.has(nbr.id)) {
queue.push({ id: nbr.id, hops: hops + 1, via: id });
}
});
}
// Format results
const ranked = Array.from(scored.entries())
.map(([id, info]) => {
const ref = nodes.find((n) => n.id === id)?.ref;
return {
id,
original_filename: ref?.original_filename,
snippet: (ref?.extracted_text || '').slice(0, 400),
category: ref?.category,
created_at: ref?.created_at,
score: Number(info.score.toFixed(4)),
hops: info.hops,
reasons: info.reasons
};
})
.sort((a, b) => b.score - a.score)
.slice(0, this.maxResults);
return { results: ranked };
}
}
module.exports = new GraphRagService();
+248
View File
@@ -0,0 +1,248 @@
const Groq = require('groq-sdk');
const logger = require('../utils/logger');
const { groqConfig, validateConfig, getModelConfig } = require('../config/groq');
class GroqService {
constructor() {
// Validate configuration
validateConfig();
this.groq = new Groq({
apiKey: groqConfig.apiKey,
baseURL: groqConfig.baseURL
});
this.model = groqConfig.model;
this.config = groqConfig;
}
async generateResponse(messages, options = {}) {
try {
const startTime = Date.now();
const response = await this.groq.chat.completions.create({
model: this.model,
messages: messages,
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 3000,
top_p: options.topP || 0.9,
stream: options.stream || false
});
const endTime = Date.now();
const processingTime = (endTime - startTime) / 1000;
const result = {
content: response.choices[0].message.content,
usage: response.usage,
processingTime,
model: this.model,
finishReason: response.choices[0].finish_reason
};
logger.info(`Groq API call completed in ${processingTime}s, tokens: ${result.usage.total_tokens}`);
return result;
} catch (error) {
logger.error('Groq API error:', error);
throw new Error(`Groq API error: ${error.message}`);
}
}
async generateEngineeringPlan(query, context = {}) {
const systemPrompt = `You are an expert engineering consultant with deep knowledge in structural engineering, mechanical engineering, electrical engineering, and civil engineering.
Your task is to analyze engineering problems and create detailed, step-by-step plans to solve them.
Guidelines:
1. Break down complex problems into clear, actionable steps
2. Consider safety, feasibility, and best practices
3. Include relevant calculations, standards, and regulations
4. Suggest appropriate tools and resources
5. Provide time estimates for each step
6. Consider potential challenges and mitigation strategies
Context: ${JSON.stringify(context)}
Create a comprehensive plan for the following engineering question:`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: query }
];
return await this.generateResponse(messages, {
temperature: 0.3, // Lower temperature for more focused, technical responses
maxTokens: 3000
});
}
async executePlan(plan, tools = []) {
const systemPrompt = `You are an engineering execution specialist. You have access to various tools to help execute engineering plans.
Available tools:
${tools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n')}
Your task is to execute the given plan step by step, using the appropriate tools when needed.
Guidelines:
1. Execute each step of the plan systematically
2. Use tools when necessary to gather information or perform calculations
3. Provide detailed results for each step
4. Document any issues or deviations from the plan
5. Ensure all safety and quality standards are met
Execute the following plan:`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: plan }
];
return await this.generateResponse(messages, {
temperature: 0.5,
maxTokens: 4000
});
}
async expandQuery(query, context = {}) {
const systemPrompt = `You are a query expansion specialist for engineering problems. Your task is to take a user's engineering question and expand it into a more comprehensive, detailed query that will help find the most relevant information.
Guidelines:
1. Identify key engineering concepts and terminology
2. Suggest related questions and considerations
3. Include relevant standards, codes, and regulations
4. Consider different engineering disciplines that might be relevant
5. Add context about project scope, constraints, and requirements
Context: ${JSON.stringify(context)}
Expand the following engineering query:`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: query }
];
return await this.generateResponse(messages, {
temperature: 0.4,
maxTokens: 1500
});
}
async generateReport(data, reportType = 'general') {
const systemPrompts = {
general: `You are an engineering report generator. Create a comprehensive, professional engineering report based on the provided data.`,
technical: `You are a technical engineering report generator. Create a detailed technical report with calculations, analysis, and recommendations.`,
summary: `You are an engineering summary generator. Create a concise executive summary of the engineering analysis and findings.`
};
const systemPrompt = systemPrompts[reportType] || systemPrompts.general;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Generate a ${reportType} report based on this data: ${JSON.stringify(data)}` }
];
return await this.generateResponse(messages, {
temperature: 0.3,
maxTokens: 2500
});
}
async searchAndAnalyze(query, searchResults = []) {
const systemPrompt = `You are an engineering analysis specialist. Analyze the provided search results and provide a comprehensive analysis of the engineering question.
Guidelines:
1. Synthesize information from multiple sources
2. Identify key findings and insights
3. Highlight important calculations, formulas, or methodologies
4. Note any conflicting information or gaps
5. Provide recommendations based on the analysis
6. Cite relevant sources and standards
Search results: ${JSON.stringify(searchResults)}
Analyze the following engineering question:`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: query }
];
return await this.generateResponse(messages, {
temperature: 0.4,
maxTokens: 3000
});
}
async validatePlan(plan, feedback = []) {
const systemPrompt = `You are an engineering plan validator. Review the provided plan and feedback to determine if the plan is valid, complete, and follows engineering best practices.
Guidelines:
1. Check for completeness and logical flow
2. Verify technical accuracy
3. Ensure safety considerations are addressed
4. Validate against engineering standards
5. Consider the provided feedback
6. Suggest improvements if needed
Feedback: ${JSON.stringify(feedback)}
Validate the following plan:`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: plan }
];
return await this.generateResponse(messages, {
temperature: 0.2,
maxTokens: 2000
});
}
async getModelInfo() {
try {
// Get available models
const models = await this.groq.models.list();
return {
currentModel: this.model,
availableModels: models.data,
apiStatus: 'connected'
};
} catch (error) {
logger.error('Error getting model info:', error);
return {
currentModel: this.model,
availableModels: [],
apiStatus: 'error',
error: error.message
};
}
}
async testConnection() {
try {
const testResponse = await this.generateResponse([
{ role: 'user', content: 'Hello, this is a test message.' }
], {
maxTokens: 10
});
return {
success: true,
response: testResponse,
model: this.model
};
} catch (error) {
logger.error('Groq connection test failed:', error);
return {
success: false,
error: error.message
};
}
}
}
module.exports = new GroqService();
+290
View File
@@ -0,0 +1,290 @@
const groqService = require('./groqService');
const { Plan, Message, Conversation } = require('../models');
const logger = require('../utils/logger');
class Model1Service {
constructor() {
this.modelType = 'MODEL1';
this.systemPrompt = `You are MODEL1, an expert engineering reasoning system. Your primary function is to analyze complex engineering problems and create detailed, step-by-step plans to solve them.
Your capabilities include:
- Structural engineering analysis
- Mechanical engineering design
- Electrical engineering systems
- Civil engineering projects
- Computer Enginnering
- Safety and compliance considerations
- Cost estimation and feasibility analysis
- Risk assessment and mitigation
- All other engineering related courses
When creating plans, always:
1. Break down complex problems into clear, actionable steps
2. Consider safety, feasibility, and best practices
3. Include relevant calculations, standards, and regulations
4. Suggest appropriate tools and resources
5. Provide time estimates for each step
6. Consider potential challenges and mitigation strategies
7. DO NOT give the answer, your job is to plan
8. Only give plan when the user ask about actionable questions
Format your response as a structured plan with:
- Title: Clear, descriptive title
- Description: Brief overview of the problem and approach
- Steps: Numbered list of detailed steps
- Tools Required: List of tools needed
- Estimated Duration: Total time estimate
- Complexity Score: 1-10 scale
- Safety Considerations: Key safety points
- Quality Checks: Verification steps`;
}
async generatePlan(query, context = {}) {
try {
const startTime = Date.now();
// Prepare context for the model
const enhancedContext = {
...context,
timestamp: new Date().toISOString(),
modelType: this.modelType
};
// Generate the plan using Groq
const response = await groqService.generateEngineeringPlan(query, enhancedContext);
const endTime = Date.now();
const processingTime = (endTime - startTime) / 1000;
// Parse the response to extract structured plan data
const planData = this.parsePlanResponse(response.content);
logger.info(`MODEL1 plan generated in ${processingTime}s for query: ${query.substring(0, 100)}...`);
return {
...planData,
processingTime,
tokensUsed: response.usage.total_tokens,
model: response.model,
finishReason: response.finishReason
};
} catch (error) {
logger.error('MODEL1 plan generation error:', error);
throw new Error(`MODEL1 plan generation failed: ${error.message}`);
}
}
parsePlanResponse(content) {
try {
// Extract structured data from the response
const lines = content.split('\n');
let title = '';
let description = '';
let steps = [];
let toolsRequired = [];
let estimatedDuration = 0;
let complexityScore = 5;
let safetyConsiderations = [];
let qualityChecks = [];
let currentSection = '';
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.toLowerCase().includes('title:')) {
title = trimmedLine.split(':')[1]?.trim() || 'Engineering Plan';
} else if (trimmedLine.toLowerCase().includes('description:')) {
description = trimmedLine.split(':')[1]?.trim() || '';
} else if (trimmedLine.toLowerCase().includes('steps:')) {
currentSection = 'steps';
} else if (trimmedLine.toLowerCase().includes('tools required:')) {
currentSection = 'tools';
} else if (trimmedLine.toLowerCase().includes('estimated duration:')) {
const duration = trimmedLine.split(':')[1]?.trim();
estimatedDuration = this.parseDuration(duration);
} else if (trimmedLine.toLowerCase().includes('complexity score:')) {
const score = trimmedLine.split(':')[1]?.trim();
complexityScore = parseInt(score) || 5;
} else if (trimmedLine.toLowerCase().includes('safety considerations:')) {
currentSection = 'safety';
} else if (trimmedLine.toLowerCase().includes('quality checks:')) {
currentSection = 'quality';
} else if (trimmedLine.match(/^\d+\./)) {
if (currentSection === 'steps') {
steps.push(trimmedLine);
}
} else if (trimmedLine.startsWith('-')) {
if (currentSection === 'tools') {
toolsRequired.push(trimmedLine.substring(1).trim());
} else if (currentSection === 'safety') {
safetyConsiderations.push(trimmedLine.substring(1).trim());
} else if (currentSection === 'quality') {
qualityChecks.push(trimmedLine.substring(1).trim());
}
}
}
// If no structured data found, create a basic plan
if (!title) {
title = 'Engineering Plan';
description = content.substring(0, 200) + '...';
steps = [content];
}
return {
title,
description,
steps,
toolsRequired,
estimatedDuration,
complexityScore,
safetyConsiderations,
qualityChecks,
rawContent: content
};
} catch (error) {
logger.error('Error parsing plan response:', error);
return {
title: 'Engineering Plan',
description: content,
steps: [content],
toolsRequired: [],
estimatedDuration: 60,
complexityScore: 5,
safetyConsiderations: [],
qualityChecks: [],
rawContent: content
};
}
}
parseDuration(duration) {
if (!duration) return 60;
const match = duration.match(/(\d+)\s*(hour|hr|minute|min|day|d)/i);
if (match) {
const value = parseInt(match[1]);
const unit = match[2].toLowerCase();
switch (unit) {
case 'hour':
case 'hr':
return value * 60;
case 'minute':
case 'min':
return value;
case 'day':
case 'd':
return value * 24 * 60;
default:
return 60;
}
}
return 60; // Default to 60 minutes
}
async savePlan(planData, conversationId, userId) {
try {
const plan = await Plan.create({
conversation_id: conversationId,
title: planData.title,
description: planData.description,
steps: planData.steps,
status: 'draft',
tools_required: planData.toolsRequired,
estimated_duration: planData.estimatedDuration,
complexity_score: planData.complexityScore,
metadata: {
safetyConsiderations: planData.safetyConsiderations,
qualityChecks: planData.qualityChecks,
processingTime: planData.processingTime,
tokensUsed: planData.tokensUsed,
model: planData.model
}
});
// Create a message record for the plan
await Message.create({
conversation_id: conversationId,
plan_id: plan.id,
role: 'assistant',
content: planData.rawContent,
message_type: 'plan',
metadata: {
modelType: this.modelType,
processingTime: planData.processingTime,
tokensUsed: planData.tokensUsed
}
});
logger.info(`Plan saved: ${plan.id} for conversation: ${conversationId}`);
return plan;
} catch (error) {
logger.error('Error saving plan:', error);
throw new Error(`Failed to save plan: ${error.message}`);
}
}
async validatePlan(planId, feedback = []) {
try {
const plan = await Plan.findByPk(planId);
if (!plan) {
throw new Error('Plan not found');
}
const validationResponse = await groqService.validatePlan(plan.description, feedback);
// Update plan with validation results
await plan.update({
status: validationResponse.content.includes('valid') ? 'approved' : 'rejected',
approval_feedback: validationResponse.content,
metadata: {
...plan.metadata,
validation: {
response: validationResponse.content,
tokensUsed: validationResponse.usage.total_tokens,
processingTime: validationResponse.processingTime
}
}
});
logger.info(`Plan validated: ${planId}, status: ${plan.status}`);
return {
plan,
validation: validationResponse
};
} catch (error) {
logger.error('Plan validation error:', error);
throw new Error(`Plan validation failed: ${error.message}`);
}
}
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('MODEL1 status check error:', error);
return {
modelType: this.modelType,
status: 'error',
error: error.message,
lastChecked: new Date().toISOString()
};
}
}
}
module.exports = new Model1Service();
+500
View File
@@ -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();