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
src/api/routers/solve.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solve API Router
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
WebSocket endpoint for real-time problem solving with streaming logs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
15
|
+
|
|
16
|
+
from src.agents.solve import MainSolver
|
|
17
|
+
from src.api.utils.history import ActivityType, history_manager
|
|
18
|
+
from src.api.utils.log_interceptor import LogInterceptor
|
|
19
|
+
from src.api.utils.task_id_manager import TaskIDManager
|
|
20
|
+
|
|
21
|
+
_project_root = Path(__file__).parent.parent.parent.parent
|
|
22
|
+
sys.path.insert(0, str(_project_root))
|
|
23
|
+
from src.logging import get_logger
|
|
24
|
+
from src.services.config import load_config_with_main
|
|
25
|
+
from src.services.llm import get_llm_config
|
|
26
|
+
|
|
27
|
+
# Initialize logger with config
|
|
28
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
29
|
+
config = load_config_with_main("solve_config.yaml", project_root)
|
|
30
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
|
|
31
|
+
logger = get_logger("SolveAPI", level="INFO", log_dir=log_dir)
|
|
32
|
+
|
|
33
|
+
router = APIRouter()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.websocket("/solve")
|
|
37
|
+
async def websocket_solve(websocket: WebSocket):
|
|
38
|
+
await websocket.accept()
|
|
39
|
+
|
|
40
|
+
task_manager = TaskIDManager.get_instance()
|
|
41
|
+
connection_closed = asyncio.Event()
|
|
42
|
+
log_queue = asyncio.Queue()
|
|
43
|
+
pusher_task = None
|
|
44
|
+
|
|
45
|
+
async def safe_send_json(data: dict[str, Any]):
|
|
46
|
+
"""Safely send JSON to WebSocket, checking if connection is closed"""
|
|
47
|
+
if connection_closed.is_set():
|
|
48
|
+
return False
|
|
49
|
+
try:
|
|
50
|
+
await websocket.send_json(data)
|
|
51
|
+
return True
|
|
52
|
+
except (WebSocketDisconnect, RuntimeError, ConnectionError) as e:
|
|
53
|
+
logger.debug(f"WebSocket connection closed: {e}")
|
|
54
|
+
connection_closed.set()
|
|
55
|
+
return False
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.debug(f"Error sending WebSocket message: {e}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
async def log_pusher():
|
|
61
|
+
while not connection_closed.is_set():
|
|
62
|
+
try:
|
|
63
|
+
# Use timeout to periodically check if connection is closed
|
|
64
|
+
entry = await asyncio.wait_for(log_queue.get(), timeout=0.5)
|
|
65
|
+
try:
|
|
66
|
+
await websocket.send_json(entry)
|
|
67
|
+
except (WebSocketDisconnect, RuntimeError, ConnectionError) as e:
|
|
68
|
+
# Connection closed, stop pushing
|
|
69
|
+
logger.debug(f"WebSocket connection closed in log_pusher: {e}")
|
|
70
|
+
connection_closed.set()
|
|
71
|
+
log_queue.task_done()
|
|
72
|
+
break
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.debug(f"Error sending log entry: {e}")
|
|
75
|
+
# Continue to next entry
|
|
76
|
+
log_queue.task_done()
|
|
77
|
+
except asyncio.TimeoutError:
|
|
78
|
+
# Timeout, check if connection is still open
|
|
79
|
+
continue
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.debug(f"Error in log_pusher: {e}")
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# 1. Wait for the initial message with the question and config
|
|
86
|
+
data = await websocket.receive_json()
|
|
87
|
+
question = data.get("question")
|
|
88
|
+
kb_name = data.get("kb_name", "ai_textbook")
|
|
89
|
+
|
|
90
|
+
if not question:
|
|
91
|
+
await websocket.send_json({"type": "error", "content": "Question is required"})
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
task_key = f"solve_{kb_name}_{hash(str(question))}"
|
|
95
|
+
task_id = task_manager.generate_task_id("solve", task_key)
|
|
96
|
+
|
|
97
|
+
await websocket.send_json({"type": "task_id", "task_id": task_id})
|
|
98
|
+
|
|
99
|
+
# 2. Initialize Solver
|
|
100
|
+
root_dir = Path(__file__).parent.parent.parent.parent
|
|
101
|
+
output_base = root_dir / "data" / "user" / "solve"
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
llm_config = get_llm_config()
|
|
105
|
+
api_key = llm_config.api_key
|
|
106
|
+
base_url = llm_config.base_url
|
|
107
|
+
api_version = getattr(llm_config, "api_version", None)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Failed to get LLM config: {e}", exc_info=True)
|
|
110
|
+
await websocket.send_json({"type": "error", "content": f"LLM configuration error: {e}"})
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
solver = MainSolver(
|
|
114
|
+
kb_name=kb_name,
|
|
115
|
+
output_base_dir=str(output_base),
|
|
116
|
+
api_key=api_key,
|
|
117
|
+
base_url=base_url,
|
|
118
|
+
api_version=api_version,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Complete async initialization
|
|
122
|
+
await solver.ainit()
|
|
123
|
+
|
|
124
|
+
logger.info(f"[{task_id}] Solving: {question[:50]}...")
|
|
125
|
+
|
|
126
|
+
target_logger = solver.logger.logger
|
|
127
|
+
|
|
128
|
+
# Note: System log forwarder removed - all logs now go to unified log file
|
|
129
|
+
# The main logger already writes to data/user/logs/ai_tutor_YYYYMMDD.log
|
|
130
|
+
|
|
131
|
+
# 3. Setup Log Queue
|
|
132
|
+
# log_queue already initialized
|
|
133
|
+
|
|
134
|
+
# 4. Setup status update mechanism
|
|
135
|
+
display_manager = None
|
|
136
|
+
if hasattr(solver.logger, "display_manager") and solver.logger.display_manager:
|
|
137
|
+
display_manager = solver.logger.display_manager
|
|
138
|
+
|
|
139
|
+
original_set_status = display_manager.set_agent_status
|
|
140
|
+
|
|
141
|
+
def wrapped_set_status(agent_name: str, status: str):
|
|
142
|
+
original_set_status(agent_name, status)
|
|
143
|
+
try:
|
|
144
|
+
log_queue.put_nowait(
|
|
145
|
+
{
|
|
146
|
+
"type": "agent_status",
|
|
147
|
+
"agent": agent_name,
|
|
148
|
+
"status": status,
|
|
149
|
+
"all_agents": display_manager.agents_status.copy(),
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
display_manager.set_agent_status = wrapped_set_status
|
|
156
|
+
|
|
157
|
+
original_update_stats = display_manager.update_token_stats
|
|
158
|
+
|
|
159
|
+
def wrapped_update_stats(summary: dict[str, Any]):
|
|
160
|
+
original_update_stats(summary)
|
|
161
|
+
try:
|
|
162
|
+
stats_copy = display_manager.stats.copy()
|
|
163
|
+
logger.debug(
|
|
164
|
+
f"Sending token_stats: model={stats_copy.get('model')}, calls={stats_copy.get('calls')}, cost={stats_copy.get('cost')}"
|
|
165
|
+
)
|
|
166
|
+
log_queue.put_nowait({"type": "token_stats", "stats": stats_copy})
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.debug(f"Failed to send token_stats: {e}")
|
|
169
|
+
|
|
170
|
+
display_manager.update_token_stats = wrapped_update_stats
|
|
171
|
+
|
|
172
|
+
# Re-register the callback to use the wrapped method
|
|
173
|
+
# (The callback was set before wrapping in main_solver.py)
|
|
174
|
+
if hasattr(solver, "token_tracker") and solver.token_tracker:
|
|
175
|
+
solver.token_tracker.set_on_usage_added_callback(wrapped_update_stats)
|
|
176
|
+
|
|
177
|
+
def send_progress_update(stage: str, progress: dict[str, Any]):
|
|
178
|
+
"""Send progress update to frontend"""
|
|
179
|
+
try:
|
|
180
|
+
log_queue.put_nowait({"type": "progress", "stage": stage, "progress": progress})
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
solver._send_progress_update = send_progress_update
|
|
185
|
+
|
|
186
|
+
# 5. Background task to push logs to WebSocket
|
|
187
|
+
pusher_task = asyncio.create_task(log_pusher())
|
|
188
|
+
|
|
189
|
+
# 6. Run Solver within the LogInterceptor context
|
|
190
|
+
interceptor = LogInterceptor(target_logger, log_queue)
|
|
191
|
+
with interceptor:
|
|
192
|
+
await safe_send_json({"type": "status", "content": "started"})
|
|
193
|
+
|
|
194
|
+
if display_manager:
|
|
195
|
+
await safe_send_json(
|
|
196
|
+
{
|
|
197
|
+
"type": "agent_status",
|
|
198
|
+
"agent": "all",
|
|
199
|
+
"status": "initial",
|
|
200
|
+
"all_agents": display_manager.agents_status.copy(),
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
await safe_send_json({"type": "token_stats", "stats": display_manager.stats.copy()})
|
|
204
|
+
|
|
205
|
+
logger.progress(f"[{task_id}] Solving started")
|
|
206
|
+
|
|
207
|
+
result = await solver.solve(question, verbose=True)
|
|
208
|
+
|
|
209
|
+
logger.success(f"[{task_id}] Solving completed")
|
|
210
|
+
task_manager.update_task_status(task_id, "completed")
|
|
211
|
+
|
|
212
|
+
# Process Markdown content to fix image paths
|
|
213
|
+
final_answer = result.get("final_answer", "")
|
|
214
|
+
output_dir_str = result.get("output_dir", "")
|
|
215
|
+
|
|
216
|
+
if output_dir_str and final_answer:
|
|
217
|
+
try:
|
|
218
|
+
output_dir = Path(output_dir_str)
|
|
219
|
+
|
|
220
|
+
if not output_dir.is_absolute():
|
|
221
|
+
output_dir = output_dir.resolve()
|
|
222
|
+
|
|
223
|
+
path_str = str(output_dir).replace("\\", "/")
|
|
224
|
+
parts = path_str.split("/")
|
|
225
|
+
|
|
226
|
+
if "user" in parts:
|
|
227
|
+
idx = parts.index("user")
|
|
228
|
+
rel_path = "/".join(parts[idx + 1 :])
|
|
229
|
+
base_url = f"/api/outputs/{rel_path}"
|
|
230
|
+
|
|
231
|
+
pattern = r"\]\(artifacts/([^)]+)\)"
|
|
232
|
+
replacement = rf"]({base_url}/artifacts/\1)"
|
|
233
|
+
final_answer = re.sub(pattern, replacement, final_answer)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.debug(f"Error processing image paths: {e}")
|
|
236
|
+
|
|
237
|
+
# Send final agent status update
|
|
238
|
+
if display_manager:
|
|
239
|
+
final_agent_status = dict.fromkeys(display_manager.agents_status.keys(), "done")
|
|
240
|
+
await safe_send_json(
|
|
241
|
+
{
|
|
242
|
+
"type": "agent_status",
|
|
243
|
+
"agent": "all",
|
|
244
|
+
"status": "complete",
|
|
245
|
+
"all_agents": final_agent_status,
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Send final result
|
|
250
|
+
final_res = {
|
|
251
|
+
"type": "result",
|
|
252
|
+
"final_answer": final_answer,
|
|
253
|
+
"output_dir": output_dir_str,
|
|
254
|
+
"metadata": result.get("metadata"),
|
|
255
|
+
}
|
|
256
|
+
await safe_send_json(final_res)
|
|
257
|
+
|
|
258
|
+
# Save to history
|
|
259
|
+
history_manager.add_entry(
|
|
260
|
+
activity_type=ActivityType.SOLVE,
|
|
261
|
+
title=question[:50] + "..." if len(question) > 50 else question,
|
|
262
|
+
content={
|
|
263
|
+
"question": question,
|
|
264
|
+
"answer": result.get("final_answer"),
|
|
265
|
+
"kb_name": kb_name,
|
|
266
|
+
},
|
|
267
|
+
summary=(
|
|
268
|
+
result.get("final_answer")[:100] + "..." if result.get("final_answer") else ""
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
# Mark connection as closed before sending error (to prevent log_pusher from interfering)
|
|
274
|
+
connection_closed.set()
|
|
275
|
+
await safe_send_json({"type": "error", "content": str(e)})
|
|
276
|
+
logger.error(f"[{task_id if 'task_id' in locals() else 'unknown'}] Solving failed: {e}")
|
|
277
|
+
if "task_id" in locals():
|
|
278
|
+
task_manager.update_task_status(task_id, "error", error=str(e))
|
|
279
|
+
finally:
|
|
280
|
+
# Stop log pusher first
|
|
281
|
+
connection_closed.set()
|
|
282
|
+
if pusher_task:
|
|
283
|
+
pusher_task.cancel()
|
|
284
|
+
try:
|
|
285
|
+
await pusher_task
|
|
286
|
+
except asyncio.CancelledError:
|
|
287
|
+
pass
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.debug(f"Error waiting for pusher task: {e}")
|
|
290
|
+
|
|
291
|
+
# Close WebSocket connection
|
|
292
|
+
try:
|
|
293
|
+
# Check if connection is still open before closing
|
|
294
|
+
if hasattr(websocket, "client_state"):
|
|
295
|
+
state = websocket.client_state
|
|
296
|
+
if hasattr(state, "name") and state.name != "DISCONNECTED":
|
|
297
|
+
await websocket.close()
|
|
298
|
+
else:
|
|
299
|
+
# Fallback: try to close anyway
|
|
300
|
+
await websocket.close()
|
|
301
|
+
except (WebSocketDisconnect, RuntimeError, ConnectionError):
|
|
302
|
+
# Connection already closed, ignore
|
|
303
|
+
pass
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.debug(f"Error closing WebSocket: {e}")
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System Status API Router
|
|
3
|
+
Manages system status checks and model connection tests
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from src.services.embedding import get_embedding_client, get_embedding_config
|
|
13
|
+
from src.services.llm import complete as llm_complete
|
|
14
|
+
from src.services.llm import get_llm_config, get_token_limit_kwargs
|
|
15
|
+
from src.services.tts import get_tts_config
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestResponse(BaseModel):
|
|
21
|
+
success: bool
|
|
22
|
+
message: str
|
|
23
|
+
model: str | None = None
|
|
24
|
+
response_time_ms: float | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get("/status")
|
|
29
|
+
async def get_system_status():
|
|
30
|
+
"""
|
|
31
|
+
Get overall system status including backend and model configurations
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dictionary containing status of backend, LLM, embeddings, and TTS
|
|
35
|
+
"""
|
|
36
|
+
result = {
|
|
37
|
+
"backend": {"status": "online", "timestamp": datetime.now().isoformat()},
|
|
38
|
+
"llm": {"status": "unknown", "model": None, "testable": True},
|
|
39
|
+
"embeddings": {"status": "unknown", "model": None, "testable": True},
|
|
40
|
+
"tts": {"status": "unknown", "model": None, "testable": True},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Check backend status (this endpoint itself proves backend is online)
|
|
44
|
+
result["backend"]["status"] = "online"
|
|
45
|
+
|
|
46
|
+
# Check LLM configuration
|
|
47
|
+
try:
|
|
48
|
+
llm_config = get_llm_config()
|
|
49
|
+
result["llm"]["model"] = llm_config.model
|
|
50
|
+
result["llm"]["status"] = "configured"
|
|
51
|
+
except ValueError as e:
|
|
52
|
+
result["llm"]["status"] = "not_configured"
|
|
53
|
+
result["llm"]["error"] = str(e)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
result["llm"]["status"] = "error"
|
|
56
|
+
result["llm"]["error"] = str(e)
|
|
57
|
+
|
|
58
|
+
# Check Embeddings configuration
|
|
59
|
+
try:
|
|
60
|
+
embedding_config = get_embedding_config()
|
|
61
|
+
result["embeddings"]["model"] = embedding_config.model
|
|
62
|
+
result["embeddings"]["status"] = "configured"
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
result["embeddings"]["status"] = "not_configured"
|
|
65
|
+
result["embeddings"]["error"] = str(e)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
result["embeddings"]["status"] = "error"
|
|
68
|
+
result["embeddings"]["error"] = str(e)
|
|
69
|
+
|
|
70
|
+
# Check TTS configuration
|
|
71
|
+
try:
|
|
72
|
+
tts_config = get_tts_config()
|
|
73
|
+
result["tts"]["model"] = tts_config.get("model")
|
|
74
|
+
result["tts"]["status"] = "configured"
|
|
75
|
+
except ValueError as e:
|
|
76
|
+
result["tts"]["status"] = "not_configured"
|
|
77
|
+
result["tts"]["error"] = str(e)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
result["tts"]["status"] = "error"
|
|
80
|
+
result["tts"]["error"] = str(e)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.post("/test/llm", response_model=TestResponse)
|
|
86
|
+
async def test_llm_connection():
|
|
87
|
+
"""
|
|
88
|
+
Test LLM model connection by sending a simple completion request
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Test result with success status and response time
|
|
92
|
+
"""
|
|
93
|
+
start_time = time.time()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
llm_config = get_llm_config()
|
|
97
|
+
model = llm_config.model
|
|
98
|
+
base_url = llm_config.base_url.rstrip("/")
|
|
99
|
+
|
|
100
|
+
# Sanitize Base URL (remove /chat/completions suffix if present)
|
|
101
|
+
for suffix in ["/chat/completions", "/completions"]:
|
|
102
|
+
if base_url.endswith(suffix):
|
|
103
|
+
base_url = base_url[: -len(suffix)]
|
|
104
|
+
|
|
105
|
+
# Handle API Key (inject dummy if missing for local LLMs)
|
|
106
|
+
api_key = llm_config.api_key
|
|
107
|
+
if not api_key:
|
|
108
|
+
api_key = "sk-no-key-required"
|
|
109
|
+
|
|
110
|
+
# Send a minimal test request with a prompt that guarantees output
|
|
111
|
+
test_prompt = "Say 'OK' to confirm you are working. Do not produce long output."
|
|
112
|
+
token_kwargs = get_token_limit_kwargs(model, max_tokens=200)
|
|
113
|
+
|
|
114
|
+
response = await llm_complete(
|
|
115
|
+
model=model,
|
|
116
|
+
prompt=test_prompt,
|
|
117
|
+
system_prompt="You are a helpful assistant. Respond briefly.",
|
|
118
|
+
binding=llm_config.binding,
|
|
119
|
+
api_key=api_key,
|
|
120
|
+
base_url=base_url,
|
|
121
|
+
temperature=0.1,
|
|
122
|
+
**token_kwargs,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
response_time = (time.time() - start_time) * 1000
|
|
126
|
+
|
|
127
|
+
if response and len(response.strip()) > 0:
|
|
128
|
+
return TestResponse(
|
|
129
|
+
success=True,
|
|
130
|
+
message="LLM connection successful",
|
|
131
|
+
model=model,
|
|
132
|
+
response_time_ms=round(response_time, 2),
|
|
133
|
+
)
|
|
134
|
+
return TestResponse(
|
|
135
|
+
success=False,
|
|
136
|
+
message="LLM connection failed: Empty response",
|
|
137
|
+
model=model,
|
|
138
|
+
error="Empty response from API",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
return TestResponse(success=False, message=f"LLM configuration error: {e!s}", error=str(e))
|
|
143
|
+
except Exception as e:
|
|
144
|
+
response_time = (time.time() - start_time) * 1000
|
|
145
|
+
return TestResponse(
|
|
146
|
+
success=False,
|
|
147
|
+
message=f"LLM connection failed: {e!s}",
|
|
148
|
+
response_time_ms=round(response_time, 2),
|
|
149
|
+
error=str(e),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@router.post("/test/embeddings", response_model=TestResponse)
|
|
154
|
+
async def test_embeddings_connection():
|
|
155
|
+
"""
|
|
156
|
+
Test Embeddings model connection by sending a simple embedding request
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Test result with success status and response time
|
|
160
|
+
"""
|
|
161
|
+
start_time = time.time()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
embedding_config = get_embedding_config()
|
|
165
|
+
embedding_client = get_embedding_client()
|
|
166
|
+
|
|
167
|
+
model = embedding_config.model
|
|
168
|
+
binding = embedding_config.binding
|
|
169
|
+
|
|
170
|
+
# Send a minimal test request using unified client
|
|
171
|
+
test_texts = ["test"]
|
|
172
|
+
embeddings = await embedding_client.embed(test_texts)
|
|
173
|
+
|
|
174
|
+
response_time = (time.time() - start_time) * 1000
|
|
175
|
+
|
|
176
|
+
if embeddings is not None and len(embeddings) > 0 and len(embeddings[0]) > 0:
|
|
177
|
+
return TestResponse(
|
|
178
|
+
success=True,
|
|
179
|
+
message=f"Embeddings connection successful ({binding} provider)",
|
|
180
|
+
model=model,
|
|
181
|
+
response_time_ms=round(response_time, 2),
|
|
182
|
+
)
|
|
183
|
+
return TestResponse(
|
|
184
|
+
success=False,
|
|
185
|
+
message="Embeddings connection failed: Empty response",
|
|
186
|
+
model=model,
|
|
187
|
+
error="Empty embedding vector",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
return TestResponse(
|
|
192
|
+
success=False, message=f"Embeddings configuration error: {e!s}", error=str(e)
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
response_time = (time.time() - start_time) * 1000
|
|
196
|
+
return TestResponse(
|
|
197
|
+
success=False,
|
|
198
|
+
message=f"Embeddings connection failed: {e!s}",
|
|
199
|
+
response_time_ms=round(response_time, 2),
|
|
200
|
+
error=str(e),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.post("/test/tts", response_model=TestResponse)
|
|
205
|
+
async def test_tts_connection():
|
|
206
|
+
"""
|
|
207
|
+
Test TTS model connection by checking configuration
|
|
208
|
+
|
|
209
|
+
Note: We don't actually generate audio for testing to avoid unnecessary API calls.
|
|
210
|
+
We only verify the configuration is valid.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Test result with success status
|
|
214
|
+
"""
|
|
215
|
+
start_time = time.time()
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
tts_config = get_tts_config()
|
|
219
|
+
model = tts_config["model"]
|
|
220
|
+
api_key = tts_config["api_key"]
|
|
221
|
+
base_url = tts_config["base_url"]
|
|
222
|
+
|
|
223
|
+
# Verify configuration is complete
|
|
224
|
+
if not model or not api_key or not base_url:
|
|
225
|
+
return TestResponse(
|
|
226
|
+
success=False,
|
|
227
|
+
message="TTS configuration incomplete",
|
|
228
|
+
model=model,
|
|
229
|
+
error="Missing required configuration",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# For TTS, we just verify the config is valid
|
|
233
|
+
# Actual audio generation would be expensive, so we skip it
|
|
234
|
+
response_time = (time.time() - start_time) * 1000
|
|
235
|
+
|
|
236
|
+
return TestResponse(
|
|
237
|
+
success=True,
|
|
238
|
+
message="TTS configuration valid",
|
|
239
|
+
model=model,
|
|
240
|
+
response_time_ms=round(response_time, 2),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
except ValueError as e:
|
|
244
|
+
return TestResponse(success=False, message=f"TTS configuration error: {e!s}", error=str(e))
|
|
245
|
+
except Exception as e:
|
|
246
|
+
response_time = (time.time() - start_time) * 1000
|
|
247
|
+
return TestResponse(
|
|
248
|
+
success=False,
|
|
249
|
+
message=f"TTS connection check failed: {e!s}",
|
|
250
|
+
response_time_ms=round(response_time, 2),
|
|
251
|
+
error=str(e),
|
|
252
|
+
)
|
src/api/run_server.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Uvicorn Server Startup Script
|
|
4
|
+
Uses Python API instead of command line to avoid Windows path parsing issues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
# Force unbuffered output
|
|
11
|
+
os.environ["PYTHONUNBUFFERED"] = "1"
|
|
12
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
13
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
14
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
15
|
+
sys.stderr.reconfigure(line_buffering=True)
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
# Get project root directory
|
|
23
|
+
project_root = Path(__file__).parent.parent.parent
|
|
24
|
+
|
|
25
|
+
# Change to project root to ensure correct module imports
|
|
26
|
+
os.chdir(str(project_root))
|
|
27
|
+
|
|
28
|
+
# Ensure project root is in Python path
|
|
29
|
+
if str(project_root) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(project_root))
|
|
31
|
+
|
|
32
|
+
# Get port from configuration
|
|
33
|
+
from src.services.setup import get_backend_port
|
|
34
|
+
|
|
35
|
+
backend_port = get_backend_port(project_root)
|
|
36
|
+
|
|
37
|
+
# Configure reload_excludes to skip directories that shouldn't trigger reloads
|
|
38
|
+
# Use absolute paths to ensure they're properly resolved
|
|
39
|
+
reload_excludes = [
|
|
40
|
+
str(project_root / "venv"), # Virtual environment
|
|
41
|
+
str(project_root / ".venv"), # Virtual environment (alternative name)
|
|
42
|
+
str(project_root / "data"), # Data directory (includes knowledge_bases, user data, logs)
|
|
43
|
+
str(project_root / "node_modules"), # Node modules (if any at root)
|
|
44
|
+
str(project_root / "web" / "node_modules"), # Web node modules
|
|
45
|
+
str(project_root / "web" / ".next"), # Next.js build
|
|
46
|
+
str(project_root / ".git"), # Git directory
|
|
47
|
+
str(project_root / "scripts"), # Scripts directory - don't reload on launcher changes
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Filter out non-existent directories to avoid warnings
|
|
51
|
+
reload_excludes = [d for d in reload_excludes if Path(d).exists()]
|
|
52
|
+
|
|
53
|
+
# Start uvicorn server with reload enabled
|
|
54
|
+
uvicorn.run(
|
|
55
|
+
"src.api.main:app",
|
|
56
|
+
host="0.0.0.0",
|
|
57
|
+
port=backend_port,
|
|
58
|
+
reload=True,
|
|
59
|
+
reload_excludes=reload_excludes,
|
|
60
|
+
log_level="info",
|
|
61
|
+
)
|