debashis2007 commited on
Commit
1eae9f8
·
verified ·
1 Parent(s): 21d1e23

Upload folder using huggingface_hub

Browse files
.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
+ ]