Spaces:
Running
Running
Joseph Pollack
commited on
adds file returns , configuration enhancements , oauth fixes , and interface fixes
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- FILE_OUTPUT_IMPLEMENTATION_PLAN.md +237 -0
- REPORT_WRITING_AGENTS_ANALYSIS.md +2 -0
- SERPER_WEBSEARCH_IMPLEMENTATION_PLAN.md +2 -0
- docs/api/agents.md +2 -0
- docs/api/models.md +2 -0
- docs/api/orchestrators.md +2 -0
- docs/api/services.md +2 -0
- docs/api/tools.md +2 -0
- docs/architecture/agents.md +2 -0
- docs/architecture/middleware.md +2 -0
- docs/architecture/services.md +2 -0
- docs/architecture/tools.md +2 -0
- docs/contributing/code-quality.md +2 -0
- docs/contributing/code-style.md +2 -0
- docs/contributing/error-handling.md +2 -0
- docs/contributing/implementation-patterns.md +2 -0
- docs/contributing/index.md +2 -0
- docs/contributing/prompt-engineering.md +2 -0
- docs/contributing/testing.md +2 -0
- docs/getting-started/examples.md +2 -0
- docs/getting-started/installation.md +2 -0
- docs/getting-started/mcp-integration.md +2 -0
- docs/getting-started/quick-start.md +2 -0
- docs/implementation/IMPLEMENTATION_SUMMARY.md +2 -0
- docs/implementation/TTS_MODAL_IMPLEMENTATION.md +2 -0
- docs/license.md +2 -0
- docs/overview/architecture.md +2 -0
- docs/overview/features.md +2 -0
- docs/team.md +2 -0
- new_env.txt +2 -0
- src/agent_factory/judges.py +45 -18
- src/app.py +8 -1
- src/middleware/state_machine.py +2 -0
- src/orchestrator/graph_orchestrator.py +40 -0
- src/orchestrator/research_flow.py +63 -0
- src/services/image_ocr.py +2 -0
- src/services/report_file_service.py +269 -0
- src/tools/crawl_adapter.py +2 -0
- src/tools/searchxng_web_search.py +2 -0
- src/tools/serper_web_search.py +2 -0
- src/tools/vendored/__init__.py +2 -0
- src/tools/vendored/searchxng_client.py +2 -0
- src/tools/vendored/serper_client.py +2 -0
- src/tools/vendored/web_search_core.py +2 -0
- src/tools/web_search_factory.py +2 -0
- src/utils/config.py +18 -0
- tests/unit/middleware/__init__.py +2 -0
- tests/unit/middleware/test_budget_tracker_phase7.py +2 -0
- tests/unit/middleware/test_state_machine.py +2 -0
- tests/unit/middleware/test_workflow_manager.py +2 -0
FILE_OUTPUT_IMPLEMENTATION_PLAN.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File Output Implementation Plan
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This plan implements file writing and return functionality for report-writing agents, enabling reports to be saved as files and returned through the Gradio ChatInterface.
|
| 6 |
+
|
| 7 |
+
## Current State Analysis
|
| 8 |
+
|
| 9 |
+
✅ **Report Generation**: All agents generate markdown strings
|
| 10 |
+
✅ **File Output Integration**: `event_to_chat_message()` supports file paths
|
| 11 |
+
✅ **Graph Orchestrator**: Can handle file paths in results
|
| 12 |
+
❌ **File Writing**: No agents write files to disk
|
| 13 |
+
❌ **File Service**: No utility service for saving reports
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Implementation Plan
|
| 18 |
+
|
| 19 |
+
### PROJECT 1: File Writing Service
|
| 20 |
+
**Goal**: Create a reusable service for saving reports to files
|
| 21 |
+
|
| 22 |
+
#### Activity 1.1: Create Report File Service
|
| 23 |
+
**File**: `src/services/report_file_service.py` (NEW)
|
| 24 |
+
|
| 25 |
+
**Tasks**:
|
| 26 |
+
1. Create `ReportFileService` class
|
| 27 |
+
2. Implement `save_report()` method
|
| 28 |
+
- Accepts: report content (str), filename (optional), output_dir (optional)
|
| 29 |
+
- Returns: file path (str)
|
| 30 |
+
- Uses temp directory by default
|
| 31 |
+
- Supports custom output directory
|
| 32 |
+
- Handles file naming with timestamps
|
| 33 |
+
3. Implement `save_report_multiple_formats()` method
|
| 34 |
+
- Save as .md (always)
|
| 35 |
+
- Optionally save as .html, .pdf (future)
|
| 36 |
+
4. Add configuration support
|
| 37 |
+
- Read from settings
|
| 38 |
+
- Enable/disable file saving
|
| 39 |
+
- Configurable output directory
|
| 40 |
+
5. Add error handling and logging
|
| 41 |
+
6. Add file cleanup utilities (optional)
|
| 42 |
+
|
| 43 |
+
**Line-level subtasks**:
|
| 44 |
+
- Line 1-20: Imports and class definition
|
| 45 |
+
- Line 21-40: `__init__()` method with settings
|
| 46 |
+
- Line 41-80: `save_report()` method
|
| 47 |
+
- Line 41-50: Input validation
|
| 48 |
+
- Line 51-60: Directory creation
|
| 49 |
+
- Line 61-70: File writing
|
| 50 |
+
- Line 71-80: Error handling
|
| 51 |
+
- Line 81-100: `save_report_multiple_formats()` method
|
| 52 |
+
- Line 101-120: Helper methods (filename generation, cleanup)
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
### PROJECT 2: Configuration Updates
|
| 57 |
+
**Goal**: Add settings for file output functionality
|
| 58 |
+
|
| 59 |
+
#### Activity 2.1: Update Settings Model
|
| 60 |
+
**File**: `src/utils/config.py`
|
| 61 |
+
|
| 62 |
+
**Tasks**:
|
| 63 |
+
1. Add `save_reports_to_file: bool` field (default: True)
|
| 64 |
+
2. Add `report_output_directory: str | None` field (default: None, uses temp)
|
| 65 |
+
3. Add `report_file_format: Literal["md", "md_html", "md_pdf"]` field (default: "md")
|
| 66 |
+
4. Add `report_filename_template: str` field (default: "report_{timestamp}_{query_hash}.md")
|
| 67 |
+
|
| 68 |
+
**Line-level subtasks**:
|
| 69 |
+
- Line 166-170: Add `save_reports_to_file` field after TTS config
|
| 70 |
+
- Line 171-175: Add `report_output_directory` field
|
| 71 |
+
- Line 176-180: Add `report_file_format` field
|
| 72 |
+
- Line 181-185: Add `report_filename_template` field
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
### PROJECT 3: Graph Orchestrator Integration
|
| 77 |
+
**Goal**: Integrate file writing into graph execution
|
| 78 |
+
|
| 79 |
+
#### Activity 3.1: Update Graph Orchestrator
|
| 80 |
+
**File**: `src/orchestrator/graph_orchestrator.py`
|
| 81 |
+
|
| 82 |
+
**Tasks**:
|
| 83 |
+
1. Import `ReportFileService` at top
|
| 84 |
+
2. Initialize service in `__init__()` (optional, can be lazy)
|
| 85 |
+
3. Modify `_execute_agent_node()` for synthesizer node
|
| 86 |
+
- After `long_writer_agent.write_report()`, save to file
|
| 87 |
+
- Return dict with `{"message": report, "file": file_path}`
|
| 88 |
+
4. Update final event generation to handle file paths
|
| 89 |
+
- Already implemented, verify it works correctly
|
| 90 |
+
|
| 91 |
+
**Line-level subtasks**:
|
| 92 |
+
- Line 1-35: Add import for `ReportFileService`
|
| 93 |
+
- Line 119-148: Update `__init__()` to accept optional file service
|
| 94 |
+
- Line 589-650: Modify `_execute_agent_node()` synthesizer handling
|
| 95 |
+
- Line 642-645: After `write_report()`, add file saving
|
| 96 |
+
- Line 646-650: Return dict with file path
|
| 97 |
+
- Line 534-564: Verify final event generation handles file paths (already done)
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
### PROJECT 4: Research Flow Integration
|
| 102 |
+
**Goal**: Integrate file writing into research flows
|
| 103 |
+
|
| 104 |
+
#### Activity 4.1: Update IterativeResearchFlow
|
| 105 |
+
**File**: `src/orchestrator/research_flow.py`
|
| 106 |
+
|
| 107 |
+
**Tasks**:
|
| 108 |
+
1. Import `ReportFileService` at top
|
| 109 |
+
2. Add optional file service to `__init__()`
|
| 110 |
+
3. Modify `_create_final_report()` method
|
| 111 |
+
- After `writer_agent.write_report()`, save to file if enabled
|
| 112 |
+
- Return string (backward compatible) OR dict with file path
|
| 113 |
+
|
| 114 |
+
**Line-level subtasks**:
|
| 115 |
+
- Line 1-50: Add import for `ReportFileService`
|
| 116 |
+
- Line 48-120: Update `__init__()` to accept optional file service
|
| 117 |
+
- Line 622-667: Modify `_create_final_report()` method
|
| 118 |
+
- Line 647-652: After `write_report()`, add file saving
|
| 119 |
+
- Line 653-667: Return report string (keep backward compatible for now)
|
| 120 |
+
|
| 121 |
+
#### Activity 4.2: Update DeepResearchFlow
|
| 122 |
+
**File**: `src/orchestrator/research_flow.py`
|
| 123 |
+
|
| 124 |
+
**Tasks**:
|
| 125 |
+
1. Add optional file service to `__init__()` (if not already)
|
| 126 |
+
2. Modify `_create_final_report()` method
|
| 127 |
+
- After `long_writer_agent.write_report()` or `proofreader_agent.proofread()`, save to file
|
| 128 |
+
- Return string (backward compatible) OR dict with file path
|
| 129 |
+
|
| 130 |
+
**Line-level subtasks**:
|
| 131 |
+
- Line 670-750: Update `DeepResearchFlow.__init__()` to accept optional file service
|
| 132 |
+
- Line 954-1005: Modify `_create_final_report()` method
|
| 133 |
+
- Line 979-983: After `write_report()`, add file saving
|
| 134 |
+
- Line 984-989: After `proofread()`, add file saving
|
| 135 |
+
- Line 990-1005: Return report string (keep backward compatible)
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
### PROJECT 5: Agent Factory Integration
|
| 140 |
+
**Goal**: Make file service available to agents if needed
|
| 141 |
+
|
| 142 |
+
#### Activity 5.1: Update Agent Factory (Optional)
|
| 143 |
+
**File**: `src/agent_factory/agents.py`
|
| 144 |
+
|
| 145 |
+
**Tasks**:
|
| 146 |
+
1. Add optional file service parameter to agent creation functions (if needed)
|
| 147 |
+
2. Pass file service to agents that need it (currently not needed, agents return strings)
|
| 148 |
+
|
| 149 |
+
**Line-level subtasks**:
|
| 150 |
+
- Not required - agents return strings, file writing happens at orchestrator level
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
### PROJECT 6: Testing & Validation
|
| 155 |
+
**Goal**: Ensure file output works end-to-end
|
| 156 |
+
|
| 157 |
+
#### Activity 6.1: Unit Tests
|
| 158 |
+
**File**: `tests/unit/services/test_report_file_service.py` (NEW)
|
| 159 |
+
|
| 160 |
+
**Tasks**:
|
| 161 |
+
1. Test `save_report()` with default settings
|
| 162 |
+
2. Test `save_report()` with custom directory
|
| 163 |
+
3. Test `save_report()` with custom filename
|
| 164 |
+
4. Test error handling (permission errors, disk full, etc.)
|
| 165 |
+
5. Test file cleanup
|
| 166 |
+
|
| 167 |
+
**Line-level subtasks**:
|
| 168 |
+
- Line 1-30: Test fixtures and setup
|
| 169 |
+
- Line 31-60: Test basic save functionality
|
| 170 |
+
- Line 61-90: Test custom directory
|
| 171 |
+
- Line 91-120: Test error handling
|
| 172 |
+
|
| 173 |
+
#### Activity 6.2: Integration Tests
|
| 174 |
+
**File**: `tests/integration/test_file_output_integration.py` (NEW)
|
| 175 |
+
|
| 176 |
+
**Tasks**:
|
| 177 |
+
1. Test graph orchestrator with file output
|
| 178 |
+
2. Test research flows with file output
|
| 179 |
+
3. Test Gradio ChatInterface receives file paths
|
| 180 |
+
4. Test file download in Gradio UI
|
| 181 |
+
|
| 182 |
+
**Line-level subtasks**:
|
| 183 |
+
- Line 1-40: Test setup with mock orchestrator
|
| 184 |
+
- Line 41-80: Test file generation in graph execution
|
| 185 |
+
- Line 81-120: Test file paths in AgentEvent
|
| 186 |
+
- Line 121-160: Test Gradio message conversion
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
## Implementation Order
|
| 191 |
+
|
| 192 |
+
1. **PROJECT 2** (Configuration) - Foundation
|
| 193 |
+
2. **PROJECT 1** (File Service) - Core functionality
|
| 194 |
+
3. **PROJECT 3** (Graph Orchestrator) - Primary integration point
|
| 195 |
+
4. **PROJECT 4** (Research Flows) - Secondary integration points
|
| 196 |
+
5. **PROJECT 6** (Testing) - Validation
|
| 197 |
+
6. **PROJECT 5** (Agent Factory) - Not needed, skip
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
## File Changes Summary
|
| 202 |
+
|
| 203 |
+
### New Files
|
| 204 |
+
- `src/services/report_file_service.py` - File writing service
|
| 205 |
+
- `tests/unit/services/test_report_file_service.py` - Unit tests
|
| 206 |
+
- `tests/integration/test_file_output_integration.py` - Integration tests
|
| 207 |
+
|
| 208 |
+
### Modified Files
|
| 209 |
+
- `src/utils/config.py` - Add file output settings
|
| 210 |
+
- `src/orchestrator/graph_orchestrator.py` - Add file saving after report generation
|
| 211 |
+
- `src/orchestrator/research_flow.py` - Add file saving in both flows
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## Gradio Integration Notes
|
| 216 |
+
|
| 217 |
+
According to Gradio ChatInterface documentation:
|
| 218 |
+
- File paths in chat message content are automatically converted to download links
|
| 219 |
+
- Markdown links like `[Download: filename](file_path)` work
|
| 220 |
+
- Files must be accessible from the Gradio server
|
| 221 |
+
- Temp files are fine as long as they exist during the session
|
| 222 |
+
|
| 223 |
+
Current implementation in `event_to_chat_message()` already handles this correctly.
|
| 224 |
+
|
| 225 |
+
---
|
| 226 |
+
|
| 227 |
+
## Success Criteria
|
| 228 |
+
|
| 229 |
+
✅ Reports are saved to files when generated
|
| 230 |
+
✅ File paths are included in AgentEvent data
|
| 231 |
+
✅ File paths appear as download links in Gradio ChatInterface
|
| 232 |
+
✅ File saving is configurable (can be disabled)
|
| 233 |
+
✅ Backward compatible (existing code still works)
|
| 234 |
+
✅ Error handling prevents crashes if file writing fails
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
|
REPORT_WRITING_AGENTS_ANALYSIS.md
CHANGED
|
@@ -181,3 +181,5 @@ return {
|
|
| 181 |
The infrastructure to handle file outputs in Gradio is in place, but the agents themselves do not yet write files. They would need to be enhanced or wrapped to add file writing capability.
|
| 182 |
|
| 183 |
|
|
|
|
|
|
|
|
|
| 181 |
The infrastructure to handle file outputs in Gradio is in place, but the agents themselves do not yet write files. They would need to be enhanced or wrapped to add file writing capability.
|
| 182 |
|
| 183 |
|
| 184 |
+
|
| 185 |
+
|
SERPER_WEBSEARCH_IMPLEMENTATION_PLAN.md
CHANGED
|
@@ -395,3 +395,5 @@ This plan details the implementation of SERPER-based web search by vendoring cod
|
|
| 395 |
- Consider adding relevance scoring in the future
|
| 396 |
|
| 397 |
|
|
|
|
|
|
|
|
|
| 395 |
- Consider adding relevance scoring in the future
|
| 396 |
|
| 397 |
|
| 398 |
+
|
| 399 |
+
|
docs/api/agents.md
CHANGED
|
@@ -272,3 +272,5 @@ def create_input_parser_agent(model: Any | None = None) -> InputParserAgent
|
|
| 272 |
|
| 273 |
|
| 274 |
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
|
| 274 |
|
| 275 |
+
|
| 276 |
+
|
docs/api/models.md
CHANGED
|
@@ -250,3 +250,5 @@ class BudgetStatus(BaseModel):
|
|
| 250 |
|
| 251 |
|
| 252 |
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
|
| 252 |
|
| 253 |
+
|
| 254 |
+
|
docs/api/orchestrators.md
CHANGED
|
@@ -197,3 +197,5 @@ Runs Magentic orchestration.
|
|
| 197 |
|
| 198 |
|
| 199 |
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
|
| 199 |
|
| 200 |
+
|
| 201 |
+
|
docs/api/services.md
CHANGED
|
@@ -203,3 +203,5 @@ Analyzes a hypothesis using statistical methods.
|
|
| 203 |
|
| 204 |
|
| 205 |
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
|
| 206 |
+
|
| 207 |
+
|
docs/api/tools.md
CHANGED
|
@@ -237,3 +237,5 @@ Searches multiple tools in parallel.
|
|
| 237 |
|
| 238 |
|
| 239 |
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
|
| 239 |
|
| 240 |
+
|
| 241 |
+
|
docs/architecture/agents.md
CHANGED
|
@@ -194,3 +194,5 @@ Factory functions:
|
|
| 194 |
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
|
| 197 |
+
|
| 198 |
+
|
docs/architecture/middleware.md
CHANGED
|
@@ -144,3 +144,5 @@ All middleware components use `ContextVar` for thread-safe isolation:
|
|
| 144 |
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
|
| 146 |
|
| 147 |
+
|
| 148 |
+
|
docs/architecture/services.md
CHANGED
|
@@ -144,3 +144,5 @@ if settings.has_openai_key:
|
|
| 144 |
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
|
| 146 |
|
| 147 |
+
|
| 148 |
+
|
docs/architecture/tools.md
CHANGED
|
@@ -177,3 +177,5 @@ search_handler = SearchHandler(
|
|
| 177 |
|
| 178 |
|
| 179 |
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
|
| 179 |
|
| 180 |
+
|
| 181 |
+
|
docs/contributing/code-quality.md
CHANGED
|
@@ -83,3 +83,5 @@ async def search(self, query: str, max_results: int = 10) -> list[Evidence]:
|
|
| 83 |
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
|
| 86 |
+
|
| 87 |
+
|
docs/contributing/code-style.md
CHANGED
|
@@ -63,3 +63,5 @@ result = await loop.run_in_executor(None, cpu_bound_function, args)
|
|
| 63 |
|
| 64 |
|
| 65 |
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
|
| 66 |
+
|
| 67 |
+
|
docs/contributing/error-handling.md
CHANGED
|
@@ -71,3 +71,5 @@ except httpx.HTTPError as e:
|
|
| 71 |
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
+
|
| 75 |
+
|
docs/contributing/implementation-patterns.md
CHANGED
|
@@ -86,3 +86,5 @@ def get_embedding_service() -> EmbeddingService:
|
|
| 86 |
|
| 87 |
|
| 88 |
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
+
|
| 90 |
+
|
docs/contributing/index.md
CHANGED
|
@@ -165,3 +165,5 @@ Thank you for contributing to DeepCritical!
|
|
| 165 |
|
| 166 |
|
| 167 |
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
|
| 167 |
|
| 168 |
+
|
| 169 |
+
|
docs/contributing/prompt-engineering.md
CHANGED
|
@@ -71,3 +71,5 @@ This document outlines prompt engineering guidelines and citation validation rul
|
|
| 71 |
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
+
|
| 75 |
+
|
docs/contributing/testing.md
CHANGED
|
@@ -67,3 +67,5 @@ async def test_real_pubmed_search():
|
|
| 67 |
|
| 68 |
|
| 69 |
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
|
| 70 |
+
|
| 71 |
+
|
docs/getting-started/examples.md
CHANGED
|
@@ -211,3 +211,5 @@ USE_GRAPH_EXECUTION=true
|
|
| 211 |
|
| 212 |
|
| 213 |
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
|
| 214 |
+
|
| 215 |
+
|
docs/getting-started/installation.md
CHANGED
|
@@ -150,3 +150,5 @@ uv run pre-commit install
|
|
| 150 |
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
|
| 152 |
|
| 153 |
+
|
| 154 |
+
|
docs/getting-started/mcp-integration.md
CHANGED
|
@@ -217,3 +217,5 @@ You can configure multiple DeepCritical instances:
|
|
| 217 |
|
| 218 |
|
| 219 |
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
|
| 219 |
|
| 220 |
+
|
| 221 |
+
|
docs/getting-started/quick-start.md
CHANGED
|
@@ -121,3 +121,5 @@ What are the active clinical trials investigating Alzheimer's disease treatments
|
|
| 121 |
|
| 122 |
|
| 123 |
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
+
|
| 125 |
+
|
docs/implementation/IMPLEMENTATION_SUMMARY.md
CHANGED
|
@@ -180,3 +180,5 @@ Located in `src/app.py` lines 667-712:
|
|
| 180 |
|
| 181 |
|
| 182 |
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
|
| 183 |
+
|
| 184 |
+
|
docs/implementation/TTS_MODAL_IMPLEMENTATION.md
CHANGED
|
@@ -134,3 +134,5 @@ To test TTS:
|
|
| 134 |
|
| 135 |
|
| 136 |
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
+
|
| 138 |
+
|
docs/license.md
CHANGED
|
@@ -41,3 +41,5 @@ SOFTWARE.
|
|
| 41 |
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
|
| 44 |
+
|
| 45 |
+
|
docs/overview/architecture.md
CHANGED
|
@@ -198,3 +198,5 @@ The system supports complex research workflows through:
|
|
| 198 |
|
| 199 |
|
| 200 |
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
|
| 200 |
|
| 201 |
+
|
| 202 |
+
|
docs/overview/features.md
CHANGED
|
@@ -150,3 +150,5 @@ DeepCritical provides a comprehensive set of features for AI-assisted research:
|
|
| 150 |
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
|
| 152 |
|
| 153 |
+
|
| 154 |
+
|
docs/team.md
CHANGED
|
@@ -46,3 +46,5 @@ We welcome contributions! See the [Contributing Guide](contributing/index.md) fo
|
|
| 46 |
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
|
| 49 |
+
|
| 50 |
+
|
new_env.txt
CHANGED
|
@@ -96,3 +96,5 @@ MODAL_TOKEN_SECRET=your_modal_token_secret_here
|
|
| 96 |
|
| 97 |
|
| 98 |
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
+
|
| 100 |
+
|
src/agent_factory/judges.py
CHANGED
|
@@ -33,34 +33,61 @@ def get_model(oauth_token: str | None = None) -> Any:
|
|
| 33 |
Explicitly passes API keys from settings to avoid requiring
|
| 34 |
users to export environment variables manually.
|
| 35 |
|
| 36 |
-
Priority:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
This ensures users logged in via HuggingFace Spaces get the free tier.
|
| 38 |
|
| 39 |
Args:
|
| 40 |
oauth_token: Optional OAuth token from HuggingFace login (takes priority over env vars)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
"""
|
| 42 |
# Priority: oauth_token > settings.hf_token > settings.huggingface_api_key
|
| 43 |
effective_hf_token = oauth_token or settings.hf_token or settings.huggingface_api_key
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
if
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
"
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
)
|
| 63 |
-
return HuggingFaceModel(model_name, provider=hf_provider)
|
| 64 |
|
| 65 |
|
| 66 |
class JudgeHandler:
|
|
|
|
| 33 |
Explicitly passes API keys from settings to avoid requiring
|
| 34 |
users to export environment variables manually.
|
| 35 |
|
| 36 |
+
Priority order:
|
| 37 |
+
1. HuggingFace (if OAuth token or API key available - preferred for free tier)
|
| 38 |
+
2. OpenAI (if API key available)
|
| 39 |
+
3. Anthropic (if API key available)
|
| 40 |
+
|
| 41 |
+
If OAuth token is available, prefer HuggingFace (even if provider is set to OpenAI).
|
| 42 |
This ensures users logged in via HuggingFace Spaces get the free tier.
|
| 43 |
|
| 44 |
Args:
|
| 45 |
oauth_token: Optional OAuth token from HuggingFace login (takes priority over env vars)
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
Configured Pydantic AI model
|
| 49 |
+
|
| 50 |
+
Raises:
|
| 51 |
+
ConfigurationError: If no LLM provider is available
|
| 52 |
"""
|
| 53 |
# Priority: oauth_token > settings.hf_token > settings.huggingface_api_key
|
| 54 |
effective_hf_token = oauth_token or settings.hf_token or settings.huggingface_api_key
|
| 55 |
|
| 56 |
+
# Try HuggingFace first (preferred for free tier)
|
| 57 |
+
if effective_hf_token:
|
| 58 |
+
model_name = settings.huggingface_model or "meta-llama/Llama-3.1-8B-Instruct"
|
| 59 |
+
hf_provider = HuggingFaceProvider(api_key=effective_hf_token)
|
| 60 |
+
logger.info(
|
| 61 |
+
"using_huggingface_with_token",
|
| 62 |
+
has_oauth=bool(oauth_token),
|
| 63 |
+
has_settings_token=bool(settings.hf_token or settings.huggingface_api_key),
|
| 64 |
+
model=model_name,
|
| 65 |
)
|
| 66 |
+
return HuggingFaceModel(model_name, provider=hf_provider)
|
| 67 |
+
|
| 68 |
+
# Fallback to OpenAI if available
|
| 69 |
+
if settings.has_openai_key:
|
| 70 |
+
assert settings.openai_api_key is not None # Type narrowing
|
| 71 |
+
model_name = settings.openai_model
|
| 72 |
+
openai_provider = OpenAIProvider(api_key=settings.openai_api_key)
|
| 73 |
+
logger.info("using_openai", model=model_name)
|
| 74 |
+
return OpenAIModel(model_name, provider=openai_provider)
|
| 75 |
+
|
| 76 |
+
# Fallback to Anthropic if available
|
| 77 |
+
if settings.has_anthropic_key:
|
| 78 |
+
assert settings.anthropic_api_key is not None # Type narrowing
|
| 79 |
+
model_name = settings.anthropic_model
|
| 80 |
+
anthropic_provider = AnthropicProvider(api_key=settings.anthropic_api_key)
|
| 81 |
+
logger.info("using_anthropic", model=model_name)
|
| 82 |
+
return AnthropicModel(model_name, provider=anthropic_provider)
|
| 83 |
+
|
| 84 |
+
# No provider available
|
| 85 |
+
raise ConfigurationError(
|
| 86 |
+
"No LLM provider available. Please configure one of:\n"
|
| 87 |
+
"1. HuggingFace: Log in via OAuth (recommended for Spaces) or set HF_TOKEN\n"
|
| 88 |
+
"2. OpenAI: Set OPENAI_API_KEY environment variable\n"
|
| 89 |
+
"3. Anthropic: Set ANTHROPIC_API_KEY environment variable"
|
| 90 |
)
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
class JudgeHandler:
|
src/app.py
CHANGED
|
@@ -158,6 +158,7 @@ def configure_orchestrator(
|
|
| 158 |
judge_handler=judge_handler,
|
| 159 |
config=config,
|
| 160 |
mode=effective_mode, # type: ignore
|
|
|
|
| 161 |
)
|
| 162 |
|
| 163 |
return orchestrator, backend_info
|
|
@@ -570,7 +571,13 @@ async def research_agent(
|
|
| 570 |
|
| 571 |
if oauth_token is not None:
|
| 572 |
# OAuthToken has a .token attribute containing the access token
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
|
| 575 |
if oauth_profile is not None:
|
| 576 |
# OAuthProfile has .username, .name, .profile_image attributes
|
|
|
|
| 158 |
judge_handler=judge_handler,
|
| 159 |
config=config,
|
| 160 |
mode=effective_mode, # type: ignore
|
| 161 |
+
oauth_token=oauth_token,
|
| 162 |
)
|
| 163 |
|
| 164 |
return orchestrator, backend_info
|
|
|
|
| 571 |
|
| 572 |
if oauth_token is not None:
|
| 573 |
# OAuthToken has a .token attribute containing the access token
|
| 574 |
+
if hasattr(oauth_token, "token"):
|
| 575 |
+
token_value = oauth_token.token
|
| 576 |
+
elif isinstance(oauth_token, str):
|
| 577 |
+
# Handle case where oauth_token is already a string (shouldn't happen but defensive)
|
| 578 |
+
token_value = oauth_token
|
| 579 |
+
else:
|
| 580 |
+
token_value = None
|
| 581 |
|
| 582 |
if oauth_profile is not None:
|
| 583 |
# OAuthProfile has .username, .name, .profile_image attributes
|
src/middleware/state_machine.py
CHANGED
|
@@ -135,3 +135,5 @@ def get_workflow_state() -> WorkflowState:
|
|
| 135 |
|
| 136 |
|
| 137 |
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
+
|
| 139 |
+
|
src/orchestrator/graph_orchestrator.py
CHANGED
|
@@ -32,6 +32,7 @@ from src.legacy_orchestrator import JudgeHandlerProtocol, SearchHandlerProtocol
|
|
| 32 |
from src.middleware.budget_tracker import BudgetTracker
|
| 33 |
from src.middleware.state_machine import WorkflowState, init_workflow_state
|
| 34 |
from src.orchestrator.research_flow import DeepResearchFlow, IterativeResearchFlow
|
|
|
|
| 35 |
from src.utils.models import AgentEvent
|
| 36 |
|
| 37 |
if TYPE_CHECKING:
|
|
@@ -147,6 +148,9 @@ class GraphOrchestrator:
|
|
| 147 |
self.oauth_token = oauth_token
|
| 148 |
self.logger = logger
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
# Initialize flows (for backward compatibility)
|
| 151 |
self._iterative_flow: IterativeResearchFlow | None = None
|
| 152 |
self._deep_flow: DeepResearchFlow | None = None
|
|
@@ -155,6 +159,21 @@ class GraphOrchestrator:
|
|
| 155 |
self._graph: ResearchGraph | None = None
|
| 156 |
self._budget_tracker: BudgetTracker | None = None
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
|
| 159 |
"""
|
| 160 |
Run the research workflow.
|
|
@@ -649,6 +668,27 @@ class GraphOrchestrator:
|
|
| 649 |
estimated_tokens = len(final_report) // 4 # Rough token estimate
|
| 650 |
context.budget_tracker.add_tokens("graph_execution", estimated_tokens)
|
| 651 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
return final_report
|
| 653 |
|
| 654 |
# Standard agent execution
|
|
|
|
| 32 |
from src.middleware.budget_tracker import BudgetTracker
|
| 33 |
from src.middleware.state_machine import WorkflowState, init_workflow_state
|
| 34 |
from src.orchestrator.research_flow import DeepResearchFlow, IterativeResearchFlow
|
| 35 |
+
from src.services.report_file_service import ReportFileService, get_report_file_service
|
| 36 |
from src.utils.models import AgentEvent
|
| 37 |
|
| 38 |
if TYPE_CHECKING:
|
|
|
|
| 148 |
self.oauth_token = oauth_token
|
| 149 |
self.logger = logger
|
| 150 |
|
| 151 |
+
# Initialize file service (lazy if not provided)
|
| 152 |
+
self._file_service: ReportFileService | None = None
|
| 153 |
+
|
| 154 |
# Initialize flows (for backward compatibility)
|
| 155 |
self._iterative_flow: IterativeResearchFlow | None = None
|
| 156 |
self._deep_flow: DeepResearchFlow | None = None
|
|
|
|
| 159 |
self._graph: ResearchGraph | None = None
|
| 160 |
self._budget_tracker: BudgetTracker | None = None
|
| 161 |
|
| 162 |
+
def _get_file_service(self) -> ReportFileService | None:
|
| 163 |
+
"""
|
| 164 |
+
Get file service instance (lazy initialization).
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
ReportFileService instance or None if disabled
|
| 168 |
+
"""
|
| 169 |
+
if self._file_service is None:
|
| 170 |
+
try:
|
| 171 |
+
self._file_service = get_report_file_service()
|
| 172 |
+
except Exception as e:
|
| 173 |
+
self.logger.warning("Failed to initialize file service", error=str(e))
|
| 174 |
+
return None
|
| 175 |
+
return self._file_service
|
| 176 |
+
|
| 177 |
async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
|
| 178 |
"""
|
| 179 |
Run the research workflow.
|
|
|
|
| 668 |
estimated_tokens = len(final_report) // 4 # Rough token estimate
|
| 669 |
context.budget_tracker.add_tokens("graph_execution", estimated_tokens)
|
| 670 |
|
| 671 |
+
# Save report to file if enabled
|
| 672 |
+
file_path: str | None = None
|
| 673 |
+
try:
|
| 674 |
+
file_service = self._get_file_service()
|
| 675 |
+
if file_service:
|
| 676 |
+
file_path = file_service.save_report(
|
| 677 |
+
report_content=final_report,
|
| 678 |
+
query=query,
|
| 679 |
+
)
|
| 680 |
+
self.logger.info("Report saved to file", file_path=file_path)
|
| 681 |
+
except Exception as e:
|
| 682 |
+
# Don't fail the entire operation if file saving fails
|
| 683 |
+
self.logger.warning("Failed to save report to file", error=str(e))
|
| 684 |
+
file_path = None
|
| 685 |
+
|
| 686 |
+
# Return dict with file path if available, otherwise return string (backward compatible)
|
| 687 |
+
if file_path:
|
| 688 |
+
return {
|
| 689 |
+
"message": final_report,
|
| 690 |
+
"file": file_path,
|
| 691 |
+
}
|
| 692 |
return final_report
|
| 693 |
|
| 694 |
# Standard agent execution
|
src/orchestrator/research_flow.py
CHANGED
|
@@ -25,6 +25,7 @@ from src.middleware.budget_tracker import BudgetTracker
|
|
| 25 |
from src.middleware.state_machine import get_workflow_state, init_workflow_state
|
| 26 |
from src.middleware.workflow_manager import WorkflowManager
|
| 27 |
from src.services.llamaindex_rag import LlamaIndexRAGService, get_rag_service
|
|
|
|
| 28 |
from src.tools.tool_executor import execute_tool_tasks
|
| 29 |
from src.utils.exceptions import ConfigurationError
|
| 30 |
from src.utils.models import (
|
|
@@ -112,6 +113,24 @@ class IterativeResearchFlow:
|
|
| 112 |
# Graph orchestrator (lazy initialization)
|
| 113 |
self._graph_orchestrator: Any = None
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
async def run(
|
| 116 |
self,
|
| 117 |
query: str,
|
|
@@ -659,6 +678,19 @@ FINDINGS:
|
|
| 659 |
tokens=estimated_tokens,
|
| 660 |
)
|
| 661 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
# Note: Citation validation for markdown reports would require Evidence objects
|
| 663 |
# Currently, findings are strings, not Evidence objects. For full validation,
|
| 664 |
# consider using ResearchReport format or passing Evidence objects separately.
|
|
@@ -725,6 +757,24 @@ class DeepResearchFlow:
|
|
| 725 |
# Graph orchestrator (lazy initialization)
|
| 726 |
self._graph_orchestrator: Any = None
|
| 727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
async def run(self, query: str) -> str:
|
| 729 |
"""
|
| 730 |
Run the deep research flow.
|
|
@@ -1000,6 +1050,19 @@ class DeepResearchFlow:
|
|
| 1000 |
agent="long_writer" if self.use_long_writer else "proofreader",
|
| 1001 |
)
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
self.logger.info("Final report created", length=len(final_report))
|
| 1004 |
|
| 1005 |
return final_report
|
|
|
|
| 25 |
from src.middleware.state_machine import get_workflow_state, init_workflow_state
|
| 26 |
from src.middleware.workflow_manager import WorkflowManager
|
| 27 |
from src.services.llamaindex_rag import LlamaIndexRAGService, get_rag_service
|
| 28 |
+
from src.services.report_file_service import ReportFileService, get_report_file_service
|
| 29 |
from src.tools.tool_executor import execute_tool_tasks
|
| 30 |
from src.utils.exceptions import ConfigurationError
|
| 31 |
from src.utils.models import (
|
|
|
|
| 113 |
# Graph orchestrator (lazy initialization)
|
| 114 |
self._graph_orchestrator: Any = None
|
| 115 |
|
| 116 |
+
# File service (lazy initialization)
|
| 117 |
+
self._file_service: ReportFileService | None = None
|
| 118 |
+
|
| 119 |
+
def _get_file_service(self) -> ReportFileService | None:
|
| 120 |
+
"""
|
| 121 |
+
Get file service instance (lazy initialization).
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
ReportFileService instance or None if disabled
|
| 125 |
+
"""
|
| 126 |
+
if self._file_service is None:
|
| 127 |
+
try:
|
| 128 |
+
self._file_service = get_report_file_service()
|
| 129 |
+
except Exception as e:
|
| 130 |
+
self.logger.warning("Failed to initialize file service", error=str(e))
|
| 131 |
+
return None
|
| 132 |
+
return self._file_service
|
| 133 |
+
|
| 134 |
async def run(
|
| 135 |
self,
|
| 136 |
query: str,
|
|
|
|
| 678 |
tokens=estimated_tokens,
|
| 679 |
)
|
| 680 |
|
| 681 |
+
# Save report to file if enabled
|
| 682 |
+
try:
|
| 683 |
+
file_service = self._get_file_service()
|
| 684 |
+
if file_service:
|
| 685 |
+
file_path = file_service.save_report(
|
| 686 |
+
report_content=report,
|
| 687 |
+
query=query,
|
| 688 |
+
)
|
| 689 |
+
self.logger.info("Report saved to file", file_path=file_path)
|
| 690 |
+
except Exception as e:
|
| 691 |
+
# Don't fail the entire operation if file saving fails
|
| 692 |
+
self.logger.warning("Failed to save report to file", error=str(e))
|
| 693 |
+
|
| 694 |
# Note: Citation validation for markdown reports would require Evidence objects
|
| 695 |
# Currently, findings are strings, not Evidence objects. For full validation,
|
| 696 |
# consider using ResearchReport format or passing Evidence objects separately.
|
|
|
|
| 757 |
# Graph orchestrator (lazy initialization)
|
| 758 |
self._graph_orchestrator: Any = None
|
| 759 |
|
| 760 |
+
# File service (lazy initialization)
|
| 761 |
+
self._file_service: ReportFileService | None = None
|
| 762 |
+
|
| 763 |
+
def _get_file_service(self) -> ReportFileService | None:
|
| 764 |
+
"""
|
| 765 |
+
Get file service instance (lazy initialization).
|
| 766 |
+
|
| 767 |
+
Returns:
|
| 768 |
+
ReportFileService instance or None if disabled
|
| 769 |
+
"""
|
| 770 |
+
if self._file_service is None:
|
| 771 |
+
try:
|
| 772 |
+
self._file_service = get_report_file_service()
|
| 773 |
+
except Exception as e:
|
| 774 |
+
self.logger.warning("Failed to initialize file service", error=str(e))
|
| 775 |
+
return None
|
| 776 |
+
return self._file_service
|
| 777 |
+
|
| 778 |
async def run(self, query: str) -> str:
|
| 779 |
"""
|
| 780 |
Run the deep research flow.
|
|
|
|
| 1050 |
agent="long_writer" if self.use_long_writer else "proofreader",
|
| 1051 |
)
|
| 1052 |
|
| 1053 |
+
# Save report to file if enabled
|
| 1054 |
+
try:
|
| 1055 |
+
file_service = self._get_file_service()
|
| 1056 |
+
if file_service:
|
| 1057 |
+
file_path = file_service.save_report(
|
| 1058 |
+
report_content=final_report,
|
| 1059 |
+
query=query,
|
| 1060 |
+
)
|
| 1061 |
+
self.logger.info("Report saved to file", file_path=file_path)
|
| 1062 |
+
except Exception as e:
|
| 1063 |
+
# Don't fail the entire operation if file saving fails
|
| 1064 |
+
self.logger.warning("Failed to save report to file", error=str(e))
|
| 1065 |
+
|
| 1066 |
self.logger.info("Final report created", length=len(final_report))
|
| 1067 |
|
| 1068 |
return final_report
|
src/services/image_ocr.py
CHANGED
|
@@ -243,3 +243,5 @@ def get_image_ocr_service() -> ImageOCRService:
|
|
| 243 |
|
| 244 |
|
| 245 |
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
|
| 245 |
|
| 246 |
+
|
| 247 |
+
|
src/services/report_file_service.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Service for saving research reports to files."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import tempfile
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Literal
|
| 8 |
+
|
| 9 |
+
import structlog
|
| 10 |
+
|
| 11 |
+
from src.utils.config import settings
|
| 12 |
+
from src.utils.exceptions import ConfigurationError
|
| 13 |
+
|
| 14 |
+
logger = structlog.get_logger()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ReportFileService:
|
| 18 |
+
"""
|
| 19 |
+
Service for saving research reports to files.
|
| 20 |
+
|
| 21 |
+
Handles file creation, naming, and directory management for report outputs.
|
| 22 |
+
Supports saving reports in multiple formats (markdown, HTML, PDF).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(
|
| 26 |
+
self,
|
| 27 |
+
output_directory: str | None = None,
|
| 28 |
+
enabled: bool | None = None,
|
| 29 |
+
file_format: Literal["md", "md_html", "md_pdf"] | None = None,
|
| 30 |
+
) -> None:
|
| 31 |
+
"""
|
| 32 |
+
Initialize the report file service.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
output_directory: Directory to save reports. If None, uses settings or temp directory.
|
| 36 |
+
enabled: Whether file saving is enabled. If None, uses settings.
|
| 37 |
+
file_format: File format to save. If None, uses settings.
|
| 38 |
+
"""
|
| 39 |
+
self.enabled = enabled if enabled is not None else settings.save_reports_to_file
|
| 40 |
+
self.file_format = file_format or settings.report_file_format
|
| 41 |
+
self.filename_template = settings.report_filename_template
|
| 42 |
+
|
| 43 |
+
# Determine output directory
|
| 44 |
+
if output_directory:
|
| 45 |
+
self.output_directory = Path(output_directory)
|
| 46 |
+
elif settings.report_output_directory:
|
| 47 |
+
self.output_directory = Path(settings.report_output_directory)
|
| 48 |
+
else:
|
| 49 |
+
# Use system temp directory
|
| 50 |
+
self.output_directory = Path(tempfile.gettempdir()) / "deepcritical_reports"
|
| 51 |
+
|
| 52 |
+
# Create output directory if it doesn't exist
|
| 53 |
+
if self.enabled:
|
| 54 |
+
try:
|
| 55 |
+
self.output_directory.mkdir(parents=True, exist_ok=True)
|
| 56 |
+
logger.debug(
|
| 57 |
+
"Report output directory initialized",
|
| 58 |
+
path=str(self.output_directory),
|
| 59 |
+
enabled=self.enabled,
|
| 60 |
+
)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logger.error("Failed to create report output directory", error=str(e), path=str(self.output_directory))
|
| 63 |
+
raise ConfigurationError(f"Failed to create report output directory: {e}") from e
|
| 64 |
+
|
| 65 |
+
def _generate_filename(self, query: str | None = None, extension: str = ".md") -> str:
|
| 66 |
+
"""
|
| 67 |
+
Generate filename for report using template.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
query: Optional query string for hash generation
|
| 71 |
+
extension: File extension (e.g., ".md", ".html")
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
Generated filename
|
| 75 |
+
"""
|
| 76 |
+
# Generate timestamp
|
| 77 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 78 |
+
|
| 79 |
+
# Generate query hash if query provided
|
| 80 |
+
query_hash = ""
|
| 81 |
+
if query:
|
| 82 |
+
query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
|
| 83 |
+
|
| 84 |
+
# Generate date
|
| 85 |
+
date = datetime.now().strftime("%Y-%m-%d")
|
| 86 |
+
|
| 87 |
+
# Replace template placeholders
|
| 88 |
+
filename = self.filename_template
|
| 89 |
+
filename = filename.replace("{timestamp}", timestamp)
|
| 90 |
+
filename = filename.replace("{query_hash}", query_hash)
|
| 91 |
+
filename = filename.replace("{date}", date)
|
| 92 |
+
|
| 93 |
+
# Ensure correct extension
|
| 94 |
+
if not filename.endswith(extension):
|
| 95 |
+
# Remove existing extension if present
|
| 96 |
+
if "." in filename:
|
| 97 |
+
filename = filename.rsplit(".", 1)[0]
|
| 98 |
+
filename += extension
|
| 99 |
+
|
| 100 |
+
return filename
|
| 101 |
+
|
| 102 |
+
def save_report(
|
| 103 |
+
self,
|
| 104 |
+
report_content: str,
|
| 105 |
+
query: str | None = None,
|
| 106 |
+
filename: str | None = None,
|
| 107 |
+
) -> str:
|
| 108 |
+
"""
|
| 109 |
+
Save a report to a file.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
report_content: The report content (markdown string)
|
| 113 |
+
query: Optional query string for filename generation
|
| 114 |
+
filename: Optional custom filename. If None, generates from template.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
Path to saved file
|
| 118 |
+
|
| 119 |
+
Raises:
|
| 120 |
+
ConfigurationError: If file saving is disabled or fails
|
| 121 |
+
"""
|
| 122 |
+
if not self.enabled:
|
| 123 |
+
logger.debug("File saving disabled, skipping")
|
| 124 |
+
raise ConfigurationError("Report file saving is disabled")
|
| 125 |
+
|
| 126 |
+
if not report_content or not report_content.strip():
|
| 127 |
+
raise ValueError("Report content cannot be empty")
|
| 128 |
+
|
| 129 |
+
# Generate filename if not provided
|
| 130 |
+
if not filename:
|
| 131 |
+
filename = self._generate_filename(query=query, extension=".md")
|
| 132 |
+
|
| 133 |
+
# Ensure filename is safe
|
| 134 |
+
filename = self._sanitize_filename(filename)
|
| 135 |
+
|
| 136 |
+
# Build full file path
|
| 137 |
+
file_path = self.output_directory / filename
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
# Write file
|
| 141 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 142 |
+
f.write(report_content)
|
| 143 |
+
|
| 144 |
+
logger.info(
|
| 145 |
+
"Report saved to file",
|
| 146 |
+
path=str(file_path),
|
| 147 |
+
size=len(report_content),
|
| 148 |
+
query=query[:50] if query else None,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
return str(file_path)
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error("Failed to save report to file", error=str(e), path=str(file_path))
|
| 155 |
+
raise ConfigurationError(f"Failed to save report to file: {e}") from e
|
| 156 |
+
|
| 157 |
+
def save_report_multiple_formats(
|
| 158 |
+
self,
|
| 159 |
+
report_content: str,
|
| 160 |
+
query: str | None = None,
|
| 161 |
+
) -> dict[str, str]:
|
| 162 |
+
"""
|
| 163 |
+
Save a report in multiple formats.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
report_content: The report content (markdown string)
|
| 167 |
+
query: Optional query string for filename generation
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
Dictionary mapping format to file path (e.g., {"md": "/path/to/report.md"})
|
| 171 |
+
|
| 172 |
+
Raises:
|
| 173 |
+
ConfigurationError: If file saving is disabled or fails
|
| 174 |
+
"""
|
| 175 |
+
if not self.enabled:
|
| 176 |
+
logger.debug("File saving disabled, skipping")
|
| 177 |
+
raise ConfigurationError("Report file saving is disabled")
|
| 178 |
+
|
| 179 |
+
saved_files: dict[str, str] = {}
|
| 180 |
+
|
| 181 |
+
# Always save markdown
|
| 182 |
+
md_path = self.save_report(report_content, query=query, filename=None)
|
| 183 |
+
saved_files["md"] = md_path
|
| 184 |
+
|
| 185 |
+
# Save additional formats based on file_format setting
|
| 186 |
+
if self.file_format == "md_html":
|
| 187 |
+
# TODO: Implement HTML conversion
|
| 188 |
+
logger.warning("HTML format not yet implemented, saving markdown only")
|
| 189 |
+
elif self.file_format == "md_pdf":
|
| 190 |
+
# TODO: Implement PDF conversion
|
| 191 |
+
logger.warning("PDF format not yet implemented, saving markdown only")
|
| 192 |
+
|
| 193 |
+
return saved_files
|
| 194 |
+
|
| 195 |
+
def _sanitize_filename(self, filename: str) -> str:
|
| 196 |
+
"""
|
| 197 |
+
Sanitize filename to remove unsafe characters.
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
filename: Original filename
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Sanitized filename
|
| 204 |
+
"""
|
| 205 |
+
# Remove or replace unsafe characters
|
| 206 |
+
unsafe_chars = '<>:"/\\|?*'
|
| 207 |
+
sanitized = filename
|
| 208 |
+
for char in unsafe_chars:
|
| 209 |
+
sanitized = sanitized.replace(char, "_")
|
| 210 |
+
|
| 211 |
+
# Limit length
|
| 212 |
+
if len(sanitized) > 200:
|
| 213 |
+
name, ext = sanitized.rsplit(".", 1) if "." in sanitized else (sanitized, "")
|
| 214 |
+
sanitized = name[:190] + ext
|
| 215 |
+
|
| 216 |
+
return sanitized
|
| 217 |
+
|
| 218 |
+
def cleanup_old_files(self, max_age_days: int = 7) -> int:
|
| 219 |
+
"""
|
| 220 |
+
Clean up old report files.
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
max_age_days: Maximum age in days for files to keep
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
Number of files deleted
|
| 227 |
+
"""
|
| 228 |
+
if not self.output_directory.exists():
|
| 229 |
+
return 0
|
| 230 |
+
|
| 231 |
+
deleted_count = 0
|
| 232 |
+
cutoff_time = datetime.now().timestamp() - (max_age_days * 24 * 60 * 60)
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
for file_path in self.output_directory.iterdir():
|
| 236 |
+
if file_path.is_file() and file_path.stat().st_mtime < cutoff_time:
|
| 237 |
+
try:
|
| 238 |
+
file_path.unlink()
|
| 239 |
+
deleted_count += 1
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logger.warning("Failed to delete old file", path=str(file_path), error=str(e))
|
| 242 |
+
|
| 243 |
+
if deleted_count > 0:
|
| 244 |
+
logger.info("Cleaned up old report files", deleted=deleted_count, max_age_days=max_age_days)
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
logger.error("Failed to cleanup old files", error=str(e))
|
| 248 |
+
|
| 249 |
+
return deleted_count
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def get_report_file_service() -> ReportFileService:
|
| 253 |
+
"""
|
| 254 |
+
Get or create a ReportFileService instance (singleton pattern).
|
| 255 |
+
|
| 256 |
+
Returns:
|
| 257 |
+
ReportFileService instance
|
| 258 |
+
"""
|
| 259 |
+
# Use lru_cache for singleton pattern
|
| 260 |
+
from functools import lru_cache
|
| 261 |
+
|
| 262 |
+
@lru_cache(maxsize=1)
|
| 263 |
+
def _get_service() -> ReportFileService:
|
| 264 |
+
return ReportFileService()
|
| 265 |
+
|
| 266 |
+
return _get_service()
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
|
src/tools/crawl_adapter.py
CHANGED
|
@@ -64,3 +64,5 @@ async def crawl_website(starting_url: str) -> str:
|
|
| 64 |
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
|
| 67 |
+
|
| 68 |
+
|
src/tools/searchxng_web_search.py
CHANGED
|
@@ -118,3 +118,5 @@ class SearchXNGWebSearchTool:
|
|
| 118 |
raise SearchError(f"SearchXNG search failed: {e}") from e
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
| 118 |
raise SearchError(f"SearchXNG search failed: {e}") from e
|
| 119 |
|
| 120 |
|
| 121 |
+
|
| 122 |
+
|
src/tools/serper_web_search.py
CHANGED
|
@@ -118,3 +118,5 @@ class SerperWebSearchTool:
|
|
| 118 |
raise SearchError(f"Serper search failed: {e}") from e
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
| 118 |
raise SearchError(f"Serper search failed: {e}") from e
|
| 119 |
|
| 120 |
|
| 121 |
+
|
| 122 |
+
|
src/tools/vendored/__init__.py
CHANGED
|
@@ -25,3 +25,5 @@ __all__ = [
|
|
| 25 |
]
|
| 26 |
|
| 27 |
|
|
|
|
|
|
|
|
|
| 25 |
]
|
| 26 |
|
| 27 |
|
| 28 |
+
|
| 29 |
+
|
src/tools/vendored/searchxng_client.py
CHANGED
|
@@ -97,3 +97,5 @@ class SearchXNGClient:
|
|
| 97 |
raise SearchError(f"SearchXNG search failed: {e}") from e
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
| 97 |
raise SearchError(f"SearchXNG search failed: {e}") from e
|
| 98 |
|
| 99 |
|
| 100 |
+
|
| 101 |
+
|
src/tools/vendored/serper_client.py
CHANGED
|
@@ -93,3 +93,5 @@ class SerperClient:
|
|
| 93 |
raise SearchError(f"Serper search failed: {e}") from e
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
| 93 |
raise SearchError(f"Serper search failed: {e}") from e
|
| 94 |
|
| 95 |
|
| 96 |
+
|
| 97 |
+
|
src/tools/vendored/web_search_core.py
CHANGED
|
@@ -204,3 +204,5 @@ def is_valid_url(url: str) -> bool:
|
|
| 204 |
return True
|
| 205 |
|
| 206 |
|
|
|
|
|
|
|
|
|
| 204 |
return True
|
| 205 |
|
| 206 |
|
| 207 |
+
|
| 208 |
+
|
src/tools/web_search_factory.py
CHANGED
|
@@ -72,3 +72,5 @@ def create_web_search_tool() -> SearchTool | None:
|
|
| 72 |
return None
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
|
|
| 72 |
return None
|
| 73 |
|
| 74 |
|
| 75 |
+
|
| 76 |
+
|
src/utils/config.py
CHANGED
|
@@ -164,6 +164,24 @@ class Settings(BaseSettings):
|
|
| 164 |
description="Modal GPU type for TTS (T4, A10, A100, L4, L40S). None uses default T4.",
|
| 165 |
)
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
@property
|
| 168 |
def modal_available(self) -> bool:
|
| 169 |
"""Check if Modal credentials are configured."""
|
|
|
|
| 164 |
description="Modal GPU type for TTS (T4, A10, A100, L4, L40S). None uses default T4.",
|
| 165 |
)
|
| 166 |
|
| 167 |
+
# Report File Output Configuration
|
| 168 |
+
save_reports_to_file: bool = Field(
|
| 169 |
+
default=True,
|
| 170 |
+
description="Save generated reports to files (enables file downloads in Gradio)",
|
| 171 |
+
)
|
| 172 |
+
report_output_directory: str | None = Field(
|
| 173 |
+
default=None,
|
| 174 |
+
description="Directory to save report files. If None, uses system temp directory.",
|
| 175 |
+
)
|
| 176 |
+
report_file_format: Literal["md", "md_html", "md_pdf"] = Field(
|
| 177 |
+
default="md",
|
| 178 |
+
description="File format(s) to save reports in. 'md' saves only markdown, others save multiple formats.",
|
| 179 |
+
)
|
| 180 |
+
report_filename_template: str = Field(
|
| 181 |
+
default="report_{timestamp}_{query_hash}.md",
|
| 182 |
+
description="Template for report filenames. Supports {timestamp}, {query_hash}, {date} placeholders.",
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
@property
|
| 186 |
def modal_available(self) -> bool:
|
| 187 |
"""Check if Modal credentials are configured."""
|
tests/unit/middleware/__init__.py
CHANGED
|
@@ -18,6 +18,8 @@
|
|
| 18 |
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
+
|
| 22 |
+
|
| 23 |
|
| 24 |
|
| 25 |
|
tests/unit/middleware/test_budget_tracker_phase7.py
CHANGED
|
@@ -176,6 +176,8 @@ class TestIterationTokenTracking:
|
|
| 176 |
|
| 177 |
|
| 178 |
|
|
|
|
|
|
|
| 179 |
|
| 180 |
|
| 181 |
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
|
| 179 |
+
|
| 180 |
+
|
| 181 |
|
| 182 |
|
| 183 |
|
tests/unit/middleware/test_state_machine.py
CHANGED
|
@@ -373,6 +373,8 @@ class TestContextVarIsolation:
|
|
| 373 |
|
| 374 |
|
| 375 |
|
|
|
|
|
|
|
| 376 |
|
| 377 |
|
| 378 |
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
|
| 376 |
+
|
| 377 |
+
|
| 378 |
|
| 379 |
|
| 380 |
|
tests/unit/middleware/test_workflow_manager.py
CHANGED
|
@@ -303,6 +303,8 @@ class TestWorkflowManager:
|
|
| 303 |
|
| 304 |
|
| 305 |
|
|
|
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
|
|
|
|
| 303 |
|
| 304 |
|
| 305 |
|
| 306 |
+
|
| 307 |
+
|
| 308 |
|
| 309 |
|
| 310 |
|