Files
virtual_board_member/app/services/agentic_rag_service.py
Jonathan Pressnell 5b5714e4c2 feat: Complete Week 5 implementation - Agentic RAG & Multi-Agent Orchestration
- 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
2025-08-10 09:25:46 -04:00

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()