- Implement Autonomous Workflow Engine with dynamic task decomposition - Add Multi-Agent Communication Protocol with message routing - Create Enhanced Reasoning Chains (CoT, ToT, Multi-Step, Parallel, Hybrid) - Add comprehensive REST API endpoints for all Week 5 features - Include 26/26 passing tests with full coverage - Add complete documentation and API guides - Update development plan to mark Week 5 as completed Features: - Dynamic task decomposition and parallel execution - Agent registration, messaging, and coordination - 5 reasoning methods with validation and learning - Robust error handling and monitoring - Multi-tenant support and security - Production-ready architecture Files added/modified: - app/services/autonomous_workflow_engine.py - app/services/agent_communication.py - app/services/enhanced_reasoning.py - app/api/v1/endpoints/week5_features.py - tests/test_week5_features.py - docs/week5_api_documentation.md - docs/week5_readme.md - WEEK5_COMPLETION_SUMMARY.md - DEVELOPMENT_PLAN.md (updated) All tests passing: 26/26
1039 lines
38 KiB
Python
1039 lines
38 KiB
Python
"""
|
|
Agentic RAG Service - State-of-the-art autonomous agent-based retrieval and reasoning.
|
|
Implements multi-agent orchestration, advanced reasoning chains, and autonomous workflows.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from app.services.vector_service import VectorService
|
|
from app.services.llm_service import llm_service
|
|
from app.core.cache import cache_service
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AgentType(Enum):
|
|
"""Types of specialized agents in the system."""
|
|
RESEARCH = "research"
|
|
ANALYSIS = "analysis"
|
|
SYNTHESIS = "synthesis"
|
|
VALIDATION = "validation"
|
|
PLANNING = "planning"
|
|
EXECUTION = "execution"
|
|
|
|
|
|
class ReasoningType(Enum):
|
|
"""Types of reasoning approaches."""
|
|
CHAIN_OF_THOUGHT = "chain_of_thought"
|
|
TREE_OF_THOUGHTS = "tree_of_thoughts"
|
|
MULTI_STEP = "multi_step"
|
|
PARALLEL = "parallel"
|
|
|
|
|
|
@dataclass
|
|
class AgentTask:
|
|
"""Represents a task assigned to an agent."""
|
|
id: str
|
|
agent_type: AgentType
|
|
description: str
|
|
input_data: Dict[str, Any]
|
|
dependencies: List[str]
|
|
priority: int
|
|
created_at: datetime
|
|
status: str = "pending"
|
|
result: Optional[Dict[str, Any]] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class ReasoningStep:
|
|
"""Represents a step in a reasoning chain."""
|
|
id: str
|
|
step_type: str
|
|
description: str
|
|
input: Dict[str, Any]
|
|
output: Optional[Dict[str, Any]] = None
|
|
confidence: float = 0.0
|
|
validation_status: str = "pending"
|
|
|
|
|
|
class Agent:
|
|
"""Base class for all agents in the system."""
|
|
|
|
def __init__(self, agent_type: AgentType, agent_id: str):
|
|
self.agent_type = agent_type
|
|
self.agent_id = agent_id
|
|
self.memory = {}
|
|
self.learning_history = []
|
|
|
|
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
"""Execute a task and return results."""
|
|
raise NotImplementedError
|
|
|
|
async def learn(self, feedback: Dict[str, Any]) -> None:
|
|
"""Learn from feedback to improve future performance."""
|
|
self.learning_history.append({
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"feedback": feedback
|
|
})
|
|
|
|
def get_memory(self) -> Dict[str, Any]:
|
|
"""Get agent's current memory state."""
|
|
return self.memory.copy()
|
|
|
|
def update_memory(self, key: str, value: Any) -> None:
|
|
"""Update agent's memory."""
|
|
self.memory[key] = value
|
|
|
|
|
|
class ResearchAgent(Agent):
|
|
"""Specialized agent for information retrieval and research."""
|
|
|
|
def __init__(self, vector_service: VectorService):
|
|
super().__init__(AgentType.RESEARCH, f"research_{uuid.uuid4().hex[:8]}")
|
|
self.vector_service = vector_service
|
|
|
|
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
"""Execute research task with autonomous retrieval decisions."""
|
|
try:
|
|
query = task.input_data.get("query", "")
|
|
context = task.input_data.get("context", {})
|
|
|
|
# Autonomous decision making for retrieval strategy
|
|
retrieval_strategy = await self._determine_retrieval_strategy(query, context)
|
|
|
|
# Execute retrieval based on strategy
|
|
if retrieval_strategy == "semantic":
|
|
results = await self._semantic_retrieval(query, context)
|
|
elif retrieval_strategy == "hybrid":
|
|
results = await self._hybrid_retrieval(query, context)
|
|
elif retrieval_strategy == "structured":
|
|
results = await self._structured_retrieval(query, context)
|
|
else:
|
|
results = await self._multi_modal_retrieval(query, context)
|
|
|
|
# Autonomous filtering and ranking
|
|
filtered_results = await self._autonomous_filtering(results, query)
|
|
|
|
# Update memory with retrieval patterns
|
|
self.update_memory("last_retrieval_strategy", retrieval_strategy)
|
|
self.update_memory("retrieval_patterns", self.memory.get("retrieval_patterns", []) + [{
|
|
"query": query,
|
|
"strategy": retrieval_strategy,
|
|
"results_count": len(filtered_results)
|
|
}])
|
|
|
|
return {
|
|
"status": "success",
|
|
"results": filtered_results,
|
|
"strategy_used": retrieval_strategy,
|
|
"confidence": self._calculate_confidence(filtered_results),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Research agent execution failed: {e}")
|
|
return {
|
|
"status": "error",
|
|
"error": str(e),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
async def _determine_retrieval_strategy(self, query: str, context: Dict[str, Any]) -> str:
|
|
"""Autonomously determine the best retrieval strategy."""
|
|
# Analyze query characteristics
|
|
query_analysis = await self._analyze_query(query)
|
|
|
|
# Consider context and history
|
|
historical_patterns = self.memory.get("retrieval_patterns", [])
|
|
|
|
# Make autonomous decision
|
|
if query_analysis.get("has_structured_terms", False):
|
|
return "structured"
|
|
elif query_analysis.get("complexity", "low") == "high":
|
|
return "hybrid"
|
|
elif query_analysis.get("modality", "text") == "multi_modal":
|
|
return "multi_modal"
|
|
else:
|
|
return "semantic"
|
|
|
|
async def _analyze_query(self, query: str) -> Dict[str, Any]:
|
|
"""Analyze query characteristics for strategy selection."""
|
|
analysis_prompt = f"""
|
|
Analyze the following query and determine its characteristics:
|
|
Query: {query}
|
|
|
|
Return a JSON object with:
|
|
- complexity: "low", "medium", "high"
|
|
- modality: "text", "structured", "multi_modal"
|
|
- has_structured_terms: boolean
|
|
- requires_context: boolean
|
|
- estimated_retrieval_count: number
|
|
"""
|
|
|
|
try:
|
|
response = await llm_service.generate_text(
|
|
analysis_prompt,
|
|
tenant_id=task.input_data.get("tenant_id", "default"),
|
|
task="classification",
|
|
temperature=0.1
|
|
)
|
|
|
|
return json.loads(response.get("text", "{}"))
|
|
except Exception:
|
|
# Fallback analysis
|
|
return {
|
|
"complexity": "medium",
|
|
"modality": "text",
|
|
"has_structured_terms": False,
|
|
"requires_context": True,
|
|
"estimated_retrieval_count": 10
|
|
}
|
|
|
|
async def _semantic_retrieval(self, query: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""Perform semantic retrieval."""
|
|
tenant_id = context.get("tenant_id", "default")
|
|
return await self.vector_service.search_similar(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
limit=15
|
|
)
|
|
|
|
async def _hybrid_retrieval(self, query: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""Perform hybrid retrieval combining semantic and keyword search."""
|
|
tenant_id = context.get("tenant_id", "default")
|
|
return await self.vector_service.hybrid_search(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
limit=15
|
|
)
|
|
|
|
async def _structured_retrieval(self, query: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""Perform structured data retrieval."""
|
|
tenant_id = context.get("tenant_id", "default")
|
|
return await self.vector_service.search_structured_data(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
data_type="table",
|
|
limit=15
|
|
)
|
|
|
|
async def _multi_modal_retrieval(self, query: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""Perform multi-modal retrieval across different content types."""
|
|
tenant_id = context.get("tenant_id", "default")
|
|
|
|
# Retrieve from different modalities
|
|
text_results = await self.vector_service.search_similar(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
limit=8,
|
|
chunk_types=["text"]
|
|
)
|
|
|
|
table_results = await self.vector_service.search_structured_data(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
data_type="table",
|
|
limit=4
|
|
)
|
|
|
|
chart_results = await self.vector_service.search_structured_data(
|
|
tenant_id=tenant_id,
|
|
query=query,
|
|
data_type="chart",
|
|
limit=3
|
|
)
|
|
|
|
# Combine and rank results
|
|
all_results = text_results + table_results + chart_results
|
|
return sorted(all_results, key=lambda x: x.get("score", 0), reverse=True)
|
|
|
|
async def _autonomous_filtering(self, results: List[Dict[str, Any]], query: str) -> List[Dict[str, Any]]:
|
|
"""Autonomously filter and rank results."""
|
|
if not results:
|
|
return []
|
|
|
|
# Use LLM to evaluate relevance
|
|
evaluation_prompt = f"""
|
|
Evaluate the relevance of each result to the query:
|
|
Query: {query}
|
|
|
|
Results:
|
|
{json.dumps(results[:10], indent=2)}
|
|
|
|
For each result, provide a relevance score (0-1) and brief reasoning.
|
|
Return as JSON array with format: [{{"id": "result_id", "relevance_score": 0.8, "reasoning": "..."}}]
|
|
"""
|
|
|
|
try:
|
|
response = await llm_service.generate_text(
|
|
evaluation_prompt,
|
|
tenant_id="default",
|
|
task="analysis",
|
|
temperature=0.1
|
|
)
|
|
|
|
evaluations = json.loads(response.get("text", "[]"))
|
|
|
|
# Apply evaluations to results
|
|
for result in results:
|
|
for eval_item in evaluations:
|
|
if eval_item.get("id") == result.get("id"):
|
|
result["llm_relevance_score"] = eval_item.get("relevance_score", 0.5)
|
|
result["llm_reasoning"] = eval_item.get("reasoning", "")
|
|
break
|
|
|
|
# Filter by relevance threshold and re-rank
|
|
filtered_results = [
|
|
r for r in results
|
|
if r.get("llm_relevance_score", 0.5) > 0.3
|
|
]
|
|
|
|
return sorted(filtered_results, key=lambda x: x.get("llm_relevance_score", 0), reverse=True)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Autonomous filtering failed, using original results: {e}")
|
|
return results
|
|
|
|
def _calculate_confidence(self, results: List[Dict[str, Any]]) -> float:
|
|
"""Calculate confidence in retrieval results."""
|
|
if not results:
|
|
return 0.0
|
|
|
|
# Consider multiple factors
|
|
avg_score = sum(r.get("score", 0) for r in results) / len(results)
|
|
avg_llm_score = sum(r.get("llm_relevance_score", 0.5) for r in results) / len(results)
|
|
result_count = len(results)
|
|
|
|
# Weighted confidence calculation
|
|
confidence = (avg_score * 0.4 + avg_llm_score * 0.4 + min(result_count / 10, 1.0) * 0.2)
|
|
return min(confidence, 1.0)
|
|
|
|
|
|
class AnalysisAgent(Agent):
|
|
"""Specialized agent for analysis and reasoning."""
|
|
|
|
def __init__(self):
|
|
super().__init__(AgentType.ANALYSIS, f"analysis_{uuid.uuid4().hex[:8]}")
|
|
|
|
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
"""Execute analysis task with advanced reasoning."""
|
|
try:
|
|
query = task.input_data.get("query", "")
|
|
retrieved_data = task.input_data.get("retrieved_data", [])
|
|
reasoning_type = task.input_data.get("reasoning_type", ReasoningType.CHAIN_OF_THOUGHT)
|
|
|
|
# Choose reasoning approach
|
|
if reasoning_type == ReasoningType.TREE_OF_THOUGHTS:
|
|
analysis_result = await self._tree_of_thoughts_analysis(query, retrieved_data)
|
|
elif reasoning_type == ReasoningType.MULTI_STEP:
|
|
analysis_result = await self._multi_step_analysis(query, retrieved_data)
|
|
else:
|
|
analysis_result = await self._chain_of_thought_analysis(query, retrieved_data)
|
|
|
|
# Validate analysis
|
|
validation_result = await self._validate_analysis(analysis_result, query, retrieved_data)
|
|
|
|
return {
|
|
"status": "success",
|
|
"analysis": analysis_result,
|
|
"validation": validation_result,
|
|
"reasoning_type": reasoning_type.value,
|
|
"confidence": validation_result.get("confidence", 0.0),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Analysis agent execution failed: {e}")
|
|
return {
|
|
"status": "error",
|
|
"error": str(e),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
async def _chain_of_thought_analysis(self, query: str, data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""Perform chain of thought analysis."""
|
|
context = self._format_context(data)
|
|
|
|
analysis_prompt = f"""
|
|
Analyze the following information step by step to answer the query:
|
|
|
|
Query: {query}
|
|
|
|
Context:
|
|
{context}
|
|
|
|
Please provide your analysis in the following format:
|
|
1. Key Findings: [List main findings]
|
|
2. Reasoning: [Step-by-step reasoning]
|
|
3. Conclusions: [Final conclusions]
|
|
4. Confidence: [0-1 score]
|
|
5. Limitations: [Any limitations or uncertainties]
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
analysis_prompt,
|
|
tenant_id="default",
|
|
task="analysis",
|
|
temperature=0.3
|
|
)
|
|
|
|
return {
|
|
"method": "chain_of_thought",
|
|
"analysis": response.get("text", ""),
|
|
"steps": self._extract_reasoning_steps(response.get("text", ""))
|
|
}
|
|
|
|
async def _tree_of_thoughts_analysis(self, query: str, data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""Perform tree of thoughts analysis with multiple reasoning paths."""
|
|
context = self._format_context(data)
|
|
|
|
# Generate multiple reasoning paths
|
|
paths_prompt = f"""
|
|
For the query: "{query}"
|
|
|
|
Context: {context}
|
|
|
|
Generate 3 different reasoning approaches to analyze this information.
|
|
Each approach should be distinct and explore different aspects.
|
|
|
|
Return as JSON:
|
|
{{
|
|
"paths": [
|
|
{{
|
|
"approach": "description",
|
|
"focus": "what this path focuses on",
|
|
"reasoning": "step-by-step reasoning"
|
|
}}
|
|
]
|
|
}}
|
|
"""
|
|
|
|
paths_response = await llm_service.generate_text(
|
|
paths_prompt,
|
|
tenant_id="default",
|
|
task="analysis",
|
|
temperature=0.7
|
|
)
|
|
|
|
try:
|
|
paths_data = json.loads(paths_response.get("text", "{}"))
|
|
paths = paths_data.get("paths", [])
|
|
|
|
# Evaluate each path
|
|
evaluated_paths = []
|
|
for path in paths:
|
|
evaluation = await self._evaluate_reasoning_path(path, query, context)
|
|
evaluated_paths.append({
|
|
**path,
|
|
"evaluation": evaluation
|
|
})
|
|
|
|
# Synthesize best insights from all paths
|
|
synthesis = await self._synthesize_paths(evaluated_paths, query)
|
|
|
|
return {
|
|
"method": "tree_of_thoughts",
|
|
"paths": evaluated_paths,
|
|
"synthesis": synthesis,
|
|
"best_path": max(evaluated_paths, key=lambda x: x["evaluation"].get("score", 0))
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Tree of thoughts failed, falling back to CoT: {e}")
|
|
return await self._chain_of_thought_analysis(query, data)
|
|
|
|
async def _multi_step_analysis(self, query: str, data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""Perform multi-step analysis with validation at each step."""
|
|
context = self._format_context(data)
|
|
|
|
steps = [
|
|
("extract_key_information", "Extract key information from the context"),
|
|
("identify_patterns", "Identify patterns and relationships"),
|
|
("analyze_implications", "Analyze implications and consequences"),
|
|
("evaluate_evidence", "Evaluate the strength of evidence"),
|
|
("form_conclusions", "Form conclusions and recommendations")
|
|
]
|
|
|
|
analysis_steps = []
|
|
current_context = context
|
|
|
|
for step_id, step_description in steps:
|
|
step_prompt = f"""
|
|
Step: {step_description}
|
|
|
|
Query: {query}
|
|
Current Context: {current_context}
|
|
|
|
Previous Steps: {json.dumps(analysis_steps, indent=2)}
|
|
|
|
Perform this step and provide:
|
|
1. Analysis: [Your analysis for this step]
|
|
2. Updated Context: [Any new information or insights]
|
|
3. Confidence: [0-1 score for this step]
|
|
"""
|
|
|
|
step_response = await llm_service.generate_text(
|
|
step_prompt,
|
|
tenant_id="default",
|
|
task="analysis",
|
|
temperature=0.3
|
|
)
|
|
|
|
step_result = {
|
|
"step_id": step_id,
|
|
"description": step_description,
|
|
"analysis": step_response.get("text", ""),
|
|
"confidence": 0.7 # Default confidence
|
|
}
|
|
|
|
analysis_steps.append(step_result)
|
|
|
|
# Update context for next step
|
|
current_context += f"\n\nStep {step_id} Analysis: {step_response.get('text', '')}"
|
|
|
|
# Final synthesis
|
|
synthesis = await self._synthesize_multi_step(analysis_steps, query)
|
|
|
|
return {
|
|
"method": "multi_step",
|
|
"steps": analysis_steps,
|
|
"synthesis": synthesis,
|
|
"overall_confidence": sum(s.get("confidence", 0) for s in analysis_steps) / len(analysis_steps)
|
|
}
|
|
|
|
async def _evaluate_reasoning_path(self, path: Dict[str, Any], query: str, context: str) -> Dict[str, Any]:
|
|
"""Evaluate the quality of a reasoning path."""
|
|
evaluation_prompt = f"""
|
|
Evaluate this reasoning approach:
|
|
|
|
Query: {query}
|
|
Approach: {path.get('approach', '')}
|
|
Reasoning: {path.get('reasoning', '')}
|
|
|
|
Rate on a scale of 0-1:
|
|
- Logical coherence
|
|
- Relevance to query
|
|
- Completeness
|
|
- Novelty of insights
|
|
|
|
Return as JSON: {{"score": 0.8, "coherence": 0.9, "relevance": 0.8, "completeness": 0.7, "novelty": 0.6}}
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
evaluation_prompt,
|
|
tenant_id="default",
|
|
task="analysis",
|
|
temperature=0.1
|
|
)
|
|
|
|
try:
|
|
return json.loads(response.get("text", "{}"))
|
|
except Exception:
|
|
return {"score": 0.5, "coherence": 0.5, "relevance": 0.5, "completeness": 0.5, "novelty": 0.5}
|
|
|
|
async def _synthesize_paths(self, paths: List[Dict[str, Any]], query: str) -> Dict[str, Any]:
|
|
"""Synthesize insights from multiple reasoning paths."""
|
|
synthesis_prompt = f"""
|
|
Synthesize insights from multiple reasoning approaches:
|
|
|
|
Query: {query}
|
|
|
|
Approaches:
|
|
{json.dumps(paths, indent=2)}
|
|
|
|
Provide a comprehensive synthesis that combines the best insights from all approaches.
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
synthesis_prompt,
|
|
tenant_id="default",
|
|
task="synthesis",
|
|
temperature=0.3
|
|
)
|
|
|
|
return {
|
|
"synthesis": response.get("text", ""),
|
|
"contributing_paths": [p["approach"] for p in paths if p["evaluation"].get("score", 0) > 0.6]
|
|
}
|
|
|
|
async def _synthesize_multi_step(self, steps: List[Dict[str, Any]], query: str) -> Dict[str, Any]:
|
|
"""Synthesize results from multi-step analysis."""
|
|
synthesis_prompt = f"""
|
|
Synthesize the results from multi-step analysis:
|
|
|
|
Query: {query}
|
|
|
|
Steps:
|
|
{json.dumps(steps, indent=2)}
|
|
|
|
Provide a comprehensive synthesis of all steps.
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
synthesis_prompt,
|
|
tenant_id="default",
|
|
task="synthesis",
|
|
temperature=0.3
|
|
)
|
|
|
|
return {
|
|
"synthesis": response.get("text", ""),
|
|
"key_insights": [s["analysis"] for s in steps if s.get("confidence", 0) > 0.7]
|
|
}
|
|
|
|
async def _validate_analysis(self, analysis: Dict[str, Any], query: str, data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""Validate the analysis results."""
|
|
validation_prompt = f"""
|
|
Validate this analysis:
|
|
|
|
Query: {query}
|
|
Analysis: {json.dumps(analysis, indent=2)}
|
|
|
|
Check for:
|
|
1. Logical consistency
|
|
2. Evidence support
|
|
3. Completeness
|
|
4. Relevance to query
|
|
|
|
Return validation results as JSON.
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
validation_prompt,
|
|
tenant_id="default",
|
|
task="validation",
|
|
temperature=0.1
|
|
)
|
|
|
|
try:
|
|
return json.loads(response.get("text", "{}"))
|
|
except Exception:
|
|
return {"confidence": 0.7, "issues": [], "validation_status": "partial"}
|
|
|
|
def _format_context(self, data: List[Dict[str, Any]]) -> str:
|
|
"""Format retrieved data as context."""
|
|
context_lines = []
|
|
for item in data[:10]: # Limit to top 10 results
|
|
meta = f"doc={item.get('document_id','?')} pages={item.get('page_numbers',[])} type={item.get('chunk_type','?')}"
|
|
text = item.get("text", "").strip()
|
|
if text:
|
|
context_lines.append(f"[{meta}] {text}")
|
|
return "\n\n".join(context_lines)
|
|
|
|
def _extract_reasoning_steps(self, analysis: str) -> List[str]:
|
|
"""Extract reasoning steps from analysis text."""
|
|
# Simple extraction - in production, use more sophisticated parsing
|
|
lines = analysis.split('\n')
|
|
steps = []
|
|
for line in lines:
|
|
if line.strip().startswith(('1.', '2.', '3.', '4.', '5.')):
|
|
steps.append(line.strip())
|
|
return steps
|
|
|
|
|
|
class SynthesisAgent(Agent):
|
|
"""Specialized agent for synthesizing and generating final responses."""
|
|
|
|
def __init__(self):
|
|
super().__init__(AgentType.SYNTHESIS, f"synthesis_{uuid.uuid4().hex[:8]}")
|
|
|
|
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
"""Execute synthesis task to generate final response."""
|
|
try:
|
|
query = task.input_data.get("query", "")
|
|
research_results = task.input_data.get("research_results", {})
|
|
analysis_results = task.input_data.get("analysis_results", {})
|
|
context = task.input_data.get("context", {})
|
|
|
|
# Synthesize all information
|
|
synthesis = await self._synthesize_information(
|
|
query, research_results, analysis_results, context
|
|
)
|
|
|
|
# Generate final response
|
|
final_response = await self._generate_response(query, synthesis, context)
|
|
|
|
# Add citations and metadata
|
|
response_with_metadata = await self._add_metadata(final_response, research_results, analysis_results)
|
|
|
|
return {
|
|
"status": "success",
|
|
"response": response_with_metadata,
|
|
"synthesis": synthesis,
|
|
"confidence": synthesis.get("confidence", 0.0),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Synthesis agent execution failed: {e}")
|
|
return {
|
|
"status": "error",
|
|
"error": str(e),
|
|
"metadata": {
|
|
"agent_id": self.agent_id,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
}
|
|
}
|
|
|
|
async def _synthesize_information(
|
|
self,
|
|
query: str,
|
|
research_results: Dict[str, Any],
|
|
analysis_results: Dict[str, Any],
|
|
context: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Synthesize information from research and analysis."""
|
|
synthesis_prompt = f"""
|
|
Synthesize the following information into a comprehensive response:
|
|
|
|
Query: {query}
|
|
|
|
Research Results:
|
|
{json.dumps(research_results, indent=2)}
|
|
|
|
Analysis Results:
|
|
{json.dumps(analysis_results, indent=2)}
|
|
|
|
Context: {json.dumps(context, indent=2)}
|
|
|
|
Create a synthesis that:
|
|
1. Addresses the query directly
|
|
2. Incorporates key insights from research
|
|
3. Uses analysis to provide reasoning
|
|
4. Maintains accuracy and relevance
|
|
5. Provides actionable insights where applicable
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
synthesis_prompt,
|
|
tenant_id=context.get("tenant_id", "default"),
|
|
task="synthesis",
|
|
temperature=0.3
|
|
)
|
|
|
|
return {
|
|
"synthesis": response.get("text", ""),
|
|
"confidence": self._calculate_synthesis_confidence(research_results, analysis_results),
|
|
"key_insights": self._extract_key_insights(response.get("text", ""))
|
|
}
|
|
|
|
async def _generate_response(self, query: str, synthesis: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
"""Generate the final response."""
|
|
response_prompt = f"""
|
|
Generate a final response to the query based on the synthesis:
|
|
|
|
Query: {query}
|
|
Synthesis: {synthesis.get('synthesis', '')}
|
|
|
|
Requirements:
|
|
1. Be direct and concise
|
|
2. Use clear, professional language
|
|
3. Include relevant citations
|
|
4. Provide actionable insights where applicable
|
|
5. Acknowledge any limitations or uncertainties
|
|
"""
|
|
|
|
response = await llm_service.generate_text(
|
|
response_prompt,
|
|
tenant_id=context.get("tenant_id", "default"),
|
|
task="synthesis",
|
|
temperature=0.2
|
|
)
|
|
|
|
return response.get("text", "")
|
|
|
|
async def _add_metadata(
|
|
self,
|
|
response: str,
|
|
research_results: Dict[str, Any],
|
|
analysis_results: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Add metadata and citations to the response."""
|
|
# Extract citations from research results
|
|
citations = []
|
|
if research_results.get("results"):
|
|
for result in research_results["results"][:5]: # Top 5 citations
|
|
citations.append({
|
|
"document_id": result.get("document_id"),
|
|
"page_numbers": result.get("page_numbers", []),
|
|
"chunk_type": result.get("chunk_type"),
|
|
"score": result.get("score", 0)
|
|
})
|
|
|
|
return {
|
|
"text": response,
|
|
"citations": citations,
|
|
"research_confidence": research_results.get("confidence", 0.0),
|
|
"analysis_confidence": analysis_results.get("confidence", 0.0),
|
|
"overall_confidence": (research_results.get("confidence", 0.0) + analysis_results.get("confidence", 0.0)) / 2
|
|
}
|
|
|
|
def _calculate_synthesis_confidence(self, research_results: Dict[str, Any], analysis_results: Dict[str, Any]) -> float:
|
|
"""Calculate confidence in synthesis."""
|
|
research_conf = research_results.get("confidence", 0.5)
|
|
analysis_conf = analysis_results.get("confidence", 0.5)
|
|
|
|
# Weighted average
|
|
return (research_conf * 0.4 + analysis_conf * 0.6)
|
|
|
|
def _extract_key_insights(self, synthesis: str) -> List[str]:
|
|
"""Extract key insights from synthesis."""
|
|
# Simple extraction - in production, use more sophisticated parsing
|
|
lines = synthesis.split('\n')
|
|
insights = []
|
|
for line in lines:
|
|
if any(keyword in line.lower() for keyword in ['key', 'important', 'critical', 'significant']):
|
|
insights.append(line.strip())
|
|
return insights[:5] # Limit to top 5 insights
|
|
|
|
|
|
class AgenticRAGService:
|
|
"""Main service orchestrating agentic RAG operations."""
|
|
|
|
def __init__(self, vector_service: Optional[VectorService] = None):
|
|
self.vector_service = vector_service or VectorService()
|
|
self.agents = {}
|
|
self.workflow_engine = None
|
|
self._initialize_agents()
|
|
|
|
def _initialize_agents(self):
|
|
"""Initialize all agents."""
|
|
self.agents[AgentType.RESEARCH] = ResearchAgent(self.vector_service)
|
|
self.agents[AgentType.ANALYSIS] = AnalysisAgent()
|
|
self.agents[AgentType.SYNTHESIS] = SynthesisAgent()
|
|
|
|
async def answer(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
query: str,
|
|
max_tokens: Optional[int] = None,
|
|
temperature: Optional[float] = None,
|
|
reasoning_type: ReasoningType = ReasoningType.CHAIN_OF_THOUGHT,
|
|
enable_autonomous_workflow: bool = True
|
|
) -> Dict[str, Any]:
|
|
"""Generate answer using agentic RAG approach."""
|
|
|
|
# Check cache first
|
|
cache_key = f"agentic_rag:answer:{tenant_id}:{hash(query)}"
|
|
cached = await cache_service.get(cache_key, tenant_id)
|
|
if isinstance(cached, dict) and cached.get("text"):
|
|
return cached
|
|
|
|
try:
|
|
if enable_autonomous_workflow:
|
|
result = await self._autonomous_workflow(tenant_id, query, reasoning_type)
|
|
else:
|
|
result = await self._simple_workflow(tenant_id, query, reasoning_type)
|
|
|
|
# Cache result
|
|
await cache_service.set(cache_key, result, tenant_id, expire=300)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Agentic RAG failed: {e}")
|
|
# Fallback to simple RAG
|
|
return await self._fallback_rag(tenant_id, query)
|
|
|
|
async def _autonomous_workflow(
|
|
self,
|
|
tenant_id: str,
|
|
query: str,
|
|
reasoning_type: ReasoningType
|
|
) -> Dict[str, Any]:
|
|
"""Execute autonomous workflow with multiple agents."""
|
|
|
|
# Create workflow context
|
|
context = {
|
|
"tenant_id": tenant_id,
|
|
"query": query,
|
|
"reasoning_type": reasoning_type,
|
|
"workflow_id": str(uuid.uuid4()),
|
|
"start_time": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
# Phase 1: Research
|
|
research_task = AgentTask(
|
|
id=str(uuid.uuid4()),
|
|
agent_type=AgentType.RESEARCH,
|
|
description=f"Research information for query: {query}",
|
|
input_data={"query": query, "context": context},
|
|
dependencies=[],
|
|
priority=1,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
|
|
research_results = await self.agents[AgentType.RESEARCH].execute(research_task)
|
|
|
|
# Phase 2: Analysis
|
|
analysis_task = AgentTask(
|
|
id=str(uuid.uuid4()),
|
|
agent_type=AgentType.ANALYSIS,
|
|
description=f"Analyze research results for query: {query}",
|
|
input_data={
|
|
"query": query,
|
|
"retrieved_data": research_results.get("results", []),
|
|
"reasoning_type": reasoning_type,
|
|
"context": context
|
|
},
|
|
dependencies=[research_task.id],
|
|
priority=2,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
|
|
analysis_results = await self.agents[AgentType.ANALYSIS].execute(analysis_task)
|
|
|
|
# Phase 3: Synthesis
|
|
synthesis_task = AgentTask(
|
|
id=str(uuid.uuid4()),
|
|
agent_type=AgentType.SYNTHESIS,
|
|
description=f"Synthesize final response for query: {query}",
|
|
input_data={
|
|
"query": query,
|
|
"research_results": research_results,
|
|
"analysis_results": analysis_results,
|
|
"context": context
|
|
},
|
|
dependencies=[research_task.id, analysis_task.id],
|
|
priority=3,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
|
|
synthesis_results = await self.agents[AgentType.SYNTHESIS].execute(synthesis_task)
|
|
|
|
# Compile final result
|
|
final_result = {
|
|
"text": synthesis_results.get("response", {}).get("text", ""),
|
|
"citations": synthesis_results.get("response", {}).get("citations", []),
|
|
"model": "agentic_rag",
|
|
"workflow_metadata": {
|
|
"workflow_id": context["workflow_id"],
|
|
"research_confidence": research_results.get("confidence", 0.0),
|
|
"analysis_confidence": analysis_results.get("confidence", 0.0),
|
|
"synthesis_confidence": synthesis_results.get("confidence", 0.0),
|
|
"reasoning_type": reasoning_type.value,
|
|
"execution_time": datetime.utcnow().isoformat()
|
|
},
|
|
"agent_insights": {
|
|
"research_strategy": research_results.get("strategy_used"),
|
|
"analysis_method": analysis_results.get("reasoning_type"),
|
|
"key_insights": synthesis_results.get("synthesis", {}).get("key_insights", [])
|
|
}
|
|
}
|
|
|
|
return final_result
|
|
|
|
async def _simple_workflow(
|
|
self,
|
|
tenant_id: str,
|
|
query: str,
|
|
reasoning_type: ReasoningType
|
|
) -> Dict[str, Any]:
|
|
"""Execute simplified workflow for basic queries."""
|
|
|
|
# Simple research
|
|
research_results = await self.agents[AgentType.RESEARCH].execute(
|
|
AgentTask(
|
|
id=str(uuid.uuid4()),
|
|
agent_type=AgentType.RESEARCH,
|
|
description=f"Simple research for: {query}",
|
|
input_data={"query": query, "context": {"tenant_id": tenant_id}},
|
|
dependencies=[],
|
|
priority=1,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
)
|
|
|
|
# Simple synthesis
|
|
synthesis_results = await self.agents[AgentType.SYNTHESIS].execute(
|
|
AgentTask(
|
|
id=str(uuid.uuid4()),
|
|
agent_type=AgentType.SYNTHESIS,
|
|
description=f"Simple synthesis for: {query}",
|
|
input_data={
|
|
"query": query,
|
|
"research_results": research_results,
|
|
"analysis_results": {},
|
|
"context": {"tenant_id": tenant_id}
|
|
},
|
|
dependencies=[],
|
|
priority=2,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
)
|
|
|
|
return {
|
|
"text": synthesis_results.get("response", {}).get("text", ""),
|
|
"citations": synthesis_results.get("response", {}).get("citations", []),
|
|
"model": "agentic_rag_simple",
|
|
"confidence": synthesis_results.get("confidence", 0.0)
|
|
}
|
|
|
|
async def _fallback_rag(self, tenant_id: str, query: str) -> Dict[str, Any]:
|
|
"""Fallback to simple RAG if agentic approach fails."""
|
|
from app.services.rag_service import rag_service
|
|
|
|
return await rag_service.answer(
|
|
tenant_id=tenant_id,
|
|
query=query
|
|
)
|
|
|
|
async def get_agent_status(self) -> Dict[str, Any]:
|
|
"""Get status of all agents."""
|
|
status = {}
|
|
for agent_type, agent in self.agents.items():
|
|
status[agent_type.value] = {
|
|
"agent_id": agent.agent_id,
|
|
"memory_size": len(agent.memory),
|
|
"learning_history_size": len(agent.learning_history),
|
|
"status": "active"
|
|
}
|
|
return status
|
|
|
|
async def reset_agent_memory(self, agent_type: Optional[AgentType] = None) -> bool:
|
|
"""Reset agent memory."""
|
|
try:
|
|
if agent_type:
|
|
if agent_type in self.agents:
|
|
self.agents[agent_type].memory = {}
|
|
self.agents[agent_type].learning_history = []
|
|
else:
|
|
for agent in self.agents.values():
|
|
agent.memory = {}
|
|
agent.learning_history = []
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to reset agent memory: {e}")
|
|
return False
|
|
|
|
|
|
# Global agentic RAG service instance
|
|
agentic_rag_service = AgenticRAGService()
|