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/main.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
from fastapi.staticfiles import StaticFiles
|
|
7
|
+
|
|
8
|
+
from src.api.routers import (
|
|
9
|
+
agent_config,
|
|
10
|
+
chat,
|
|
11
|
+
co_writer,
|
|
12
|
+
config,
|
|
13
|
+
dashboard,
|
|
14
|
+
guide,
|
|
15
|
+
ideagen,
|
|
16
|
+
knowledge,
|
|
17
|
+
notebook,
|
|
18
|
+
question,
|
|
19
|
+
research,
|
|
20
|
+
settings,
|
|
21
|
+
solve,
|
|
22
|
+
system,
|
|
23
|
+
)
|
|
24
|
+
from src.logging import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger("API")
|
|
27
|
+
|
|
28
|
+
CONFIG_DRIFT_ERROR_TEMPLATE = (
|
|
29
|
+
"Configuration Drift Detected: Tools {drift} found in agents.yaml "
|
|
30
|
+
"investigate.valid_tools but missing from main.yaml solve.valid_tools. "
|
|
31
|
+
"Add these tools to main.yaml solve.valid_tools or remove them from "
|
|
32
|
+
"agents.yaml investigate.valid_tools."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_tool_consistency():
|
|
37
|
+
"""
|
|
38
|
+
Validate that the tools configured for agents are consistent with the main application
|
|
39
|
+
configuration.
|
|
40
|
+
|
|
41
|
+
This function loads the main configuration (``main.yaml``) and the agents configuration
|
|
42
|
+
(``agents.yaml``) from the project root and compares:
|
|
43
|
+
|
|
44
|
+
* ``solve.valid_tools`` in ``main.yaml``
|
|
45
|
+
* ``investigate.valid_tools`` in ``agents.yaml``
|
|
46
|
+
|
|
47
|
+
All tools referenced by agents must be present in the main configuration. If any tools are
|
|
48
|
+
defined for agents that are not listed in the main configuration, a ``RuntimeError`` is
|
|
49
|
+
raised describing the drift. The error is logged and re-raised, which causes the FastAPI
|
|
50
|
+
application startup to fail when this function is called from the ``lifespan`` handler.
|
|
51
|
+
|
|
52
|
+
Impact on startup
|
|
53
|
+
------------------
|
|
54
|
+
This validation runs during application startup. Any configuration drift will:
|
|
55
|
+
|
|
56
|
+
* Be logged as an error with details about the unknown tools.
|
|
57
|
+
* Prevent the API from starting until the configuration is corrected.
|
|
58
|
+
|
|
59
|
+
How to resolve configuration drift
|
|
60
|
+
----------------------------------
|
|
61
|
+
If startup fails with a configuration drift error:
|
|
62
|
+
|
|
63
|
+
1. Inspect the set of tools reported in the error message.
|
|
64
|
+
2. Either:
|
|
65
|
+
* Add the missing tools to ``solve.valid_tools`` in ``main.yaml``, **or**
|
|
66
|
+
* Remove or rename the offending tools from ``investigate.valid_tools`` in ``agents.yaml``.
|
|
67
|
+
3. Restart the application after updating the configuration files.
|
|
68
|
+
|
|
69
|
+
Example of aligned configuration
|
|
70
|
+
--------------------------------
|
|
71
|
+
``main.yaml``::
|
|
72
|
+
|
|
73
|
+
solve:
|
|
74
|
+
valid_tools:
|
|
75
|
+
- web_search
|
|
76
|
+
- code_execution
|
|
77
|
+
|
|
78
|
+
``agents.yaml``::
|
|
79
|
+
|
|
80
|
+
investigate:
|
|
81
|
+
valid_tools:
|
|
82
|
+
- web_search
|
|
83
|
+
|
|
84
|
+
In this case, validation passes because ``investigate.valid_tools`` is a subset of
|
|
85
|
+
``solve.valid_tools``.
|
|
86
|
+
|
|
87
|
+
Example of configuration drift
|
|
88
|
+
------------------------------
|
|
89
|
+
``agents.yaml``::
|
|
90
|
+
|
|
91
|
+
investigate:
|
|
92
|
+
valid_tools:
|
|
93
|
+
- web_search
|
|
94
|
+
- unknown_tool
|
|
95
|
+
|
|
96
|
+
Here, ``unknown_tool`` is not present in ``solve.valid_tools`` in ``main.yaml``, so
|
|
97
|
+
validation will fail and prevent the application from starting until the configurations
|
|
98
|
+
are aligned.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
from src.services.config import load_config_with_main
|
|
102
|
+
|
|
103
|
+
project_root = Path(__file__).parent.parent.parent
|
|
104
|
+
main_config = load_config_with_main("main.yaml", project_root)
|
|
105
|
+
agent_config_data = load_config_with_main("agents.yaml", project_root)
|
|
106
|
+
|
|
107
|
+
main_tools = set(main_config.get("solve", {}).get("valid_tools", []))
|
|
108
|
+
agent_tools = set(agent_config_data.get("investigate", {}).get("valid_tools", []))
|
|
109
|
+
|
|
110
|
+
if not agent_tools.issubset(main_tools):
|
|
111
|
+
drift = agent_tools - main_tools
|
|
112
|
+
raise RuntimeError(CONFIG_DRIFT_ERROR_TEMPLATE.format(drift=drift))
|
|
113
|
+
except RuntimeError:
|
|
114
|
+
logger.exception("Configuration validation failed")
|
|
115
|
+
raise
|
|
116
|
+
except Exception:
|
|
117
|
+
logger.exception("Failed to load configuration for validation")
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@asynccontextmanager
|
|
122
|
+
async def lifespan(app: FastAPI):
|
|
123
|
+
"""
|
|
124
|
+
Application lifecycle management
|
|
125
|
+
Gracefully handle startup and shutdown events, avoid CancelledError
|
|
126
|
+
"""
|
|
127
|
+
# Execute on startup
|
|
128
|
+
logger.info("Application startup")
|
|
129
|
+
|
|
130
|
+
# Validate configuration consistency
|
|
131
|
+
validate_tool_consistency()
|
|
132
|
+
|
|
133
|
+
yield
|
|
134
|
+
# Execute on shutdown
|
|
135
|
+
logger.info("Application shutdown")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
app = FastAPI(
|
|
139
|
+
title="DeepTutor API",
|
|
140
|
+
version="1.0.0",
|
|
141
|
+
lifespan=lifespan,
|
|
142
|
+
# Disable automatic trailing slash redirects to prevent protocol downgrade issues
|
|
143
|
+
# when deployed behind HTTPS reverse proxies (e.g., nginx).
|
|
144
|
+
# Without this, FastAPI's 307 redirects may change HTTPS to HTTP.
|
|
145
|
+
# See: https://github.com/HKUDS/DeepTutor/issues/112
|
|
146
|
+
redirect_slashes=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Configure CORS
|
|
150
|
+
app.add_middleware(
|
|
151
|
+
CORSMiddleware,
|
|
152
|
+
allow_origins=["*"], # In production, replace with specific frontend origin
|
|
153
|
+
allow_credentials=True,
|
|
154
|
+
allow_methods=["*"],
|
|
155
|
+
allow_headers=["*"],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Mount user directory as static root for generated artifacts
|
|
159
|
+
# This allows frontend to access generated artifacts (images, PDFs, etc.)
|
|
160
|
+
# URL: /api/outputs/solve/solve_xxx/artifacts/image.png
|
|
161
|
+
# Physical Path: DeepTutor/data/user/solve/solve_xxx/artifacts/image.png
|
|
162
|
+
project_root = Path(__file__).parent.parent.parent
|
|
163
|
+
user_dir = project_root / "data" / "user"
|
|
164
|
+
|
|
165
|
+
# Initialize user directories on startup
|
|
166
|
+
try:
|
|
167
|
+
from src.services.setup import init_user_directories
|
|
168
|
+
|
|
169
|
+
init_user_directories(project_root)
|
|
170
|
+
except Exception:
|
|
171
|
+
# Fallback: just create the main directory if it doesn't exist
|
|
172
|
+
if not user_dir.exists():
|
|
173
|
+
user_dir.mkdir(parents=True)
|
|
174
|
+
|
|
175
|
+
app.mount("/api/outputs", StaticFiles(directory=str(user_dir)), name="outputs")
|
|
176
|
+
|
|
177
|
+
# Include routers
|
|
178
|
+
app.include_router(solve.router, prefix="/api/v1", tags=["solve"])
|
|
179
|
+
app.include_router(chat.router, prefix="/api/v1", tags=["chat"])
|
|
180
|
+
app.include_router(question.router, prefix="/api/v1/question", tags=["question"])
|
|
181
|
+
app.include_router(research.router, prefix="/api/v1/research", tags=["research"])
|
|
182
|
+
app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
|
|
183
|
+
app.include_router(dashboard.router, prefix="/api/v1/dashboard", tags=["dashboard"])
|
|
184
|
+
app.include_router(co_writer.router, prefix="/api/v1/co_writer", tags=["co_writer"])
|
|
185
|
+
app.include_router(notebook.router, prefix="/api/v1/notebook", tags=["notebook"])
|
|
186
|
+
app.include_router(guide.router, prefix="/api/v1/guide", tags=["guide"])
|
|
187
|
+
app.include_router(ideagen.router, prefix="/api/v1/ideagen", tags=["ideagen"])
|
|
188
|
+
app.include_router(settings.router, prefix="/api/v1/settings", tags=["settings"])
|
|
189
|
+
app.include_router(system.router, prefix="/api/v1/system", tags=["system"])
|
|
190
|
+
app.include_router(config.router, prefix="/api/v1/config", tags=["config"])
|
|
191
|
+
app.include_router(agent_config.router, prefix="/api/v1/agent-config", tags=["agent-config"])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.get("/")
|
|
195
|
+
async def root():
|
|
196
|
+
return {"message": "Welcome to DeepTutor API"}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
from pathlib import Path
|
|
201
|
+
|
|
202
|
+
import uvicorn
|
|
203
|
+
|
|
204
|
+
# Get project root directory
|
|
205
|
+
project_root = Path(__file__).parent.parent.parent
|
|
206
|
+
|
|
207
|
+
# Ensure project root is in Python path
|
|
208
|
+
import sys
|
|
209
|
+
|
|
210
|
+
if str(project_root) not in sys.path:
|
|
211
|
+
sys.path.insert(0, str(project_root))
|
|
212
|
+
|
|
213
|
+
# Get port from configuration
|
|
214
|
+
from src.services.setup import get_backend_port
|
|
215
|
+
|
|
216
|
+
backend_port = get_backend_port(project_root)
|
|
217
|
+
|
|
218
|
+
# Configure reload_excludes with absolute paths to properly exclude directories
|
|
219
|
+
venv_dir = project_root / "venv"
|
|
220
|
+
data_dir = project_root / "data"
|
|
221
|
+
reload_excludes = [
|
|
222
|
+
str(d)
|
|
223
|
+
for d in [
|
|
224
|
+
venv_dir,
|
|
225
|
+
project_root / ".venv",
|
|
226
|
+
data_dir,
|
|
227
|
+
project_root / "web" / "node_modules",
|
|
228
|
+
project_root / "web" / ".next",
|
|
229
|
+
project_root / ".git",
|
|
230
|
+
]
|
|
231
|
+
if d.exists()
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
uvicorn.run(
|
|
235
|
+
"api.main:app",
|
|
236
|
+
host="0.0.0.0",
|
|
237
|
+
port=backend_port,
|
|
238
|
+
reload=True,
|
|
239
|
+
reload_excludes=reload_excludes,
|
|
240
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init file for routers
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Agent Configuration API - Provides agent metadata for data-driven UI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
# Agent registry - single source of truth for agent UI metadata
|
|
11
|
+
AGENT_REGISTRY = {
|
|
12
|
+
"solve": {
|
|
13
|
+
"icon": "HelpCircle",
|
|
14
|
+
"color": "blue",
|
|
15
|
+
"label_key": "Problem Solved",
|
|
16
|
+
},
|
|
17
|
+
"question": {
|
|
18
|
+
"icon": "FileText",
|
|
19
|
+
"color": "purple",
|
|
20
|
+
"label_key": "Question Generated",
|
|
21
|
+
},
|
|
22
|
+
"research": {
|
|
23
|
+
"icon": "Search",
|
|
24
|
+
"color": "emerald",
|
|
25
|
+
"label_key": "Research Report",
|
|
26
|
+
},
|
|
27
|
+
"co_writer": {
|
|
28
|
+
"icon": "PenTool",
|
|
29
|
+
"color": "amber",
|
|
30
|
+
"label_key": "Co-Writer",
|
|
31
|
+
},
|
|
32
|
+
"guide": {
|
|
33
|
+
"icon": "BookOpen",
|
|
34
|
+
"color": "indigo",
|
|
35
|
+
"label_key": "Guided Learning",
|
|
36
|
+
},
|
|
37
|
+
"ideagen": {
|
|
38
|
+
"icon": "Lightbulb",
|
|
39
|
+
"color": "yellow",
|
|
40
|
+
"label_key": "Idea Generated",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/agents")
|
|
46
|
+
async def get_agent_config():
|
|
47
|
+
"""
|
|
48
|
+
Get agent UI configuration.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict mapping agent type to UI metadata (icon, color, label_key)
|
|
52
|
+
"""
|
|
53
|
+
return AGENT_REGISTRY
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/agents/{agent_type}")
|
|
57
|
+
async def get_single_agent_config(agent_type: str):
|
|
58
|
+
"""
|
|
59
|
+
Get UI configuration for a specific agent.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
agent_type: Agent type (solve, question, research, etc.)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Agent UI metadata or 404 if not found
|
|
66
|
+
"""
|
|
67
|
+
if agent_type in AGENT_REGISTRY:
|
|
68
|
+
return AGENT_REGISTRY[agent_type]
|
|
69
|
+
return {"error": f"Agent type '{agent_type}' not found"}
|
src/api/routers/chat.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat API Router
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
WebSocket endpoint for lightweight chat with session management.
|
|
6
|
+
REST endpoints for session operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
|
13
|
+
|
|
14
|
+
_project_root = Path(__file__).parent.parent.parent.parent
|
|
15
|
+
sys.path.insert(0, str(_project_root))
|
|
16
|
+
|
|
17
|
+
from src.agents.chat import ChatAgent, SessionManager
|
|
18
|
+
from src.logging import get_logger
|
|
19
|
+
from src.services.config import load_config_with_main
|
|
20
|
+
from src.services.llm.config import get_llm_config
|
|
21
|
+
|
|
22
|
+
# Initialize logger
|
|
23
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
24
|
+
config = load_config_with_main("solve_config.yaml", project_root)
|
|
25
|
+
log_dir = config.get("paths", {}).get("user_log_dir") or config.get("logging", {}).get("log_dir")
|
|
26
|
+
logger = get_logger("ChatAPI", level="INFO", log_dir=log_dir)
|
|
27
|
+
|
|
28
|
+
router = APIRouter()
|
|
29
|
+
|
|
30
|
+
# Initialize session manager
|
|
31
|
+
session_manager = SessionManager()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# REST Endpoints for Session Management
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/chat/sessions")
|
|
40
|
+
async def list_sessions(limit: int = 20):
|
|
41
|
+
"""
|
|
42
|
+
List recent chat sessions.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
limit: Maximum number of sessions to return
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of session summaries
|
|
49
|
+
"""
|
|
50
|
+
return session_manager.list_sessions(limit=limit, include_messages=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/chat/sessions/{session_id}")
|
|
54
|
+
async def get_session(session_id: str):
|
|
55
|
+
"""
|
|
56
|
+
Get a specific chat session with full message history.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
session_id: Session identifier
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Complete session data including messages
|
|
63
|
+
"""
|
|
64
|
+
session = session_manager.get_session(session_id)
|
|
65
|
+
if not session:
|
|
66
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
67
|
+
return session
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.delete("/chat/sessions/{session_id}")
|
|
71
|
+
async def delete_session(session_id: str):
|
|
72
|
+
"""
|
|
73
|
+
Delete a chat session.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
session_id: Session identifier
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Success message
|
|
80
|
+
"""
|
|
81
|
+
if session_manager.delete_session(session_id):
|
|
82
|
+
return {"status": "deleted", "session_id": session_id}
|
|
83
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# WebSocket Endpoint for Chat
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.websocket("/chat")
|
|
92
|
+
async def websocket_chat(websocket: WebSocket):
|
|
93
|
+
"""
|
|
94
|
+
WebSocket endpoint for chat with session and context management.
|
|
95
|
+
|
|
96
|
+
Message format:
|
|
97
|
+
{
|
|
98
|
+
"message": str, # User message
|
|
99
|
+
"session_id": str | null, # Session ID (null for new session)
|
|
100
|
+
"history": [...] | null, # Optional: explicit history override
|
|
101
|
+
"kb_name": str, # Knowledge base name (for RAG)
|
|
102
|
+
"enable_rag": bool, # Enable RAG retrieval
|
|
103
|
+
"enable_web_search": bool # Enable Web Search
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Response format:
|
|
107
|
+
- {"type": "session", "session_id": str} # Session ID (new or existing)
|
|
108
|
+
- {"type": "status", "stage": str, "message": str} # Status updates
|
|
109
|
+
- {"type": "stream", "content": str} # Streaming response chunks
|
|
110
|
+
- {"type": "sources", "rag": list, "web": list} # Source citations
|
|
111
|
+
- {"type": "result", "content": str} # Final complete response
|
|
112
|
+
- {"type": "error", "message": str} # Error message
|
|
113
|
+
"""
|
|
114
|
+
await websocket.accept()
|
|
115
|
+
|
|
116
|
+
# Get system language for agent
|
|
117
|
+
language = config.get("system", {}).get("language", "en")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
while True:
|
|
121
|
+
# Receive message
|
|
122
|
+
data = await websocket.receive_json()
|
|
123
|
+
message = data.get("message", "").strip()
|
|
124
|
+
session_id = data.get("session_id")
|
|
125
|
+
explicit_history = data.get("history") # Optional override
|
|
126
|
+
kb_name = data.get("kb_name", "")
|
|
127
|
+
enable_rag = data.get("enable_rag", False)
|
|
128
|
+
enable_web_search = data.get("enable_web_search", False)
|
|
129
|
+
|
|
130
|
+
if not message:
|
|
131
|
+
await websocket.send_json({"type": "error", "message": "Message is required"})
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
f"Chat request: session={session_id}, "
|
|
136
|
+
f"message={message[:50]}..., rag={enable_rag}, web={enable_web_search}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Get or create session
|
|
141
|
+
if session_id:
|
|
142
|
+
session = session_manager.get_session(session_id)
|
|
143
|
+
if not session:
|
|
144
|
+
# Session not found, create new one
|
|
145
|
+
session = session_manager.create_session(
|
|
146
|
+
title=message[:50] + ("..." if len(message) > 50 else ""),
|
|
147
|
+
settings={
|
|
148
|
+
"kb_name": kb_name,
|
|
149
|
+
"enable_rag": enable_rag,
|
|
150
|
+
"enable_web_search": enable_web_search,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
session_id = session["session_id"]
|
|
154
|
+
else:
|
|
155
|
+
# Create new session
|
|
156
|
+
session = session_manager.create_session(
|
|
157
|
+
title=message[:50] + ("..." if len(message) > 50 else ""),
|
|
158
|
+
settings={
|
|
159
|
+
"kb_name": kb_name,
|
|
160
|
+
"enable_rag": enable_rag,
|
|
161
|
+
"enable_web_search": enable_web_search,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
session_id = session["session_id"]
|
|
165
|
+
|
|
166
|
+
# Send session ID to frontend
|
|
167
|
+
await websocket.send_json(
|
|
168
|
+
{
|
|
169
|
+
"type": "session",
|
|
170
|
+
"session_id": session_id,
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Build history from session or explicit override
|
|
175
|
+
if explicit_history is not None:
|
|
176
|
+
history = explicit_history
|
|
177
|
+
else:
|
|
178
|
+
# Get history from session messages
|
|
179
|
+
history = [
|
|
180
|
+
{"role": msg["role"], "content": msg["content"]}
|
|
181
|
+
for msg in session.get("messages", [])
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# Add user message to session
|
|
185
|
+
session_manager.add_message(
|
|
186
|
+
session_id=session_id,
|
|
187
|
+
role="user",
|
|
188
|
+
content=message,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Initialize ChatAgent
|
|
192
|
+
try:
|
|
193
|
+
llm_config = get_llm_config()
|
|
194
|
+
api_key = llm_config.api_key
|
|
195
|
+
base_url = llm_config.base_url
|
|
196
|
+
api_version = getattr(llm_config, "api_version", None)
|
|
197
|
+
except Exception:
|
|
198
|
+
api_key = None
|
|
199
|
+
base_url = None
|
|
200
|
+
api_version = None
|
|
201
|
+
|
|
202
|
+
agent = ChatAgent(
|
|
203
|
+
language=language,
|
|
204
|
+
config=config,
|
|
205
|
+
api_key=api_key,
|
|
206
|
+
base_url=base_url,
|
|
207
|
+
api_version=api_version,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Send status updates
|
|
211
|
+
if enable_rag and kb_name:
|
|
212
|
+
await websocket.send_json(
|
|
213
|
+
{
|
|
214
|
+
"type": "status",
|
|
215
|
+
"stage": "rag",
|
|
216
|
+
"message": f"Searching knowledge base: {kb_name}...",
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if enable_web_search:
|
|
221
|
+
await websocket.send_json(
|
|
222
|
+
{
|
|
223
|
+
"type": "status",
|
|
224
|
+
"stage": "web",
|
|
225
|
+
"message": "Searching the web...",
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
await websocket.send_json(
|
|
230
|
+
{
|
|
231
|
+
"type": "status",
|
|
232
|
+
"stage": "generating",
|
|
233
|
+
"message": "Generating response...",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Process with streaming
|
|
238
|
+
full_response = ""
|
|
239
|
+
sources = {"rag": [], "web": []}
|
|
240
|
+
|
|
241
|
+
stream_generator = await agent.process(
|
|
242
|
+
message=message,
|
|
243
|
+
history=history,
|
|
244
|
+
kb_name=kb_name,
|
|
245
|
+
enable_rag=enable_rag,
|
|
246
|
+
enable_web_search=enable_web_search,
|
|
247
|
+
stream=True,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async for chunk_data in stream_generator:
|
|
251
|
+
if chunk_data["type"] == "chunk":
|
|
252
|
+
await websocket.send_json(
|
|
253
|
+
{
|
|
254
|
+
"type": "stream",
|
|
255
|
+
"content": chunk_data["content"],
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
full_response += chunk_data["content"]
|
|
259
|
+
elif chunk_data["type"] == "complete":
|
|
260
|
+
full_response = chunk_data["response"]
|
|
261
|
+
sources = chunk_data.get("sources", {"rag": [], "web": []})
|
|
262
|
+
|
|
263
|
+
# Send sources if any
|
|
264
|
+
if sources.get("rag") or sources.get("web"):
|
|
265
|
+
await websocket.send_json({"type": "sources", **sources})
|
|
266
|
+
|
|
267
|
+
# Send final result
|
|
268
|
+
await websocket.send_json(
|
|
269
|
+
{
|
|
270
|
+
"type": "result",
|
|
271
|
+
"content": full_response,
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Save assistant message to session
|
|
276
|
+
session_manager.add_message(
|
|
277
|
+
session_id=session_id,
|
|
278
|
+
role="assistant",
|
|
279
|
+
content=full_response,
|
|
280
|
+
sources=sources if (sources.get("rag") or sources.get("web")) else None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
logger.info(f"Chat completed: session={session_id}, {len(full_response)} chars")
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(f"Chat processing error: {e}")
|
|
287
|
+
await websocket.send_json({"type": "error", "message": str(e)})
|
|
288
|
+
|
|
289
|
+
except WebSocketDisconnect:
|
|
290
|
+
logger.debug("Client disconnected from chat")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"WebSocket error: {e}")
|
|
293
|
+
try:
|
|
294
|
+
await websocket.send_json({"type": "error", "message": str(e)})
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|