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.
Files changed (276) hide show
  1. realtimex_deeptutor/__init__.py +67 -0
  2. realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
  3. realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
  4. realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
  5. realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
  6. realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
  7. realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
  8. src/__init__.py +40 -0
  9. src/agents/__init__.py +24 -0
  10. src/agents/base_agent.py +657 -0
  11. src/agents/chat/__init__.py +24 -0
  12. src/agents/chat/chat_agent.py +435 -0
  13. src/agents/chat/prompts/en/chat_agent.yaml +35 -0
  14. src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
  15. src/agents/chat/session_manager.py +311 -0
  16. src/agents/co_writer/__init__.py +0 -0
  17. src/agents/co_writer/edit_agent.py +260 -0
  18. src/agents/co_writer/narrator_agent.py +423 -0
  19. src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
  20. src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
  21. src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
  22. src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
  23. src/agents/guide/__init__.py +16 -0
  24. src/agents/guide/agents/__init__.py +11 -0
  25. src/agents/guide/agents/chat_agent.py +104 -0
  26. src/agents/guide/agents/interactive_agent.py +223 -0
  27. src/agents/guide/agents/locate_agent.py +149 -0
  28. src/agents/guide/agents/summary_agent.py +150 -0
  29. src/agents/guide/guide_manager.py +500 -0
  30. src/agents/guide/prompts/en/chat_agent.yaml +41 -0
  31. src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
  32. src/agents/guide/prompts/en/locate_agent.yaml +68 -0
  33. src/agents/guide/prompts/en/summary_agent.yaml +157 -0
  34. src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
  35. src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
  36. src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
  37. src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
  38. src/agents/ideagen/__init__.py +12 -0
  39. src/agents/ideagen/idea_generation_workflow.py +426 -0
  40. src/agents/ideagen/material_organizer_agent.py +173 -0
  41. src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
  42. src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
  43. src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
  44. src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
  45. src/agents/question/__init__.py +24 -0
  46. src/agents/question/agents/__init__.py +18 -0
  47. src/agents/question/agents/generate_agent.py +381 -0
  48. src/agents/question/agents/relevance_analyzer.py +207 -0
  49. src/agents/question/agents/retrieve_agent.py +239 -0
  50. src/agents/question/coordinator.py +718 -0
  51. src/agents/question/example.py +109 -0
  52. src/agents/question/prompts/en/coordinator.yaml +75 -0
  53. src/agents/question/prompts/en/generate_agent.yaml +77 -0
  54. src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
  55. src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
  56. src/agents/question/prompts/zh/coordinator.yaml +75 -0
  57. src/agents/question/prompts/zh/generate_agent.yaml +77 -0
  58. src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
  59. src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
  60. src/agents/research/agents/__init__.py +23 -0
  61. src/agents/research/agents/decompose_agent.py +507 -0
  62. src/agents/research/agents/manager_agent.py +228 -0
  63. src/agents/research/agents/note_agent.py +180 -0
  64. src/agents/research/agents/rephrase_agent.py +263 -0
  65. src/agents/research/agents/reporting_agent.py +1333 -0
  66. src/agents/research/agents/research_agent.py +714 -0
  67. src/agents/research/data_structures.py +451 -0
  68. src/agents/research/main.py +188 -0
  69. src/agents/research/prompts/en/decompose_agent.yaml +89 -0
  70. src/agents/research/prompts/en/manager_agent.yaml +24 -0
  71. src/agents/research/prompts/en/note_agent.yaml +121 -0
  72. src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
  73. src/agents/research/prompts/en/reporting_agent.yaml +380 -0
  74. src/agents/research/prompts/en/research_agent.yaml +173 -0
  75. src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
  76. src/agents/research/prompts/zh/manager_agent.yaml +24 -0
  77. src/agents/research/prompts/zh/note_agent.yaml +121 -0
  78. src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
  79. src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
  80. src/agents/research/prompts/zh/research_agent.yaml +173 -0
  81. src/agents/research/research_pipeline.py +1309 -0
  82. src/agents/research/utils/__init__.py +60 -0
  83. src/agents/research/utils/citation_manager.py +799 -0
  84. src/agents/research/utils/json_utils.py +98 -0
  85. src/agents/research/utils/token_tracker.py +297 -0
  86. src/agents/solve/__init__.py +80 -0
  87. src/agents/solve/analysis_loop/__init__.py +14 -0
  88. src/agents/solve/analysis_loop/investigate_agent.py +414 -0
  89. src/agents/solve/analysis_loop/note_agent.py +190 -0
  90. src/agents/solve/main_solver.py +862 -0
  91. src/agents/solve/memory/__init__.py +34 -0
  92. src/agents/solve/memory/citation_memory.py +353 -0
  93. src/agents/solve/memory/investigate_memory.py +226 -0
  94. src/agents/solve/memory/solve_memory.py +340 -0
  95. src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
  96. src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
  97. src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
  98. src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
  99. src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
  100. src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
  101. src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
  102. src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
  103. src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
  104. src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
  105. src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
  106. src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
  107. src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
  108. src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
  109. src/agents/solve/solve_loop/__init__.py +22 -0
  110. src/agents/solve/solve_loop/citation_manager.py +74 -0
  111. src/agents/solve/solve_loop/manager_agent.py +274 -0
  112. src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
  113. src/agents/solve/solve_loop/response_agent.py +301 -0
  114. src/agents/solve/solve_loop/solve_agent.py +325 -0
  115. src/agents/solve/solve_loop/tool_agent.py +470 -0
  116. src/agents/solve/utils/__init__.py +64 -0
  117. src/agents/solve/utils/config_validator.py +313 -0
  118. src/agents/solve/utils/display_manager.py +223 -0
  119. src/agents/solve/utils/error_handler.py +363 -0
  120. src/agents/solve/utils/json_utils.py +98 -0
  121. src/agents/solve/utils/performance_monitor.py +407 -0
  122. src/agents/solve/utils/token_tracker.py +541 -0
  123. src/api/__init__.py +0 -0
  124. src/api/main.py +240 -0
  125. src/api/routers/__init__.py +1 -0
  126. src/api/routers/agent_config.py +69 -0
  127. src/api/routers/chat.py +296 -0
  128. src/api/routers/co_writer.py +337 -0
  129. src/api/routers/config.py +627 -0
  130. src/api/routers/dashboard.py +18 -0
  131. src/api/routers/guide.py +337 -0
  132. src/api/routers/ideagen.py +436 -0
  133. src/api/routers/knowledge.py +821 -0
  134. src/api/routers/notebook.py +247 -0
  135. src/api/routers/question.py +537 -0
  136. src/api/routers/research.py +394 -0
  137. src/api/routers/settings.py +164 -0
  138. src/api/routers/solve.py +305 -0
  139. src/api/routers/system.py +252 -0
  140. src/api/run_server.py +61 -0
  141. src/api/utils/history.py +172 -0
  142. src/api/utils/log_interceptor.py +21 -0
  143. src/api/utils/notebook_manager.py +415 -0
  144. src/api/utils/progress_broadcaster.py +72 -0
  145. src/api/utils/task_id_manager.py +100 -0
  146. src/config/__init__.py +0 -0
  147. src/config/accessors.py +18 -0
  148. src/config/constants.py +34 -0
  149. src/config/defaults.py +18 -0
  150. src/config/schema.py +38 -0
  151. src/config/settings.py +50 -0
  152. src/core/errors.py +62 -0
  153. src/knowledge/__init__.py +23 -0
  154. src/knowledge/add_documents.py +606 -0
  155. src/knowledge/config.py +65 -0
  156. src/knowledge/example_add_documents.py +236 -0
  157. src/knowledge/extract_numbered_items.py +1039 -0
  158. src/knowledge/initializer.py +621 -0
  159. src/knowledge/kb.py +22 -0
  160. src/knowledge/manager.py +782 -0
  161. src/knowledge/progress_tracker.py +182 -0
  162. src/knowledge/start_kb.py +535 -0
  163. src/logging/__init__.py +103 -0
  164. src/logging/adapters/__init__.py +17 -0
  165. src/logging/adapters/lightrag.py +184 -0
  166. src/logging/adapters/llamaindex.py +141 -0
  167. src/logging/config.py +80 -0
  168. src/logging/handlers/__init__.py +20 -0
  169. src/logging/handlers/console.py +75 -0
  170. src/logging/handlers/file.py +201 -0
  171. src/logging/handlers/websocket.py +127 -0
  172. src/logging/logger.py +709 -0
  173. src/logging/stats/__init__.py +16 -0
  174. src/logging/stats/llm_stats.py +179 -0
  175. src/services/__init__.py +56 -0
  176. src/services/config/__init__.py +61 -0
  177. src/services/config/knowledge_base_config.py +210 -0
  178. src/services/config/loader.py +260 -0
  179. src/services/config/unified_config.py +603 -0
  180. src/services/embedding/__init__.py +45 -0
  181. src/services/embedding/adapters/__init__.py +22 -0
  182. src/services/embedding/adapters/base.py +106 -0
  183. src/services/embedding/adapters/cohere.py +127 -0
  184. src/services/embedding/adapters/jina.py +99 -0
  185. src/services/embedding/adapters/ollama.py +116 -0
  186. src/services/embedding/adapters/openai_compatible.py +96 -0
  187. src/services/embedding/client.py +159 -0
  188. src/services/embedding/config.py +156 -0
  189. src/services/embedding/provider.py +119 -0
  190. src/services/llm/__init__.py +152 -0
  191. src/services/llm/capabilities.py +313 -0
  192. src/services/llm/client.py +302 -0
  193. src/services/llm/cloud_provider.py +530 -0
  194. src/services/llm/config.py +200 -0
  195. src/services/llm/error_mapping.py +103 -0
  196. src/services/llm/exceptions.py +152 -0
  197. src/services/llm/factory.py +450 -0
  198. src/services/llm/local_provider.py +347 -0
  199. src/services/llm/providers/anthropic.py +95 -0
  200. src/services/llm/providers/base_provider.py +93 -0
  201. src/services/llm/providers/open_ai.py +83 -0
  202. src/services/llm/registry.py +71 -0
  203. src/services/llm/telemetry.py +40 -0
  204. src/services/llm/types.py +27 -0
  205. src/services/llm/utils.py +333 -0
  206. src/services/prompt/__init__.py +25 -0
  207. src/services/prompt/manager.py +206 -0
  208. src/services/rag/__init__.py +64 -0
  209. src/services/rag/components/__init__.py +29 -0
  210. src/services/rag/components/base.py +59 -0
  211. src/services/rag/components/chunkers/__init__.py +18 -0
  212. src/services/rag/components/chunkers/base.py +34 -0
  213. src/services/rag/components/chunkers/fixed.py +71 -0
  214. src/services/rag/components/chunkers/numbered_item.py +94 -0
  215. src/services/rag/components/chunkers/semantic.py +97 -0
  216. src/services/rag/components/embedders/__init__.py +14 -0
  217. src/services/rag/components/embedders/base.py +32 -0
  218. src/services/rag/components/embedders/openai.py +63 -0
  219. src/services/rag/components/indexers/__init__.py +18 -0
  220. src/services/rag/components/indexers/base.py +35 -0
  221. src/services/rag/components/indexers/graph.py +172 -0
  222. src/services/rag/components/indexers/lightrag.py +156 -0
  223. src/services/rag/components/indexers/vector.py +146 -0
  224. src/services/rag/components/parsers/__init__.py +18 -0
  225. src/services/rag/components/parsers/base.py +35 -0
  226. src/services/rag/components/parsers/markdown.py +52 -0
  227. src/services/rag/components/parsers/pdf.py +115 -0
  228. src/services/rag/components/parsers/text.py +86 -0
  229. src/services/rag/components/retrievers/__init__.py +18 -0
  230. src/services/rag/components/retrievers/base.py +34 -0
  231. src/services/rag/components/retrievers/dense.py +200 -0
  232. src/services/rag/components/retrievers/hybrid.py +164 -0
  233. src/services/rag/components/retrievers/lightrag.py +169 -0
  234. src/services/rag/components/routing.py +286 -0
  235. src/services/rag/factory.py +234 -0
  236. src/services/rag/pipeline.py +215 -0
  237. src/services/rag/pipelines/__init__.py +32 -0
  238. src/services/rag/pipelines/academic.py +44 -0
  239. src/services/rag/pipelines/lightrag.py +43 -0
  240. src/services/rag/pipelines/llamaindex.py +313 -0
  241. src/services/rag/pipelines/raganything.py +384 -0
  242. src/services/rag/service.py +244 -0
  243. src/services/rag/types.py +73 -0
  244. src/services/search/__init__.py +284 -0
  245. src/services/search/base.py +87 -0
  246. src/services/search/consolidation.py +398 -0
  247. src/services/search/providers/__init__.py +128 -0
  248. src/services/search/providers/baidu.py +188 -0
  249. src/services/search/providers/exa.py +194 -0
  250. src/services/search/providers/jina.py +161 -0
  251. src/services/search/providers/perplexity.py +153 -0
  252. src/services/search/providers/serper.py +209 -0
  253. src/services/search/providers/tavily.py +161 -0
  254. src/services/search/types.py +114 -0
  255. src/services/setup/__init__.py +34 -0
  256. src/services/setup/init.py +285 -0
  257. src/services/tts/__init__.py +16 -0
  258. src/services/tts/config.py +99 -0
  259. src/tools/__init__.py +91 -0
  260. src/tools/code_executor.py +536 -0
  261. src/tools/paper_search_tool.py +171 -0
  262. src/tools/query_item_tool.py +310 -0
  263. src/tools/question/__init__.py +15 -0
  264. src/tools/question/exam_mimic.py +616 -0
  265. src/tools/question/pdf_parser.py +211 -0
  266. src/tools/question/question_extractor.py +397 -0
  267. src/tools/rag_tool.py +173 -0
  268. src/tools/tex_chunker.py +339 -0
  269. src/tools/tex_downloader.py +253 -0
  270. src/tools/web_search.py +71 -0
  271. src/utils/config_manager.py +206 -0
  272. src/utils/document_validator.py +168 -0
  273. src/utils/error_rate_tracker.py +111 -0
  274. src/utils/error_utils.py +82 -0
  275. src/utils/json_parser.py +110 -0
  276. src/utils/network/circuit_breaker.py +79 -0
