first commit
This commit is contained in:
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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