Spaces:
Running
Running
Joseph Pollack
commited on
adds tests , integration tests , github readme , and more!
Browse files- .github/README.md +203 -0
- CONTRIBUTING.md +1 -0
- README.md +98 -18
- docs/CONFIGURATION.md +3 -0
- docs/architecture/graph_orchestration.md +3 -0
- docs/examples/writer_agents_usage.md +3 -0
- src/agent_factory/graph_builder.py +1 -1
- src/agent_factory/judges.py +1 -1
- src/app.py +1 -1
- src/orchestrator/graph_orchestrator.py +23 -2
- src/orchestrator/planner_agent.py +11 -1
- src/tools/pubmed.py +19 -6
- src/utils/llm_factory.py +1 -1
- tests/unit/agent_factory/test_graph_builder.py +4 -4
- tests/unit/agent_factory/test_judges_factory.py +1 -1
- tests/unit/agents/test_long_writer.py +19 -13
- tests/unit/agents/test_proofreader.py +37 -24
- tests/unit/agents/test_writer.py +17 -14
- tests/unit/orchestrator/test_graph_orchestrator.py +47 -11
- tests/unit/orchestrator/test_planner_agent.py +9 -10
- tests/unit/orchestrator/test_research_flow.py +2 -2
- tests/unit/services/test_embeddings.py +10 -2
- tests/unit/test_no_webtool_references.py +5 -1
- tests/unit/tools/test_pubmed.py +33 -16
- tests/unit/tools/test_rag_tool.py +2 -2
.github/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: DeepCritical
|
| 3 |
+
emoji: 🧬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "6.0.1"
|
| 8 |
+
python_version: "3.11"
|
| 9 |
+
app_file: src/app.py
|
| 10 |
+
pinned: false
|
| 11 |
+
license: mit
|
| 12 |
+
tags:
|
| 13 |
+
- mcp-in-action-track-enterprise
|
| 14 |
+
- mcp-hackathon
|
| 15 |
+
- drug-repurposing
|
| 16 |
+
- biomedical-ai
|
| 17 |
+
- pydantic-ai
|
| 18 |
+
- llamaindex
|
| 19 |
+
- modal
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
# DeepCritical
|
| 23 |
+
|
| 24 |
+
## Intro
|
| 25 |
+
|
| 26 |
+
## Features
|
| 27 |
+
|
| 28 |
+
- **Multi-Source Search**: PubMed, ClinicalTrials.gov, bioRxiv/medRxiv
|
| 29 |
+
- **MCP Integration**: Use our tools from Claude Desktop or any MCP client
|
| 30 |
+
- **Modal Sandbox**: Secure execution of AI-generated statistical code
|
| 31 |
+
- **LlamaIndex RAG**: Semantic search and evidence synthesis
|
| 32 |
+
- **HuggingfaceInference**:
|
| 33 |
+
- **HuggingfaceMCP Custom Config To Use Community Tools**:
|
| 34 |
+
- **Strongly Typed Composable Graphs**:
|
| 35 |
+
- **Specialized Research Teams of Agents**:
|
| 36 |
+
|
| 37 |
+
## Quick Start
|
| 38 |
+
|
| 39 |
+
### 1. Environment Setup
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
# Install uv if you haven't already
|
| 43 |
+
pip install uv
|
| 44 |
+
|
| 45 |
+
# Sync dependencies
|
| 46 |
+
uv sync
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 2. Run the UI
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
# Start the Gradio app
|
| 53 |
+
uv run gradio run src/app.py
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Open your browser to `http://localhost:7860`.
|
| 57 |
+
|
| 58 |
+
### 3. Connect via MCP
|
| 59 |
+
|
| 60 |
+
This application exposes a Model Context Protocol (MCP) server, allowing you to use its search tools directly from Claude Desktop or other MCP clients.
|
| 61 |
+
|
| 62 |
+
**MCP Server URL**: `http://localhost:7860/gradio_api/mcp/`
|
| 63 |
+
|
| 64 |
+
**Claude Desktop Configuration**:
|
| 65 |
+
Add this to your `claude_desktop_config.json`:
|
| 66 |
+
```json
|
| 67 |
+
{
|
| 68 |
+
"mcpServers": {
|
| 69 |
+
"deepcritical": {
|
| 70 |
+
"url": "http://localhost:7860/gradio_api/mcp/"
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**Available Tools**:
|
| 77 |
+
- `search_pubmed`: Search peer-reviewed biomedical literature.
|
| 78 |
+
- `search_clinical_trials`: Search ClinicalTrials.gov.
|
| 79 |
+
- `search_biorxiv`: Search bioRxiv/medRxiv preprints.
|
| 80 |
+
- `search_all`: Search all sources simultaneously.
|
| 81 |
+
- `analyze_hypothesis`: Secure statistical analysis using Modal sandboxes.
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
## Deep Research Flows
|
| 85 |
+
|
| 86 |
+
- iterativeResearch
|
| 87 |
+
- deepResearch
|
| 88 |
+
- researchTeam
|
| 89 |
+
|
| 90 |
+
### Iterative Research
|
| 91 |
+
|
| 92 |
+
sequenceDiagram
|
| 93 |
+
participant IterativeFlow
|
| 94 |
+
participant ThinkingAgent
|
| 95 |
+
participant KnowledgeGapAgent
|
| 96 |
+
participant ToolSelector
|
| 97 |
+
participant ToolExecutor
|
| 98 |
+
participant JudgeHandler
|
| 99 |
+
participant WriterAgent
|
| 100 |
+
|
| 101 |
+
IterativeFlow->>IterativeFlow: run(query)
|
| 102 |
+
|
| 103 |
+
loop Until complete or max_iterations
|
| 104 |
+
IterativeFlow->>ThinkingAgent: generate_observations()
|
| 105 |
+
ThinkingAgent-->>IterativeFlow: observations
|
| 106 |
+
|
| 107 |
+
IterativeFlow->>KnowledgeGapAgent: evaluate_gaps()
|
| 108 |
+
KnowledgeGapAgent-->>IterativeFlow: KnowledgeGapOutput
|
| 109 |
+
|
| 110 |
+
alt Research complete
|
| 111 |
+
IterativeFlow->>WriterAgent: create_final_report()
|
| 112 |
+
WriterAgent-->>IterativeFlow: final_report
|
| 113 |
+
else Gaps remain
|
| 114 |
+
IterativeFlow->>ToolSelector: select_agents(gap)
|
| 115 |
+
ToolSelector-->>IterativeFlow: AgentSelectionPlan
|
| 116 |
+
|
| 117 |
+
IterativeFlow->>ToolExecutor: execute_tool_tasks()
|
| 118 |
+
ToolExecutor-->>IterativeFlow: ToolAgentOutput[]
|
| 119 |
+
|
| 120 |
+
IterativeFlow->>JudgeHandler: assess_evidence()
|
| 121 |
+
JudgeHandler-->>IterativeFlow: should_continue
|
| 122 |
+
end
|
| 123 |
+
end
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
### Deep Research
|
| 127 |
+
|
| 128 |
+
sequenceDiagram
|
| 129 |
+
actor User
|
| 130 |
+
participant GraphOrchestrator
|
| 131 |
+
participant InputParser
|
| 132 |
+
participant GraphBuilder
|
| 133 |
+
participant GraphExecutor
|
| 134 |
+
participant Agent
|
| 135 |
+
participant BudgetTracker
|
| 136 |
+
participant WorkflowState
|
| 137 |
+
|
| 138 |
+
User->>GraphOrchestrator: run(query)
|
| 139 |
+
GraphOrchestrator->>InputParser: detect_research_mode(query)
|
| 140 |
+
InputParser-->>GraphOrchestrator: mode (iterative/deep)
|
| 141 |
+
GraphOrchestrator->>GraphBuilder: build_graph(mode)
|
| 142 |
+
GraphBuilder-->>GraphOrchestrator: ResearchGraph
|
| 143 |
+
GraphOrchestrator->>WorkflowState: init_workflow_state()
|
| 144 |
+
GraphOrchestrator->>BudgetTracker: create_budget()
|
| 145 |
+
GraphOrchestrator->>GraphExecutor: _execute_graph(graph)
|
| 146 |
+
|
| 147 |
+
loop For each node in graph
|
| 148 |
+
GraphExecutor->>Agent: execute_node(agent_node)
|
| 149 |
+
Agent->>Agent: process_input
|
| 150 |
+
Agent-->>GraphExecutor: result
|
| 151 |
+
GraphExecutor->>WorkflowState: update_state(result)
|
| 152 |
+
GraphExecutor->>BudgetTracker: add_tokens(used)
|
| 153 |
+
GraphExecutor->>BudgetTracker: check_budget()
|
| 154 |
+
alt Budget exceeded
|
| 155 |
+
GraphExecutor->>GraphOrchestrator: emit(error_event)
|
| 156 |
+
else Continue
|
| 157 |
+
GraphExecutor->>GraphOrchestrator: emit(progress_event)
|
| 158 |
+
end
|
| 159 |
+
end
|
| 160 |
+
|
| 161 |
+
GraphOrchestrator->>User: AsyncGenerator[AgentEvent]
|
| 162 |
+
|
| 163 |
+
### Research Team
|
| 164 |
+
Critical Deep Research Agent
|
| 165 |
+
|
| 166 |
+
## Development
|
| 167 |
+
|
| 168 |
+
### Run Tests
|
| 169 |
+
|
| 170 |
+
```bash
|
| 171 |
+
uv run pytest
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Run Checks
|
| 175 |
+
|
| 176 |
+
```bash
|
| 177 |
+
make check
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
## Architecture
|
| 181 |
+
|
| 182 |
+
DeepCritical uses a Vertical Slice Architecture:
|
| 183 |
+
|
| 184 |
+
1. **Search Slice**: Retrieving evidence from PubMed, ClinicalTrials.gov, and bioRxiv.
|
| 185 |
+
2. **Judge Slice**: Evaluating evidence quality using LLMs.
|
| 186 |
+
3. **Orchestrator Slice**: Managing the research loop and UI.
|
| 187 |
+
|
| 188 |
+
Built with:
|
| 189 |
+
- **PydanticAI**: For robust agent interactions.
|
| 190 |
+
- **Gradio**: For the streaming user interface.
|
| 191 |
+
- **PubMed, ClinicalTrials.gov, bioRxiv**: For biomedical data.
|
| 192 |
+
- **MCP**: For universal tool access.
|
| 193 |
+
- **Modal**: For secure code execution.
|
| 194 |
+
|
| 195 |
+
## Team
|
| 196 |
+
|
| 197 |
+
- The-Obstacle-Is-The-Way
|
| 198 |
+
- MarioAderman
|
| 199 |
+
- Josephrp
|
| 200 |
+
|
| 201 |
+
## Links
|
| 202 |
+
|
| 203 |
+
- [GitHub Repository](https://github.com/The-Obstacle-Is-The-Way/DeepCritical-1)
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
make sure you run the full pre-commit checks before opening a PR (not draft) otherwise Obstacle is the Way will loose his mind
|
README.md
CHANGED
|
@@ -21,7 +21,7 @@ tags:
|
|
| 21 |
|
| 22 |
# DeepCritical
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
## Features
|
| 27 |
|
|
@@ -29,6 +29,10 @@ AI-Powered Drug Repurposing Research Agent
|
|
| 29 |
- **MCP Integration**: Use our tools from Claude Desktop or any MCP client
|
| 30 |
- **Modal Sandbox**: Secure execution of AI-generated statistical code
|
| 31 |
- **LlamaIndex RAG**: Semantic search and evidence synthesis
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
## Quick Start
|
| 34 |
|
|
@@ -46,7 +50,7 @@ uv sync
|
|
| 46 |
|
| 47 |
```bash
|
| 48 |
# Start the Gradio app
|
| 49 |
-
uv run
|
| 50 |
```
|
| 51 |
|
| 52 |
Open your browser to `http://localhost:7860`.
|
|
@@ -76,6 +80,97 @@ Add this to your `claude_desktop_config.json`:
|
|
| 76 |
- `search_all`: Search all sources simultaneously.
|
| 77 |
- `analyze_hypothesis`: Secure statistical analysis using Modal sandboxes.
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
## Development
|
| 80 |
|
| 81 |
### Run Tests
|
|
@@ -90,22 +185,7 @@ uv run pytest
|
|
| 90 |
make check
|
| 91 |
```
|
| 92 |
|
| 93 |
-
##
|
| 94 |
-
|
| 95 |
-
DeepCritical uses a Vertical Slice Architecture:
|
| 96 |
-
|
| 97 |
-
1. **Search Slice**: Retrieving evidence from PubMed, ClinicalTrials.gov, and bioRxiv.
|
| 98 |
-
2. **Judge Slice**: Evaluating evidence quality using LLMs.
|
| 99 |
-
3. **Orchestrator Slice**: Managing the research loop and UI.
|
| 100 |
-
|
| 101 |
-
Built with:
|
| 102 |
-
- **PydanticAI**: For robust agent interactions.
|
| 103 |
-
- **Gradio**: For the streaming user interface.
|
| 104 |
-
- **PubMed, ClinicalTrials.gov, bioRxiv**: For biomedical data.
|
| 105 |
-
- **MCP**: For universal tool access.
|
| 106 |
-
- **Modal**: For secure code execution.
|
| 107 |
-
|
| 108 |
-
## Team
|
| 109 |
|
| 110 |
- The-Obstacle-Is-The-Way
|
| 111 |
- MarioAderman
|
|
|
|
| 21 |
|
| 22 |
# DeepCritical
|
| 23 |
|
| 24 |
+
## Intro
|
| 25 |
|
| 26 |
## Features
|
| 27 |
|
|
|
|
| 29 |
- **MCP Integration**: Use our tools from Claude Desktop or any MCP client
|
| 30 |
- **Modal Sandbox**: Secure execution of AI-generated statistical code
|
| 31 |
- **LlamaIndex RAG**: Semantic search and evidence synthesis
|
| 32 |
+
- **HuggingfaceInference**:
|
| 33 |
+
- **HuggingfaceMCP Custom Config To Use Community Tools**:
|
| 34 |
+
- **Strongly Typed Composable Graphs**:
|
| 35 |
+
- **Specialized Research Teams of Agents**:
|
| 36 |
|
| 37 |
## Quick Start
|
| 38 |
|
|
|
|
| 50 |
|
| 51 |
```bash
|
| 52 |
# Start the Gradio app
|
| 53 |
+
uv run gradio run src/app.py
|
| 54 |
```
|
| 55 |
|
| 56 |
Open your browser to `http://localhost:7860`.
|
|
|
|
| 80 |
- `search_all`: Search all sources simultaneously.
|
| 81 |
- `analyze_hypothesis`: Secure statistical analysis using Modal sandboxes.
|
| 82 |
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
## Architecture
|
| 86 |
+
|
| 87 |
+
DeepCritical uses a Vertical Slice Architecture:
|
| 88 |
+
|
| 89 |
+
1. **Search Slice**: Retrieving evidence from PubMed, ClinicalTrials.gov, and bioRxiv.
|
| 90 |
+
2. **Judge Slice**: Evaluating evidence quality using LLMs.
|
| 91 |
+
3. **Orchestrator Slice**: Managing the research loop and UI.
|
| 92 |
+
|
| 93 |
+
- iterativeResearch
|
| 94 |
+
- deepResearch
|
| 95 |
+
- researchTeam
|
| 96 |
+
|
| 97 |
+
### Iterative Research
|
| 98 |
+
|
| 99 |
+
sequenceDiagram
|
| 100 |
+
participant IterativeFlow
|
| 101 |
+
participant ThinkingAgent
|
| 102 |
+
participant KnowledgeGapAgent
|
| 103 |
+
participant ToolSelector
|
| 104 |
+
participant ToolExecutor
|
| 105 |
+
participant JudgeHandler
|
| 106 |
+
participant WriterAgent
|
| 107 |
+
|
| 108 |
+
IterativeFlow->>IterativeFlow: run(query)
|
| 109 |
+
|
| 110 |
+
loop Until complete or max_iterations
|
| 111 |
+
IterativeFlow->>ThinkingAgent: generate_observations()
|
| 112 |
+
ThinkingAgent-->>IterativeFlow: observations
|
| 113 |
+
|
| 114 |
+
IterativeFlow->>KnowledgeGapAgent: evaluate_gaps()
|
| 115 |
+
KnowledgeGapAgent-->>IterativeFlow: KnowledgeGapOutput
|
| 116 |
+
|
| 117 |
+
alt Research complete
|
| 118 |
+
IterativeFlow->>WriterAgent: create_final_report()
|
| 119 |
+
WriterAgent-->>IterativeFlow: final_report
|
| 120 |
+
else Gaps remain
|
| 121 |
+
IterativeFlow->>ToolSelector: select_agents(gap)
|
| 122 |
+
ToolSelector-->>IterativeFlow: AgentSelectionPlan
|
| 123 |
+
|
| 124 |
+
IterativeFlow->>ToolExecutor: execute_tool_tasks()
|
| 125 |
+
ToolExecutor-->>IterativeFlow: ToolAgentOutput[]
|
| 126 |
+
|
| 127 |
+
IterativeFlow->>JudgeHandler: assess_evidence()
|
| 128 |
+
JudgeHandler-->>IterativeFlow: should_continue
|
| 129 |
+
end
|
| 130 |
+
end
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
### Deep Research
|
| 134 |
+
|
| 135 |
+
sequenceDiagram
|
| 136 |
+
actor User
|
| 137 |
+
participant GraphOrchestrator
|
| 138 |
+
participant InputParser
|
| 139 |
+
participant GraphBuilder
|
| 140 |
+
participant GraphExecutor
|
| 141 |
+
participant Agent
|
| 142 |
+
participant BudgetTracker
|
| 143 |
+
participant WorkflowState
|
| 144 |
+
|
| 145 |
+
User->>GraphOrchestrator: run(query)
|
| 146 |
+
GraphOrchestrator->>InputParser: detect_research_mode(query)
|
| 147 |
+
InputParser-->>GraphOrchestrator: mode (iterative/deep)
|
| 148 |
+
GraphOrchestrator->>GraphBuilder: build_graph(mode)
|
| 149 |
+
GraphBuilder-->>GraphOrchestrator: ResearchGraph
|
| 150 |
+
GraphOrchestrator->>WorkflowState: init_workflow_state()
|
| 151 |
+
GraphOrchestrator->>BudgetTracker: create_budget()
|
| 152 |
+
GraphOrchestrator->>GraphExecutor: _execute_graph(graph)
|
| 153 |
+
|
| 154 |
+
loop For each node in graph
|
| 155 |
+
GraphExecutor->>Agent: execute_node(agent_node)
|
| 156 |
+
Agent->>Agent: process_input
|
| 157 |
+
Agent-->>GraphExecutor: result
|
| 158 |
+
GraphExecutor->>WorkflowState: update_state(result)
|
| 159 |
+
GraphExecutor->>BudgetTracker: add_tokens(used)
|
| 160 |
+
GraphExecutor->>BudgetTracker: check_budget()
|
| 161 |
+
alt Budget exceeded
|
| 162 |
+
GraphExecutor->>GraphOrchestrator: emit(error_event)
|
| 163 |
+
else Continue
|
| 164 |
+
GraphExecutor->>GraphOrchestrator: emit(progress_event)
|
| 165 |
+
end
|
| 166 |
+
end
|
| 167 |
+
|
| 168 |
+
GraphOrchestrator->>User: AsyncGenerator[AgentEvent]
|
| 169 |
+
|
| 170 |
+
### Research Team
|
| 171 |
+
|
| 172 |
+
Critical Deep Research Agent
|
| 173 |
+
|
| 174 |
## Development
|
| 175 |
|
| 176 |
### Run Tests
|
|
|
|
| 185 |
make check
|
| 186 |
```
|
| 187 |
|
| 188 |
+
## Join Us
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
- The-Obstacle-Is-The-Way
|
| 191 |
- MarioAderman
|
docs/CONFIGURATION.md
CHANGED
|
@@ -289,3 +289,6 @@ See `CONFIGURATION_ANALYSIS.md` for the complete implementation plan.
|
|
| 289 |
|
| 290 |
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
|
| 291 |
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
|
docs/architecture/graph_orchestration.md
CHANGED
|
@@ -139,3 +139,6 @@ This allows gradual migration and fallback if needed.
|
|
| 139 |
|
| 140 |
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
|
docs/examples/writer_agents_usage.md
CHANGED
|
@@ -413,3 +413,6 @@ For large reports:
|
|
| 413 |
|
| 414 |
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
|
| 415 |
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
|
src/agent_factory/graph_builder.py
CHANGED
|
@@ -79,7 +79,7 @@ class ParallelNode(GraphNode):
|
|
| 79 |
|
| 80 |
node_type: Literal["parallel"] = "parallel"
|
| 81 |
parallel_nodes: list[str] = Field(
|
| 82 |
-
description="List of node IDs to run in parallel", min_length=
|
| 83 |
)
|
| 84 |
aggregator: Callable[[list[Any]], Any] | None = Field(
|
| 85 |
default=None, description="Function to aggregate parallel results"
|
|
|
|
| 79 |
|
| 80 |
node_type: Literal["parallel"] = "parallel"
|
| 81 |
parallel_nodes: list[str] = Field(
|
| 82 |
+
description="List of node IDs to run in parallel", min_length=0
|
| 83 |
)
|
| 84 |
aggregator: Callable[[list[Any]], Any] | None = Field(
|
| 85 |
default=None, description="Function to aggregate parallel results"
|
src/agent_factory/judges.py
CHANGED
|
@@ -9,7 +9,7 @@ from huggingface_hub import InferenceClient
|
|
| 9 |
from pydantic_ai import Agent
|
| 10 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 11 |
from pydantic_ai.models.huggingface import HuggingFaceModel
|
| 12 |
-
from pydantic_ai.models.openai import OpenAIModel
|
| 13 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 14 |
from pydantic_ai.providers.huggingface import HuggingFaceProvider
|
| 15 |
from pydantic_ai.providers.openai import OpenAIProvider
|
|
|
|
| 9 |
from pydantic_ai import Agent
|
| 10 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 11 |
from pydantic_ai.models.huggingface import HuggingFaceModel
|
| 12 |
+
from pydantic_ai.models.openai import OpenAIChatModel as OpenAIModel
|
| 13 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 14 |
from pydantic_ai.providers.huggingface import HuggingFaceProvider
|
| 15 |
from pydantic_ai.providers.openai import OpenAIProvider
|
src/app.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 9 |
-
from pydantic_ai.models.openai import OpenAIModel
|
| 10 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 11 |
from pydantic_ai.providers.openai import OpenAIProvider
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 9 |
+
from pydantic_ai.models.openai import OpenAIChatModel as OpenAIModel
|
| 10 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 11 |
from pydantic_ai.providers.openai import OpenAIProvider
|
| 12 |
|
src/orchestrator/graph_orchestrator.py
CHANGED
|
@@ -250,7 +250,18 @@ class GraphOrchestrator:
|
|
| 250 |
max_time_minutes=self.max_time_minutes,
|
| 251 |
)
|
| 252 |
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
yield AgentEvent(
|
| 256 |
type="complete",
|
|
@@ -272,7 +283,17 @@ class GraphOrchestrator:
|
|
| 272 |
max_time_minutes=self.max_time_minutes,
|
| 273 |
)
|
| 274 |
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
yield AgentEvent(
|
| 278 |
type="complete",
|
|
|
|
| 250 |
max_time_minutes=self.max_time_minutes,
|
| 251 |
)
|
| 252 |
|
| 253 |
+
try:
|
| 254 |
+
final_report = await self._iterative_flow.run(query)
|
| 255 |
+
except Exception as e:
|
| 256 |
+
self.logger.error("Iterative flow failed", error=str(e), exc_info=True)
|
| 257 |
+
# Yield error event - outer handler will also catch and yield error event
|
| 258 |
+
yield AgentEvent(
|
| 259 |
+
type="error",
|
| 260 |
+
message=f"Iterative research failed: {e!s}",
|
| 261 |
+
iteration=1,
|
| 262 |
+
)
|
| 263 |
+
# Re-raise so outer handler can also yield error event for consistency
|
| 264 |
+
raise
|
| 265 |
|
| 266 |
yield AgentEvent(
|
| 267 |
type="complete",
|
|
|
|
| 283 |
max_time_minutes=self.max_time_minutes,
|
| 284 |
)
|
| 285 |
|
| 286 |
+
try:
|
| 287 |
+
final_report = await self._deep_flow.run(query)
|
| 288 |
+
except Exception as e:
|
| 289 |
+
self.logger.error("Deep flow failed", error=str(e), exc_info=True)
|
| 290 |
+
# Yield error event before re-raising so test can capture it
|
| 291 |
+
yield AgentEvent(
|
| 292 |
+
type="error",
|
| 293 |
+
message=f"Deep research failed: {e!s}",
|
| 294 |
+
iteration=1,
|
| 295 |
+
)
|
| 296 |
+
raise
|
| 297 |
|
| 298 |
yield AgentEvent(
|
| 299 |
type="complete",
|
src/orchestrator/planner_agent.py
CHANGED
|
@@ -114,7 +114,17 @@ class PlannerAgent:
|
|
| 114 |
# Validate report plan
|
| 115 |
if not report_plan.report_outline:
|
| 116 |
self.logger.warning("Report plan has no sections", query=query[:100])
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
if not report_plan.report_title:
|
| 120 |
self.logger.warning("Report plan has no title", query=query[:100])
|
|
|
|
| 114 |
# Validate report plan
|
| 115 |
if not report_plan.report_outline:
|
| 116 |
self.logger.warning("Report plan has no sections", query=query[:100])
|
| 117 |
+
# Return fallback plan instead of raising error
|
| 118 |
+
return ReportPlan(
|
| 119 |
+
background_context=report_plan.background_context or "",
|
| 120 |
+
report_outline=[
|
| 121 |
+
ReportPlanSection(
|
| 122 |
+
title="Overview",
|
| 123 |
+
key_question=query,
|
| 124 |
+
)
|
| 125 |
+
],
|
| 126 |
+
report_title=report_plan.report_title or f"Research Report: {query[:50]}",
|
| 127 |
+
)
|
| 128 |
|
| 129 |
if not report_plan.report_title:
|
| 130 |
self.logger.warning("Report plan has no title", query=query[:100])
|
src/tools/pubmed.py
CHANGED
|
@@ -77,6 +77,8 @@ class PubMedTool:
|
|
| 77 |
params=search_params,
|
| 78 |
)
|
| 79 |
search_resp.raise_for_status()
|
|
|
|
|
|
|
| 80 |
except httpx.HTTPStatusError as e:
|
| 81 |
if e.response.status_code == self.HTTP_TOO_MANY_REQUESTS:
|
| 82 |
raise RateLimitError("PubMed rate limit exceeded") from e
|
|
@@ -98,11 +100,14 @@ class PubMedTool:
|
|
| 98 |
# Use XML for fetch (more reliable parsing)
|
| 99 |
fetch_params["retmode"] = "xml"
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
# Step 3: Parse XML to Evidence
|
| 108 |
return self._parse_pubmed_xml(fetch_resp.text)
|
|
@@ -114,7 +119,15 @@ class PubMedTool:
|
|
| 114 |
except Exception as e:
|
| 115 |
raise SearchError(f"Failed to parse PubMed XML: {e}") from e
|
| 116 |
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
# Handle single article (xmltodict returns dict instead of list)
|
| 120 |
if isinstance(articles, dict):
|
|
|
|
| 77 |
params=search_params,
|
| 78 |
)
|
| 79 |
search_resp.raise_for_status()
|
| 80 |
+
except httpx.TimeoutException as e:
|
| 81 |
+
raise SearchError(f"PubMed search timeout: {e}") from e
|
| 82 |
except httpx.HTTPStatusError as e:
|
| 83 |
if e.response.status_code == self.HTTP_TOO_MANY_REQUESTS:
|
| 84 |
raise RateLimitError("PubMed rate limit exceeded") from e
|
|
|
|
| 100 |
# Use XML for fetch (more reliable parsing)
|
| 101 |
fetch_params["retmode"] = "xml"
|
| 102 |
|
| 103 |
+
try:
|
| 104 |
+
fetch_resp = await client.get(
|
| 105 |
+
f"{self.BASE_URL}/efetch.fcgi",
|
| 106 |
+
params=fetch_params,
|
| 107 |
+
)
|
| 108 |
+
fetch_resp.raise_for_status()
|
| 109 |
+
except httpx.TimeoutException as e:
|
| 110 |
+
raise SearchError(f"PubMed fetch timeout: {e}") from e
|
| 111 |
|
| 112 |
# Step 3: Parse XML to Evidence
|
| 113 |
return self._parse_pubmed_xml(fetch_resp.text)
|
|
|
|
| 119 |
except Exception as e:
|
| 120 |
raise SearchError(f"Failed to parse PubMed XML: {e}") from e
|
| 121 |
|
| 122 |
+
if data is None:
|
| 123 |
+
return []
|
| 124 |
+
|
| 125 |
+
# Handle case where PubmedArticleSet might not exist or be empty
|
| 126 |
+
pubmed_set = data.get("PubmedArticleSet")
|
| 127 |
+
if not pubmed_set:
|
| 128 |
+
return []
|
| 129 |
+
|
| 130 |
+
articles = pubmed_set.get("PubmedArticle", [])
|
| 131 |
|
| 132 |
# Handle single article (xmltodict returns dict instead of list)
|
| 133 |
if isinstance(articles, dict):
|
src/utils/llm_factory.py
CHANGED
|
@@ -56,7 +56,7 @@ def get_pydantic_ai_model() -> Any:
|
|
| 56 |
Configured pydantic-ai model
|
| 57 |
"""
|
| 58 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 59 |
-
from pydantic_ai.models.openai import OpenAIModel
|
| 60 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 61 |
from pydantic_ai.providers.openai import OpenAIProvider
|
| 62 |
|
|
|
|
| 56 |
Configured pydantic-ai model
|
| 57 |
"""
|
| 58 |
from pydantic_ai.models.anthropic import AnthropicModel
|
| 59 |
+
from pydantic_ai.models.openai import OpenAIChatModel as OpenAIModel
|
| 60 |
from pydantic_ai.providers.anthropic import AnthropicProvider
|
| 61 |
from pydantic_ai.providers.openai import OpenAIProvider
|
| 62 |
|
tests/unit/agent_factory/test_graph_builder.py
CHANGED
|
@@ -240,7 +240,7 @@ class TestResearchGraph:
|
|
| 240 |
def test_validate_empty_graph(self):
|
| 241 |
"""Test validating an empty graph."""
|
| 242 |
graph = ResearchGraph(entry_node="start", exit_nodes=["end"])
|
| 243 |
-
errors = graph.
|
| 244 |
assert len(errors) > 0 # Should have errors for missing entry/exit nodes
|
| 245 |
|
| 246 |
def test_validate_valid_graph(self):
|
|
@@ -252,7 +252,7 @@ class TestResearchGraph:
|
|
| 252 |
graph.add_node(end_node)
|
| 253 |
graph.add_edge(SequentialEdge(from_node="start", to_node="end"))
|
| 254 |
|
| 255 |
-
errors = graph.
|
| 256 |
assert len(errors) == 0
|
| 257 |
|
| 258 |
def test_validate_unreachable_nodes(self):
|
|
@@ -266,7 +266,7 @@ class TestResearchGraph:
|
|
| 266 |
graph.add_node(unreachable)
|
| 267 |
graph.add_edge(SequentialEdge(from_node="start", to_node="end"))
|
| 268 |
|
| 269 |
-
errors = graph.
|
| 270 |
assert len(errors) > 0
|
| 271 |
assert any("unreachable" in error.lower() for error in errors)
|
| 272 |
|
|
@@ -435,5 +435,5 @@ class TestFactoryFunctions:
|
|
| 435 |
assert graph.entry_node == "planner"
|
| 436 |
assert "synthesizer" in graph.exit_nodes
|
| 437 |
assert "planner" in graph.nodes
|
| 438 |
-
assert "
|
| 439 |
assert "synthesizer" in graph.nodes
|
|
|
|
| 240 |
def test_validate_empty_graph(self):
|
| 241 |
"""Test validating an empty graph."""
|
| 242 |
graph = ResearchGraph(entry_node="start", exit_nodes=["end"])
|
| 243 |
+
errors = graph.validate_structure()
|
| 244 |
assert len(errors) > 0 # Should have errors for missing entry/exit nodes
|
| 245 |
|
| 246 |
def test_validate_valid_graph(self):
|
|
|
|
| 252 |
graph.add_node(end_node)
|
| 253 |
graph.add_edge(SequentialEdge(from_node="start", to_node="end"))
|
| 254 |
|
| 255 |
+
errors = graph.validate_structure()
|
| 256 |
assert len(errors) == 0
|
| 257 |
|
| 258 |
def test_validate_unreachable_nodes(self):
|
|
|
|
| 266 |
graph.add_node(unreachable)
|
| 267 |
graph.add_edge(SequentialEdge(from_node="start", to_node="end"))
|
| 268 |
|
| 269 |
+
errors = graph.validate_structure()
|
| 270 |
assert len(errors) > 0
|
| 271 |
assert any("unreachable" in error.lower() for error in errors)
|
| 272 |
|
|
|
|
| 435 |
assert graph.entry_node == "planner"
|
| 436 |
assert "synthesizer" in graph.exit_nodes
|
| 437 |
assert "planner" in graph.nodes
|
| 438 |
+
assert "parallel_loops" in graph.nodes
|
| 439 |
assert "synthesizer" in graph.nodes
|
tests/unit/agent_factory/test_judges_factory.py
CHANGED
|
@@ -10,7 +10,7 @@ from pydantic_ai.models.anthropic import AnthropicModel
|
|
| 10 |
# We expect this import to exist after we implement it, or we mock it if it's not there yet
|
| 11 |
# For TDD, we assume we will use the library class
|
| 12 |
from pydantic_ai.models.huggingface import HuggingFaceModel
|
| 13 |
-
from pydantic_ai.models.openai import OpenAIModel
|
| 14 |
|
| 15 |
from src.agent_factory.judges import get_model
|
| 16 |
|
|
|
|
| 10 |
# We expect this import to exist after we implement it, or we mock it if it's not there yet
|
| 11 |
# For TDD, we assume we will use the library class
|
| 12 |
from pydantic_ai.models.huggingface import HuggingFaceModel
|
| 13 |
+
from pydantic_ai.models.openai import OpenAIChatModel as OpenAIModel
|
| 14 |
|
| 15 |
from src.agent_factory.judges import get_model
|
| 16 |
|
tests/unit/agents/test_long_writer.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
-
from pydantic_ai import
|
| 7 |
|
| 8 |
from src.agents.long_writer import LongWriterAgent, LongWriterOutput, create_long_writer_agent
|
| 9 |
from src.utils.models import ReportDraft, ReportDraftSection
|
|
@@ -27,9 +27,11 @@ def mock_long_writer_output() -> LongWriterOutput:
|
|
| 27 |
|
| 28 |
|
| 29 |
@pytest.fixture
|
| 30 |
-
def mock_agent_result(
|
|
|
|
|
|
|
| 31 |
"""Create a mock agent result."""
|
| 32 |
-
result = MagicMock(spec=
|
| 33 |
result.output = mock_long_writer_output
|
| 34 |
return result
|
| 35 |
|
|
@@ -90,7 +92,7 @@ class TestWriteNextSection:
|
|
| 90 |
async def test_write_next_section_basic(
|
| 91 |
self,
|
| 92 |
long_writer_agent: LongWriterAgent,
|
| 93 |
-
mock_agent_result:
|
| 94 |
) -> None:
|
| 95 |
"""Test basic section writing."""
|
| 96 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -116,7 +118,7 @@ class TestWriteNextSection:
|
|
| 116 |
async def test_write_next_section_first_section(
|
| 117 |
self,
|
| 118 |
long_writer_agent: LongWriterAgent,
|
| 119 |
-
mock_agent_result:
|
| 120 |
) -> None:
|
| 121 |
"""Test writing the first section (no existing draft)."""
|
| 122 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -142,7 +144,7 @@ class TestWriteNextSection:
|
|
| 142 |
async def test_write_next_section_with_existing_draft(
|
| 143 |
self,
|
| 144 |
long_writer_agent: LongWriterAgent,
|
| 145 |
-
mock_agent_result:
|
| 146 |
) -> None:
|
| 147 |
"""Test writing section with existing draft."""
|
| 148 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -168,7 +170,7 @@ class TestWriteNextSection:
|
|
| 168 |
async def test_write_next_section_returns_references(
|
| 169 |
self,
|
| 170 |
long_writer_agent: LongWriterAgent,
|
| 171 |
-
mock_agent_result:
|
| 172 |
) -> None:
|
| 173 |
"""Test that write_next_section returns references."""
|
| 174 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -187,7 +189,7 @@ class TestWriteNextSection:
|
|
| 187 |
async def test_write_next_section_handles_empty_draft(
|
| 188 |
self,
|
| 189 |
long_writer_agent: LongWriterAgent,
|
| 190 |
-
mock_agent_result:
|
| 191 |
) -> None:
|
| 192 |
"""Test writing section with empty draft."""
|
| 193 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -226,7 +228,7 @@ class TestWriteReport:
|
|
| 226 |
async def test_write_report_complete_flow(
|
| 227 |
self,
|
| 228 |
long_writer_agent: LongWriterAgent,
|
| 229 |
-
mock_agent_result:
|
| 230 |
sample_report_draft: ReportDraft,
|
| 231 |
) -> None:
|
| 232 |
"""Test complete report writing flow."""
|
|
@@ -253,7 +255,7 @@ class TestWriteReport:
|
|
| 253 |
async def test_write_report_single_section(
|
| 254 |
self,
|
| 255 |
long_writer_agent: LongWriterAgent,
|
| 256 |
-
mock_agent_result:
|
| 257 |
) -> None:
|
| 258 |
"""Test writing report with single section."""
|
| 259 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -281,7 +283,7 @@ class TestWriteReport:
|
|
| 281 |
async def test_write_report_multiple_sections(
|
| 282 |
self,
|
| 283 |
long_writer_agent: LongWriterAgent,
|
| 284 |
-
mock_agent_result:
|
| 285 |
sample_report_draft: ReportDraft,
|
| 286 |
) -> None:
|
| 287 |
"""Test writing report with multiple sections."""
|
|
@@ -302,7 +304,7 @@ class TestWriteReport:
|
|
| 302 |
async def test_write_report_creates_table_of_contents(
|
| 303 |
self,
|
| 304 |
long_writer_agent: LongWriterAgent,
|
| 305 |
-
mock_agent_result:
|
| 306 |
sample_report_draft: ReportDraft,
|
| 307 |
) -> None:
|
| 308 |
"""Test that write_report creates table of contents."""
|
|
@@ -335,7 +337,11 @@ class TestWriteReport:
|
|
| 335 |
references=["[1] https://example.com/2"],
|
| 336 |
)
|
| 337 |
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
long_writer_agent.agent.run = AsyncMock(side_effect=results)
|
| 340 |
|
| 341 |
result = await long_writer_agent.write_report(
|
|
|
|
| 3 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
+
from pydantic_ai import AgentRunResult
|
| 7 |
|
| 8 |
from src.agents.long_writer import LongWriterAgent, LongWriterOutput, create_long_writer_agent
|
| 9 |
from src.utils.models import ReportDraft, ReportDraftSection
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
@pytest.fixture
|
| 30 |
+
def mock_agent_result(
|
| 31 |
+
mock_long_writer_output: LongWriterOutput,
|
| 32 |
+
) -> AgentRunResult[LongWriterOutput]:
|
| 33 |
"""Create a mock agent result."""
|
| 34 |
+
result = MagicMock(spec=AgentRunResult)
|
| 35 |
result.output = mock_long_writer_output
|
| 36 |
return result
|
| 37 |
|
|
|
|
| 92 |
async def test_write_next_section_basic(
|
| 93 |
self,
|
| 94 |
long_writer_agent: LongWriterAgent,
|
| 95 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 96 |
) -> None:
|
| 97 |
"""Test basic section writing."""
|
| 98 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 118 |
async def test_write_next_section_first_section(
|
| 119 |
self,
|
| 120 |
long_writer_agent: LongWriterAgent,
|
| 121 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 122 |
) -> None:
|
| 123 |
"""Test writing the first section (no existing draft)."""
|
| 124 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 144 |
async def test_write_next_section_with_existing_draft(
|
| 145 |
self,
|
| 146 |
long_writer_agent: LongWriterAgent,
|
| 147 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 148 |
) -> None:
|
| 149 |
"""Test writing section with existing draft."""
|
| 150 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 170 |
async def test_write_next_section_returns_references(
|
| 171 |
self,
|
| 172 |
long_writer_agent: LongWriterAgent,
|
| 173 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 174 |
) -> None:
|
| 175 |
"""Test that write_next_section returns references."""
|
| 176 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 189 |
async def test_write_next_section_handles_empty_draft(
|
| 190 |
self,
|
| 191 |
long_writer_agent: LongWriterAgent,
|
| 192 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 193 |
) -> None:
|
| 194 |
"""Test writing section with empty draft."""
|
| 195 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 228 |
async def test_write_report_complete_flow(
|
| 229 |
self,
|
| 230 |
long_writer_agent: LongWriterAgent,
|
| 231 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 232 |
sample_report_draft: ReportDraft,
|
| 233 |
) -> None:
|
| 234 |
"""Test complete report writing flow."""
|
|
|
|
| 255 |
async def test_write_report_single_section(
|
| 256 |
self,
|
| 257 |
long_writer_agent: LongWriterAgent,
|
| 258 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 259 |
) -> None:
|
| 260 |
"""Test writing report with single section."""
|
| 261 |
long_writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 283 |
async def test_write_report_multiple_sections(
|
| 284 |
self,
|
| 285 |
long_writer_agent: LongWriterAgent,
|
| 286 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 287 |
sample_report_draft: ReportDraft,
|
| 288 |
) -> None:
|
| 289 |
"""Test writing report with multiple sections."""
|
|
|
|
| 304 |
async def test_write_report_creates_table_of_contents(
|
| 305 |
self,
|
| 306 |
long_writer_agent: LongWriterAgent,
|
| 307 |
+
mock_agent_result: AgentRunResult[LongWriterOutput],
|
| 308 |
sample_report_draft: ReportDraft,
|
| 309 |
) -> None:
|
| 310 |
"""Test that write_report creates table of contents."""
|
|
|
|
| 337 |
references=["[1] https://example.com/2"],
|
| 338 |
)
|
| 339 |
|
| 340 |
+
result1 = MagicMock(spec=AgentRunResult)
|
| 341 |
+
result1.output = output1
|
| 342 |
+
result2 = MagicMock(spec=AgentRunResult)
|
| 343 |
+
result2.output = output2
|
| 344 |
+
results = [result1, result2]
|
| 345 |
long_writer_agent.agent.run = AsyncMock(side_effect=results)
|
| 346 |
|
| 347 |
result = await long_writer_agent.write_report(
|
tests/unit/agents/test_proofreader.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
| 4 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
-
from pydantic_ai import
|
| 8 |
|
| 9 |
from src.agents.proofreader import ProofreaderAgent, create_proofreader_agent
|
| 10 |
from src.utils.models import ReportDraft, ReportDraftSection
|
|
@@ -19,9 +19,9 @@ def mock_model() -> MagicMock:
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
-
def mock_agent_result() ->
|
| 23 |
"""Create a mock agent result."""
|
| 24 |
-
result = MagicMock(spec=
|
| 25 |
result.output = """# Final Report
|
| 26 |
|
| 27 |
## Summary
|
|
@@ -82,10 +82,13 @@ class TestProofreaderAgentInit:
|
|
| 82 |
self, proofreader_agent: ProofreaderAgent
|
| 83 |
) -> None:
|
| 84 |
"""Test that ProofreaderAgent has correct system prompt."""
|
| 85 |
-
# System prompt should contain key instructions
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
|
| 91 |
class TestProofread:
|
|
@@ -95,7 +98,7 @@ class TestProofread:
|
|
| 95 |
async def test_proofread_basic(
|
| 96 |
self,
|
| 97 |
proofreader_agent: ProofreaderAgent,
|
| 98 |
-
mock_agent_result:
|
| 99 |
sample_report_draft: ReportDraft,
|
| 100 |
) -> None:
|
| 101 |
"""Test basic proofreading."""
|
|
@@ -112,7 +115,7 @@ class TestProofread:
|
|
| 112 |
async def test_proofread_single_section(
|
| 113 |
self,
|
| 114 |
proofreader_agent: ProofreaderAgent,
|
| 115 |
-
mock_agent_result:
|
| 116 |
) -> None:
|
| 117 |
"""Test proofreading with single section."""
|
| 118 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -135,7 +138,7 @@ class TestProofread:
|
|
| 135 |
async def test_proofread_multiple_sections(
|
| 136 |
self,
|
| 137 |
proofreader_agent: ProofreaderAgent,
|
| 138 |
-
mock_agent_result:
|
| 139 |
sample_report_draft: ReportDraft,
|
| 140 |
) -> None:
|
| 141 |
"""Test proofreading with multiple sections."""
|
|
@@ -152,7 +155,7 @@ class TestProofread:
|
|
| 152 |
async def test_proofread_removes_duplicates(
|
| 153 |
self,
|
| 154 |
proofreader_agent: ProofreaderAgent,
|
| 155 |
-
mock_agent_result:
|
| 156 |
) -> None:
|
| 157 |
"""Test that proofreader removes duplicate content."""
|
| 158 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -181,7 +184,7 @@ class TestProofread:
|
|
| 181 |
async def test_proofread_adds_summary(
|
| 182 |
self,
|
| 183 |
proofreader_agent: ProofreaderAgent,
|
| 184 |
-
mock_agent_result:
|
| 185 |
sample_report_draft: ReportDraft,
|
| 186 |
) -> None:
|
| 187 |
"""Test that proofreader adds summary."""
|
|
@@ -190,15 +193,16 @@ class TestProofread:
|
|
| 190 |
result = await proofreader_agent.proofread(query="Test", report_draft=sample_report_draft)
|
| 191 |
|
| 192 |
assert isinstance(result, str)
|
| 193 |
-
# System prompt should instruct to add summary
|
| 194 |
-
|
| 195 |
-
|
|
|
|
| 196 |
|
| 197 |
@pytest.mark.asyncio
|
| 198 |
async def test_proofread_preserves_references(
|
| 199 |
self,
|
| 200 |
proofreader_agent: ProofreaderAgent,
|
| 201 |
-
mock_agent_result:
|
| 202 |
sample_report_draft: ReportDraft,
|
| 203 |
) -> None:
|
| 204 |
"""Test that proofreader preserves references."""
|
|
@@ -207,15 +211,20 @@ class TestProofread:
|
|
| 207 |
result = await proofreader_agent.proofread(query="Test", report_draft=sample_report_draft)
|
| 208 |
|
| 209 |
assert isinstance(result, str)
|
| 210 |
-
# System prompt should instruct to preserve sources
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
@pytest.mark.asyncio
|
| 215 |
async def test_proofread_empty_draft(
|
| 216 |
self,
|
| 217 |
proofreader_agent: ProofreaderAgent,
|
| 218 |
-
mock_agent_result:
|
| 219 |
) -> None:
|
| 220 |
"""Test proofreading with empty draft."""
|
| 221 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -225,13 +234,17 @@ class TestProofread:
|
|
| 225 |
result = await proofreader_agent.proofread(query="Test", report_draft=report_draft)
|
| 226 |
|
| 227 |
assert isinstance(result, str)
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
@pytest.mark.asyncio
|
| 231 |
async def test_proofread_single_section_draft(
|
| 232 |
self,
|
| 233 |
proofreader_agent: ProofreaderAgent,
|
| 234 |
-
mock_agent_result:
|
| 235 |
) -> None:
|
| 236 |
"""Test proofreading with single section draft."""
|
| 237 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -253,7 +266,7 @@ class TestProofread:
|
|
| 253 |
async def test_proofread_very_long_draft(
|
| 254 |
self,
|
| 255 |
proofreader_agent: ProofreaderAgent,
|
| 256 |
-
mock_agent_result:
|
| 257 |
) -> None:
|
| 258 |
"""Test proofreading with very long draft."""
|
| 259 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -276,7 +289,7 @@ class TestProofread:
|
|
| 276 |
async def test_proofread_malformed_sections(
|
| 277 |
self,
|
| 278 |
proofreader_agent: ProofreaderAgent,
|
| 279 |
-
mock_agent_result:
|
| 280 |
) -> None:
|
| 281 |
"""Test proofreading with malformed sections."""
|
| 282 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 4 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
+
from pydantic_ai import AgentRunResult
|
| 8 |
|
| 9 |
from src.agents.proofreader import ProofreaderAgent, create_proofreader_agent
|
| 10 |
from src.utils.models import ReportDraft, ReportDraftSection
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
+
def mock_agent_result() -> AgentRunResult[Any]:
|
| 23 |
"""Create a mock agent result."""
|
| 24 |
+
result = MagicMock(spec=AgentRunResult)
|
| 25 |
result.output = """# Final Report
|
| 26 |
|
| 27 |
## Summary
|
|
|
|
| 82 |
self, proofreader_agent: ProofreaderAgent
|
| 83 |
) -> None:
|
| 84 |
"""Test that ProofreaderAgent has correct system prompt."""
|
| 85 |
+
# System prompt should exist and contain key instructions
|
| 86 |
+
# Check the source constant directly since system_prompt property may be a callable
|
| 87 |
+
from src.agents.proofreader import SYSTEM_PROMPT
|
| 88 |
+
|
| 89 |
+
assert SYSTEM_PROMPT is not None
|
| 90 |
+
assert "proofread" in SYSTEM_PROMPT.lower()
|
| 91 |
+
assert "report" in SYSTEM_PROMPT.lower()
|
| 92 |
|
| 93 |
|
| 94 |
class TestProofread:
|
|
|
|
| 98 |
async def test_proofread_basic(
|
| 99 |
self,
|
| 100 |
proofreader_agent: ProofreaderAgent,
|
| 101 |
+
mock_agent_result: AgentRunResult[Any],
|
| 102 |
sample_report_draft: ReportDraft,
|
| 103 |
) -> None:
|
| 104 |
"""Test basic proofreading."""
|
|
|
|
| 115 |
async def test_proofread_single_section(
|
| 116 |
self,
|
| 117 |
proofreader_agent: ProofreaderAgent,
|
| 118 |
+
mock_agent_result: AgentRunResult[Any],
|
| 119 |
) -> None:
|
| 120 |
"""Test proofreading with single section."""
|
| 121 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 138 |
async def test_proofread_multiple_sections(
|
| 139 |
self,
|
| 140 |
proofreader_agent: ProofreaderAgent,
|
| 141 |
+
mock_agent_result: AgentRunResult[Any],
|
| 142 |
sample_report_draft: ReportDraft,
|
| 143 |
) -> None:
|
| 144 |
"""Test proofreading with multiple sections."""
|
|
|
|
| 155 |
async def test_proofread_removes_duplicates(
|
| 156 |
self,
|
| 157 |
proofreader_agent: ProofreaderAgent,
|
| 158 |
+
mock_agent_result: AgentRunResult[Any],
|
| 159 |
) -> None:
|
| 160 |
"""Test that proofreader removes duplicate content."""
|
| 161 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 184 |
async def test_proofread_adds_summary(
|
| 185 |
self,
|
| 186 |
proofreader_agent: ProofreaderAgent,
|
| 187 |
+
mock_agent_result: AgentRunResult[Any],
|
| 188 |
sample_report_draft: ReportDraft,
|
| 189 |
) -> None:
|
| 190 |
"""Test that proofreader adds summary."""
|
|
|
|
| 193 |
result = await proofreader_agent.proofread(query="Test", report_draft=sample_report_draft)
|
| 194 |
|
| 195 |
assert isinstance(result, str)
|
| 196 |
+
# System prompt should instruct to add summary - check source constant
|
| 197 |
+
from src.agents.proofreader import SYSTEM_PROMPT
|
| 198 |
+
|
| 199 |
+
assert "summary" in SYSTEM_PROMPT.lower() or "Add a summary" in SYSTEM_PROMPT
|
| 200 |
|
| 201 |
@pytest.mark.asyncio
|
| 202 |
async def test_proofread_preserves_references(
|
| 203 |
self,
|
| 204 |
proofreader_agent: ProofreaderAgent,
|
| 205 |
+
mock_agent_result: AgentRunResult[Any],
|
| 206 |
sample_report_draft: ReportDraft,
|
| 207 |
) -> None:
|
| 208 |
"""Test that proofreader preserves references."""
|
|
|
|
| 211 |
result = await proofreader_agent.proofread(query="Test", report_draft=sample_report_draft)
|
| 212 |
|
| 213 |
assert isinstance(result, str)
|
| 214 |
+
# System prompt should instruct to preserve sources - check source constant
|
| 215 |
+
from src.agents.proofreader import SYSTEM_PROMPT
|
| 216 |
+
|
| 217 |
+
assert (
|
| 218 |
+
"sources" in SYSTEM_PROMPT.lower()
|
| 219 |
+
or "references" in SYSTEM_PROMPT.lower()
|
| 220 |
+
or "Preserve sources" in SYSTEM_PROMPT
|
| 221 |
+
)
|
| 222 |
|
| 223 |
@pytest.mark.asyncio
|
| 224 |
async def test_proofread_empty_draft(
|
| 225 |
self,
|
| 226 |
proofreader_agent: ProofreaderAgent,
|
| 227 |
+
mock_agent_result: AgentRunResult[Any],
|
| 228 |
) -> None:
|
| 229 |
"""Test proofreading with empty draft."""
|
| 230 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 234 |
result = await proofreader_agent.proofread(query="Test", report_draft=report_draft)
|
| 235 |
|
| 236 |
assert isinstance(result, str)
|
| 237 |
+
# When draft is empty, agent returns early without calling run
|
| 238 |
+
assert "Research Report" in result
|
| 239 |
+
assert "Query" in result
|
| 240 |
+
# Agent.run should not be called for empty drafts (early return)
|
| 241 |
+
assert not proofreader_agent.agent.run.called
|
| 242 |
|
| 243 |
@pytest.mark.asyncio
|
| 244 |
async def test_proofread_single_section_draft(
|
| 245 |
self,
|
| 246 |
proofreader_agent: ProofreaderAgent,
|
| 247 |
+
mock_agent_result: AgentRunResult[Any],
|
| 248 |
) -> None:
|
| 249 |
"""Test proofreading with single section draft."""
|
| 250 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 266 |
async def test_proofread_very_long_draft(
|
| 267 |
self,
|
| 268 |
proofreader_agent: ProofreaderAgent,
|
| 269 |
+
mock_agent_result: AgentRunResult[Any],
|
| 270 |
) -> None:
|
| 271 |
"""Test proofreading with very long draft."""
|
| 272 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 289 |
async def test_proofread_malformed_sections(
|
| 290 |
self,
|
| 291 |
proofreader_agent: ProofreaderAgent,
|
| 292 |
+
mock_agent_result: AgentRunResult[Any],
|
| 293 |
) -> None:
|
| 294 |
"""Test proofreading with malformed sections."""
|
| 295 |
proofreader_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
tests/unit/agents/test_writer.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
| 4 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
-
from pydantic_ai import
|
| 8 |
|
| 9 |
from src.agents.writer import WriterAgent, create_writer_agent
|
| 10 |
from src.utils.exceptions import ConfigurationError
|
|
@@ -19,9 +19,9 @@ def mock_model() -> MagicMock:
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
-
def mock_agent_result() ->
|
| 23 |
"""Create a mock agent result."""
|
| 24 |
-
result = MagicMock(spec=
|
| 25 |
result.output = "# Research Report\n\nThis is a test report with citations [1].\n\nReferences:\n[1] https://example.com"
|
| 26 |
return result
|
| 27 |
|
|
@@ -53,10 +53,13 @@ class TestWriterAgentInit:
|
|
| 53 |
|
| 54 |
def test_writer_agent_has_correct_system_prompt(self, writer_agent: WriterAgent) -> None:
|
| 55 |
"""Test that WriterAgent has correct system prompt."""
|
| 56 |
-
# System prompt should contain key instructions
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
class TestWriteReport:
|
|
@@ -64,7 +67,7 @@ class TestWriteReport:
|
|
| 64 |
|
| 65 |
@pytest.mark.asyncio
|
| 66 |
async def test_write_report_basic(
|
| 67 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 68 |
) -> None:
|
| 69 |
"""Test basic report writing."""
|
| 70 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -80,7 +83,7 @@ class TestWriteReport:
|
|
| 80 |
|
| 81 |
@pytest.mark.asyncio
|
| 82 |
async def test_write_report_with_output_length(
|
| 83 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 84 |
) -> None:
|
| 85 |
"""Test report writing with output length specification."""
|
| 86 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -100,7 +103,7 @@ class TestWriteReport:
|
|
| 100 |
|
| 101 |
@pytest.mark.asyncio
|
| 102 |
async def test_write_report_with_instructions(
|
| 103 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 104 |
) -> None:
|
| 105 |
"""Test report writing with additional instructions."""
|
| 106 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -120,7 +123,7 @@ class TestWriteReport:
|
|
| 120 |
|
| 121 |
@pytest.mark.asyncio
|
| 122 |
async def test_write_report_with_citations(
|
| 123 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 124 |
) -> None:
|
| 125 |
"""Test report writing includes citations."""
|
| 126 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -135,7 +138,7 @@ class TestWriteReport:
|
|
| 135 |
|
| 136 |
@pytest.mark.asyncio
|
| 137 |
async def test_write_report_empty_findings(
|
| 138 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 139 |
) -> None:
|
| 140 |
"""Test report writing with empty findings."""
|
| 141 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -150,7 +153,7 @@ class TestWriteReport:
|
|
| 150 |
|
| 151 |
@pytest.mark.asyncio
|
| 152 |
async def test_write_report_very_long_findings(
|
| 153 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 154 |
) -> None:
|
| 155 |
"""Test report writing with very long findings."""
|
| 156 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
@@ -165,7 +168,7 @@ class TestWriteReport:
|
|
| 165 |
|
| 166 |
@pytest.mark.asyncio
|
| 167 |
async def test_write_report_special_characters(
|
| 168 |
-
self, writer_agent: WriterAgent, mock_agent_result:
|
| 169 |
) -> None:
|
| 170 |
"""Test report writing with special characters in findings."""
|
| 171 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 4 |
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
+
from pydantic_ai import AgentRunResult
|
| 8 |
|
| 9 |
from src.agents.writer import WriterAgent, create_writer_agent
|
| 10 |
from src.utils.exceptions import ConfigurationError
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
+
def mock_agent_result() -> AgentRunResult[Any]:
|
| 23 |
"""Create a mock agent result."""
|
| 24 |
+
result = MagicMock(spec=AgentRunResult)
|
| 25 |
result.output = "# Research Report\n\nThis is a test report with citations [1].\n\nReferences:\n[1] https://example.com"
|
| 26 |
return result
|
| 27 |
|
|
|
|
| 53 |
|
| 54 |
def test_writer_agent_has_correct_system_prompt(self, writer_agent: WriterAgent) -> None:
|
| 55 |
"""Test that WriterAgent has correct system prompt."""
|
| 56 |
+
# System prompt should exist and contain key instructions
|
| 57 |
+
# Check the source constant directly since system_prompt property may be a callable
|
| 58 |
+
from src.agents.writer import SYSTEM_PROMPT
|
| 59 |
+
|
| 60 |
+
assert SYSTEM_PROMPT is not None
|
| 61 |
+
assert "researcher" in SYSTEM_PROMPT.lower()
|
| 62 |
+
assert "markdown" in SYSTEM_PROMPT.lower()
|
| 63 |
|
| 64 |
|
| 65 |
class TestWriteReport:
|
|
|
|
| 67 |
|
| 68 |
@pytest.mark.asyncio
|
| 69 |
async def test_write_report_basic(
|
| 70 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 71 |
) -> None:
|
| 72 |
"""Test basic report writing."""
|
| 73 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 83 |
|
| 84 |
@pytest.mark.asyncio
|
| 85 |
async def test_write_report_with_output_length(
|
| 86 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 87 |
) -> None:
|
| 88 |
"""Test report writing with output length specification."""
|
| 89 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 103 |
|
| 104 |
@pytest.mark.asyncio
|
| 105 |
async def test_write_report_with_instructions(
|
| 106 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 107 |
) -> None:
|
| 108 |
"""Test report writing with additional instructions."""
|
| 109 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 123 |
|
| 124 |
@pytest.mark.asyncio
|
| 125 |
async def test_write_report_with_citations(
|
| 126 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 127 |
) -> None:
|
| 128 |
"""Test report writing includes citations."""
|
| 129 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 138 |
|
| 139 |
@pytest.mark.asyncio
|
| 140 |
async def test_write_report_empty_findings(
|
| 141 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 142 |
) -> None:
|
| 143 |
"""Test report writing with empty findings."""
|
| 144 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 153 |
|
| 154 |
@pytest.mark.asyncio
|
| 155 |
async def test_write_report_very_long_findings(
|
| 156 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 157 |
) -> None:
|
| 158 |
"""Test report writing with very long findings."""
|
| 159 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
|
|
|
| 168 |
|
| 169 |
@pytest.mark.asyncio
|
| 170 |
async def test_write_report_special_characters(
|
| 171 |
+
self, writer_agent: WriterAgent, mock_agent_result: AgentRunResult[Any]
|
| 172 |
) -> None:
|
| 173 |
"""Test report writing with special characters in findings."""
|
| 174 |
writer_agent.agent.run = AsyncMock(return_value=mock_agent_result)
|
tests/unit/orchestrator/test_graph_orchestrator.py
CHANGED
|
@@ -89,16 +89,18 @@ class TestGraphOrchestrator:
|
|
| 89 |
assert orchestrator._iterative_flow is None
|
| 90 |
assert orchestrator._deep_flow is None
|
| 91 |
|
| 92 |
-
|
|
|
|
| 93 |
"""Test detecting deep research mode from query."""
|
| 94 |
orchestrator = GraphOrchestrator(mode="auto")
|
| 95 |
-
mode = orchestrator._detect_research_mode("Create a report with sections about X")
|
| 96 |
assert mode == "deep"
|
| 97 |
|
| 98 |
-
|
|
|
|
| 99 |
"""Test detecting iterative research mode from query."""
|
| 100 |
orchestrator = GraphOrchestrator(mode="auto")
|
| 101 |
-
mode = orchestrator._detect_research_mode("What is the mechanism of action?")
|
| 102 |
assert mode == "iterative"
|
| 103 |
|
| 104 |
@pytest.mark.asyncio
|
|
@@ -200,18 +202,52 @@ class TestGraphOrchestrator:
|
|
| 200 |
max_time_minutes=5,
|
| 201 |
use_graph=False,
|
| 202 |
)
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
mock_flow.run = AsyncMock(side_effect=Exception("Test error"))
|
| 207 |
-
mock_flow_class.return_value = mock_flow
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
events = []
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
error_events = [e for e in events if e.type == "error"]
|
| 214 |
-
assert
|
|
|
|
|
|
|
| 215 |
assert (
|
| 216 |
"error" in error_events[0].message.lower()
|
| 217 |
or "failed" in error_events[0].message.lower()
|
|
|
|
| 89 |
assert orchestrator._iterative_flow is None
|
| 90 |
assert orchestrator._deep_flow is None
|
| 91 |
|
| 92 |
+
@pytest.mark.asyncio
|
| 93 |
+
async def test_detect_research_mode_deep(self):
|
| 94 |
"""Test detecting deep research mode from query."""
|
| 95 |
orchestrator = GraphOrchestrator(mode="auto")
|
| 96 |
+
mode = await orchestrator._detect_research_mode("Create a report with sections about X")
|
| 97 |
assert mode == "deep"
|
| 98 |
|
| 99 |
+
@pytest.mark.asyncio
|
| 100 |
+
async def test_detect_research_mode_iterative(self):
|
| 101 |
"""Test detecting iterative research mode from query."""
|
| 102 |
orchestrator = GraphOrchestrator(mode="auto")
|
| 103 |
+
mode = await orchestrator._detect_research_mode("What is the mechanism of action?")
|
| 104 |
assert mode == "iterative"
|
| 105 |
|
| 106 |
@pytest.mark.asyncio
|
|
|
|
| 202 |
max_time_minutes=5,
|
| 203 |
use_graph=False,
|
| 204 |
)
|
| 205 |
+
# Ensure flow is None so it gets created fresh
|
| 206 |
+
orchestrator._iterative_flow = None
|
| 207 |
|
| 208 |
+
# Create the flow first, then patch its run method
|
| 209 |
+
from src.orchestrator.research_flow import IterativeResearchFlow
|
|
|
|
|
|
|
| 210 |
|
| 211 |
+
# Create flow and patch its run method to raise exception
|
| 212 |
+
original_flow = IterativeResearchFlow(
|
| 213 |
+
max_iterations=2,
|
| 214 |
+
max_time_minutes=5,
|
| 215 |
+
)
|
| 216 |
+
orchestrator._iterative_flow = original_flow
|
| 217 |
+
|
| 218 |
+
with patch.object(original_flow, "run", side_effect=Exception("Test error")):
|
| 219 |
events = []
|
| 220 |
+
# Collect events manually to ensure we get error events even when exception occurs
|
| 221 |
+
gen = orchestrator.run("Test query")
|
| 222 |
+
while True:
|
| 223 |
+
try:
|
| 224 |
+
event = await gen.__anext__()
|
| 225 |
+
events.append(event)
|
| 226 |
+
# If we got an error event, continue to see if outer handler also yields one
|
| 227 |
+
if event.type == "error":
|
| 228 |
+
# Try to get outer handler's error event too
|
| 229 |
+
try:
|
| 230 |
+
next_event = await gen.__anext__()
|
| 231 |
+
events.append(next_event)
|
| 232 |
+
except (StopAsyncIteration, Exception):
|
| 233 |
+
break
|
| 234 |
+
break
|
| 235 |
+
except StopAsyncIteration:
|
| 236 |
+
break
|
| 237 |
+
except Exception:
|
| 238 |
+
# Exception occurred - outer handler should yield error event
|
| 239 |
+
# Try to get it
|
| 240 |
+
try:
|
| 241 |
+
event = await gen.__anext__()
|
| 242 |
+
events.append(event)
|
| 243 |
+
except (StopAsyncIteration, Exception):
|
| 244 |
+
break
|
| 245 |
+
break
|
| 246 |
|
| 247 |
error_events = [e for e in events if e.type == "error"]
|
| 248 |
+
assert (
|
| 249 |
+
len(error_events) > 0
|
| 250 |
+
), f"No error events found. Events: {[e.type for e in events]}"
|
| 251 |
assert (
|
| 252 |
"error" in error_events[0].message.lower()
|
| 253 |
or "failed" in error_events[0].message.lower()
|
tests/unit/orchestrator/test_planner_agent.py
CHANGED
|
@@ -39,7 +39,7 @@ class TestPlannerAgent:
|
|
| 39 |
@pytest.mark.asyncio
|
| 40 |
async def test_planner_agent_creates_report_plan(self, mock_model, mock_agent_run_result):
|
| 41 |
"""PlannerAgent should create a valid ReportPlan."""
|
| 42 |
-
with patch("src.orchestrator.planner_agent.
|
| 43 |
mock_get_model.return_value = mock_model
|
| 44 |
|
| 45 |
mock_agent = AsyncMock()
|
|
@@ -72,7 +72,7 @@ class TestPlannerAgent:
|
|
| 72 |
mock_agent = AsyncMock()
|
| 73 |
mock_agent.run = AsyncMock(return_value=mock_result)
|
| 74 |
|
| 75 |
-
with patch("src.orchestrator.planner_agent.
|
| 76 |
mock_get_model.return_value = mock_model
|
| 77 |
|
| 78 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
@@ -94,7 +94,7 @@ class TestPlannerAgent:
|
|
| 94 |
mock_agent = AsyncMock()
|
| 95 |
mock_agent.run = AsyncMock(side_effect=Exception("API Error"))
|
| 96 |
|
| 97 |
-
with patch("src.orchestrator.planner_agent.
|
| 98 |
mock_get_model.return_value = mock_model
|
| 99 |
|
| 100 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
@@ -108,10 +108,9 @@ class TestPlannerAgent:
|
|
| 108 |
# Should return fallback plan
|
| 109 |
assert isinstance(result, ReportPlan)
|
| 110 |
assert len(result.report_outline) > 0
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
)
|
| 115 |
|
| 116 |
@pytest.mark.asyncio
|
| 117 |
async def test_planner_agent_uses_tools(self, mock_model, mock_agent_run_result):
|
|
@@ -119,7 +118,7 @@ class TestPlannerAgent:
|
|
| 119 |
mock_agent = AsyncMock()
|
| 120 |
mock_agent.run = AsyncMock(return_value=mock_agent_run_result)
|
| 121 |
|
| 122 |
-
with patch("src.orchestrator.planner_agent.
|
| 123 |
mock_get_model.return_value = mock_model
|
| 124 |
|
| 125 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
@@ -139,7 +138,7 @@ class TestPlannerAgent:
|
|
| 139 |
@pytest.mark.asyncio
|
| 140 |
async def test_create_planner_agent_factory(self, mock_model):
|
| 141 |
"""create_planner_agent should create a PlannerAgent instance."""
|
| 142 |
-
with patch("src.orchestrator.planner_agent.
|
| 143 |
mock_get_model.return_value = mock_model
|
| 144 |
|
| 145 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
@@ -155,7 +154,7 @@ class TestPlannerAgent:
|
|
| 155 |
"""create_planner_agent should use default model when None provided."""
|
| 156 |
mock_model = MagicMock()
|
| 157 |
|
| 158 |
-
with patch("src.orchestrator.planner_agent.
|
| 159 |
mock_get_model.return_value = mock_model
|
| 160 |
|
| 161 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
|
|
| 39 |
@pytest.mark.asyncio
|
| 40 |
async def test_planner_agent_creates_report_plan(self, mock_model, mock_agent_run_result):
|
| 41 |
"""PlannerAgent should create a valid ReportPlan."""
|
| 42 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 43 |
mock_get_model.return_value = mock_model
|
| 44 |
|
| 45 |
mock_agent = AsyncMock()
|
|
|
|
| 72 |
mock_agent = AsyncMock()
|
| 73 |
mock_agent.run = AsyncMock(return_value=mock_result)
|
| 74 |
|
| 75 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 76 |
mock_get_model.return_value = mock_model
|
| 77 |
|
| 78 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
|
|
| 94 |
mock_agent = AsyncMock()
|
| 95 |
mock_agent.run = AsyncMock(side_effect=Exception("API Error"))
|
| 96 |
|
| 97 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 98 |
mock_get_model.return_value = mock_model
|
| 99 |
|
| 100 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
|
|
| 108 |
# Should return fallback plan
|
| 109 |
assert isinstance(result, ReportPlan)
|
| 110 |
assert len(result.report_outline) > 0
|
| 111 |
+
# Fallback plan has title "Research Findings" and empty background_context
|
| 112 |
+
assert result.report_outline[0].title == "Research Findings"
|
| 113 |
+
assert result.background_context == ""
|
|
|
|
| 114 |
|
| 115 |
@pytest.mark.asyncio
|
| 116 |
async def test_planner_agent_uses_tools(self, mock_model, mock_agent_run_result):
|
|
|
|
| 118 |
mock_agent = AsyncMock()
|
| 119 |
mock_agent.run = AsyncMock(return_value=mock_agent_run_result)
|
| 120 |
|
| 121 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 122 |
mock_get_model.return_value = mock_model
|
| 123 |
|
| 124 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
|
|
| 138 |
@pytest.mark.asyncio
|
| 139 |
async def test_create_planner_agent_factory(self, mock_model):
|
| 140 |
"""create_planner_agent should create a PlannerAgent instance."""
|
| 141 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 142 |
mock_get_model.return_value = mock_model
|
| 143 |
|
| 144 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
|
|
|
| 154 |
"""create_planner_agent should use default model when None provided."""
|
| 155 |
mock_model = MagicMock()
|
| 156 |
|
| 157 |
+
with patch("src.orchestrator.planner_agent.get_model") as mock_get_model:
|
| 158 |
mock_get_model.return_value = mock_model
|
| 159 |
|
| 160 |
with patch("src.orchestrator.planner_agent.Agent") as mock_agent_class:
|
tests/unit/orchestrator/test_research_flow.py
CHANGED
|
@@ -46,7 +46,7 @@ class TestIterativeResearchFlow:
|
|
| 46 |
"task_1": ToolAgentOutput(output="Finding 1", sources=["url1"]),
|
| 47 |
}
|
| 48 |
|
| 49 |
-
|
| 50 |
|
| 51 |
@pytest.mark.asyncio
|
| 52 |
async def test_iterative_flow_completes_when_research_complete(self, flow, mock_agents):
|
|
@@ -208,7 +208,7 @@ class TestDeepResearchFlow:
|
|
| 208 |
mock_long_writer.return_value = mock_agents["long_writer"]
|
| 209 |
mock_proofreader.return_value = mock_agents["proofreader"]
|
| 210 |
|
| 211 |
-
|
| 212 |
|
| 213 |
@pytest.mark.asyncio
|
| 214 |
async def test_deep_flow_creates_report_plan(self, flow, mock_agents):
|
|
|
|
| 46 |
"task_1": ToolAgentOutput(output="Finding 1", sources=["url1"]),
|
| 47 |
}
|
| 48 |
|
| 49 |
+
yield IterativeResearchFlow(max_iterations=2, max_time_minutes=5)
|
| 50 |
|
| 51 |
@pytest.mark.asyncio
|
| 52 |
async def test_iterative_flow_completes_when_research_complete(self, flow, mock_agents):
|
|
|
|
| 208 |
mock_long_writer.return_value = mock_agents["long_writer"]
|
| 209 |
mock_proofreader.return_value = mock_agents["proofreader"]
|
| 210 |
|
| 211 |
+
yield DeepResearchFlow(max_iterations=2, max_time_minutes=5)
|
| 212 |
|
| 213 |
@pytest.mark.asyncio
|
| 214 |
async def test_deep_flow_creates_report_plan(self, flow, mock_agents):
|
tests/unit/services/test_embeddings.py
CHANGED
|
@@ -6,8 +6,16 @@ import numpy as np
|
|
| 6 |
import pytest
|
| 7 |
|
| 8 |
# Skip if embeddings dependencies are not installed
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
from src.services.embeddings import EmbeddingService
|
| 13 |
|
|
|
|
| 6 |
import pytest
|
| 7 |
|
| 8 |
# Skip if embeddings dependencies are not installed
|
| 9 |
+
# Handle Windows-specific scipy import issues
|
| 10 |
+
try:
|
| 11 |
+
pytest.importorskip("chromadb")
|
| 12 |
+
pytest.importorskip("sentence_transformers")
|
| 13 |
+
except OSError:
|
| 14 |
+
# On Windows, scipy import can fail with OSError during collection
|
| 15 |
+
# Skip the entire test module in this case
|
| 16 |
+
pytest.skip(
|
| 17 |
+
"Embeddings dependencies not available (scipy import issue)", allow_module_level=True
|
| 18 |
+
)
|
| 19 |
|
| 20 |
from src.services.embeddings import EmbeddingService
|
| 21 |
|
tests/unit/test_no_webtool_references.py
CHANGED
|
@@ -9,7 +9,11 @@ def test_examples_no_webtool_imports():
|
|
| 9 |
examples_dir = pathlib.Path("examples")
|
| 10 |
|
| 11 |
for py_file in examples_dir.rglob("*.py"):
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
tree = ast.parse(content)
|
| 14 |
|
| 15 |
for node in ast.walk(tree):
|
|
|
|
| 9 |
examples_dir = pathlib.Path("examples")
|
| 10 |
|
| 11 |
for py_file in examples_dir.rglob("*.py"):
|
| 12 |
+
try:
|
| 13 |
+
content = py_file.read_text(encoding="utf-8")
|
| 14 |
+
except UnicodeDecodeError:
|
| 15 |
+
# Skip files that can't be decoded as UTF-8
|
| 16 |
+
continue
|
| 17 |
tree = ast.parse(content)
|
| 18 |
|
| 19 |
for node in ast.walk(tree):
|
tests/unit/tools/test_pubmed.py
CHANGED
|
@@ -142,23 +142,40 @@ class TestPubMedTool:
|
|
| 142 |
|
| 143 |
mocker.patch("httpx.AsyncClient", return_value=mock_client)
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
tool = PubMedTool()
|
| 146 |
-
# Reset
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
#
|
| 150 |
-
with patch("asyncio.
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
@pytest.mark.asyncio
|
| 164 |
async def test_api_key_included_in_params(self, mocker):
|
|
|
|
| 142 |
|
| 143 |
mocker.patch("httpx.AsyncClient", return_value=mock_client)
|
| 144 |
|
| 145 |
+
from src.tools.rate_limiter import reset_pubmed_limiter
|
| 146 |
+
|
| 147 |
+
# Reset the rate limiter to ensure clean state
|
| 148 |
+
reset_pubmed_limiter()
|
| 149 |
+
|
| 150 |
+
mock_search_response = MagicMock()
|
| 151 |
+
mock_search_response.json.return_value = {"esearchresult": {"idlist": []}}
|
| 152 |
+
mock_search_response.raise_for_status = MagicMock()
|
| 153 |
+
mock_client = AsyncMock()
|
| 154 |
+
mock_client.get = AsyncMock(return_value=mock_search_response)
|
| 155 |
+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
| 156 |
+
mock_client.__aexit__ = AsyncMock(return_value=None)
|
| 157 |
+
mocker.patch("httpx.AsyncClient", return_value=mock_client)
|
| 158 |
+
|
| 159 |
tool = PubMedTool()
|
| 160 |
+
tool._limiter.reset() # Reset storage to start fresh
|
| 161 |
+
|
| 162 |
+
# For 3 requests/second rate limit, we need to make 4 requests quickly to trigger the limit
|
| 163 |
+
# Make first 3 requests - should all succeed without sleep (within rate limit)
|
| 164 |
+
with patch("asyncio.sleep") as mock_sleep_first:
|
| 165 |
+
for i in range(3):
|
| 166 |
+
await tool.search(f"test query {i+1}")
|
| 167 |
+
# First 3 requests should not sleep (within 3/second limit)
|
| 168 |
+
assert mock_sleep_first.call_count == 0
|
| 169 |
+
|
| 170 |
+
# Make 4th request immediately - should trigger rate limit
|
| 171 |
+
# For 3 requests/second, the 4th request should wait
|
| 172 |
+
with patch("asyncio.sleep") as mock_sleep:
|
| 173 |
+
await tool.search("test query 4")
|
| 174 |
+
# Rate limiter uses polling with 0.01s sleep, so sleep should be called
|
| 175 |
+
# multiple times until enough time has passed (at least once)
|
| 176 |
+
assert (
|
| 177 |
+
mock_sleep.call_count > 0
|
| 178 |
+
), f"Rate limiter should call sleep when rate limit is hit. Call count: {mock_sleep.call_count}"
|
| 179 |
|
| 180 |
@pytest.mark.asyncio
|
| 181 |
async def test_api_key_included_in_params(self, mocker):
|
tests/unit/tools/test_rag_tool.py
CHANGED
|
@@ -148,7 +148,7 @@ class TestRAGTool:
|
|
| 148 |
@pytest.mark.asyncio
|
| 149 |
async def test_search_lazy_initialization_success(self):
|
| 150 |
"""RAGTool should lazy-initialize RAG service when needed."""
|
| 151 |
-
with patch("src.
|
| 152 |
mock_service = MagicMock()
|
| 153 |
mock_service.retrieve.return_value = [
|
| 154 |
{
|
|
@@ -173,7 +173,7 @@ class TestRAGTool:
|
|
| 173 |
@pytest.mark.asyncio
|
| 174 |
async def test_search_lazy_initialization_failure(self):
|
| 175 |
"""RAGTool should return empty list if RAG service unavailable."""
|
| 176 |
-
with patch("src.
|
| 177 |
mock_get_service.side_effect = ConfigurationError("OPENAI_API_KEY required")
|
| 178 |
|
| 179 |
tool = RAGTool(rag_service=None)
|
|
|
|
| 148 |
@pytest.mark.asyncio
|
| 149 |
async def test_search_lazy_initialization_success(self):
|
| 150 |
"""RAGTool should lazy-initialize RAG service when needed."""
|
| 151 |
+
with patch("src.services.llamaindex_rag.get_rag_service") as mock_get_service:
|
| 152 |
mock_service = MagicMock()
|
| 153 |
mock_service.retrieve.return_value = [
|
| 154 |
{
|
|
|
|
| 173 |
@pytest.mark.asyncio
|
| 174 |
async def test_search_lazy_initialization_failure(self):
|
| 175 |
"""RAGTool should return empty list if RAG service unavailable."""
|
| 176 |
+
with patch("src.services.llamaindex_rag.get_rag_service") as mock_get_service:
|
| 177 |
mock_get_service.side_effect = ConfigurationError("OPENAI_API_KEY required")
|
| 178 |
|
| 179 |
tool = RAGTool(rag_service=None)
|