@@ -0,0 +1,40 @@
1
+ """
2
+ LLM Telemetry
3
+ =============
4
+
5
+ Basic telemetry tracking for LLM calls.
6
+ """
7
+
8
+ import functools
9
+ import logging
10
+ from typing import Any, Callable
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def track_llm_call(provider_name: str):
16
+ """
17
+ Decorator to track LLM calls for telemetry.
18
+
19
+ Args:
20
+ provider_name: Name of the provider being called
21
+
22
+ Returns:
23
+ Decorator function
24
+ """
25
+
26
+ def decorator(func: Callable) -> Callable:
27
+ @functools.wraps(func)
28
+ async def wrapper(*args, **kwargs) -> Any:
29
+ logger.debug(f"LLM call to {provider_name}: {func.__name__}")
30
+ try:
31
+ result = await func(*args, **kwargs)
32
+ logger.debug(f"LLM call to {provider_name} completed successfully")
33
+ return result
34
+ except Exception as e:
35
+ logger.warning(f"LLM call to {provider_name} failed: {e}")
36
+ raise
37
+
38
+ return wrapper
39
+
40
+ return decorator
@@ -0,0 +1,27 @@
1
+ from typing import Any, AsyncGenerator, Dict, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class TutorResponse(BaseModel):
7
+ content: str
8
+ raw_response: Dict[str, Any]
9
+ usage: Dict[str, int] = Field(
10
+ default_factory=lambda: {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
11
+ )
12
+ provider: str
13
+ model: str
14
+ finish_reason: Optional[str] = None
15
+ cost_estimate: float = 0.0
16
+
17
+
18
+ class TutorStreamChunk(BaseModel):
19
+ content: str
20
+ delta: str
21
+ provider: str
22
+ model: str
23
+ is_complete: bool = False
24
+ usage: Optional[Dict[str, int]] = None
25
+
26
+
27
+ AsyncStreamGenerator = AsyncGenerator[TutorStreamChunk, None]
@@ -0,0 +1,333 @@
1
+ """
2
+ LLM Utilities
3
+ =============
4
+
5
+ Utility functions for LLM service:
6
+ - URL handling for local and cloud servers
7
+ - Response content extraction
8
+ - Thinking tags cleaning
9
+ """
10
+
11
+ import re
12
+ from typing import Any, Optional
13
+
14
+ # Known cloud provider domains (should never be treated as local)
15
+ CLOUD_DOMAINS = [
16
+ ".openai.com",
17
+ ".anthropic.com",
18
+ ".deepseek.com",
19
+ ".openrouter.ai",
20
+ ".azure.com",
21
+ ".googleapis.com",
22
+ ".cohere.ai",
23
+ ".mistral.ai",
24
+ ".together.ai",
25
+ ".fireworks.ai",
26
+ ".groq.com",
27
+ ".perplexity.ai",
28
+ ]
29
+
30
+ # Common local server ports
31
+ LOCAL_PORTS = [
32
+ ":1234", # LM Studio
33
+ ":11434", # Ollama
34
+ ":8000", # vLLM
35
+ ":8080", # llama.cpp
36
+ ":5000", # Common dev port
37
+ ":3000", # Common dev port
38
+ ":8001", # Alternative vLLM
39
+ ":5001", # Alternative dev port
40
+ ]
41
+
42
+ # Local hostname indicators
43
+ LOCAL_HOSTS = [
44
+ "localhost",
45
+ "127.0.0.1",
46
+ "0.0.0.0",
47
+ ]
48
+
49
+ # Ports that need /v1 suffix for OpenAI compatibility
50
+ V1_SUFFIX_PORTS = {
51
+ ":11434", # Ollama
52
+ ":1234", # LM Studio
53
+ ":8000", # vLLM
54
+ ":8001", # Alternative vLLM
55
+ ":8080", # llama.cpp
56
+ }
57
+
58
+
59
+ def is_local_llm_server(base_url: str) -> bool:
60
+ """
61
+ Check if the given URL points to a local LLM server.
62
+
63
+ Detects local servers by:
64
+ 1. Checking for local hostnames (localhost, 127.0.0.1, 0.0.0.0)
65
+ 2. Checking for common local LLM server ports
66
+ 3. Excluding known cloud provider domains
67
+
68
+ Args:
69
+ base_url: The base URL to check
70
+
71
+ Returns:
72
+ True if the URL appears to be a local LLM server
73
+ """
74
+ if not base_url:
75
+ return False
76
+
77
+ base_url_lower = base_url.lower()
78
+
79
+ # First, exclude known cloud providers
80
+ for domain in CLOUD_DOMAINS:
81
+ if domain in base_url_lower:
82
+ return False
83
+
84
+ # Check for local hostname indicators
85
+ for host in LOCAL_HOSTS:
86
+ if host in base_url_lower:
87
+ return True
88
+
89
+ # Check for common local server ports
90
+ for port in LOCAL_PORTS:
91
+ if port in base_url_lower:
92
+ return True
93
+
94
+ return False
95
+
96
+
97
+ def _needs_v1_suffix(url: str) -> bool:
98
+ """
99
+ Check if the URL needs /v1 suffix for OpenAI compatibility.
100
+
101
+ Most local LLM servers (Ollama, LM Studio, vLLM, llama.cpp) expose
102
+ OpenAI-compatible endpoints at /v1.
103
+
104
+ Args:
105
+ url: The URL to check
106
+
107
+ Returns:
108
+ True if /v1 should be appended
109
+ """
110
+ if not url:
111
+ return False
112
+
113
+ url_lower = url.lower()
114
+
115
+ # Skip if already has /v1
116
+ if url_lower.endswith("/v1"):
117
+ return False
118
+
119
+ # Only add /v1 for local servers with known ports that need it
120
+ if not is_local_llm_server(url):
121
+ return False
122
+
123
+ # Check if URL contains any port that needs /v1 suffix
124
+ # Also check for "ollama" in URL (but not ollama.com cloud service)
125
+ is_ollama = "ollama" in url_lower and "ollama.com" not in url_lower
126
+ if is_ollama:
127
+ return True
128
+
129
+ return any(port in url_lower for port in V1_SUFFIX_PORTS)
130
+
131
+
132
+ def sanitize_url(base_url: str, model: str = "") -> str:
133
+ """
134
+ Sanitize base URL for OpenAI-compatible APIs, with special handling for local LLM servers.
135
+
136
+ Handles:
137
+ - Ollama (port 11434)
138
+ - LM Studio (port 1234)
139
+ - vLLM (port 8000)
140
+ - llama.cpp (port 8080)
141
+ - Other localhost OpenAI-compatible servers
142
+
143
+ Args:
144
+ base_url: The base URL to sanitize
145
+ model: Optional model name (unused, kept for API compatibility)
146
+
147
+ Returns:
148
+ Sanitized URL string
149
+ """
150
+ if not base_url:
151
+ return base_url
152
+
153
+ url = base_url.rstrip("/")
154
+
155
+ # Ensure URL has a protocol (default to http for local servers)
156
+ if url and not url.startswith(("http://", "https://")):
157
+ url = "http://" + url
158
+
159
+ # Standard OpenAI client library is strict about URLs:
160
+ # - No trailing slashes
161
+ # - No /chat/completions or /completions/messages/embeddings suffixes
162
+ # (it adds these automatically)
163
+ for suffix in ["/chat/completions", "/completions", "/messages", "/embeddings"]:
164
+ if url.endswith(suffix):
165
+ url = url[: -len(suffix)]
166
+ url = url.rstrip("/")
167
+
168
+ # For local LLM servers, ensure /v1 is present for OpenAI compatibility
169
+ if _needs_v1_suffix(url):
170
+ url = url.rstrip("/") + "/v1"
171
+
172
+ return url
173
+
174
+
175
+ def clean_thinking_tags(
176
+ content: str,
177
+ binding: Optional[str] = None,
178
+ model: Optional[str] = None,
179
+ ) -> str:
180
+ """
181
+ Remove thinking tags from model output.
182
+
183
+ Some reasoning models (DeepSeek, Qwen, etc.) include <think>...</think> blocks
184
+ that should be stripped from the final response.
185
+
186
+ Args:
187
+ content: Raw model output
188
+ binding: Provider binding name (optional, for capability check)
189
+ model: Model name (optional, for capability check)
190
+
191
+ Returns:
192
+ Cleaned content without thinking tags
193
+ """
194
+ if not content:
195
+ return content
196
+
197
+ # Check if model produces thinking tags (if binding/model provided)
198
+ if binding:
199
+ # Lazy import to avoid circular dependency
200
+ from .capabilities import has_thinking_tags
201
+
202
+ if not has_thinking_tags(binding, model):
203
+ return content
204
+
205
+ # Remove <think>...</think> blocks
206
+ if "<think>" in content:
207
+ content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
208
+
209
+ return content.strip()
210
+
211
+
212
+ def build_chat_url(
213
+ base_url: str,
214
+ api_version: Optional[str] = None,
215
+ binding: Optional[str] = None,
216
+ ) -> str:
217
+ """
218
+ Build the full chat completions endpoint URL.
219
+
220
+ Handles:
221
+ - Adding /chat/completions suffix for OpenAI-compatible endpoints
222
+ - Adding /messages suffix for Anthropic endpoints
223
+ - Adding api-version query parameter for Azure OpenAI
224
+
225
+ Args:
226
+ base_url: Base URL (should be sanitized first)
227
+ api_version: API version for Azure OpenAI (optional)
228
+ binding: Provider binding name (optional, for Anthropic detection)
229
+
230
+ Returns:
231
+ Full endpoint URL
232
+ """
233
+ if not base_url:
234
+ return base_url
235
+
236
+ url = base_url.rstrip("/")
237
+
238
+ # Anthropic uses /messages endpoint
239
+ binding_lower = (binding or "").lower()
240
+ if binding_lower in ["anthropic", "claude"]:
241
+ if not url.endswith("/messages"):
242
+ url += "/messages"
243
+ else:
244
+ # OpenAI-compatible endpoints use /chat/completions
245
+ if not url.endswith("/chat/completions"):
246
+ url += "/chat/completions"
247
+
248
+ # Add api-version for Azure OpenAI
249
+ if api_version:
250
+ separator = "&" if "?" in url else "?"
251
+ url += f"{separator}api-version={api_version}"
252
+
253
+ return url
254
+
255
+
256
+ def extract_response_content(message: dict[str, Any]) -> str:
257
+ """
258
+ Extract content from LLM response message.
259
+
260
+ Handles different response formats from various models:
261
+ - Standard content field
262
+ - Reasoning models that use reasoning_content, reasoning, or thought fields
263
+
264
+ Args:
265
+ message: Message dict from LLM response (e.g., choices[0].message)
266
+
267
+ Returns:
268
+ Extracted content string
269
+ """
270
+ if not message:
271
+ return ""
272
+
273
+ content = message.get("content", "")
274
+
275
+ # Handle reasoning models that return content in different fields
276
+ if not content:
277
+ content = (
278
+ message.get("reasoning_content")
279
+ or message.get("reasoning")
280
+ or message.get("thought")
281
+ or ""
282
+ )
283
+
284
+ return content
285
+
286
+
287
+ def build_auth_headers(
288
+ api_key: Optional[str],
289
+ binding: Optional[str] = None,
290
+ ) -> dict[str, str]:
291
+ """
292
+ Build authentication headers for LLM API requests.
293
+
294
+ Args:
295
+ api_key: API key
296
+ binding: Provider binding name (for provider-specific headers)
297
+
298
+ Returns:
299
+ Headers dict
300
+ """
301
+ headers = {"Content-Type": "application/json"}
302
+
303
+ if not api_key:
304
+ return headers
305
+
306
+ binding_lower = (binding or "").lower()
307
+
308
+ if binding_lower in ["anthropic", "claude"]:
309
+ headers["x-api-key"] = api_key
310
+ headers["anthropic-version"] = "2023-06-01"
311
+ elif binding_lower == "azure_openai":
312
+ headers["api-key"] = api_key
313
+ else:
314
+ headers["Authorization"] = f"Bearer {api_key}"
315
+
316
+ return headers
317
+
318
+
319
+ __all__ = [
320
+ # URL utilities
321
+ "sanitize_url",
322
+ "is_local_llm_server",
323
+ "build_chat_url",
324
+ "build_auth_headers",
325
+ # Content utilities
326
+ "clean_thinking_tags",
327
+ "extract_response_content",
328
+ # Constants
329
+ "CLOUD_DOMAINS",
330
+ "LOCAL_PORTS",
331
+ "LOCAL_HOSTS",
332
+ "V1_SUFFIX_PORTS",
333
+ ]
@@ -0,0 +1,25 @@
1
+ """
2
+ Prompt Service
3
+ ==============
4
+
5
+ Unified prompt management for all DeepTutor modules.
6
+
7
+ Usage:
8
+ from src.services.prompt import get_prompt_manager, PromptManager
9
+
10
+ # Get singleton manager
11
+ pm = get_prompt_manager()
12
+
13
+ # Load prompts for an agent
14
+ prompts = pm.load_prompts("guide", "tutor_agent", language="en")
15
+
16
+ # Get specific prompt
17
+ system_prompt = pm.get_prompt(prompts, "system", "base")
18
+ """
19
+
20
+ from .manager import PromptManager, get_prompt_manager
21
+
22
+ __all__ = [
23
+ "PromptManager",
24
+ "get_prompt_manager",
25
+ ]
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Unified Prompt Manager - Single source of truth for all prompt loading.
4
+ Supports multi-language, caching, and language fallbacks.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from src.services.config import PROJECT_ROOT, parse_language
13
+
14
+
15
+ class PromptManager:
16
+ """Unified prompt manager with singleton pattern and global caching."""
17
+
18
+ _instance: "PromptManager | None" = None
19
+ _cache: dict[str, dict[str, Any]] = {}
20
+
21
+ # Language fallback chain: if primary language not found, try alternatives
22
+ LANGUAGE_FALLBACKS = {
23
+ "zh": ["zh", "en"],
24
+ "en": ["en", "zh"],
25
+ }
26
+
27
+ # Supported modules
28
+ MODULES = ["research", "solve", "guide", "question", "ideagen", "co_writer"]
29
+
30
+ def __new__(cls) -> "PromptManager":
31
+ if cls._instance is None:
32
+ cls._instance = super().__new__(cls)
33
+ return cls._instance
34
+
35
+ def load_prompts(
36
+ self,
37
+ module_name: str,
38
+ agent_name: str,
39
+ language: str = "zh",
40
+ subdirectory: str | None = None,
41
+ ) -> dict[str, Any]:
42
+ """
43
+ Load prompts for an agent.
44
+
45
+ Args:
46
+ module_name: Module name (research, solve, guide, question, ideagen, co_writer)
47
+ agent_name: Agent name (filename without .yaml)
48
+ language: Language code ('zh' or 'en')
49
+ subdirectory: Optional subdirectory (e.g., 'solve_loop' for solve module)
50
+
51
+ Returns:
52
+ Loaded prompt configuration dictionary
53
+ """
54
+ lang_code = parse_language(language)
55
+ cache_key = self._build_cache_key(module_name, agent_name, lang_code, subdirectory)
56
+
57
+ if cache_key in self._cache:
58
+ return self._cache[cache_key]
59
+
60
+ prompts = self._load_with_fallback(module_name, agent_name, lang_code, subdirectory)
61
+ self._cache[cache_key] = prompts
62
+ return prompts
63
+
64
+ def _build_cache_key(
65
+ self,
66
+ module_name: str,
67
+ agent_name: str,
68
+ lang_code: str,
69
+ subdirectory: str | None,
70
+ ) -> str:
71
+ """Build unique cache key."""
72
+ subdir_part = f"_{subdirectory}" if subdirectory else ""
73
+ return f"{module_name}_{agent_name}_{lang_code}{subdir_part}"
74
+
75
+ def _load_with_fallback(
76
+ self,
77
+ module_name: str,
78
+ agent_name: str,
79
+ lang_code: str,
80
+ subdirectory: str | None,
81
+ ) -> dict[str, Any]:
82
+ """Load prompt file with language fallback."""
83
+ prompts_dir = PROJECT_ROOT / "src" / "agents" / module_name / "prompts"
84
+ fallback_chain = self.LANGUAGE_FALLBACKS.get(lang_code, ["en"])
85
+
86
+ for lang in fallback_chain:
87
+ prompt_file = self._resolve_prompt_path(prompts_dir, lang, agent_name, subdirectory)
88
+ if prompt_file and prompt_file.exists():
89
+ try:
90
+ with open(prompt_file, encoding="utf-8") as f:
91
+ return yaml.safe_load(f) or {}
92
+ except Exception as e:
93
+ print(f"Warning: Failed to load {prompt_file}: {e}")
94
+ continue
95
+
96
+ print(f"Warning: No prompt file found for {module_name}/{agent_name}")
97
+ return {}
98
+
99
+ def _resolve_prompt_path(
100
+ self,
101
+ prompts_dir: Path,
102
+ lang: str,
103
+ agent_name: str,
104
+ subdirectory: str | None,
105
+ ) -> Path | None:
106
+ """Resolve prompt file path, supporting subdirectory and recursive search."""
107
+ lang_dir = prompts_dir / lang
108
+
109
+ if not lang_dir.exists():
110
+ return None
111
+
112
+ # If subdirectory specified, look there first
113
+ if subdirectory:
114
+ direct_path = lang_dir / subdirectory / f"{agent_name}.yaml"
115
+ if direct_path.exists():
116
+ return direct_path
117
+
118
+ # Try direct path
119
+ direct_path = lang_dir / f"{agent_name}.yaml"
120
+ if direct_path.exists():
121
+ return direct_path
122
+
123
+ # Recursive search in subdirectories
124
+ found = list(lang_dir.rglob(f"{agent_name}.yaml"))
125
+ if found:
126
+ return found[0]
127
+
128
+ return None
129
+
130
+ def get_prompt(
131
+ self,
132
+ prompts: dict[str, Any],
133
+ section: str,
134
+ field: str | None = None,
135
+ fallback: str = "",
136
+ ) -> str:
137
+ """
138
+ Safely get prompt from loaded configuration.
139
+
140
+ Args:
141
+ prompts: Loaded prompt dictionary
142
+ section: Top-level section name
143
+ field: Optional nested field name
144
+ fallback: Default value if not found
145
+
146
+ Returns:
147
+ Prompt string or fallback
148
+ """
149
+ if section not in prompts:
150
+ return fallback
151
+
152
+ value = prompts[section]
153
+
154
+ if field is None:
155
+ return value if isinstance(value, str) else fallback
156
+
157
+ if isinstance(value, dict) and field in value:
158
+ result = value[field]
159
+ return result if isinstance(result, str) else fallback
160
+
161
+ return fallback
162
+
163
+ def clear_cache(self, module_name: str | None = None) -> None:
164
+ """
165
+ Clear cached prompts.
166
+
167
+ Args:
168
+ module_name: If provided, only clear cache for this module
169
+ """
170
+ if module_name:
171
+ keys_to_remove = [k for k in self._cache if k.startswith(f"{module_name}_")]
172
+ for key in keys_to_remove:
173
+ del self._cache[key]
174
+ else:
175
+ self._cache.clear()
176
+
177
+ def reload_prompts(
178
+ self,
179
+ module_name: str,
180
+ agent_name: str,
181
+ language: str = "zh",
182
+ subdirectory: str | None = None,
183
+ ) -> dict[str, Any]:
184
+ """Force reload prompts, bypassing cache."""
185
+ lang_code = parse_language(language)
186
+ cache_key = self._build_cache_key(module_name, agent_name, lang_code, subdirectory)
187
+
188
+ if cache_key in self._cache:
189
+ del self._cache[cache_key]
190
+
191
+ return self.load_prompts(module_name, agent_name, language, subdirectory)
192
+
193
+
194
+ # Global singleton instance
195
+ _prompt_manager: PromptManager | None = None
196
+
197
+
198
+ def get_prompt_manager() -> PromptManager:
199
+ """Get the global PromptManager instance."""
200
+ global _prompt_manager
201
+ if _prompt_manager is None:
202
+ _prompt_manager = PromptManager()
203
+ return _prompt_manager
204
+
205
+
206
+ __all__ = ["PromptManager", "get_prompt_manager"]
@@ -0,0 +1,64 @@
1
+ """
2
+ RAG Service
3
+ ===========
4
+
5
+ Unified RAG pipeline service for DeepTutor.
6
+
7
+ Provides:
8
+ - RAGService: Unified entry point for all RAG operations
9
+ - Composable RAG pipelines
10
+ - Pre-configured pipelines (RAGAnything, LightRAG, LlamaIndex, Academic)
11
+ - Modular components (parsers, chunkers, embedders, indexers, retrievers)
12
+ - Factory for pipeline creation
13
+
14
+ Usage:
15
+ # Recommended: Use RAGService for all operations
16
+ from src.services.rag import RAGService
17
+
18
+ service = RAGService(provider="llamaindex")
19
+ await service.initialize("kb_name", ["doc1.txt", "doc2.txt"])
20
+ result = await service.search("query", "kb_name")
21
+
22
+ # Alternative: Use factory directly
23
+ from src.services.rag import get_pipeline
24
+
25
+ pipeline = get_pipeline("raganything")
26
+ await pipeline.initialize("kb_name", ["doc1.pdf"])
27
+ result = await pipeline.search("query", "kb_name")
28
+
29
+ # Or build custom pipeline
30
+ from src.services.rag import RAGPipeline
31
+ from src.services.rag.components import TextParser, SemanticChunker
32
+
33
+ custom = (
34
+ RAGPipeline("custom")
35
+ .parser(TextParser())
36
+ .chunker(SemanticChunker())
37
+ )
38
+ """
39
+
40
+ from .factory import get_pipeline, has_pipeline, list_pipelines, register_pipeline
41
+ from .pipeline import RAGPipeline
42
+
43
+ # Import pipeline classes for convenience
44
+ from .pipelines.raganything import RAGAnythingPipeline
45
+ from .service import RAGService
46
+ from .types import Chunk, Document, SearchResult
47
+
48
+ __all__ = [
49
+ # Service (recommended entry point)
50
+ "RAGService",
51
+ # Types
52
+ "Document",
53
+ "Chunk",
54
+ "SearchResult",
55
+ # Pipeline
56
+ "RAGPipeline",
57
+ # Factory
58
+ "get_pipeline",
59
+ "list_pipelines",
60
+ "register_pipeline",
61
+ "has_pipeline",
62
+ # Pipeline implementations
63
+ "RAGAnythingPipeline",
64
+ ]