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,603 @@
1
+ """
2
+ Unified Configuration Manager
3
+ =============================
4
+
5
+ Manages configurations for LLM, Embedding, TTS, and Search services.
6
+ Supports both .env defaults and user-defined configurations.
7
+ """
8
+
9
+ from enum import Enum
10
+ import json
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+ import uuid
16
+
17
+ from dotenv import load_dotenv
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Load environment variables
22
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
23
+ load_dotenv(PROJECT_ROOT / "DeepTutor.env", override=False)
24
+ load_dotenv(PROJECT_ROOT / ".env", override=False)
25
+
26
+ # Storage directory
27
+ SETTINGS_DIR = PROJECT_ROOT / "data" / "user" / "settings"
28
+
29
+
30
+ class ConfigType(str, Enum):
31
+ """Configuration types."""
32
+
33
+ LLM = "llm"
34
+ EMBEDDING = "embedding"
35
+ TTS = "tts"
36
+ SEARCH = "search"
37
+
38
+
39
+ # Provider options for each service type
40
+ PROVIDER_OPTIONS = {
41
+ ConfigType.LLM: [
42
+ "openai",
43
+ "anthropic",
44
+ "azure_openai",
45
+ "deepseek",
46
+ "ollama",
47
+ "lm_studio",
48
+ "vllm",
49
+ ],
50
+ ConfigType.EMBEDDING: ["openai", "azure_openai", "ollama", "jina", "cohere", "huggingface"],
51
+ ConfigType.TTS: ["openai", "azure_openai"],
52
+ ConfigType.SEARCH: ["perplexity", "tavily", "exa", "jina", "serper", "baidu"],
53
+ }
54
+
55
+ # Environment variable mappings for each service type
56
+ ENV_VAR_MAPPINGS = {
57
+ ConfigType.LLM: {
58
+ "provider": "LLM_BINDING",
59
+ "base_url": "LLM_HOST",
60
+ "api_key": "LLM_API_KEY",
61
+ "model": "LLM_MODEL",
62
+ "api_version": "LLM_API_VERSION",
63
+ },
64
+ ConfigType.EMBEDDING: {
65
+ "provider": "EMBEDDING_BINDING",
66
+ "base_url": "EMBEDDING_HOST",
67
+ "api_key": "EMBEDDING_API_KEY",
68
+ "model": "EMBEDDING_MODEL",
69
+ "dimensions": "EMBEDDING_DIMENSION",
70
+ "api_version": "EMBEDDING_API_VERSION",
71
+ },
72
+ ConfigType.TTS: {
73
+ "provider": "TTS_BINDING",
74
+ "base_url": "TTS_URL",
75
+ "api_key": "TTS_API_KEY",
76
+ "model": "TTS_MODEL",
77
+ "voice": "TTS_VOICE",
78
+ "api_version": "TTS_BINDING_API_VERSION",
79
+ },
80
+ ConfigType.SEARCH: {
81
+ "provider": "SEARCH_PROVIDER",
82
+ "api_key": "SEARCH_API_KEY", # Unified API key for all providers
83
+ },
84
+ }
85
+
86
+
87
+ def _resolve_env_value(value: Any) -> Any:
88
+ """
89
+ Resolve a value that may reference an environment variable.
90
+
91
+ If value is {"use_env": "VAR_NAME"}, returns os.environ.get("VAR_NAME").
92
+ Otherwise, returns the value as-is.
93
+ """
94
+ if isinstance(value, dict) and "use_env" in value:
95
+ env_var = value["use_env"]
96
+ return os.environ.get(env_var, "")
97
+ return value
98
+
99
+
100
+ def _get_env_value(env_var: str) -> Optional[str]:
101
+ """Get and strip an environment variable value."""
102
+ value = os.environ.get(env_var)
103
+ if value:
104
+ return value.strip().strip("\"'")
105
+ return None
106
+
107
+
108
+ class UnifiedConfigManager:
109
+ """
110
+ Manages configurations for all service types.
111
+
112
+ Each service type has:
113
+ - A "default" configuration that comes from .env (cannot be deleted)
114
+ - User-defined configurations that can be added/edited/deleted
115
+ - An "active" configuration that is currently in use
116
+ """
117
+
118
+ _instance: Optional["UnifiedConfigManager"] = None
119
+
120
+ def __new__(cls):
121
+ if cls._instance is None:
122
+ cls._instance = super(UnifiedConfigManager, cls).__new__(cls)
123
+ cls._instance._initialized = False
124
+ return cls._instance
125
+
126
+ def __init__(self):
127
+ if getattr(self, "_initialized", False):
128
+ return
129
+
130
+ SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
131
+ self._initialized = True
132
+
133
+ # Ensure default configs exist and are synced with env on startup
134
+ self._ensure_default_configs()
135
+
136
+ def _ensure_default_configs(self) -> None:
137
+ """
138
+ Ensure default configurations exist in storage files and sync with env.
139
+
140
+ This method is called on startup to:
141
+ 1. Create a "default" config entry in each config file if it doesn't exist
142
+ 2. Sync the model/provider fields from env variables (these may change)
143
+ 3. Store base_url/api_key as {"use_env": "VAR_NAME"} references
144
+ """
145
+ for config_type in ConfigType:
146
+ try:
147
+ self._ensure_default_config_for_type(config_type)
148
+ except Exception as e:
149
+ logger.warning(f"Failed to ensure default config for {config_type.value}: {e}")
150
+
151
+ def _ensure_default_config_for_type(self, config_type: ConfigType) -> None:
152
+ """Ensure default config exists and is synced for a specific config type."""
153
+ env_mapping = ENV_VAR_MAPPINGS.get(config_type, {})
154
+ data = self._load_configs(config_type)
155
+
156
+ # Find existing default config in the configs list
157
+ default_config = None
158
+ default_index = -1
159
+ for i, cfg in enumerate(data.get("configs", [])):
160
+ if cfg.get("id") == "default":
161
+ default_config = cfg
162
+ default_index = i
163
+ break
164
+
165
+ # Build the default config to store
166
+ stored_default = self._build_stored_default_config(config_type, env_mapping)
167
+
168
+ if default_config is None:
169
+ # Add default config to the list
170
+ if "configs" not in data:
171
+ data["configs"] = []
172
+ data["configs"].insert(0, stored_default)
173
+ logger.info(f"Created default config for {config_type.value}")
174
+ else:
175
+ # Update only the dynamic fields (model, provider) from env
176
+ # Keep other fields unchanged (they reference env vars)
177
+ if config_type == ConfigType.LLM:
178
+ default_config["model"] = _get_env_value(env_mapping.get("model")) or ""
179
+ default_config["provider"] = _get_env_value(env_mapping.get("provider")) or "openai"
180
+ elif config_type == ConfigType.EMBEDDING:
181
+ default_config["model"] = _get_env_value(env_mapping.get("model")) or ""
182
+ default_config["provider"] = _get_env_value(env_mapping.get("provider")) or "openai"
183
+ dim_str = _get_env_value(env_mapping.get("dimensions"))
184
+ default_config["dimensions"] = (
185
+ int(dim_str) if dim_str and dim_str.isdigit() else 3072
186
+ )
187
+ elif config_type == ConfigType.TTS:
188
+ default_config["model"] = _get_env_value(env_mapping.get("model")) or ""
189
+ default_config["provider"] = _get_env_value(env_mapping.get("provider")) or "openai"
190
+ default_config["voice"] = _get_env_value(env_mapping.get("voice")) or "alloy"
191
+ elif config_type == ConfigType.SEARCH:
192
+ default_config["provider"] = (
193
+ _get_env_value(env_mapping.get("provider")) or "perplexity"
194
+ )
195
+
196
+ data["configs"][default_index] = default_config
197
+ logger.debug(f"Updated default config for {config_type.value}")
198
+
199
+ self._save_configs(config_type, data)
200
+
201
+ def _build_stored_default_config(
202
+ self, config_type: ConfigType, env_mapping: Dict[str, str]
203
+ ) -> Dict[str, Any]:
204
+ """Build the default configuration to be stored in JSON (with env references)."""
205
+ base_config = {
206
+ "id": "default",
207
+ "name": "Default (from .env)",
208
+ "is_default": True,
209
+ }
210
+
211
+ if config_type == ConfigType.LLM:
212
+ return {
213
+ **base_config,
214
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
215
+ "model": _get_env_value(env_mapping.get("model")) or "",
216
+ "base_url": {"use_env": "LLM_HOST"},
217
+ "api_key": {"use_env": "LLM_API_KEY"},
218
+ "api_version": {"use_env": "LLM_API_VERSION"},
219
+ }
220
+
221
+ elif config_type == ConfigType.EMBEDDING:
222
+ dim_str = _get_env_value(env_mapping.get("dimensions"))
223
+ return {
224
+ **base_config,
225
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
226
+ "model": _get_env_value(env_mapping.get("model")) or "",
227
+ "dimensions": int(dim_str) if dim_str and dim_str.isdigit() else 3072,
228
+ "base_url": {"use_env": "EMBEDDING_HOST"},
229
+ "api_key": {"use_env": "EMBEDDING_API_KEY"},
230
+ "api_version": {"use_env": "EMBEDDING_API_VERSION"},
231
+ }
232
+
233
+ elif config_type == ConfigType.TTS:
234
+ return {
235
+ **base_config,
236
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
237
+ "model": _get_env_value(env_mapping.get("model")) or "",
238
+ "voice": _get_env_value(env_mapping.get("voice")) or "alloy",
239
+ "base_url": {"use_env": "TTS_URL"},
240
+ "api_key": {"use_env": "TTS_API_KEY"},
241
+ "api_version": {"use_env": "TTS_BINDING_API_VERSION"},
242
+ }
243
+
244
+ elif config_type == ConfigType.SEARCH:
245
+ return {
246
+ **base_config,
247
+ "provider": _get_env_value(env_mapping.get("provider")) or "perplexity",
248
+ "api_key": {"use_env": "SEARCH_API_KEY"},
249
+ }
250
+
251
+ return base_config
252
+
253
+ def _get_storage_path(self, config_type: ConfigType) -> Path:
254
+ """Get the storage file path for a config type."""
255
+ return SETTINGS_DIR / f"{config_type.value}_configs.json"
256
+
257
+ def _load_configs(self, config_type: ConfigType) -> Dict[str, Any]:
258
+ """Load configurations from storage."""
259
+ path = self._get_storage_path(config_type)
260
+ if path.exists():
261
+ try:
262
+ with open(path, "r", encoding="utf-8") as f:
263
+ return json.load(f)
264
+ except (json.JSONDecodeError, IOError) as e:
265
+ logger.warning(f"Failed to load {config_type.value} configs: {e}")
266
+ return {"configs": [], "active_id": "default"}
267
+
268
+ def _save_configs(self, config_type: ConfigType, data: Dict[str, Any]) -> bool:
269
+ """Save configurations to storage."""
270
+ path = self._get_storage_path(config_type)
271
+ try:
272
+ with open(path, "w", encoding="utf-8") as f:
273
+ json.dump(data, f, indent=2, ensure_ascii=False)
274
+ return True
275
+ except IOError as e:
276
+ logger.error(f"Failed to save {config_type.value} configs: {e}")
277
+ return False
278
+
279
+ def _build_default_config(self, config_type: ConfigType) -> Dict[str, Any]:
280
+ """Build the default configuration from environment variables."""
281
+ env_mapping = ENV_VAR_MAPPINGS.get(config_type, {})
282
+
283
+ if config_type == ConfigType.LLM:
284
+ return {
285
+ "id": "default",
286
+ "name": "Default (from .env)",
287
+ "is_default": True,
288
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
289
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
290
+ "api_key": "***", # Hidden for security
291
+ "model": _get_env_value(env_mapping.get("model")) or "",
292
+ "api_version": _get_env_value(env_mapping.get("api_version")),
293
+ }
294
+
295
+ elif config_type == ConfigType.EMBEDDING:
296
+ dim_str = _get_env_value(env_mapping.get("dimensions"))
297
+ return {
298
+ "id": "default",
299
+ "name": "Default (from .env)",
300
+ "is_default": True,
301
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
302
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
303
+ "api_key": "***",
304
+ "model": _get_env_value(env_mapping.get("model")) or "",
305
+ "dimensions": int(dim_str) if dim_str and dim_str.isdigit() else 3072,
306
+ "api_version": _get_env_value(env_mapping.get("api_version")),
307
+ }
308
+
309
+ elif config_type == ConfigType.TTS:
310
+ return {
311
+ "id": "default",
312
+ "name": "Default (from .env)",
313
+ "is_default": True,
314
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
315
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
316
+ "api_key": "***",
317
+ "model": _get_env_value(env_mapping.get("model")) or "",
318
+ "voice": _get_env_value(env_mapping.get("voice")) or "alloy",
319
+ "api_version": _get_env_value(env_mapping.get("api_version")),
320
+ }
321
+
322
+ elif config_type == ConfigType.SEARCH:
323
+ provider = _get_env_value(env_mapping.get("provider")) or "perplexity"
324
+ return {
325
+ "id": "default",
326
+ "name": "Default (from .env)",
327
+ "is_default": True,
328
+ "provider": provider,
329
+ "api_key": "***",
330
+ }
331
+
332
+ return {"id": "default", "name": "Default (from .env)", "is_default": True}
333
+
334
+ def _get_default_config_resolved(self, config_type: ConfigType) -> Dict[str, Any]:
335
+ """Get the default configuration with actual values resolved (for internal use)."""
336
+ env_mapping = ENV_VAR_MAPPINGS.get(config_type, {})
337
+
338
+ if config_type == ConfigType.LLM:
339
+ return {
340
+ "id": "default",
341
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
342
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
343
+ "api_key": _get_env_value(env_mapping.get("api_key")) or "",
344
+ "model": _get_env_value(env_mapping.get("model")) or "",
345
+ "api_version": _get_env_value(env_mapping.get("api_version")),
346
+ }
347
+
348
+ elif config_type == ConfigType.EMBEDDING:
349
+ dim_str = _get_env_value(env_mapping.get("dimensions"))
350
+ return {
351
+ "id": "default",
352
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
353
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
354
+ "api_key": _get_env_value(env_mapping.get("api_key")) or "",
355
+ "model": _get_env_value(env_mapping.get("model")) or "",
356
+ "dimensions": int(dim_str) if dim_str and dim_str.isdigit() else 3072,
357
+ "api_version": _get_env_value(env_mapping.get("api_version")),
358
+ }
359
+
360
+ elif config_type == ConfigType.TTS:
361
+ return {
362
+ "id": "default",
363
+ "provider": _get_env_value(env_mapping.get("provider")) or "openai",
364
+ "base_url": _get_env_value(env_mapping.get("base_url")) or "",
365
+ "api_key": _get_env_value(env_mapping.get("api_key")) or "",
366
+ "model": _get_env_value(env_mapping.get("model")) or "",
367
+ "voice": _get_env_value(env_mapping.get("voice")) or "alloy",
368
+ "api_version": _get_env_value(env_mapping.get("api_version")),
369
+ }
370
+
371
+ elif config_type == ConfigType.SEARCH:
372
+ provider = _get_env_value(env_mapping.get("provider")) or "perplexity"
373
+ return {
374
+ "id": "default",
375
+ "provider": provider,
376
+ "api_key": _get_env_value(env_mapping.get("api_key")) or "",
377
+ }
378
+
379
+ return {"id": "default"}
380
+
381
+ def _resolve_config(self, config: Dict[str, Any], config_type: ConfigType) -> Dict[str, Any]:
382
+ """Resolve all {"use_env": ...} references in a configuration."""
383
+ resolved = {}
384
+ env_mapping = ENV_VAR_MAPPINGS.get(config_type, {})
385
+
386
+ for key, value in config.items():
387
+ if isinstance(value, dict) and "use_env" in value:
388
+ env_var = value["use_env"]
389
+ resolved[key] = _get_env_value(env_var) or ""
390
+ else:
391
+ resolved[key] = value
392
+
393
+ return resolved
394
+
395
+ def get_provider_options(self, config_type: ConfigType) -> List[str]:
396
+ """Get available provider options for a config type."""
397
+ return PROVIDER_OPTIONS.get(config_type, [])
398
+
399
+ def list_configs(self, config_type: ConfigType) -> List[Dict[str, Any]]:
400
+ """
401
+ List all configurations for a service type.
402
+ Includes the default config (from .env) and user-defined configs.
403
+
404
+ For display purposes, the default config shows:
405
+ - Current model/provider from env (dynamically refreshed)
406
+ - base_url/api_key as "***" (hidden for security)
407
+ """
408
+ data = self._load_configs(config_type)
409
+ configs = data.get("configs", [])
410
+ active_id = data.get("active_id", "default")
411
+
412
+ # Build a display version of the default config (with current env values)
413
+ display_default = self._build_default_config(config_type)
414
+
415
+ # Process all configs for display
416
+ result = []
417
+ has_default = False
418
+
419
+ for cfg in configs:
420
+ if cfg.get("id") == "default":
421
+ # Use the dynamically built display config for default
422
+ has_default = True
423
+ display_cfg = display_default.copy()
424
+ display_cfg["is_active"] = active_id == "default"
425
+ result.append(display_cfg)
426
+ else:
427
+ # For user configs, create a display copy
428
+ display_cfg = dict(cfg)
429
+ # Hide api_key if it's not an env reference
430
+ if "api_key" in display_cfg:
431
+ api_key = display_cfg["api_key"]
432
+ if not isinstance(api_key, dict): # Not {"use_env": ...}
433
+ display_cfg["api_key"] = "***"
434
+ display_cfg["is_active"] = cfg.get("id") == active_id
435
+ result.append(display_cfg)
436
+
437
+ # If no default config found in file, prepend the display default
438
+ if not has_default:
439
+ display_default["is_active"] = active_id == "default"
440
+ result.insert(0, display_default)
441
+
442
+ return result
443
+
444
+ def get_config(self, config_type: ConfigType, config_id: str) -> Optional[Dict[str, Any]]:
445
+ """Get a specific configuration by ID."""
446
+ if config_id == "default":
447
+ return self._build_default_config(config_type)
448
+
449
+ data = self._load_configs(config_type)
450
+ for cfg in data.get("configs", []):
451
+ if cfg.get("id") == config_id:
452
+ return cfg
453
+ return None
454
+
455
+ def get_active_config(self, config_type: ConfigType) -> Optional[Dict[str, Any]]:
456
+ """
457
+ Get the currently active configuration with all values resolved.
458
+ This is used internally when services need actual configuration values.
459
+ """
460
+ data = self._load_configs(config_type)
461
+ active_id = data.get("active_id", "default")
462
+
463
+ if active_id == "default":
464
+ return self._get_default_config_resolved(config_type)
465
+
466
+ for cfg in data.get("configs", []):
467
+ if cfg.get("id") == active_id:
468
+ return self._resolve_config(cfg, config_type)
469
+
470
+ # Fallback to default if active config not found
471
+ return self._get_default_config_resolved(config_type)
472
+
473
+ def add_config(self, config_type: ConfigType, config: Dict[str, Any]) -> Dict[str, Any]:
474
+ """Add a new configuration."""
475
+ data = self._load_configs(config_type)
476
+
477
+ # Generate ID if not provided
478
+ if "id" not in config:
479
+ config["id"] = str(uuid.uuid4())[:8]
480
+
481
+ # Ensure required fields
482
+ config["is_default"] = False
483
+
484
+ # Add to list
485
+ data["configs"].append(config)
486
+ self._save_configs(config_type, data)
487
+
488
+ return config
489
+
490
+ def update_config(
491
+ self, config_type: ConfigType, config_id: str, updates: Dict[str, Any]
492
+ ) -> Optional[Dict[str, Any]]:
493
+ """Update an existing configuration."""
494
+ if config_id == "default":
495
+ return None # Cannot update default config
496
+
497
+ data = self._load_configs(config_type)
498
+
499
+ for i, cfg in enumerate(data.get("configs", [])):
500
+ if cfg.get("id") == config_id:
501
+ # Update fields
502
+ cfg.update(updates)
503
+ cfg["id"] = config_id # Preserve ID
504
+ cfg["is_default"] = False # Ensure it's not marked as default
505
+ data["configs"][i] = cfg
506
+ self._save_configs(config_type, data)
507
+ return cfg
508
+
509
+ return None
510
+
511
+ def delete_config(self, config_type: ConfigType, config_id: str) -> bool:
512
+ """Delete a configuration."""
513
+ if config_id == "default":
514
+ return False # Cannot delete default config
515
+
516
+ data = self._load_configs(config_type)
517
+ original_len = len(data.get("configs", []))
518
+ data["configs"] = [c for c in data.get("configs", []) if c.get("id") != config_id]
519
+
520
+ if len(data["configs"]) < original_len:
521
+ # If deleted config was active, switch to default
522
+ if data.get("active_id") == config_id:
523
+ data["active_id"] = "default"
524
+ self._save_configs(config_type, data)
525
+ return True
526
+
527
+ return False
528
+
529
+ def set_active_config(self, config_type: ConfigType, config_id: str) -> bool:
530
+ """Set a configuration as active."""
531
+ data = self._load_configs(config_type)
532
+
533
+ # Verify config exists
534
+ if config_id != "default":
535
+ found = any(c.get("id") == config_id for c in data.get("configs", []))
536
+ if not found:
537
+ return False
538
+
539
+ data["active_id"] = config_id
540
+ return self._save_configs(config_type, data)
541
+
542
+ def get_env_status(self, config_type: ConfigType) -> Dict[str, bool]:
543
+ """Check which environment variables are configured for a service type."""
544
+ env_mapping = ENV_VAR_MAPPINGS.get(config_type, {})
545
+ status = {}
546
+
547
+ for field, env_var in env_mapping.items():
548
+ if not env_var.endswith("_key"): # Skip individual search provider keys
549
+ status[field] = bool(_get_env_value(env_var))
550
+
551
+ return status
552
+
553
+ def get_default_config(self, config_type: ConfigType) -> Dict[str, Any]:
554
+ """
555
+ Get the default configuration with actual values resolved.
556
+ Used for testing the default configuration.
557
+ """
558
+ return self._get_default_config_resolved(config_type)
559
+
560
+ def resolve_config_env_values(self, config: Dict[str, Any]) -> Dict[str, Any]:
561
+ """
562
+ Resolve all {"use_env": "VAR_NAME"} references in a user configuration.
563
+ Returns a copy with actual values from environment variables.
564
+ """
565
+ resolved = dict(config)
566
+ for key, value in config.items():
567
+ if isinstance(value, dict) and "use_env" in value:
568
+ env_var = value["use_env"]
569
+ resolved[key] = _get_env_value(env_var) or ""
570
+ return resolved
571
+
572
+
573
+ # Global instance
574
+ _config_manager: Optional[UnifiedConfigManager] = None
575
+
576
+
577
+ def get_config_manager() -> UnifiedConfigManager:
578
+ """Get the global config manager instance."""
579
+ global _config_manager
580
+ if _config_manager is None:
581
+ _config_manager = UnifiedConfigManager()
582
+ return _config_manager
583
+
584
+
585
+ # Convenience functions for services
586
+ def get_active_llm_config() -> Optional[Dict[str, Any]]:
587
+ """Get the active LLM configuration."""
588
+ return get_config_manager().get_active_config(ConfigType.LLM)
589
+
590
+
591
+ def get_active_embedding_config() -> Optional[Dict[str, Any]]:
592
+ """Get the active embedding configuration."""
593
+ return get_config_manager().get_active_config(ConfigType.EMBEDDING)
594
+
595
+
596
+ def get_active_tts_config() -> Optional[Dict[str, Any]]:
597
+ """Get the active TTS configuration."""
598
+ return get_config_manager().get_active_config(ConfigType.TTS)
599
+
600
+
601
+ def get_active_search_config() -> Optional[Dict[str, Any]]:
602
+ """Get the active search configuration."""
603
+ return get_config_manager().get_active_config(ConfigType.SEARCH)
@@ -0,0 +1,45 @@
1
+ """
2
+ Embedding Service
3
+ =================
4
+
5
+ Unified embedding client for all DeepTutor modules.
6
+ Supports multiple providers: OpenAI, Azure, Google, Cohere, Ollama, Jina, HuggingFace.
7
+
8
+ Usage:
9
+ from src.services.embedding import get_embedding_client, EmbeddingClient, EmbeddingConfig
10
+
11
+ # Get singleton client
12
+ client = get_embedding_client()
13
+ vectors = await client.embed(["text1", "text2"])
14
+
15
+ # Get LightRAG-compatible EmbeddingFunc
16
+ embed_func = client.get_embedding_func()
17
+ """
18
+
19
+ from .adapters import (
20
+ BaseEmbeddingAdapter,
21
+ CohereEmbeddingAdapter,
22
+ EmbeddingRequest,
23
+ EmbeddingResponse,
24
+ OllamaEmbeddingAdapter,
25
+ OpenAICompatibleEmbeddingAdapter,
26
+ )
27
+ from .client import EmbeddingClient, get_embedding_client, reset_embedding_client
28
+ from .config import EmbeddingConfig, get_embedding_config
29
+ from .provider import get_embedding_provider_manager, reset_embedding_provider_manager
30
+
31
+ __all__ = [
32
+ "EmbeddingClient",
33
+ "EmbeddingConfig",
34
+ "get_embedding_client",
35
+ "get_embedding_config",
36
+ "reset_embedding_client",
37
+ "get_embedding_provider_manager",
38
+ "reset_embedding_provider_manager",
39
+ "BaseEmbeddingAdapter",
40
+ "EmbeddingRequest",
41
+ "EmbeddingResponse",
42
+ "OpenAICompatibleEmbeddingAdapter",
43
+ "CohereEmbeddingAdapter",
44
+ "OllamaEmbeddingAdapter",
45
+ ]
@@ -0,0 +1,22 @@
1
+ """
2
+ Adapters Package
3
+ ================
4
+
5
+ Embedding adapters for different providers.
6
+ """
7
+
8
+ from .base import BaseEmbeddingAdapter, EmbeddingRequest, EmbeddingResponse
9
+ from .cohere import CohereEmbeddingAdapter
10
+ from .jina import JinaEmbeddingAdapter
11
+ from .ollama import OllamaEmbeddingAdapter
12
+ from .openai_compatible import OpenAICompatibleEmbeddingAdapter
13
+
14
+ __all__ = [
15
+ "BaseEmbeddingAdapter",
16
+ "EmbeddingRequest",
17
+ "EmbeddingResponse",
18
+ "OpenAICompatibleEmbeddingAdapter",
19
+ "JinaEmbeddingAdapter",
20
+ "CohereEmbeddingAdapter",
21
+ "OllamaEmbeddingAdapter",
22
+ ]