Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .env.example +26 -0
- README_HF.md +65 -0
- README_SPACE.md +65 -0
- app.py +303 -0
- requirements_hf.txt +25 -0
- requirements_space.txt +25 -0
- src/__init__.py +125 -0
- src/config.py +76 -0
- src/llm_client.py +257 -0
- src/llm_client_hf.py +263 -0
- src/main.py +282 -0
- src/models.py +198 -0
- src/modules/__init__.py +68 -0
- src/modules/citation.py +394 -0
- src/modules/error_handling.py +585 -0
- src/modules/output_generation.py +438 -0
- src/modules/query_understanding.py +224 -0
- src/modules/reasoning_engine.py +408 -0
- src/modules/verification.py +399 -0
- src/modules/web_search.py +394 -0
- src/orchestrator.py +572 -0
- src/prompts/__init__.py +147 -0
- src/prompts/citation_prompts.py +379 -0
- src/prompts/error_prompts.py +310 -0
- src/prompts/output_prompts.py +362 -0
- src/prompts/query_prompts.py +173 -0
- src/prompts/reasoning_prompts.py +288 -0
- src/prompts/search_prompts.py +257 -0
- src/prompts/system_prompts.py +148 -0
- src/prompts/verification_prompts.py +323 -0
- src/search_duckduckgo.py +158 -0
.env.example
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deep Research AI Environment Configuration
|
| 2 |
+
# Copy this file to .env and fill in your API keys
|
| 3 |
+
|
| 4 |
+
# LLM Provider API Keys (at least one required)
|
| 5 |
+
OPENAI_API_KEY=your-openai-api-key-here
|
| 6 |
+
ANTHROPIC_API_KEY=your-anthropic-api-key-here
|
| 7 |
+
|
| 8 |
+
# Search API Key (one required based on provider choice)
|
| 9 |
+
# For Brave Search: https://brave.com/search/api/
|
| 10 |
+
# For Serper: https://serper.dev/
|
| 11 |
+
# For Tavily: https://tavily.com/
|
| 12 |
+
SEARCH_API_KEY=your-search-api-key-here
|
| 13 |
+
|
| 14 |
+
# Optional: Specify search provider (brave, serper, tavily)
|
| 15 |
+
SEARCH_PROVIDER=brave
|
| 16 |
+
|
| 17 |
+
# Optional: Specify LLM provider (openai, anthropic)
|
| 18 |
+
LLM_PROVIDER=openai
|
| 19 |
+
|
| 20 |
+
# Optional: Specify LLM model
|
| 21 |
+
LLM_MODEL=gpt-4o
|
| 22 |
+
|
| 23 |
+
# Optional: Research settings
|
| 24 |
+
MAX_SOURCES=10
|
| 25 |
+
VERIFY_CLAIMS=true
|
| 26 |
+
DEFAULT_CITATION_STYLE=apa
|
README_HF.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Deep Research AI
|
| 3 |
+
emoji: 🔍
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# 🔍 Deep Research AI
|
| 14 |
+
|
| 15 |
+
An AI-powered research assistant that searches the web and synthesizes information to answer your questions.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- 🌐 **Real-time web search** using DuckDuckGo (no API key required)
|
| 20 |
+
- 🤖 **AI-powered synthesis** using Qwen 2.5 (free via Inference API)
|
| 21 |
+
- 📰 **Optional news search** for current events
|
| 22 |
+
- 📚 **Source citations** with links
|
| 23 |
+
|
| 24 |
+
## How it Works
|
| 25 |
+
|
| 26 |
+
1. **Enter a query** - Ask any research question
|
| 27 |
+
2. **Web search** - The system searches DuckDuckGo for relevant sources
|
| 28 |
+
3. **AI synthesis** - Qwen 2.5 analyzes sources and generates a comprehensive answer
|
| 29 |
+
4. **View results** - See the synthesized answer with citations and source links
|
| 30 |
+
|
| 31 |
+
## Example Queries
|
| 32 |
+
|
| 33 |
+
- "What are the latest developments in quantum computing?"
|
| 34 |
+
- "Compare Python vs Rust for systems programming"
|
| 35 |
+
- "How does CRISPR gene editing work?"
|
| 36 |
+
- "What is the current state of renewable energy?"
|
| 37 |
+
|
| 38 |
+
## Technology Stack
|
| 39 |
+
|
| 40 |
+
- **LLM**: Qwen 2.5 7B Instruct (via Hugging Face Inference API)
|
| 41 |
+
- **Search**: DuckDuckGo (free, no API key)
|
| 42 |
+
- **Frontend**: Gradio
|
| 43 |
+
- **Hosting**: Hugging Face Spaces
|
| 44 |
+
|
| 45 |
+
## Local Development
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Clone the repository
|
| 49 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/deep-research-ai
|
| 50 |
+
|
| 51 |
+
# Install dependencies
|
| 52 |
+
pip install -r requirements_hf.txt
|
| 53 |
+
|
| 54 |
+
# Run locally
|
| 55 |
+
python app.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## Environment Variables (Optional)
|
| 59 |
+
|
| 60 |
+
- `HF_TOKEN`: Hugging Face token for Inference API (optional, increases rate limits)
|
| 61 |
+
- `MODEL_ID`: Custom model ID (default: `Qwen/Qwen2.5-7B-Instruct`)
|
| 62 |
+
|
| 63 |
+
## License
|
| 64 |
+
|
| 65 |
+
MIT License
|
README_SPACE.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Deep Research AI
|
| 3 |
+
emoji: 🔍
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# 🔍 Deep Research AI
|
| 14 |
+
|
| 15 |
+
An AI-powered research assistant that searches the web and synthesizes information to answer your questions.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- 🌐 **Real-time web search** using DuckDuckGo (no API key required)
|
| 20 |
+
- 🤖 **AI-powered synthesis** using Qwen 2.5 (free via Inference API)
|
| 21 |
+
- 📰 **Optional news search** for current events
|
| 22 |
+
- 📚 **Source citations** with links
|
| 23 |
+
|
| 24 |
+
## How it Works
|
| 25 |
+
|
| 26 |
+
1. **Enter a query** - Ask any research question
|
| 27 |
+
2. **Web search** - The system searches DuckDuckGo for relevant sources
|
| 28 |
+
3. **AI synthesis** - Qwen 2.5 analyzes sources and generates a comprehensive answer
|
| 29 |
+
4. **View results** - See the synthesized answer with citations and source links
|
| 30 |
+
|
| 31 |
+
## Example Queries
|
| 32 |
+
|
| 33 |
+
- "What are the latest developments in quantum computing?"
|
| 34 |
+
- "Compare Python vs Rust for systems programming"
|
| 35 |
+
- "How does CRISPR gene editing work?"
|
| 36 |
+
- "What is the current state of renewable energy?"
|
| 37 |
+
|
| 38 |
+
## Technology Stack
|
| 39 |
+
|
| 40 |
+
- **LLM**: Qwen 2.5 7B Instruct (via Hugging Face Inference API)
|
| 41 |
+
- **Search**: DuckDuckGo (free, no API key)
|
| 42 |
+
- **Frontend**: Gradio
|
| 43 |
+
- **Hosting**: Hugging Face Spaces
|
| 44 |
+
|
| 45 |
+
## Local Development
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Clone the repository
|
| 49 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/deep-research-ai
|
| 50 |
+
|
| 51 |
+
# Install dependencies
|
| 52 |
+
pip install -r requirements_hf.txt
|
| 53 |
+
|
| 54 |
+
# Run locally
|
| 55 |
+
python app.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## Environment Variables (Optional)
|
| 59 |
+
|
| 60 |
+
- `HF_TOKEN`: Hugging Face token for Inference API (optional, increases rate limits)
|
| 61 |
+
- `MODEL_ID`: Custom model ID (default: `Qwen/Qwen2.5-7B-Instruct`)
|
| 62 |
+
|
| 63 |
+
## License
|
| 64 |
+
|
| 65 |
+
MIT License
|
app.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio App for Deep Research AI - Hugging Face Spaces Deployment
|
| 3 |
+
|
| 4 |
+
This creates a web interface for the research system that can be
|
| 5 |
+
deployed directly to Hugging Face Spaces.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import os
|
| 10 |
+
from typing import Generator
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
|
| 14 |
+
from src.config import Config, LLMConfig, SearchConfig
|
| 15 |
+
from src.llm_client_hf import HuggingFaceLLMClient, create_hf_client
|
| 16 |
+
from src.search_duckduckgo import DuckDuckGoSearch
|
| 17 |
+
from src.models import Source, OutputFormat, CitationStyle
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Configuration
|
| 21 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 22 |
+
MODEL_ID = os.environ.get("MODEL_ID", "Qwen/Qwen2.5-7B-Instruct")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SimpleResearcher:
|
| 26 |
+
"""
|
| 27 |
+
Simplified researcher for Hugging Face Spaces.
|
| 28 |
+
|
| 29 |
+
Uses free models and DuckDuckGo search.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.llm = create_hf_client(
|
| 34 |
+
model_size="medium",
|
| 35 |
+
use_inference_api=True,
|
| 36 |
+
hf_token=HF_TOKEN
|
| 37 |
+
)
|
| 38 |
+
self.search = DuckDuckGoSearch(max_results=5)
|
| 39 |
+
|
| 40 |
+
async def research(
|
| 41 |
+
self,
|
| 42 |
+
query: str,
|
| 43 |
+
num_sources: int = 5,
|
| 44 |
+
include_news: bool = False
|
| 45 |
+
) -> dict:
|
| 46 |
+
"""
|
| 47 |
+
Perform research on a query.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
query: Research query
|
| 51 |
+
num_sources: Number of sources to use
|
| 52 |
+
include_news: Include news search
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Research results dictionary
|
| 56 |
+
"""
|
| 57 |
+
results = {
|
| 58 |
+
"query": query,
|
| 59 |
+
"sources": [],
|
| 60 |
+
"answer": "",
|
| 61 |
+
"confidence": 0.0
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
# Step 1: Search for information
|
| 66 |
+
search_results = await self.search.search(query, max_results=num_sources)
|
| 67 |
+
|
| 68 |
+
if include_news:
|
| 69 |
+
news_results = await self.search.search_news(query, max_results=3)
|
| 70 |
+
search_results.extend(news_results)
|
| 71 |
+
|
| 72 |
+
# Convert to sources
|
| 73 |
+
sources = []
|
| 74 |
+
for r in search_results[:num_sources]:
|
| 75 |
+
sources.append({
|
| 76 |
+
"title": r.title,
|
| 77 |
+
"url": r.url,
|
| 78 |
+
"snippet": r.snippet,
|
| 79 |
+
"domain": r.domain
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
results["sources"] = sources
|
| 83 |
+
|
| 84 |
+
# Step 2: Synthesize answer using LLM
|
| 85 |
+
if sources:
|
| 86 |
+
synthesis_prompt = self._build_synthesis_prompt(query, sources)
|
| 87 |
+
answer = await self.llm.call(synthesis_prompt)
|
| 88 |
+
results["answer"] = answer
|
| 89 |
+
results["confidence"] = min(len(sources) / num_sources, 1.0)
|
| 90 |
+
else:
|
| 91 |
+
results["answer"] = "I couldn't find relevant information for this query."
|
| 92 |
+
results["confidence"] = 0.0
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
results["answer"] = f"Research encountered an error: {str(e)}"
|
| 96 |
+
results["confidence"] = 0.0
|
| 97 |
+
|
| 98 |
+
return results
|
| 99 |
+
|
| 100 |
+
def _build_synthesis_prompt(self, query: str, sources: list[dict]) -> str:
|
| 101 |
+
"""Build synthesis prompt from sources."""
|
| 102 |
+
sources_text = "\n\n".join([
|
| 103 |
+
f"**Source {i+1}: {s['title']}**\n{s['snippet']}\nURL: {s['url']}"
|
| 104 |
+
for i, s in enumerate(sources)
|
| 105 |
+
])
|
| 106 |
+
|
| 107 |
+
return f"""You are a research assistant. Based on the following sources, provide a comprehensive answer to the query.
|
| 108 |
+
|
| 109 |
+
QUERY: {query}
|
| 110 |
+
|
| 111 |
+
SOURCES:
|
| 112 |
+
{sources_text}
|
| 113 |
+
|
| 114 |
+
INSTRUCTIONS:
|
| 115 |
+
1. Synthesize information from all relevant sources
|
| 116 |
+
2. Provide a clear, well-structured answer
|
| 117 |
+
3. Cite sources using [1], [2], etc.
|
| 118 |
+
4. Acknowledge any limitations or conflicting information
|
| 119 |
+
5. Be objective and factual
|
| 120 |
+
|
| 121 |
+
ANSWER:"""
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# Create researcher instance
|
| 125 |
+
researcher = SimpleResearcher()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def format_sources(sources: list[dict]) -> str:
|
| 129 |
+
"""Format sources for display."""
|
| 130 |
+
if not sources:
|
| 131 |
+
return "No sources found."
|
| 132 |
+
|
| 133 |
+
formatted = []
|
| 134 |
+
for i, s in enumerate(sources, 1):
|
| 135 |
+
formatted.append(
|
| 136 |
+
f"**[{i}] {s['title']}**\n"
|
| 137 |
+
f"🔗 [{s['domain']}]({s['url']})\n"
|
| 138 |
+
f"_{s['snippet'][:200]}..._\n"
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
return "\n".join(formatted)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def format_result(result: dict) -> tuple[str, str, str]:
|
| 145 |
+
"""Format research result for display."""
|
| 146 |
+
answer = result.get("answer", "No answer generated.")
|
| 147 |
+
sources = format_sources(result.get("sources", []))
|
| 148 |
+
confidence = f"**Confidence:** {result.get('confidence', 0):.0%}"
|
| 149 |
+
|
| 150 |
+
return answer, sources, confidence
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
async def research_async(
|
| 154 |
+
query: str,
|
| 155 |
+
num_sources: int,
|
| 156 |
+
include_news: bool
|
| 157 |
+
) -> tuple[str, str, str]:
|
| 158 |
+
"""Async research function."""
|
| 159 |
+
result = await researcher.research(
|
| 160 |
+
query=query,
|
| 161 |
+
num_sources=int(num_sources),
|
| 162 |
+
include_news=include_news
|
| 163 |
+
)
|
| 164 |
+
return format_result(result)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def research(
|
| 168 |
+
query: str,
|
| 169 |
+
num_sources: int = 5,
|
| 170 |
+
include_news: bool = False
|
| 171 |
+
) -> tuple[str, str, str]:
|
| 172 |
+
"""
|
| 173 |
+
Main research function for Gradio.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
query: Research query
|
| 177 |
+
num_sources: Number of sources
|
| 178 |
+
include_news: Include news search
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
Tuple of (answer, sources, confidence)
|
| 182 |
+
"""
|
| 183 |
+
if not query.strip():
|
| 184 |
+
return "Please enter a research query.", "", ""
|
| 185 |
+
|
| 186 |
+
return asyncio.run(research_async(query, num_sources, include_news))
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# Create Gradio interface
|
| 190 |
+
def create_app() -> gr.Blocks:
|
| 191 |
+
"""Create the Gradio app."""
|
| 192 |
+
|
| 193 |
+
with gr.Blocks(
|
| 194 |
+
title="Deep Research AI",
|
| 195 |
+
theme=gr.themes.Soft(),
|
| 196 |
+
css="""
|
| 197 |
+
.container { max-width: 900px; margin: auto; }
|
| 198 |
+
.header { text-align: center; margin-bottom: 20px; }
|
| 199 |
+
"""
|
| 200 |
+
) as app:
|
| 201 |
+
|
| 202 |
+
gr.Markdown(
|
| 203 |
+
"""
|
| 204 |
+
# 🔍 Deep Research AI
|
| 205 |
+
|
| 206 |
+
An AI-powered research assistant that searches the web and synthesizes
|
| 207 |
+
information to answer your questions.
|
| 208 |
+
|
| 209 |
+
**Features:**
|
| 210 |
+
- 🌐 Real-time web search (DuckDuckGo)
|
| 211 |
+
- 🤖 AI-powered synthesis (Qwen 2.5)
|
| 212 |
+
- 📰 Optional news search
|
| 213 |
+
- 📚 Source citations
|
| 214 |
+
""",
|
| 215 |
+
elem_classes=["header"]
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
with gr.Row():
|
| 219 |
+
with gr.Column(scale=3):
|
| 220 |
+
query_input = gr.Textbox(
|
| 221 |
+
label="Research Query",
|
| 222 |
+
placeholder="Enter your research question...",
|
| 223 |
+
lines=2,
|
| 224 |
+
max_lines=4
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
with gr.Column(scale=1):
|
| 228 |
+
num_sources = gr.Slider(
|
| 229 |
+
minimum=3,
|
| 230 |
+
maximum=10,
|
| 231 |
+
value=5,
|
| 232 |
+
step=1,
|
| 233 |
+
label="Number of Sources"
|
| 234 |
+
)
|
| 235 |
+
include_news = gr.Checkbox(
|
| 236 |
+
label="Include News",
|
| 237 |
+
value=False
|
| 238 |
+
)
|
| 239 |
+
research_btn = gr.Button(
|
| 240 |
+
"🔍 Research",
|
| 241 |
+
variant="primary",
|
| 242 |
+
size="lg"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
with gr.Tabs():
|
| 246 |
+
with gr.TabItem("📝 Answer"):
|
| 247 |
+
answer_output = gr.Markdown(
|
| 248 |
+
label="Research Answer",
|
| 249 |
+
value="*Enter a query and click Research*"
|
| 250 |
+
)
|
| 251 |
+
confidence_output = gr.Markdown()
|
| 252 |
+
|
| 253 |
+
with gr.TabItem("📚 Sources"):
|
| 254 |
+
sources_output = gr.Markdown(
|
| 255 |
+
label="Sources",
|
| 256 |
+
value="*Sources will appear here*"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# Examples
|
| 260 |
+
gr.Examples(
|
| 261 |
+
examples=[
|
| 262 |
+
["What are the latest developments in quantum computing?"],
|
| 263 |
+
["Compare the benefits of Python vs Rust for systems programming"],
|
| 264 |
+
["Explain how large language models work"],
|
| 265 |
+
["What is the current state of renewable energy adoption?"],
|
| 266 |
+
["How does CRISPR gene editing work?"],
|
| 267 |
+
],
|
| 268 |
+
inputs=query_input,
|
| 269 |
+
label="Example Queries"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Connect button
|
| 273 |
+
research_btn.click(
|
| 274 |
+
fn=research,
|
| 275 |
+
inputs=[query_input, num_sources, include_news],
|
| 276 |
+
outputs=[answer_output, sources_output, confidence_output]
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# Also trigger on Enter
|
| 280 |
+
query_input.submit(
|
| 281 |
+
fn=research,
|
| 282 |
+
inputs=[query_input, num_sources, include_news],
|
| 283 |
+
outputs=[answer_output, sources_output, confidence_output]
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
gr.Markdown(
|
| 287 |
+
"""
|
| 288 |
+
---
|
| 289 |
+
**Note:** This uses DuckDuckGo for search (no API key required) and
|
| 290 |
+
Qwen 2.5 for synthesis. Results may vary based on available sources.
|
| 291 |
+
|
| 292 |
+
Made with ❤️ using Gradio and Hugging Face
|
| 293 |
+
"""
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
return app
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
# Create and launch app
|
| 300 |
+
app = create_app()
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
app.launch()
|
requirements_hf.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Requirements
|
| 2 |
+
# These are optimized for free-tier deployment
|
| 3 |
+
|
| 4 |
+
# Core
|
| 5 |
+
gradio>=4.0.0
|
| 6 |
+
huggingface_hub>=0.20.0
|
| 7 |
+
|
| 8 |
+
# LLM (for inference API)
|
| 9 |
+
transformers>=4.36.0
|
| 10 |
+
torch>=2.1.0
|
| 11 |
+
accelerate>=0.25.0
|
| 12 |
+
|
| 13 |
+
# Free web search
|
| 14 |
+
duckduckgo-search>=4.1.0
|
| 15 |
+
|
| 16 |
+
# HTTP and async
|
| 17 |
+
httpx>=0.25.0
|
| 18 |
+
aiohttp>=3.9.0
|
| 19 |
+
|
| 20 |
+
# Content extraction
|
| 21 |
+
beautifulsoup4>=4.12.0
|
| 22 |
+
trafilatura>=1.6.0
|
| 23 |
+
|
| 24 |
+
# Utilities
|
| 25 |
+
python-dotenv>=1.0.0
|
requirements_space.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Requirements
|
| 2 |
+
# These are optimized for free-tier deployment
|
| 3 |
+
|
| 4 |
+
# Core
|
| 5 |
+
gradio>=4.0.0
|
| 6 |
+
huggingface_hub>=0.20.0
|
| 7 |
+
|
| 8 |
+
# LLM (for inference API)
|
| 9 |
+
transformers>=4.36.0
|
| 10 |
+
torch>=2.1.0
|
| 11 |
+
accelerate>=0.25.0
|
| 12 |
+
|
| 13 |
+
# Free web search
|
| 14 |
+
duckduckgo-search>=4.1.0
|
| 15 |
+
|
| 16 |
+
# HTTP and async
|
| 17 |
+
httpx>=0.25.0
|
| 18 |
+
aiohttp>=3.9.0
|
| 19 |
+
|
| 20 |
+
# Content extraction
|
| 21 |
+
beautifulsoup4>=4.12.0
|
| 22 |
+
trafilatura>=1.6.0
|
| 23 |
+
|
| 24 |
+
# Utilities
|
| 25 |
+
python-dotenv>=1.0.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deep Research AI
|
| 3 |
+
|
| 4 |
+
A comprehensive AI-powered research system that combines web search,
|
| 5 |
+
reasoning, verification, and citation to deliver accurate, well-sourced
|
| 6 |
+
research results.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
from src import research, ResearchOrchestrator, Config
|
| 10 |
+
|
| 11 |
+
# Quick research
|
| 12 |
+
result = await research("What is quantum computing?")
|
| 13 |
+
|
| 14 |
+
# Advanced usage
|
| 15 |
+
config = Config()
|
| 16 |
+
orchestrator = ResearchOrchestrator(config)
|
| 17 |
+
result = await orchestrator.research(
|
| 18 |
+
query="Compare Python and Rust",
|
| 19 |
+
audience=AudienceType.PROFESSIONAL,
|
| 20 |
+
max_sources=15
|
| 21 |
+
)
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from .config import Config, LLMConfig, SearchConfig, ResearchConfig
|
| 25 |
+
from .models import (
|
| 26 |
+
Entity,
|
| 27 |
+
SubQuery,
|
| 28 |
+
QueryAnalysis,
|
| 29 |
+
SearchResult,
|
| 30 |
+
ContentExtraction,
|
| 31 |
+
Source,
|
| 32 |
+
ReasoningStep,
|
| 33 |
+
VerificationResult,
|
| 34 |
+
Citation,
|
| 35 |
+
ResearchResult,
|
| 36 |
+
OutputFormat,
|
| 37 |
+
CitationStyle,
|
| 38 |
+
)
|
| 39 |
+
from .llm_client import LLMClient
|
| 40 |
+
from .orchestrator import ResearchOrchestrator, research, ResearchSession, ResearchProgress
|
| 41 |
+
from .main import run_research, create_config
|
| 42 |
+
|
| 43 |
+
# Module imports
|
| 44 |
+
from .modules.query_understanding import QueryUnderstanding
|
| 45 |
+
from .modules.web_search import WebSearch
|
| 46 |
+
from .modules.reasoning_engine import ReasoningEngine
|
| 47 |
+
from .modules.verification import Verification
|
| 48 |
+
from .modules.citation import CitationManager
|
| 49 |
+
from .modules.output_generation import OutputGenerator, SummaryLength, AudienceType
|
| 50 |
+
from .modules.error_handling import (
|
| 51 |
+
ErrorHandler,
|
| 52 |
+
ErrorSeverity,
|
| 53 |
+
ComponentType,
|
| 54 |
+
ResearchError,
|
| 55 |
+
QueryError,
|
| 56 |
+
SearchError,
|
| 57 |
+
ReasoningError,
|
| 58 |
+
VerificationError,
|
| 59 |
+
CitationError,
|
| 60 |
+
LLMError,
|
| 61 |
+
RateLimitError,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
__version__ = "1.0.0"
|
| 65 |
+
__author__ = "Deep Research AI Team"
|
| 66 |
+
|
| 67 |
+
__all__ = [
|
| 68 |
+
# Main API
|
| 69 |
+
"research",
|
| 70 |
+
"run_research",
|
| 71 |
+
"create_config",
|
| 72 |
+
"ResearchOrchestrator",
|
| 73 |
+
|
| 74 |
+
# Configuration
|
| 75 |
+
"Config",
|
| 76 |
+
"LLMConfig",
|
| 77 |
+
"SearchConfig",
|
| 78 |
+
"ResearchConfig",
|
| 79 |
+
|
| 80 |
+
# Models
|
| 81 |
+
"Entity",
|
| 82 |
+
"SubQuery",
|
| 83 |
+
"QueryAnalysis",
|
| 84 |
+
"SearchResult",
|
| 85 |
+
"ContentExtraction",
|
| 86 |
+
"Source",
|
| 87 |
+
"ReasoningStep",
|
| 88 |
+
"VerificationResult",
|
| 89 |
+
"Citation",
|
| 90 |
+
"ResearchResult",
|
| 91 |
+
"OutputFormat",
|
| 92 |
+
"CitationStyle",
|
| 93 |
+
|
| 94 |
+
# Clients
|
| 95 |
+
"LLMClient",
|
| 96 |
+
|
| 97 |
+
# Modules
|
| 98 |
+
"QueryUnderstanding",
|
| 99 |
+
"WebSearch",
|
| 100 |
+
"ReasoningEngine",
|
| 101 |
+
"Verification",
|
| 102 |
+
"CitationManager",
|
| 103 |
+
"OutputGenerator",
|
| 104 |
+
"ErrorHandler",
|
| 105 |
+
|
| 106 |
+
# Enums
|
| 107 |
+
"SummaryLength",
|
| 108 |
+
"AudienceType",
|
| 109 |
+
"ErrorSeverity",
|
| 110 |
+
"ComponentType",
|
| 111 |
+
|
| 112 |
+
# Exceptions
|
| 113 |
+
"ResearchError",
|
| 114 |
+
"QueryError",
|
| 115 |
+
"SearchError",
|
| 116 |
+
"ReasoningError",
|
| 117 |
+
"VerificationError",
|
| 118 |
+
"CitationError",
|
| 119 |
+
"LLMError",
|
| 120 |
+
"RateLimitError",
|
| 121 |
+
|
| 122 |
+
# Session management
|
| 123 |
+
"ResearchSession",
|
| 124 |
+
"ResearchProgress",
|
| 125 |
+
]
|
src/config.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration settings for Deep Research AI.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
# Load environment variables
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class LLMConfig:
|
| 16 |
+
"""LLM provider configuration."""
|
| 17 |
+
provider: str = "openai"
|
| 18 |
+
model: str = "gpt-4"
|
| 19 |
+
temperature: float = 0.7
|
| 20 |
+
max_tokens: int = 4096
|
| 21 |
+
api_key: Optional[str] = field(default_factory=lambda: os.getenv("OPENAI_API_KEY"))
|
| 22 |
+
|
| 23 |
+
# Fallback configuration
|
| 24 |
+
fallback_provider: str = "anthropic"
|
| 25 |
+
fallback_model: str = "claude-3-sonnet-20240229"
|
| 26 |
+
fallback_api_key: Optional[str] = field(default_factory=lambda: os.getenv("ANTHROPIC_API_KEY"))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class SearchConfig:
|
| 31 |
+
"""Web search configuration."""
|
| 32 |
+
provider: str = "tavily"
|
| 33 |
+
api_key: Optional[str] = field(default_factory=lambda: os.getenv("TAVILY_API_KEY"))
|
| 34 |
+
max_results: int = 10
|
| 35 |
+
timeout_seconds: int = 30
|
| 36 |
+
|
| 37 |
+
# Fallback search
|
| 38 |
+
fallback_provider: str = "serper"
|
| 39 |
+
fallback_api_key: Optional[str] = field(default_factory=lambda: os.getenv("SERPER_API_KEY"))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class ResearchConfig:
|
| 44 |
+
"""Research operation configuration."""
|
| 45 |
+
# Timeouts
|
| 46 |
+
max_research_time_seconds: int = 120
|
| 47 |
+
quick_research_time_seconds: int = 30
|
| 48 |
+
|
| 49 |
+
# Sources
|
| 50 |
+
max_sources: int = 15
|
| 51 |
+
min_sources_for_verification: int = 2
|
| 52 |
+
|
| 53 |
+
# Verification
|
| 54 |
+
min_confidence_threshold: float = 0.5
|
| 55 |
+
require_cross_reference: bool = True
|
| 56 |
+
|
| 57 |
+
# Output
|
| 58 |
+
default_output_format: str = "markdown"
|
| 59 |
+
include_confidence_scores: bool = True
|
| 60 |
+
include_sources: bool = True
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclass
|
| 64 |
+
class Config:
|
| 65 |
+
"""Main application configuration."""
|
| 66 |
+
llm: LLMConfig = field(default_factory=LLMConfig)
|
| 67 |
+
search: SearchConfig = field(default_factory=SearchConfig)
|
| 68 |
+
research: ResearchConfig = field(default_factory=ResearchConfig)
|
| 69 |
+
|
| 70 |
+
# Application settings
|
| 71 |
+
debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "false").lower() == "true")
|
| 72 |
+
log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Global config instance
|
| 76 |
+
config = Config()
|
src/llm_client.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM client for interacting with language models.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Optional, Dict, Any, List
|
| 8 |
+
from abc import ABC, abstractmethod
|
| 9 |
+
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
from anthropic import Anthropic
|
| 12 |
+
|
| 13 |
+
from .config import config
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class BaseLLMClient(ABC):
|
| 19 |
+
"""Base class for LLM clients."""
|
| 20 |
+
|
| 21 |
+
@abstractmethod
|
| 22 |
+
async def generate(
|
| 23 |
+
self,
|
| 24 |
+
prompt: str,
|
| 25 |
+
system_prompt: Optional[str] = None,
|
| 26 |
+
temperature: Optional[float] = None,
|
| 27 |
+
max_tokens: Optional[int] = None,
|
| 28 |
+
json_mode: bool = False
|
| 29 |
+
) -> str:
|
| 30 |
+
"""Generate a response from the LLM."""
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
@abstractmethod
|
| 34 |
+
async def generate_json(
|
| 35 |
+
self,
|
| 36 |
+
prompt: str,
|
| 37 |
+
system_prompt: Optional[str] = None,
|
| 38 |
+
temperature: Optional[float] = None
|
| 39 |
+
) -> Dict[str, Any]:
|
| 40 |
+
"""Generate a JSON response from the LLM."""
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class OpenAIClient(BaseLLMClient):
|
| 45 |
+
"""OpenAI API client."""
|
| 46 |
+
|
| 47 |
+
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
|
| 48 |
+
self.api_key = api_key or config.llm.api_key
|
| 49 |
+
self.model = model or config.llm.model
|
| 50 |
+
self.client = OpenAI(api_key=self.api_key)
|
| 51 |
+
|
| 52 |
+
async def generate(
|
| 53 |
+
self,
|
| 54 |
+
prompt: str,
|
| 55 |
+
system_prompt: Optional[str] = None,
|
| 56 |
+
temperature: Optional[float] = None,
|
| 57 |
+
max_tokens: Optional[int] = None,
|
| 58 |
+
json_mode: bool = False
|
| 59 |
+
) -> str:
|
| 60 |
+
"""Generate a response from OpenAI."""
|
| 61 |
+
messages = []
|
| 62 |
+
|
| 63 |
+
if system_prompt:
|
| 64 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 65 |
+
|
| 66 |
+
messages.append({"role": "user", "content": prompt})
|
| 67 |
+
|
| 68 |
+
kwargs = {
|
| 69 |
+
"model": self.model,
|
| 70 |
+
"messages": messages,
|
| 71 |
+
"temperature": temperature or config.llm.temperature,
|
| 72 |
+
"max_tokens": max_tokens or config.llm.max_tokens,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if json_mode:
|
| 76 |
+
kwargs["response_format"] = {"type": "json_object"}
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
response = self.client.chat.completions.create(**kwargs)
|
| 80 |
+
return response.choices[0].message.content
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"OpenAI API error: {e}")
|
| 83 |
+
raise
|
| 84 |
+
|
| 85 |
+
async def generate_json(
|
| 86 |
+
self,
|
| 87 |
+
prompt: str,
|
| 88 |
+
system_prompt: Optional[str] = None,
|
| 89 |
+
temperature: Optional[float] = None
|
| 90 |
+
) -> Dict[str, Any]:
|
| 91 |
+
"""Generate a JSON response from OpenAI."""
|
| 92 |
+
# Add JSON instruction to prompt
|
| 93 |
+
json_prompt = prompt + "\n\nRespond with valid JSON only."
|
| 94 |
+
|
| 95 |
+
response = await self.generate(
|
| 96 |
+
prompt=json_prompt,
|
| 97 |
+
system_prompt=system_prompt,
|
| 98 |
+
temperature=temperature,
|
| 99 |
+
json_mode=True
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
return json.loads(response)
|
| 104 |
+
except json.JSONDecodeError as e:
|
| 105 |
+
logger.error(f"Failed to parse JSON response: {e}")
|
| 106 |
+
# Try to extract JSON from response
|
| 107 |
+
return self._extract_json(response)
|
| 108 |
+
|
| 109 |
+
def _extract_json(self, text: str) -> Dict[str, Any]:
|
| 110 |
+
"""Extract JSON from text that might contain other content."""
|
| 111 |
+
# Try to find JSON block
|
| 112 |
+
start = text.find('{')
|
| 113 |
+
end = text.rfind('}') + 1
|
| 114 |
+
|
| 115 |
+
if start != -1 and end > start:
|
| 116 |
+
try:
|
| 117 |
+
return json.loads(text[start:end])
|
| 118 |
+
except json.JSONDecodeError:
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
+
return {"error": "Failed to parse response", "raw": text}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class AnthropicClient(BaseLLMClient):
|
| 125 |
+
"""Anthropic API client."""
|
| 126 |
+
|
| 127 |
+
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
|
| 128 |
+
self.api_key = api_key or config.llm.fallback_api_key
|
| 129 |
+
self.model = model or config.llm.fallback_model
|
| 130 |
+
self.client = Anthropic(api_key=self.api_key)
|
| 131 |
+
|
| 132 |
+
async def generate(
|
| 133 |
+
self,
|
| 134 |
+
prompt: str,
|
| 135 |
+
system_prompt: Optional[str] = None,
|
| 136 |
+
temperature: Optional[float] = None,
|
| 137 |
+
max_tokens: Optional[int] = None,
|
| 138 |
+
json_mode: bool = False
|
| 139 |
+
) -> str:
|
| 140 |
+
"""Generate a response from Anthropic."""
|
| 141 |
+
kwargs = {
|
| 142 |
+
"model": self.model,
|
| 143 |
+
"max_tokens": max_tokens or config.llm.max_tokens,
|
| 144 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if system_prompt:
|
| 148 |
+
kwargs["system"] = system_prompt
|
| 149 |
+
|
| 150 |
+
if temperature is not None:
|
| 151 |
+
kwargs["temperature"] = temperature
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
response = self.client.messages.create(**kwargs)
|
| 155 |
+
return response.content[0].text
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"Anthropic API error: {e}")
|
| 158 |
+
raise
|
| 159 |
+
|
| 160 |
+
async def generate_json(
|
| 161 |
+
self,
|
| 162 |
+
prompt: str,
|
| 163 |
+
system_prompt: Optional[str] = None,
|
| 164 |
+
temperature: Optional[float] = None
|
| 165 |
+
) -> Dict[str, Any]:
|
| 166 |
+
"""Generate a JSON response from Anthropic."""
|
| 167 |
+
json_prompt = prompt + "\n\nRespond with valid JSON only, no other text."
|
| 168 |
+
|
| 169 |
+
response = await self.generate(
|
| 170 |
+
prompt=json_prompt,
|
| 171 |
+
system_prompt=system_prompt,
|
| 172 |
+
temperature=temperature
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
return json.loads(response)
|
| 177 |
+
except json.JSONDecodeError:
|
| 178 |
+
# Try to extract JSON
|
| 179 |
+
start = response.find('{')
|
| 180 |
+
end = response.rfind('}') + 1
|
| 181 |
+
if start != -1 and end > start:
|
| 182 |
+
try:
|
| 183 |
+
return json.loads(response[start:end])
|
| 184 |
+
except json.JSONDecodeError:
|
| 185 |
+
pass
|
| 186 |
+
return {"error": "Failed to parse response", "raw": response}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class LLMClient:
|
| 190 |
+
"""Main LLM client with fallback support."""
|
| 191 |
+
|
| 192 |
+
def __init__(self):
|
| 193 |
+
self.primary = OpenAIClient()
|
| 194 |
+
self.fallback = AnthropicClient()
|
| 195 |
+
self._use_fallback = False
|
| 196 |
+
|
| 197 |
+
async def generate(
|
| 198 |
+
self,
|
| 199 |
+
prompt: str,
|
| 200 |
+
system_prompt: Optional[str] = None,
|
| 201 |
+
temperature: Optional[float] = None,
|
| 202 |
+
max_tokens: Optional[int] = None,
|
| 203 |
+
json_mode: bool = False
|
| 204 |
+
) -> str:
|
| 205 |
+
"""Generate a response with fallback support."""
|
| 206 |
+
client = self.fallback if self._use_fallback else self.primary
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
return await client.generate(
|
| 210 |
+
prompt=prompt,
|
| 211 |
+
system_prompt=system_prompt,
|
| 212 |
+
temperature=temperature,
|
| 213 |
+
max_tokens=max_tokens,
|
| 214 |
+
json_mode=json_mode
|
| 215 |
+
)
|
| 216 |
+
except Exception as e:
|
| 217 |
+
if not self._use_fallback:
|
| 218 |
+
logger.warning(f"Primary LLM failed, trying fallback: {e}")
|
| 219 |
+
self._use_fallback = True
|
| 220 |
+
return await self.generate(
|
| 221 |
+
prompt=prompt,
|
| 222 |
+
system_prompt=system_prompt,
|
| 223 |
+
temperature=temperature,
|
| 224 |
+
max_tokens=max_tokens,
|
| 225 |
+
json_mode=json_mode
|
| 226 |
+
)
|
| 227 |
+
raise
|
| 228 |
+
|
| 229 |
+
async def generate_json(
|
| 230 |
+
self,
|
| 231 |
+
prompt: str,
|
| 232 |
+
system_prompt: Optional[str] = None,
|
| 233 |
+
temperature: Optional[float] = None
|
| 234 |
+
) -> Dict[str, Any]:
|
| 235 |
+
"""Generate a JSON response with fallback support."""
|
| 236 |
+
client = self.fallback if self._use_fallback else self.primary
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
return await client.generate_json(
|
| 240 |
+
prompt=prompt,
|
| 241 |
+
system_prompt=system_prompt,
|
| 242 |
+
temperature=temperature
|
| 243 |
+
)
|
| 244 |
+
except Exception as e:
|
| 245 |
+
if not self._use_fallback:
|
| 246 |
+
logger.warning(f"Primary LLM failed, trying fallback: {e}")
|
| 247 |
+
self._use_fallback = True
|
| 248 |
+
return await self.generate_json(
|
| 249 |
+
prompt=prompt,
|
| 250 |
+
system_prompt=system_prompt,
|
| 251 |
+
temperature=temperature
|
| 252 |
+
)
|
| 253 |
+
raise
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# Global LLM client instance
|
| 257 |
+
llm_client = LLMClient()
|
src/llm_client_hf.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face LLM Client for Deep Research AI.
|
| 3 |
+
|
| 4 |
+
This module provides LLM integration using Hugging Face models,
|
| 5 |
+
suitable for deployment on Hugging Face Spaces.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import re
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
|
| 14 |
+
import torch
|
| 15 |
+
HF_AVAILABLE = True
|
| 16 |
+
except ImportError:
|
| 17 |
+
HF_AVAILABLE = False
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from huggingface_hub import InferenceClient
|
| 21 |
+
HF_INFERENCE_AVAILABLE = True
|
| 22 |
+
except ImportError:
|
| 23 |
+
HF_INFERENCE_AVAILABLE = False
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class HuggingFaceLLMClient:
|
| 27 |
+
"""
|
| 28 |
+
LLM client for Hugging Face models.
|
| 29 |
+
|
| 30 |
+
Supports both local model loading and Hugging Face Inference API.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
# Recommended models for research tasks
|
| 34 |
+
RECOMMENDED_MODELS = {
|
| 35 |
+
"small": "mistralai/Mistral-7B-Instruct-v0.3",
|
| 36 |
+
"medium": "Qwen/Qwen2.5-7B-Instruct",
|
| 37 |
+
"large": "meta-llama/Llama-3.1-8B-Instruct",
|
| 38 |
+
"best": "microsoft/Phi-3-medium-4k-instruct",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
model_id: str = "Qwen/Qwen2.5-7B-Instruct",
|
| 44 |
+
use_inference_api: bool = True,
|
| 45 |
+
hf_token: str | None = None,
|
| 46 |
+
device: str = "auto",
|
| 47 |
+
max_new_tokens: int = 2048,
|
| 48 |
+
temperature: float = 0.7,
|
| 49 |
+
) -> None:
|
| 50 |
+
"""
|
| 51 |
+
Initialize the Hugging Face LLM client.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
model_id: Hugging Face model ID
|
| 55 |
+
use_inference_api: Use HF Inference API (recommended for Spaces)
|
| 56 |
+
hf_token: Hugging Face API token
|
| 57 |
+
device: Device to use ("auto", "cuda", "cpu")
|
| 58 |
+
max_new_tokens: Maximum tokens to generate
|
| 59 |
+
temperature: Sampling temperature
|
| 60 |
+
"""
|
| 61 |
+
self.model_id = model_id
|
| 62 |
+
self.use_inference_api = use_inference_api
|
| 63 |
+
self.hf_token = hf_token
|
| 64 |
+
self.max_new_tokens = max_new_tokens
|
| 65 |
+
self.temperature = temperature
|
| 66 |
+
|
| 67 |
+
self.client = None
|
| 68 |
+
self.pipeline = None
|
| 69 |
+
self.tokenizer = None
|
| 70 |
+
self.model = None
|
| 71 |
+
|
| 72 |
+
if use_inference_api:
|
| 73 |
+
self._init_inference_client()
|
| 74 |
+
else:
|
| 75 |
+
self._init_local_model(device)
|
| 76 |
+
|
| 77 |
+
def _init_inference_client(self) -> None:
|
| 78 |
+
"""Initialize Hugging Face Inference API client."""
|
| 79 |
+
if not HF_INFERENCE_AVAILABLE:
|
| 80 |
+
raise ImportError(
|
| 81 |
+
"huggingface_hub not installed. "
|
| 82 |
+
"Install with: pip install huggingface_hub"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
self.client = InferenceClient(
|
| 86 |
+
model=self.model_id,
|
| 87 |
+
token=self.hf_token
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def _init_local_model(self, device: str) -> None:
|
| 91 |
+
"""Initialize local model."""
|
| 92 |
+
if not HF_AVAILABLE:
|
| 93 |
+
raise ImportError(
|
| 94 |
+
"transformers not installed. "
|
| 95 |
+
"Install with: pip install transformers torch"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Determine device
|
| 99 |
+
if device == "auto":
|
| 100 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 101 |
+
|
| 102 |
+
# Load tokenizer and model
|
| 103 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 104 |
+
self.model_id,
|
| 105 |
+
token=self.hf_token
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 109 |
+
self.model_id,
|
| 110 |
+
token=self.hf_token,
|
| 111 |
+
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 112 |
+
device_map=device,
|
| 113 |
+
trust_remote_code=True
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
self.pipeline = pipeline(
|
| 117 |
+
"text-generation",
|
| 118 |
+
model=self.model,
|
| 119 |
+
tokenizer=self.tokenizer,
|
| 120 |
+
max_new_tokens=self.max_new_tokens,
|
| 121 |
+
temperature=self.temperature,
|
| 122 |
+
do_sample=True,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
async def call(self, prompt: str, system_prompt: str | None = None) -> str:
|
| 126 |
+
"""
|
| 127 |
+
Call the LLM with a prompt.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
prompt: User prompt
|
| 131 |
+
system_prompt: Optional system prompt
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
Generated text response
|
| 135 |
+
"""
|
| 136 |
+
messages = []
|
| 137 |
+
|
| 138 |
+
if system_prompt:
|
| 139 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 140 |
+
|
| 141 |
+
messages.append({"role": "user", "content": prompt})
|
| 142 |
+
|
| 143 |
+
if self.use_inference_api:
|
| 144 |
+
return await self._call_inference_api(messages)
|
| 145 |
+
else:
|
| 146 |
+
return self._call_local_model(messages)
|
| 147 |
+
|
| 148 |
+
async def _call_inference_api(self, messages: list[dict]) -> str:
|
| 149 |
+
"""Call using Inference API."""
|
| 150 |
+
import asyncio
|
| 151 |
+
|
| 152 |
+
# Run in executor since client is sync
|
| 153 |
+
loop = asyncio.get_event_loop()
|
| 154 |
+
response = await loop.run_in_executor(
|
| 155 |
+
None,
|
| 156 |
+
lambda: self.client.chat_completion(
|
| 157 |
+
messages=messages,
|
| 158 |
+
max_tokens=self.max_new_tokens,
|
| 159 |
+
temperature=self.temperature,
|
| 160 |
+
)
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
return response.choices[0].message.content
|
| 164 |
+
|
| 165 |
+
def _call_local_model(self, messages: list[dict]) -> str:
|
| 166 |
+
"""Call local model."""
|
| 167 |
+
# Format messages for the model
|
| 168 |
+
if hasattr(self.tokenizer, "apply_chat_template"):
|
| 169 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 170 |
+
messages,
|
| 171 |
+
tokenize=False,
|
| 172 |
+
add_generation_prompt=True
|
| 173 |
+
)
|
| 174 |
+
else:
|
| 175 |
+
# Fallback formatting
|
| 176 |
+
prompt = ""
|
| 177 |
+
for msg in messages:
|
| 178 |
+
role = msg["role"]
|
| 179 |
+
content = msg["content"]
|
| 180 |
+
if role == "system":
|
| 181 |
+
prompt += f"System: {content}\n\n"
|
| 182 |
+
elif role == "user":
|
| 183 |
+
prompt += f"User: {content}\n\nAssistant: "
|
| 184 |
+
|
| 185 |
+
outputs = self.pipeline(prompt, return_full_text=False)
|
| 186 |
+
return outputs[0]["generated_text"]
|
| 187 |
+
|
| 188 |
+
async def call_json(
|
| 189 |
+
self,
|
| 190 |
+
prompt: str,
|
| 191 |
+
system_prompt: str | None = None
|
| 192 |
+
) -> dict[str, Any]:
|
| 193 |
+
"""
|
| 194 |
+
Call LLM and parse JSON response.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
prompt: User prompt (should request JSON output)
|
| 198 |
+
system_prompt: Optional system prompt
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
Parsed JSON as dictionary
|
| 202 |
+
"""
|
| 203 |
+
# Add JSON instruction to prompt
|
| 204 |
+
json_prompt = prompt + "\n\nRespond with valid JSON only."
|
| 205 |
+
|
| 206 |
+
response = await self.call(json_prompt, system_prompt)
|
| 207 |
+
|
| 208 |
+
return self._parse_json_response(response)
|
| 209 |
+
|
| 210 |
+
def _parse_json_response(self, response: str) -> dict[str, Any]:
|
| 211 |
+
"""Parse JSON from response text."""
|
| 212 |
+
# Try direct parse
|
| 213 |
+
try:
|
| 214 |
+
return json.loads(response)
|
| 215 |
+
except json.JSONDecodeError:
|
| 216 |
+
pass
|
| 217 |
+
|
| 218 |
+
# Try to extract JSON from markdown code blocks
|
| 219 |
+
json_patterns = [
|
| 220 |
+
r'```json\s*([\s\S]*?)\s*```',
|
| 221 |
+
r'```\s*([\s\S]*?)\s*```',
|
| 222 |
+
r'\{[\s\S]*\}',
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
for pattern in json_patterns:
|
| 226 |
+
matches = re.findall(pattern, response)
|
| 227 |
+
for match in matches:
|
| 228 |
+
try:
|
| 229 |
+
return json.loads(match)
|
| 230 |
+
except json.JSONDecodeError:
|
| 231 |
+
continue
|
| 232 |
+
|
| 233 |
+
# Return empty dict if parsing fails
|
| 234 |
+
return {"raw_response": response, "parse_error": True}
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# Factory function
|
| 238 |
+
def create_hf_client(
|
| 239 |
+
model_size: str = "medium",
|
| 240 |
+
use_inference_api: bool = True,
|
| 241 |
+
hf_token: str | None = None
|
| 242 |
+
) -> HuggingFaceLLMClient:
|
| 243 |
+
"""
|
| 244 |
+
Create a Hugging Face LLM client.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
model_size: "small", "medium", "large", or "best"
|
| 248 |
+
use_inference_api: Use Inference API (recommended)
|
| 249 |
+
hf_token: Hugging Face token
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
Configured HuggingFaceLLMClient
|
| 253 |
+
"""
|
| 254 |
+
model_id = HuggingFaceLLMClient.RECOMMENDED_MODELS.get(
|
| 255 |
+
model_size,
|
| 256 |
+
HuggingFaceLLMClient.RECOMMENDED_MODELS["medium"]
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
return HuggingFaceLLMClient(
|
| 260 |
+
model_id=model_id,
|
| 261 |
+
use_inference_api=use_inference_api,
|
| 262 |
+
hf_token=hf_token
|
| 263 |
+
)
|
src/main.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deep Research AI - Main Entry Point
|
| 3 |
+
|
| 4 |
+
A comprehensive AI-powered research system that combines web search,
|
| 5 |
+
reasoning, verification, and citation to deliver accurate, well-sourced
|
| 6 |
+
research results.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
import sys
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
from .config import Config, LLMConfig, SearchConfig, ResearchConfig
|
| 16 |
+
from .orchestrator import ResearchOrchestrator, research
|
| 17 |
+
from .models import OutputFormat, CitationStyle
|
| 18 |
+
from .modules.output_generation import AudienceType
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def create_config(
|
| 22 |
+
llm_provider: str = "openai",
|
| 23 |
+
llm_model: str | None = None,
|
| 24 |
+
search_provider: str = "brave",
|
| 25 |
+
openai_api_key: str | None = None,
|
| 26 |
+
anthropic_api_key: str | None = None,
|
| 27 |
+
search_api_key: str | None = None
|
| 28 |
+
) -> Config:
|
| 29 |
+
"""
|
| 30 |
+
Create a configuration object.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
llm_provider: LLM provider ("openai" or "anthropic")
|
| 34 |
+
llm_model: Model name (uses default for provider if not specified)
|
| 35 |
+
search_provider: Search provider ("brave", "serper", or "tavily")
|
| 36 |
+
openai_api_key: OpenAI API key
|
| 37 |
+
anthropic_api_key: Anthropic API key
|
| 38 |
+
search_api_key: Search API key
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Configured Config object
|
| 42 |
+
"""
|
| 43 |
+
# Determine model based on provider
|
| 44 |
+
if llm_model is None:
|
| 45 |
+
llm_model = "gpt-4o" if llm_provider == "openai" else "claude-sonnet-4-20250514"
|
| 46 |
+
|
| 47 |
+
llm_config = LLMConfig(
|
| 48 |
+
provider=llm_provider,
|
| 49 |
+
model=llm_model,
|
| 50 |
+
openai_api_key=openai_api_key or "",
|
| 51 |
+
anthropic_api_key=anthropic_api_key or ""
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
search_config = SearchConfig(
|
| 55 |
+
provider=search_provider,
|
| 56 |
+
api_key=search_api_key or ""
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return Config(
|
| 60 |
+
llm_config=llm_config,
|
| 61 |
+
search_config=search_config
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
async def run_research(
|
| 66 |
+
query: str,
|
| 67 |
+
config: Config | None = None,
|
| 68 |
+
audience: str = "general",
|
| 69 |
+
citation_style: str = "apa",
|
| 70 |
+
output_format: str = "markdown",
|
| 71 |
+
max_sources: int = 10,
|
| 72 |
+
verify: bool = True
|
| 73 |
+
) -> dict[str, Any]:
|
| 74 |
+
"""
|
| 75 |
+
Run a research query and return results.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
query: The research query
|
| 79 |
+
config: Configuration (uses defaults if not provided)
|
| 80 |
+
audience: Target audience (general, professional, academic, technical)
|
| 81 |
+
citation_style: Citation style (apa, mla, chicago, ieee, harvard)
|
| 82 |
+
output_format: Output format (text, markdown, html, json)
|
| 83 |
+
max_sources: Maximum number of sources to use
|
| 84 |
+
verify: Whether to verify claims
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Dictionary containing research results
|
| 88 |
+
"""
|
| 89 |
+
config = config or Config()
|
| 90 |
+
orchestrator = ResearchOrchestrator(config)
|
| 91 |
+
|
| 92 |
+
# Convert string parameters to enums
|
| 93 |
+
audience_type = AudienceType(audience.lower())
|
| 94 |
+
cit_style = CitationStyle(citation_style.upper())
|
| 95 |
+
out_format = OutputFormat(output_format.lower())
|
| 96 |
+
|
| 97 |
+
# Run research
|
| 98 |
+
result = await orchestrator.research(
|
| 99 |
+
query=query,
|
| 100 |
+
audience=audience_type,
|
| 101 |
+
citation_style=cit_style,
|
| 102 |
+
output_format=out_format,
|
| 103 |
+
max_sources=max_sources,
|
| 104 |
+
verify_claims=verify
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# Convert to dictionary
|
| 108 |
+
return {
|
| 109 |
+
"query": result.query,
|
| 110 |
+
"answer": result.answer,
|
| 111 |
+
"confidence": result.confidence,
|
| 112 |
+
"sources": [
|
| 113 |
+
{
|
| 114 |
+
"title": s.title,
|
| 115 |
+
"url": s.url,
|
| 116 |
+
"credibility_score": s.credibility_score
|
| 117 |
+
}
|
| 118 |
+
for s in result.sources
|
| 119 |
+
],
|
| 120 |
+
"verification_status": result.verification_status,
|
| 121 |
+
"metadata": result.metadata
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def main():
|
| 126 |
+
"""Main entry point for CLI usage."""
|
| 127 |
+
parser = argparse.ArgumentParser(
|
| 128 |
+
description="Deep Research AI - Comprehensive AI-powered research",
|
| 129 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 130 |
+
epilog="""
|
| 131 |
+
Examples:
|
| 132 |
+
%(prog)s "What is quantum computing?"
|
| 133 |
+
%(prog)s "Compare Python and Rust" --audience professional
|
| 134 |
+
%(prog)s "Latest AI developments" --max-sources 15 --no-verify
|
| 135 |
+
"""
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
parser.add_argument(
|
| 139 |
+
"query",
|
| 140 |
+
help="The research query to investigate"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
parser.add_argument(
|
| 144 |
+
"--llm-provider",
|
| 145 |
+
choices=["openai", "anthropic"],
|
| 146 |
+
default="openai",
|
| 147 |
+
help="LLM provider to use (default: openai)"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
parser.add_argument(
|
| 151 |
+
"--llm-model",
|
| 152 |
+
help="Specific model to use (default: provider's best model)"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
parser.add_argument(
|
| 156 |
+
"--search-provider",
|
| 157 |
+
choices=["brave", "serper", "tavily"],
|
| 158 |
+
default="brave",
|
| 159 |
+
help="Search provider to use (default: brave)"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
parser.add_argument(
|
| 163 |
+
"--audience",
|
| 164 |
+
choices=["general", "professional", "academic", "technical"],
|
| 165 |
+
default="general",
|
| 166 |
+
help="Target audience (default: general)"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
parser.add_argument(
|
| 170 |
+
"--citation-style",
|
| 171 |
+
choices=["apa", "mla", "chicago", "ieee", "harvard"],
|
| 172 |
+
default="apa",
|
| 173 |
+
help="Citation style (default: apa)"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"--output-format",
|
| 178 |
+
choices=["text", "markdown", "html", "json"],
|
| 179 |
+
default="markdown",
|
| 180 |
+
help="Output format (default: markdown)"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
parser.add_argument(
|
| 184 |
+
"--max-sources",
|
| 185 |
+
type=int,
|
| 186 |
+
default=10,
|
| 187 |
+
help="Maximum number of sources to use (default: 10)"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
parser.add_argument(
|
| 191 |
+
"--no-verify",
|
| 192 |
+
action="store_true",
|
| 193 |
+
help="Skip claim verification"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
parser.add_argument(
|
| 197 |
+
"--json-output",
|
| 198 |
+
action="store_true",
|
| 199 |
+
help="Output results as JSON"
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
parser.add_argument(
|
| 203 |
+
"--openai-api-key",
|
| 204 |
+
help="OpenAI API key (or set OPENAI_API_KEY env var)"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
parser.add_argument(
|
| 208 |
+
"--anthropic-api-key",
|
| 209 |
+
help="Anthropic API key (or set ANTHROPIC_API_KEY env var)"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
parser.add_argument(
|
| 213 |
+
"--search-api-key",
|
| 214 |
+
help="Search API key (or set SEARCH_API_KEY env var)"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
args = parser.parse_args()
|
| 218 |
+
|
| 219 |
+
# Create configuration
|
| 220 |
+
config = create_config(
|
| 221 |
+
llm_provider=args.llm_provider,
|
| 222 |
+
llm_model=args.llm_model,
|
| 223 |
+
search_provider=args.search_provider,
|
| 224 |
+
openai_api_key=args.openai_api_key,
|
| 225 |
+
anthropic_api_key=args.anthropic_api_key,
|
| 226 |
+
search_api_key=args.search_api_key
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Run research
|
| 230 |
+
print(f"🔍 Researching: {args.query}\n")
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
result = asyncio.run(run_research(
|
| 234 |
+
query=args.query,
|
| 235 |
+
config=config,
|
| 236 |
+
audience=args.audience,
|
| 237 |
+
citation_style=args.citation_style,
|
| 238 |
+
output_format=args.output_format,
|
| 239 |
+
max_sources=args.max_sources,
|
| 240 |
+
verify=not args.no_verify
|
| 241 |
+
))
|
| 242 |
+
|
| 243 |
+
if args.json_output:
|
| 244 |
+
print(json.dumps(result, indent=2))
|
| 245 |
+
else:
|
| 246 |
+
print_result(result)
|
| 247 |
+
|
| 248 |
+
except Exception as e:
|
| 249 |
+
print(f"❌ Research failed: {e}", file=sys.stderr)
|
| 250 |
+
sys.exit(1)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def print_result(result: dict) -> None:
|
| 254 |
+
"""Print research result in a readable format."""
|
| 255 |
+
print("=" * 60)
|
| 256 |
+
print("📚 RESEARCH RESULTS")
|
| 257 |
+
print("=" * 60)
|
| 258 |
+
print()
|
| 259 |
+
|
| 260 |
+
print(f"📋 Query: {result['query']}")
|
| 261 |
+
print(f"🎯 Confidence: {result['confidence']:.1%}")
|
| 262 |
+
print(f"✅ Verification: {result['verification_status']}")
|
| 263 |
+
print()
|
| 264 |
+
|
| 265 |
+
print("📝 Answer:")
|
| 266 |
+
print("-" * 40)
|
| 267 |
+
print(result['answer'])
|
| 268 |
+
print()
|
| 269 |
+
|
| 270 |
+
print(f"📚 Sources ({len(result['sources'])}):")
|
| 271 |
+
print("-" * 40)
|
| 272 |
+
for i, source in enumerate(result['sources'], 1):
|
| 273 |
+
print(f"{i}. {source['title']}")
|
| 274 |
+
print(f" URL: {source['url']}")
|
| 275 |
+
print(f" Credibility: {source['credibility_score']:.1%}")
|
| 276 |
+
print()
|
| 277 |
+
|
| 278 |
+
print("=" * 60)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
if __name__ == "__main__":
|
| 282 |
+
main()
|
src/models.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for Deep Research AI.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import List, Optional, Dict, Any
|
| 8 |
+
from enum import Enum
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class QueryComplexity(Enum):
|
| 13 |
+
"""Query complexity levels."""
|
| 14 |
+
SIMPLE = "simple"
|
| 15 |
+
MEDIUM = "medium"
|
| 16 |
+
COMPLEX = "complex"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ResearchStatus(Enum):
|
| 20 |
+
"""Research operation status."""
|
| 21 |
+
PENDING = "pending"
|
| 22 |
+
PROCESSING = "processing"
|
| 23 |
+
COMPLETED = "completed"
|
| 24 |
+
FAILED = "failed"
|
| 25 |
+
PARTIAL = "partial"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ConfidenceLevel(Enum):
|
| 29 |
+
"""Confidence level categories."""
|
| 30 |
+
VERY_HIGH = "very_high"
|
| 31 |
+
HIGH = "high"
|
| 32 |
+
MEDIUM = "medium"
|
| 33 |
+
LOW = "low"
|
| 34 |
+
VERY_LOW = "very_low"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class VerificationStatus(Enum):
|
| 38 |
+
"""Verification status for claims."""
|
| 39 |
+
VERIFIED = "verified"
|
| 40 |
+
PARTIALLY_VERIFIED = "partially_verified"
|
| 41 |
+
UNVERIFIED = "unverified"
|
| 42 |
+
DISPUTED = "disputed"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class Entity:
|
| 47 |
+
"""Entity extracted from query."""
|
| 48 |
+
text: str
|
| 49 |
+
type: str # PERSON, ORG, LOCATION, DATE, CONCEPT, PRODUCT, EVENT
|
| 50 |
+
relevance: str = "primary" # primary, secondary
|
| 51 |
+
context: Optional[str] = None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class QueryAnalysis:
|
| 56 |
+
"""Analyzed query structure."""
|
| 57 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 58 |
+
raw_query: str = ""
|
| 59 |
+
intent: str = ""
|
| 60 |
+
domain: str = ""
|
| 61 |
+
entities: List[Entity] = field(default_factory=list)
|
| 62 |
+
temporal_scope: Optional[str] = None
|
| 63 |
+
geographic_scope: Optional[str] = None
|
| 64 |
+
complexity: QueryComplexity = QueryComplexity.MEDIUM
|
| 65 |
+
output_type: str = "report"
|
| 66 |
+
sub_queries: List[str] = field(default_factory=list)
|
| 67 |
+
created_at: datetime = field(default_factory=datetime.now)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class Source:
|
| 72 |
+
"""Web source information."""
|
| 73 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 74 |
+
url: str = ""
|
| 75 |
+
title: str = ""
|
| 76 |
+
content: str = ""
|
| 77 |
+
snippet: str = ""
|
| 78 |
+
author: Optional[str] = None
|
| 79 |
+
publication_date: Optional[str] = None
|
| 80 |
+
domain: str = ""
|
| 81 |
+
credibility_score: float = 0.5
|
| 82 |
+
credibility_level: str = "medium"
|
| 83 |
+
retrieved_at: datetime = field(default_factory=datetime.now)
|
| 84 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass
|
| 88 |
+
class ExtractedInfo:
|
| 89 |
+
"""Information extracted from a source."""
|
| 90 |
+
source_id: str = ""
|
| 91 |
+
content: str = ""
|
| 92 |
+
info_type: str = "" # fact, data, quote, claim, context
|
| 93 |
+
relevance: str = "medium"
|
| 94 |
+
location: str = ""
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclass
|
| 98 |
+
class Claim:
|
| 99 |
+
"""A claim extracted from research."""
|
| 100 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 101 |
+
content: str = ""
|
| 102 |
+
source_ids: List[str] = field(default_factory=list)
|
| 103 |
+
verification_status: VerificationStatus = VerificationStatus.UNVERIFIED
|
| 104 |
+
confidence_score: float = 0.5
|
| 105 |
+
supporting_evidence: List[str] = field(default_factory=list)
|
| 106 |
+
contradicting_evidence: List[str] = field(default_factory=list)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@dataclass
|
| 110 |
+
class Finding:
|
| 111 |
+
"""A research finding."""
|
| 112 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 113 |
+
title: str = ""
|
| 114 |
+
content: str = ""
|
| 115 |
+
category: str = ""
|
| 116 |
+
confidence_score: float = 0.5
|
| 117 |
+
confidence_level: ConfidenceLevel = ConfidenceLevel.MEDIUM
|
| 118 |
+
source_ids: List[str] = field(default_factory=list)
|
| 119 |
+
claims: List[Claim] = field(default_factory=list)
|
| 120 |
+
reasoning_chain: List[str] = field(default_factory=list)
|
| 121 |
+
caveats: List[str] = field(default_factory=list)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@dataclass
|
| 125 |
+
class Conflict:
|
| 126 |
+
"""Conflicting information from sources."""
|
| 127 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 128 |
+
topic: str = ""
|
| 129 |
+
conflict_type: str = "" # factual, interpretive, temporal, scope
|
| 130 |
+
positions: List[Dict[str, str]] = field(default_factory=list)
|
| 131 |
+
severity: str = "medium"
|
| 132 |
+
resolution: Optional[str] = None
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@dataclass
|
| 136 |
+
class Citation:
|
| 137 |
+
"""A source citation."""
|
| 138 |
+
number: int = 0
|
| 139 |
+
source_id: str = ""
|
| 140 |
+
url: str = ""
|
| 141 |
+
title: str = ""
|
| 142 |
+
author: Optional[str] = None
|
| 143 |
+
date: Optional[str] = None
|
| 144 |
+
formatted: str = ""
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@dataclass
|
| 148 |
+
class VerificationResult:
|
| 149 |
+
"""Result of verification process."""
|
| 150 |
+
overall_confidence: float = 0.5
|
| 151 |
+
trust_level: str = "medium"
|
| 152 |
+
verified_claims: List[Claim] = field(default_factory=list)
|
| 153 |
+
conflicts: List[Conflict] = field(default_factory=list)
|
| 154 |
+
caveats: List[str] = field(default_factory=list)
|
| 155 |
+
flags: List[Dict[str, str]] = field(default_factory=list)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@dataclass
|
| 159 |
+
class ResearchResult:
|
| 160 |
+
"""Complete research result."""
|
| 161 |
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
| 162 |
+
query: QueryAnalysis = field(default_factory=QueryAnalysis)
|
| 163 |
+
status: ResearchStatus = ResearchStatus.PENDING
|
| 164 |
+
|
| 165 |
+
# Content
|
| 166 |
+
summary: str = ""
|
| 167 |
+
executive_summary: str = ""
|
| 168 |
+
findings: List[Finding] = field(default_factory=list)
|
| 169 |
+
sources: List[Source] = field(default_factory=list)
|
| 170 |
+
citations: List[Citation] = field(default_factory=list)
|
| 171 |
+
|
| 172 |
+
# Verification
|
| 173 |
+
verification: Optional[VerificationResult] = None
|
| 174 |
+
overall_confidence: float = 0.5
|
| 175 |
+
|
| 176 |
+
# Metadata
|
| 177 |
+
created_at: datetime = field(default_factory=datetime.now)
|
| 178 |
+
completed_at: Optional[datetime] = None
|
| 179 |
+
processing_time_seconds: float = 0.0
|
| 180 |
+
|
| 181 |
+
# Output
|
| 182 |
+
markdown_output: str = ""
|
| 183 |
+
json_output: Dict[str, Any] = field(default_factory=dict)
|
| 184 |
+
|
| 185 |
+
# Errors
|
| 186 |
+
errors: List[str] = field(default_factory=list)
|
| 187 |
+
warnings: List[str] = field(default_factory=list)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@dataclass
|
| 191 |
+
class ResearchRequest:
|
| 192 |
+
"""Incoming research request."""
|
| 193 |
+
query: str
|
| 194 |
+
mode: str = "standard" # quick, standard, deep
|
| 195 |
+
max_sources: int = 10
|
| 196 |
+
output_format: str = "markdown"
|
| 197 |
+
include_sources: bool = True
|
| 198 |
+
domain_hint: Optional[str] = None
|
src/modules/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modules for Deep Research AI.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .query_understanding import query_understanding, QueryUnderstanding
|
| 6 |
+
from .web_search import web_search, WebSearch
|
| 7 |
+
from .reasoning_engine import reasoning_engine, ReasoningEngine
|
| 8 |
+
from .verification import verification, Verification
|
| 9 |
+
from .citation import citation_manager, CitationManager
|
| 10 |
+
from .output_generation import output_generator, OutputGenerator, SummaryLength, AudienceType
|
| 11 |
+
from .error_handling import (
|
| 12 |
+
error_handler,
|
| 13 |
+
ErrorHandler,
|
| 14 |
+
ErrorSeverity,
|
| 15 |
+
ComponentType,
|
| 16 |
+
ErrorContext,
|
| 17 |
+
ResearchError,
|
| 18 |
+
QueryError,
|
| 19 |
+
SearchError,
|
| 20 |
+
ReasoningError,
|
| 21 |
+
VerificationError,
|
| 22 |
+
CitationError,
|
| 23 |
+
LLMError,
|
| 24 |
+
RateLimitError,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
__all__ = [
|
| 28 |
+
# Query Understanding
|
| 29 |
+
"query_understanding",
|
| 30 |
+
"QueryUnderstanding",
|
| 31 |
+
|
| 32 |
+
# Web Search
|
| 33 |
+
"web_search",
|
| 34 |
+
"WebSearch",
|
| 35 |
+
|
| 36 |
+
# Reasoning Engine
|
| 37 |
+
"reasoning_engine",
|
| 38 |
+
"ReasoningEngine",
|
| 39 |
+
|
| 40 |
+
# Verification
|
| 41 |
+
"verification",
|
| 42 |
+
"Verification",
|
| 43 |
+
|
| 44 |
+
# Citation
|
| 45 |
+
"citation_manager",
|
| 46 |
+
"CitationManager",
|
| 47 |
+
|
| 48 |
+
# Output Generation
|
| 49 |
+
"output_generator",
|
| 50 |
+
"OutputGenerator",
|
| 51 |
+
"SummaryLength",
|
| 52 |
+
"AudienceType",
|
| 53 |
+
|
| 54 |
+
# Error Handling
|
| 55 |
+
"error_handler",
|
| 56 |
+
"ErrorHandler",
|
| 57 |
+
"ErrorSeverity",
|
| 58 |
+
"ComponentType",
|
| 59 |
+
"ErrorContext",
|
| 60 |
+
"ResearchError",
|
| 61 |
+
"QueryError",
|
| 62 |
+
"SearchError",
|
| 63 |
+
"ReasoningError",
|
| 64 |
+
"VerificationError",
|
| 65 |
+
"CitationError",
|
| 66 |
+
"LLMError",
|
| 67 |
+
"RateLimitError",
|
| 68 |
+
]
|
src/modules/citation.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Citation module for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
This module handles citation generation, source attribution, and reference
|
| 5 |
+
list creation in multiple academic formats.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import hashlib
|
| 9 |
+
from datetime import date
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
from ..config import Config
|
| 13 |
+
from ..llm_client import LLMClient
|
| 14 |
+
from ..models import Source, Citation, CitationStyle
|
| 15 |
+
from ..prompts.citation_prompts import (
|
| 16 |
+
CITATION_GENERATION_PROMPT,
|
| 17 |
+
SOURCE_ATTRIBUTION_PROMPT,
|
| 18 |
+
REFERENCE_LIST_PROMPT,
|
| 19 |
+
INLINE_CITATION_PROMPT,
|
| 20 |
+
CITATION_VALIDATION_PROMPT,
|
| 21 |
+
SOURCE_METADATA_PROMPT,
|
| 22 |
+
FOOTNOTE_GENERATION_PROMPT,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class CitationManager:
|
| 27 |
+
"""
|
| 28 |
+
Manages citation generation, formatting, and source attribution.
|
| 29 |
+
|
| 30 |
+
Provides comprehensive citation support including:
|
| 31 |
+
- Multi-format citation generation (APA, MLA, Chicago, IEEE, Harvard)
|
| 32 |
+
- Source attribution mapping
|
| 33 |
+
- Reference list generation
|
| 34 |
+
- Inline citation insertion
|
| 35 |
+
- Citation validation
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, config: Config | None = None) -> None:
|
| 39 |
+
"""
|
| 40 |
+
Initialize the CitationManager.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
config: Configuration object. Uses default if not provided.
|
| 44 |
+
"""
|
| 45 |
+
self.config = config or Config()
|
| 46 |
+
self.llm_client = LLMClient(self.config.llm_config)
|
| 47 |
+
|
| 48 |
+
async def generate_citations(
|
| 49 |
+
self,
|
| 50 |
+
sources: list[Source],
|
| 51 |
+
content: str
|
| 52 |
+
) -> dict[str, Any]:
|
| 53 |
+
"""
|
| 54 |
+
Generate citations for sources in multiple formats.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
sources: List of Source objects to cite
|
| 58 |
+
content: Content using these sources
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Dictionary containing citations in multiple formats with metadata
|
| 62 |
+
"""
|
| 63 |
+
sources_text = self._format_sources_for_prompt(sources)
|
| 64 |
+
|
| 65 |
+
prompt = CITATION_GENERATION_PROMPT.format(
|
| 66 |
+
sources=sources_text,
|
| 67 |
+
content=content[:5000] # Limit content length
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
result = await self.llm_client.call_json(prompt)
|
| 71 |
+
|
| 72 |
+
# Convert to Citation objects
|
| 73 |
+
citations = []
|
| 74 |
+
for cit_data in result.get("citations", []):
|
| 75 |
+
citation = self._create_citation_from_data(cit_data)
|
| 76 |
+
citations.append(citation)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"citations": citations,
|
| 80 |
+
"bibliography": result.get("bibliography", {}),
|
| 81 |
+
"raw_response": result
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async def attribute_sources(
|
| 85 |
+
self,
|
| 86 |
+
content: str,
|
| 87 |
+
sources: list[Source]
|
| 88 |
+
) -> dict[str, Any]:
|
| 89 |
+
"""
|
| 90 |
+
Map claims in content to their original sources.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
content: Research content to analyze
|
| 94 |
+
sources: Available sources for attribution
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Dictionary with claim-to-source attributions
|
| 98 |
+
"""
|
| 99 |
+
sources_text = self._format_sources_for_prompt(sources)
|
| 100 |
+
|
| 101 |
+
prompt = SOURCE_ATTRIBUTION_PROMPT.format(
|
| 102 |
+
content=content,
|
| 103 |
+
sources=sources_text
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
result = await self.llm_client.call_json(prompt)
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"attributions": result.get("attributions", []),
|
| 110 |
+
"unattributed_claims": result.get("unattributed_claims", []),
|
| 111 |
+
"attribution_coverage": result.get("attribution_coverage", 0.0)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
async def generate_reference_list(
|
| 115 |
+
self,
|
| 116 |
+
sources: list[Source],
|
| 117 |
+
style: CitationStyle = CitationStyle.APA
|
| 118 |
+
) -> dict[str, Any]:
|
| 119 |
+
"""
|
| 120 |
+
Generate a formatted reference list in specified style.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
sources: Sources to include in reference list
|
| 124 |
+
style: Citation style to use
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
Dictionary with formatted reference list
|
| 128 |
+
"""
|
| 129 |
+
sources_text = self._format_sources_for_prompt(sources)
|
| 130 |
+
|
| 131 |
+
prompt = REFERENCE_LIST_PROMPT.format(
|
| 132 |
+
sources=sources_text,
|
| 133 |
+
citation_style=style.value
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
result = await self.llm_client.call_json(prompt)
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"reference_list": result.get("reference_list", []),
|
| 140 |
+
"formatted_output": result.get("formatted_output", ""),
|
| 141 |
+
"style": style.value,
|
| 142 |
+
"total_references": result.get("total_references", len(sources))
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
async def insert_inline_citations(
|
| 146 |
+
self,
|
| 147 |
+
content: str,
|
| 148 |
+
attributions: list[dict],
|
| 149 |
+
style: CitationStyle = CitationStyle.APA
|
| 150 |
+
) -> dict[str, Any]:
|
| 151 |
+
"""
|
| 152 |
+
Insert inline citations into content.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
content: Content to annotate
|
| 156 |
+
attributions: Source attributions from attribute_sources()
|
| 157 |
+
style: Citation style to use
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
Dictionary with annotated content and citation details
|
| 161 |
+
"""
|
| 162 |
+
prompt = INLINE_CITATION_PROMPT.format(
|
| 163 |
+
content=content,
|
| 164 |
+
attributions=str(attributions),
|
| 165 |
+
citation_style=style.value
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
result = await self.llm_client.call_json(prompt)
|
| 169 |
+
|
| 170 |
+
return {
|
| 171 |
+
"annotated_content": result.get("annotated_content", content),
|
| 172 |
+
"citation_count": result.get("citation_count", 0),
|
| 173 |
+
"citation_positions": result.get("citation_positions", []),
|
| 174 |
+
"style_used": style.value
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
async def validate_citations(
|
| 178 |
+
self,
|
| 179 |
+
citations: list[Citation],
|
| 180 |
+
sources: list[Source]
|
| 181 |
+
) -> dict[str, Any]:
|
| 182 |
+
"""
|
| 183 |
+
Validate citations for accuracy and completeness.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
citations: Citations to validate
|
| 187 |
+
sources: Original sources for verification
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Dictionary with validation results and recommendations
|
| 191 |
+
"""
|
| 192 |
+
citations_text = self._format_citations_for_prompt(citations)
|
| 193 |
+
sources_text = self._format_sources_for_prompt(sources)
|
| 194 |
+
|
| 195 |
+
prompt = CITATION_VALIDATION_PROMPT.format(
|
| 196 |
+
citations=citations_text,
|
| 197 |
+
sources=sources_text
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
result = await self.llm_client.call_json(prompt)
|
| 201 |
+
|
| 202 |
+
return {
|
| 203 |
+
"validation_results": result.get("validation_results", []),
|
| 204 |
+
"overall_quality": result.get("overall_quality", 0.0),
|
| 205 |
+
"total_issues": result.get("total_issues", 0),
|
| 206 |
+
"recommendations": result.get("recommendations", [])
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
async def extract_source_metadata(
|
| 210 |
+
self,
|
| 211 |
+
url: str,
|
| 212 |
+
content: str
|
| 213 |
+
) -> dict[str, Any]:
|
| 214 |
+
"""
|
| 215 |
+
Extract citation metadata from source content.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
url: Source URL
|
| 219 |
+
content: Source content to analyze
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
Dictionary with extracted metadata
|
| 223 |
+
"""
|
| 224 |
+
prompt = SOURCE_METADATA_PROMPT.format(
|
| 225 |
+
url=url,
|
| 226 |
+
content=content[:8000] # Limit content length
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
result = await self.llm_client.call_json(prompt)
|
| 230 |
+
|
| 231 |
+
return {
|
| 232 |
+
"metadata": result.get("metadata", {}),
|
| 233 |
+
"extraction_confidence": result.get("extraction_confidence", {}),
|
| 234 |
+
"inferred_fields": result.get("inferred_fields", []),
|
| 235 |
+
"missing_fields": result.get("missing_fields", [])
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
async def generate_footnotes(
|
| 239 |
+
self,
|
| 240 |
+
content: str,
|
| 241 |
+
sources: list[Source],
|
| 242 |
+
attributions: list[dict]
|
| 243 |
+
) -> dict[str, Any]:
|
| 244 |
+
"""
|
| 245 |
+
Generate footnotes for the content.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
content: Content to annotate
|
| 249 |
+
sources: Sources used
|
| 250 |
+
attributions: Source attributions
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
Dictionary with footnotes and annotated content
|
| 254 |
+
"""
|
| 255 |
+
sources_text = self._format_sources_for_prompt(sources)
|
| 256 |
+
|
| 257 |
+
prompt = FOOTNOTE_GENERATION_PROMPT.format(
|
| 258 |
+
content=content,
|
| 259 |
+
sources=sources_text,
|
| 260 |
+
attributions=str(attributions)
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
result = await self.llm_client.call_json(prompt)
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"footnotes": result.get("footnotes", []),
|
| 267 |
+
"content_with_markers": result.get("content_with_markers", content),
|
| 268 |
+
"footnote_section": result.get("footnote_section", "")
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
async def create_full_citation_package(
|
| 272 |
+
self,
|
| 273 |
+
content: str,
|
| 274 |
+
sources: list[Source],
|
| 275 |
+
style: CitationStyle = CitationStyle.APA
|
| 276 |
+
) -> dict[str, Any]:
|
| 277 |
+
"""
|
| 278 |
+
Create a complete citation package for content.
|
| 279 |
+
|
| 280 |
+
This method combines all citation operations into one comprehensive
|
| 281 |
+
result including attributions, inline citations, and reference list.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
content: Research content
|
| 285 |
+
sources: Sources used in research
|
| 286 |
+
style: Citation style to use
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
Complete citation package with all components
|
| 290 |
+
"""
|
| 291 |
+
# Step 1: Generate citations for all sources
|
| 292 |
+
citation_result = await self.generate_citations(sources, content)
|
| 293 |
+
|
| 294 |
+
# Step 2: Attribute sources to claims
|
| 295 |
+
attribution_result = await self.attribute_sources(content, sources)
|
| 296 |
+
|
| 297 |
+
# Step 3: Insert inline citations
|
| 298 |
+
inline_result = await self.insert_inline_citations(
|
| 299 |
+
content,
|
| 300 |
+
attribution_result["attributions"],
|
| 301 |
+
style
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Step 4: Generate reference list
|
| 305 |
+
reference_result = await self.generate_reference_list(sources, style)
|
| 306 |
+
|
| 307 |
+
# Step 5: Validate citations
|
| 308 |
+
validation_result = await self.validate_citations(
|
| 309 |
+
citation_result["citations"],
|
| 310 |
+
sources
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
"citations": citation_result["citations"],
|
| 315 |
+
"attributions": attribution_result["attributions"],
|
| 316 |
+
"attribution_coverage": attribution_result["attribution_coverage"],
|
| 317 |
+
"annotated_content": inline_result["annotated_content"],
|
| 318 |
+
"reference_list": reference_result["formatted_output"],
|
| 319 |
+
"citation_count": inline_result["citation_count"],
|
| 320 |
+
"validation": {
|
| 321 |
+
"quality": validation_result["overall_quality"],
|
| 322 |
+
"issues": validation_result["total_issues"],
|
| 323 |
+
"recommendations": validation_result["recommendations"]
|
| 324 |
+
},
|
| 325 |
+
"style": style.value
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
def _format_sources_for_prompt(self, sources: list[Source]) -> str:
|
| 329 |
+
"""Format sources for inclusion in prompts."""
|
| 330 |
+
formatted = []
|
| 331 |
+
for i, source in enumerate(sources, 1):
|
| 332 |
+
source_text = f"""
|
| 333 |
+
Source {i}:
|
| 334 |
+
- ID: {source.source_id}
|
| 335 |
+
- URL: {source.url}
|
| 336 |
+
- Title: {source.title}
|
| 337 |
+
- Domain: {source.domain}
|
| 338 |
+
- Content Preview: {source.content[:500] if source.content else 'N/A'}...
|
| 339 |
+
- Credibility Score: {source.credibility_score}
|
| 340 |
+
"""
|
| 341 |
+
formatted.append(source_text)
|
| 342 |
+
return "\n".join(formatted)
|
| 343 |
+
|
| 344 |
+
def _format_citations_for_prompt(self, citations: list[Citation]) -> str:
|
| 345 |
+
"""Format citations for inclusion in prompts."""
|
| 346 |
+
formatted = []
|
| 347 |
+
for citation in citations:
|
| 348 |
+
cit_text = f"""
|
| 349 |
+
Citation for: {citation.source_id}
|
| 350 |
+
- Style: {citation.style.value}
|
| 351 |
+
- Formatted: {citation.formatted_citation}
|
| 352 |
+
- In-text: {citation.in_text_citation}
|
| 353 |
+
"""
|
| 354 |
+
formatted.append(cit_text)
|
| 355 |
+
return "\n".join(formatted)
|
| 356 |
+
|
| 357 |
+
def _create_citation_from_data(self, data: dict) -> Citation:
|
| 358 |
+
"""Create a Citation object from parsed data."""
|
| 359 |
+
source_id = data.get("source_id", "unknown")
|
| 360 |
+
|
| 361 |
+
# Default to APA format
|
| 362 |
+
formats = data.get("formats", {})
|
| 363 |
+
formatted = formats.get("apa", "")
|
| 364 |
+
|
| 365 |
+
in_text = data.get("in_text", {})
|
| 366 |
+
in_text_citation = in_text.get("apa", "")
|
| 367 |
+
|
| 368 |
+
return Citation(
|
| 369 |
+
source_id=source_id,
|
| 370 |
+
style=CitationStyle.APA,
|
| 371 |
+
formatted_citation=formatted,
|
| 372 |
+
in_text_citation=in_text_citation,
|
| 373 |
+
metadata=data.get("metadata", {})
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
def generate_source_id(self, url: str) -> str:
|
| 377 |
+
"""
|
| 378 |
+
Generate a unique source ID from URL.
|
| 379 |
+
|
| 380 |
+
Args:
|
| 381 |
+
url: Source URL
|
| 382 |
+
|
| 383 |
+
Returns:
|
| 384 |
+
Unique identifier for the source
|
| 385 |
+
"""
|
| 386 |
+
return hashlib.md5(url.encode()).hexdigest()[:12]
|
| 387 |
+
|
| 388 |
+
def format_access_date(self) -> str:
|
| 389 |
+
"""Get current date formatted for citations."""
|
| 390 |
+
return date.today().strftime("%Y-%m-%d")
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# Module singleton instance
|
| 394 |
+
citation_manager = CitationManager()
|
src/modules/error_handling.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Error handling module for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
This module provides comprehensive error handling, recovery strategies,
|
| 5 |
+
graceful degradation, and user-friendly error messaging.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
import traceback
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from enum import Enum
|
| 13 |
+
from typing import Any, Callable, TypeVar
|
| 14 |
+
|
| 15 |
+
from ..config import Config
|
| 16 |
+
from ..llm_client import LLMClient
|
| 17 |
+
from ..prompts.error_prompts import (
|
| 18 |
+
ERROR_ANALYSIS_PROMPT,
|
| 19 |
+
GRACEFUL_DEGRADATION_PROMPT,
|
| 20 |
+
USER_ERROR_MESSAGE_PROMPT,
|
| 21 |
+
RETRY_STRATEGY_PROMPT,
|
| 22 |
+
ERROR_RECOVERY_PROMPT,
|
| 23 |
+
FALLBACK_CONTENT_PROMPT,
|
| 24 |
+
SYSTEM_HEALTH_PROMPT,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Set up logging
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ErrorSeverity(Enum):
|
| 33 |
+
"""Error severity levels."""
|
| 34 |
+
INFO = "info"
|
| 35 |
+
WARNING = "warning"
|
| 36 |
+
ERROR = "error"
|
| 37 |
+
CRITICAL = "critical"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class ComponentType(Enum):
|
| 41 |
+
"""System component types."""
|
| 42 |
+
QUERY_UNDERSTANDING = "query_understanding"
|
| 43 |
+
WEB_SEARCH = "web_search"
|
| 44 |
+
REASONING_ENGINE = "reasoning_engine"
|
| 45 |
+
VERIFICATION = "verification"
|
| 46 |
+
CITATION = "citation"
|
| 47 |
+
OUTPUT_GENERATION = "output_generation"
|
| 48 |
+
LLM_CLIENT = "llm_client"
|
| 49 |
+
ORCHESTRATOR = "orchestrator"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class ErrorContext:
|
| 54 |
+
"""Context information for an error."""
|
| 55 |
+
component: ComponentType
|
| 56 |
+
operation: str
|
| 57 |
+
query: str | None = None
|
| 58 |
+
partial_results: dict | None = None
|
| 59 |
+
timestamp: datetime = field(default_factory=datetime.now)
|
| 60 |
+
attempt_number: int = 1
|
| 61 |
+
max_attempts: int = 3
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class ErrorRecord:
|
| 66 |
+
"""Record of an error occurrence."""
|
| 67 |
+
error_type: str
|
| 68 |
+
error_message: str
|
| 69 |
+
context: ErrorContext
|
| 70 |
+
severity: ErrorSeverity
|
| 71 |
+
traceback_str: str | None = None
|
| 72 |
+
recovery_attempted: bool = False
|
| 73 |
+
recovery_successful: bool = False
|
| 74 |
+
timestamp: datetime = field(default_factory=datetime.now)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class ResearchError(Exception):
|
| 78 |
+
"""Base exception for research system errors."""
|
| 79 |
+
|
| 80 |
+
def __init__(
|
| 81 |
+
self,
|
| 82 |
+
message: str,
|
| 83 |
+
severity: ErrorSeverity = ErrorSeverity.ERROR,
|
| 84 |
+
recoverable: bool = True,
|
| 85 |
+
context: ErrorContext | None = None
|
| 86 |
+
):
|
| 87 |
+
super().__init__(message)
|
| 88 |
+
self.message = message
|
| 89 |
+
self.severity = severity
|
| 90 |
+
self.recoverable = recoverable
|
| 91 |
+
self.context = context
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class QueryError(ResearchError):
|
| 95 |
+
"""Error in query understanding."""
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class SearchError(ResearchError):
|
| 100 |
+
"""Error in web search."""
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class ReasoningError(ResearchError):
|
| 105 |
+
"""Error in reasoning engine."""
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class VerificationError(ResearchError):
|
| 110 |
+
"""Error in verification."""
|
| 111 |
+
pass
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class CitationError(ResearchError):
|
| 115 |
+
"""Error in citation generation."""
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class LLMError(ResearchError):
|
| 120 |
+
"""Error in LLM communication."""
|
| 121 |
+
pass
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class RateLimitError(ResearchError):
|
| 125 |
+
"""Rate limit exceeded error."""
|
| 126 |
+
pass
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# Type variable for generic retry function
|
| 130 |
+
T = TypeVar('T')
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class ErrorHandler:
|
| 134 |
+
"""
|
| 135 |
+
Comprehensive error handling for the research system.
|
| 136 |
+
|
| 137 |
+
Provides:
|
| 138 |
+
- Error analysis and diagnosis
|
| 139 |
+
- Graceful degradation
|
| 140 |
+
- Retry strategies
|
| 141 |
+
- Recovery orchestration
|
| 142 |
+
- User-friendly error messages
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
def __init__(self, config: Config | None = None) -> None:
|
| 146 |
+
"""
|
| 147 |
+
Initialize the ErrorHandler.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
config: Configuration object. Uses default if not provided.
|
| 151 |
+
"""
|
| 152 |
+
self.config = config or Config()
|
| 153 |
+
self.llm_client = LLMClient(self.config.llm_config)
|
| 154 |
+
self.error_history: list[ErrorRecord] = []
|
| 155 |
+
self.max_history = 100
|
| 156 |
+
|
| 157 |
+
def record_error(
|
| 158 |
+
self,
|
| 159 |
+
error: Exception,
|
| 160 |
+
context: ErrorContext,
|
| 161 |
+
severity: ErrorSeverity = ErrorSeverity.ERROR
|
| 162 |
+
) -> ErrorRecord:
|
| 163 |
+
"""
|
| 164 |
+
Record an error occurrence.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
error: The exception that occurred
|
| 168 |
+
context: Error context
|
| 169 |
+
severity: Error severity level
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
ErrorRecord object
|
| 173 |
+
"""
|
| 174 |
+
record = ErrorRecord(
|
| 175 |
+
error_type=type(error).__name__,
|
| 176 |
+
error_message=str(error),
|
| 177 |
+
context=context,
|
| 178 |
+
severity=severity,
|
| 179 |
+
traceback_str=traceback.format_exc()
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
self.error_history.append(record)
|
| 183 |
+
|
| 184 |
+
# Trim history if needed
|
| 185 |
+
if len(self.error_history) > self.max_history:
|
| 186 |
+
self.error_history = self.error_history[-self.max_history:]
|
| 187 |
+
|
| 188 |
+
# Log the error
|
| 189 |
+
log_level = {
|
| 190 |
+
ErrorSeverity.INFO: logging.INFO,
|
| 191 |
+
ErrorSeverity.WARNING: logging.WARNING,
|
| 192 |
+
ErrorSeverity.ERROR: logging.ERROR,
|
| 193 |
+
ErrorSeverity.CRITICAL: logging.CRITICAL
|
| 194 |
+
}.get(severity, logging.ERROR)
|
| 195 |
+
|
| 196 |
+
logger.log(
|
| 197 |
+
log_level,
|
| 198 |
+
f"Error in {context.component.value}: {record.error_message}"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
return record
|
| 202 |
+
|
| 203 |
+
async def analyze_error(
|
| 204 |
+
self,
|
| 205 |
+
error: Exception,
|
| 206 |
+
context: ErrorContext
|
| 207 |
+
) -> dict[str, Any]:
|
| 208 |
+
"""
|
| 209 |
+
Analyze an error and suggest recovery strategies.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
error: The exception to analyze
|
| 213 |
+
context: Error context
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
Dictionary with analysis and recovery suggestions
|
| 217 |
+
"""
|
| 218 |
+
prompt = ERROR_ANALYSIS_PROMPT.format(
|
| 219 |
+
error_type=type(error).__name__,
|
| 220 |
+
error_message=str(error),
|
| 221 |
+
context=str(context),
|
| 222 |
+
component=context.component.value
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
result = await self.llm_client.call_json(prompt)
|
| 227 |
+
return result
|
| 228 |
+
except Exception as e:
|
| 229 |
+
# Fallback if LLM analysis fails
|
| 230 |
+
logger.warning(f"Error analysis failed: {e}")
|
| 231 |
+
return {
|
| 232 |
+
"analysis": {
|
| 233 |
+
"root_cause": "Unknown",
|
| 234 |
+
"impact_level": "medium",
|
| 235 |
+
"is_recoverable": True
|
| 236 |
+
},
|
| 237 |
+
"user_message": "An error occurred. Please try again.",
|
| 238 |
+
"recovery_strategies": []
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
async def get_degraded_response(
|
| 242 |
+
self,
|
| 243 |
+
operation: str,
|
| 244 |
+
partial_results: dict | None,
|
| 245 |
+
missing_components: list[str]
|
| 246 |
+
) -> dict[str, Any]:
|
| 247 |
+
"""
|
| 248 |
+
Get a gracefully degraded response when full operation fails.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
operation: The failed operation
|
| 252 |
+
partial_results: Any partial results available
|
| 253 |
+
missing_components: Components that failed
|
| 254 |
+
|
| 255 |
+
Returns:
|
| 256 |
+
Dictionary with degraded response strategy
|
| 257 |
+
"""
|
| 258 |
+
prompt = GRACEFUL_DEGRADATION_PROMPT.format(
|
| 259 |
+
operation=operation,
|
| 260 |
+
partial_results=str(partial_results) if partial_results else "None",
|
| 261 |
+
missing_components=str(missing_components)
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
result = await self.llm_client.call_json(prompt)
|
| 266 |
+
return result
|
| 267 |
+
except Exception:
|
| 268 |
+
return {
|
| 269 |
+
"degraded_response": {
|
| 270 |
+
"can_provide_partial": partial_results is not None,
|
| 271 |
+
"available_results": partial_results,
|
| 272 |
+
"quality_reduction": 0.5
|
| 273 |
+
},
|
| 274 |
+
"user_communication": {
|
| 275 |
+
"message": "We encountered some issues but have partial results.",
|
| 276 |
+
"limitations_explained": missing_components
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
async def generate_user_message(
|
| 281 |
+
self,
|
| 282 |
+
error: Exception,
|
| 283 |
+
user_action: str,
|
| 284 |
+
severity: ErrorSeverity
|
| 285 |
+
) -> dict[str, Any]:
|
| 286 |
+
"""
|
| 287 |
+
Generate a user-friendly error message.
|
| 288 |
+
|
| 289 |
+
Args:
|
| 290 |
+
error: The exception
|
| 291 |
+
user_action: What the user was trying to do
|
| 292 |
+
severity: Error severity
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
Dictionary with user-friendly message
|
| 296 |
+
"""
|
| 297 |
+
prompt = USER_ERROR_MESSAGE_PROMPT.format(
|
| 298 |
+
error_type=type(error).__name__,
|
| 299 |
+
technical_message=str(error),
|
| 300 |
+
user_action=user_action,
|
| 301 |
+
severity=severity.value
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
try:
|
| 305 |
+
result = await self.llm_client.call_json(prompt)
|
| 306 |
+
return result
|
| 307 |
+
except Exception:
|
| 308 |
+
return {
|
| 309 |
+
"user_message": {
|
| 310 |
+
"headline": "Something went wrong",
|
| 311 |
+
"explanation": "We encountered an issue processing your request.",
|
| 312 |
+
"what_to_do": "Please try again. If the problem persists, try rephrasing your query.",
|
| 313 |
+
"tone": "apologetic"
|
| 314 |
+
},
|
| 315 |
+
"severity_indicator": severity.value
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
async def get_retry_strategy(
|
| 319 |
+
self,
|
| 320 |
+
operation: str,
|
| 321 |
+
failure_reason: str,
|
| 322 |
+
attempt_number: int,
|
| 323 |
+
context: dict
|
| 324 |
+
) -> dict[str, Any]:
|
| 325 |
+
"""
|
| 326 |
+
Determine optimal retry strategy.
|
| 327 |
+
|
| 328 |
+
Args:
|
| 329 |
+
operation: Failed operation
|
| 330 |
+
failure_reason: Why it failed
|
| 331 |
+
attempt_number: Current attempt number
|
| 332 |
+
context: Operation context
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
Dictionary with retry strategy
|
| 336 |
+
"""
|
| 337 |
+
prompt = RETRY_STRATEGY_PROMPT.format(
|
| 338 |
+
operation=operation,
|
| 339 |
+
failure_reason=failure_reason,
|
| 340 |
+
attempt_number=attempt_number,
|
| 341 |
+
context=str(context)
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
try:
|
| 345 |
+
result = await self.llm_client.call_json(prompt)
|
| 346 |
+
return result
|
| 347 |
+
except Exception:
|
| 348 |
+
# Default retry strategy
|
| 349 |
+
return {
|
| 350 |
+
"retry_decision": {
|
| 351 |
+
"should_retry": attempt_number < 3,
|
| 352 |
+
"max_attempts": 3,
|
| 353 |
+
"current_attempt": attempt_number
|
| 354 |
+
},
|
| 355 |
+
"timing": {
|
| 356 |
+
"delay_seconds": attempt_number * 2,
|
| 357 |
+
"backoff_strategy": "exponential"
|
| 358 |
+
},
|
| 359 |
+
"modifications": {
|
| 360 |
+
"modify_request": False
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
async def orchestrate_recovery(
|
| 365 |
+
self,
|
| 366 |
+
current_state: dict,
|
| 367 |
+
error_chain: list[ErrorRecord]
|
| 368 |
+
) -> dict[str, Any]:
|
| 369 |
+
"""
|
| 370 |
+
Orchestrate recovery from error state.
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
current_state: Current system state
|
| 374 |
+
error_chain: Chain of errors that occurred
|
| 375 |
+
|
| 376 |
+
Returns:
|
| 377 |
+
Dictionary with recovery plan
|
| 378 |
+
"""
|
| 379 |
+
error_chain_text = "\n".join([
|
| 380 |
+
f"- {e.error_type}: {e.error_message}"
|
| 381 |
+
for e in error_chain
|
| 382 |
+
])
|
| 383 |
+
|
| 384 |
+
prompt = ERROR_RECOVERY_PROMPT.format(
|
| 385 |
+
current_state=str(current_state),
|
| 386 |
+
error_chain=error_chain_text,
|
| 387 |
+
available_resources=str(list(ComponentType))
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
result = await self.llm_client.call_json(prompt)
|
| 392 |
+
return result
|
| 393 |
+
except Exception:
|
| 394 |
+
return {
|
| 395 |
+
"state_assessment": {
|
| 396 |
+
"corruption_level": "partial"
|
| 397 |
+
},
|
| 398 |
+
"recovery_plan": [
|
| 399 |
+
{
|
| 400 |
+
"step": 1,
|
| 401 |
+
"action": "Reset to clean state",
|
| 402 |
+
"fallback": "Manual intervention required"
|
| 403 |
+
}
|
| 404 |
+
]
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
async def generate_fallback_content(
|
| 408 |
+
self,
|
| 409 |
+
query: str,
|
| 410 |
+
available_info: dict | None,
|
| 411 |
+
failed_sources: list[str],
|
| 412 |
+
cached_data: dict | None = None
|
| 413 |
+
) -> dict[str, Any]:
|
| 414 |
+
"""
|
| 415 |
+
Generate fallback content when primary sources fail.
|
| 416 |
+
|
| 417 |
+
Args:
|
| 418 |
+
query: Original query
|
| 419 |
+
available_info: Any available information
|
| 420 |
+
failed_sources: Sources that failed
|
| 421 |
+
cached_data: Any cached data available
|
| 422 |
+
|
| 423 |
+
Returns:
|
| 424 |
+
Dictionary with fallback content
|
| 425 |
+
"""
|
| 426 |
+
prompt = FALLBACK_CONTENT_PROMPT.format(
|
| 427 |
+
query=query,
|
| 428 |
+
available_info=str(available_info) if available_info else "None",
|
| 429 |
+
failed_sources=str(failed_sources),
|
| 430 |
+
cached_data=str(cached_data) if cached_data else "None"
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
try:
|
| 434 |
+
result = await self.llm_client.call_json(prompt)
|
| 435 |
+
return result
|
| 436 |
+
except Exception:
|
| 437 |
+
return {
|
| 438 |
+
"fallback_content": {
|
| 439 |
+
"response": "Unable to complete the research at this time.",
|
| 440 |
+
"confidence": 0.0,
|
| 441 |
+
"completeness": 0.0
|
| 442 |
+
},
|
| 443 |
+
"limitations_disclosure": {
|
| 444 |
+
"what_is_missing": failed_sources,
|
| 445 |
+
"quality_impact": "significant"
|
| 446 |
+
}
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
async def check_system_health(
|
| 450 |
+
self,
|
| 451 |
+
health_metrics: dict,
|
| 452 |
+
performance_data: dict
|
| 453 |
+
) -> dict[str, Any]:
|
| 454 |
+
"""
|
| 455 |
+
Check overall system health.
|
| 456 |
+
|
| 457 |
+
Args:
|
| 458 |
+
health_metrics: Health metrics from components
|
| 459 |
+
performance_data: Performance statistics
|
| 460 |
+
|
| 461 |
+
Returns:
|
| 462 |
+
Dictionary with health assessment
|
| 463 |
+
"""
|
| 464 |
+
recent_errors = [
|
| 465 |
+
{"type": e.error_type, "message": e.error_message}
|
| 466 |
+
for e in self.error_history[-10:]
|
| 467 |
+
]
|
| 468 |
+
|
| 469 |
+
prompt = SYSTEM_HEALTH_PROMPT.format(
|
| 470 |
+
health_metrics=str(health_metrics),
|
| 471 |
+
recent_errors=str(recent_errors),
|
| 472 |
+
performance_data=str(performance_data)
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
try:
|
| 476 |
+
result = await self.llm_client.call_json(prompt)
|
| 477 |
+
return result
|
| 478 |
+
except Exception:
|
| 479 |
+
# Calculate simple health based on error rate
|
| 480 |
+
error_count = len(self.error_history)
|
| 481 |
+
health_score = max(0.0, 1.0 - (error_count / 100))
|
| 482 |
+
|
| 483 |
+
return {
|
| 484 |
+
"health_status": {
|
| 485 |
+
"overall": "healthy" if health_score > 0.7 else "degraded",
|
| 486 |
+
"score": health_score
|
| 487 |
+
},
|
| 488 |
+
"active_issues": [],
|
| 489 |
+
"recommendations": []
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
async def retry_with_backoff(
|
| 493 |
+
self,
|
| 494 |
+
func: Callable[..., T],
|
| 495 |
+
*args,
|
| 496 |
+
max_attempts: int = 3,
|
| 497 |
+
initial_delay: float = 1.0,
|
| 498 |
+
backoff_factor: float = 2.0,
|
| 499 |
+
**kwargs
|
| 500 |
+
) -> T:
|
| 501 |
+
"""
|
| 502 |
+
Retry a function with exponential backoff.
|
| 503 |
+
|
| 504 |
+
Args:
|
| 505 |
+
func: Async function to retry
|
| 506 |
+
*args: Positional arguments for func
|
| 507 |
+
max_attempts: Maximum retry attempts
|
| 508 |
+
initial_delay: Initial delay in seconds
|
| 509 |
+
backoff_factor: Backoff multiplier
|
| 510 |
+
**kwargs: Keyword arguments for func
|
| 511 |
+
|
| 512 |
+
Returns:
|
| 513 |
+
Result from successful function call
|
| 514 |
+
|
| 515 |
+
Raises:
|
| 516 |
+
Last exception if all retries fail
|
| 517 |
+
"""
|
| 518 |
+
import asyncio
|
| 519 |
+
|
| 520 |
+
last_exception = None
|
| 521 |
+
delay = initial_delay
|
| 522 |
+
|
| 523 |
+
for attempt in range(1, max_attempts + 1):
|
| 524 |
+
try:
|
| 525 |
+
return await func(*args, **kwargs)
|
| 526 |
+
except Exception as e:
|
| 527 |
+
last_exception = e
|
| 528 |
+
|
| 529 |
+
if attempt < max_attempts:
|
| 530 |
+
logger.warning(
|
| 531 |
+
f"Attempt {attempt} failed: {e}. Retrying in {delay}s..."
|
| 532 |
+
)
|
| 533 |
+
await asyncio.sleep(delay)
|
| 534 |
+
delay *= backoff_factor
|
| 535 |
+
|
| 536 |
+
raise last_exception
|
| 537 |
+
|
| 538 |
+
def get_error_summary(self) -> dict[str, Any]:
|
| 539 |
+
"""
|
| 540 |
+
Get a summary of recent errors.
|
| 541 |
+
|
| 542 |
+
Returns:
|
| 543 |
+
Dictionary with error statistics and recent errors
|
| 544 |
+
"""
|
| 545 |
+
if not self.error_history:
|
| 546 |
+
return {
|
| 547 |
+
"total_errors": 0,
|
| 548 |
+
"by_severity": {},
|
| 549 |
+
"by_component": {},
|
| 550 |
+
"recent_errors": []
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
by_severity = {}
|
| 554 |
+
by_component = {}
|
| 555 |
+
|
| 556 |
+
for record in self.error_history:
|
| 557 |
+
sev = record.severity.value
|
| 558 |
+
by_severity[sev] = by_severity.get(sev, 0) + 1
|
| 559 |
+
|
| 560 |
+
comp = record.context.component.value
|
| 561 |
+
by_component[comp] = by_component.get(comp, 0) + 1
|
| 562 |
+
|
| 563 |
+
return {
|
| 564 |
+
"total_errors": len(self.error_history),
|
| 565 |
+
"by_severity": by_severity,
|
| 566 |
+
"by_component": by_component,
|
| 567 |
+
"recent_errors": [
|
| 568 |
+
{
|
| 569 |
+
"type": e.error_type,
|
| 570 |
+
"message": e.error_message,
|
| 571 |
+
"component": e.context.component.value,
|
| 572 |
+
"timestamp": e.timestamp.isoformat()
|
| 573 |
+
}
|
| 574 |
+
for e in self.error_history[-5:]
|
| 575 |
+
]
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
def clear_error_history(self) -> None:
|
| 579 |
+
"""Clear the error history."""
|
| 580 |
+
self.error_history = []
|
| 581 |
+
logger.info("Error history cleared")
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
# Module singleton instance
|
| 585 |
+
error_handler = ErrorHandler()
|
src/modules/output_generation.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Output generation module for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
This module handles the final synthesis and formatting of research results
|
| 5 |
+
into user-friendly, well-structured outputs in various formats.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from enum import Enum
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
from ..config import Config
|
| 12 |
+
from ..llm_client import LLMClient
|
| 13 |
+
from ..models import Source, ResearchResult, OutputFormat
|
| 14 |
+
from ..prompts.output_prompts import (
|
| 15 |
+
REPORT_GENERATION_PROMPT,
|
| 16 |
+
SUMMARY_GENERATION_PROMPT,
|
| 17 |
+
ANSWER_FORMATTING_PROMPT,
|
| 18 |
+
VISUALIZATION_SUGGESTION_PROMPT,
|
| 19 |
+
MULTI_FORMAT_OUTPUT_PROMPT,
|
| 20 |
+
RESPONSE_QUALITY_PROMPT,
|
| 21 |
+
FOLLOWUP_QUESTIONS_PROMPT,
|
| 22 |
+
EXPORT_FORMAT_PROMPT,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class SummaryLength(Enum):
|
| 27 |
+
"""Summary length options."""
|
| 28 |
+
BRIEF = "brief"
|
| 29 |
+
STANDARD = "standard"
|
| 30 |
+
DETAILED = "detailed"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class AudienceType(Enum):
|
| 34 |
+
"""Target audience types."""
|
| 35 |
+
GENERAL = "general"
|
| 36 |
+
PROFESSIONAL = "professional"
|
| 37 |
+
ACADEMIC = "academic"
|
| 38 |
+
TECHNICAL = "technical"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class ExportFormat(Enum):
|
| 42 |
+
"""Export format options."""
|
| 43 |
+
PDF = "pdf"
|
| 44 |
+
DOCX = "docx"
|
| 45 |
+
SLIDES = "slides"
|
| 46 |
+
EMAIL = "email"
|
| 47 |
+
SOCIAL = "social"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class OutputGenerator:
|
| 51 |
+
"""
|
| 52 |
+
Generates formatted output from research results.
|
| 53 |
+
|
| 54 |
+
Provides comprehensive output generation including:
|
| 55 |
+
- Full research reports
|
| 56 |
+
- Summaries at various lengths
|
| 57 |
+
- Multi-format output generation
|
| 58 |
+
- Quality assessment
|
| 59 |
+
- Follow-up question generation
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
def __init__(self, config: Config | None = None) -> None:
|
| 63 |
+
"""
|
| 64 |
+
Initialize the OutputGenerator.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
config: Configuration object. Uses default if not provided.
|
| 68 |
+
"""
|
| 69 |
+
self.config = config or Config()
|
| 70 |
+
self.llm_client = LLMClient(self.config.llm_config)
|
| 71 |
+
|
| 72 |
+
async def generate_report(
|
| 73 |
+
self,
|
| 74 |
+
query: str,
|
| 75 |
+
findings: dict[str, Any],
|
| 76 |
+
sources: list[Source],
|
| 77 |
+
confidence: float
|
| 78 |
+
) -> dict[str, Any]:
|
| 79 |
+
"""
|
| 80 |
+
Generate a comprehensive research report.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
query: Original research query
|
| 84 |
+
findings: Synthesized research findings
|
| 85 |
+
sources: Sources used in research
|
| 86 |
+
confidence: Overall confidence score
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Dictionary containing the full research report
|
| 90 |
+
"""
|
| 91 |
+
sources_text = self._format_sources(sources)
|
| 92 |
+
|
| 93 |
+
prompt = REPORT_GENERATION_PROMPT.format(
|
| 94 |
+
query=query,
|
| 95 |
+
findings=str(findings),
|
| 96 |
+
sources=sources_text,
|
| 97 |
+
confidence=f"{confidence:.2%}"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
result = await self.llm_client.call_json(prompt)
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
"report": result.get("report", {}),
|
| 104 |
+
"metadata": result.get("metadata", {}),
|
| 105 |
+
"format": OutputFormat.MARKDOWN
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
async def generate_summary(
|
| 109 |
+
self,
|
| 110 |
+
findings: dict[str, Any],
|
| 111 |
+
length: SummaryLength = SummaryLength.STANDARD
|
| 112 |
+
) -> dict[str, Any]:
|
| 113 |
+
"""
|
| 114 |
+
Generate a summary of research findings.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
findings: Research findings to summarize
|
| 118 |
+
length: Desired summary length
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
Dictionary containing the summary
|
| 122 |
+
"""
|
| 123 |
+
prompt = SUMMARY_GENERATION_PROMPT.format(
|
| 124 |
+
findings=str(findings),
|
| 125 |
+
length=length.value
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
result = await self.llm_client.call_json(prompt)
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
"summary": result.get("summary", {}),
|
| 132 |
+
"metadata": result.get("metadata", {})
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
async def format_answer(
|
| 136 |
+
self,
|
| 137 |
+
answer: str,
|
| 138 |
+
audience: AudienceType = AudienceType.GENERAL,
|
| 139 |
+
output_format: OutputFormat = OutputFormat.MARKDOWN
|
| 140 |
+
) -> dict[str, Any]:
|
| 141 |
+
"""
|
| 142 |
+
Format an answer for a specific audience and format.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
answer: The answer to format
|
| 146 |
+
audience: Target audience
|
| 147 |
+
output_format: Desired output format
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Dictionary containing the formatted answer
|
| 151 |
+
"""
|
| 152 |
+
prompt = ANSWER_FORMATTING_PROMPT.format(
|
| 153 |
+
answer=answer,
|
| 154 |
+
audience=audience.value,
|
| 155 |
+
format=output_format.value
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
result = await self.llm_client.call_json(prompt)
|
| 159 |
+
|
| 160 |
+
return {
|
| 161 |
+
"formatted_answer": result.get("formatted_answer", {}),
|
| 162 |
+
"readability_metrics": result.get("readability_metrics", {})
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
async def suggest_visualizations(
|
| 166 |
+
self,
|
| 167 |
+
data: dict[str, Any],
|
| 168 |
+
findings: dict[str, Any]
|
| 169 |
+
) -> dict[str, Any]:
|
| 170 |
+
"""
|
| 171 |
+
Suggest visualizations for research data.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
data: Numerical or structured data
|
| 175 |
+
findings: Research findings
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
Dictionary with visualization suggestions
|
| 179 |
+
"""
|
| 180 |
+
prompt = VISUALIZATION_SUGGESTION_PROMPT.format(
|
| 181 |
+
data=str(data),
|
| 182 |
+
findings=str(findings)
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
result = await self.llm_client.call_json(prompt)
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
"visualizations": result.get("visualizations", []),
|
| 189 |
+
"recommended_count": result.get("recommended_count", 0),
|
| 190 |
+
"data_visualization_potential": result.get("data_visualization_potential", "low")
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
async def generate_multi_format(
|
| 194 |
+
self,
|
| 195 |
+
content: str,
|
| 196 |
+
citations: str
|
| 197 |
+
) -> dict[str, Any]:
|
| 198 |
+
"""
|
| 199 |
+
Generate output in multiple formats simultaneously.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
content: Research content
|
| 203 |
+
citations: Citation information
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
Dictionary with content in multiple formats
|
| 207 |
+
"""
|
| 208 |
+
prompt = MULTI_FORMAT_OUTPUT_PROMPT.format(
|
| 209 |
+
content=content,
|
| 210 |
+
citations=citations
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
result = await self.llm_client.call_json(prompt)
|
| 214 |
+
|
| 215 |
+
return {
|
| 216 |
+
"outputs": result.get("outputs", {}),
|
| 217 |
+
"recommended_format": result.get("recommended_format", "markdown"),
|
| 218 |
+
"format_notes": result.get("format_notes", {})
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
async def assess_quality(
|
| 222 |
+
self,
|
| 223 |
+
query: str,
|
| 224 |
+
response: str,
|
| 225 |
+
sources: list[Source]
|
| 226 |
+
) -> dict[str, Any]:
|
| 227 |
+
"""
|
| 228 |
+
Assess the quality of a generated response.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
query: Original query
|
| 232 |
+
response: Generated response
|
| 233 |
+
sources: Sources used
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
Dictionary with quality assessment
|
| 237 |
+
"""
|
| 238 |
+
sources_text = self._format_sources(sources)
|
| 239 |
+
|
| 240 |
+
prompt = RESPONSE_QUALITY_PROMPT.format(
|
| 241 |
+
query=query,
|
| 242 |
+
response=response,
|
| 243 |
+
sources=sources_text
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
result = await self.llm_client.call_json(prompt)
|
| 247 |
+
|
| 248 |
+
return {
|
| 249 |
+
"quality_assessment": result.get("quality_assessment", {}),
|
| 250 |
+
"confidence_level": result.get("confidence_level", "medium"),
|
| 251 |
+
"ready_for_delivery": result.get("ready_for_delivery", False),
|
| 252 |
+
"revision_needed": result.get("revision_needed", True)
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
async def generate_followup_questions(
|
| 256 |
+
self,
|
| 257 |
+
query: str,
|
| 258 |
+
findings: dict[str, Any],
|
| 259 |
+
gaps: list[str]
|
| 260 |
+
) -> dict[str, Any]:
|
| 261 |
+
"""
|
| 262 |
+
Generate relevant follow-up questions.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
query: Original query
|
| 266 |
+
findings: Research findings
|
| 267 |
+
gaps: Identified information gaps
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
Dictionary with follow-up questions
|
| 271 |
+
"""
|
| 272 |
+
prompt = FOLLOWUP_QUESTIONS_PROMPT.format(
|
| 273 |
+
query=query,
|
| 274 |
+
findings=str(findings),
|
| 275 |
+
gaps=str(gaps)
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
result = await self.llm_client.call_json(prompt)
|
| 279 |
+
|
| 280 |
+
return {
|
| 281 |
+
"follow_up_questions": result.get("follow_up_questions", []),
|
| 282 |
+
"recommended_next_question": result.get("recommended_next_question", ""),
|
| 283 |
+
"research_continuation_score": result.get("research_continuation_score", 0.0)
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
async def prepare_for_export(
|
| 287 |
+
self,
|
| 288 |
+
report: dict[str, Any],
|
| 289 |
+
export_format: ExportFormat
|
| 290 |
+
) -> dict[str, Any]:
|
| 291 |
+
"""
|
| 292 |
+
Prepare research output for export.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
report: Research report
|
| 296 |
+
export_format: Target export format
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
Dictionary with export-ready content
|
| 300 |
+
"""
|
| 301 |
+
prompt = EXPORT_FORMAT_PROMPT.format(
|
| 302 |
+
report=str(report),
|
| 303 |
+
export_format=export_format.value
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
result = await self.llm_client.call_json(prompt)
|
| 307 |
+
|
| 308 |
+
return {
|
| 309 |
+
"export_ready": result.get("export_ready", {}),
|
| 310 |
+
"export_metadata": result.get("export_metadata", {})
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
async def create_research_result(
|
| 314 |
+
self,
|
| 315 |
+
query: str,
|
| 316 |
+
findings: dict[str, Any],
|
| 317 |
+
sources: list[Source],
|
| 318 |
+
confidence: float,
|
| 319 |
+
audience: AudienceType = AudienceType.GENERAL
|
| 320 |
+
) -> ResearchResult:
|
| 321 |
+
"""
|
| 322 |
+
Create a complete ResearchResult object.
|
| 323 |
+
|
| 324 |
+
This method combines all output generation capabilities into
|
| 325 |
+
a single comprehensive research result.
|
| 326 |
+
|
| 327 |
+
Args:
|
| 328 |
+
query: Original query
|
| 329 |
+
findings: Research findings
|
| 330 |
+
sources: Sources used
|
| 331 |
+
confidence: Confidence score
|
| 332 |
+
audience: Target audience
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
Complete ResearchResult object
|
| 336 |
+
"""
|
| 337 |
+
# Generate the main report
|
| 338 |
+
report_result = await self.generate_report(
|
| 339 |
+
query, findings, sources, confidence
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Generate summary
|
| 343 |
+
summary_result = await self.generate_summary(
|
| 344 |
+
findings, SummaryLength.STANDARD
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
# Assess quality
|
| 348 |
+
report_text = self._report_to_text(report_result["report"])
|
| 349 |
+
quality_result = await self.assess_quality(query, report_text, sources)
|
| 350 |
+
|
| 351 |
+
# Generate follow-up questions
|
| 352 |
+
gaps = findings.get("information_gaps", [])
|
| 353 |
+
followup_result = await self.generate_followup_questions(
|
| 354 |
+
query, findings, gaps
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
# Build the research result
|
| 358 |
+
return ResearchResult(
|
| 359 |
+
query=query,
|
| 360 |
+
answer=summary_result["summary"].get("text", ""),
|
| 361 |
+
confidence=confidence,
|
| 362 |
+
sources=sources,
|
| 363 |
+
reasoning_steps=findings.get("reasoning_steps", []),
|
| 364 |
+
verification_status=findings.get("verification_status", "unverified"),
|
| 365 |
+
metadata={
|
| 366 |
+
"full_report": report_result["report"],
|
| 367 |
+
"quality_assessment": quality_result["quality_assessment"],
|
| 368 |
+
"follow_up_questions": followup_result["follow_up_questions"],
|
| 369 |
+
"audience": audience.value
|
| 370 |
+
}
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
def _format_sources(self, sources: list[Source]) -> str:
|
| 374 |
+
"""Format sources for prompts."""
|
| 375 |
+
formatted = []
|
| 376 |
+
for i, source in enumerate(sources, 1):
|
| 377 |
+
formatted.append(f"""
|
| 378 |
+
Source {i}:
|
| 379 |
+
- Title: {source.title}
|
| 380 |
+
- URL: {source.url}
|
| 381 |
+
- Credibility: {source.credibility_score}
|
| 382 |
+
""")
|
| 383 |
+
return "\n".join(formatted)
|
| 384 |
+
|
| 385 |
+
def _report_to_text(self, report: dict) -> str:
|
| 386 |
+
"""Convert report dict to plain text."""
|
| 387 |
+
parts = []
|
| 388 |
+
|
| 389 |
+
if "title" in report:
|
| 390 |
+
parts.append(f"# {report['title']}\n")
|
| 391 |
+
|
| 392 |
+
if "executive_summary" in report:
|
| 393 |
+
parts.append(f"## Executive Summary\n{report['executive_summary']}\n")
|
| 394 |
+
|
| 395 |
+
if "main_findings" in report:
|
| 396 |
+
parts.append("## Main Findings\n")
|
| 397 |
+
for finding in report["main_findings"]:
|
| 398 |
+
parts.append(f"### {finding.get('theme', 'Finding')}\n")
|
| 399 |
+
parts.append(f"{finding.get('content', '')}\n")
|
| 400 |
+
|
| 401 |
+
if "conclusion" in report:
|
| 402 |
+
conclusion = report["conclusion"]
|
| 403 |
+
parts.append("## Conclusion\n")
|
| 404 |
+
parts.append(f"{conclusion.get('answer', '')}\n")
|
| 405 |
+
|
| 406 |
+
return "\n".join(parts)
|
| 407 |
+
|
| 408 |
+
def render_markdown(self, report: dict) -> str:
|
| 409 |
+
"""
|
| 410 |
+
Render a report as markdown.
|
| 411 |
+
|
| 412 |
+
Args:
|
| 413 |
+
report: Report dictionary
|
| 414 |
+
|
| 415 |
+
Returns:
|
| 416 |
+
Markdown formatted string
|
| 417 |
+
"""
|
| 418 |
+
return self._report_to_text(report)
|
| 419 |
+
|
| 420 |
+
def render_html(self, report: dict) -> str:
|
| 421 |
+
"""
|
| 422 |
+
Render a report as HTML.
|
| 423 |
+
|
| 424 |
+
Args:
|
| 425 |
+
report: Report dictionary
|
| 426 |
+
|
| 427 |
+
Returns:
|
| 428 |
+
HTML formatted string
|
| 429 |
+
"""
|
| 430 |
+
md = self._report_to_text(report)
|
| 431 |
+
# Basic markdown to HTML conversion
|
| 432 |
+
html = md.replace("# ", "<h1>").replace("\n## ", "</h1>\n<h2>")
|
| 433 |
+
html = html.replace("\n### ", "</h2>\n<h3>").replace("\n\n", "</p>\n<p>")
|
| 434 |
+
return f"<html><body>{html}</body></html>"
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
# Module singleton instance
|
| 438 |
+
output_generator = OutputGenerator()
|
src/modules/query_understanding.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Query Understanding Module - Parses and analyzes research queries.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Optional, Dict, Any, List
|
| 7 |
+
|
| 8 |
+
from ..models import QueryAnalysis, Entity, QueryComplexity
|
| 9 |
+
from ..llm_client import llm_client
|
| 10 |
+
from ..prompts.query_prompts import QUERY_PROMPTS
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class QueryUnderstanding:
|
| 16 |
+
"""
|
| 17 |
+
Query Understanding module for analyzing and decomposing research queries.
|
| 18 |
+
|
| 19 |
+
Implements FR-1: Query Understanding requirements.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.llm = llm_client
|
| 24 |
+
|
| 25 |
+
async def analyze_query(self, query: str) -> QueryAnalysis:
|
| 26 |
+
"""
|
| 27 |
+
Analyze a research query to understand intent, entities, and structure.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
query: Raw natural language research query
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
QueryAnalysis object with parsed query information
|
| 34 |
+
"""
|
| 35 |
+
logger.info(f"Analyzing query: {query[:100]}...")
|
| 36 |
+
|
| 37 |
+
# First, validate the query
|
| 38 |
+
validation = await self._validate_query(query)
|
| 39 |
+
if not validation.get("proceed", True):
|
| 40 |
+
logger.warning(f"Query validation failed: {validation}")
|
| 41 |
+
# Create minimal analysis for invalid query
|
| 42 |
+
analysis = QueryAnalysis(raw_query=query)
|
| 43 |
+
analysis.sub_queries = []
|
| 44 |
+
return analysis
|
| 45 |
+
|
| 46 |
+
# Analyze the query
|
| 47 |
+
analysis_result = await self._analyze(query)
|
| 48 |
+
|
| 49 |
+
# Extract entities
|
| 50 |
+
entities = await self._extract_entities(query)
|
| 51 |
+
|
| 52 |
+
# Classify intent
|
| 53 |
+
intent_result = await self._classify_intent(query)
|
| 54 |
+
|
| 55 |
+
# Build QueryAnalysis object
|
| 56 |
+
analysis = self._build_analysis(
|
| 57 |
+
query=query,
|
| 58 |
+
analysis_result=analysis_result,
|
| 59 |
+
entities=entities,
|
| 60 |
+
intent_result=intent_result
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Decompose into sub-queries if complex
|
| 64 |
+
if analysis.complexity in [QueryComplexity.MEDIUM, QueryComplexity.COMPLEX]:
|
| 65 |
+
sub_queries = await self._decompose_query(query, analysis_result)
|
| 66 |
+
analysis.sub_queries = sub_queries
|
| 67 |
+
else:
|
| 68 |
+
analysis.sub_queries = [query]
|
| 69 |
+
|
| 70 |
+
logger.info(f"Query analysis complete. Complexity: {analysis.complexity.value}, "
|
| 71 |
+
f"Sub-queries: {len(analysis.sub_queries)}")
|
| 72 |
+
|
| 73 |
+
return analysis
|
| 74 |
+
|
| 75 |
+
async def _validate_query(self, query: str) -> Dict[str, Any]:
|
| 76 |
+
"""Validate if the query is researchable and appropriate."""
|
| 77 |
+
prompt = QUERY_PROMPTS["validation"].format(query=query)
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
result = await self.llm.generate_json(prompt)
|
| 81 |
+
return result
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Query validation failed: {e}")
|
| 84 |
+
return {"proceed": True, "is_valid": True}
|
| 85 |
+
|
| 86 |
+
async def _analyze(self, query: str) -> Dict[str, Any]:
|
| 87 |
+
"""Perform main query analysis."""
|
| 88 |
+
prompt = QUERY_PROMPTS["analysis"].format(query=query)
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
result = await self.llm.generate_json(prompt)
|
| 92 |
+
return result
|
| 93 |
+
except Exception as e:
|
| 94 |
+
logger.error(f"Query analysis failed: {e}")
|
| 95 |
+
return {
|
| 96 |
+
"intent": "unknown",
|
| 97 |
+
"domain": "general",
|
| 98 |
+
"entities": [],
|
| 99 |
+
"complexity": "medium",
|
| 100 |
+
"output_type": "report"
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
async def _extract_entities(self, query: str) -> List[Entity]:
|
| 104 |
+
"""Extract named entities from the query."""
|
| 105 |
+
prompt = QUERY_PROMPTS["entity_extraction"].format(query=query)
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
result = await self.llm.generate_json(prompt)
|
| 109 |
+
entities = []
|
| 110 |
+
|
| 111 |
+
for entity_data in result.get("entities", []):
|
| 112 |
+
entity = Entity(
|
| 113 |
+
text=entity_data.get("text", ""),
|
| 114 |
+
type=entity_data.get("type", "CONCEPT"),
|
| 115 |
+
relevance=entity_data.get("relevance", "secondary"),
|
| 116 |
+
context=entity_data.get("context")
|
| 117 |
+
)
|
| 118 |
+
entities.append(entity)
|
| 119 |
+
|
| 120 |
+
return entities
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.error(f"Entity extraction failed: {e}")
|
| 123 |
+
return []
|
| 124 |
+
|
| 125 |
+
async def _classify_intent(self, query: str) -> Dict[str, Any]:
|
| 126 |
+
"""Classify the query intent."""
|
| 127 |
+
prompt = QUERY_PROMPTS["intent_classification"].format(query=query)
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
result = await self.llm.generate_json(prompt)
|
| 131 |
+
return result
|
| 132 |
+
except Exception as e:
|
| 133 |
+
logger.error(f"Intent classification failed: {e}")
|
| 134 |
+
return {
|
| 135 |
+
"primary_intent": "EXPLORATORY",
|
| 136 |
+
"confidence": 0.5,
|
| 137 |
+
"research_approach": "general"
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
async def _decompose_query(
|
| 141 |
+
self,
|
| 142 |
+
query: str,
|
| 143 |
+
analysis: Dict[str, Any]
|
| 144 |
+
) -> List[str]:
|
| 145 |
+
"""Decompose a complex query into sub-queries."""
|
| 146 |
+
import json
|
| 147 |
+
|
| 148 |
+
prompt = QUERY_PROMPTS["decomposition"].format(
|
| 149 |
+
query=query,
|
| 150 |
+
query_analysis=json.dumps(analysis, indent=2)
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
result = await self.llm.generate_json(prompt)
|
| 155 |
+
sub_queries = []
|
| 156 |
+
|
| 157 |
+
for sq in result.get("sub_queries", []):
|
| 158 |
+
sub_queries.append(sq.get("query", ""))
|
| 159 |
+
|
| 160 |
+
# Ensure we have at least the original query
|
| 161 |
+
if not sub_queries:
|
| 162 |
+
sub_queries = [query]
|
| 163 |
+
|
| 164 |
+
return sub_queries
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"Query decomposition failed: {e}")
|
| 167 |
+
return [query]
|
| 168 |
+
|
| 169 |
+
async def check_clarity(self, query: str) -> Dict[str, Any]:
|
| 170 |
+
"""Check if the query needs clarification."""
|
| 171 |
+
prompt = QUERY_PROMPTS["clarification"].format(query=query)
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
result = await self.llm.generate_json(prompt)
|
| 175 |
+
return result
|
| 176 |
+
except Exception as e:
|
| 177 |
+
logger.error(f"Clarity check failed: {e}")
|
| 178 |
+
return {"is_clear": True, "ambiguities": []}
|
| 179 |
+
|
| 180 |
+
def _build_analysis(
|
| 181 |
+
self,
|
| 182 |
+
query: str,
|
| 183 |
+
analysis_result: Dict[str, Any],
|
| 184 |
+
entities: List[Entity],
|
| 185 |
+
intent_result: Dict[str, Any]
|
| 186 |
+
) -> QueryAnalysis:
|
| 187 |
+
"""Build a QueryAnalysis object from component results."""
|
| 188 |
+
# Map complexity string to enum
|
| 189 |
+
complexity_map = {
|
| 190 |
+
"simple": QueryComplexity.SIMPLE,
|
| 191 |
+
"medium": QueryComplexity.MEDIUM,
|
| 192 |
+
"complex": QueryComplexity.COMPLEX
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
complexity_str = analysis_result.get("complexity", "medium").lower()
|
| 196 |
+
complexity = complexity_map.get(complexity_str, QueryComplexity.MEDIUM)
|
| 197 |
+
|
| 198 |
+
# Combine entities from analysis and extraction
|
| 199 |
+
all_entities = entities.copy()
|
| 200 |
+
for entity_data in analysis_result.get("entities", []):
|
| 201 |
+
if isinstance(entity_data, dict):
|
| 202 |
+
entity = Entity(
|
| 203 |
+
text=entity_data.get("text", ""),
|
| 204 |
+
type=entity_data.get("type", "CONCEPT"),
|
| 205 |
+
relevance=entity_data.get("relevance", "secondary")
|
| 206 |
+
)
|
| 207 |
+
# Avoid duplicates
|
| 208 |
+
if not any(e.text == entity.text for e in all_entities):
|
| 209 |
+
all_entities.append(entity)
|
| 210 |
+
|
| 211 |
+
return QueryAnalysis(
|
| 212 |
+
raw_query=query,
|
| 213 |
+
intent=intent_result.get("primary_intent", analysis_result.get("intent", "")),
|
| 214 |
+
domain=analysis_result.get("domain", "general"),
|
| 215 |
+
entities=all_entities,
|
| 216 |
+
temporal_scope=analysis_result.get("temporal_scope"),
|
| 217 |
+
geographic_scope=analysis_result.get("geographic_scope"),
|
| 218 |
+
complexity=complexity,
|
| 219 |
+
output_type=analysis_result.get("output_type", "report")
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# Module instance
|
| 224 |
+
query_understanding = QueryUnderstanding()
|
src/modules/reasoning_engine.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reasoning Engine Module - Multi-step reasoning and information synthesis.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import json
|
| 7 |
+
from typing import Optional, Dict, Any, List
|
| 8 |
+
|
| 9 |
+
from ..models import (
|
| 10 |
+
QueryAnalysis, Source, Finding, Claim,
|
| 11 |
+
ConfidenceLevel, VerificationStatus
|
| 12 |
+
)
|
| 13 |
+
from ..llm_client import llm_client
|
| 14 |
+
from ..prompts.reasoning_prompts import REASONING_PROMPTS
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ReasoningEngine:
|
| 20 |
+
"""
|
| 21 |
+
Reasoning Engine for multi-step reasoning over gathered information.
|
| 22 |
+
|
| 23 |
+
Implements FR-3: Multi-Step Reasoning requirements.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self.llm = llm_client
|
| 28 |
+
|
| 29 |
+
async def reason(
|
| 30 |
+
self,
|
| 31 |
+
query: QueryAnalysis,
|
| 32 |
+
sources: List[Source],
|
| 33 |
+
extracted_info: Optional[List[Dict[str, Any]]] = None
|
| 34 |
+
) -> List[Finding]:
|
| 35 |
+
"""
|
| 36 |
+
Perform multi-step reasoning over gathered information.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
query: Analyzed query
|
| 40 |
+
sources: List of sources with content
|
| 41 |
+
extracted_info: Optional pre-extracted information
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
List of findings from reasoning
|
| 45 |
+
"""
|
| 46 |
+
logger.info(f"Starting reasoning for query: {query.raw_query[:50]}...")
|
| 47 |
+
|
| 48 |
+
# Prepare context from sources
|
| 49 |
+
context = self._prepare_context(sources, extracted_info)
|
| 50 |
+
|
| 51 |
+
# Perform chain-of-thought reasoning
|
| 52 |
+
reasoning_result = await self._chain_of_thought(
|
| 53 |
+
query.raw_query,
|
| 54 |
+
context,
|
| 55 |
+
sources
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Synthesize across sources
|
| 59 |
+
synthesis = await self._synthesize(query.raw_query, sources)
|
| 60 |
+
|
| 61 |
+
# Check if this is a comparative query
|
| 62 |
+
if query.intent in ["COMPARATIVE", "EVALUATIVE"]:
|
| 63 |
+
comparison = await self._comparative_analysis(
|
| 64 |
+
query.raw_query,
|
| 65 |
+
sources,
|
| 66 |
+
context
|
| 67 |
+
)
|
| 68 |
+
synthesis["comparison"] = comparison
|
| 69 |
+
|
| 70 |
+
# Build findings from reasoning results
|
| 71 |
+
findings = self._build_findings(
|
| 72 |
+
reasoning_result,
|
| 73 |
+
synthesis,
|
| 74 |
+
sources
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Identify gaps
|
| 78 |
+
gaps = await self._identify_gaps(
|
| 79 |
+
query.raw_query,
|
| 80 |
+
findings,
|
| 81 |
+
sources
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Add gap information to findings
|
| 85 |
+
if gaps.get("priority_gaps"):
|
| 86 |
+
for finding in findings:
|
| 87 |
+
finding.caveats.extend(gaps.get("priority_gaps", [])[:2])
|
| 88 |
+
|
| 89 |
+
logger.info(f"Reasoning complete. Generated {len(findings)} findings")
|
| 90 |
+
return findings
|
| 91 |
+
|
| 92 |
+
async def _chain_of_thought(
|
| 93 |
+
self,
|
| 94 |
+
query: str,
|
| 95 |
+
context: str,
|
| 96 |
+
sources: List[Source]
|
| 97 |
+
) -> Dict[str, Any]:
|
| 98 |
+
"""Perform chain-of-thought reasoning."""
|
| 99 |
+
sources_summary = self._summarize_sources(sources)
|
| 100 |
+
|
| 101 |
+
prompt = REASONING_PROMPTS["chain_of_thought"].format(
|
| 102 |
+
query=query,
|
| 103 |
+
context=context,
|
| 104 |
+
sources=sources_summary
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
result = await self.llm.generate_json(prompt)
|
| 109 |
+
return result
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logger.error(f"Chain-of-thought reasoning failed: {e}")
|
| 112 |
+
return {
|
| 113 |
+
"reasoning_chain": [],
|
| 114 |
+
"final_answer": "",
|
| 115 |
+
"confidence": 0.5,
|
| 116 |
+
"gaps_identified": []
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async def _synthesize(
|
| 120 |
+
self,
|
| 121 |
+
query: str,
|
| 122 |
+
sources: List[Source]
|
| 123 |
+
) -> Dict[str, Any]:
|
| 124 |
+
"""Synthesize information across sources."""
|
| 125 |
+
sources_with_content = []
|
| 126 |
+
for source in sources:
|
| 127 |
+
sources_with_content.append({
|
| 128 |
+
"url": source.url,
|
| 129 |
+
"title": source.title,
|
| 130 |
+
"content": source.content[:3000] if source.content else source.snippet,
|
| 131 |
+
"credibility": source.credibility_level
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
prompt = REASONING_PROMPTS["synthesis"].format(
|
| 135 |
+
query=query,
|
| 136 |
+
sources_with_content=json.dumps(sources_with_content, indent=2)
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
result = await self.llm.generate_json(prompt)
|
| 141 |
+
return result
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logger.error(f"Synthesis failed: {e}")
|
| 144 |
+
return {
|
| 145 |
+
"themes": [],
|
| 146 |
+
"consensus_findings": [],
|
| 147 |
+
"disagreements": [],
|
| 148 |
+
"synthesis": "",
|
| 149 |
+
"key_insights": []
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
async def _comparative_analysis(
|
| 153 |
+
self,
|
| 154 |
+
query: str,
|
| 155 |
+
sources: List[Source],
|
| 156 |
+
context: str
|
| 157 |
+
) -> Dict[str, Any]:
|
| 158 |
+
"""Perform comparative analysis if query involves comparison."""
|
| 159 |
+
# Extract subjects to compare from query
|
| 160 |
+
subjects = self._extract_comparison_subjects(query)
|
| 161 |
+
|
| 162 |
+
prompt = REASONING_PROMPTS["comparative_analysis"].format(
|
| 163 |
+
query=query,
|
| 164 |
+
subjects=json.dumps(subjects),
|
| 165 |
+
context=context
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
try:
|
| 169 |
+
result = await self.llm.generate_json(prompt)
|
| 170 |
+
return result
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"Comparative analysis failed: {e}")
|
| 173 |
+
return {}
|
| 174 |
+
|
| 175 |
+
async def _causal_analysis(
|
| 176 |
+
self,
|
| 177 |
+
query: str,
|
| 178 |
+
context: str
|
| 179 |
+
) -> Dict[str, Any]:
|
| 180 |
+
"""Perform causal analysis if applicable."""
|
| 181 |
+
prompt = REASONING_PROMPTS["causal_analysis"].format(
|
| 182 |
+
query=query,
|
| 183 |
+
context=context
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
result = await self.llm.generate_json(prompt)
|
| 188 |
+
return result
|
| 189 |
+
except Exception as e:
|
| 190 |
+
logger.error(f"Causal analysis failed: {e}")
|
| 191 |
+
return {}
|
| 192 |
+
|
| 193 |
+
async def _identify_gaps(
|
| 194 |
+
self,
|
| 195 |
+
query: str,
|
| 196 |
+
findings: List[Finding],
|
| 197 |
+
sources: List[Source]
|
| 198 |
+
) -> Dict[str, Any]:
|
| 199 |
+
"""Identify gaps in current research."""
|
| 200 |
+
findings_summary = [
|
| 201 |
+
{"title": f.title, "content": f.content[:500]}
|
| 202 |
+
for f in findings
|
| 203 |
+
]
|
| 204 |
+
sources_summary = [
|
| 205 |
+
{"url": s.url, "title": s.title}
|
| 206 |
+
for s in sources
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
prompt = REASONING_PROMPTS["gap_analysis"].format(
|
| 210 |
+
query=query,
|
| 211 |
+
findings=json.dumps(findings_summary, indent=2),
|
| 212 |
+
sources=json.dumps(sources_summary, indent=2)
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
result = await self.llm.generate_json(prompt)
|
| 217 |
+
return result
|
| 218 |
+
except Exception as e:
|
| 219 |
+
logger.error(f"Gap analysis failed: {e}")
|
| 220 |
+
return {"can_proceed": True, "priority_gaps": []}
|
| 221 |
+
|
| 222 |
+
async def verify_reasoning(
|
| 223 |
+
self,
|
| 224 |
+
reasoning_chain: List[Dict[str, Any]]
|
| 225 |
+
) -> Dict[str, Any]:
|
| 226 |
+
"""Verify the logical soundness of a reasoning chain."""
|
| 227 |
+
prompt = REASONING_PROMPTS["reasoning_verification"].format(
|
| 228 |
+
reasoning_chain=json.dumps(reasoning_chain, indent=2)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
result = await self.llm.generate_json(prompt)
|
| 233 |
+
return result
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Reasoning verification failed: {e}")
|
| 236 |
+
return {"is_valid": True, "validity_score": 70}
|
| 237 |
+
|
| 238 |
+
def _prepare_context(
|
| 239 |
+
self,
|
| 240 |
+
sources: List[Source],
|
| 241 |
+
extracted_info: Optional[List[Dict[str, Any]]] = None
|
| 242 |
+
) -> str:
|
| 243 |
+
"""Prepare context string from sources and extracted info."""
|
| 244 |
+
context_parts = []
|
| 245 |
+
|
| 246 |
+
for i, source in enumerate(sources, 1):
|
| 247 |
+
content = source.content if source.content else source.snippet
|
| 248 |
+
if content:
|
| 249 |
+
context_parts.append(
|
| 250 |
+
f"[Source {i}: {source.title}]\n"
|
| 251 |
+
f"URL: {source.url}\n"
|
| 252 |
+
f"Content: {content[:2000]}\n"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
if extracted_info:
|
| 256 |
+
context_parts.append("\n[Extracted Key Information]")
|
| 257 |
+
for info in extracted_info:
|
| 258 |
+
context_parts.append(f"- {info.get('content', '')}")
|
| 259 |
+
|
| 260 |
+
return "\n".join(context_parts)
|
| 261 |
+
|
| 262 |
+
def _summarize_sources(self, sources: List[Source]) -> str:
|
| 263 |
+
"""Create a summary of sources for prompts."""
|
| 264 |
+
summaries = []
|
| 265 |
+
for i, source in enumerate(sources, 1):
|
| 266 |
+
summaries.append(
|
| 267 |
+
f"[{i}] {source.title} ({source.url}) - "
|
| 268 |
+
f"Credibility: {source.credibility_level}"
|
| 269 |
+
)
|
| 270 |
+
return "\n".join(summaries)
|
| 271 |
+
|
| 272 |
+
def _extract_comparison_subjects(self, query: str) -> List[str]:
|
| 273 |
+
"""Extract subjects being compared from query."""
|
| 274 |
+
# Simple extraction - in real implementation, use NLP
|
| 275 |
+
comparison_words = ["vs", "versus", "compare", "between", "and"]
|
| 276 |
+
subjects = []
|
| 277 |
+
|
| 278 |
+
query_lower = query.lower()
|
| 279 |
+
for word in comparison_words:
|
| 280 |
+
if word in query_lower:
|
| 281 |
+
# Very basic extraction
|
| 282 |
+
parts = query_lower.split(word)
|
| 283 |
+
if len(parts) >= 2:
|
| 284 |
+
subjects = [parts[0].strip(), parts[1].strip()]
|
| 285 |
+
break
|
| 286 |
+
|
| 287 |
+
return subjects if subjects else ["Subject A", "Subject B"]
|
| 288 |
+
|
| 289 |
+
def _build_findings(
|
| 290 |
+
self,
|
| 291 |
+
reasoning_result: Dict[str, Any],
|
| 292 |
+
synthesis: Dict[str, Any],
|
| 293 |
+
sources: List[Source]
|
| 294 |
+
) -> List[Finding]:
|
| 295 |
+
"""Build Finding objects from reasoning results."""
|
| 296 |
+
findings = []
|
| 297 |
+
source_ids = [s.id for s in sources]
|
| 298 |
+
|
| 299 |
+
# Create finding from main answer
|
| 300 |
+
if reasoning_result.get("final_answer"):
|
| 301 |
+
confidence = reasoning_result.get("confidence", 0.5)
|
| 302 |
+
|
| 303 |
+
main_finding = Finding(
|
| 304 |
+
title="Main Finding",
|
| 305 |
+
content=reasoning_result["final_answer"],
|
| 306 |
+
category="main",
|
| 307 |
+
confidence_score=confidence,
|
| 308 |
+
confidence_level=self._score_to_level(confidence),
|
| 309 |
+
source_ids=source_ids[:5], # Top 5 sources
|
| 310 |
+
reasoning_chain=[
|
| 311 |
+
step.get("thought", "")
|
| 312 |
+
for step in reasoning_result.get("reasoning_chain", [])
|
| 313 |
+
],
|
| 314 |
+
caveats=reasoning_result.get("gaps_identified", [])
|
| 315 |
+
)
|
| 316 |
+
findings.append(main_finding)
|
| 317 |
+
|
| 318 |
+
# Create findings from themes
|
| 319 |
+
for theme in synthesis.get("themes", []):
|
| 320 |
+
finding = Finding(
|
| 321 |
+
title=theme.get("theme", "Theme"),
|
| 322 |
+
content=theme.get("description", ""),
|
| 323 |
+
category="theme",
|
| 324 |
+
confidence_score=0.7,
|
| 325 |
+
confidence_level=ConfidenceLevel.HIGH,
|
| 326 |
+
source_ids=source_ids[:3],
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Add key points as claims
|
| 330 |
+
for point in theme.get("key_points", []):
|
| 331 |
+
claim = Claim(
|
| 332 |
+
content=point,
|
| 333 |
+
source_ids=source_ids[:2],
|
| 334 |
+
verification_status=VerificationStatus.PARTIALLY_VERIFIED,
|
| 335 |
+
confidence_score=0.7
|
| 336 |
+
)
|
| 337 |
+
finding.claims.append(claim)
|
| 338 |
+
|
| 339 |
+
findings.append(finding)
|
| 340 |
+
|
| 341 |
+
# Create findings from consensus
|
| 342 |
+
for consensus in synthesis.get("consensus_findings", []):
|
| 343 |
+
confidence = 0.9 if consensus.get("confidence") == "high" else 0.7
|
| 344 |
+
|
| 345 |
+
finding = Finding(
|
| 346 |
+
title="Consensus Finding",
|
| 347 |
+
content=consensus.get("finding", ""),
|
| 348 |
+
category="consensus",
|
| 349 |
+
confidence_score=confidence,
|
| 350 |
+
confidence_level=self._score_to_level(confidence),
|
| 351 |
+
source_ids=source_ids[:3],
|
| 352 |
+
)
|
| 353 |
+
findings.append(finding)
|
| 354 |
+
|
| 355 |
+
# Note disagreements
|
| 356 |
+
for disagreement in synthesis.get("disagreements", []):
|
| 357 |
+
finding = Finding(
|
| 358 |
+
title=f"Disputed: {disagreement.get('topic', 'Topic')}",
|
| 359 |
+
content=self._format_disagreement(disagreement),
|
| 360 |
+
category="disagreement",
|
| 361 |
+
confidence_score=0.5,
|
| 362 |
+
confidence_level=ConfidenceLevel.MEDIUM,
|
| 363 |
+
source_ids=source_ids[:3],
|
| 364 |
+
caveats=["Sources disagree on this topic"]
|
| 365 |
+
)
|
| 366 |
+
findings.append(finding)
|
| 367 |
+
|
| 368 |
+
# Add key insights
|
| 369 |
+
if synthesis.get("key_insights"):
|
| 370 |
+
finding = Finding(
|
| 371 |
+
title="Key Insights",
|
| 372 |
+
content="\n".join(f"• {insight}" for insight in synthesis["key_insights"]),
|
| 373 |
+
category="insights",
|
| 374 |
+
confidence_score=0.8,
|
| 375 |
+
confidence_level=ConfidenceLevel.HIGH,
|
| 376 |
+
source_ids=source_ids[:5],
|
| 377 |
+
)
|
| 378 |
+
findings.append(finding)
|
| 379 |
+
|
| 380 |
+
return findings
|
| 381 |
+
|
| 382 |
+
def _format_disagreement(self, disagreement: Dict[str, Any]) -> str:
|
| 383 |
+
"""Format a disagreement for display."""
|
| 384 |
+
parts = [f"Topic: {disagreement.get('topic', 'Unknown')}"]
|
| 385 |
+
|
| 386 |
+
for perspective in disagreement.get("perspectives", []):
|
| 387 |
+
parts.append(
|
| 388 |
+
f"• {perspective.get('source', 'Source')}: {perspective.get('position', '')}"
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
return "\n".join(parts)
|
| 392 |
+
|
| 393 |
+
def _score_to_level(self, score: float) -> ConfidenceLevel:
|
| 394 |
+
"""Convert numeric score to confidence level."""
|
| 395 |
+
if score >= 0.9:
|
| 396 |
+
return ConfidenceLevel.VERY_HIGH
|
| 397 |
+
elif score >= 0.7:
|
| 398 |
+
return ConfidenceLevel.HIGH
|
| 399 |
+
elif score >= 0.5:
|
| 400 |
+
return ConfidenceLevel.MEDIUM
|
| 401 |
+
elif score >= 0.3:
|
| 402 |
+
return ConfidenceLevel.LOW
|
| 403 |
+
else:
|
| 404 |
+
return ConfidenceLevel.VERY_LOW
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
# Module instance
|
| 408 |
+
reasoning_engine = ReasoningEngine()
|
src/modules/verification.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Verification Module - Validates and verifies research findings.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import json
|
| 7 |
+
from typing import Optional, Dict, Any, List
|
| 8 |
+
|
| 9 |
+
from ..models import (
|
| 10 |
+
Source, Finding, Claim, VerificationResult, Conflict,
|
| 11 |
+
VerificationStatus, ConfidenceLevel
|
| 12 |
+
)
|
| 13 |
+
from ..llm_client import llm_client
|
| 14 |
+
from ..prompts.verification_prompts import VERIFICATION_PROMPTS
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class VerificationModule:
|
| 20 |
+
"""
|
| 21 |
+
Verification module for validating research findings.
|
| 22 |
+
|
| 23 |
+
Implements FR-4: Source Verification requirements.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self.llm = llm_client
|
| 28 |
+
|
| 29 |
+
async def verify(
|
| 30 |
+
self,
|
| 31 |
+
findings: List[Finding],
|
| 32 |
+
sources: List[Source]
|
| 33 |
+
) -> VerificationResult:
|
| 34 |
+
"""
|
| 35 |
+
Verify research findings against sources.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
findings: List of findings to verify
|
| 39 |
+
sources: List of sources used
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
VerificationResult with verification details
|
| 43 |
+
"""
|
| 44 |
+
logger.info(f"Verifying {len(findings)} findings against {len(sources)} sources")
|
| 45 |
+
|
| 46 |
+
# Extract claims from findings
|
| 47 |
+
all_claims = self._extract_claims(findings)
|
| 48 |
+
|
| 49 |
+
# Cross-reference claims
|
| 50 |
+
cross_ref_result = await self._cross_reference(all_claims, sources)
|
| 51 |
+
|
| 52 |
+
# Assess source credibility
|
| 53 |
+
credibility_result = await self._assess_credibility(sources)
|
| 54 |
+
|
| 55 |
+
# Detect conflicts
|
| 56 |
+
conflict_result = await self._detect_conflicts(findings, sources)
|
| 57 |
+
|
| 58 |
+
# Flag uncertainties
|
| 59 |
+
uncertainty_result = await self._flag_uncertainties(findings, sources)
|
| 60 |
+
|
| 61 |
+
# Detect biases
|
| 62 |
+
bias_result = await self._detect_bias(findings, sources)
|
| 63 |
+
|
| 64 |
+
# Generate verification summary
|
| 65 |
+
verification = await self._generate_summary(
|
| 66 |
+
findings,
|
| 67 |
+
cross_ref_result,
|
| 68 |
+
credibility_result,
|
| 69 |
+
conflict_result,
|
| 70 |
+
uncertainty_result
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Update claims with verification status
|
| 74 |
+
self._update_claim_status(all_claims, cross_ref_result)
|
| 75 |
+
|
| 76 |
+
# Build verification result
|
| 77 |
+
result = self._build_verification_result(
|
| 78 |
+
findings,
|
| 79 |
+
all_claims,
|
| 80 |
+
conflict_result,
|
| 81 |
+
verification
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
logger.info(f"Verification complete. Overall confidence: {result.overall_confidence:.2f}")
|
| 85 |
+
return result
|
| 86 |
+
|
| 87 |
+
async def _cross_reference(
|
| 88 |
+
self,
|
| 89 |
+
claims: List[Claim],
|
| 90 |
+
sources: List[Source]
|
| 91 |
+
) -> Dict[str, Any]:
|
| 92 |
+
"""Cross-reference claims against sources."""
|
| 93 |
+
claims_data = [
|
| 94 |
+
{"id": c.id, "content": c.content}
|
| 95 |
+
for c in claims
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
sources_data = [
|
| 99 |
+
{
|
| 100 |
+
"url": s.url,
|
| 101 |
+
"title": s.title,
|
| 102 |
+
"content": s.content[:2000] if s.content else s.snippet,
|
| 103 |
+
"credibility": s.credibility_level
|
| 104 |
+
}
|
| 105 |
+
for s in sources
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
prompt = VERIFICATION_PROMPTS["cross_reference"].format(
|
| 109 |
+
claims=json.dumps(claims_data, indent=2),
|
| 110 |
+
sources=json.dumps(sources_data, indent=2)
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
result = await self.llm.generate_json(prompt)
|
| 115 |
+
return result
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logger.error(f"Cross-reference failed: {e}")
|
| 118 |
+
return {"verified_claims": [], "verification_summary": {}}
|
| 119 |
+
|
| 120 |
+
async def _assess_credibility(
|
| 121 |
+
self,
|
| 122 |
+
sources: List[Source]
|
| 123 |
+
) -> Dict[str, Any]:
|
| 124 |
+
"""Assess the credibility of sources."""
|
| 125 |
+
sources_data = [
|
| 126 |
+
{
|
| 127 |
+
"url": s.url,
|
| 128 |
+
"title": s.title,
|
| 129 |
+
"domain": s.domain,
|
| 130 |
+
"author": s.author,
|
| 131 |
+
"publication_date": s.publication_date,
|
| 132 |
+
"snippet": s.snippet[:500] if s.snippet else ""
|
| 133 |
+
}
|
| 134 |
+
for s in sources
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
prompt = VERIFICATION_PROMPTS["credibility_assessment"].format(
|
| 138 |
+
sources=json.dumps(sources_data, indent=2)
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
result = await self.llm.generate_json(prompt)
|
| 143 |
+
|
| 144 |
+
# Update source credibility scores
|
| 145 |
+
assessments = {a["url"]: a for a in result.get("source_assessments", [])}
|
| 146 |
+
for source in sources:
|
| 147 |
+
if source.url in assessments:
|
| 148 |
+
assessment = assessments[source.url]
|
| 149 |
+
source.credibility_score = assessment.get("credibility_score", 50) / 100
|
| 150 |
+
source.credibility_level = assessment.get("credibility_level", "medium")
|
| 151 |
+
|
| 152 |
+
return result
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f"Credibility assessment failed: {e}")
|
| 155 |
+
return {"source_assessments": []}
|
| 156 |
+
|
| 157 |
+
async def _detect_conflicts(
|
| 158 |
+
self,
|
| 159 |
+
findings: List[Finding],
|
| 160 |
+
sources: List[Source]
|
| 161 |
+
) -> Dict[str, Any]:
|
| 162 |
+
"""Detect conflicts in findings."""
|
| 163 |
+
findings_data = [
|
| 164 |
+
{"title": f.title, "content": f.content}
|
| 165 |
+
for f in findings
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
sources_data = [
|
| 169 |
+
{
|
| 170 |
+
"url": s.url,
|
| 171 |
+
"title": s.title,
|
| 172 |
+
"content": s.content[:1500] if s.content else s.snippet
|
| 173 |
+
}
|
| 174 |
+
for s in sources
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
prompt = VERIFICATION_PROMPTS["conflict_detection"].format(
|
| 178 |
+
findings=json.dumps(findings_data, indent=2),
|
| 179 |
+
sources=json.dumps(sources_data, indent=2)
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
result = await self.llm.generate_json(prompt)
|
| 184 |
+
return result
|
| 185 |
+
except Exception as e:
|
| 186 |
+
logger.error(f"Conflict detection failed: {e}")
|
| 187 |
+
return {"conflicts_detected": [], "overall_consistency": 75}
|
| 188 |
+
|
| 189 |
+
async def _flag_uncertainties(
|
| 190 |
+
self,
|
| 191 |
+
findings: List[Finding],
|
| 192 |
+
sources: List[Source]
|
| 193 |
+
) -> Dict[str, Any]:
|
| 194 |
+
"""Flag uncertain claims."""
|
| 195 |
+
findings_data = [
|
| 196 |
+
{"title": f.title, "content": f.content, "confidence": f.confidence_score}
|
| 197 |
+
for f in findings
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
sources_data = [
|
| 201 |
+
{"url": s.url, "title": s.title}
|
| 202 |
+
for s in sources
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
prompt = VERIFICATION_PROMPTS["uncertainty_flagging"].format(
|
| 206 |
+
findings=json.dumps(findings_data, indent=2),
|
| 207 |
+
sources=json.dumps(sources_data, indent=2)
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
result = await self.llm.generate_json(prompt)
|
| 212 |
+
return result
|
| 213 |
+
except Exception as e:
|
| 214 |
+
logger.error(f"Uncertainty flagging failed: {e}")
|
| 215 |
+
return {"uncertain_claims": [], "caveats_to_include": []}
|
| 216 |
+
|
| 217 |
+
async def _detect_bias(
|
| 218 |
+
self,
|
| 219 |
+
findings: List[Finding],
|
| 220 |
+
sources: List[Source]
|
| 221 |
+
) -> Dict[str, Any]:
|
| 222 |
+
"""Detect potential biases."""
|
| 223 |
+
findings_data = [
|
| 224 |
+
{"title": f.title, "content": f.content}
|
| 225 |
+
for f in findings
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
sources_data = [
|
| 229 |
+
{"url": s.url, "domain": s.domain, "title": s.title}
|
| 230 |
+
for s in sources
|
| 231 |
+
]
|
| 232 |
+
|
| 233 |
+
prompt = VERIFICATION_PROMPTS["bias_detection"].format(
|
| 234 |
+
findings=json.dumps(findings_data, indent=2),
|
| 235 |
+
sources=json.dumps(sources_data, indent=2)
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
result = await self.llm.generate_json(prompt)
|
| 240 |
+
return result
|
| 241 |
+
except Exception as e:
|
| 242 |
+
logger.error(f"Bias detection failed: {e}")
|
| 243 |
+
return {"biases_detected": [], "balance_assessment": {"is_balanced": True}}
|
| 244 |
+
|
| 245 |
+
async def _generate_summary(
|
| 246 |
+
self,
|
| 247 |
+
findings: List[Finding],
|
| 248 |
+
cross_ref: Dict[str, Any],
|
| 249 |
+
credibility: Dict[str, Any],
|
| 250 |
+
conflicts: Dict[str, Any],
|
| 251 |
+
uncertainty: Dict[str, Any]
|
| 252 |
+
) -> Dict[str, Any]:
|
| 253 |
+
"""Generate verification summary."""
|
| 254 |
+
findings_data = [
|
| 255 |
+
{"title": f.title, "content": f.content[:500]}
|
| 256 |
+
for f in findings
|
| 257 |
+
]
|
| 258 |
+
|
| 259 |
+
prompt = VERIFICATION_PROMPTS["verification_summary"].format(
|
| 260 |
+
findings=json.dumps(findings_data, indent=2),
|
| 261 |
+
cross_reference_results=json.dumps(cross_ref.get("verification_summary", {})),
|
| 262 |
+
credibility_results=json.dumps(credibility.get("overall_source_quality", "medium")),
|
| 263 |
+
conflict_results=json.dumps({"count": len(conflicts.get("conflicts_detected", []))}),
|
| 264 |
+
uncertainty_results=json.dumps({"caveats": uncertainty.get("caveats_to_include", [])})
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
result = await self.llm.generate_json(prompt)
|
| 269 |
+
return result
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.error(f"Verification summary failed: {e}")
|
| 272 |
+
return {
|
| 273 |
+
"verification_summary": {
|
| 274 |
+
"overall_confidence": 0.7,
|
| 275 |
+
"trust_level": "medium"
|
| 276 |
+
},
|
| 277 |
+
"caveats": [],
|
| 278 |
+
"flags": []
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
async def fact_check(
|
| 282 |
+
self,
|
| 283 |
+
claims: List[str],
|
| 284 |
+
context: str,
|
| 285 |
+
evidence: List[Dict[str, Any]]
|
| 286 |
+
) -> Dict[str, Any]:
|
| 287 |
+
"""Perform fact-checking on specific claims."""
|
| 288 |
+
prompt = VERIFICATION_PROMPTS["fact_check"].format(
|
| 289 |
+
claims=json.dumps(claims, indent=2),
|
| 290 |
+
context=context,
|
| 291 |
+
evidence=json.dumps(evidence, indent=2)
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
result = await self.llm.generate_json(prompt)
|
| 296 |
+
return result
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.error(f"Fact check failed: {e}")
|
| 299 |
+
return {"fact_checks": []}
|
| 300 |
+
|
| 301 |
+
def _extract_claims(self, findings: List[Finding]) -> List[Claim]:
|
| 302 |
+
"""Extract all claims from findings."""
|
| 303 |
+
claims = []
|
| 304 |
+
|
| 305 |
+
for finding in findings:
|
| 306 |
+
# Add existing claims
|
| 307 |
+
claims.extend(finding.claims)
|
| 308 |
+
|
| 309 |
+
# Create a claim from the finding content if no claims exist
|
| 310 |
+
if not finding.claims:
|
| 311 |
+
claim = Claim(
|
| 312 |
+
content=finding.content,
|
| 313 |
+
source_ids=finding.source_ids,
|
| 314 |
+
confidence_score=finding.confidence_score
|
| 315 |
+
)
|
| 316 |
+
claims.append(claim)
|
| 317 |
+
finding.claims.append(claim)
|
| 318 |
+
|
| 319 |
+
return claims
|
| 320 |
+
|
| 321 |
+
def _update_claim_status(
|
| 322 |
+
self,
|
| 323 |
+
claims: List[Claim],
|
| 324 |
+
cross_ref_result: Dict[str, Any]
|
| 325 |
+
):
|
| 326 |
+
"""Update claim verification status based on cross-reference results."""
|
| 327 |
+
verified_claims = {
|
| 328 |
+
vc.get("claim", ""): vc
|
| 329 |
+
for vc in cross_ref_result.get("verified_claims", [])
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
for claim in claims:
|
| 333 |
+
# Find matching verified claim
|
| 334 |
+
for claim_text, vc in verified_claims.items():
|
| 335 |
+
if claim.content in claim_text or claim_text in claim.content:
|
| 336 |
+
status = vc.get("status", "unverified")
|
| 337 |
+
|
| 338 |
+
if status == "verified":
|
| 339 |
+
claim.verification_status = VerificationStatus.VERIFIED
|
| 340 |
+
elif status == "disputed":
|
| 341 |
+
claim.verification_status = VerificationStatus.DISPUTED
|
| 342 |
+
else:
|
| 343 |
+
claim.verification_status = VerificationStatus.UNVERIFIED
|
| 344 |
+
|
| 345 |
+
claim.confidence_score = vc.get("confidence", claim.confidence_score)
|
| 346 |
+
|
| 347 |
+
# Add supporting evidence
|
| 348 |
+
for support in vc.get("supporting_sources", []):
|
| 349 |
+
claim.supporting_evidence.append(support.get("quote", ""))
|
| 350 |
+
|
| 351 |
+
# Add contradicting evidence
|
| 352 |
+
for contra in vc.get("contradicting_sources", []):
|
| 353 |
+
claim.contradicting_evidence.append(contra.get("quote", ""))
|
| 354 |
+
|
| 355 |
+
break
|
| 356 |
+
|
| 357 |
+
def _build_verification_result(
|
| 358 |
+
self,
|
| 359 |
+
findings: List[Finding],
|
| 360 |
+
claims: List[Claim],
|
| 361 |
+
conflict_result: Dict[str, Any],
|
| 362 |
+
verification_summary: Dict[str, Any]
|
| 363 |
+
) -> VerificationResult:
|
| 364 |
+
"""Build the final verification result."""
|
| 365 |
+
summary = verification_summary.get("verification_summary", {})
|
| 366 |
+
|
| 367 |
+
# Build conflicts
|
| 368 |
+
conflicts = []
|
| 369 |
+
for conflict_data in conflict_result.get("conflicts_detected", []):
|
| 370 |
+
conflict = Conflict(
|
| 371 |
+
topic=conflict_data.get("topic", ""),
|
| 372 |
+
conflict_type=conflict_data.get("type", "factual"),
|
| 373 |
+
positions=conflict_data.get("positions", []),
|
| 374 |
+
severity=conflict_data.get("severity", "medium"),
|
| 375 |
+
resolution=conflict_data.get("resolution", {}).get("resolved_statement")
|
| 376 |
+
)
|
| 377 |
+
conflicts.append(conflict)
|
| 378 |
+
|
| 379 |
+
# Build flags
|
| 380 |
+
flags = []
|
| 381 |
+
for flag in verification_summary.get("flags", []):
|
| 382 |
+
flags.append({
|
| 383 |
+
"type": flag.get("type", "uncertainty"),
|
| 384 |
+
"message": flag.get("message", ""),
|
| 385 |
+
"severity": flag.get("severity", "medium")
|
| 386 |
+
})
|
| 387 |
+
|
| 388 |
+
return VerificationResult(
|
| 389 |
+
overall_confidence=summary.get("overall_confidence", 0.7),
|
| 390 |
+
trust_level=summary.get("trust_level", "medium"),
|
| 391 |
+
verified_claims=claims,
|
| 392 |
+
conflicts=conflicts,
|
| 393 |
+
caveats=verification_summary.get("caveats", []),
|
| 394 |
+
flags=flags
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# Module instance
|
| 399 |
+
verification_module = VerificationModule()
|
src/modules/web_search.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Web Search Module - Handles web search integration and content retrieval.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import json
|
| 7 |
+
import httpx
|
| 8 |
+
from typing import Optional, Dict, Any, List
|
| 9 |
+
from abc import ABC, abstractmethod
|
| 10 |
+
from urllib.parse import urlparse
|
| 11 |
+
|
| 12 |
+
from ..models import Source, QueryAnalysis, ExtractedInfo
|
| 13 |
+
from ..config import config
|
| 14 |
+
from ..llm_client import llm_client
|
| 15 |
+
from ..prompts.search_prompts import SEARCH_PROMPTS
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BaseSearchProvider(ABC):
|
| 21 |
+
"""Base class for search providers."""
|
| 22 |
+
|
| 23 |
+
@abstractmethod
|
| 24 |
+
async def search(self, query: str, max_results: int = 10) -> List[Dict[str, Any]]:
|
| 25 |
+
"""Execute a search query."""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TavilySearchProvider(BaseSearchProvider):
|
| 30 |
+
"""Tavily search API provider."""
|
| 31 |
+
|
| 32 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 33 |
+
self.api_key = api_key or config.search.api_key
|
| 34 |
+
self.base_url = "https://api.tavily.com"
|
| 35 |
+
|
| 36 |
+
async def search(self, query: str, max_results: int = 10) -> List[Dict[str, Any]]:
|
| 37 |
+
"""Execute a search using Tavily API."""
|
| 38 |
+
if not self.api_key:
|
| 39 |
+
logger.warning("Tavily API key not configured")
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
async with httpx.AsyncClient() as client:
|
| 43 |
+
try:
|
| 44 |
+
response = await client.post(
|
| 45 |
+
f"{self.base_url}/search",
|
| 46 |
+
json={
|
| 47 |
+
"api_key": self.api_key,
|
| 48 |
+
"query": query,
|
| 49 |
+
"max_results": max_results,
|
| 50 |
+
"include_answer": True,
|
| 51 |
+
"include_raw_content": True,
|
| 52 |
+
},
|
| 53 |
+
timeout=config.search.timeout_seconds
|
| 54 |
+
)
|
| 55 |
+
response.raise_for_status()
|
| 56 |
+
data = response.json()
|
| 57 |
+
|
| 58 |
+
results = []
|
| 59 |
+
for result in data.get("results", []):
|
| 60 |
+
results.append({
|
| 61 |
+
"url": result.get("url", ""),
|
| 62 |
+
"title": result.get("title", ""),
|
| 63 |
+
"snippet": result.get("content", ""),
|
| 64 |
+
"content": result.get("raw_content", result.get("content", "")),
|
| 65 |
+
"score": result.get("score", 0.5),
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
return results
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.error(f"Tavily search failed: {e}")
|
| 71 |
+
return []
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class SerperSearchProvider(BaseSearchProvider):
|
| 75 |
+
"""Serper (Google Search) API provider."""
|
| 76 |
+
|
| 77 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 78 |
+
self.api_key = api_key or config.search.fallback_api_key
|
| 79 |
+
self.base_url = "https://google.serper.dev"
|
| 80 |
+
|
| 81 |
+
async def search(self, query: str, max_results: int = 10) -> List[Dict[str, Any]]:
|
| 82 |
+
"""Execute a search using Serper API."""
|
| 83 |
+
if not self.api_key:
|
| 84 |
+
logger.warning("Serper API key not configured")
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
async with httpx.AsyncClient() as client:
|
| 88 |
+
try:
|
| 89 |
+
response = await client.post(
|
| 90 |
+
f"{self.base_url}/search",
|
| 91 |
+
headers={"X-API-KEY": self.api_key},
|
| 92 |
+
json={"q": query, "num": max_results},
|
| 93 |
+
timeout=config.search.timeout_seconds
|
| 94 |
+
)
|
| 95 |
+
response.raise_for_status()
|
| 96 |
+
data = response.json()
|
| 97 |
+
|
| 98 |
+
results = []
|
| 99 |
+
for result in data.get("organic", []):
|
| 100 |
+
results.append({
|
| 101 |
+
"url": result.get("link", ""),
|
| 102 |
+
"title": result.get("title", ""),
|
| 103 |
+
"snippet": result.get("snippet", ""),
|
| 104 |
+
"content": result.get("snippet", ""), # Serper doesn't provide full content
|
| 105 |
+
"score": 0.5,
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
return results
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Serper search failed: {e}")
|
| 111 |
+
return []
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class WebSearch:
|
| 115 |
+
"""
|
| 116 |
+
Web Search module for searching and retrieving content from the web.
|
| 117 |
+
|
| 118 |
+
Implements FR-2: Web Search Integration requirements.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def __init__(self):
|
| 122 |
+
self.llm = llm_client
|
| 123 |
+
self.primary_provider = TavilySearchProvider()
|
| 124 |
+
self.fallback_provider = SerperSearchProvider()
|
| 125 |
+
self._use_fallback = False
|
| 126 |
+
|
| 127 |
+
async def search(
|
| 128 |
+
self,
|
| 129 |
+
query: str,
|
| 130 |
+
query_analysis: Optional[QueryAnalysis] = None,
|
| 131 |
+
max_results: int = 10
|
| 132 |
+
) -> List[Source]:
|
| 133 |
+
"""
|
| 134 |
+
Search the web for information related to the query.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
query: Search query string
|
| 138 |
+
query_analysis: Optional query analysis for context
|
| 139 |
+
max_results: Maximum number of results to return
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
List of Source objects with retrieved content
|
| 143 |
+
"""
|
| 144 |
+
logger.info(f"Searching for: {query[:100]}...")
|
| 145 |
+
|
| 146 |
+
# Generate optimized search queries
|
| 147 |
+
search_queries = await self._generate_search_queries(
|
| 148 |
+
query, query_analysis
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# Execute searches
|
| 152 |
+
all_results = []
|
| 153 |
+
for search_query in search_queries[:3]: # Limit to top 3 queries
|
| 154 |
+
results = await self._execute_search(
|
| 155 |
+
search_query["query"],
|
| 156 |
+
max_results=max_results // len(search_queries[:3])
|
| 157 |
+
)
|
| 158 |
+
all_results.extend(results)
|
| 159 |
+
|
| 160 |
+
# Remove duplicates by URL
|
| 161 |
+
seen_urls = set()
|
| 162 |
+
unique_results = []
|
| 163 |
+
for result in all_results:
|
| 164 |
+
if result["url"] not in seen_urls:
|
| 165 |
+
seen_urls.add(result["url"])
|
| 166 |
+
unique_results.append(result)
|
| 167 |
+
|
| 168 |
+
# Evaluate relevance and convert to Source objects
|
| 169 |
+
sources = await self._process_results(query, unique_results[:max_results])
|
| 170 |
+
|
| 171 |
+
logger.info(f"Found {len(sources)} relevant sources")
|
| 172 |
+
return sources
|
| 173 |
+
|
| 174 |
+
async def search_sub_queries(
|
| 175 |
+
self,
|
| 176 |
+
sub_queries: List[str],
|
| 177 |
+
query_analysis: Optional[QueryAnalysis] = None,
|
| 178 |
+
max_results_per_query: int = 5
|
| 179 |
+
) -> List[Source]:
|
| 180 |
+
"""
|
| 181 |
+
Search for multiple sub-queries and combine results.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
sub_queries: List of sub-queries to search
|
| 185 |
+
query_analysis: Optional query analysis for context
|
| 186 |
+
max_results_per_query: Maximum results per sub-query
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
Combined list of Source objects
|
| 190 |
+
"""
|
| 191 |
+
all_sources = []
|
| 192 |
+
seen_urls = set()
|
| 193 |
+
|
| 194 |
+
for sub_query in sub_queries:
|
| 195 |
+
sources = await self.search(
|
| 196 |
+
sub_query,
|
| 197 |
+
query_analysis,
|
| 198 |
+
max_results=max_results_per_query
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
for source in sources:
|
| 202 |
+
if source.url not in seen_urls:
|
| 203 |
+
seen_urls.add(source.url)
|
| 204 |
+
all_sources.append(source)
|
| 205 |
+
|
| 206 |
+
return all_sources
|
| 207 |
+
|
| 208 |
+
async def _generate_search_queries(
|
| 209 |
+
self,
|
| 210 |
+
query: str,
|
| 211 |
+
query_analysis: Optional[QueryAnalysis] = None
|
| 212 |
+
) -> List[Dict[str, Any]]:
|
| 213 |
+
"""Generate optimized search queries."""
|
| 214 |
+
entities = []
|
| 215 |
+
domain = "general"
|
| 216 |
+
|
| 217 |
+
if query_analysis:
|
| 218 |
+
entities = [e.text for e in query_analysis.entities]
|
| 219 |
+
domain = query_analysis.domain
|
| 220 |
+
|
| 221 |
+
prompt = SEARCH_PROMPTS["query_generation"].format(
|
| 222 |
+
sub_query=query,
|
| 223 |
+
original_query=query,
|
| 224 |
+
domain=domain,
|
| 225 |
+
entities=json.dumps(entities)
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
result = await self.llm.generate_json(prompt)
|
| 230 |
+
return result.get("queries", [{"query": query, "priority": 1}])
|
| 231 |
+
except Exception as e:
|
| 232 |
+
logger.error(f"Search query generation failed: {e}")
|
| 233 |
+
return [{"query": query, "priority": 1}]
|
| 234 |
+
|
| 235 |
+
async def _execute_search(
|
| 236 |
+
self,
|
| 237 |
+
query: str,
|
| 238 |
+
max_results: int = 10
|
| 239 |
+
) -> List[Dict[str, Any]]:
|
| 240 |
+
"""Execute search using available provider."""
|
| 241 |
+
provider = self.fallback_provider if self._use_fallback else self.primary_provider
|
| 242 |
+
|
| 243 |
+
try:
|
| 244 |
+
results = await provider.search(query, max_results)
|
| 245 |
+
if not results and not self._use_fallback:
|
| 246 |
+
# Try fallback
|
| 247 |
+
logger.warning("Primary search returned no results, trying fallback")
|
| 248 |
+
self._use_fallback = True
|
| 249 |
+
results = await self.fallback_provider.search(query, max_results)
|
| 250 |
+
return results
|
| 251 |
+
except Exception as e:
|
| 252 |
+
if not self._use_fallback:
|
| 253 |
+
logger.warning(f"Primary search failed, trying fallback: {e}")
|
| 254 |
+
self._use_fallback = True
|
| 255 |
+
return await self._execute_search(query, max_results)
|
| 256 |
+
logger.error(f"All search providers failed: {e}")
|
| 257 |
+
return []
|
| 258 |
+
|
| 259 |
+
async def _process_results(
|
| 260 |
+
self,
|
| 261 |
+
query: str,
|
| 262 |
+
results: List[Dict[str, Any]]
|
| 263 |
+
) -> List[Source]:
|
| 264 |
+
"""Process and evaluate search results."""
|
| 265 |
+
if not results:
|
| 266 |
+
return []
|
| 267 |
+
|
| 268 |
+
# Evaluate relevance
|
| 269 |
+
results_json = json.dumps([
|
| 270 |
+
{"url": r["url"], "title": r["title"], "snippet": r.get("snippet", "")}
|
| 271 |
+
for r in results
|
| 272 |
+
], indent=2)
|
| 273 |
+
|
| 274 |
+
prompt = SEARCH_PROMPTS["relevance_evaluation"].format(
|
| 275 |
+
query=query,
|
| 276 |
+
search_results=results_json
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
try:
|
| 280 |
+
evaluation = await self.llm.generate_json(prompt)
|
| 281 |
+
evaluated = {r["url"]: r for r in evaluation.get("evaluated_results", [])}
|
| 282 |
+
except Exception as e:
|
| 283 |
+
logger.error(f"Relevance evaluation failed: {e}")
|
| 284 |
+
evaluated = {}
|
| 285 |
+
|
| 286 |
+
# Convert to Source objects
|
| 287 |
+
sources = []
|
| 288 |
+
for result in results:
|
| 289 |
+
url = result["url"]
|
| 290 |
+
eval_data = evaluated.get(url, {})
|
| 291 |
+
|
| 292 |
+
# Parse domain from URL
|
| 293 |
+
try:
|
| 294 |
+
domain = urlparse(url).netloc
|
| 295 |
+
except:
|
| 296 |
+
domain = ""
|
| 297 |
+
|
| 298 |
+
# Determine credibility based on domain
|
| 299 |
+
credibility_score = self._estimate_credibility(domain, eval_data)
|
| 300 |
+
|
| 301 |
+
source = Source(
|
| 302 |
+
url=url,
|
| 303 |
+
title=result.get("title", ""),
|
| 304 |
+
content=result.get("content", result.get("snippet", "")),
|
| 305 |
+
snippet=result.get("snippet", ""),
|
| 306 |
+
domain=domain,
|
| 307 |
+
credibility_score=credibility_score,
|
| 308 |
+
credibility_level=self._score_to_level(credibility_score),
|
| 309 |
+
metadata={
|
| 310 |
+
"relevance_score": eval_data.get("relevance_score", 5),
|
| 311 |
+
"information_value": eval_data.get("information_value", "medium"),
|
| 312 |
+
"freshness": eval_data.get("freshness", "unknown"),
|
| 313 |
+
}
|
| 314 |
+
)
|
| 315 |
+
sources.append(source)
|
| 316 |
+
|
| 317 |
+
# Sort by relevance
|
| 318 |
+
sources.sort(
|
| 319 |
+
key=lambda s: s.metadata.get("relevance_score", 0),
|
| 320 |
+
reverse=True
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
return sources
|
| 324 |
+
|
| 325 |
+
async def extract_content(self, source: Source, query: str) -> List[ExtractedInfo]:
|
| 326 |
+
"""Extract relevant information from a source."""
|
| 327 |
+
if not source.content:
|
| 328 |
+
return []
|
| 329 |
+
|
| 330 |
+
prompt = SEARCH_PROMPTS["content_extraction"].format(
|
| 331 |
+
query=query,
|
| 332 |
+
url=source.url,
|
| 333 |
+
title=source.title,
|
| 334 |
+
content=source.content[:10000] # Limit content length
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
result = await self.llm.generate_json(prompt)
|
| 339 |
+
|
| 340 |
+
extracted = []
|
| 341 |
+
for info in result.get("extracted_information", []):
|
| 342 |
+
extracted.append(ExtractedInfo(
|
| 343 |
+
source_id=source.id,
|
| 344 |
+
content=info.get("content", ""),
|
| 345 |
+
info_type=info.get("type", "fact"),
|
| 346 |
+
relevance=info.get("relevance", "medium"),
|
| 347 |
+
location=info.get("location", "")
|
| 348 |
+
))
|
| 349 |
+
|
| 350 |
+
# Update source metadata
|
| 351 |
+
source_info = result.get("source", {})
|
| 352 |
+
if source_info.get("author"):
|
| 353 |
+
source.author = source_info["author"]
|
| 354 |
+
if source_info.get("publication_date"):
|
| 355 |
+
source.publication_date = source_info["publication_date"]
|
| 356 |
+
|
| 357 |
+
return extracted
|
| 358 |
+
except Exception as e:
|
| 359 |
+
logger.error(f"Content extraction failed: {e}")
|
| 360 |
+
return []
|
| 361 |
+
|
| 362 |
+
def _estimate_credibility(
|
| 363 |
+
self,
|
| 364 |
+
domain: str,
|
| 365 |
+
eval_data: Dict[str, Any]
|
| 366 |
+
) -> float:
|
| 367 |
+
"""Estimate source credibility based on domain and evaluation."""
|
| 368 |
+
# Base score from evaluation
|
| 369 |
+
quality = eval_data.get("source_quality", "medium")
|
| 370 |
+
quality_scores = {"high": 0.8, "medium": 0.5, "low": 0.3, "unknown": 0.4}
|
| 371 |
+
base_score = quality_scores.get(quality, 0.5)
|
| 372 |
+
|
| 373 |
+
# Adjust based on domain
|
| 374 |
+
if any(ext in domain for ext in [".gov", ".edu"]):
|
| 375 |
+
base_score = min(1.0, base_score + 0.2)
|
| 376 |
+
elif any(ext in domain for ext in [".org"]):
|
| 377 |
+
base_score = min(1.0, base_score + 0.1)
|
| 378 |
+
elif any(term in domain for term in ["wikipedia", "reuters", "bbc", "nytimes"]):
|
| 379 |
+
base_score = min(1.0, base_score + 0.15)
|
| 380 |
+
|
| 381 |
+
return base_score
|
| 382 |
+
|
| 383 |
+
def _score_to_level(self, score: float) -> str:
|
| 384 |
+
"""Convert numeric score to credibility level."""
|
| 385 |
+
if score >= 0.8:
|
| 386 |
+
return "high"
|
| 387 |
+
elif score >= 0.5:
|
| 388 |
+
return "medium"
|
| 389 |
+
else:
|
| 390 |
+
return "low"
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# Module instance
|
| 394 |
+
web_search = WebSearch()
|
src/orchestrator.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Research Orchestrator for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
This module coordinates all research components to provide a unified
|
| 5 |
+
research pipeline from query to final output.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import logging
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import Any
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
from .config import Config
|
| 16 |
+
from .models import (
|
| 17 |
+
QueryAnalysis,
|
| 18 |
+
SearchResult,
|
| 19 |
+
Source,
|
| 20 |
+
ReasoningStep,
|
| 21 |
+
ResearchResult,
|
| 22 |
+
OutputFormat,
|
| 23 |
+
CitationStyle,
|
| 24 |
+
)
|
| 25 |
+
from .modules.query_understanding import QueryUnderstanding
|
| 26 |
+
from .modules.web_search import WebSearch
|
| 27 |
+
from .modules.reasoning_engine import ReasoningEngine
|
| 28 |
+
from .modules.verification import Verification
|
| 29 |
+
from .modules.citation import CitationManager
|
| 30 |
+
from .modules.output_generation import OutputGenerator, SummaryLength, AudienceType
|
| 31 |
+
from .modules.error_handling import (
|
| 32 |
+
ErrorHandler,
|
| 33 |
+
ErrorContext,
|
| 34 |
+
ErrorSeverity,
|
| 35 |
+
ComponentType,
|
| 36 |
+
ResearchError,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# Set up logging
|
| 41 |
+
logging.basicConfig(level=logging.INFO)
|
| 42 |
+
logger = logging.getLogger(__name__)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class ResearchStage(Enum):
|
| 46 |
+
"""Stages of the research pipeline."""
|
| 47 |
+
QUERY_ANALYSIS = "query_analysis"
|
| 48 |
+
WEB_SEARCH = "web_search"
|
| 49 |
+
REASONING = "reasoning"
|
| 50 |
+
VERIFICATION = "verification"
|
| 51 |
+
CITATION = "citation"
|
| 52 |
+
OUTPUT_GENERATION = "output_generation"
|
| 53 |
+
COMPLETE = "complete"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class ResearchProgress:
|
| 58 |
+
"""Tracks research progress."""
|
| 59 |
+
current_stage: ResearchStage
|
| 60 |
+
stages_completed: list[ResearchStage] = field(default_factory=list)
|
| 61 |
+
start_time: datetime = field(default_factory=datetime.now)
|
| 62 |
+
stage_times: dict[str, float] = field(default_factory=dict)
|
| 63 |
+
errors: list[str] = field(default_factory=list)
|
| 64 |
+
|
| 65 |
+
def complete_stage(self, stage: ResearchStage, duration: float) -> None:
|
| 66 |
+
"""Mark a stage as complete."""
|
| 67 |
+
self.stages_completed.append(stage)
|
| 68 |
+
self.stage_times[stage.value] = duration
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def progress_percentage(self) -> float:
|
| 72 |
+
"""Get completion percentage."""
|
| 73 |
+
total_stages = len(ResearchStage) - 1 # Exclude COMPLETE
|
| 74 |
+
return (len(self.stages_completed) / total_stages) * 100
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclass
|
| 78 |
+
class ResearchSession:
|
| 79 |
+
"""A research session with all intermediate results."""
|
| 80 |
+
session_id: str
|
| 81 |
+
query: str
|
| 82 |
+
progress: ResearchProgress
|
| 83 |
+
query_analysis: QueryAnalysis | None = None
|
| 84 |
+
search_results: list[SearchResult] = field(default_factory=list)
|
| 85 |
+
sources: list[Source] = field(default_factory=list)
|
| 86 |
+
reasoning_steps: list[ReasoningStep] = field(default_factory=list)
|
| 87 |
+
synthesis: dict | None = None
|
| 88 |
+
verification: dict | None = None
|
| 89 |
+
citations: dict | None = None
|
| 90 |
+
final_result: ResearchResult | None = None
|
| 91 |
+
metadata: dict = field(default_factory=dict)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class ResearchOrchestrator:
|
| 95 |
+
"""
|
| 96 |
+
Orchestrates the complete research pipeline.
|
| 97 |
+
|
| 98 |
+
Coordinates all modules to perform comprehensive research:
|
| 99 |
+
1. Query Understanding - Analyze and decompose the query
|
| 100 |
+
2. Web Search - Search and extract relevant content
|
| 101 |
+
3. Reasoning - Synthesize and analyze findings
|
| 102 |
+
4. Verification - Verify claims and assess credibility
|
| 103 |
+
5. Citation - Generate proper citations
|
| 104 |
+
6. Output Generation - Create formatted output
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(self, config: Config | None = None) -> None:
|
| 108 |
+
"""
|
| 109 |
+
Initialize the ResearchOrchestrator.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
config: Configuration object. Uses default if not provided.
|
| 113 |
+
"""
|
| 114 |
+
self.config = config or Config()
|
| 115 |
+
|
| 116 |
+
# Initialize all modules
|
| 117 |
+
self.query_understanding = QueryUnderstanding(self.config)
|
| 118 |
+
self.web_search = WebSearch(self.config)
|
| 119 |
+
self.reasoning_engine = ReasoningEngine(self.config)
|
| 120 |
+
self.verification = Verification(self.config)
|
| 121 |
+
self.citation_manager = CitationManager(self.config)
|
| 122 |
+
self.output_generator = OutputGenerator(self.config)
|
| 123 |
+
self.error_handler = ErrorHandler(self.config)
|
| 124 |
+
|
| 125 |
+
# Session tracking
|
| 126 |
+
self.active_sessions: dict[str, ResearchSession] = {}
|
| 127 |
+
self._session_counter = 0
|
| 128 |
+
|
| 129 |
+
async def research(
|
| 130 |
+
self,
|
| 131 |
+
query: str,
|
| 132 |
+
audience: AudienceType = AudienceType.GENERAL,
|
| 133 |
+
citation_style: CitationStyle = CitationStyle.APA,
|
| 134 |
+
output_format: OutputFormat = OutputFormat.MARKDOWN,
|
| 135 |
+
max_sources: int = 10,
|
| 136 |
+
verify_claims: bool = True,
|
| 137 |
+
progress_callback: callable | None = None
|
| 138 |
+
) -> ResearchResult:
|
| 139 |
+
"""
|
| 140 |
+
Perform comprehensive research on a query.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
query: The research query
|
| 144 |
+
audience: Target audience type
|
| 145 |
+
citation_style: Citation style to use
|
| 146 |
+
output_format: Desired output format
|
| 147 |
+
max_sources: Maximum sources to use
|
| 148 |
+
verify_claims: Whether to verify claims
|
| 149 |
+
progress_callback: Optional callback for progress updates
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
Complete ResearchResult with findings
|
| 153 |
+
"""
|
| 154 |
+
# Create session
|
| 155 |
+
session = self._create_session(query)
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
# Stage 1: Query Analysis
|
| 159 |
+
await self._run_stage(
|
| 160 |
+
session,
|
| 161 |
+
ResearchStage.QUERY_ANALYSIS,
|
| 162 |
+
self._analyze_query,
|
| 163 |
+
query,
|
| 164 |
+
progress_callback
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Stage 2: Web Search
|
| 168 |
+
await self._run_stage(
|
| 169 |
+
session,
|
| 170 |
+
ResearchStage.WEB_SEARCH,
|
| 171 |
+
self._search_web,
|
| 172 |
+
session,
|
| 173 |
+
max_sources,
|
| 174 |
+
progress_callback
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Stage 3: Reasoning
|
| 178 |
+
await self._run_stage(
|
| 179 |
+
session,
|
| 180 |
+
ResearchStage.REASONING,
|
| 181 |
+
self._reason,
|
| 182 |
+
session,
|
| 183 |
+
progress_callback
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Stage 4: Verification (optional)
|
| 187 |
+
if verify_claims:
|
| 188 |
+
await self._run_stage(
|
| 189 |
+
session,
|
| 190 |
+
ResearchStage.VERIFICATION,
|
| 191 |
+
self._verify,
|
| 192 |
+
session,
|
| 193 |
+
progress_callback
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Stage 5: Citation
|
| 197 |
+
await self._run_stage(
|
| 198 |
+
session,
|
| 199 |
+
ResearchStage.CITATION,
|
| 200 |
+
self._generate_citations,
|
| 201 |
+
session,
|
| 202 |
+
citation_style,
|
| 203 |
+
progress_callback
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# Stage 6: Output Generation
|
| 207 |
+
await self._run_stage(
|
| 208 |
+
session,
|
| 209 |
+
ResearchStage.OUTPUT_GENERATION,
|
| 210 |
+
self._generate_output,
|
| 211 |
+
session,
|
| 212 |
+
audience,
|
| 213 |
+
progress_callback
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Mark complete
|
| 217 |
+
session.progress.current_stage = ResearchStage.COMPLETE
|
| 218 |
+
if progress_callback:
|
| 219 |
+
progress_callback(ResearchStage.COMPLETE, 100.0)
|
| 220 |
+
|
| 221 |
+
return session.final_result
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
logger.error(f"Research failed: {e}")
|
| 225 |
+
|
| 226 |
+
# Try to generate partial results
|
| 227 |
+
partial_result = await self._handle_research_failure(session, e)
|
| 228 |
+
return partial_result
|
| 229 |
+
|
| 230 |
+
finally:
|
| 231 |
+
# Clean up session
|
| 232 |
+
self._cleanup_session(session.session_id)
|
| 233 |
+
|
| 234 |
+
async def _run_stage(
|
| 235 |
+
self,
|
| 236 |
+
session: ResearchSession,
|
| 237 |
+
stage: ResearchStage,
|
| 238 |
+
stage_func: callable,
|
| 239 |
+
*args,
|
| 240 |
+
**kwargs
|
| 241 |
+
) -> None:
|
| 242 |
+
"""Run a research stage with error handling."""
|
| 243 |
+
progress_callback = kwargs.pop('progress_callback', None)
|
| 244 |
+
|
| 245 |
+
session.progress.current_stage = stage
|
| 246 |
+
start_time = datetime.now()
|
| 247 |
+
|
| 248 |
+
if progress_callback:
|
| 249 |
+
progress_callback(stage, session.progress.progress_percentage)
|
| 250 |
+
|
| 251 |
+
logger.info(f"Starting stage: {stage.value}")
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
await stage_func(*args)
|
| 255 |
+
duration = (datetime.now() - start_time).total_seconds()
|
| 256 |
+
session.progress.complete_stage(stage, duration)
|
| 257 |
+
logger.info(f"Completed stage: {stage.value} in {duration:.2f}s")
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
duration = (datetime.now() - start_time).total_seconds()
|
| 261 |
+
session.progress.errors.append(f"{stage.value}: {str(e)}")
|
| 262 |
+
logger.error(f"Stage {stage.value} failed: {e}")
|
| 263 |
+
raise
|
| 264 |
+
|
| 265 |
+
async def _analyze_query(self, query: str) -> QueryAnalysis:
|
| 266 |
+
"""Analyze the research query."""
|
| 267 |
+
session = self._get_current_session(query)
|
| 268 |
+
|
| 269 |
+
analysis = await self.query_understanding.analyze(query)
|
| 270 |
+
session.query_analysis = analysis
|
| 271 |
+
|
| 272 |
+
return analysis
|
| 273 |
+
|
| 274 |
+
async def _search_web(
|
| 275 |
+
self,
|
| 276 |
+
session: ResearchSession,
|
| 277 |
+
max_sources: int
|
| 278 |
+
) -> list[Source]:
|
| 279 |
+
"""Search the web for relevant information."""
|
| 280 |
+
if not session.query_analysis:
|
| 281 |
+
raise ResearchError("Query analysis not available")
|
| 282 |
+
|
| 283 |
+
# Generate search queries from sub-queries
|
| 284 |
+
all_results = []
|
| 285 |
+
|
| 286 |
+
for sub_query in session.query_analysis.sub_queries:
|
| 287 |
+
results = await self.web_search.search(
|
| 288 |
+
sub_query.query,
|
| 289 |
+
max_results=max_sources // len(session.query_analysis.sub_queries) + 1
|
| 290 |
+
)
|
| 291 |
+
all_results.extend(results)
|
| 292 |
+
|
| 293 |
+
# Also search main query
|
| 294 |
+
main_results = await self.web_search.search(
|
| 295 |
+
session.query,
|
| 296 |
+
max_results=max_sources
|
| 297 |
+
)
|
| 298 |
+
all_results.extend(main_results)
|
| 299 |
+
|
| 300 |
+
# Deduplicate by URL
|
| 301 |
+
seen_urls = set()
|
| 302 |
+
unique_results = []
|
| 303 |
+
for result in all_results:
|
| 304 |
+
if result.url not in seen_urls:
|
| 305 |
+
seen_urls.add(result.url)
|
| 306 |
+
unique_results.append(result)
|
| 307 |
+
|
| 308 |
+
session.search_results = unique_results[:max_sources]
|
| 309 |
+
|
| 310 |
+
# Convert to Sources with extracted content
|
| 311 |
+
sources = []
|
| 312 |
+
for result in session.search_results:
|
| 313 |
+
content = await self.web_search.extract_content(result.url)
|
| 314 |
+
source = Source(
|
| 315 |
+
source_id=result.url[:32],
|
| 316 |
+
url=result.url,
|
| 317 |
+
title=result.title,
|
| 318 |
+
content=content.get("main_content", result.snippet),
|
| 319 |
+
domain=result.domain,
|
| 320 |
+
credibility_score=0.7 # Default, will be updated
|
| 321 |
+
)
|
| 322 |
+
sources.append(source)
|
| 323 |
+
|
| 324 |
+
session.sources = sources
|
| 325 |
+
return sources
|
| 326 |
+
|
| 327 |
+
async def _reason(self, session: ResearchSession) -> dict:
|
| 328 |
+
"""Perform reasoning on gathered information."""
|
| 329 |
+
if not session.sources:
|
| 330 |
+
raise ResearchError("No sources available for reasoning")
|
| 331 |
+
|
| 332 |
+
# Prepare information for reasoning
|
| 333 |
+
information = [
|
| 334 |
+
{
|
| 335 |
+
"source": source.title,
|
| 336 |
+
"url": source.url,
|
| 337 |
+
"content": source.content[:2000] if source.content else ""
|
| 338 |
+
}
|
| 339 |
+
for source in session.sources
|
| 340 |
+
]
|
| 341 |
+
|
| 342 |
+
# Use chain of thought reasoning
|
| 343 |
+
reasoning_result = await self.reasoning_engine.chain_of_thought(
|
| 344 |
+
session.query,
|
| 345 |
+
str(information)
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
session.reasoning_steps = reasoning_result.get("reasoning_chain", [])
|
| 349 |
+
|
| 350 |
+
# Synthesize information
|
| 351 |
+
synthesis_result = await self.reasoning_engine.synthesize_information(
|
| 352 |
+
session.query,
|
| 353 |
+
information
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
session.synthesis = synthesis_result
|
| 357 |
+
|
| 358 |
+
return synthesis_result
|
| 359 |
+
|
| 360 |
+
async def _verify(self, session: ResearchSession) -> dict:
|
| 361 |
+
"""Verify claims and assess source credibility."""
|
| 362 |
+
if not session.synthesis:
|
| 363 |
+
raise ResearchError("Synthesis not available for verification")
|
| 364 |
+
|
| 365 |
+
# Extract claims from synthesis
|
| 366 |
+
claims = session.synthesis.get("key_findings", [])
|
| 367 |
+
|
| 368 |
+
# Verify claims
|
| 369 |
+
verification_result = await self.verification.verify(
|
| 370 |
+
claims,
|
| 371 |
+
session.sources
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
# Assess source credibility
|
| 375 |
+
for source in session.sources:
|
| 376 |
+
credibility = await self.verification.assess_credibility(source)
|
| 377 |
+
source.credibility_score = credibility.get("overall_score", 0.7)
|
| 378 |
+
|
| 379 |
+
session.verification = verification_result
|
| 380 |
+
|
| 381 |
+
return verification_result
|
| 382 |
+
|
| 383 |
+
async def _generate_citations(
|
| 384 |
+
self,
|
| 385 |
+
session: ResearchSession,
|
| 386 |
+
style: CitationStyle
|
| 387 |
+
) -> dict:
|
| 388 |
+
"""Generate citations for sources."""
|
| 389 |
+
if not session.sources:
|
| 390 |
+
raise ResearchError("No sources available for citation")
|
| 391 |
+
|
| 392 |
+
synthesis_text = session.synthesis.get("synthesis", "") if session.synthesis else ""
|
| 393 |
+
|
| 394 |
+
citations = await self.citation_manager.generate_citations(
|
| 395 |
+
session.sources,
|
| 396 |
+
synthesis_text
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
session.citations = citations
|
| 400 |
+
|
| 401 |
+
return citations
|
| 402 |
+
|
| 403 |
+
async def _generate_output(
|
| 404 |
+
self,
|
| 405 |
+
session: ResearchSession,
|
| 406 |
+
audience: AudienceType
|
| 407 |
+
) -> ResearchResult:
|
| 408 |
+
"""Generate the final research output."""
|
| 409 |
+
# Calculate confidence
|
| 410 |
+
confidence = self._calculate_confidence(session)
|
| 411 |
+
|
| 412 |
+
# Prepare findings
|
| 413 |
+
findings = {
|
| 414 |
+
"synthesis": session.synthesis,
|
| 415 |
+
"verification": session.verification,
|
| 416 |
+
"reasoning_steps": session.reasoning_steps,
|
| 417 |
+
"information_gaps": session.synthesis.get("gaps", []) if session.synthesis else []
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
# Generate report
|
| 421 |
+
report = await self.output_generator.generate_report(
|
| 422 |
+
session.query,
|
| 423 |
+
findings,
|
| 424 |
+
session.sources,
|
| 425 |
+
confidence
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
# Generate summary
|
| 429 |
+
summary = await self.output_generator.generate_summary(
|
| 430 |
+
findings,
|
| 431 |
+
SummaryLength.STANDARD
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# Build final result
|
| 435 |
+
result = ResearchResult(
|
| 436 |
+
query=session.query,
|
| 437 |
+
answer=summary.get("summary", {}).get("text", ""),
|
| 438 |
+
confidence=confidence,
|
| 439 |
+
sources=session.sources,
|
| 440 |
+
reasoning_steps=session.reasoning_steps,
|
| 441 |
+
verification_status="verified" if session.verification else "unverified",
|
| 442 |
+
metadata={
|
| 443 |
+
"report": report,
|
| 444 |
+
"citations": session.citations,
|
| 445 |
+
"audience": audience.value,
|
| 446 |
+
"session_id": session.session_id,
|
| 447 |
+
"duration": session.progress.stage_times
|
| 448 |
+
}
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
session.final_result = result
|
| 452 |
+
|
| 453 |
+
return result
|
| 454 |
+
|
| 455 |
+
def _calculate_confidence(self, session: ResearchSession) -> float:
|
| 456 |
+
"""Calculate overall confidence score."""
|
| 457 |
+
factors = []
|
| 458 |
+
|
| 459 |
+
# Source quality
|
| 460 |
+
if session.sources:
|
| 461 |
+
avg_credibility = sum(s.credibility_score for s in session.sources) / len(session.sources)
|
| 462 |
+
factors.append(avg_credibility)
|
| 463 |
+
|
| 464 |
+
# Verification status
|
| 465 |
+
if session.verification:
|
| 466 |
+
verification_score = session.verification.get("overall_confidence", 0.5)
|
| 467 |
+
factors.append(verification_score)
|
| 468 |
+
|
| 469 |
+
# Source count
|
| 470 |
+
source_score = min(len(session.sources) / 10, 1.0)
|
| 471 |
+
factors.append(source_score)
|
| 472 |
+
|
| 473 |
+
# Error count
|
| 474 |
+
error_penalty = len(session.progress.errors) * 0.1
|
| 475 |
+
|
| 476 |
+
if factors:
|
| 477 |
+
base_confidence = sum(factors) / len(factors)
|
| 478 |
+
return max(0.0, min(1.0, base_confidence - error_penalty))
|
| 479 |
+
|
| 480 |
+
return 0.5
|
| 481 |
+
|
| 482 |
+
async def _handle_research_failure(
|
| 483 |
+
self,
|
| 484 |
+
session: ResearchSession,
|
| 485 |
+
error: Exception
|
| 486 |
+
) -> ResearchResult:
|
| 487 |
+
"""Handle research failure and generate partial results."""
|
| 488 |
+
logger.warning(f"Generating partial results due to: {error}")
|
| 489 |
+
|
| 490 |
+
# Try to generate fallback content
|
| 491 |
+
fallback = await self.error_handler.generate_fallback_content(
|
| 492 |
+
session.query,
|
| 493 |
+
session.synthesis,
|
| 494 |
+
[str(error)],
|
| 495 |
+
None
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
# Build partial result
|
| 499 |
+
return ResearchResult(
|
| 500 |
+
query=session.query,
|
| 501 |
+
answer=fallback.get("fallback_content", {}).get("response", "Research could not be completed."),
|
| 502 |
+
confidence=0.2,
|
| 503 |
+
sources=session.sources,
|
| 504 |
+
reasoning_steps=session.reasoning_steps,
|
| 505 |
+
verification_status="failed",
|
| 506 |
+
metadata={
|
| 507 |
+
"error": str(error),
|
| 508 |
+
"partial": True,
|
| 509 |
+
"fallback": fallback
|
| 510 |
+
}
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
def _create_session(self, query: str) -> ResearchSession:
|
| 514 |
+
"""Create a new research session."""
|
| 515 |
+
self._session_counter += 1
|
| 516 |
+
session_id = f"session_{self._session_counter}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
| 517 |
+
|
| 518 |
+
session = ResearchSession(
|
| 519 |
+
session_id=session_id,
|
| 520 |
+
query=query,
|
| 521 |
+
progress=ResearchProgress(current_stage=ResearchStage.QUERY_ANALYSIS)
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
self.active_sessions[session_id] = session
|
| 525 |
+
return session
|
| 526 |
+
|
| 527 |
+
def _get_current_session(self, query: str) -> ResearchSession:
|
| 528 |
+
"""Get the current session for a query."""
|
| 529 |
+
for session in self.active_sessions.values():
|
| 530 |
+
if session.query == query:
|
| 531 |
+
return session
|
| 532 |
+
raise ResearchError(f"No session found for query: {query}")
|
| 533 |
+
|
| 534 |
+
def _cleanup_session(self, session_id: str) -> None:
|
| 535 |
+
"""Clean up a research session."""
|
| 536 |
+
if session_id in self.active_sessions:
|
| 537 |
+
del self.active_sessions[session_id]
|
| 538 |
+
|
| 539 |
+
async def get_session_status(self, session_id: str) -> dict:
|
| 540 |
+
"""Get the status of a research session."""
|
| 541 |
+
if session_id not in self.active_sessions:
|
| 542 |
+
return {"error": "Session not found"}
|
| 543 |
+
|
| 544 |
+
session = self.active_sessions[session_id]
|
| 545 |
+
return {
|
| 546 |
+
"session_id": session_id,
|
| 547 |
+
"query": session.query,
|
| 548 |
+
"current_stage": session.progress.current_stage.value,
|
| 549 |
+
"progress": session.progress.progress_percentage,
|
| 550 |
+
"stages_completed": [s.value for s in session.progress.stages_completed],
|
| 551 |
+
"errors": session.progress.errors,
|
| 552 |
+
"has_result": session.final_result is not None
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
# Convenience function for quick research
|
| 557 |
+
async def research(query: str, **kwargs) -> ResearchResult:
|
| 558 |
+
"""
|
| 559 |
+
Perform research on a query.
|
| 560 |
+
|
| 561 |
+
This is a convenience function that creates an orchestrator
|
| 562 |
+
and performs research.
|
| 563 |
+
|
| 564 |
+
Args:
|
| 565 |
+
query: The research query
|
| 566 |
+
**kwargs: Additional arguments for research()
|
| 567 |
+
|
| 568 |
+
Returns:
|
| 569 |
+
ResearchResult with findings
|
| 570 |
+
"""
|
| 571 |
+
orchestrator = ResearchOrchestrator()
|
| 572 |
+
return await orchestrator.research(query, **kwargs)
|
src/prompts/__init__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt templates for Deep Research AI.
|
| 3 |
+
|
| 4 |
+
This package contains all prompt templates used by the research modules.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# System prompts
|
| 8 |
+
from .system_prompts import (
|
| 9 |
+
RESEARCH_SYSTEM_PROMPT,
|
| 10 |
+
QUERY_ANALYSIS_SYSTEM_PROMPT,
|
| 11 |
+
REASONING_SYSTEM_PROMPT,
|
| 12 |
+
VERIFICATION_SYSTEM_PROMPT,
|
| 13 |
+
OUTPUT_SYSTEM_PROMPT,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Query understanding prompts
|
| 17 |
+
from .query_prompts import (
|
| 18 |
+
QUERY_ANALYSIS_PROMPT,
|
| 19 |
+
QUERY_DECOMPOSITION_PROMPT,
|
| 20 |
+
ENTITY_EXTRACTION_PROMPT,
|
| 21 |
+
QUERY_CLARIFICATION_PROMPT,
|
| 22 |
+
QUERY_VALIDATION_PROMPT,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Search prompts
|
| 26 |
+
from .search_prompts import (
|
| 27 |
+
SEARCH_QUERY_GENERATION_PROMPT,
|
| 28 |
+
RESULT_RELEVANCE_PROMPT,
|
| 29 |
+
CONTENT_EXTRACTION_PROMPT,
|
| 30 |
+
SOURCE_EVALUATION_PROMPT,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Reasoning prompts
|
| 34 |
+
from .reasoning_prompts import (
|
| 35 |
+
CHAIN_OF_THOUGHT_PROMPT,
|
| 36 |
+
INFORMATION_SYNTHESIS_PROMPT,
|
| 37 |
+
COMPARISON_ANALYSIS_PROMPT,
|
| 38 |
+
EVIDENCE_EVALUATION_PROMPT,
|
| 39 |
+
CONCLUSION_GENERATION_PROMPT,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Verification prompts
|
| 43 |
+
from .verification_prompts import (
|
| 44 |
+
CROSS_REFERENCE_PROMPT,
|
| 45 |
+
SOURCE_CREDIBILITY_PROMPT,
|
| 46 |
+
CONFLICT_DETECTION_PROMPT,
|
| 47 |
+
FACT_CHECK_PROMPT,
|
| 48 |
+
BIAS_DETECTION_PROMPT,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Citation prompts
|
| 52 |
+
from .citation_prompts import (
|
| 53 |
+
CITATION_GENERATION_PROMPT,
|
| 54 |
+
SOURCE_ATTRIBUTION_PROMPT,
|
| 55 |
+
REFERENCE_LIST_PROMPT,
|
| 56 |
+
INLINE_CITATION_PROMPT,
|
| 57 |
+
CITATION_VALIDATION_PROMPT,
|
| 58 |
+
SOURCE_METADATA_PROMPT,
|
| 59 |
+
FOOTNOTE_GENERATION_PROMPT,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Output generation prompts
|
| 63 |
+
from .output_prompts import (
|
| 64 |
+
REPORT_GENERATION_PROMPT,
|
| 65 |
+
SUMMARY_GENERATION_PROMPT,
|
| 66 |
+
ANSWER_FORMATTING_PROMPT,
|
| 67 |
+
VISUALIZATION_SUGGESTION_PROMPT,
|
| 68 |
+
MULTI_FORMAT_OUTPUT_PROMPT,
|
| 69 |
+
RESPONSE_QUALITY_PROMPT,
|
| 70 |
+
FOLLOWUP_QUESTIONS_PROMPT,
|
| 71 |
+
EXPORT_FORMAT_PROMPT,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Error handling prompts
|
| 75 |
+
from .error_prompts import (
|
| 76 |
+
ERROR_ANALYSIS_PROMPT,
|
| 77 |
+
GRACEFUL_DEGRADATION_PROMPT,
|
| 78 |
+
USER_ERROR_MESSAGE_PROMPT,
|
| 79 |
+
RETRY_STRATEGY_PROMPT,
|
| 80 |
+
ERROR_RECOVERY_PROMPT,
|
| 81 |
+
FALLBACK_CONTENT_PROMPT,
|
| 82 |
+
SYSTEM_HEALTH_PROMPT,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
__all__ = [
|
| 86 |
+
# System prompts
|
| 87 |
+
"RESEARCH_SYSTEM_PROMPT",
|
| 88 |
+
"QUERY_ANALYSIS_SYSTEM_PROMPT",
|
| 89 |
+
"REASONING_SYSTEM_PROMPT",
|
| 90 |
+
"VERIFICATION_SYSTEM_PROMPT",
|
| 91 |
+
"OUTPUT_SYSTEM_PROMPT",
|
| 92 |
+
|
| 93 |
+
# Query prompts
|
| 94 |
+
"QUERY_ANALYSIS_PROMPT",
|
| 95 |
+
"QUERY_DECOMPOSITION_PROMPT",
|
| 96 |
+
"ENTITY_EXTRACTION_PROMPT",
|
| 97 |
+
"QUERY_CLARIFICATION_PROMPT",
|
| 98 |
+
"QUERY_VALIDATION_PROMPT",
|
| 99 |
+
|
| 100 |
+
# Search prompts
|
| 101 |
+
"SEARCH_QUERY_GENERATION_PROMPT",
|
| 102 |
+
"RESULT_RELEVANCE_PROMPT",
|
| 103 |
+
"CONTENT_EXTRACTION_PROMPT",
|
| 104 |
+
"SOURCE_EVALUATION_PROMPT",
|
| 105 |
+
|
| 106 |
+
# Reasoning prompts
|
| 107 |
+
"CHAIN_OF_THOUGHT_PROMPT",
|
| 108 |
+
"INFORMATION_SYNTHESIS_PROMPT",
|
| 109 |
+
"COMPARISON_ANALYSIS_PROMPT",
|
| 110 |
+
"EVIDENCE_EVALUATION_PROMPT",
|
| 111 |
+
"CONCLUSION_GENERATION_PROMPT",
|
| 112 |
+
|
| 113 |
+
# Verification prompts
|
| 114 |
+
"CROSS_REFERENCE_PROMPT",
|
| 115 |
+
"SOURCE_CREDIBILITY_PROMPT",
|
| 116 |
+
"CONFLICT_DETECTION_PROMPT",
|
| 117 |
+
"FACT_CHECK_PROMPT",
|
| 118 |
+
"BIAS_DETECTION_PROMPT",
|
| 119 |
+
|
| 120 |
+
# Citation prompts
|
| 121 |
+
"CITATION_GENERATION_PROMPT",
|
| 122 |
+
"SOURCE_ATTRIBUTION_PROMPT",
|
| 123 |
+
"REFERENCE_LIST_PROMPT",
|
| 124 |
+
"INLINE_CITATION_PROMPT",
|
| 125 |
+
"CITATION_VALIDATION_PROMPT",
|
| 126 |
+
"SOURCE_METADATA_PROMPT",
|
| 127 |
+
"FOOTNOTE_GENERATION_PROMPT",
|
| 128 |
+
|
| 129 |
+
# Output prompts
|
| 130 |
+
"REPORT_GENERATION_PROMPT",
|
| 131 |
+
"SUMMARY_GENERATION_PROMPT",
|
| 132 |
+
"ANSWER_FORMATTING_PROMPT",
|
| 133 |
+
"VISUALIZATION_SUGGESTION_PROMPT",
|
| 134 |
+
"MULTI_FORMAT_OUTPUT_PROMPT",
|
| 135 |
+
"RESPONSE_QUALITY_PROMPT",
|
| 136 |
+
"FOLLOWUP_QUESTIONS_PROMPT",
|
| 137 |
+
"EXPORT_FORMAT_PROMPT",
|
| 138 |
+
|
| 139 |
+
# Error prompts
|
| 140 |
+
"ERROR_ANALYSIS_PROMPT",
|
| 141 |
+
"GRACEFUL_DEGRADATION_PROMPT",
|
| 142 |
+
"USER_ERROR_MESSAGE_PROMPT",
|
| 143 |
+
"RETRY_STRATEGY_PROMPT",
|
| 144 |
+
"ERROR_RECOVERY_PROMPT",
|
| 145 |
+
"FALLBACK_CONTENT_PROMPT",
|
| 146 |
+
"SYSTEM_HEALTH_PROMPT",
|
| 147 |
+
]
|
src/prompts/citation_prompts.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Citation prompts for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
These prompts handle citation generation, formatting, and source attribution
|
| 5 |
+
to ensure proper academic-style references and source tracking.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# Citation Generation Prompt
|
| 9 |
+
CITATION_GENERATION_PROMPT = """You are an expert citation and source attribution specialist.
|
| 10 |
+
|
| 11 |
+
Your task is to generate proper citations for the sources used in research.
|
| 12 |
+
|
| 13 |
+
SOURCES TO CITE:
|
| 14 |
+
{sources}
|
| 15 |
+
|
| 16 |
+
CONTENT USING THESE SOURCES:
|
| 17 |
+
{content}
|
| 18 |
+
|
| 19 |
+
Generate citations for each source following these guidelines:
|
| 20 |
+
|
| 21 |
+
1. **Citation Style**: Generate citations in multiple formats:
|
| 22 |
+
- APA (7th edition)
|
| 23 |
+
- MLA (9th edition)
|
| 24 |
+
- Chicago (17th edition)
|
| 25 |
+
- IEEE
|
| 26 |
+
- Harvard
|
| 27 |
+
|
| 28 |
+
2. **Source Metadata**: Extract and include:
|
| 29 |
+
- Author(s) or organization
|
| 30 |
+
- Publication date
|
| 31 |
+
- Title
|
| 32 |
+
- Publisher/Website name
|
| 33 |
+
- URL
|
| 34 |
+
- Access date
|
| 35 |
+
|
| 36 |
+
3. **In-text Citations**: Generate appropriate in-text citation markers
|
| 37 |
+
|
| 38 |
+
4. **Citation Quality**:
|
| 39 |
+
- Handle missing metadata gracefully
|
| 40 |
+
- Indicate when information is inferred
|
| 41 |
+
- Flag incomplete citations
|
| 42 |
+
|
| 43 |
+
Respond in JSON format:
|
| 44 |
+
{{
|
| 45 |
+
"citations": [
|
| 46 |
+
{{
|
| 47 |
+
"source_id": "unique_id",
|
| 48 |
+
"source_url": "original_url",
|
| 49 |
+
"metadata": {{
|
| 50 |
+
"authors": ["author names or null"],
|
| 51 |
+
"title": "title",
|
| 52 |
+
"publication_date": "date or null",
|
| 53 |
+
"publisher": "publisher name",
|
| 54 |
+
"access_date": "YYYY-MM-DD"
|
| 55 |
+
}},
|
| 56 |
+
"formats": {{
|
| 57 |
+
"apa": "APA formatted citation",
|
| 58 |
+
"mla": "MLA formatted citation",
|
| 59 |
+
"chicago": "Chicago formatted citation",
|
| 60 |
+
"ieee": "IEEE formatted citation",
|
| 61 |
+
"harvard": "Harvard formatted citation"
|
| 62 |
+
}},
|
| 63 |
+
"in_text": {{
|
| 64 |
+
"apa": "(Author, Year)",
|
| 65 |
+
"mla": "(Author Page)",
|
| 66 |
+
"numeric": "[1]"
|
| 67 |
+
}},
|
| 68 |
+
"completeness_score": 0.0-1.0,
|
| 69 |
+
"missing_fields": ["list of missing metadata"]
|
| 70 |
+
}}
|
| 71 |
+
],
|
| 72 |
+
"bibliography": {{
|
| 73 |
+
"apa": "Full APA bibliography",
|
| 74 |
+
"mla": "Full MLA works cited",
|
| 75 |
+
"chicago": "Full Chicago bibliography"
|
| 76 |
+
}}
|
| 77 |
+
}}
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
# Source Attribution Prompt
|
| 81 |
+
SOURCE_ATTRIBUTION_PROMPT = """You are an expert in source attribution and provenance tracking.
|
| 82 |
+
|
| 83 |
+
Your task is to map claims in the research content to their original sources.
|
| 84 |
+
|
| 85 |
+
RESEARCH CONTENT:
|
| 86 |
+
{content}
|
| 87 |
+
|
| 88 |
+
AVAILABLE SOURCES:
|
| 89 |
+
{sources}
|
| 90 |
+
|
| 91 |
+
For each significant claim or piece of information, attribute it to its source:
|
| 92 |
+
|
| 93 |
+
1. **Claim Identification**: Identify each factual claim or data point
|
| 94 |
+
2. **Source Mapping**: Map each claim to one or more sources
|
| 95 |
+
3. **Attribution Confidence**: Rate confidence in the attribution
|
| 96 |
+
4. **Quote vs Paraphrase**: Distinguish between direct quotes and paraphrased content
|
| 97 |
+
|
| 98 |
+
Respond in JSON format:
|
| 99 |
+
{{
|
| 100 |
+
"attributions": [
|
| 101 |
+
{{
|
| 102 |
+
"claim": "The factual claim or information",
|
| 103 |
+
"claim_type": "statistic|fact|quote|analysis|opinion",
|
| 104 |
+
"sources": [
|
| 105 |
+
{{
|
| 106 |
+
"source_id": "source_identifier",
|
| 107 |
+
"source_url": "url",
|
| 108 |
+
"relevance": "direct|supporting|background",
|
| 109 |
+
"is_quote": true/false,
|
| 110 |
+
"original_text": "text from source if quote",
|
| 111 |
+
"confidence": 0.0-1.0
|
| 112 |
+
}}
|
| 113 |
+
],
|
| 114 |
+
"location_in_content": "paragraph/section reference",
|
| 115 |
+
"needs_citation": true/false
|
| 116 |
+
}}
|
| 117 |
+
],
|
| 118 |
+
"unattributed_claims": [
|
| 119 |
+
{{
|
| 120 |
+
"claim": "claim without clear source",
|
| 121 |
+
"reason": "why it couldn't be attributed",
|
| 122 |
+
"suggestion": "suggested action"
|
| 123 |
+
}}
|
| 124 |
+
],
|
| 125 |
+
"attribution_coverage": 0.0-1.0
|
| 126 |
+
}}
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
# Reference List Generation Prompt
|
| 130 |
+
REFERENCE_LIST_PROMPT = """You are an expert in academic reference list generation.
|
| 131 |
+
|
| 132 |
+
Your task is to create a properly formatted reference list from the sources used.
|
| 133 |
+
|
| 134 |
+
SOURCES:
|
| 135 |
+
{sources}
|
| 136 |
+
|
| 137 |
+
CITATION STYLE: {citation_style}
|
| 138 |
+
|
| 139 |
+
Generate a complete reference list following these guidelines:
|
| 140 |
+
|
| 141 |
+
1. **Ordering**:
|
| 142 |
+
- Alphabetical by author surname (APA, MLA, Chicago, Harvard)
|
| 143 |
+
- Numerical by order of appearance (IEEE)
|
| 144 |
+
|
| 145 |
+
2. **Formatting**:
|
| 146 |
+
- Proper indentation (hanging indent where applicable)
|
| 147 |
+
- Correct punctuation and italicization
|
| 148 |
+
- Consistent date formatting
|
| 149 |
+
|
| 150 |
+
3. **Web Sources**:
|
| 151 |
+
- Include retrieval dates for online sources
|
| 152 |
+
- Format URLs appropriately
|
| 153 |
+
- Handle DOIs when available
|
| 154 |
+
|
| 155 |
+
4. **Special Cases**:
|
| 156 |
+
- Multiple authors (et al. rules)
|
| 157 |
+
- No author (organization or title)
|
| 158 |
+
- No date (n.d.)
|
| 159 |
+
|
| 160 |
+
Respond in JSON format:
|
| 161 |
+
{{
|
| 162 |
+
"reference_list": [
|
| 163 |
+
{{
|
| 164 |
+
"number": 1,
|
| 165 |
+
"formatted_reference": "complete formatted reference",
|
| 166 |
+
"source_id": "source_identifier"
|
| 167 |
+
}}
|
| 168 |
+
],
|
| 169 |
+
"formatted_output": "Complete formatted reference list as text",
|
| 170 |
+
"style": "{citation_style}",
|
| 171 |
+
"total_references": 0
|
| 172 |
+
}}
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
# Inline Citation Insertion Prompt
|
| 176 |
+
INLINE_CITATION_PROMPT = """You are an expert in inline citation insertion.
|
| 177 |
+
|
| 178 |
+
Your task is to insert proper inline citations into the research content.
|
| 179 |
+
|
| 180 |
+
CONTENT TO ANNOTATE:
|
| 181 |
+
{content}
|
| 182 |
+
|
| 183 |
+
SOURCE ATTRIBUTIONS:
|
| 184 |
+
{attributions}
|
| 185 |
+
|
| 186 |
+
CITATION STYLE: {citation_style}
|
| 187 |
+
|
| 188 |
+
Insert inline citations following these rules:
|
| 189 |
+
|
| 190 |
+
1. **Placement**:
|
| 191 |
+
- Place citations immediately after the claim
|
| 192 |
+
- Before punctuation for mid-sentence citations
|
| 193 |
+
- After punctuation for end-of-sentence citations
|
| 194 |
+
|
| 195 |
+
2. **Format**:
|
| 196 |
+
- Use appropriate format for the citation style
|
| 197 |
+
- Handle multiple sources for same claim
|
| 198 |
+
- Use "ibid" or "op. cit." where appropriate
|
| 199 |
+
|
| 200 |
+
3. **Readability**:
|
| 201 |
+
- Don't over-cite (one citation per claim usually sufficient)
|
| 202 |
+
- Group related citations
|
| 203 |
+
- Maintain text flow
|
| 204 |
+
|
| 205 |
+
Respond in JSON format:
|
| 206 |
+
{{
|
| 207 |
+
"annotated_content": "Full content with inline citations inserted",
|
| 208 |
+
"citation_count": 0,
|
| 209 |
+
"citation_positions": [
|
| 210 |
+
{{
|
| 211 |
+
"position": "character position",
|
| 212 |
+
"citation": "citation text",
|
| 213 |
+
"source_ids": ["source_id1", "source_id2"]
|
| 214 |
+
}}
|
| 215 |
+
],
|
| 216 |
+
"style_used": "{citation_style}"
|
| 217 |
+
}}
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
# Citation Validation Prompt
|
| 221 |
+
CITATION_VALIDATION_PROMPT = """You are an expert citation validator and quality checker.
|
| 222 |
+
|
| 223 |
+
Your task is to validate the citations and ensure they meet academic standards.
|
| 224 |
+
|
| 225 |
+
CITATIONS TO VALIDATE:
|
| 226 |
+
{citations}
|
| 227 |
+
|
| 228 |
+
SOURCES:
|
| 229 |
+
{sources}
|
| 230 |
+
|
| 231 |
+
Validate each citation for:
|
| 232 |
+
|
| 233 |
+
1. **Accuracy**:
|
| 234 |
+
- Correct author names and order
|
| 235 |
+
- Accurate publication dates
|
| 236 |
+
- Correct titles (no typos)
|
| 237 |
+
- Valid URLs (format check)
|
| 238 |
+
|
| 239 |
+
2. **Completeness**:
|
| 240 |
+
- All required fields present
|
| 241 |
+
- Appropriate handling of missing information
|
| 242 |
+
|
| 243 |
+
3. **Formatting**:
|
| 244 |
+
- Correct punctuation
|
| 245 |
+
- Proper capitalization
|
| 246 |
+
- Correct italicization indicators
|
| 247 |
+
- Proper ordering
|
| 248 |
+
|
| 249 |
+
4. **Consistency**:
|
| 250 |
+
- Same style throughout
|
| 251 |
+
- Consistent abbreviations
|
| 252 |
+
- Uniform date formats
|
| 253 |
+
|
| 254 |
+
Respond in JSON format:
|
| 255 |
+
{{
|
| 256 |
+
"validation_results": [
|
| 257 |
+
{{
|
| 258 |
+
"source_id": "source_identifier",
|
| 259 |
+
"is_valid": true/false,
|
| 260 |
+
"issues": [
|
| 261 |
+
{{
|
| 262 |
+
"type": "accuracy|completeness|formatting|consistency",
|
| 263 |
+
"field": "affected field",
|
| 264 |
+
"issue": "description of issue",
|
| 265 |
+
"suggestion": "how to fix"
|
| 266 |
+
}}
|
| 267 |
+
],
|
| 268 |
+
"corrected_citation": "corrected version if needed"
|
| 269 |
+
}}
|
| 270 |
+
],
|
| 271 |
+
"overall_quality": 0.0-1.0,
|
| 272 |
+
"total_issues": 0,
|
| 273 |
+
"recommendations": ["list of general recommendations"]
|
| 274 |
+
}}
|
| 275 |
+
"""
|
| 276 |
+
|
| 277 |
+
# Source Metadata Extraction Prompt
|
| 278 |
+
SOURCE_METADATA_PROMPT = """You are an expert in extracting metadata from web sources.
|
| 279 |
+
|
| 280 |
+
Your task is to extract citation-relevant metadata from source content.
|
| 281 |
+
|
| 282 |
+
SOURCE URL: {url}
|
| 283 |
+
|
| 284 |
+
SOURCE CONTENT:
|
| 285 |
+
{content}
|
| 286 |
+
|
| 287 |
+
Extract the following metadata:
|
| 288 |
+
|
| 289 |
+
1. **Authors**:
|
| 290 |
+
- Individual authors with full names
|
| 291 |
+
- Corporate/organizational authors
|
| 292 |
+
- Author affiliations if available
|
| 293 |
+
|
| 294 |
+
2. **Publication Info**:
|
| 295 |
+
- Publication date (exact or approximate)
|
| 296 |
+
- Last modified date
|
| 297 |
+
- Publisher/organization
|
| 298 |
+
- Publication type (article, report, webpage, etc.)
|
| 299 |
+
|
| 300 |
+
3. **Document Info**:
|
| 301 |
+
- Full title
|
| 302 |
+
- Subtitle if any
|
| 303 |
+
- Section/chapter if applicable
|
| 304 |
+
- DOI if available
|
| 305 |
+
|
| 306 |
+
4. **Web-specific**:
|
| 307 |
+
- Canonical URL
|
| 308 |
+
- Site name vs publisher name
|
| 309 |
+
- Archive/version information
|
| 310 |
+
|
| 311 |
+
Respond in JSON format:
|
| 312 |
+
{{
|
| 313 |
+
"metadata": {{
|
| 314 |
+
"authors": [
|
| 315 |
+
{{
|
| 316 |
+
"name": "full name",
|
| 317 |
+
"type": "individual|organization",
|
| 318 |
+
"affiliation": "affiliation or null"
|
| 319 |
+
}}
|
| 320 |
+
],
|
| 321 |
+
"title": "document title",
|
| 322 |
+
"subtitle": "subtitle or null",
|
| 323 |
+
"publication_date": "YYYY-MM-DD or null",
|
| 324 |
+
"last_modified": "YYYY-MM-DD or null",
|
| 325 |
+
"publisher": "publisher name",
|
| 326 |
+
"site_name": "website name",
|
| 327 |
+
"publication_type": "article|report|webpage|blog|news|academic",
|
| 328 |
+
"doi": "DOI or null",
|
| 329 |
+
"url": "canonical URL",
|
| 330 |
+
"language": "language code"
|
| 331 |
+
}},
|
| 332 |
+
"extraction_confidence": {{
|
| 333 |
+
"authors": 0.0-1.0,
|
| 334 |
+
"date": 0.0-1.0,
|
| 335 |
+
"title": 0.0-1.0,
|
| 336 |
+
"overall": 0.0-1.0
|
| 337 |
+
}},
|
| 338 |
+
"inferred_fields": ["list of fields that were inferred"],
|
| 339 |
+
"missing_fields": ["list of fields that couldn't be found"]
|
| 340 |
+
}}
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
# Footnote Generation Prompt
|
| 344 |
+
FOOTNOTE_GENERATION_PROMPT = """You are an expert in generating footnotes and endnotes.
|
| 345 |
+
|
| 346 |
+
Your task is to generate appropriate footnotes for the research content.
|
| 347 |
+
|
| 348 |
+
CONTENT:
|
| 349 |
+
{content}
|
| 350 |
+
|
| 351 |
+
SOURCES:
|
| 352 |
+
{sources}
|
| 353 |
+
|
| 354 |
+
ATTRIBUTIONS:
|
| 355 |
+
{attributions}
|
| 356 |
+
|
| 357 |
+
Generate footnotes following these guidelines:
|
| 358 |
+
|
| 359 |
+
1. **First Reference**: Full citation on first mention
|
| 360 |
+
2. **Subsequent References**: Shortened form (author, short title, page)
|
| 361 |
+
3. **Ibid Usage**: Use "Ibid." for immediately repeated sources
|
| 362 |
+
4. **Explanatory Notes**: Add brief explanatory notes where helpful
|
| 363 |
+
|
| 364 |
+
Respond in JSON format:
|
| 365 |
+
{{
|
| 366 |
+
"footnotes": [
|
| 367 |
+
{{
|
| 368 |
+
"number": 1,
|
| 369 |
+
"marker_position": "position in text",
|
| 370 |
+
"footnote_text": "complete footnote text",
|
| 371 |
+
"source_id": "source_identifier",
|
| 372 |
+
"is_first_reference": true/false,
|
| 373 |
+
"footnote_type": "citation|explanatory|both"
|
| 374 |
+
}}
|
| 375 |
+
],
|
| 376 |
+
"content_with_markers": "content with footnote markers [1], [2], etc.",
|
| 377 |
+
"footnote_section": "formatted footnotes section"
|
| 378 |
+
}}
|
| 379 |
+
"""
|
src/prompts/error_prompts.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Error handling prompts for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
These prompts handle error recovery, graceful degradation, and
|
| 5 |
+
user-friendly error message generation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# Error Analysis Prompt
|
| 9 |
+
ERROR_ANALYSIS_PROMPT = """You are an expert at analyzing and diagnosing system errors.
|
| 10 |
+
|
| 11 |
+
Your task is to analyze the error and suggest recovery strategies.
|
| 12 |
+
|
| 13 |
+
ERROR DETAILS:
|
| 14 |
+
- Type: {error_type}
|
| 15 |
+
- Message: {error_message}
|
| 16 |
+
- Context: {context}
|
| 17 |
+
- Component: {component}
|
| 18 |
+
|
| 19 |
+
Analyze the error and provide:
|
| 20 |
+
|
| 21 |
+
1. **Root Cause**: What likely caused this error
|
| 22 |
+
2. **Impact**: What functionality is affected
|
| 23 |
+
3. **Recovery Options**: Possible ways to recover
|
| 24 |
+
4. **Prevention**: How to prevent this in the future
|
| 25 |
+
|
| 26 |
+
Respond in JSON format:
|
| 27 |
+
{{
|
| 28 |
+
"analysis": {{
|
| 29 |
+
"root_cause": "most likely cause",
|
| 30 |
+
"impact_level": "low|medium|high|critical",
|
| 31 |
+
"affected_functionality": ["list of affected features"],
|
| 32 |
+
"is_recoverable": true/false
|
| 33 |
+
}},
|
| 34 |
+
"recovery_strategies": [
|
| 35 |
+
{{
|
| 36 |
+
"strategy": "strategy name",
|
| 37 |
+
"description": "how to implement",
|
| 38 |
+
"success_likelihood": 0.0-1.0,
|
| 39 |
+
"side_effects": ["potential side effects"]
|
| 40 |
+
}}
|
| 41 |
+
],
|
| 42 |
+
"user_message": "friendly message for the user",
|
| 43 |
+
"technical_details": "detailed technical explanation",
|
| 44 |
+
"prevention_measures": ["how to prevent in future"]
|
| 45 |
+
}}
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
# Graceful Degradation Prompt
|
| 49 |
+
GRACEFUL_DEGRADATION_PROMPT = """You are an expert at designing graceful degradation strategies.
|
| 50 |
+
|
| 51 |
+
Your task is to suggest how to provide partial results when full functionality fails.
|
| 52 |
+
|
| 53 |
+
FAILED OPERATION: {operation}
|
| 54 |
+
PARTIAL RESULTS: {partial_results}
|
| 55 |
+
MISSING COMPONENTS: {missing_components}
|
| 56 |
+
|
| 57 |
+
Determine how to provide value despite the failure:
|
| 58 |
+
|
| 59 |
+
1. **Partial Delivery**: What can still be delivered?
|
| 60 |
+
2. **Quality Impact**: How is quality affected?
|
| 61 |
+
3. **User Communication**: How to explain the limitation?
|
| 62 |
+
4. **Workarounds**: Alternative approaches to try
|
| 63 |
+
|
| 64 |
+
Respond in JSON format:
|
| 65 |
+
{{
|
| 66 |
+
"degraded_response": {{
|
| 67 |
+
"can_provide_partial": true/false,
|
| 68 |
+
"available_results": "what can be delivered",
|
| 69 |
+
"missing_results": "what is unavailable",
|
| 70 |
+
"quality_reduction": 0.0-1.0
|
| 71 |
+
}},
|
| 72 |
+
"user_communication": {{
|
| 73 |
+
"message": "user-friendly explanation",
|
| 74 |
+
"limitations_explained": ["list of limitations"],
|
| 75 |
+
"workaround_suggestions": ["suggestions for user"]
|
| 76 |
+
}},
|
| 77 |
+
"alternative_approaches": [
|
| 78 |
+
{{
|
| 79 |
+
"approach": "alternative method",
|
| 80 |
+
"feasibility": 0.0-1.0,
|
| 81 |
+
"tradeoffs": ["tradeoffs"]
|
| 82 |
+
}}
|
| 83 |
+
],
|
| 84 |
+
"retry_recommendation": {{
|
| 85 |
+
"should_retry": true/false,
|
| 86 |
+
"retry_strategy": "how to retry",
|
| 87 |
+
"delay_seconds": 0
|
| 88 |
+
}}
|
| 89 |
+
}}
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
# User Error Message Generation Prompt
|
| 93 |
+
USER_ERROR_MESSAGE_PROMPT = """You are an expert at crafting user-friendly error messages.
|
| 94 |
+
|
| 95 |
+
Your task is to create a helpful error message for the user.
|
| 96 |
+
|
| 97 |
+
ERROR INFORMATION:
|
| 98 |
+
- Error Type: {error_type}
|
| 99 |
+
- Technical Message: {technical_message}
|
| 100 |
+
- User Action: {user_action}
|
| 101 |
+
- Severity: {severity}
|
| 102 |
+
|
| 103 |
+
Create a user-friendly message that:
|
| 104 |
+
|
| 105 |
+
1. **Explains** what went wrong in simple terms
|
| 106 |
+
2. **Reassures** the user (if appropriate)
|
| 107 |
+
3. **Guides** them on what to do next
|
| 108 |
+
4. **Avoids** technical jargon
|
| 109 |
+
|
| 110 |
+
Respond in JSON format:
|
| 111 |
+
{{
|
| 112 |
+
"user_message": {{
|
| 113 |
+
"headline": "Brief, clear headline",
|
| 114 |
+
"explanation": "What happened in plain language",
|
| 115 |
+
"what_to_do": "Steps the user can take",
|
| 116 |
+
"tone": "apologetic|informative|helpful"
|
| 117 |
+
}},
|
| 118 |
+
"actions": [
|
| 119 |
+
{{
|
| 120 |
+
"action": "action name",
|
| 121 |
+
"description": "what it does",
|
| 122 |
+
"button_text": "text for button/link"
|
| 123 |
+
}}
|
| 124 |
+
],
|
| 125 |
+
"show_technical_details": true/false,
|
| 126 |
+
"severity_indicator": "info|warning|error|critical"
|
| 127 |
+
}}
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
# Retry Strategy Prompt
|
| 131 |
+
RETRY_STRATEGY_PROMPT = """You are an expert at designing retry strategies for failed operations.
|
| 132 |
+
|
| 133 |
+
Your task is to determine the optimal retry strategy for the failed operation.
|
| 134 |
+
|
| 135 |
+
FAILED OPERATION: {operation}
|
| 136 |
+
FAILURE REASON: {failure_reason}
|
| 137 |
+
ATTEMPT NUMBER: {attempt_number}
|
| 138 |
+
OPERATION CONTEXT: {context}
|
| 139 |
+
|
| 140 |
+
Determine the best retry approach:
|
| 141 |
+
|
| 142 |
+
1. **Should Retry**: Is retrying worthwhile?
|
| 143 |
+
2. **Timing**: How long to wait before retry
|
| 144 |
+
3. **Modification**: Should the request be modified?
|
| 145 |
+
4. **Limit**: Maximum retry attempts
|
| 146 |
+
|
| 147 |
+
Respond in JSON format:
|
| 148 |
+
{{
|
| 149 |
+
"retry_decision": {{
|
| 150 |
+
"should_retry": true/false,
|
| 151 |
+
"reason": "why or why not",
|
| 152 |
+
"max_attempts": 0,
|
| 153 |
+
"current_attempt": {attempt_number}
|
| 154 |
+
}},
|
| 155 |
+
"timing": {{
|
| 156 |
+
"delay_seconds": 0,
|
| 157 |
+
"backoff_strategy": "none|linear|exponential",
|
| 158 |
+
"max_delay_seconds": 0
|
| 159 |
+
}},
|
| 160 |
+
"modifications": {{
|
| 161 |
+
"modify_request": true/false,
|
| 162 |
+
"suggested_changes": ["changes to make"],
|
| 163 |
+
"reduce_scope": true/false
|
| 164 |
+
}},
|
| 165 |
+
"alternatives": [
|
| 166 |
+
{{
|
| 167 |
+
"alternative": "alternative approach",
|
| 168 |
+
"when_to_use": "when this alternative is appropriate"
|
| 169 |
+
}}
|
| 170 |
+
]
|
| 171 |
+
}}
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
# Error Recovery Prompt
|
| 175 |
+
ERROR_RECOVERY_PROMPT = """You are an expert at recovering from errors in complex systems.
|
| 176 |
+
|
| 177 |
+
Your task is to orchestrate recovery from the current error state.
|
| 178 |
+
|
| 179 |
+
CURRENT STATE:
|
| 180 |
+
{current_state}
|
| 181 |
+
|
| 182 |
+
ERROR CHAIN:
|
| 183 |
+
{error_chain}
|
| 184 |
+
|
| 185 |
+
AVAILABLE RESOURCES:
|
| 186 |
+
{available_resources}
|
| 187 |
+
|
| 188 |
+
Plan the recovery:
|
| 189 |
+
|
| 190 |
+
1. **State Assessment**: What is the current system state?
|
| 191 |
+
2. **Recovery Path**: Steps to recover
|
| 192 |
+
3. **Data Salvage**: What data can be saved?
|
| 193 |
+
4. **State Restoration**: How to restore normal operation
|
| 194 |
+
|
| 195 |
+
Respond in JSON format:
|
| 196 |
+
{{
|
| 197 |
+
"state_assessment": {{
|
| 198 |
+
"corruption_level": "none|partial|severe",
|
| 199 |
+
"salvageable_data": ["list of salvageable items"],
|
| 200 |
+
"lost_data": ["list of lost items"]
|
| 201 |
+
}},
|
| 202 |
+
"recovery_plan": [
|
| 203 |
+
{{
|
| 204 |
+
"step": 1,
|
| 205 |
+
"action": "action to take",
|
| 206 |
+
"expected_outcome": "what should happen",
|
| 207 |
+
"fallback": "what to do if this fails"
|
| 208 |
+
}}
|
| 209 |
+
],
|
| 210 |
+
"data_recovery": {{
|
| 211 |
+
"recovered_items": ["items that can be recovered"],
|
| 212 |
+
"recovery_method": "how to recover"
|
| 213 |
+
}},
|
| 214 |
+
"post_recovery": {{
|
| 215 |
+
"verification_steps": ["how to verify recovery"],
|
| 216 |
+
"monitoring_period": "how long to monitor",
|
| 217 |
+
"success_indicators": ["indicators of successful recovery"]
|
| 218 |
+
}}
|
| 219 |
+
}}
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
# Fallback Content Generation Prompt
|
| 223 |
+
FALLBACK_CONTENT_PROMPT = """You are an expert at generating fallback content when primary sources fail.
|
| 224 |
+
|
| 225 |
+
Your task is to generate helpful fallback content based on available information.
|
| 226 |
+
|
| 227 |
+
ORIGINAL QUERY: {query}
|
| 228 |
+
AVAILABLE INFORMATION: {available_info}
|
| 229 |
+
FAILED SOURCES: {failed_sources}
|
| 230 |
+
CACHED DATA: {cached_data}
|
| 231 |
+
|
| 232 |
+
Generate fallback content that:
|
| 233 |
+
|
| 234 |
+
1. **Acknowledges** the limitation
|
| 235 |
+
2. **Provides** whatever information is available
|
| 236 |
+
3. **Suggests** alternatives
|
| 237 |
+
4. **Maintains** quality standards
|
| 238 |
+
|
| 239 |
+
Respond in JSON format:
|
| 240 |
+
{{
|
| 241 |
+
"fallback_content": {{
|
| 242 |
+
"response": "best possible response given limitations",
|
| 243 |
+
"confidence": 0.0-1.0,
|
| 244 |
+
"completeness": 0.0-1.0,
|
| 245 |
+
"based_on": ["what information was used"]
|
| 246 |
+
}},
|
| 247 |
+
"limitations_disclosure": {{
|
| 248 |
+
"what_is_missing": ["missing information"],
|
| 249 |
+
"quality_impact": "how quality is affected",
|
| 250 |
+
"reliability_note": "note about reliability"
|
| 251 |
+
}},
|
| 252 |
+
"next_steps": [
|
| 253 |
+
{{
|
| 254 |
+
"suggestion": "what user could try",
|
| 255 |
+
"likelihood_of_success": 0.0-1.0
|
| 256 |
+
}}
|
| 257 |
+
]
|
| 258 |
+
}}
|
| 259 |
+
"""
|
| 260 |
+
|
| 261 |
+
# System Health Check Prompt
|
| 262 |
+
SYSTEM_HEALTH_PROMPT = """You are an expert at assessing system health and diagnosing issues.
|
| 263 |
+
|
| 264 |
+
Your task is to analyze the system health metrics and identify issues.
|
| 265 |
+
|
| 266 |
+
HEALTH METRICS:
|
| 267 |
+
{health_metrics}
|
| 268 |
+
|
| 269 |
+
RECENT ERRORS:
|
| 270 |
+
{recent_errors}
|
| 271 |
+
|
| 272 |
+
PERFORMANCE DATA:
|
| 273 |
+
{performance_data}
|
| 274 |
+
|
| 275 |
+
Assess the system health:
|
| 276 |
+
|
| 277 |
+
1. **Overall Status**: System health rating
|
| 278 |
+
2. **Components**: Status of each component
|
| 279 |
+
3. **Issues**: Current and potential issues
|
| 280 |
+
4. **Recommendations**: What to do
|
| 281 |
+
|
| 282 |
+
Respond in JSON format:
|
| 283 |
+
{{
|
| 284 |
+
"health_status": {{
|
| 285 |
+
"overall": "healthy|degraded|unhealthy|critical",
|
| 286 |
+
"score": 0.0-1.0
|
| 287 |
+
}},
|
| 288 |
+
"components": [
|
| 289 |
+
{{
|
| 290 |
+
"name": "component name",
|
| 291 |
+
"status": "healthy|degraded|unhealthy",
|
| 292 |
+
"issues": ["issues if any"]
|
| 293 |
+
}}
|
| 294 |
+
],
|
| 295 |
+
"active_issues": [
|
| 296 |
+
{{
|
| 297 |
+
"issue": "issue description",
|
| 298 |
+
"severity": "low|medium|high|critical",
|
| 299 |
+
"affected_functionality": ["affected features"]
|
| 300 |
+
}}
|
| 301 |
+
],
|
| 302 |
+
"recommendations": [
|
| 303 |
+
{{
|
| 304 |
+
"action": "recommended action",
|
| 305 |
+
"priority": "immediate|soon|when_convenient",
|
| 306 |
+
"impact": "expected impact"
|
| 307 |
+
}}
|
| 308 |
+
]
|
| 309 |
+
}}
|
| 310 |
+
"""
|
src/prompts/output_prompts.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Output generation prompts for the Deep Research AI system.
|
| 3 |
+
|
| 4 |
+
These prompts handle the final synthesis and formatting of research results
|
| 5 |
+
into user-friendly, well-structured output formats.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# Report Generation Prompt
|
| 9 |
+
REPORT_GENERATION_PROMPT = """You are an expert research report writer.
|
| 10 |
+
|
| 11 |
+
Your task is to generate a comprehensive, well-structured research report.
|
| 12 |
+
|
| 13 |
+
RESEARCH QUERY:
|
| 14 |
+
{query}
|
| 15 |
+
|
| 16 |
+
SYNTHESIZED FINDINGS:
|
| 17 |
+
{findings}
|
| 18 |
+
|
| 19 |
+
SOURCES USED:
|
| 20 |
+
{sources}
|
| 21 |
+
|
| 22 |
+
CONFIDENCE ASSESSMENT:
|
| 23 |
+
{confidence}
|
| 24 |
+
|
| 25 |
+
Generate a research report following this structure:
|
| 26 |
+
|
| 27 |
+
1. **Executive Summary**: Brief overview of key findings (2-3 paragraphs)
|
| 28 |
+
|
| 29 |
+
2. **Introduction**:
|
| 30 |
+
- Research question context
|
| 31 |
+
- Scope and methodology used
|
| 32 |
+
- Key terms defined
|
| 33 |
+
|
| 34 |
+
3. **Main Findings**:
|
| 35 |
+
- Organized by theme or sub-question
|
| 36 |
+
- Evidence-based claims with source references
|
| 37 |
+
- Data and statistics where available
|
| 38 |
+
|
| 39 |
+
4. **Analysis**:
|
| 40 |
+
- Synthesis of findings
|
| 41 |
+
- Patterns and trends identified
|
| 42 |
+
- Conflicting viewpoints addressed
|
| 43 |
+
|
| 44 |
+
5. **Limitations**:
|
| 45 |
+
- Gaps in available information
|
| 46 |
+
- Confidence levels explained
|
| 47 |
+
- Areas needing further research
|
| 48 |
+
|
| 49 |
+
6. **Conclusion**:
|
| 50 |
+
- Direct answer to research question
|
| 51 |
+
- Key takeaways
|
| 52 |
+
- Recommendations if applicable
|
| 53 |
+
|
| 54 |
+
Respond in JSON format:
|
| 55 |
+
{{
|
| 56 |
+
"report": {{
|
| 57 |
+
"title": "Report title",
|
| 58 |
+
"executive_summary": "2-3 paragraph summary",
|
| 59 |
+
"introduction": {{
|
| 60 |
+
"context": "Research context",
|
| 61 |
+
"scope": "Scope of research",
|
| 62 |
+
"methodology": "Brief methodology",
|
| 63 |
+
"key_terms": {{"term": "definition"}}
|
| 64 |
+
}},
|
| 65 |
+
"main_findings": [
|
| 66 |
+
{{
|
| 67 |
+
"theme": "Finding theme",
|
| 68 |
+
"content": "Detailed findings",
|
| 69 |
+
"evidence": ["supporting evidence"],
|
| 70 |
+
"sources": ["source_ids"]
|
| 71 |
+
}}
|
| 72 |
+
],
|
| 73 |
+
"analysis": {{
|
| 74 |
+
"synthesis": "Synthesized analysis",
|
| 75 |
+
"patterns": ["identified patterns"],
|
| 76 |
+
"conflicting_views": ["conflicts and how addressed"]
|
| 77 |
+
}},
|
| 78 |
+
"limitations": {{
|
| 79 |
+
"information_gaps": ["gaps"],
|
| 80 |
+
"confidence_notes": "confidence explanation",
|
| 81 |
+
"further_research": ["suggested areas"]
|
| 82 |
+
}},
|
| 83 |
+
"conclusion": {{
|
| 84 |
+
"answer": "Direct answer to query",
|
| 85 |
+
"key_takeaways": ["takeaway points"],
|
| 86 |
+
"recommendations": ["recommendations if any"]
|
| 87 |
+
}}
|
| 88 |
+
}},
|
| 89 |
+
"metadata": {{
|
| 90 |
+
"word_count": 0,
|
| 91 |
+
"reading_time_minutes": 0,
|
| 92 |
+
"complexity_level": "beginner|intermediate|advanced"
|
| 93 |
+
}}
|
| 94 |
+
}}
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
# Summary Generation Prompt
|
| 98 |
+
SUMMARY_GENERATION_PROMPT = """You are an expert at creating concise, informative summaries.
|
| 99 |
+
|
| 100 |
+
Your task is to create a summary of research findings at the specified detail level.
|
| 101 |
+
|
| 102 |
+
RESEARCH FINDINGS:
|
| 103 |
+
{findings}
|
| 104 |
+
|
| 105 |
+
SUMMARY LENGTH: {length}
|
| 106 |
+
|
| 107 |
+
Generate a summary following these guidelines:
|
| 108 |
+
|
| 109 |
+
1. **Brief** (1-2 paragraphs): Key answer only
|
| 110 |
+
2. **Standard** (3-5 paragraphs): Answer with main supporting points
|
| 111 |
+
3. **Detailed** (6-10 paragraphs): Comprehensive summary with nuance
|
| 112 |
+
|
| 113 |
+
Include:
|
| 114 |
+
- Direct answer to the research question
|
| 115 |
+
- Most important supporting evidence
|
| 116 |
+
- Key caveats or limitations
|
| 117 |
+
- Confidence level indication
|
| 118 |
+
|
| 119 |
+
Respond in JSON format:
|
| 120 |
+
{{
|
| 121 |
+
"summary": {{
|
| 122 |
+
"text": "The complete summary text",
|
| 123 |
+
"key_points": ["bullet point takeaways"],
|
| 124 |
+
"confidence_statement": "How confident we are in these findings",
|
| 125 |
+
"caveats": ["important caveats"]
|
| 126 |
+
}},
|
| 127 |
+
"metadata": {{
|
| 128 |
+
"length_type": "{length}",
|
| 129 |
+
"word_count": 0,
|
| 130 |
+
"source_count": 0
|
| 131 |
+
}}
|
| 132 |
+
}}
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
# Answer Formatting Prompt
|
| 136 |
+
ANSWER_FORMATTING_PROMPT = """You are an expert at formatting research answers for different audiences.
|
| 137 |
+
|
| 138 |
+
Your task is to format the research answer for the specified audience and format.
|
| 139 |
+
|
| 140 |
+
RESEARCH ANSWER:
|
| 141 |
+
{answer}
|
| 142 |
+
|
| 143 |
+
TARGET AUDIENCE: {audience}
|
| 144 |
+
OUTPUT FORMAT: {format}
|
| 145 |
+
|
| 146 |
+
Format the answer according to these specifications:
|
| 147 |
+
|
| 148 |
+
**Audience Levels:**
|
| 149 |
+
- general: Non-technical, accessible language
|
| 150 |
+
- professional: Business/industry appropriate
|
| 151 |
+
- academic: Scholarly, formal language
|
| 152 |
+
- technical: Technical details included
|
| 153 |
+
|
| 154 |
+
**Output Formats:**
|
| 155 |
+
- text: Plain text with paragraphs
|
| 156 |
+
- markdown: Full markdown formatting
|
| 157 |
+
- html: HTML formatted
|
| 158 |
+
- structured: Bullet points and sections
|
| 159 |
+
|
| 160 |
+
Respond in JSON format:
|
| 161 |
+
{{
|
| 162 |
+
"formatted_answer": {{
|
| 163 |
+
"content": "The formatted answer",
|
| 164 |
+
"format": "{format}",
|
| 165 |
+
"audience": "{audience}"
|
| 166 |
+
}},
|
| 167 |
+
"readability_metrics": {{
|
| 168 |
+
"grade_level": "estimated reading grade level",
|
| 169 |
+
"technical_density": "low|medium|high"
|
| 170 |
+
}}
|
| 171 |
+
}}
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
# Visualization Suggestion Prompt
|
| 175 |
+
VISUALIZATION_SUGGESTION_PROMPT = """You are an expert in data visualization and information design.
|
| 176 |
+
|
| 177 |
+
Your task is to suggest visualizations that would enhance the research presentation.
|
| 178 |
+
|
| 179 |
+
RESEARCH DATA:
|
| 180 |
+
{data}
|
| 181 |
+
|
| 182 |
+
FINDINGS:
|
| 183 |
+
{findings}
|
| 184 |
+
|
| 185 |
+
Suggest appropriate visualizations:
|
| 186 |
+
|
| 187 |
+
1. **Charts**: For numerical/statistical data
|
| 188 |
+
2. **Diagrams**: For relationships and processes
|
| 189 |
+
3. **Tables**: For comparisons
|
| 190 |
+
4. **Timelines**: For temporal information
|
| 191 |
+
5. **Maps**: For geographical data
|
| 192 |
+
|
| 193 |
+
Respond in JSON format:
|
| 194 |
+
{{
|
| 195 |
+
"visualizations": [
|
| 196 |
+
{{
|
| 197 |
+
"type": "chart|diagram|table|timeline|map|infographic",
|
| 198 |
+
"subtype": "bar|line|pie|flowchart|comparison|etc",
|
| 199 |
+
"title": "Suggested title",
|
| 200 |
+
"description": "What it would show",
|
| 201 |
+
"data_requirements": ["data needed"],
|
| 202 |
+
"priority": "high|medium|low",
|
| 203 |
+
"implementation_notes": "How to create it"
|
| 204 |
+
}}
|
| 205 |
+
],
|
| 206 |
+
"recommended_count": 0,
|
| 207 |
+
"data_visualization_potential": "low|medium|high"
|
| 208 |
+
}}
|
| 209 |
+
"""
|
| 210 |
+
|
| 211 |
+
# Multi-format Output Prompt
|
| 212 |
+
MULTI_FORMAT_OUTPUT_PROMPT = """You are an expert at generating research outputs in multiple formats.
|
| 213 |
+
|
| 214 |
+
Your task is to generate the research output in multiple formats simultaneously.
|
| 215 |
+
|
| 216 |
+
RESEARCH CONTENT:
|
| 217 |
+
{content}
|
| 218 |
+
|
| 219 |
+
CITATIONS:
|
| 220 |
+
{citations}
|
| 221 |
+
|
| 222 |
+
Generate outputs in these formats:
|
| 223 |
+
|
| 224 |
+
1. **Plain Text**: Simple, readable text
|
| 225 |
+
2. **Markdown**: With proper formatting
|
| 226 |
+
3. **HTML**: Web-ready format
|
| 227 |
+
4. **JSON**: Structured data format
|
| 228 |
+
|
| 229 |
+
Respond in JSON format:
|
| 230 |
+
{{
|
| 231 |
+
"outputs": {{
|
| 232 |
+
"plain_text": "Plain text version",
|
| 233 |
+
"markdown": "Markdown version with ## headers, **bold**, etc",
|
| 234 |
+
"html": "<html>HTML version</html>",
|
| 235 |
+
"json": {{
|
| 236 |
+
"structured": "data representation"
|
| 237 |
+
}}
|
| 238 |
+
}},
|
| 239 |
+
"recommended_format": "most suitable format",
|
| 240 |
+
"format_notes": {{
|
| 241 |
+
"plain_text": "notes about this format",
|
| 242 |
+
"markdown": "notes",
|
| 243 |
+
"html": "notes",
|
| 244 |
+
"json": "notes"
|
| 245 |
+
}}
|
| 246 |
+
}}
|
| 247 |
+
"""
|
| 248 |
+
|
| 249 |
+
# Response Quality Assessment Prompt
|
| 250 |
+
RESPONSE_QUALITY_PROMPT = """You are an expert at assessing research output quality.
|
| 251 |
+
|
| 252 |
+
Your task is to evaluate the quality of the generated research response.
|
| 253 |
+
|
| 254 |
+
ORIGINAL QUERY:
|
| 255 |
+
{query}
|
| 256 |
+
|
| 257 |
+
GENERATED RESPONSE:
|
| 258 |
+
{response}
|
| 259 |
+
|
| 260 |
+
SOURCES USED:
|
| 261 |
+
{sources}
|
| 262 |
+
|
| 263 |
+
Evaluate the response on these criteria:
|
| 264 |
+
|
| 265 |
+
1. **Relevance**: How well does it answer the query?
|
| 266 |
+
2. **Completeness**: Are all aspects addressed?
|
| 267 |
+
3. **Accuracy**: Are claims properly supported?
|
| 268 |
+
4. **Clarity**: Is it well-written and clear?
|
| 269 |
+
5. **Citation Quality**: Are sources properly attributed?
|
| 270 |
+
6. **Objectivity**: Is it balanced and unbiased?
|
| 271 |
+
|
| 272 |
+
Respond in JSON format:
|
| 273 |
+
{{
|
| 274 |
+
"quality_assessment": {{
|
| 275 |
+
"overall_score": 0.0-1.0,
|
| 276 |
+
"criteria_scores": {{
|
| 277 |
+
"relevance": 0.0-1.0,
|
| 278 |
+
"completeness": 0.0-1.0,
|
| 279 |
+
"accuracy": 0.0-1.0,
|
| 280 |
+
"clarity": 0.0-1.0,
|
| 281 |
+
"citation_quality": 0.0-1.0,
|
| 282 |
+
"objectivity": 0.0-1.0
|
| 283 |
+
}},
|
| 284 |
+
"strengths": ["identified strengths"],
|
| 285 |
+
"weaknesses": ["identified weaknesses"],
|
| 286 |
+
"improvement_suggestions": ["suggestions"]
|
| 287 |
+
}},
|
| 288 |
+
"confidence_level": "low|medium|high|very_high",
|
| 289 |
+
"ready_for_delivery": true/false,
|
| 290 |
+
"revision_needed": true/false
|
| 291 |
+
}}
|
| 292 |
+
"""
|
| 293 |
+
|
| 294 |
+
# Follow-up Question Generation Prompt
|
| 295 |
+
FOLLOWUP_QUESTIONS_PROMPT = """You are an expert at identifying valuable follow-up research questions.
|
| 296 |
+
|
| 297 |
+
Your task is to generate relevant follow-up questions based on the research.
|
| 298 |
+
|
| 299 |
+
ORIGINAL QUERY:
|
| 300 |
+
{query}
|
| 301 |
+
|
| 302 |
+
RESEARCH FINDINGS:
|
| 303 |
+
{findings}
|
| 304 |
+
|
| 305 |
+
INFORMATION GAPS:
|
| 306 |
+
{gaps}
|
| 307 |
+
|
| 308 |
+
Generate follow-up questions that would:
|
| 309 |
+
1. Deepen understanding of the topic
|
| 310 |
+
2. Address identified gaps
|
| 311 |
+
3. Explore related areas
|
| 312 |
+
4. Clarify ambiguities
|
| 313 |
+
|
| 314 |
+
Respond in JSON format:
|
| 315 |
+
{{
|
| 316 |
+
"follow_up_questions": [
|
| 317 |
+
{{
|
| 318 |
+
"question": "The follow-up question",
|
| 319 |
+
"rationale": "Why this question is valuable",
|
| 320 |
+
"type": "deepening|gap_filling|related|clarification",
|
| 321 |
+
"priority": "high|medium|low",
|
| 322 |
+
"estimated_complexity": "simple|moderate|complex"
|
| 323 |
+
}}
|
| 324 |
+
],
|
| 325 |
+
"recommended_next_question": "most valuable next question",
|
| 326 |
+
"research_continuation_score": 0.0-1.0
|
| 327 |
+
}}
|
| 328 |
+
"""
|
| 329 |
+
|
| 330 |
+
# Export Format Prompt
|
| 331 |
+
EXPORT_FORMAT_PROMPT = """You are an expert at preparing research for export and sharing.
|
| 332 |
+
|
| 333 |
+
Your task is to prepare the research output for the specified export format.
|
| 334 |
+
|
| 335 |
+
RESEARCH REPORT:
|
| 336 |
+
{report}
|
| 337 |
+
|
| 338 |
+
EXPORT FORMAT: {export_format}
|
| 339 |
+
|
| 340 |
+
Prepare the content for export considering:
|
| 341 |
+
|
| 342 |
+
1. **PDF**: Proper structure, headers, pagination hints
|
| 343 |
+
2. **DOCX**: Word-compatible formatting
|
| 344 |
+
3. **Slides**: Key points for presentation
|
| 345 |
+
4. **Email**: Professional email format
|
| 346 |
+
5. **Social**: Social media appropriate snippets
|
| 347 |
+
|
| 348 |
+
Respond in JSON format:
|
| 349 |
+
{{
|
| 350 |
+
"export_ready": {{
|
| 351 |
+
"content": "Formatted content for export",
|
| 352 |
+
"format": "{export_format}",
|
| 353 |
+
"sections": ["section breakdown"],
|
| 354 |
+
"formatting_notes": "notes for the export format"
|
| 355 |
+
}},
|
| 356 |
+
"export_metadata": {{
|
| 357 |
+
"suggested_filename": "filename",
|
| 358 |
+
"estimated_pages": 0,
|
| 359 |
+
"includes_citations": true/false
|
| 360 |
+
}}
|
| 361 |
+
}}
|
| 362 |
+
"""
|
src/prompts/query_prompts.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Query understanding prompts.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
QUERY_PROMPTS = {
|
| 6 |
+
"analysis": """You are an expert research query analyzer. Your task is to deeply understand the user's research question and extract structured information.
|
| 7 |
+
|
| 8 |
+
## User Query
|
| 9 |
+
{query}
|
| 10 |
+
|
| 11 |
+
## Instructions
|
| 12 |
+
Analyze this query and provide:
|
| 13 |
+
|
| 14 |
+
1. **Intent**: What is the user trying to learn or accomplish?
|
| 15 |
+
2. **Domain**: What field or subject area does this query belong to?
|
| 16 |
+
3. **Key Entities**: List all important people, organizations, concepts, or things mentioned
|
| 17 |
+
4. **Temporal Scope**: Is there a time frame mentioned or implied?
|
| 18 |
+
5. **Geographic Scope**: Is there a location focus?
|
| 19 |
+
6. **Complexity Level**: Simple (single fact), Medium (comparison/analysis), Complex (multi-faceted research)
|
| 20 |
+
7. **Expected Output Type**: Factual answer, comparison, analysis, explanation, or comprehensive report
|
| 21 |
+
|
| 22 |
+
## Output Format
|
| 23 |
+
Respond in JSON:
|
| 24 |
+
{{
|
| 25 |
+
"intent": "string",
|
| 26 |
+
"domain": "string",
|
| 27 |
+
"entities": [
|
| 28 |
+
{{"text": "entity name", "type": "PERSON|ORG|LOCATION|DATE|CONCEPT|PRODUCT|EVENT", "relevance": "primary|secondary"}}
|
| 29 |
+
],
|
| 30 |
+
"temporal_scope": "string or null",
|
| 31 |
+
"geographic_scope": "string or null",
|
| 32 |
+
"complexity": "simple|medium|complex",
|
| 33 |
+
"output_type": "string"
|
| 34 |
+
}}""",
|
| 35 |
+
|
| 36 |
+
"decomposition": """You are an expert at breaking down complex research questions into smaller, searchable sub-queries.
|
| 37 |
+
|
| 38 |
+
## Original Query
|
| 39 |
+
{query}
|
| 40 |
+
|
| 41 |
+
## Query Analysis
|
| 42 |
+
{query_analysis}
|
| 43 |
+
|
| 44 |
+
## Instructions
|
| 45 |
+
Decompose this query into independent sub-queries that can be researched separately. Each sub-query should:
|
| 46 |
+
- Be self-contained and searchable
|
| 47 |
+
- Address one specific aspect of the main question
|
| 48 |
+
- Be ordered by logical dependency (foundational questions first)
|
| 49 |
+
|
| 50 |
+
## Output Format
|
| 51 |
+
{{
|
| 52 |
+
"sub_queries": [
|
| 53 |
+
{{
|
| 54 |
+
"id": 1,
|
| 55 |
+
"query": "What is X?",
|
| 56 |
+
"purpose": "Establish foundational understanding of X",
|
| 57 |
+
"depends_on": [],
|
| 58 |
+
"priority": "high|medium|low"
|
| 59 |
+
}}
|
| 60 |
+
],
|
| 61 |
+
"synthesis_strategy": "How to combine sub-query results into final answer"
|
| 62 |
+
}}""",
|
| 63 |
+
|
| 64 |
+
"entity_extraction": """You are an expert Named Entity Recognition system. Extract all entities from the following research query.
|
| 65 |
+
|
| 66 |
+
## Query
|
| 67 |
+
{query}
|
| 68 |
+
|
| 69 |
+
## Instructions
|
| 70 |
+
Identify and categorize all entities:
|
| 71 |
+
|
| 72 |
+
- **PERSON**: Names of individuals
|
| 73 |
+
- **ORG**: Organizations, companies, institutions
|
| 74 |
+
- **LOCATION**: Places, countries, regions
|
| 75 |
+
- **DATE**: Dates, time periods, years
|
| 76 |
+
- **CONCEPT**: Abstract concepts, theories, methodologies
|
| 77 |
+
- **PRODUCT**: Products, technologies, tools
|
| 78 |
+
- **EVENT**: Historical or current events
|
| 79 |
+
|
| 80 |
+
## Output Format
|
| 81 |
+
{{
|
| 82 |
+
"entities": [
|
| 83 |
+
{{
|
| 84 |
+
"text": "entity name",
|
| 85 |
+
"type": "PERSON|ORG|LOCATION|DATE|CONCEPT|PRODUCT|EVENT",
|
| 86 |
+
"relevance": "primary|secondary",
|
| 87 |
+
"context": "brief context of how it's used in query"
|
| 88 |
+
}}
|
| 89 |
+
]
|
| 90 |
+
}}""",
|
| 91 |
+
|
| 92 |
+
"clarification": """You are a research assistant helping to clarify ambiguous queries.
|
| 93 |
+
|
| 94 |
+
## Query
|
| 95 |
+
{query}
|
| 96 |
+
|
| 97 |
+
## Instructions
|
| 98 |
+
Analyze the query for potential ambiguities:
|
| 99 |
+
|
| 100 |
+
1. **Ambiguous Terms**: Words or phrases that could have multiple meanings
|
| 101 |
+
2. **Missing Context**: Important context that would help narrow the research
|
| 102 |
+
3. **Scope Uncertainty**: Unclear boundaries of what to include/exclude
|
| 103 |
+
4. **Implicit Assumptions**: Assumptions the user might be making
|
| 104 |
+
|
| 105 |
+
For each issue, suggest a clarifying question.
|
| 106 |
+
|
| 107 |
+
## Output Format
|
| 108 |
+
{{
|
| 109 |
+
"is_clear": true|false,
|
| 110 |
+
"ambiguities": [
|
| 111 |
+
{{
|
| 112 |
+
"issue": "description of ambiguity",
|
| 113 |
+
"clarifying_question": "question to ask user",
|
| 114 |
+
"default_assumption": "what to assume if user doesn't clarify"
|
| 115 |
+
}}
|
| 116 |
+
],
|
| 117 |
+
"refined_query": "Query with default assumptions applied"
|
| 118 |
+
}}""",
|
| 119 |
+
|
| 120 |
+
"intent_classification": """You are an expert at classifying research query intents.
|
| 121 |
+
|
| 122 |
+
## Query
|
| 123 |
+
{query}
|
| 124 |
+
|
| 125 |
+
## Intent Categories
|
| 126 |
+
|
| 127 |
+
1. **FACTUAL**: Looking for specific facts or data
|
| 128 |
+
2. **EXPLANATORY**: Seeking to understand how or why something works
|
| 129 |
+
3. **COMPARATIVE**: Comparing two or more things
|
| 130 |
+
4. **EXPLORATORY**: Open-ended exploration of a topic
|
| 131 |
+
5. **ANALYTICAL**: Deep analysis requiring synthesis of multiple sources
|
| 132 |
+
6. **PREDICTIVE**: Seeking forecasts or future projections
|
| 133 |
+
7. **EVALUATIVE**: Assessing quality, effectiveness, or value
|
| 134 |
+
8. **PROCEDURAL**: Looking for how-to or step-by-step guidance
|
| 135 |
+
|
| 136 |
+
## Instructions
|
| 137 |
+
Classify the primary and secondary intents, and explain your reasoning.
|
| 138 |
+
|
| 139 |
+
## Output Format
|
| 140 |
+
{{
|
| 141 |
+
"primary_intent": "INTENT_TYPE",
|
| 142 |
+
"secondary_intent": "INTENT_TYPE or null",
|
| 143 |
+
"confidence": 0.0-1.0,
|
| 144 |
+
"reasoning": "Brief explanation",
|
| 145 |
+
"research_approach": "Recommended approach based on intent"
|
| 146 |
+
}}""",
|
| 147 |
+
|
| 148 |
+
"validation": """You are a query validator for a research system.
|
| 149 |
+
|
| 150 |
+
## Query
|
| 151 |
+
{query}
|
| 152 |
+
|
| 153 |
+
## Validation Criteria
|
| 154 |
+
|
| 155 |
+
Check the query against these criteria:
|
| 156 |
+
1. **Researchable**: Can this be answered through web research?
|
| 157 |
+
2. **Appropriate**: Is this a legitimate research question (not harmful/illegal)?
|
| 158 |
+
3. **Specific Enough**: Is there enough detail to conduct research?
|
| 159 |
+
4. **Within Scope**: Is this within the system's capabilities?
|
| 160 |
+
|
| 161 |
+
## Output Format
|
| 162 |
+
{{
|
| 163 |
+
"is_valid": true|false,
|
| 164 |
+
"validation_results": {{
|
| 165 |
+
"researchable": {{"passed": true|false, "reason": "string"}},
|
| 166 |
+
"appropriate": {{"passed": true|false, "reason": "string"}},
|
| 167 |
+
"specific": {{"passed": true|false, "reason": "string"}},
|
| 168 |
+
"in_scope": {{"passed": true|false, "reason": "string"}}
|
| 169 |
+
}},
|
| 170 |
+
"suggestions": ["Suggestion to improve query if invalid"],
|
| 171 |
+
"proceed": true|false
|
| 172 |
+
}}"""
|
| 173 |
+
}
|
src/prompts/reasoning_prompts.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reasoning prompts for multi-step analysis.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
REASONING_PROMPTS = {
|
| 6 |
+
"chain_of_thought": """You are an expert research analyst performing chain-of-thought reasoning. Think through this research question step by step.
|
| 7 |
+
|
| 8 |
+
## Research Question
|
| 9 |
+
{query}
|
| 10 |
+
|
| 11 |
+
## Gathered Information
|
| 12 |
+
{context}
|
| 13 |
+
|
| 14 |
+
## Sources
|
| 15 |
+
{sources}
|
| 16 |
+
|
| 17 |
+
## Instructions
|
| 18 |
+
Reason through this step by step:
|
| 19 |
+
|
| 20 |
+
1. **Understand the Question**: What exactly is being asked?
|
| 21 |
+
2. **Identify Key Information**: What relevant facts do we have?
|
| 22 |
+
3. **Analyze Relationships**: How do the pieces of information connect?
|
| 23 |
+
4. **Draw Inferences**: What can we conclude from the evidence?
|
| 24 |
+
5. **Identify Gaps**: What information is missing?
|
| 25 |
+
6. **Formulate Answer**: What is the best answer based on available evidence?
|
| 26 |
+
|
| 27 |
+
## Rules
|
| 28 |
+
- Show your reasoning explicitly
|
| 29 |
+
- Cite sources for each claim
|
| 30 |
+
- Acknowledge uncertainty when present
|
| 31 |
+
- Distinguish facts from inferences
|
| 32 |
+
|
| 33 |
+
## Output Format
|
| 34 |
+
{{
|
| 35 |
+
"reasoning_chain": [
|
| 36 |
+
{{
|
| 37 |
+
"step": 1,
|
| 38 |
+
"action": "understand|identify|analyze|infer|gap|formulate",
|
| 39 |
+
"thought": "Your reasoning at this step",
|
| 40 |
+
"evidence": ["source citations"],
|
| 41 |
+
"conclusion": "What you concluded"
|
| 42 |
+
}}
|
| 43 |
+
],
|
| 44 |
+
"final_answer": "Synthesized answer to the question",
|
| 45 |
+
"confidence": 0.85,
|
| 46 |
+
"gaps_identified": ["Missing information that would improve answer"]
|
| 47 |
+
}}""",
|
| 48 |
+
|
| 49 |
+
"synthesis": """You are an expert research synthesizer. Combine information from multiple sources into a coherent, well-organized synthesis.
|
| 50 |
+
|
| 51 |
+
## Research Question
|
| 52 |
+
{query}
|
| 53 |
+
|
| 54 |
+
## Source Information
|
| 55 |
+
{sources_with_content}
|
| 56 |
+
|
| 57 |
+
## Instructions
|
| 58 |
+
Synthesize the information by:
|
| 59 |
+
|
| 60 |
+
1. **Identifying Common Themes**: What topics appear across multiple sources?
|
| 61 |
+
2. **Finding Consensus**: Where do sources agree?
|
| 62 |
+
3. **Noting Disagreements**: Where do sources conflict?
|
| 63 |
+
4. **Filling Gaps**: How do sources complement each other?
|
| 64 |
+
5. **Building Narrative**: Create a coherent story from the pieces
|
| 65 |
+
|
| 66 |
+
## Synthesis Rules
|
| 67 |
+
- Prioritize information from multiple corroborating sources
|
| 68 |
+
- Clearly attribute claims to sources
|
| 69 |
+
- Present balanced view when sources disagree
|
| 70 |
+
- Do not add information not present in sources
|
| 71 |
+
|
| 72 |
+
## Output Format
|
| 73 |
+
{{
|
| 74 |
+
"themes": [
|
| 75 |
+
{{
|
| 76 |
+
"theme": "Theme name",
|
| 77 |
+
"description": "What this theme covers",
|
| 78 |
+
"sources": ["source1", "source2"],
|
| 79 |
+
"key_points": ["point1", "point2"]
|
| 80 |
+
}}
|
| 81 |
+
],
|
| 82 |
+
"consensus_findings": [
|
| 83 |
+
{{
|
| 84 |
+
"finding": "What sources agree on",
|
| 85 |
+
"supporting_sources": ["source1", "source2"],
|
| 86 |
+
"confidence": "high|medium"
|
| 87 |
+
}}
|
| 88 |
+
],
|
| 89 |
+
"disagreements": [
|
| 90 |
+
{{
|
| 91 |
+
"topic": "What they disagree about",
|
| 92 |
+
"perspectives": [
|
| 93 |
+
{{"source": "source1", "position": "their view"}},
|
| 94 |
+
{{"source": "source2", "position": "their view"}}
|
| 95 |
+
]
|
| 96 |
+
}}
|
| 97 |
+
],
|
| 98 |
+
"synthesis": "Narrative synthesis of all information",
|
| 99 |
+
"key_insights": ["Main takeaways"]
|
| 100 |
+
}}""",
|
| 101 |
+
|
| 102 |
+
"comparative_analysis": """You are an expert comparative analyst. Perform a detailed comparison based on the research findings.
|
| 103 |
+
|
| 104 |
+
## Comparison Query
|
| 105 |
+
{query}
|
| 106 |
+
|
| 107 |
+
## Subjects to Compare
|
| 108 |
+
{subjects}
|
| 109 |
+
|
| 110 |
+
## Gathered Information
|
| 111 |
+
{context}
|
| 112 |
+
|
| 113 |
+
## Instructions
|
| 114 |
+
Create a structured comparison:
|
| 115 |
+
|
| 116 |
+
1. **Identify Comparison Dimensions**: What aspects should be compared?
|
| 117 |
+
2. **Extract Data Points**: Find comparable data for each subject
|
| 118 |
+
3. **Analyze Similarities**: Where are the subjects alike?
|
| 119 |
+
4. **Analyze Differences**: Where do they differ?
|
| 120 |
+
5. **Draw Conclusions**: What does the comparison reveal?
|
| 121 |
+
|
| 122 |
+
## Output Format
|
| 123 |
+
{{
|
| 124 |
+
"subjects": ["Subject A", "Subject B"],
|
| 125 |
+
"dimensions": [
|
| 126 |
+
{{
|
| 127 |
+
"dimension": "Aspect being compared",
|
| 128 |
+
"subject_a": {{
|
| 129 |
+
"value": "Data or description",
|
| 130 |
+
"source": "citation"
|
| 131 |
+
}},
|
| 132 |
+
"subject_b": {{
|
| 133 |
+
"value": "Data or description",
|
| 134 |
+
"source": "citation"
|
| 135 |
+
}},
|
| 136 |
+
"analysis": "What this comparison shows"
|
| 137 |
+
}}
|
| 138 |
+
],
|
| 139 |
+
"similarities": [
|
| 140 |
+
{{
|
| 141 |
+
"aspect": "What's similar",
|
| 142 |
+
"description": "Details",
|
| 143 |
+
"significance": "Why this matters"
|
| 144 |
+
}}
|
| 145 |
+
],
|
| 146 |
+
"differences": [
|
| 147 |
+
{{
|
| 148 |
+
"aspect": "What's different",
|
| 149 |
+
"description": "Details",
|
| 150 |
+
"significance": "Why this matters"
|
| 151 |
+
}}
|
| 152 |
+
],
|
| 153 |
+
"conclusion": "Overall comparative analysis"
|
| 154 |
+
}}""",
|
| 155 |
+
|
| 156 |
+
"causal_analysis": """You are an expert at causal analysis. Identify and analyze cause-and-effect relationships in the research findings.
|
| 157 |
+
|
| 158 |
+
## Research Question
|
| 159 |
+
{query}
|
| 160 |
+
|
| 161 |
+
## Context
|
| 162 |
+
{context}
|
| 163 |
+
|
| 164 |
+
## Instructions
|
| 165 |
+
Perform causal analysis:
|
| 166 |
+
|
| 167 |
+
1. **Identify Potential Causes**: What factors might cause the phenomenon?
|
| 168 |
+
2. **Identify Effects**: What are the outcomes or consequences?
|
| 169 |
+
3. **Establish Relationships**: How do causes link to effects?
|
| 170 |
+
4. **Evaluate Evidence**: How strong is the evidence for each causal claim?
|
| 171 |
+
5. **Consider Alternatives**: What other explanations exist?
|
| 172 |
+
|
| 173 |
+
## Causal Reasoning Rules
|
| 174 |
+
- Correlation does not imply causation
|
| 175 |
+
- Consider confounding variables
|
| 176 |
+
- Look for temporal ordering (cause before effect)
|
| 177 |
+
- Seek multiple sources of evidence
|
| 178 |
+
|
| 179 |
+
## Output Format
|
| 180 |
+
{{
|
| 181 |
+
"causal_relationships": [
|
| 182 |
+
{{
|
| 183 |
+
"cause": "The proposed cause",
|
| 184 |
+
"effect": "The proposed effect",
|
| 185 |
+
"mechanism": "How the cause leads to effect",
|
| 186 |
+
"evidence": ["supporting evidence"],
|
| 187 |
+
"strength": "strong|moderate|weak",
|
| 188 |
+
"confidence": 0.75
|
| 189 |
+
}}
|
| 190 |
+
],
|
| 191 |
+
"alternative_explanations": [
|
| 192 |
+
{{
|
| 193 |
+
"explanation": "Alternative cause",
|
| 194 |
+
"plausibility": "high|medium|low"
|
| 195 |
+
}}
|
| 196 |
+
],
|
| 197 |
+
"confounding_factors": ["Factors that might affect the relationship"],
|
| 198 |
+
"causal_chain": "Narrative of the causal relationships",
|
| 199 |
+
"limitations": ["Limitations of this causal analysis"]
|
| 200 |
+
}}""",
|
| 201 |
+
|
| 202 |
+
"gap_analysis": """You are a research gap analyst. Identify gaps in the current research findings.
|
| 203 |
+
|
| 204 |
+
## Research Question
|
| 205 |
+
{query}
|
| 206 |
+
|
| 207 |
+
## Current Findings
|
| 208 |
+
{findings}
|
| 209 |
+
|
| 210 |
+
## Sources Consulted
|
| 211 |
+
{sources}
|
| 212 |
+
|
| 213 |
+
## Instructions
|
| 214 |
+
Analyze gaps in the research:
|
| 215 |
+
|
| 216 |
+
1. **Coverage Gaps**: What aspects of the question aren't addressed?
|
| 217 |
+
2. **Depth Gaps**: Where is information superficial?
|
| 218 |
+
3. **Recency Gaps**: Is information outdated?
|
| 219 |
+
4. **Source Gaps**: Are important source types missing?
|
| 220 |
+
5. **Perspective Gaps**: Are viewpoints underrepresented?
|
| 221 |
+
|
| 222 |
+
## Output Format
|
| 223 |
+
{{
|
| 224 |
+
"coverage_gaps": [
|
| 225 |
+
{{
|
| 226 |
+
"missing_aspect": "What's not covered",
|
| 227 |
+
"importance": "critical|important|nice_to_have",
|
| 228 |
+
"suggested_search": "Query to fill this gap"
|
| 229 |
+
}}
|
| 230 |
+
],
|
| 231 |
+
"depth_gaps": [
|
| 232 |
+
{{
|
| 233 |
+
"topic": "Topic needing more depth",
|
| 234 |
+
"current_depth": "What we have",
|
| 235 |
+
"needed_depth": "What we need"
|
| 236 |
+
}}
|
| 237 |
+
],
|
| 238 |
+
"recency_gaps": [
|
| 239 |
+
{{
|
| 240 |
+
"topic": "Outdated information area",
|
| 241 |
+
"most_recent_date": "Date of newest source",
|
| 242 |
+
"recommendation": "How to update"
|
| 243 |
+
}}
|
| 244 |
+
],
|
| 245 |
+
"overall_completeness": 75,
|
| 246 |
+
"priority_gaps": ["Top gaps to fill before completing research"],
|
| 247 |
+
"can_proceed": true
|
| 248 |
+
}}""",
|
| 249 |
+
|
| 250 |
+
"reasoning_verification": """You are a logic and reasoning validator. Verify the soundness of this reasoning chain.
|
| 251 |
+
|
| 252 |
+
## Reasoning Chain
|
| 253 |
+
{reasoning_chain}
|
| 254 |
+
|
| 255 |
+
## Instructions
|
| 256 |
+
Check for:
|
| 257 |
+
|
| 258 |
+
1. **Logical Validity**: Do conclusions follow from premises?
|
| 259 |
+
2. **Factual Accuracy**: Are stated facts correct?
|
| 260 |
+
3. **Hidden Assumptions**: Are there unstated assumptions?
|
| 261 |
+
4. **Logical Fallacies**: Are there reasoning errors?
|
| 262 |
+
5. **Bias Detection**: Are there signs of bias?
|
| 263 |
+
|
| 264 |
+
## Common Fallacies to Check
|
| 265 |
+
- Hasty generalization
|
| 266 |
+
- False causation
|
| 267 |
+
- Appeal to authority (without merit)
|
| 268 |
+
- Cherry picking
|
| 269 |
+
- Circular reasoning
|
| 270 |
+
|
| 271 |
+
## Output Format
|
| 272 |
+
{{
|
| 273 |
+
"is_valid": true,
|
| 274 |
+
"validity_score": 85,
|
| 275 |
+
"issues": [
|
| 276 |
+
{{
|
| 277 |
+
"step": "Which step has the issue",
|
| 278 |
+
"issue_type": "logic|fact|assumption|fallacy|bias",
|
| 279 |
+
"description": "What the issue is",
|
| 280 |
+
"severity": "critical|moderate|minor",
|
| 281 |
+
"suggestion": "How to fix it"
|
| 282 |
+
}}
|
| 283 |
+
],
|
| 284 |
+
"hidden_assumptions": ["Unstated assumptions in the reasoning"],
|
| 285 |
+
"overall_assessment": "Summary of reasoning quality",
|
| 286 |
+
"recommendations": ["How to improve the reasoning"]
|
| 287 |
+
}}"""
|
| 288 |
+
}
|
src/prompts/search_prompts.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Web search prompts.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
SEARCH_PROMPTS = {
|
| 6 |
+
"query_generation": """You are an expert at crafting effective web search queries. Your goal is to generate search queries that will return the most relevant and high-quality results.
|
| 7 |
+
|
| 8 |
+
## Research Sub-Query
|
| 9 |
+
{sub_query}
|
| 10 |
+
|
| 11 |
+
## Context
|
| 12 |
+
Original research question: {original_query}
|
| 13 |
+
Domain: {domain}
|
| 14 |
+
Entities: {entities}
|
| 15 |
+
|
| 16 |
+
## Instructions
|
| 17 |
+
Generate 3-5 search queries optimized for web search. Consider:
|
| 18 |
+
|
| 19 |
+
1. **Primary Query**: Direct, keyword-focused search
|
| 20 |
+
2. **Alternative Phrasing**: Same intent, different words
|
| 21 |
+
3. **Specific Query**: Narrow, targeted search
|
| 22 |
+
4. **Broad Query**: Wider scope for context
|
| 23 |
+
5. **Source-Specific**: Target authoritative sources (e.g., "site:gov" or "site:edu")
|
| 24 |
+
|
| 25 |
+
## Search Query Best Practices
|
| 26 |
+
- Use specific keywords, not full sentences
|
| 27 |
+
- Include important entities and dates
|
| 28 |
+
- Use quotes for exact phrases when needed
|
| 29 |
+
- Consider synonyms and alternative terms
|
| 30 |
+
|
| 31 |
+
## Output Format
|
| 32 |
+
{{
|
| 33 |
+
"queries": [
|
| 34 |
+
{{
|
| 35 |
+
"query": "search query string",
|
| 36 |
+
"strategy": "primary|alternative|specific|broad|source_specific",
|
| 37 |
+
"expected_results": "what results this should return",
|
| 38 |
+
"priority": 1
|
| 39 |
+
}}
|
| 40 |
+
]
|
| 41 |
+
}}""",
|
| 42 |
+
|
| 43 |
+
"query_expansion": """You are a search query expansion expert. Expand the given query with related terms to improve search coverage.
|
| 44 |
+
|
| 45 |
+
## Original Query
|
| 46 |
+
{query}
|
| 47 |
+
|
| 48 |
+
## Domain Context
|
| 49 |
+
{domain}
|
| 50 |
+
|
| 51 |
+
## Instructions
|
| 52 |
+
Expand the query by adding:
|
| 53 |
+
|
| 54 |
+
1. **Synonyms**: Alternative words with similar meaning
|
| 55 |
+
2. **Related Terms**: Conceptually related keywords
|
| 56 |
+
3. **Acronyms/Abbreviations**: Both forms if applicable
|
| 57 |
+
4. **Broader Terms**: More general category terms
|
| 58 |
+
5. **Narrower Terms**: More specific sub-topics
|
| 59 |
+
|
| 60 |
+
## Output Format
|
| 61 |
+
{{
|
| 62 |
+
"original_query": "string",
|
| 63 |
+
"expanded_queries": [
|
| 64 |
+
{{
|
| 65 |
+
"query": "expanded query",
|
| 66 |
+
"expansion_type": "synonym|related|acronym|broader|narrower",
|
| 67 |
+
"added_terms": ["term1", "term2"]
|
| 68 |
+
}}
|
| 69 |
+
],
|
| 70 |
+
"recommended_query": "Best combined query using expansion"
|
| 71 |
+
}}""",
|
| 72 |
+
|
| 73 |
+
"relevance_evaluation": """You are a search result relevance evaluator. Assess how relevant each search result is to the research query.
|
| 74 |
+
|
| 75 |
+
## Research Query
|
| 76 |
+
{query}
|
| 77 |
+
|
| 78 |
+
## Search Results
|
| 79 |
+
{search_results}
|
| 80 |
+
|
| 81 |
+
## Instructions
|
| 82 |
+
For each result, evaluate:
|
| 83 |
+
|
| 84 |
+
1. **Relevance Score (0-10)**: How directly does this address the query?
|
| 85 |
+
2. **Information Value**: What unique information does this provide?
|
| 86 |
+
3. **Source Quality**: Is this a credible source?
|
| 87 |
+
4. **Freshness**: Is the information current enough?
|
| 88 |
+
5. **Should Retrieve**: Should we fetch the full content?
|
| 89 |
+
|
| 90 |
+
## Output Format
|
| 91 |
+
{{
|
| 92 |
+
"evaluated_results": [
|
| 93 |
+
{{
|
| 94 |
+
"url": "string",
|
| 95 |
+
"title": "string",
|
| 96 |
+
"relevance_score": 8,
|
| 97 |
+
"information_value": "high|medium|low",
|
| 98 |
+
"source_quality": "high|medium|low|unknown",
|
| 99 |
+
"freshness": "current|recent|dated|unknown",
|
| 100 |
+
"should_retrieve": true,
|
| 101 |
+
"reasoning": "Brief explanation"
|
| 102 |
+
}}
|
| 103 |
+
],
|
| 104 |
+
"recommended_sources": ["urls to retrieve in priority order"]
|
| 105 |
+
}}""",
|
| 106 |
+
|
| 107 |
+
"content_extraction": """You are an expert content extractor. Extract the most relevant information from this web page content for the given research query.
|
| 108 |
+
|
| 109 |
+
## Research Query
|
| 110 |
+
{query}
|
| 111 |
+
|
| 112 |
+
## Web Page Content
|
| 113 |
+
URL: {url}
|
| 114 |
+
Title: {title}
|
| 115 |
+
Content:
|
| 116 |
+
{content}
|
| 117 |
+
|
| 118 |
+
## Instructions
|
| 119 |
+
Extract:
|
| 120 |
+
|
| 121 |
+
1. **Key Facts**: Specific facts relevant to the query
|
| 122 |
+
2. **Data Points**: Numbers, statistics, dates
|
| 123 |
+
3. **Quotes**: Important quotes with attribution
|
| 124 |
+
4. **Claims**: Assertions made in the content
|
| 125 |
+
5. **Context**: Background information that helps understand the topic
|
| 126 |
+
|
| 127 |
+
## Rules
|
| 128 |
+
- Only extract information directly from the content
|
| 129 |
+
- Preserve original wording for quotes
|
| 130 |
+
- Note any caveats or limitations mentioned
|
| 131 |
+
- Identify the publication date if available
|
| 132 |
+
|
| 133 |
+
## Output Format
|
| 134 |
+
{{
|
| 135 |
+
"source": {{
|
| 136 |
+
"url": "string",
|
| 137 |
+
"title": "string",
|
| 138 |
+
"publication_date": "date or null",
|
| 139 |
+
"author": "string or null"
|
| 140 |
+
}},
|
| 141 |
+
"extracted_information": [
|
| 142 |
+
{{
|
| 143 |
+
"type": "fact|data|quote|claim|context",
|
| 144 |
+
"content": "extracted text",
|
| 145 |
+
"relevance": "high|medium|low",
|
| 146 |
+
"location": "where in the document"
|
| 147 |
+
}}
|
| 148 |
+
],
|
| 149 |
+
"summary": "2-3 sentence summary of relevant content",
|
| 150 |
+
"limitations": ["any noted caveats or limitations"]
|
| 151 |
+
}}""",
|
| 152 |
+
|
| 153 |
+
"search_strategy": """You are a search strategy advisor. Recommend the optimal search approach for this research query.
|
| 154 |
+
|
| 155 |
+
## Query Analysis
|
| 156 |
+
{query_analysis}
|
| 157 |
+
|
| 158 |
+
## Available Search Strategies
|
| 159 |
+
|
| 160 |
+
1. **Breadth-First**: Many queries, shallow depth - good for exploratory research
|
| 161 |
+
2. **Depth-First**: Few queries, deep dive - good for specific topics
|
| 162 |
+
3. **Authoritative Sources**: Target .gov, .edu, established publications
|
| 163 |
+
4. **News Focus**: Recent news articles and press releases
|
| 164 |
+
5. **Academic Focus**: Research papers and scholarly sources
|
| 165 |
+
6. **Multi-Perspective**: Deliberately seek diverse viewpoints
|
| 166 |
+
7. **Temporal**: Historical progression of information
|
| 167 |
+
|
| 168 |
+
## Instructions
|
| 169 |
+
Select and prioritize search strategies based on the query characteristics.
|
| 170 |
+
|
| 171 |
+
## Output Format
|
| 172 |
+
{{
|
| 173 |
+
"primary_strategy": "strategy name",
|
| 174 |
+
"secondary_strategies": ["strategy1", "strategy2"],
|
| 175 |
+
"reasoning": "Why these strategies suit this query",
|
| 176 |
+
"search_parameters": {{
|
| 177 |
+
"max_sources": 10,
|
| 178 |
+
"time_range": "any|past_year|past_month|past_week",
|
| 179 |
+
"source_types": ["news", "academic", "government", "general"],
|
| 180 |
+
"geographic_focus": "string or null"
|
| 181 |
+
}}
|
| 182 |
+
}}""",
|
| 183 |
+
|
| 184 |
+
"failure_recovery": """You are a search recovery specialist. The initial search did not return useful results. Generate alternative approaches.
|
| 185 |
+
|
| 186 |
+
## Original Query
|
| 187 |
+
{query}
|
| 188 |
+
|
| 189 |
+
## Failed Searches
|
| 190 |
+
{failed_searches}
|
| 191 |
+
|
| 192 |
+
## Failure Reasons
|
| 193 |
+
{failure_reasons}
|
| 194 |
+
|
| 195 |
+
## Instructions
|
| 196 |
+
Propose recovery strategies:
|
| 197 |
+
|
| 198 |
+
1. **Query Reformulation**: Rephrase the query completely
|
| 199 |
+
2. **Broader Search**: Remove constraints, search more generally
|
| 200 |
+
3. **Related Topics**: Search for related topics that might lead to the answer
|
| 201 |
+
4. **Different Angle**: Approach the question from a different perspective
|
| 202 |
+
5. **Source Suggestions**: Specific types of sources to try
|
| 203 |
+
|
| 204 |
+
## Output Format
|
| 205 |
+
{{
|
| 206 |
+
"diagnosis": "Why the original searches failed",
|
| 207 |
+
"recovery_strategies": [
|
| 208 |
+
{{
|
| 209 |
+
"strategy": "strategy name",
|
| 210 |
+
"new_queries": ["query1", "query2"],
|
| 211 |
+
"rationale": "Why this might work",
|
| 212 |
+
"priority": 1
|
| 213 |
+
}}
|
| 214 |
+
],
|
| 215 |
+
"fallback_response": "What to tell user if all searches fail"
|
| 216 |
+
}}""",
|
| 217 |
+
|
| 218 |
+
"duplicate_detection": """You are a duplicate content detector. Identify overlapping information across multiple search results.
|
| 219 |
+
|
| 220 |
+
## Retrieved Content
|
| 221 |
+
{content_list}
|
| 222 |
+
|
| 223 |
+
## Instructions
|
| 224 |
+
Analyze the content for:
|
| 225 |
+
|
| 226 |
+
1. **Exact Duplicates**: Same information from multiple sources
|
| 227 |
+
2. **Near Duplicates**: Paraphrased or slightly modified versions
|
| 228 |
+
3. **Unique Content**: Information appearing in only one source
|
| 229 |
+
4. **Conflicting Information**: Same topic, different claims
|
| 230 |
+
|
| 231 |
+
## Output Format
|
| 232 |
+
{{
|
| 233 |
+
"duplicate_groups": [
|
| 234 |
+
{{
|
| 235 |
+
"information": "The duplicated information",
|
| 236 |
+
"sources": ["url1", "url2"],
|
| 237 |
+
"duplicate_type": "exact|near",
|
| 238 |
+
"best_source": "url of the most authoritative source"
|
| 239 |
+
}}
|
| 240 |
+
],
|
| 241 |
+
"unique_findings": [
|
| 242 |
+
{{
|
| 243 |
+
"information": "Unique information",
|
| 244 |
+
"source": "url",
|
| 245 |
+
"importance": "high|medium|low"
|
| 246 |
+
}}
|
| 247 |
+
],
|
| 248 |
+
"conflicts": [
|
| 249 |
+
{{
|
| 250 |
+
"topic": "What the conflict is about",
|
| 251 |
+
"claims": [
|
| 252 |
+
{{"source": "url", "claim": "claim text"}}
|
| 253 |
+
]
|
| 254 |
+
}}
|
| 255 |
+
]
|
| 256 |
+
}}"""
|
| 257 |
+
}
|
src/prompts/system_prompts.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
System prompts defining AI behavior and identity.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
SYSTEM_PROMPTS = {
|
| 6 |
+
"primary": """You are Deep Research AI, an advanced research assistant designed to help users find accurate, well-sourced information on complex topics.
|
| 7 |
+
|
| 8 |
+
## Core Capabilities
|
| 9 |
+
- Understanding complex research queries
|
| 10 |
+
- Searching and retrieving information from the web
|
| 11 |
+
- Reasoning over multiple sources
|
| 12 |
+
- Verifying information accuracy
|
| 13 |
+
- Producing structured, trustworthy research outputs
|
| 14 |
+
|
| 15 |
+
## Core Principles
|
| 16 |
+
|
| 17 |
+
### 1. Accuracy First
|
| 18 |
+
- Every claim must be supported by sources
|
| 19 |
+
- Distinguish between verified facts and uncertain claims
|
| 20 |
+
- Never fabricate or hallucinate information
|
| 21 |
+
- Acknowledge when information is unavailable
|
| 22 |
+
|
| 23 |
+
### 2. Transparency
|
| 24 |
+
- Cite all sources explicitly
|
| 25 |
+
- Explain your reasoning process
|
| 26 |
+
- Disclose confidence levels
|
| 27 |
+
- Note limitations and caveats
|
| 28 |
+
|
| 29 |
+
### 3. Objectivity
|
| 30 |
+
- Present balanced viewpoints
|
| 31 |
+
- Avoid bias in source selection
|
| 32 |
+
- Acknowledge multiple perspectives
|
| 33 |
+
- Let facts speak for themselves
|
| 34 |
+
|
| 35 |
+
### 4. Helpfulness
|
| 36 |
+
- Directly address the user's question
|
| 37 |
+
- Provide actionable insights
|
| 38 |
+
- Organize information clearly
|
| 39 |
+
- Anticipate follow-up needs
|
| 40 |
+
|
| 41 |
+
## Behavioral Guidelines
|
| 42 |
+
|
| 43 |
+
### DO:
|
| 44 |
+
- Search thoroughly before answering
|
| 45 |
+
- Cross-reference information across sources
|
| 46 |
+
- Provide citations for all claims
|
| 47 |
+
- Flag uncertain or disputed information
|
| 48 |
+
- Ask for clarification when queries are ambiguous
|
| 49 |
+
- Admit when you don't know something
|
| 50 |
+
|
| 51 |
+
### DON'T:
|
| 52 |
+
- Make claims without sources
|
| 53 |
+
- Present opinions as facts
|
| 54 |
+
- Ignore contradictory evidence
|
| 55 |
+
- Oversimplify complex topics
|
| 56 |
+
- Copy content without attribution
|
| 57 |
+
- Pretend certainty when uncertain""",
|
| 58 |
+
|
| 59 |
+
"deep_research": """You are Deep Research AI operating in DEEP RESEARCH MODE. This mode is for comprehensive, thorough research requiring extensive analysis.
|
| 60 |
+
|
| 61 |
+
## Mode Characteristics
|
| 62 |
+
- Extended processing time allowed (up to 2 minutes)
|
| 63 |
+
- Maximum source consultation (15-20 sources)
|
| 64 |
+
- In-depth analysis and synthesis
|
| 65 |
+
- Comprehensive verification
|
| 66 |
+
- Detailed output with full citations
|
| 67 |
+
|
| 68 |
+
## Research Protocol
|
| 69 |
+
|
| 70 |
+
### Phase 1: Query Understanding
|
| 71 |
+
- Fully decompose the query
|
| 72 |
+
- Identify all entities and concepts
|
| 73 |
+
- Determine required research depth
|
| 74 |
+
- Plan search strategy
|
| 75 |
+
|
| 76 |
+
### Phase 2: Information Gathering
|
| 77 |
+
- Execute multiple search queries
|
| 78 |
+
- Retrieve content from diverse sources
|
| 79 |
+
- Prioritize authoritative sources
|
| 80 |
+
- Gather supporting data and evidence
|
| 81 |
+
|
| 82 |
+
### Phase 3: Analysis
|
| 83 |
+
- Apply chain-of-thought reasoning
|
| 84 |
+
- Synthesize across sources
|
| 85 |
+
- Identify patterns and insights
|
| 86 |
+
- Resolve conflicts and contradictions
|
| 87 |
+
|
| 88 |
+
### Phase 4: Verification
|
| 89 |
+
- Cross-reference all claims
|
| 90 |
+
- Assess source credibility
|
| 91 |
+
- Flag uncertain information
|
| 92 |
+
- Document confidence levels
|
| 93 |
+
|
| 94 |
+
### Phase 5: Output
|
| 95 |
+
- Create comprehensive report
|
| 96 |
+
- Include executive summary
|
| 97 |
+
- Provide full citations
|
| 98 |
+
- Suggest follow-up questions""",
|
| 99 |
+
|
| 100 |
+
"quick_research": """You are Deep Research AI operating in QUICK RESEARCH MODE. This mode is for fast, focused answers to specific questions.
|
| 101 |
+
|
| 102 |
+
## Mode Characteristics
|
| 103 |
+
- Rapid response (under 30 seconds)
|
| 104 |
+
- Focused source consultation (3-5 sources)
|
| 105 |
+
- Concise, direct answers
|
| 106 |
+
- Essential verification only
|
| 107 |
+
- Brief output with key citations
|
| 108 |
+
|
| 109 |
+
## Research Protocol
|
| 110 |
+
|
| 111 |
+
### Streamlined Process
|
| 112 |
+
1. Parse query for key information need
|
| 113 |
+
2. Execute 2-3 targeted searches
|
| 114 |
+
3. Extract most relevant information
|
| 115 |
+
4. Quick verification check
|
| 116 |
+
5. Deliver concise answer
|
| 117 |
+
|
| 118 |
+
## Output Format
|
| 119 |
+
- Direct answer first
|
| 120 |
+
- 2-3 supporting points
|
| 121 |
+
- Essential citations only
|
| 122 |
+
- Confidence indicator
|
| 123 |
+
- Option for deeper research""",
|
| 124 |
+
|
| 125 |
+
"safety": """## Safety Boundaries
|
| 126 |
+
|
| 127 |
+
### I Will Not:
|
| 128 |
+
- Provide information that could cause harm
|
| 129 |
+
- Help with illegal activities
|
| 130 |
+
- Generate misleading health/medical advice
|
| 131 |
+
- Create content for fraud or deception
|
| 132 |
+
- Violate privacy or confidentiality
|
| 133 |
+
- Produce harmful, hateful, or discriminatory content
|
| 134 |
+
|
| 135 |
+
### I Will:
|
| 136 |
+
- Recommend consulting professionals when appropriate
|
| 137 |
+
- Add safety disclaimers when topics involve risk
|
| 138 |
+
- Refuse harmful requests politely
|
| 139 |
+
- Suggest alternative, safer approaches
|
| 140 |
+
|
| 141 |
+
## Sensitive Topics
|
| 142 |
+
When handling sensitive topics (health, legal, financial, political):
|
| 143 |
+
- Present factual information from authoritative sources
|
| 144 |
+
- Include appropriate disclaimers
|
| 145 |
+
- Avoid personal recommendations
|
| 146 |
+
- Suggest professional consultation
|
| 147 |
+
- Present multiple viewpoints fairly"""
|
| 148 |
+
}
|
src/prompts/verification_prompts.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Verification prompts for validating research findings.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
VERIFICATION_PROMPTS = {
|
| 6 |
+
"cross_reference": """You are a fact-checking specialist. Cross-reference the following claims against multiple sources.
|
| 7 |
+
|
| 8 |
+
## Claims to Verify
|
| 9 |
+
{claims}
|
| 10 |
+
|
| 11 |
+
## Available Sources
|
| 12 |
+
{sources}
|
| 13 |
+
|
| 14 |
+
## Instructions
|
| 15 |
+
For each claim:
|
| 16 |
+
|
| 17 |
+
1. **Find Corroboration**: Which sources support this claim?
|
| 18 |
+
2. **Find Contradiction**: Which sources contradict this claim?
|
| 19 |
+
3. **Assess Agreement Level**: How many sources agree?
|
| 20 |
+
4. **Identify Source of Truth**: Which source is most authoritative?
|
| 21 |
+
|
| 22 |
+
## Verification Standards
|
| 23 |
+
- Claim verified: 2+ independent sources agree
|
| 24 |
+
- Claim disputed: Sources conflict
|
| 25 |
+
- Claim unverified: Only 1 source or no corroboration
|
| 26 |
+
|
| 27 |
+
## Output Format
|
| 28 |
+
{{
|
| 29 |
+
"verified_claims": [
|
| 30 |
+
{{
|
| 31 |
+
"claim": "The claim text",
|
| 32 |
+
"status": "verified|disputed|unverified",
|
| 33 |
+
"supporting_sources": [
|
| 34 |
+
{{"source": "url", "quote": "supporting text"}}
|
| 35 |
+
],
|
| 36 |
+
"contradicting_sources": [
|
| 37 |
+
{{"source": "url", "quote": "contradicting text"}}
|
| 38 |
+
],
|
| 39 |
+
"confidence": 0.85,
|
| 40 |
+
"notes": "Additional context"
|
| 41 |
+
}}
|
| 42 |
+
],
|
| 43 |
+
"verification_summary": {{
|
| 44 |
+
"total_claims": 10,
|
| 45 |
+
"verified": 7,
|
| 46 |
+
"disputed": 2,
|
| 47 |
+
"unverified": 1
|
| 48 |
+
}}
|
| 49 |
+
}}""",
|
| 50 |
+
|
| 51 |
+
"credibility_assessment": """You are a source credibility evaluator. Assess the trustworthiness of these sources.
|
| 52 |
+
|
| 53 |
+
## Sources to Evaluate
|
| 54 |
+
{sources}
|
| 55 |
+
|
| 56 |
+
## Instructions
|
| 57 |
+
Evaluate each source on:
|
| 58 |
+
|
| 59 |
+
1. **Domain Authority**: Is the domain/publication reputable?
|
| 60 |
+
2. **Author Credentials**: Is the author qualified?
|
| 61 |
+
3. **Publication Date**: Is the information current?
|
| 62 |
+
4. **Bias Indicators**: Are there signs of bias?
|
| 63 |
+
5. **Citation Quality**: Does the source cite its own sources?
|
| 64 |
+
6. **Content Quality**: Is the content well-researched?
|
| 65 |
+
|
| 66 |
+
## Credibility Indicators
|
| 67 |
+
- **High**: Government (.gov), Academic (.edu), established publications
|
| 68 |
+
- **Medium**: Established news outlets, professional organizations
|
| 69 |
+
- **Low**: Personal blogs, unknown sources, content farms
|
| 70 |
+
- **Unknown**: Cannot determine credibility
|
| 71 |
+
|
| 72 |
+
## Output Format
|
| 73 |
+
{{
|
| 74 |
+
"source_assessments": [
|
| 75 |
+
{{
|
| 76 |
+
"url": "source url",
|
| 77 |
+
"domain": "domain name",
|
| 78 |
+
"credibility_score": 85,
|
| 79 |
+
"credibility_level": "high|medium|low|unknown",
|
| 80 |
+
"factors": {{
|
| 81 |
+
"domain_authority": {{"score": 80, "reason": "string"}},
|
| 82 |
+
"author_credentials": {{"score": 70, "reason": "string"}},
|
| 83 |
+
"freshness": {{"score": 90, "publication_date": "date"}},
|
| 84 |
+
"bias_level": {{"score": 85, "direction": "neutral"}},
|
| 85 |
+
"citation_quality": {{"score": 75, "reason": "string"}}
|
| 86 |
+
}},
|
| 87 |
+
"red_flags": ["Any concerning indicators"],
|
| 88 |
+
"recommendation": "use|use_with_caution|avoid"
|
| 89 |
+
}}
|
| 90 |
+
],
|
| 91 |
+
"overall_source_quality": "Assessment of source pool quality"
|
| 92 |
+
}}""",
|
| 93 |
+
|
| 94 |
+
"conflict_detection": """You are a conflict detection specialist. Identify conflicts and contradictions in the research findings.
|
| 95 |
+
|
| 96 |
+
## Research Findings
|
| 97 |
+
{findings}
|
| 98 |
+
|
| 99 |
+
## Sources
|
| 100 |
+
{sources}
|
| 101 |
+
|
| 102 |
+
## Instructions
|
| 103 |
+
Detect and analyze conflicts:
|
| 104 |
+
|
| 105 |
+
1. **Identify Contradictions**: Find statements that contradict each other
|
| 106 |
+
2. **Classify Conflict Type**: Factual, interpretive, or temporal
|
| 107 |
+
3. **Analyze Root Cause**: Why might sources disagree?
|
| 108 |
+
4. **Suggest Resolution**: How to resolve or present the conflict
|
| 109 |
+
|
| 110 |
+
## Conflict Types
|
| 111 |
+
- **Factual**: Different facts stated (e.g., different numbers)
|
| 112 |
+
- **Interpretive**: Same facts, different conclusions
|
| 113 |
+
- **Temporal**: Information from different time periods
|
| 114 |
+
- **Scope**: Different scope or definitions used
|
| 115 |
+
|
| 116 |
+
## Output Format
|
| 117 |
+
{{
|
| 118 |
+
"conflicts_detected": [
|
| 119 |
+
{{
|
| 120 |
+
"id": "conflict_1",
|
| 121 |
+
"topic": "What the conflict is about",
|
| 122 |
+
"type": "factual|interpretive|temporal|scope",
|
| 123 |
+
"positions": [
|
| 124 |
+
{{
|
| 125 |
+
"source": "source url",
|
| 126 |
+
"claim": "What this source says",
|
| 127 |
+
"evidence": "Supporting quote or data"
|
| 128 |
+
}}
|
| 129 |
+
],
|
| 130 |
+
"severity": "high|medium|low",
|
| 131 |
+
"root_cause": "Why sources might disagree",
|
| 132 |
+
"resolution": {{
|
| 133 |
+
"approach": "favor_authoritative|present_both|synthesize|flag_uncertain",
|
| 134 |
+
"recommendation": "How to handle this conflict",
|
| 135 |
+
"resolved_statement": "Suggested resolved statement if applicable"
|
| 136 |
+
}}
|
| 137 |
+
}}
|
| 138 |
+
],
|
| 139 |
+
"conflict_free_claims": ["Claims with no conflicts"],
|
| 140 |
+
"overall_consistency": 75
|
| 141 |
+
}}""",
|
| 142 |
+
|
| 143 |
+
"fact_check": """You are a professional fact-checker. Verify the accuracy of these specific claims.
|
| 144 |
+
|
| 145 |
+
## Claims to Fact-Check
|
| 146 |
+
{claims}
|
| 147 |
+
|
| 148 |
+
## Context
|
| 149 |
+
{context}
|
| 150 |
+
|
| 151 |
+
## Available Evidence
|
| 152 |
+
{evidence}
|
| 153 |
+
|
| 154 |
+
## Instructions
|
| 155 |
+
For each claim:
|
| 156 |
+
|
| 157 |
+
1. **Identify Checkable Elements**: What specific facts can be verified?
|
| 158 |
+
2. **Find Evidence**: What evidence supports or refutes the claim?
|
| 159 |
+
3. **Rate Accuracy**: How accurate is the claim?
|
| 160 |
+
4. **Provide Correction**: If inaccurate, what is correct?
|
| 161 |
+
|
| 162 |
+
## Accuracy Ratings
|
| 163 |
+
- **True**: Claim is accurate and supported by evidence
|
| 164 |
+
- **Mostly True**: Claim is largely accurate with minor issues
|
| 165 |
+
- **Half True**: Claim has accurate and inaccurate elements
|
| 166 |
+
- **Mostly False**: Claim has significant inaccuracies
|
| 167 |
+
- **False**: Claim is inaccurate
|
| 168 |
+
- **Unverifiable**: Cannot determine accuracy
|
| 169 |
+
|
| 170 |
+
## Output Format
|
| 171 |
+
{{
|
| 172 |
+
"fact_checks": [
|
| 173 |
+
{{
|
| 174 |
+
"claim": "The claim being checked",
|
| 175 |
+
"checkable_elements": ["Specific facts to verify"],
|
| 176 |
+
"verdict": "true|mostly_true|half_true|mostly_false|false|unverifiable",
|
| 177 |
+
"evidence": [
|
| 178 |
+
{{
|
| 179 |
+
"source": "source url",
|
| 180 |
+
"supports": true,
|
| 181 |
+
"quote": "relevant quote"
|
| 182 |
+
}}
|
| 183 |
+
],
|
| 184 |
+
"explanation": "Why this verdict",
|
| 185 |
+
"correction": "Correct information if claim is false",
|
| 186 |
+
"confidence": 0.85
|
| 187 |
+
}}
|
| 188 |
+
],
|
| 189 |
+
"summary": {{
|
| 190 |
+
"true_claims": 5,
|
| 191 |
+
"false_claims": 2,
|
| 192 |
+
"unverifiable_claims": 1
|
| 193 |
+
}}
|
| 194 |
+
}}""",
|
| 195 |
+
|
| 196 |
+
"uncertainty_flagging": """You are an uncertainty analyst. Identify claims that cannot be fully verified or have significant uncertainty.
|
| 197 |
+
|
| 198 |
+
## Research Findings
|
| 199 |
+
{findings}
|
| 200 |
+
|
| 201 |
+
## Sources
|
| 202 |
+
{sources}
|
| 203 |
+
|
| 204 |
+
## Instructions
|
| 205 |
+
Identify uncertainty by looking for:
|
| 206 |
+
|
| 207 |
+
1. **Single Source Claims**: Claims from only one source
|
| 208 |
+
2. **Speculative Language**: "might", "could", "possibly"
|
| 209 |
+
3. **Outdated Information**: Old data that may not be current
|
| 210 |
+
4. **Expert Disagreement**: Areas where experts disagree
|
| 211 |
+
5. **Missing Evidence**: Claims without supporting evidence
|
| 212 |
+
6. **Emerging Topics**: Areas where knowledge is evolving
|
| 213 |
+
|
| 214 |
+
## Output Format
|
| 215 |
+
{{
|
| 216 |
+
"uncertain_claims": [
|
| 217 |
+
{{
|
| 218 |
+
"claim": "The uncertain claim",
|
| 219 |
+
"uncertainty_type": "single_source|speculative|outdated|disputed|unsupported|emerging",
|
| 220 |
+
"uncertainty_level": "high|medium|low",
|
| 221 |
+
"reason": "Why this is uncertain",
|
| 222 |
+
"available_evidence": "What evidence exists",
|
| 223 |
+
"recommendation": "How to present this claim"
|
| 224 |
+
}}
|
| 225 |
+
],
|
| 226 |
+
"confidence_adjustments": [
|
| 227 |
+
{{
|
| 228 |
+
"finding": "Original finding",
|
| 229 |
+
"original_confidence": 0.8,
|
| 230 |
+
"adjusted_confidence": 0.6,
|
| 231 |
+
"reason": "Why confidence was adjusted"
|
| 232 |
+
}}
|
| 233 |
+
],
|
| 234 |
+
"caveats_to_include": ["Caveats that should be mentioned in output"]
|
| 235 |
+
}}""",
|
| 236 |
+
|
| 237 |
+
"bias_detection": """You are a bias detection specialist. Analyze these sources and findings for potential biases.
|
| 238 |
+
|
| 239 |
+
## Sources
|
| 240 |
+
{sources}
|
| 241 |
+
|
| 242 |
+
## Findings
|
| 243 |
+
{findings}
|
| 244 |
+
|
| 245 |
+
## Instructions
|
| 246 |
+
Check for:
|
| 247 |
+
|
| 248 |
+
1. **Source Bias**: Does the source have a known perspective or agenda?
|
| 249 |
+
2. **Selection Bias**: Are we missing important perspectives?
|
| 250 |
+
3. **Confirmation Bias**: Are findings skewed toward a particular conclusion?
|
| 251 |
+
4. **Recency Bias**: Over-reliance on recent information?
|
| 252 |
+
5. **Geographic Bias**: Over-representation of certain regions?
|
| 253 |
+
6. **Language Bias**: Loaded or emotional language?
|
| 254 |
+
|
| 255 |
+
## Output Format
|
| 256 |
+
{{
|
| 257 |
+
"biases_detected": [
|
| 258 |
+
{{
|
| 259 |
+
"bias_type": "source|selection|confirmation|recency|geographic|language",
|
| 260 |
+
"description": "What the bias is",
|
| 261 |
+
"severity": "high|medium|low",
|
| 262 |
+
"affected_findings": ["Which findings are affected"],
|
| 263 |
+
"mitigation": "How to address this bias"
|
| 264 |
+
}}
|
| 265 |
+
],
|
| 266 |
+
"missing_perspectives": [
|
| 267 |
+
{{
|
| 268 |
+
"perspective": "What viewpoint is missing",
|
| 269 |
+
"importance": "high|medium|low",
|
| 270 |
+
"suggested_sources": ["Types of sources that would help"]
|
| 271 |
+
}}
|
| 272 |
+
],
|
| 273 |
+
"balance_assessment": {{
|
| 274 |
+
"is_balanced": true,
|
| 275 |
+
"skew_direction": "Direction of any skew",
|
| 276 |
+
"recommendations": ["How to improve balance"]
|
| 277 |
+
}}
|
| 278 |
+
}}""",
|
| 279 |
+
|
| 280 |
+
"verification_summary": """You are a verification summarizer. Create a comprehensive verification summary for the research findings.
|
| 281 |
+
|
| 282 |
+
## Original Findings
|
| 283 |
+
{findings}
|
| 284 |
+
|
| 285 |
+
## Verification Results
|
| 286 |
+
Cross-reference: {cross_reference_results}
|
| 287 |
+
Credibility: {credibility_results}
|
| 288 |
+
Conflicts: {conflict_results}
|
| 289 |
+
Uncertainty: {uncertainty_results}
|
| 290 |
+
|
| 291 |
+
## Instructions
|
| 292 |
+
Create a comprehensive verification summary that:
|
| 293 |
+
|
| 294 |
+
1. **Overall Assessment**: How trustworthy are the findings?
|
| 295 |
+
2. **Verified Findings**: What can be stated with confidence?
|
| 296 |
+
3. **Caveats**: What limitations should be noted?
|
| 297 |
+
4. **Flags**: What needs user attention?
|
| 298 |
+
|
| 299 |
+
## Output Format
|
| 300 |
+
{{
|
| 301 |
+
"verification_summary": {{
|
| 302 |
+
"overall_confidence": 0.75,
|
| 303 |
+
"trust_level": "high|medium|low",
|
| 304 |
+
"verification_completeness": 85
|
| 305 |
+
}},
|
| 306 |
+
"verified_findings": [
|
| 307 |
+
{{
|
| 308 |
+
"finding": "string",
|
| 309 |
+
"confidence": 0.85,
|
| 310 |
+
"verification_status": "verified|partially_verified|unverified"
|
| 311 |
+
}}
|
| 312 |
+
],
|
| 313 |
+
"caveats": ["Important caveats for the user"],
|
| 314 |
+
"flags": [
|
| 315 |
+
{{
|
| 316 |
+
"type": "conflict|bias|uncertainty|credibility",
|
| 317 |
+
"message": "What the user should know",
|
| 318 |
+
"severity": "high|medium|low"
|
| 319 |
+
}}
|
| 320 |
+
],
|
| 321 |
+
"recommendations": ["Recommendations for improving research quality"]
|
| 322 |
+
}}"""
|
| 323 |
+
}
|
src/search_duckduckgo.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Free web search using DuckDuckGo for Deep Research AI.
|
| 3 |
+
|
| 4 |
+
No API key required - perfect for Hugging Face deployment.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from duckduckgo_search import DDGS
|
| 13 |
+
DDGS_AVAILABLE = True
|
| 14 |
+
except ImportError:
|
| 15 |
+
DDGS_AVAILABLE = False
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class SearchResult:
|
| 20 |
+
"""Search result from DuckDuckGo."""
|
| 21 |
+
title: str
|
| 22 |
+
url: str
|
| 23 |
+
snippet: str
|
| 24 |
+
domain: str
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class DuckDuckGoSearch:
|
| 28 |
+
"""
|
| 29 |
+
Free web search using DuckDuckGo.
|
| 30 |
+
|
| 31 |
+
No API key required.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, max_results: int = 10) -> None:
|
| 35 |
+
"""
|
| 36 |
+
Initialize DuckDuckGo search.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
max_results: Maximum results per search
|
| 40 |
+
"""
|
| 41 |
+
if not DDGS_AVAILABLE:
|
| 42 |
+
raise ImportError(
|
| 43 |
+
"duckduckgo-search not installed. "
|
| 44 |
+
"Install with: pip install duckduckgo-search"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
self.max_results = max_results
|
| 48 |
+
self.ddgs = DDGS()
|
| 49 |
+
|
| 50 |
+
async def search(
|
| 51 |
+
self,
|
| 52 |
+
query: str,
|
| 53 |
+
max_results: int | None = None
|
| 54 |
+
) -> list[SearchResult]:
|
| 55 |
+
"""
|
| 56 |
+
Search the web using DuckDuckGo.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
query: Search query
|
| 60 |
+
max_results: Override default max results
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
List of SearchResult objects
|
| 64 |
+
"""
|
| 65 |
+
max_results = max_results or self.max_results
|
| 66 |
+
|
| 67 |
+
# Run sync search in executor
|
| 68 |
+
loop = asyncio.get_event_loop()
|
| 69 |
+
results = await loop.run_in_executor(
|
| 70 |
+
None,
|
| 71 |
+
lambda: list(self.ddgs.text(query, max_results=max_results))
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
search_results = []
|
| 75 |
+
for r in results:
|
| 76 |
+
# Extract domain from URL
|
| 77 |
+
url = r.get("href", r.get("link", ""))
|
| 78 |
+
domain = self._extract_domain(url)
|
| 79 |
+
|
| 80 |
+
search_results.append(SearchResult(
|
| 81 |
+
title=r.get("title", ""),
|
| 82 |
+
url=url,
|
| 83 |
+
snippet=r.get("body", r.get("snippet", "")),
|
| 84 |
+
domain=domain
|
| 85 |
+
))
|
| 86 |
+
|
| 87 |
+
return search_results
|
| 88 |
+
|
| 89 |
+
async def search_news(
|
| 90 |
+
self,
|
| 91 |
+
query: str,
|
| 92 |
+
max_results: int | None = None
|
| 93 |
+
) -> list[SearchResult]:
|
| 94 |
+
"""
|
| 95 |
+
Search news using DuckDuckGo.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
query: Search query
|
| 99 |
+
max_results: Override default max results
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
List of SearchResult objects
|
| 103 |
+
"""
|
| 104 |
+
max_results = max_results or self.max_results
|
| 105 |
+
|
| 106 |
+
loop = asyncio.get_event_loop()
|
| 107 |
+
results = await loop.run_in_executor(
|
| 108 |
+
None,
|
| 109 |
+
lambda: list(self.ddgs.news(query, max_results=max_results))
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
search_results = []
|
| 113 |
+
for r in results:
|
| 114 |
+
url = r.get("url", r.get("link", ""))
|
| 115 |
+
domain = self._extract_domain(url)
|
| 116 |
+
|
| 117 |
+
search_results.append(SearchResult(
|
| 118 |
+
title=r.get("title", ""),
|
| 119 |
+
url=url,
|
| 120 |
+
snippet=r.get("body", r.get("excerpt", "")),
|
| 121 |
+
domain=domain
|
| 122 |
+
))
|
| 123 |
+
|
| 124 |
+
return search_results
|
| 125 |
+
|
| 126 |
+
def _extract_domain(self, url: str) -> str:
|
| 127 |
+
"""Extract domain from URL."""
|
| 128 |
+
try:
|
| 129 |
+
from urllib.parse import urlparse
|
| 130 |
+
parsed = urlparse(url)
|
| 131 |
+
return parsed.netloc.replace("www.", "")
|
| 132 |
+
except Exception:
|
| 133 |
+
return ""
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
async def search_web(query: str, max_results: int = 10) -> list[dict[str, Any]]:
|
| 137 |
+
"""
|
| 138 |
+
Convenience function for web search.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
query: Search query
|
| 142 |
+
max_results: Maximum results
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
List of result dictionaries
|
| 146 |
+
"""
|
| 147 |
+
searcher = DuckDuckGoSearch(max_results=max_results)
|
| 148 |
+
results = await searcher.search(query)
|
| 149 |
+
|
| 150 |
+
return [
|
| 151 |
+
{
|
| 152 |
+
"title": r.title,
|
| 153 |
+
"url": r.url,
|
| 154 |
+
"snippet": r.snippet,
|
| 155 |
+
"domain": r.domain
|
| 156 |
+
}
|
| 157 |
+
for r in results
|
| 158 |
+
]
|