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,470 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
ToolAgent - Tool executor
|
|
5
|
+
Responsible for reading tool calls in solve-chain, actually executing tools and producing summary
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
15
|
+
if str(project_root) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(project_root))
|
|
17
|
+
|
|
18
|
+
from src.agents.base_agent import BaseAgent
|
|
19
|
+
from src.tools.code_executor import run_code
|
|
20
|
+
from src.tools.rag_tool import rag_search
|
|
21
|
+
from src.tools.web_search import web_search
|
|
22
|
+
|
|
23
|
+
from ..memory import CitationMemory, SolveChainStep, SolveMemory
|
|
24
|
+
from ..memory.solve_memory import ToolCallRecord
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ToolAgent(BaseAgent):
|
|
28
|
+
"""Execute tool calls and generate summary"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
config: dict[str, Any],
|
|
33
|
+
api_key: str,
|
|
34
|
+
base_url: str,
|
|
35
|
+
api_version: str | None = None,
|
|
36
|
+
token_tracker=None,
|
|
37
|
+
):
|
|
38
|
+
language = config.get("system", {}).get("language", "zh")
|
|
39
|
+
super().__init__(
|
|
40
|
+
module_name="solve",
|
|
41
|
+
agent_name="tool_agent",
|
|
42
|
+
api_key=api_key,
|
|
43
|
+
base_url=base_url,
|
|
44
|
+
api_version=api_version,
|
|
45
|
+
language=language,
|
|
46
|
+
config=config,
|
|
47
|
+
token_tracker=token_tracker,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _generate_code_from_intent(self, intent: str) -> str:
|
|
51
|
+
system_prompt = """
|
|
52
|
+
You are a Python code generator.
|
|
53
|
+
Generate ONLY executable Python code.
|
|
54
|
+
Do NOT include explanations.
|
|
55
|
+
Do NOT include markdown fences.
|
|
56
|
+
Do NOT include comments unless necessary.
|
|
57
|
+
The code must be self-contained and runnable.
|
|
58
|
+
"""
|
|
59
|
+
user_prompt = f"""
|
|
60
|
+
Task:
|
|
61
|
+
{intent}
|
|
62
|
+
|
|
63
|
+
Rules:
|
|
64
|
+
- Output only Python code
|
|
65
|
+
- No ``` fences
|
|
66
|
+
- No natural language
|
|
67
|
+
"""
|
|
68
|
+
code = await self.call_llm(
|
|
69
|
+
system_prompt=system_prompt,
|
|
70
|
+
user_prompt=user_prompt,
|
|
71
|
+
verbose=False,
|
|
72
|
+
)
|
|
73
|
+
if "```" in code:
|
|
74
|
+
raise ValueError("LLM returned markdown code fences, which is forbidden")
|
|
75
|
+
if len(code) > 8000:
|
|
76
|
+
raise ValueError("Generated code too large")
|
|
77
|
+
|
|
78
|
+
return code.strip()
|
|
79
|
+
|
|
80
|
+
async def process(
|
|
81
|
+
self,
|
|
82
|
+
step: SolveChainStep,
|
|
83
|
+
solve_memory: SolveMemory,
|
|
84
|
+
citation_memory: CitationMemory,
|
|
85
|
+
kb_name: str,
|
|
86
|
+
output_dir: str | None = None,
|
|
87
|
+
verbose: bool = True,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
pending = [
|
|
90
|
+
call
|
|
91
|
+
for call in step.tool_calls
|
|
92
|
+
if call.tool_type not in {"none", "finish"} and call.status in {"pending", "running"}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
if not pending:
|
|
96
|
+
return {"step_id": step.step_id, "executed": [], "status": "idle"}
|
|
97
|
+
|
|
98
|
+
logs: list[dict[str, Any]] = []
|
|
99
|
+
base_dir = Path(output_dir).resolve() if output_dir else Path().resolve()
|
|
100
|
+
artifacts_dir = base_dir / "artifacts"
|
|
101
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
self.logger.log_stage_progress(
|
|
104
|
+
"Tool", "start", f"step={step.step_id}, pending_calls={len(pending)}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for record in pending:
|
|
108
|
+
call_label = f"{record.tool_type} | cite={record.cite_id or '-'}"
|
|
109
|
+
self.logger.log_stage_progress(
|
|
110
|
+
"Tool", "running", f"step={step.step_id}, call={call_label}"
|
|
111
|
+
)
|
|
112
|
+
start_ts = time.time()
|
|
113
|
+
try:
|
|
114
|
+
raw_answer, metadata = await self._execute_single_call(
|
|
115
|
+
record=record,
|
|
116
|
+
kb_name=kb_name,
|
|
117
|
+
output_dir=output_dir,
|
|
118
|
+
artifacts_dir=str(artifacts_dir),
|
|
119
|
+
verbose=verbose,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check if code execution failed
|
|
123
|
+
is_failed = False
|
|
124
|
+
if record.tool_type == "code_execution":
|
|
125
|
+
is_failed = metadata.get("execution_failed", False)
|
|
126
|
+
exit_code = metadata.get("exit_code", 0)
|
|
127
|
+
if exit_code != 0:
|
|
128
|
+
is_failed = True
|
|
129
|
+
|
|
130
|
+
summary = await self._summarize_tool_result(
|
|
131
|
+
tool_type=record.tool_type, query=record.query, raw_answer=raw_answer
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Set correct status based on execution result
|
|
135
|
+
status = "failed" if is_failed else "success"
|
|
136
|
+
solve_memory.update_tool_call_result(
|
|
137
|
+
step_id=step.step_id,
|
|
138
|
+
call_id=record.call_id,
|
|
139
|
+
raw_answer=raw_answer,
|
|
140
|
+
summary=summary,
|
|
141
|
+
status=status,
|
|
142
|
+
metadata=metadata, # Pass metadata to ensure artifacts are saved
|
|
143
|
+
)
|
|
144
|
+
citation_memory.update_citation(
|
|
145
|
+
cite_id=record.cite_id,
|
|
146
|
+
raw_result=raw_answer,
|
|
147
|
+
content=summary,
|
|
148
|
+
metadata=metadata,
|
|
149
|
+
step_id=step.step_id,
|
|
150
|
+
)
|
|
151
|
+
elapsed_ms = (time.time() - start_ts) * 1000
|
|
152
|
+
self.logger.log_tool_call(
|
|
153
|
+
tool_name=record.tool_type,
|
|
154
|
+
tool_input={
|
|
155
|
+
"step_id": step.step_id,
|
|
156
|
+
"call_id": record.call_id,
|
|
157
|
+
"query": record.query,
|
|
158
|
+
},
|
|
159
|
+
tool_output=raw_answer,
|
|
160
|
+
status="success",
|
|
161
|
+
elapsed_ms=elapsed_ms,
|
|
162
|
+
step_id=step.step_id,
|
|
163
|
+
cite_id=record.cite_id,
|
|
164
|
+
)
|
|
165
|
+
logs.append(
|
|
166
|
+
{
|
|
167
|
+
"call_id": record.call_id,
|
|
168
|
+
"tool_type": record.tool_type,
|
|
169
|
+
"cite_id": record.cite_id,
|
|
170
|
+
"status": "success",
|
|
171
|
+
"summary": summary,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
error_msg = str(e)
|
|
176
|
+
elapsed_ms = (time.time() - start_ts) * 1000
|
|
177
|
+
solve_memory.update_tool_call_result(
|
|
178
|
+
step_id=step.step_id,
|
|
179
|
+
call_id=record.call_id,
|
|
180
|
+
raw_answer=error_msg,
|
|
181
|
+
summary=error_msg[:200],
|
|
182
|
+
status="failed",
|
|
183
|
+
metadata={"error": True},
|
|
184
|
+
)
|
|
185
|
+
citation_memory.update_citation(
|
|
186
|
+
cite_id=record.cite_id,
|
|
187
|
+
raw_result=error_msg,
|
|
188
|
+
content=error_msg[:200],
|
|
189
|
+
metadata={"error": True},
|
|
190
|
+
step_id=step.step_id,
|
|
191
|
+
)
|
|
192
|
+
self.logger.log_tool_call(
|
|
193
|
+
tool_name=record.tool_type,
|
|
194
|
+
tool_input={
|
|
195
|
+
"step_id": step.step_id,
|
|
196
|
+
"call_id": record.call_id,
|
|
197
|
+
"query": record.query,
|
|
198
|
+
},
|
|
199
|
+
tool_output=error_msg,
|
|
200
|
+
status="failed",
|
|
201
|
+
elapsed_ms=elapsed_ms,
|
|
202
|
+
step_id=step.step_id,
|
|
203
|
+
cite_id=record.cite_id,
|
|
204
|
+
)
|
|
205
|
+
self.logger.log_stage_progress(
|
|
206
|
+
"Tool", "warning", f"step={step.step_id}, call={call_label}, error={error_msg}"
|
|
207
|
+
)
|
|
208
|
+
logs.append(
|
|
209
|
+
{
|
|
210
|
+
"call_id": record.call_id,
|
|
211
|
+
"tool_type": record.tool_type,
|
|
212
|
+
"cite_id": record.cite_id,
|
|
213
|
+
"status": "failed",
|
|
214
|
+
"error": error_msg,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
solve_memory.save()
|
|
219
|
+
citation_memory.save()
|
|
220
|
+
|
|
221
|
+
self.logger.log_stage_progress(
|
|
222
|
+
"Tool", "complete", f"step={step.step_id}, executed={len(logs)}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return {"step_id": step.step_id, "executed": logs, "status": "completed"}
|
|
226
|
+
|
|
227
|
+
async def _execute_single_call(
|
|
228
|
+
self,
|
|
229
|
+
record: ToolCallRecord,
|
|
230
|
+
kb_name: str,
|
|
231
|
+
output_dir: str | None,
|
|
232
|
+
artifacts_dir: str,
|
|
233
|
+
verbose: bool,
|
|
234
|
+
) -> tuple[str, dict[str, Any]]:
|
|
235
|
+
tool_type = record.tool_type
|
|
236
|
+
query = record.query
|
|
237
|
+
|
|
238
|
+
if tool_type == "rag_naive":
|
|
239
|
+
result = await rag_search(query=query, kb_name=kb_name, mode="naive")
|
|
240
|
+
answer = result.get("answer", "")
|
|
241
|
+
source, auto_sources = self._infer_sources(answer)
|
|
242
|
+
metadata = {"source": source, "auto_sources": auto_sources, "mode": "naive"}
|
|
243
|
+
return answer, metadata
|
|
244
|
+
|
|
245
|
+
if tool_type == "rag_hybrid":
|
|
246
|
+
result = await rag_search(query=query, kb_name=kb_name, mode="hybrid")
|
|
247
|
+
answer = result.get("answer", "")
|
|
248
|
+
source, auto_sources = self._infer_sources(answer)
|
|
249
|
+
metadata = {"source": source, "auto_sources": auto_sources, "mode": "hybrid"}
|
|
250
|
+
return answer, metadata
|
|
251
|
+
|
|
252
|
+
if tool_type == "web_search":
|
|
253
|
+
result = web_search(query=query, output_dir=output_dir, verbose=verbose)
|
|
254
|
+
answer = result.get("answer") or result.get("summary") or ""
|
|
255
|
+
used_citation_ids = self._extract_answer_citations(answer)
|
|
256
|
+
filtered_citations = self._select_web_citations(used_citation_ids, result)
|
|
257
|
+
metadata = {"result_file": result.get("result_file"), "citations": filtered_citations}
|
|
258
|
+
return answer, metadata
|
|
259
|
+
|
|
260
|
+
if tool_type == "code_execution":
|
|
261
|
+
artifacts_path = Path(artifacts_dir)
|
|
262
|
+
before_snapshot = self._snapshot_image_artifacts(artifacts_path)
|
|
263
|
+
|
|
264
|
+
if not query or not query.strip():
|
|
265
|
+
# If code is empty, directly return failure without execution
|
|
266
|
+
raw_answer = "【⚠️ Code execution failed】\nError: No valid code input received (Code is empty). Please check if [QUERY] contains a markdown code block."
|
|
267
|
+
metadata = {
|
|
268
|
+
"exit_code": 1,
|
|
269
|
+
"artifacts": [],
|
|
270
|
+
"artifact_paths": [],
|
|
271
|
+
"artifact_rel_paths": [],
|
|
272
|
+
"work_dir": artifacts_dir,
|
|
273
|
+
"execution_failed": True,
|
|
274
|
+
}
|
|
275
|
+
return raw_answer, metadata
|
|
276
|
+
|
|
277
|
+
code = await self._generate_code_from_intent(query)
|
|
278
|
+
|
|
279
|
+
exec_result = await run_code(
|
|
280
|
+
language="python",
|
|
281
|
+
code=code,
|
|
282
|
+
timeout=self.agent_config.get("code_timeout", 20),
|
|
283
|
+
assets_dir=artifacts_dir,
|
|
284
|
+
)
|
|
285
|
+
raw_answer = self._format_code_answer(exec_result, artifacts_dir)
|
|
286
|
+
exit_code = exec_result.get("exit_code", 0)
|
|
287
|
+
|
|
288
|
+
# Check if code execution failed
|
|
289
|
+
is_failed = exit_code != 0
|
|
290
|
+
if is_failed:
|
|
291
|
+
stderr = exec_result.get("stderr", "")
|
|
292
|
+
# Add obvious error prefix at the beginning of raw_answer
|
|
293
|
+
error_prefix = "【⚠️ Code execution failed】\n"
|
|
294
|
+
if "FileNotFoundError" in stderr and "artifacts/" in stderr:
|
|
295
|
+
error_prefix += "Path error detected: Code uses 'artifacts/xxx.png', but working directory is already the artifacts directory.\n"
|
|
296
|
+
error_prefix += "Please use 'xxx.png' instead of 'artifacts/xxx.png'.\n\n"
|
|
297
|
+
raw_answer = error_prefix + raw_answer
|
|
298
|
+
|
|
299
|
+
new_image_paths = self._collect_new_image_artifacts(
|
|
300
|
+
artifacts_path=artifacts_path,
|
|
301
|
+
before_snapshot=before_snapshot,
|
|
302
|
+
output_dir=output_dir,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
metadata = {
|
|
306
|
+
"exit_code": exit_code,
|
|
307
|
+
"artifacts": exec_result.get("artifacts", []),
|
|
308
|
+
"artifact_paths": exec_result.get("artifact_paths", []),
|
|
309
|
+
"artifact_rel_paths": new_image_paths,
|
|
310
|
+
"work_dir": artifacts_dir,
|
|
311
|
+
"execution_failed": is_failed,
|
|
312
|
+
}
|
|
313
|
+
return raw_answer, metadata
|
|
314
|
+
|
|
315
|
+
raise ValueError(f"Unknown tool type: {tool_type}")
|
|
316
|
+
|
|
317
|
+
def _format_code_answer(self, exec_result: dict[str, Any], artifacts_dir: str) -> str:
|
|
318
|
+
stdout = exec_result.get("stdout", "")
|
|
319
|
+
stderr = exec_result.get("stderr", "")
|
|
320
|
+
artifacts = exec_result.get("artifacts", [])
|
|
321
|
+
artifact_paths = exec_result.get("artifact_paths", [])
|
|
322
|
+
|
|
323
|
+
lines = [
|
|
324
|
+
"【Code Execution Result】",
|
|
325
|
+
f"Exit code: {exec_result.get('exit_code')}",
|
|
326
|
+
f"Elapsed time: {exec_result.get('elapsed_ms', 0):.2f} ms",
|
|
327
|
+
f"Working directory: {artifacts_dir}",
|
|
328
|
+
"",
|
|
329
|
+
"stdout:",
|
|
330
|
+
stdout or "(empty)",
|
|
331
|
+
"",
|
|
332
|
+
"stderr:",
|
|
333
|
+
stderr or "(empty)",
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
if artifacts:
|
|
337
|
+
lines.append("")
|
|
338
|
+
lines.append("Artifacts:")
|
|
339
|
+
for idx, artifact in enumerate(artifacts):
|
|
340
|
+
abs_path = (
|
|
341
|
+
artifact_paths[idx]
|
|
342
|
+
if idx < len(artifact_paths)
|
|
343
|
+
else str(Path(artifacts_dir) / artifact)
|
|
344
|
+
)
|
|
345
|
+
lines.append(f"- {abs_path}")
|
|
346
|
+
|
|
347
|
+
return "\n".join(lines)
|
|
348
|
+
|
|
349
|
+
async def _summarize_tool_result(self, tool_type: str, query: str, raw_answer: str) -> str:
|
|
350
|
+
system_prompt = self.get_prompt("system") if self.has_prompts() else None
|
|
351
|
+
if not system_prompt:
|
|
352
|
+
raise ValueError(
|
|
353
|
+
"ToolAgent missing system prompt, please configure system in prompts/{lang}/solve_loop/tool_agent.yaml"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
template = self.get_prompt("user_template") if self.has_prompts() else None
|
|
357
|
+
if not template:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
"ToolAgent missing user_template, please configure user_template in prompts/{lang}/solve_loop/tool_agent.yaml"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
user_prompt = template.format(
|
|
363
|
+
tool_type=tool_type, query=query, raw_answer=raw_answer[:2000]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
response = await self.call_llm(
|
|
367
|
+
user_prompt=user_prompt, system_prompt=system_prompt, verbose=False
|
|
368
|
+
)
|
|
369
|
+
return response.strip()
|
|
370
|
+
|
|
371
|
+
def _infer_sources(self, text: str) -> tuple[str, list[str]]:
|
|
372
|
+
if not text:
|
|
373
|
+
return "", []
|
|
374
|
+
matches = re.findall(r"(https?://[^\s\)\]]+)", text)
|
|
375
|
+
cleaned = []
|
|
376
|
+
for item in matches:
|
|
377
|
+
normalized = item.strip().strip(".,;:()[]{}")
|
|
378
|
+
if normalized and normalized not in cleaned:
|
|
379
|
+
cleaned.append(normalized)
|
|
380
|
+
return (", ".join(cleaned), cleaned)
|
|
381
|
+
|
|
382
|
+
def _extract_answer_citations(self, answer: str) -> list[str]:
|
|
383
|
+
if not answer:
|
|
384
|
+
return []
|
|
385
|
+
pattern = re.compile(r"\[(\d+)\]")
|
|
386
|
+
ids = pattern.findall(answer)
|
|
387
|
+
unique: list[str] = []
|
|
388
|
+
for cid in ids:
|
|
389
|
+
if cid not in unique:
|
|
390
|
+
unique.append(cid)
|
|
391
|
+
return unique
|
|
392
|
+
|
|
393
|
+
def _select_web_citations(
|
|
394
|
+
self, used_ids: list[str], result: dict[str, Any]
|
|
395
|
+
) -> list[dict[str, Any]]:
|
|
396
|
+
if not used_ids:
|
|
397
|
+
return []
|
|
398
|
+
raw_citations = result.get("citations") or []
|
|
399
|
+
search_results = result.get("search_results") or []
|
|
400
|
+
search_map = {item.get("url"): item for item in search_results if item.get("url")}
|
|
401
|
+
|
|
402
|
+
selected: list[dict[str, Any]] = []
|
|
403
|
+
for cid in used_ids:
|
|
404
|
+
matched = None
|
|
405
|
+
for raw in raw_citations:
|
|
406
|
+
ref_id = str(raw.get("id")) if raw.get("id") is not None else ""
|
|
407
|
+
ref_token = (raw.get("reference") or "").strip()
|
|
408
|
+
normalized_token = ref_token.strip("[]")
|
|
409
|
+
if cid == ref_id or cid == normalized_token:
|
|
410
|
+
matched = dict(raw)
|
|
411
|
+
break
|
|
412
|
+
if not matched:
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
url = matched.get("url")
|
|
416
|
+
if url and url in search_map:
|
|
417
|
+
fallback = search_map[url]
|
|
418
|
+
matched.setdefault("title", fallback.get("title", ""))
|
|
419
|
+
matched.setdefault("snippet", fallback.get("snippet", ""))
|
|
420
|
+
|
|
421
|
+
selected.append(
|
|
422
|
+
{
|
|
423
|
+
"id": matched.get("id") or (int(cid) if cid.isdigit() else cid),
|
|
424
|
+
"reference": matched.get("reference") or f"[{cid}]",
|
|
425
|
+
"url": matched.get("url"),
|
|
426
|
+
"title": matched.get("title", ""),
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
return selected
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------ #
|
|
432
|
+
# Artifacts helpers
|
|
433
|
+
# ------------------------------------------------------------------ #
|
|
434
|
+
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp"}
|
|
435
|
+
|
|
436
|
+
def _snapshot_image_artifacts(self, artifacts_path: Path) -> set:
|
|
437
|
+
if not artifacts_path.exists():
|
|
438
|
+
return set()
|
|
439
|
+
snapshot = set()
|
|
440
|
+
for file_path in artifacts_path.rglob("*"):
|
|
441
|
+
if file_path.is_file() and file_path.suffix.lower() in self.IMAGE_SUFFIXES:
|
|
442
|
+
snapshot.add(file_path.resolve())
|
|
443
|
+
return snapshot
|
|
444
|
+
|
|
445
|
+
def _collect_new_image_artifacts(
|
|
446
|
+
self, artifacts_path: Path, before_snapshot: set, output_dir: str | None
|
|
447
|
+
) -> list[str]:
|
|
448
|
+
after_snapshot = self._snapshot_image_artifacts(artifacts_path)
|
|
449
|
+
new_files = sorted(after_snapshot - before_snapshot)
|
|
450
|
+
if not new_files:
|
|
451
|
+
return []
|
|
452
|
+
|
|
453
|
+
rel_paths: list[str] = []
|
|
454
|
+
output_base = Path(output_dir).resolve() if output_dir else None
|
|
455
|
+
|
|
456
|
+
for file_path in new_files:
|
|
457
|
+
rel_path: str | None = None
|
|
458
|
+
if output_base:
|
|
459
|
+
try:
|
|
460
|
+
rel_path = str(file_path.relative_to(output_base)).replace("\\", "/")
|
|
461
|
+
except ValueError:
|
|
462
|
+
rel_path = None
|
|
463
|
+
if rel_path is None:
|
|
464
|
+
try:
|
|
465
|
+
rel_path = str(file_path.relative_to(artifacts_path.parent)).replace("\\", "/")
|
|
466
|
+
except ValueError:
|
|
467
|
+
rel_path = str(Path("artifacts") / file_path.name)
|
|
468
|
+
rel_paths.append(rel_path)
|
|
469
|
+
|
|
470
|
+
return rel_paths
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Utility Module
|
|
4
|
+
Contains logging, performance monitoring, config validation, parsers, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Logging system (from unified logs module)
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# Add project root to path for imports
|
|
12
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
13
|
+
if str(project_root) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(project_root))
|
|
15
|
+
|
|
16
|
+
from src.logging import Logger, LogLevel, get_logger, reset_logger
|
|
17
|
+
|
|
18
|
+
# Backwards compatibility aliases
|
|
19
|
+
# These aliases maintain compatibility with code that imported these names before refactoring
|
|
20
|
+
SolveAgentLogger = Logger # Alias for Logger class
|
|
21
|
+
from .config_validator import ConfigValidator
|
|
22
|
+
from .error_handler import (
|
|
23
|
+
LLMParseError,
|
|
24
|
+
retry_on_parse_error,
|
|
25
|
+
validate_investigate_output,
|
|
26
|
+
validate_none_tool_constraint,
|
|
27
|
+
validate_note_output,
|
|
28
|
+
validate_output,
|
|
29
|
+
validate_reflect_output,
|
|
30
|
+
validate_solve_output,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Backwards compatibility alias
|
|
34
|
+
ParseError = LLMParseError
|
|
35
|
+
from .performance_monitor import PerformanceMonitor
|
|
36
|
+
|
|
37
|
+
# Token tracker
|
|
38
|
+
from .token_tracker import TokenTracker, calculate_cost, get_model_pricing
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Logging system
|
|
42
|
+
"Logger",
|
|
43
|
+
"get_logger",
|
|
44
|
+
"reset_logger",
|
|
45
|
+
"LogLevel",
|
|
46
|
+
"SolveAgentLogger", # Backwards compatibility
|
|
47
|
+
# Performance monitoring
|
|
48
|
+
"PerformanceMonitor",
|
|
49
|
+
# Config validation
|
|
50
|
+
"ConfigValidator",
|
|
51
|
+
# Token tracker
|
|
52
|
+
"TokenTracker",
|
|
53
|
+
"calculate_cost",
|
|
54
|
+
"get_model_pricing",
|
|
55
|
+
# Error handling
|
|
56
|
+
"ParseError",
|
|
57
|
+
"retry_on_parse_error",
|
|
58
|
+
"validate_output",
|
|
59
|
+
"validate_investigate_output",
|
|
60
|
+
"validate_note_output",
|
|
61
|
+
"validate_none_tool_constraint",
|
|
62
|
+
"validate_reflect_output",
|
|
63
|
+
"validate_solve_output",
|
|
64
|
+
]
|