realtimex-deeptutor 0.5.0.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- realtimex_deeptutor/__init__.py +67 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
- src/__init__.py +40 -0
- src/agents/__init__.py +24 -0
- src/agents/base_agent.py +657 -0
- src/agents/chat/__init__.py +24 -0
- src/agents/chat/chat_agent.py +435 -0
- src/agents/chat/prompts/en/chat_agent.yaml +35 -0
- src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
- src/agents/chat/session_manager.py +311 -0
- src/agents/co_writer/__init__.py +0 -0
- src/agents/co_writer/edit_agent.py +260 -0
- src/agents/co_writer/narrator_agent.py +423 -0
- src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
- src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
- src/agents/guide/__init__.py +16 -0
- src/agents/guide/agents/__init__.py +11 -0
- src/agents/guide/agents/chat_agent.py +104 -0
- src/agents/guide/agents/interactive_agent.py +223 -0
- src/agents/guide/agents/locate_agent.py +149 -0
- src/agents/guide/agents/summary_agent.py +150 -0
- src/agents/guide/guide_manager.py +500 -0
- src/agents/guide/prompts/en/chat_agent.yaml +41 -0
- src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
- src/agents/guide/prompts/en/locate_agent.yaml +68 -0
- src/agents/guide/prompts/en/summary_agent.yaml +157 -0
- src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
- src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
- src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
- src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
- src/agents/ideagen/__init__.py +12 -0
- src/agents/ideagen/idea_generation_workflow.py +426 -0
- src/agents/ideagen/material_organizer_agent.py +173 -0
- src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
- src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
- src/agents/question/__init__.py +24 -0
- src/agents/question/agents/__init__.py +18 -0
- src/agents/question/agents/generate_agent.py +381 -0
- src/agents/question/agents/relevance_analyzer.py +207 -0
- src/agents/question/agents/retrieve_agent.py +239 -0
- src/agents/question/coordinator.py +718 -0
- src/agents/question/example.py +109 -0
- src/agents/question/prompts/en/coordinator.yaml +75 -0
- src/agents/question/prompts/en/generate_agent.yaml +77 -0
- src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
- src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
- src/agents/question/prompts/zh/coordinator.yaml +75 -0
- src/agents/question/prompts/zh/generate_agent.yaml +77 -0
- src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
- src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
- src/agents/research/agents/__init__.py +23 -0
- src/agents/research/agents/decompose_agent.py +507 -0
- src/agents/research/agents/manager_agent.py +228 -0
- src/agents/research/agents/note_agent.py +180 -0
- src/agents/research/agents/rephrase_agent.py +263 -0
- src/agents/research/agents/reporting_agent.py +1333 -0
- src/agents/research/agents/research_agent.py +714 -0
- src/agents/research/data_structures.py +451 -0
- src/agents/research/main.py +188 -0
- src/agents/research/prompts/en/decompose_agent.yaml +89 -0
- src/agents/research/prompts/en/manager_agent.yaml +24 -0
- src/agents/research/prompts/en/note_agent.yaml +121 -0
- src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/en/reporting_agent.yaml +380 -0
- src/agents/research/prompts/en/research_agent.yaml +173 -0
- src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
- src/agents/research/prompts/zh/manager_agent.yaml +24 -0
- src/agents/research/prompts/zh/note_agent.yaml +121 -0
- src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
- src/agents/research/prompts/zh/research_agent.yaml +173 -0
- src/agents/research/research_pipeline.py +1309 -0
- src/agents/research/utils/__init__.py +60 -0
- src/agents/research/utils/citation_manager.py +799 -0
- src/agents/research/utils/json_utils.py +98 -0
- src/agents/research/utils/token_tracker.py +297 -0
- src/agents/solve/__init__.py +80 -0
- src/agents/solve/analysis_loop/__init__.py +14 -0
- src/agents/solve/analysis_loop/investigate_agent.py +414 -0
- src/agents/solve/analysis_loop/note_agent.py +190 -0
- src/agents/solve/main_solver.py +862 -0
- src/agents/solve/memory/__init__.py +34 -0
- src/agents/solve/memory/citation_memory.py +353 -0
- src/agents/solve/memory/investigate_memory.py +226 -0
- src/agents/solve/memory/solve_memory.py +340 -0
- src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
- src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
- src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
- src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
- src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
- src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
- src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
- src/agents/solve/solve_loop/__init__.py +22 -0
- src/agents/solve/solve_loop/citation_manager.py +74 -0
- src/agents/solve/solve_loop/manager_agent.py +274 -0
- src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
- src/agents/solve/solve_loop/response_agent.py +301 -0
- src/agents/solve/solve_loop/solve_agent.py +325 -0
- src/agents/solve/solve_loop/tool_agent.py +470 -0
- src/agents/solve/utils/__init__.py +64 -0
- src/agents/solve/utils/config_validator.py +313 -0
- src/agents/solve/utils/display_manager.py +223 -0
- src/agents/solve/utils/error_handler.py +363 -0
- src/agents/solve/utils/json_utils.py +98 -0
- src/agents/solve/utils/performance_monitor.py +407 -0
- src/agents/solve/utils/token_tracker.py +541 -0
- src/api/__init__.py +0 -0
- src/api/main.py +240 -0
- src/api/routers/__init__.py +1 -0
- src/api/routers/agent_config.py +69 -0
- src/api/routers/chat.py +296 -0
- src/api/routers/co_writer.py +337 -0
- src/api/routers/config.py +627 -0
- src/api/routers/dashboard.py +18 -0
- src/api/routers/guide.py +337 -0
- src/api/routers/ideagen.py +436 -0
- src/api/routers/knowledge.py +821 -0
- src/api/routers/notebook.py +247 -0
- src/api/routers/question.py +537 -0
- src/api/routers/research.py +394 -0
- src/api/routers/settings.py +164 -0
- src/api/routers/solve.py +305 -0
- src/api/routers/system.py +252 -0
- src/api/run_server.py +61 -0
- src/api/utils/history.py +172 -0
- src/api/utils/log_interceptor.py +21 -0
- src/api/utils/notebook_manager.py +415 -0
- src/api/utils/progress_broadcaster.py +72 -0
- src/api/utils/task_id_manager.py +100 -0
- src/config/__init__.py +0 -0
- src/config/accessors.py +18 -0
- src/config/constants.py +34 -0
- src/config/defaults.py +18 -0
- src/config/schema.py +38 -0
- src/config/settings.py +50 -0
- src/core/errors.py +62 -0
- src/knowledge/__init__.py +23 -0
- src/knowledge/add_documents.py +606 -0
- src/knowledge/config.py +65 -0
- src/knowledge/example_add_documents.py +236 -0
- src/knowledge/extract_numbered_items.py +1039 -0
- src/knowledge/initializer.py +621 -0
- src/knowledge/kb.py +22 -0
- src/knowledge/manager.py +782 -0
- src/knowledge/progress_tracker.py +182 -0
- src/knowledge/start_kb.py +535 -0
- src/logging/__init__.py +103 -0
- src/logging/adapters/__init__.py +17 -0
- src/logging/adapters/lightrag.py +184 -0
- src/logging/adapters/llamaindex.py +141 -0
- src/logging/config.py +80 -0
- src/logging/handlers/__init__.py +20 -0
- src/logging/handlers/console.py +75 -0
- src/logging/handlers/file.py +201 -0
- src/logging/handlers/websocket.py +127 -0
- src/logging/logger.py +709 -0
- src/logging/stats/__init__.py +16 -0
- src/logging/stats/llm_stats.py +179 -0
- src/services/__init__.py +56 -0
- src/services/config/__init__.py +61 -0
- src/services/config/knowledge_base_config.py +210 -0
- src/services/config/loader.py +260 -0
- src/services/config/unified_config.py +603 -0
- src/services/embedding/__init__.py +45 -0
- src/services/embedding/adapters/__init__.py +22 -0
- src/services/embedding/adapters/base.py +106 -0
- src/services/embedding/adapters/cohere.py +127 -0
- src/services/embedding/adapters/jina.py +99 -0
- src/services/embedding/adapters/ollama.py +116 -0
- src/services/embedding/adapters/openai_compatible.py +96 -0
- src/services/embedding/client.py +159 -0
- src/services/embedding/config.py +156 -0
- src/services/embedding/provider.py +119 -0
- src/services/llm/__init__.py +152 -0
- src/services/llm/capabilities.py +313 -0
- src/services/llm/client.py +302 -0
- src/services/llm/cloud_provider.py +530 -0
- src/services/llm/config.py +200 -0
- src/services/llm/error_mapping.py +103 -0
- src/services/llm/exceptions.py +152 -0
- src/services/llm/factory.py +450 -0
- src/services/llm/local_provider.py +347 -0
- src/services/llm/providers/anthropic.py +95 -0
- src/services/llm/providers/base_provider.py +93 -0
- src/services/llm/providers/open_ai.py +83 -0
- src/services/llm/registry.py +71 -0
- src/services/llm/telemetry.py +40 -0
- src/services/llm/types.py +27 -0
- src/services/llm/utils.py +333 -0
- src/services/prompt/__init__.py +25 -0
- src/services/prompt/manager.py +206 -0
- src/services/rag/__init__.py +64 -0
- src/services/rag/components/__init__.py +29 -0
- src/services/rag/components/base.py +59 -0
- src/services/rag/components/chunkers/__init__.py +18 -0
- src/services/rag/components/chunkers/base.py +34 -0
- src/services/rag/components/chunkers/fixed.py +71 -0
- src/services/rag/components/chunkers/numbered_item.py +94 -0
- src/services/rag/components/chunkers/semantic.py +97 -0
- src/services/rag/components/embedders/__init__.py +14 -0
- src/services/rag/components/embedders/base.py +32 -0
- src/services/rag/components/embedders/openai.py +63 -0
- src/services/rag/components/indexers/__init__.py +18 -0
- src/services/rag/components/indexers/base.py +35 -0
- src/services/rag/components/indexers/graph.py +172 -0
- src/services/rag/components/indexers/lightrag.py +156 -0
- src/services/rag/components/indexers/vector.py +146 -0
- src/services/rag/components/parsers/__init__.py +18 -0
- src/services/rag/components/parsers/base.py +35 -0
- src/services/rag/components/parsers/markdown.py +52 -0
- src/services/rag/components/parsers/pdf.py +115 -0
- src/services/rag/components/parsers/text.py +86 -0
- src/services/rag/components/retrievers/__init__.py +18 -0
- src/services/rag/components/retrievers/base.py +34 -0
- src/services/rag/components/retrievers/dense.py +200 -0
- src/services/rag/components/retrievers/hybrid.py +164 -0
- src/services/rag/components/retrievers/lightrag.py +169 -0
- src/services/rag/components/routing.py +286 -0
- src/services/rag/factory.py +234 -0
- src/services/rag/pipeline.py +215 -0
- src/services/rag/pipelines/__init__.py +32 -0
- src/services/rag/pipelines/academic.py +44 -0
- src/services/rag/pipelines/lightrag.py +43 -0
- src/services/rag/pipelines/llamaindex.py +313 -0
- src/services/rag/pipelines/raganything.py +384 -0
- src/services/rag/service.py +244 -0
- src/services/rag/types.py +73 -0
- src/services/search/__init__.py +284 -0
- src/services/search/base.py +87 -0
- src/services/search/consolidation.py +398 -0
- src/services/search/providers/__init__.py +128 -0
- src/services/search/providers/baidu.py +188 -0
- src/services/search/providers/exa.py +194 -0
- src/services/search/providers/jina.py +161 -0
- src/services/search/providers/perplexity.py +153 -0
- src/services/search/providers/serper.py +209 -0
- src/services/search/providers/tavily.py +161 -0
- src/services/search/types.py +114 -0
- src/services/setup/__init__.py +34 -0
- src/services/setup/init.py +285 -0
- src/services/tts/__init__.py +16 -0
- src/services/tts/config.py +99 -0
- src/tools/__init__.py +91 -0
- src/tools/code_executor.py +536 -0
- src/tools/paper_search_tool.py +171 -0
- src/tools/query_item_tool.py +310 -0
- src/tools/question/__init__.py +15 -0
- src/tools/question/exam_mimic.py +616 -0
- src/tools/question/pdf_parser.py +211 -0
- src/tools/question/question_extractor.py +397 -0
- src/tools/rag_tool.py +173 -0
- src/tools/tex_chunker.py +339 -0
- src/tools/tex_downloader.py +253 -0
- src/tools/web_search.py +71 -0
- src/utils/config_manager.py +206 -0
- src/utils/document_validator.py +168 -0
- src/utils/error_rate_tracker.py +111 -0
- src/utils/error_utils.py +82 -0
- src/utils/json_parser.py +110 -0
- src/utils/network/circuit_breaker.py +79 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Main Solver - Problem-Solving System Controller
|
|
6
|
+
|
|
7
|
+
Based on Dual-Loop Architecture: Analysis Loop + Solve Loop
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import traceback
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
from ...services.config import parse_language
|
|
21
|
+
from .analysis_loop import InvestigateAgent, NoteAgent
|
|
22
|
+
|
|
23
|
+
# Dual-Loop Architecture
|
|
24
|
+
from .memory import CitationMemory, InvestigateMemory, SolveChainStep, SolveMemory
|
|
25
|
+
from .solve_loop import (
|
|
26
|
+
ManagerAgent,
|
|
27
|
+
PrecisionAnswerAgent,
|
|
28
|
+
ResponseAgent,
|
|
29
|
+
SolveAgent,
|
|
30
|
+
ToolAgent,
|
|
31
|
+
)
|
|
32
|
+
from .utils import ConfigValidator, PerformanceMonitor, SolveAgentLogger
|
|
33
|
+
from .utils.display_manager import get_display_manager
|
|
34
|
+
from .utils.token_tracker import TokenTracker
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MainSolver:
|
|
38
|
+
"""Problem-Solving System Controller"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
config_path: str | None = None,
|
|
43
|
+
api_key: str | None = None,
|
|
44
|
+
base_url: str | None = None,
|
|
45
|
+
api_version: str | None = None,
|
|
46
|
+
kb_name: str = "ai_textbook",
|
|
47
|
+
output_base_dir: str | None = None,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize MainSolver with lightweight setup.
|
|
51
|
+
Call ainit() to complete async initialization.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config_path: Config file path (default: config.yaml in current directory)
|
|
55
|
+
api_key: API key (if not provided, read from environment)
|
|
56
|
+
base_url: API URL (if not provided, read from environment)
|
|
57
|
+
api_version: API version (if not provided, read from environment)
|
|
58
|
+
kb_name: Knowledge base name
|
|
59
|
+
output_base_dir: Output base directory (optional, overrides config)
|
|
60
|
+
"""
|
|
61
|
+
# Store initialization parameters
|
|
62
|
+
self._config_path = config_path
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._base_url = base_url
|
|
65
|
+
self._api_version = api_version
|
|
66
|
+
self._kb_name = kb_name
|
|
67
|
+
self._output_base_dir = output_base_dir
|
|
68
|
+
|
|
69
|
+
# Initialize with None - will be set in ainit()
|
|
70
|
+
self.config = None
|
|
71
|
+
self.api_key = None
|
|
72
|
+
self.base_url = None
|
|
73
|
+
self.api_version = None
|
|
74
|
+
self.kb_name = kb_name
|
|
75
|
+
self.logger = None
|
|
76
|
+
self.monitor = None
|
|
77
|
+
self.token_tracker = None
|
|
78
|
+
|
|
79
|
+
async def ainit(self) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Complete the asynchronous second phase of MainSolver initialization.
|
|
82
|
+
|
|
83
|
+
This class uses a two-phase initialization pattern:
|
|
84
|
+
|
|
85
|
+
1. ``__init__`` performs only lightweight, synchronous setup and stores
|
|
86
|
+
constructor arguments. Attributes such as ``config``, ``api_key``,
|
|
87
|
+
``base_url``, ``api_version``, ``logger``, ``monitor``, and
|
|
88
|
+
``token_tracker`` are intentionally left as ``None``.
|
|
89
|
+
2. :meth:`ainit` performs all I/O-bound and asynchronous work required to
|
|
90
|
+
make the instance fully usable (e.g., loading configuration, wiring up
|
|
91
|
+
logging/monitoring, and preparing external-service clients).
|
|
92
|
+
|
|
93
|
+
You **must** call and await this method exactly once after constructing
|
|
94
|
+
``MainSolver`` and **before** invoking any other methods that rely on
|
|
95
|
+
configuration, logging, metrics, or API access. Using the object prior
|
|
96
|
+
to calling :meth:`ainit` may result in attributes still being ``None``,
|
|
97
|
+
which can lead to confusing runtime errors such as ``AttributeError``,
|
|
98
|
+
misconfigured API calls, missing logs/metrics, or incorrect output paths.
|
|
99
|
+
|
|
100
|
+
This async initialization pattern is used instead of performing all setup
|
|
101
|
+
in ``__init__`` so that object construction remains fast and synchronous,
|
|
102
|
+
while allowing potentially slow operations (disk I/O, network requests,
|
|
103
|
+
validation) to be awaited explicitly by the caller in an async context.
|
|
104
|
+
"""
|
|
105
|
+
config_path = self._config_path
|
|
106
|
+
api_key = self._api_key
|
|
107
|
+
base_url = self._base_url
|
|
108
|
+
api_version = self._api_version
|
|
109
|
+
kb_name = self._kb_name
|
|
110
|
+
output_base_dir = self._output_base_dir
|
|
111
|
+
|
|
112
|
+
# Load config from config directory (main.yaml unified config)
|
|
113
|
+
if config_path is None:
|
|
114
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
115
|
+
# Load main.yaml (solve_config.yaml is optional and will be merged if exists)
|
|
116
|
+
from ...services.config.loader import load_config_with_main_async
|
|
117
|
+
|
|
118
|
+
full_config = await load_config_with_main_async("main.yaml", project_root)
|
|
119
|
+
|
|
120
|
+
# Extract solve-specific config and build validator-compatible structure
|
|
121
|
+
solve_config = full_config.get("solve", {})
|
|
122
|
+
paths_config = full_config.get("paths", {})
|
|
123
|
+
|
|
124
|
+
# Build config structure expected by ConfigValidator
|
|
125
|
+
self.config = {
|
|
126
|
+
"system": {
|
|
127
|
+
"output_base_dir": paths_config.get("solve_output_dir", "./data/user/solve"),
|
|
128
|
+
"save_intermediate_results": solve_config.get(
|
|
129
|
+
"save_intermediate_results", True
|
|
130
|
+
),
|
|
131
|
+
"language": full_config.get("system", {}).get("language", "en"),
|
|
132
|
+
},
|
|
133
|
+
"agents": solve_config.get("agents", {}),
|
|
134
|
+
"logging": full_config.get("logging", {}),
|
|
135
|
+
"tools": full_config.get("tools", {}),
|
|
136
|
+
"paths": paths_config,
|
|
137
|
+
# Keep solve-specific settings accessible
|
|
138
|
+
"solve": solve_config,
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
# If custom config path provided, load it directly (for backward compatibility)
|
|
142
|
+
local_config = {}
|
|
143
|
+
if Path(config_path).exists():
|
|
144
|
+
try:
|
|
145
|
+
|
|
146
|
+
def load_local_config(path: str) -> dict:
|
|
147
|
+
with open(path, encoding="utf-8") as f:
|
|
148
|
+
return yaml.safe_load(f) or {}
|
|
149
|
+
|
|
150
|
+
local_config = await asyncio.to_thread(load_local_config, config_path)
|
|
151
|
+
except Exception:
|
|
152
|
+
# Config loading warning will be handled by config_loader
|
|
153
|
+
pass
|
|
154
|
+
self.config = local_config if isinstance(local_config, dict) else {}
|
|
155
|
+
|
|
156
|
+
if self.config is None or not isinstance(self.config, dict):
|
|
157
|
+
self.config = {}
|
|
158
|
+
|
|
159
|
+
# Override output directory config
|
|
160
|
+
if output_base_dir:
|
|
161
|
+
if "system" not in self.config:
|
|
162
|
+
self.config["system"] = {}
|
|
163
|
+
self.config["system"]["output_base_dir"] = str(output_base_dir)
|
|
164
|
+
|
|
165
|
+
# Note: log_dir and performance_log_dir are now in paths section from main.yaml
|
|
166
|
+
# Only override if explicitly needed
|
|
167
|
+
|
|
168
|
+
# Validate config
|
|
169
|
+
validator = ConfigValidator()
|
|
170
|
+
is_valid, errors, warnings = validator.validate(self.config)
|
|
171
|
+
if not is_valid:
|
|
172
|
+
raise ValueError(f"Config validation failed: {errors}")
|
|
173
|
+
|
|
174
|
+
# API config
|
|
175
|
+
if api_key is None or base_url is None or "llm" not in self.config:
|
|
176
|
+
try:
|
|
177
|
+
from ...services.llm.config import get_llm_config_async
|
|
178
|
+
|
|
179
|
+
llm_config = await get_llm_config_async()
|
|
180
|
+
if api_key is None:
|
|
181
|
+
api_key = llm_config.api_key
|
|
182
|
+
if base_url is None:
|
|
183
|
+
base_url = llm_config.base_url
|
|
184
|
+
if api_version is None:
|
|
185
|
+
api_version = getattr(llm_config, "api_version", None)
|
|
186
|
+
|
|
187
|
+
# Ensure LLM config is populated in self.config for agents
|
|
188
|
+
if "llm" not in self.config:
|
|
189
|
+
self.config["llm"] = {}
|
|
190
|
+
|
|
191
|
+
# Update config with complete details (binding, model, etc.)
|
|
192
|
+
from dataclasses import asdict
|
|
193
|
+
|
|
194
|
+
self.config["llm"].update(asdict(llm_config))
|
|
195
|
+
|
|
196
|
+
except ValueError as e:
|
|
197
|
+
raise ValueError(f"LLM config error: {e!s}")
|
|
198
|
+
|
|
199
|
+
# Check if API key is required
|
|
200
|
+
# Local LLM servers (Ollama, LM Studio, etc.) don't need API keys
|
|
201
|
+
from src.services.llm import is_local_llm_server
|
|
202
|
+
|
|
203
|
+
if not api_key and not is_local_llm_server(base_url):
|
|
204
|
+
raise ValueError("API key not set. Provide api_key param or set LLM_API_KEY in .env")
|
|
205
|
+
|
|
206
|
+
# For local servers, use a placeholder key if none provided
|
|
207
|
+
if not api_key and is_local_llm_server(base_url):
|
|
208
|
+
api_key = "sk-no-key-required"
|
|
209
|
+
|
|
210
|
+
self.api_key = api_key
|
|
211
|
+
self.base_url = base_url
|
|
212
|
+
self.api_version = api_version
|
|
213
|
+
self.kb_name = kb_name
|
|
214
|
+
|
|
215
|
+
# Initialize logging system
|
|
216
|
+
logging_config = self.config.get("logging", {})
|
|
217
|
+
# Get log_dir from paths (user_log_dir from main.yaml) or logging config
|
|
218
|
+
log_dir = (
|
|
219
|
+
self.config.get("paths", {}).get("user_log_dir")
|
|
220
|
+
or self.config.get("paths", {}).get("log_dir")
|
|
221
|
+
or logging_config.get("log_dir")
|
|
222
|
+
)
|
|
223
|
+
self.logger = SolveAgentLogger(
|
|
224
|
+
name="Solver",
|
|
225
|
+
level=logging_config.get("level", "INFO"),
|
|
226
|
+
log_dir=log_dir,
|
|
227
|
+
console_output=logging_config.get("console_output", True),
|
|
228
|
+
file_output=logging_config.get("save_to_file", True),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Attach display manager for TUI and frontend status updates
|
|
232
|
+
self.logger.display_manager = get_display_manager()
|
|
233
|
+
|
|
234
|
+
# Initialize performance monitor (disabled by default - performance logging is deprecated)
|
|
235
|
+
monitoring_config = self.config.get("monitoring", {})
|
|
236
|
+
# Disable performance monitor by default to avoid creating performance directory
|
|
237
|
+
self.monitor = PerformanceMonitor(
|
|
238
|
+
enabled=False,
|
|
239
|
+
save_dir=None, # Disabled - performance logging is deprecated
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Initialize Token tracker
|
|
243
|
+
self.token_tracker = TokenTracker(prefer_tiktoken=True)
|
|
244
|
+
|
|
245
|
+
# Connect token_tracker to display_manager for real-time updates
|
|
246
|
+
if self.logger.display_manager:
|
|
247
|
+
self.token_tracker.set_on_usage_added_callback(
|
|
248
|
+
self.logger.display_manager.update_token_stats
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.logger.section("Dual-Loop Solver Initializing")
|
|
252
|
+
self.logger.info(f"Knowledge Base: {kb_name}")
|
|
253
|
+
|
|
254
|
+
# Initialize Agents
|
|
255
|
+
self._init_agents()
|
|
256
|
+
|
|
257
|
+
self.logger.success("Solver ready")
|
|
258
|
+
|
|
259
|
+
def _deep_merge(self, base: dict, update: dict) -> dict:
|
|
260
|
+
"""Deep merge two dictionaries"""
|
|
261
|
+
if base is None:
|
|
262
|
+
base = {}
|
|
263
|
+
if update is None:
|
|
264
|
+
update = {}
|
|
265
|
+
|
|
266
|
+
result = base.copy() if base else {}
|
|
267
|
+
for key, value in update.items():
|
|
268
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
269
|
+
result[key] = self._deep_merge(result[key], value)
|
|
270
|
+
else:
|
|
271
|
+
result[key] = value
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
def _init_agents(self):
|
|
275
|
+
"""Initialize all Agents - Dual-Loop Architecture"""
|
|
276
|
+
self.logger.progress("Initializing agents...")
|
|
277
|
+
|
|
278
|
+
# Analysis Loop Agents
|
|
279
|
+
self.investigate_agent = InvestigateAgent(
|
|
280
|
+
config=self.config,
|
|
281
|
+
api_key=self.api_key,
|
|
282
|
+
base_url=self.base_url,
|
|
283
|
+
api_version=self.api_version,
|
|
284
|
+
token_tracker=self.token_tracker,
|
|
285
|
+
)
|
|
286
|
+
self.logger.info(" InvestigateAgent initialized")
|
|
287
|
+
|
|
288
|
+
self.note_agent = NoteAgent(
|
|
289
|
+
config=self.config,
|
|
290
|
+
api_key=self.api_key,
|
|
291
|
+
base_url=self.base_url,
|
|
292
|
+
api_version=self.api_version,
|
|
293
|
+
token_tracker=self.token_tracker,
|
|
294
|
+
)
|
|
295
|
+
self.logger.info(" NoteAgent initialized")
|
|
296
|
+
|
|
297
|
+
# Solve Loop Agents (lazy initialization)
|
|
298
|
+
self.manager_agent = None
|
|
299
|
+
self.solve_agent = None
|
|
300
|
+
self.tool_agent = None
|
|
301
|
+
self.response_agent = None
|
|
302
|
+
self.precision_answer_agent = None
|
|
303
|
+
self.logger.info(" Solve Loop agents (lazy init)")
|
|
304
|
+
|
|
305
|
+
async def solve(self, question: str, verbose: bool = True) -> dict[str, Any]:
|
|
306
|
+
"""
|
|
307
|
+
Main solving process - Dual-Loop Architecture
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
question: User question
|
|
311
|
+
verbose: Whether to print detailed info
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
dict: Solving result
|
|
315
|
+
"""
|
|
316
|
+
# Create output directory
|
|
317
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
318
|
+
output_base_dir = self.config.get("system", {}).get("output_base_dir", "./user/solve")
|
|
319
|
+
output_dir = os.path.join(output_base_dir, f"solve_{timestamp}")
|
|
320
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
321
|
+
|
|
322
|
+
# Add task log file handler
|
|
323
|
+
task_log_file = os.path.join(output_dir, "task.log")
|
|
324
|
+
self.logger.add_task_log_handler(task_log_file)
|
|
325
|
+
|
|
326
|
+
self.logger.section("Problem Solving Started")
|
|
327
|
+
self.logger.info(f"Question: {question[:100]}{'...' if len(question) > 100 else ''}")
|
|
328
|
+
self.logger.info(f"Output: {output_dir}")
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Execute dual-loop pipeline
|
|
332
|
+
result = await self._run_dual_loop_pipeline(question, output_dir)
|
|
333
|
+
|
|
334
|
+
# Add metadata
|
|
335
|
+
result["metadata"] = {
|
|
336
|
+
"mode": "dual_loop",
|
|
337
|
+
"timestamp": timestamp,
|
|
338
|
+
"output_dir": output_dir,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Save performance report
|
|
342
|
+
if self.config.get("monitoring", {}).get("enabled", True):
|
|
343
|
+
perf_report = self.monitor.generate_report()
|
|
344
|
+
perf_file = os.path.join(output_dir, "performance_report.json")
|
|
345
|
+
with open(perf_file, "w", encoding="utf-8") as f:
|
|
346
|
+
json.dump(perf_report, f, ensure_ascii=False, indent=2)
|
|
347
|
+
self.logger.debug(f"Performance report saved: {perf_file}")
|
|
348
|
+
|
|
349
|
+
# Output cost report
|
|
350
|
+
if self.token_tracker:
|
|
351
|
+
cost_summary = self.token_tracker.get_summary()
|
|
352
|
+
if cost_summary["total_calls"] > 0:
|
|
353
|
+
cost_text = self.token_tracker.format_summary()
|
|
354
|
+
self.logger.info(f"\n{cost_text}")
|
|
355
|
+
|
|
356
|
+
cost_file = os.path.join(output_dir, "cost_report.json")
|
|
357
|
+
self.token_tracker.save(cost_file)
|
|
358
|
+
self.logger.debug(f"Cost report saved: {cost_file}")
|
|
359
|
+
|
|
360
|
+
self.token_tracker.reset()
|
|
361
|
+
|
|
362
|
+
self.logger.success("Problem solving completed")
|
|
363
|
+
self.logger.remove_task_log_handlers()
|
|
364
|
+
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.logger.error(f"Solving failed: {e!s}")
|
|
369
|
+
self.logger.error(traceback.format_exc())
|
|
370
|
+
self.logger.remove_task_log_handlers()
|
|
371
|
+
raise
|
|
372
|
+
|
|
373
|
+
finally:
|
|
374
|
+
if hasattr(self, "logger"):
|
|
375
|
+
self.logger.shutdown()
|
|
376
|
+
|
|
377
|
+
async def _run_dual_loop_pipeline(self, question: str, output_dir: str) -> dict[str, Any]:
|
|
378
|
+
"""
|
|
379
|
+
Dual-Loop Pipeline:
|
|
380
|
+
1) Analysis Loop: Investigate → Note
|
|
381
|
+
2) Solve Loop: Plan → Manager → Solve → Check → Format
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
self.logger.info("Pipeline: Analysis Loop → Solve Loop")
|
|
385
|
+
|
|
386
|
+
# ========== Analysis Loop ==========
|
|
387
|
+
self.logger.stage("Analysis Loop", "start", "Understanding the question")
|
|
388
|
+
|
|
389
|
+
investigate_memory = InvestigateMemory.load_or_create(
|
|
390
|
+
output_dir=output_dir, user_question=question
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
citation_memory = CitationMemory.load_or_create(output_dir=output_dir)
|
|
394
|
+
|
|
395
|
+
# Read max_iterations from solve.agents.investigate_agent config (authoritative source)
|
|
396
|
+
agent_config = self.config.get("solve", {}).get("agents", {}).get("investigate_agent", {})
|
|
397
|
+
max_analysis_iterations = agent_config.get("max_iterations", 5)
|
|
398
|
+
self.logger.log_stage_progress(
|
|
399
|
+
"AnalysisLoop", "start", f"max_iterations={max_analysis_iterations}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
analysis_completed = False
|
|
403
|
+
|
|
404
|
+
# Analysis Loop iterations
|
|
405
|
+
for i in range(max_analysis_iterations):
|
|
406
|
+
self.logger.log_stage_progress("AnalysisLoop", "running", f"round={i + 1}")
|
|
407
|
+
|
|
408
|
+
# 1. Investigate: Generate queries and call tools
|
|
409
|
+
with self.monitor.track(f"analysis_investigate_{i + 1}"):
|
|
410
|
+
investigate_result = await self.investigate_agent.process(
|
|
411
|
+
question=question,
|
|
412
|
+
memory=investigate_memory,
|
|
413
|
+
citation_memory=citation_memory,
|
|
414
|
+
kb_name=self.kb_name,
|
|
415
|
+
output_dir=output_dir,
|
|
416
|
+
verbose=False,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
knowledge_ids: list[str] = investigate_result.get("knowledge_item_ids", [])
|
|
420
|
+
should_stop = investigate_result.get("should_stop", False)
|
|
421
|
+
reasoning = investigate_result.get("reasoning", "")
|
|
422
|
+
actions = investigate_result.get("actions", [])
|
|
423
|
+
|
|
424
|
+
self.logger.debug(f" [Investigate] Reasoning: {reasoning or 'N/A'}")
|
|
425
|
+
|
|
426
|
+
if hasattr(self, "_send_progress_update"):
|
|
427
|
+
queries = [action.get("query", "") for action in actions if action.get("query")]
|
|
428
|
+
self._send_progress_update("investigate", {"round": i + 1, "queries": queries})
|
|
429
|
+
|
|
430
|
+
if actions:
|
|
431
|
+
for action in actions:
|
|
432
|
+
tool_label = action["tool_type"]
|
|
433
|
+
query = action.get("query") or ""
|
|
434
|
+
cite_id = action.get("cite_id")
|
|
435
|
+
suffix = f" → cite_id={cite_id}" if cite_id else ""
|
|
436
|
+
self.logger.info(f" Tool: {tool_label} | {query[:50]}{suffix}")
|
|
437
|
+
else:
|
|
438
|
+
self.logger.debug(" No queries generated this round")
|
|
439
|
+
|
|
440
|
+
# 2. Note: Generate notes (if new knowledge exists)
|
|
441
|
+
if knowledge_ids:
|
|
442
|
+
self.logger.log_stage_progress("Note", "start")
|
|
443
|
+
|
|
444
|
+
with self.monitor.track(f"analysis_note_{i + 1}"):
|
|
445
|
+
note_result = await self.note_agent.process(
|
|
446
|
+
question=question,
|
|
447
|
+
memory=investigate_memory,
|
|
448
|
+
new_knowledge_ids=knowledge_ids,
|
|
449
|
+
citation_memory=citation_memory,
|
|
450
|
+
output_dir=output_dir,
|
|
451
|
+
verbose=False,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if note_result.get("success"):
|
|
455
|
+
processed = note_result.get("processed_items", 0)
|
|
456
|
+
self.logger.info(f" Note: {processed} items processed")
|
|
457
|
+
self.logger.log_stage_progress("Note", "complete")
|
|
458
|
+
else:
|
|
459
|
+
self.logger.warning(f" Note failed: {note_result.get('reason', 'unknown')}")
|
|
460
|
+
self.logger.log_stage_progress("Note", "error")
|
|
461
|
+
|
|
462
|
+
# Update Token stats
|
|
463
|
+
self.logger.update_token_stats(self.token_tracker.get_summary())
|
|
464
|
+
|
|
465
|
+
# 3. Check stop condition
|
|
466
|
+
if should_stop:
|
|
467
|
+
analysis_completed = True
|
|
468
|
+
self.logger.log_stage_progress(
|
|
469
|
+
"AnalysisLoop",
|
|
470
|
+
"complete",
|
|
471
|
+
f"rounds={i + 1}, knowledge={len(investigate_memory.knowledge_chain)}",
|
|
472
|
+
)
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
if not analysis_completed:
|
|
476
|
+
self.logger.log_stage_progress(
|
|
477
|
+
"AnalysisLoop",
|
|
478
|
+
"warning",
|
|
479
|
+
f"max_iterations({max_analysis_iterations}) reached, knowledge={len(investigate_memory.knowledge_chain)}",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Update investigate_memory metadata
|
|
483
|
+
investigate_memory.metadata["total_iterations"] = i + 1
|
|
484
|
+
investigate_memory.metadata["total_knowledge_items"] = len(
|
|
485
|
+
investigate_memory.knowledge_chain
|
|
486
|
+
)
|
|
487
|
+
investigate_memory.reflections.remaining_questions = []
|
|
488
|
+
|
|
489
|
+
if analysis_completed:
|
|
490
|
+
investigate_memory.metadata["coverage_rate"] = 1.0
|
|
491
|
+
investigate_memory.metadata["avg_confidence"] = 0.9
|
|
492
|
+
else:
|
|
493
|
+
coverage = min(
|
|
494
|
+
1.0, len(investigate_memory.knowledge_chain) / max(1, max_analysis_iterations)
|
|
495
|
+
)
|
|
496
|
+
investigate_memory.metadata["coverage_rate"] = coverage
|
|
497
|
+
investigate_memory.metadata["avg_confidence"] = 0.6
|
|
498
|
+
|
|
499
|
+
investigate_memory.save()
|
|
500
|
+
|
|
501
|
+
# ========== Solve Loop ==========
|
|
502
|
+
self.logger.stage("Solve Loop", "start", "Generating solution")
|
|
503
|
+
|
|
504
|
+
solve_memory = SolveMemory.load_or_create(output_dir=output_dir, user_question=question)
|
|
505
|
+
|
|
506
|
+
# Initialize Solve Loop Agents (if not yet initialized)
|
|
507
|
+
if self.manager_agent is None:
|
|
508
|
+
self.logger.progress("Initializing Solve Loop agents...")
|
|
509
|
+
self.manager_agent = ManagerAgent(
|
|
510
|
+
self.config,
|
|
511
|
+
self.api_key,
|
|
512
|
+
self.base_url,
|
|
513
|
+
api_version=self.api_version,
|
|
514
|
+
token_tracker=self.token_tracker,
|
|
515
|
+
)
|
|
516
|
+
self.solve_agent = SolveAgent(
|
|
517
|
+
self.config,
|
|
518
|
+
self.api_key,
|
|
519
|
+
self.base_url,
|
|
520
|
+
api_version=self.api_version,
|
|
521
|
+
token_tracker=self.token_tracker,
|
|
522
|
+
)
|
|
523
|
+
self.tool_agent = ToolAgent(
|
|
524
|
+
self.config,
|
|
525
|
+
self.api_key,
|
|
526
|
+
self.base_url,
|
|
527
|
+
api_version=self.api_version,
|
|
528
|
+
token_tracker=self.token_tracker,
|
|
529
|
+
)
|
|
530
|
+
self.response_agent = ResponseAgent(
|
|
531
|
+
self.config,
|
|
532
|
+
self.api_key,
|
|
533
|
+
self.base_url,
|
|
534
|
+
api_version=self.api_version,
|
|
535
|
+
token_tracker=self.token_tracker,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
precision_enabled = (
|
|
539
|
+
self.config.get("agents", {})
|
|
540
|
+
.get("precision_answer_agent", {})
|
|
541
|
+
.get("enabled", False)
|
|
542
|
+
)
|
|
543
|
+
if precision_enabled:
|
|
544
|
+
self.precision_answer_agent = PrecisionAnswerAgent(
|
|
545
|
+
self.config,
|
|
546
|
+
self.api_key,
|
|
547
|
+
self.base_url,
|
|
548
|
+
api_version=self.api_version,
|
|
549
|
+
token_tracker=self.token_tracker,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# 1. Plan: Generate solving plan
|
|
553
|
+
self.logger.info("Plan: Generating solution strategy...")
|
|
554
|
+
|
|
555
|
+
plan_result = None
|
|
556
|
+
for attempt in range(2):
|
|
557
|
+
try:
|
|
558
|
+
with self.monitor.track(f"solve_plan_attempt_{attempt + 1}"):
|
|
559
|
+
plan_result = await self.manager_agent.process(
|
|
560
|
+
question=question,
|
|
561
|
+
investigate_memory=investigate_memory,
|
|
562
|
+
solve_memory=solve_memory,
|
|
563
|
+
verbose=(attempt > 0),
|
|
564
|
+
)
|
|
565
|
+
num_steps = plan_result.get("num_steps") or plan_result.get("steps_count", 0)
|
|
566
|
+
self.logger.log_stage_progress("Plan", "complete", f"steps={num_steps}")
|
|
567
|
+
self.logger.update_token_stats(self.token_tracker.get_summary())
|
|
568
|
+
break
|
|
569
|
+
except Exception as e:
|
|
570
|
+
if attempt == 0:
|
|
571
|
+
self.logger.error(f"ManagerAgent attempt {attempt + 1} failed: {e!s}")
|
|
572
|
+
self.logger.warning("Retrying plan generation...")
|
|
573
|
+
solve_memory = SolveMemory.load_or_create(
|
|
574
|
+
output_dir=output_dir, user_question=question
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
self.logger.error(f"ManagerAgent attempt {attempt + 1} also failed")
|
|
578
|
+
raise ValueError(f"ManagerAgent failed after retry: {e!s}")
|
|
579
|
+
|
|
580
|
+
if plan_result is None:
|
|
581
|
+
raise ValueError("ManagerAgent failed to generate plan")
|
|
582
|
+
|
|
583
|
+
# 2. Solve Loop - Execute steps
|
|
584
|
+
self.logger.info("Solve: Executing solution steps...")
|
|
585
|
+
max_correction_iterations = self.config.get("system", {}).get(
|
|
586
|
+
"max_solve_correction_iterations", 3
|
|
587
|
+
)
|
|
588
|
+
total_planned_steps = len(solve_memory.solve_chains)
|
|
589
|
+
self.logger.log_stage_progress(
|
|
590
|
+
"SolveLoop",
|
|
591
|
+
"start",
|
|
592
|
+
f"planned_steps={total_planned_steps}, max_corrections={max_correction_iterations}",
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
for step_index, step in enumerate(solve_memory.solve_chains, 1):
|
|
596
|
+
if step.status in ("waiting_response", "done"):
|
|
597
|
+
continue
|
|
598
|
+
|
|
599
|
+
self.logger.info(f" Step {step_index}: {step.step_id}")
|
|
600
|
+
self.logger.debug(f" Target: {step.step_target[:80]}")
|
|
601
|
+
|
|
602
|
+
if hasattr(self, "_send_progress_update"):
|
|
603
|
+
self._send_progress_update(
|
|
604
|
+
"solve",
|
|
605
|
+
{
|
|
606
|
+
"step_index": step_index,
|
|
607
|
+
"step_id": step.step_id,
|
|
608
|
+
"step_target": step.step_target,
|
|
609
|
+
},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
self.logger.log_stage_progress("SolveLoop", "running", f"step={step.step_id}")
|
|
613
|
+
|
|
614
|
+
if self._has_pending_tool_calls(step):
|
|
615
|
+
await self._execute_tool_calls(step, solve_memory, citation_memory, output_dir)
|
|
616
|
+
|
|
617
|
+
iteration = 0
|
|
618
|
+
while iteration < max_correction_iterations:
|
|
619
|
+
iteration += 1
|
|
620
|
+
current_step = solve_memory.get_step(step.step_id) or step
|
|
621
|
+
|
|
622
|
+
with self.monitor.track(f"solve_execute_{step.step_id}_iter_{iteration}"):
|
|
623
|
+
solve_result = await self.solve_agent.process(
|
|
624
|
+
question=question,
|
|
625
|
+
current_step=current_step,
|
|
626
|
+
solve_memory=solve_memory,
|
|
627
|
+
investigate_memory=investigate_memory,
|
|
628
|
+
citation_memory=citation_memory,
|
|
629
|
+
kb_name=self.kb_name,
|
|
630
|
+
output_dir=output_dir,
|
|
631
|
+
verbose=False,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if solve_result.get("raw_llm_response"):
|
|
635
|
+
self.logger.log_stage_progress(
|
|
636
|
+
"SolveLoop", "running", f"step={step.step_id}, iteration={iteration}"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if solve_result.get("requested_calls"):
|
|
640
|
+
await self._execute_tool_calls(
|
|
641
|
+
current_step, solve_memory, citation_memory, output_dir
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
self.logger.update_token_stats(self.token_tracker.get_summary())
|
|
645
|
+
|
|
646
|
+
if solve_result.get("finish_requested"):
|
|
647
|
+
current_step = solve_memory.get_step(step.step_id) or step
|
|
648
|
+
if self._has_pending_tool_calls(current_step):
|
|
649
|
+
self.logger.debug(" Finish triggered but tools pending, continuing...")
|
|
650
|
+
continue
|
|
651
|
+
solve_memory.mark_step_waiting_response(current_step.step_id)
|
|
652
|
+
solve_memory.save()
|
|
653
|
+
self.logger.log_stage_progress(
|
|
654
|
+
"SolveLoop", "complete", f"step={current_step.step_id} ready for response"
|
|
655
|
+
)
|
|
656
|
+
break
|
|
657
|
+
else:
|
|
658
|
+
self.logger.warning(f" Step {step.step_id} max iterations reached")
|
|
659
|
+
solve_memory.mark_step_waiting_response(step.step_id)
|
|
660
|
+
solve_memory.save()
|
|
661
|
+
|
|
662
|
+
pending_steps = [
|
|
663
|
+
s.step_id
|
|
664
|
+
for s in solve_memory.solve_chains
|
|
665
|
+
if s.status not in ("waiting_response", "done")
|
|
666
|
+
]
|
|
667
|
+
if pending_steps:
|
|
668
|
+
self.logger.warning(f"Steps not ready for response: {', '.join(pending_steps)}")
|
|
669
|
+
|
|
670
|
+
self.logger.log_stage_progress(
|
|
671
|
+
"SolveLoop", "complete", f"steps_processed={total_planned_steps - len(pending_steps)}"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# 3. Response: Generate responses for each step
|
|
675
|
+
self.logger.info("Response: Generating step responses...")
|
|
676
|
+
self.logger.log_stage_progress("ResponseLoop", "start", "Generating responses")
|
|
677
|
+
|
|
678
|
+
accumulated_response = ""
|
|
679
|
+
for step in solve_memory.solve_chains:
|
|
680
|
+
if step.status == "done" and step.step_response:
|
|
681
|
+
accumulated_response += step.step_response + "\n\n"
|
|
682
|
+
|
|
683
|
+
for step in solve_memory.solve_chains:
|
|
684
|
+
if step.status != "waiting_response":
|
|
685
|
+
continue
|
|
686
|
+
|
|
687
|
+
original_step_index = next(
|
|
688
|
+
(
|
|
689
|
+
i + 1
|
|
690
|
+
for i, s in enumerate(solve_memory.solve_chains)
|
|
691
|
+
if s.step_id == step.step_id
|
|
692
|
+
),
|
|
693
|
+
0,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if hasattr(self, "_send_progress_update"):
|
|
697
|
+
self._send_progress_update(
|
|
698
|
+
"response",
|
|
699
|
+
{
|
|
700
|
+
"step_index": original_step_index,
|
|
701
|
+
"step_id": step.step_id,
|
|
702
|
+
"step_target": step.step_target,
|
|
703
|
+
},
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
with self.monitor.track(f"solve_response_{step.step_id}"):
|
|
707
|
+
response_result = await self.response_agent.process(
|
|
708
|
+
question=question,
|
|
709
|
+
step=step,
|
|
710
|
+
solve_memory=solve_memory,
|
|
711
|
+
investigate_memory=investigate_memory,
|
|
712
|
+
citation_memory=citation_memory,
|
|
713
|
+
output_dir=output_dir,
|
|
714
|
+
verbose=False,
|
|
715
|
+
accumulated_response=accumulated_response,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
step_response = response_result.get("step_response", "")
|
|
719
|
+
if step_response:
|
|
720
|
+
accumulated_response += step_response + "\n\n"
|
|
721
|
+
|
|
722
|
+
if response_result.get("raw_response"):
|
|
723
|
+
self.logger.log_stage_progress(
|
|
724
|
+
"ResponseLoop", "running", f"step={step.step_id} response generated"
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
self.logger.update_token_stats(self.token_tracker.get_summary())
|
|
728
|
+
|
|
729
|
+
self.logger.log_stage_progress("ResponseLoop", "complete", "All responses generated")
|
|
730
|
+
|
|
731
|
+
# 4. Finalize: Compile final answer
|
|
732
|
+
self.logger.info("Finalize: Compiling final answer...")
|
|
733
|
+
self.logger.log_stage_progress("Finalize", "start", "Compiling steps")
|
|
734
|
+
|
|
735
|
+
actual_total_steps = len(solve_memory.solve_chains)
|
|
736
|
+
completed_step_objs = [
|
|
737
|
+
step
|
|
738
|
+
for step in solve_memory.solve_chains
|
|
739
|
+
if step.status == "done" and step.step_response
|
|
740
|
+
]
|
|
741
|
+
completed_steps = len(completed_step_objs)
|
|
742
|
+
|
|
743
|
+
solve_memory.metadata["total_steps"] = actual_total_steps
|
|
744
|
+
solve_memory.metadata["completed_steps"] = completed_steps
|
|
745
|
+
solve_memory.save()
|
|
746
|
+
self.logger.info(f" Stats: {completed_steps}/{actual_total_steps} steps completed")
|
|
747
|
+
|
|
748
|
+
used_cite_ids = []
|
|
749
|
+
for step in completed_step_objs:
|
|
750
|
+
used_cite_ids.extend(step.used_citations)
|
|
751
|
+
used_cite_ids = list(dict.fromkeys(used_cite_ids))
|
|
752
|
+
|
|
753
|
+
step_responses = [step.step_response for step in completed_step_objs]
|
|
754
|
+
final_answer = "\n\n".join(step_responses)
|
|
755
|
+
|
|
756
|
+
# Get language setting from config (unified in config/main.yaml system.language)
|
|
757
|
+
language = self.config.get("system", {}).get("language", "zh")
|
|
758
|
+
lang_code = parse_language(language)
|
|
759
|
+
|
|
760
|
+
# Check if citations are enabled
|
|
761
|
+
enable_citations = self.config.get("system", {}).get("enable_citations", True)
|
|
762
|
+
|
|
763
|
+
citations_section = ""
|
|
764
|
+
if enable_citations and citation_memory:
|
|
765
|
+
citations_section = citation_memory.format_citations_markdown(
|
|
766
|
+
used_cite_ids=used_cite_ids, language=lang_code
|
|
767
|
+
)
|
|
768
|
+
if citations_section:
|
|
769
|
+
final_answer = f"{final_answer}\n\n---\n\n{citations_section}"
|
|
770
|
+
|
|
771
|
+
format_result = {
|
|
772
|
+
"final_answer": final_answer.strip(),
|
|
773
|
+
"citations": used_cite_ids,
|
|
774
|
+
"metadata": {
|
|
775
|
+
"refined_steps": len(completed_step_objs),
|
|
776
|
+
"total_steps": actual_total_steps,
|
|
777
|
+
"citations_section": bool(citations_section),
|
|
778
|
+
},
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
self.logger.info(f" Final answer: {len(format_result['final_answer'])} chars")
|
|
782
|
+
self.logger.info(f" Citations: {len(format_result['citations'])}")
|
|
783
|
+
|
|
784
|
+
# 5. Precision Answer (if enabled)
|
|
785
|
+
precision_answer_enabled = (
|
|
786
|
+
self.config.get("agents", {}).get("precision_answer_agent", {}).get("enabled", False)
|
|
787
|
+
)
|
|
788
|
+
final_answer_content = format_result["final_answer"]
|
|
789
|
+
|
|
790
|
+
if precision_answer_enabled and self.precision_answer_agent:
|
|
791
|
+
self.logger.info("PrecisionAnswer: Generating concise answer...")
|
|
792
|
+
with self.monitor.track("precision_answer"):
|
|
793
|
+
precision_result = await self.precision_answer_agent.process(
|
|
794
|
+
question=question, detailed_answer=format_result["final_answer"], verbose=False
|
|
795
|
+
)
|
|
796
|
+
if precision_result.get("needs_precision"):
|
|
797
|
+
precision_answer = precision_result.get("precision_answer", "")
|
|
798
|
+
self.logger.info(f" Precision answer: {len(precision_answer)} chars")
|
|
799
|
+
final_answer_content = f"## Concise Answer\n\n{precision_answer}\n\n---\n\n## Detailed Answer\n\n{format_result['final_answer']}"
|
|
800
|
+
else:
|
|
801
|
+
self.logger.debug(" No precision answer needed")
|
|
802
|
+
|
|
803
|
+
# Save final answer
|
|
804
|
+
final_answer_file = Path(output_dir) / "final_answer.md"
|
|
805
|
+
with open(final_answer_file, "w", encoding="utf-8") as f:
|
|
806
|
+
f.write(final_answer_content)
|
|
807
|
+
|
|
808
|
+
self.logger.success(f"Final answer saved: {final_answer_file}")
|
|
809
|
+
self.logger.log_stage_progress("Format", "complete", f"output={final_answer_file}")
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
"question": question,
|
|
813
|
+
"output_dir": output_dir,
|
|
814
|
+
"final_answer": final_answer_content,
|
|
815
|
+
"output_md": str(final_answer_file),
|
|
816
|
+
"output_json": str(Path(output_dir) / "solve_chain.json"),
|
|
817
|
+
"formatted_solution": final_answer_content,
|
|
818
|
+
"citations": format_result["citations"],
|
|
819
|
+
"pipeline": "reworked",
|
|
820
|
+
"total_steps": solve_memory.metadata["total_steps"],
|
|
821
|
+
"analysis_iterations": investigate_memory.metadata.get("total_iterations", 0),
|
|
822
|
+
"solve_steps": solve_memory.metadata["completed_steps"],
|
|
823
|
+
"metadata": {
|
|
824
|
+
"coverage_rate": investigate_memory.metadata.get("coverage_rate", 0.0),
|
|
825
|
+
"avg_confidence": investigate_memory.metadata.get("avg_confidence", 0.0),
|
|
826
|
+
"total_steps": solve_memory.metadata["total_steps"],
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async def _execute_tool_calls(
|
|
831
|
+
self,
|
|
832
|
+
step: SolveChainStep,
|
|
833
|
+
solve_memory: SolveMemory,
|
|
834
|
+
citation_memory: CitationMemory,
|
|
835
|
+
output_dir: str | None,
|
|
836
|
+
) -> dict[str, Any]:
|
|
837
|
+
tool_result = await self.tool_agent.process(
|
|
838
|
+
step=step,
|
|
839
|
+
solve_memory=solve_memory,
|
|
840
|
+
citation_memory=citation_memory,
|
|
841
|
+
kb_name=self.kb_name,
|
|
842
|
+
output_dir=output_dir,
|
|
843
|
+
verbose=False,
|
|
844
|
+
)
|
|
845
|
+
return tool_result
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def _has_pending_tool_calls(step: SolveChainStep) -> bool:
|
|
849
|
+
return any(call.status in {"pending", "running"} for call in step.tool_calls)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
if __name__ == "__main__":
|
|
853
|
+
from dotenv import load_dotenv
|
|
854
|
+
|
|
855
|
+
load_dotenv()
|
|
856
|
+
|
|
857
|
+
async def test():
|
|
858
|
+
solver = MainSolver(kb_name="ai_textbook")
|
|
859
|
+
result = await solver.solve(question="What is linear convolution?", verbose=True)
|
|
860
|
+
print(f"Output file: {result['output_md']}")
|
|
861
|
+
|
|
862
|
+
asyncio.run(test())
|