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,627 @@
1
+ """
2
+ Unified Configuration API Router
3
+ =================================
4
+
5
+ Provides REST API for managing configurations for:
6
+ - LLM (Language Models)
7
+ - Embedding Models
8
+ - TTS (Text-to-Speech)
9
+ - Search Providers
10
+ """
11
+
12
+ import os
13
+ from typing import Any, Dict, Literal, Optional
14
+
15
+ from fastapi import APIRouter, HTTPException
16
+ from pydantic import BaseModel, Field
17
+
18
+ from src.services.config import (
19
+ ConfigType,
20
+ get_config_manager,
21
+ )
22
+ from src.services.llm import complete as llm_complete
23
+ from src.services.llm import sanitize_url
24
+
25
+ router = APIRouter()
26
+
27
+
28
+ # ==================== Request/Response Models ====================
29
+
30
+
31
+ class ConfigBase(BaseModel):
32
+ """Base configuration model."""
33
+
34
+ name: str = Field(..., description="Display name for this configuration")
35
+ provider: str = Field(..., description="Provider type")
36
+
37
+
38
+ class LLMConfigCreate(ConfigBase):
39
+ """LLM configuration for creation."""
40
+
41
+ base_url: str | Dict[str, str] = Field(
42
+ ..., description="API endpoint or {'use_env': 'VAR_NAME'}"
43
+ )
44
+ api_key: str | Dict[str, str] = Field(..., description="API key or {'use_env': 'VAR_NAME'}")
45
+ model: str = Field(..., description="Model name")
46
+ api_version: Optional[str] = None
47
+
48
+
49
+ class EmbeddingConfigCreate(ConfigBase):
50
+ """Embedding configuration for creation."""
51
+
52
+ base_url: str | Dict[str, str]
53
+ api_key: str | Dict[str, str]
54
+ model: str
55
+ dimensions: int = 3072
56
+ api_version: Optional[str] = None
57
+
58
+
59
+ class TTSConfigCreate(ConfigBase):
60
+ """TTS configuration for creation."""
61
+
62
+ base_url: str | Dict[str, str]
63
+ api_key: str | Dict[str, str]
64
+ model: str
65
+ voice: str = "alloy"
66
+ api_version: Optional[str] = None
67
+
68
+
69
+ class SearchConfigCreate(ConfigBase):
70
+ """Search configuration for creation.
71
+
72
+ Uses unified SEARCH_API_KEY environment variable.
73
+ """
74
+
75
+ api_key: str | Dict[str, str] = Field(
76
+ ..., description="API key or {'use_env': 'SEARCH_API_KEY'}"
77
+ )
78
+
79
+
80
+ class ConfigUpdate(BaseModel):
81
+ """Configuration update model."""
82
+
83
+ name: Optional[str] = None
84
+ provider: Optional[str] = None
85
+ base_url: Optional[str | Dict[str, str]] = None
86
+ api_key: Optional[str | Dict[str, str]] = None
87
+ model: Optional[str] = None
88
+ dimensions: Optional[int] = None
89
+ voice: Optional[str] = None
90
+ api_version: Optional[str] = None
91
+
92
+
93
+ class SetActiveRequest(BaseModel):
94
+ """Request to set active configuration."""
95
+
96
+ config_id: str
97
+
98
+
99
+ class TestConnectionRequest(BaseModel):
100
+ """Request to test a connection.
101
+
102
+ base_url and api_key can be either:
103
+ - A string value
104
+ - A dict with {"use_env": "VAR_NAME"} to load from environment
105
+ """
106
+
107
+ provider: str
108
+ base_url: str | Dict[str, str]
109
+ api_key: str | Dict[str, str]
110
+ model: str
111
+ dimensions: Optional[int] = None # For embedding models
112
+ voice: Optional[str] = None # For TTS models
113
+
114
+
115
+ def resolve_env_value(value: str | Dict[str, str], fallback: str = "") -> str:
116
+ """
117
+ Resolve a value that may be a string or {"use_env": "VAR_NAME"}.
118
+
119
+ If value is a dict with use_env, fetch from environment variable.
120
+ Otherwise return the string value directly.
121
+ """
122
+ if isinstance(value, dict) and "use_env" in value:
123
+ env_var = value["use_env"]
124
+ return os.environ.get(env_var, fallback)
125
+ return value if isinstance(value, str) else fallback
126
+
127
+
128
+ class ConfigStatusResponse(BaseModel):
129
+ """Response for configuration status."""
130
+
131
+ llm: Dict[str, Any]
132
+ embedding: Dict[str, Any]
133
+ tts: Dict[str, Any]
134
+ search: Dict[str, Any]
135
+
136
+
137
+ class PortsResponse(BaseModel):
138
+ """Response for port configuration."""
139
+
140
+ backend_port: int
141
+ frontend_port: int
142
+
143
+
144
+ # ==================== Status Endpoints ====================
145
+
146
+
147
+ @router.get("/status", response_model=ConfigStatusResponse)
148
+ async def get_config_status():
149
+ """
150
+ Get configuration status for all services.
151
+ Shows which service has active configuration and what it is.
152
+ """
153
+ manager = get_config_manager()
154
+
155
+ def get_status(config_type: ConfigType) -> Dict[str, Any]:
156
+ active = manager.get_active_config(config_type)
157
+ env_status = manager.get_env_status(config_type)
158
+ configs = manager.list_configs(config_type)
159
+ active_config = next((c for c in configs if c.get("is_active")), None)
160
+
161
+ return {
162
+ "configured": bool(
163
+ active and active.get("model" if config_type != ConfigType.SEARCH else "provider")
164
+ ),
165
+ "active_config_id": active_config.get("id") if active_config else "default",
166
+ "active_config_name": active_config.get("name") if active_config else "Default",
167
+ "model": active.get("model") if active else None,
168
+ "provider": active.get("provider") if active else None,
169
+ "env_configured": env_status,
170
+ "total_configs": len(configs),
171
+ }
172
+
173
+ return ConfigStatusResponse(
174
+ llm=get_status(ConfigType.LLM),
175
+ embedding=get_status(ConfigType.EMBEDDING),
176
+ tts=get_status(ConfigType.TTS),
177
+ search=get_status(ConfigType.SEARCH),
178
+ )
179
+
180
+
181
+ @router.get("/ports", response_model=PortsResponse)
182
+ async def get_ports():
183
+ """Get current port configuration (read-only)."""
184
+ return PortsResponse(
185
+ backend_port=int(os.environ.get("BACKEND_PORT", 8000)),
186
+ frontend_port=int(os.environ.get("FRONTEND_PORT", 3000)),
187
+ )
188
+
189
+
190
+ @router.get("/providers/{config_type}")
191
+ async def get_providers(config_type: Literal["llm", "embedding", "tts", "search"]):
192
+ """Get available provider options for a configuration type."""
193
+ manager = get_config_manager()
194
+ ct = ConfigType(config_type)
195
+ return {"providers": manager.get_provider_options(ct)}
196
+
197
+
198
+ # ==================== LLM Configuration Endpoints ====================
199
+
200
+
201
+ @router.get("/llm")
202
+ async def list_llm_configs():
203
+ """List all LLM configurations."""
204
+ manager = get_config_manager()
205
+ return {"configs": manager.list_configs(ConfigType.LLM)}
206
+
207
+
208
+ @router.post("/llm")
209
+ async def add_llm_config(config: LLMConfigCreate):
210
+ """Add a new LLM configuration."""
211
+ manager = get_config_manager()
212
+ data = config.model_dump()
213
+ result = manager.add_config(ConfigType.LLM, data)
214
+ return result
215
+
216
+
217
+ @router.put("/llm/{config_id}")
218
+ async def update_llm_config(config_id: str, updates: ConfigUpdate):
219
+ """Update an LLM configuration."""
220
+ if config_id == "default":
221
+ raise HTTPException(status_code=400, detail="Cannot update default configuration")
222
+
223
+ manager = get_config_manager()
224
+ result = manager.update_config(ConfigType.LLM, config_id, updates.model_dump(exclude_none=True))
225
+ if not result:
226
+ raise HTTPException(status_code=404, detail="Configuration not found")
227
+ return result
228
+
229
+
230
+ @router.delete("/llm/{config_id}")
231
+ async def delete_llm_config(config_id: str):
232
+ """Delete an LLM configuration."""
233
+ if config_id == "default":
234
+ raise HTTPException(status_code=400, detail="Cannot delete default configuration")
235
+
236
+ manager = get_config_manager()
237
+ success = manager.delete_config(ConfigType.LLM, config_id)
238
+ if not success:
239
+ raise HTTPException(status_code=404, detail="Configuration not found")
240
+ return {"message": "Configuration deleted"}
241
+
242
+
243
+ @router.post("/llm/{config_id}/active")
244
+ async def set_active_llm_config(config_id: str):
245
+ """Set an LLM configuration as active."""
246
+ manager = get_config_manager()
247
+ success = manager.set_active_config(ConfigType.LLM, config_id)
248
+ if not success:
249
+ raise HTTPException(status_code=404, detail="Configuration not found")
250
+ return {"message": "Configuration activated", "active_id": config_id}
251
+
252
+
253
+ @router.post("/llm/test")
254
+ async def test_llm_connection(request: TestConnectionRequest):
255
+ """Test connection to an LLM provider."""
256
+ try:
257
+ # Resolve use_env references to actual values
258
+ base_url = resolve_env_value(request.base_url)
259
+ api_key = resolve_env_value(request.api_key)
260
+
261
+ # Validate required fields
262
+ if not base_url:
263
+ return {"success": False, "message": "Base URL is required"}
264
+ if not request.model:
265
+ return {"success": False, "message": "Model name is required"}
266
+
267
+ base_url = sanitize_url(base_url)
268
+ api_key = api_key or "sk-no-key-required"
269
+
270
+ response = await llm_complete(
271
+ model=request.model,
272
+ prompt="Hello, are you working?",
273
+ system_prompt="You are a helpful assistant. Reply with 'Yes'.",
274
+ api_key=api_key,
275
+ base_url=base_url,
276
+ binding=request.provider,
277
+ max_tokens=200,
278
+ )
279
+ return {"success": True, "message": "Connection successful", "response": response[:100]}
280
+ except Exception as e:
281
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
282
+
283
+
284
+ @router.post("/llm/{config_id}/test")
285
+ async def test_llm_config_by_id(config_id: str):
286
+ """Test connection for an existing LLM configuration by ID."""
287
+ try:
288
+ manager = get_config_manager()
289
+
290
+ if config_id == "default":
291
+ # Get default config from env
292
+ config = manager.get_default_config(ConfigType.LLM)
293
+ else:
294
+ # Find config by ID
295
+ configs = manager.list_configs(ConfigType.LLM)
296
+ config = next((c for c in configs if c.get("id") == config_id), None)
297
+ if not config:
298
+ return {"success": False, "message": f"Configuration '{config_id}' not found"}
299
+ # Resolve use_env references for user configs
300
+ config = manager.resolve_config_env_values(config)
301
+
302
+ if not config or not config.get("base_url"):
303
+ return {"success": False, "message": "Configuration is incomplete (missing base_url)"}
304
+
305
+ base_url = sanitize_url(config.get("base_url", ""))
306
+ api_key = config.get("api_key") or "sk-no-key-required"
307
+ model = config.get("model", "")
308
+ provider = config.get("provider", "openai")
309
+
310
+ response = await llm_complete(
311
+ model=model,
312
+ prompt="Hello, are you working?",
313
+ system_prompt="You are a helpful assistant. Reply with 'Yes'.",
314
+ api_key=api_key,
315
+ base_url=base_url,
316
+ binding=provider,
317
+ max_tokens=200,
318
+ )
319
+ return {"success": True, "message": "Connection successful", "response": response[:100]}
320
+ except Exception as e:
321
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
322
+
323
+
324
+ # ==================== Embedding Configuration Endpoints ====================
325
+
326
+
327
+ @router.get("/embedding")
328
+ async def list_embedding_configs():
329
+ """List all embedding configurations."""
330
+ manager = get_config_manager()
331
+ return {"configs": manager.list_configs(ConfigType.EMBEDDING)}
332
+
333
+
334
+ @router.post("/embedding")
335
+ async def add_embedding_config(config: EmbeddingConfigCreate):
336
+ """Add a new embedding configuration."""
337
+ manager = get_config_manager()
338
+ data = config.model_dump()
339
+ result = manager.add_config(ConfigType.EMBEDDING, data)
340
+ return result
341
+
342
+
343
+ @router.put("/embedding/{config_id}")
344
+ async def update_embedding_config(config_id: str, updates: ConfigUpdate):
345
+ """Update an embedding configuration."""
346
+ if config_id == "default":
347
+ raise HTTPException(status_code=400, detail="Cannot update default configuration")
348
+
349
+ manager = get_config_manager()
350
+ result = manager.update_config(
351
+ ConfigType.EMBEDDING, config_id, updates.model_dump(exclude_none=True)
352
+ )
353
+ if not result:
354
+ raise HTTPException(status_code=404, detail="Configuration not found")
355
+ return result
356
+
357
+
358
+ @router.delete("/embedding/{config_id}")
359
+ async def delete_embedding_config(config_id: str):
360
+ """Delete an embedding configuration."""
361
+ if config_id == "default":
362
+ raise HTTPException(status_code=400, detail="Cannot delete default configuration")
363
+
364
+ manager = get_config_manager()
365
+ success = manager.delete_config(ConfigType.EMBEDDING, config_id)
366
+ if not success:
367
+ raise HTTPException(status_code=404, detail="Configuration not found")
368
+ return {"message": "Configuration deleted"}
369
+
370
+
371
+ @router.post("/embedding/{config_id}/active")
372
+ async def set_active_embedding_config(config_id: str):
373
+ """Set an embedding configuration as active."""
374
+ manager = get_config_manager()
375
+ success = manager.set_active_config(ConfigType.EMBEDDING, config_id)
376
+ if not success:
377
+ raise HTTPException(status_code=404, detail="Configuration not found")
378
+ return {"message": "Configuration activated", "active_id": config_id}
379
+
380
+
381
+ @router.post("/embedding/test")
382
+ async def test_embedding_connection(request: TestConnectionRequest):
383
+ """Test connection to an embedding provider."""
384
+ try:
385
+ from src.services.embedding.client import EmbeddingClient
386
+ from src.services.embedding.config import EmbeddingConfig
387
+
388
+ # Resolve use_env references
389
+ base_url = resolve_env_value(request.base_url)
390
+ api_key = resolve_env_value(request.api_key)
391
+
392
+ # Validate required fields
393
+ if not base_url:
394
+ return {"success": False, "message": "Base URL is required"}
395
+ if not request.model:
396
+ return {"success": False, "message": "Model name is required"}
397
+ if not request.dimensions:
398
+ return {"success": False, "message": "Dimensions is required for embedding models"}
399
+
400
+ # Create a test config with dimensions
401
+ test_config = EmbeddingConfig(
402
+ model=request.model,
403
+ api_key=api_key or "sk-no-key-required",
404
+ base_url=base_url,
405
+ binding=request.provider,
406
+ dim=request.dimensions,
407
+ )
408
+
409
+ # Create a temporary client for testing
410
+ client = EmbeddingClient(test_config)
411
+ # Use embed() method with a list of texts
412
+ embeddings = await client.embed(["test"])
413
+ if embeddings and len(embeddings) > 0 and len(embeddings[0]) > 0:
414
+ return {"success": True, "message": f"Connection successful (dim={len(embeddings[0])})"}
415
+ return {"success": False, "message": "Failed to generate embeddings"}
416
+ except Exception as e:
417
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
418
+
419
+
420
+ @router.post("/embedding/{config_id}/test")
421
+ async def test_embedding_config_by_id(config_id: str):
422
+ """Test connection for an existing embedding configuration by ID."""
423
+ try:
424
+ from src.services.embedding.client import EmbeddingClient
425
+ from src.services.embedding.config import EmbeddingConfig
426
+
427
+ manager = get_config_manager()
428
+
429
+ if config_id == "default":
430
+ config = manager.get_default_config(ConfigType.EMBEDDING)
431
+ else:
432
+ configs = manager.list_configs(ConfigType.EMBEDDING)
433
+ config = next((c for c in configs if c.get("id") == config_id), None)
434
+ if not config:
435
+ return {"success": False, "message": f"Configuration '{config_id}' not found"}
436
+ config = manager.resolve_config_env_values(config)
437
+
438
+ if not config or not config.get("base_url"):
439
+ return {"success": False, "message": "Configuration is incomplete (missing base_url)"}
440
+
441
+ # Create a test config
442
+ test_config = EmbeddingConfig(
443
+ model=config.get("model", ""),
444
+ api_key=config.get("api_key") or "sk-no-key-required",
445
+ base_url=config.get("base_url", ""),
446
+ binding=config.get("provider", "openai"),
447
+ dim=config.get("dimensions", 3072),
448
+ )
449
+
450
+ # Create a temporary client for testing
451
+ client = EmbeddingClient(test_config)
452
+ # Use embed() method with a list of texts
453
+ embeddings = await client.embed(["test"])
454
+ if embeddings and len(embeddings) > 0 and len(embeddings[0]) > 0:
455
+ return {"success": True, "message": f"Connection successful (dim={len(embeddings[0])})"}
456
+ return {"success": False, "message": "Failed to generate embeddings"}
457
+ except Exception as e:
458
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
459
+
460
+
461
+ # ==================== TTS Configuration Endpoints ====================
462
+
463
+
464
+ @router.get("/tts")
465
+ async def list_tts_configs():
466
+ """List all TTS configurations."""
467
+ manager = get_config_manager()
468
+ return {"configs": manager.list_configs(ConfigType.TTS)}
469
+
470
+
471
+ @router.post("/tts")
472
+ async def add_tts_config(config: TTSConfigCreate):
473
+ """Add a new TTS configuration."""
474
+ manager = get_config_manager()
475
+ data = config.model_dump()
476
+ result = manager.add_config(ConfigType.TTS, data)
477
+ return result
478
+
479
+
480
+ @router.put("/tts/{config_id}")
481
+ async def update_tts_config(config_id: str, updates: ConfigUpdate):
482
+ """Update a TTS configuration."""
483
+ if config_id == "default":
484
+ raise HTTPException(status_code=400, detail="Cannot update default configuration")
485
+
486
+ manager = get_config_manager()
487
+ result = manager.update_config(ConfigType.TTS, config_id, updates.model_dump(exclude_none=True))
488
+ if not result:
489
+ raise HTTPException(status_code=404, detail="Configuration not found")
490
+ return result
491
+
492
+
493
+ @router.delete("/tts/{config_id}")
494
+ async def delete_tts_config(config_id: str):
495
+ """Delete a TTS configuration."""
496
+ if config_id == "default":
497
+ raise HTTPException(status_code=400, detail="Cannot delete default configuration")
498
+
499
+ manager = get_config_manager()
500
+ success = manager.delete_config(ConfigType.TTS, config_id)
501
+ if not success:
502
+ raise HTTPException(status_code=404, detail="Configuration not found")
503
+ return {"message": "Configuration deleted"}
504
+
505
+
506
+ @router.post("/tts/{config_id}/active")
507
+ async def set_active_tts_config(config_id: str):
508
+ """Set a TTS configuration as active."""
509
+ manager = get_config_manager()
510
+ success = manager.set_active_config(ConfigType.TTS, config_id)
511
+ if not success:
512
+ raise HTTPException(status_code=404, detail="Configuration not found")
513
+ return {"message": "Configuration activated", "active_id": config_id}
514
+
515
+
516
+ @router.post("/tts/test")
517
+ async def test_tts_connection(request: TestConnectionRequest):
518
+ """Test connection to a TTS provider."""
519
+ try:
520
+ from openai import AsyncOpenAI
521
+
522
+ # Resolve use_env references
523
+ base_url = resolve_env_value(request.base_url)
524
+ api_key = resolve_env_value(request.api_key)
525
+
526
+ # Validate required fields
527
+ if not base_url:
528
+ return {"success": False, "message": "Base URL is required"}
529
+ if not request.model:
530
+ return {"success": False, "message": "Model name is required"}
531
+
532
+ # Test by creating client and checking if we can reach the API
533
+ client = AsyncOpenAI(
534
+ api_key=api_key or "sk-no-key-required",
535
+ base_url=base_url,
536
+ )
537
+ # Just verify we can create the client - actual TTS test would require audio generation
538
+ return {"success": True, "message": "TTS configuration validated"}
539
+ except Exception as e:
540
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
541
+
542
+
543
+ @router.post("/tts/{config_id}/test")
544
+ async def test_tts_config_by_id(config_id: str):
545
+ """Test connection for an existing TTS configuration by ID."""
546
+ try:
547
+ from openai import AsyncOpenAI
548
+
549
+ manager = get_config_manager()
550
+
551
+ if config_id == "default":
552
+ config = manager.get_default_config(ConfigType.TTS)
553
+ else:
554
+ configs = manager.list_configs(ConfigType.TTS)
555
+ config = next((c for c in configs if c.get("id") == config_id), None)
556
+ if not config:
557
+ return {"success": False, "message": f"Configuration '{config_id}' not found"}
558
+ config = manager.resolve_config_env_values(config)
559
+
560
+ if not config or not config.get("base_url"):
561
+ return {"success": False, "message": "Configuration is incomplete (missing base_url)"}
562
+
563
+ # Test by creating client
564
+ client = AsyncOpenAI(
565
+ api_key=config.get("api_key") or "sk-no-key-required",
566
+ base_url=config.get("base_url", ""),
567
+ )
568
+ return {"success": True, "message": "TTS configuration validated"}
569
+ except Exception as e:
570
+ return {"success": False, "message": f"Connection failed: {str(e)}"}
571
+
572
+
573
+ # ==================== Search Configuration Endpoints ====================
574
+
575
+
576
+ @router.get("/search")
577
+ async def list_search_configs():
578
+ """List all search configurations."""
579
+ manager = get_config_manager()
580
+ return {"configs": manager.list_configs(ConfigType.SEARCH)}
581
+
582
+
583
+ @router.post("/search")
584
+ async def add_search_config(config: SearchConfigCreate):
585
+ """Add a new search configuration."""
586
+ manager = get_config_manager()
587
+ data = config.model_dump()
588
+ result = manager.add_config(ConfigType.SEARCH, data)
589
+ return result
590
+
591
+
592
+ @router.put("/search/{config_id}")
593
+ async def update_search_config(config_id: str, updates: ConfigUpdate):
594
+ """Update a search configuration."""
595
+ if config_id == "default":
596
+ raise HTTPException(status_code=400, detail="Cannot update default configuration")
597
+
598
+ manager = get_config_manager()
599
+ result = manager.update_config(
600
+ ConfigType.SEARCH, config_id, updates.model_dump(exclude_none=True)
601
+ )
602
+ if not result:
603
+ raise HTTPException(status_code=404, detail="Configuration not found")
604
+ return result
605
+
606
+
607
+ @router.delete("/search/{config_id}")
608
+ async def delete_search_config(config_id: str):
609
+ """Delete a search configuration."""
610
+ if config_id == "default":
611
+ raise HTTPException(status_code=400, detail="Cannot delete default configuration")
612
+
613
+ manager = get_config_manager()
614
+ success = manager.delete_config(ConfigType.SEARCH, config_id)
615
+ if not success:
616
+ raise HTTPException(status_code=404, detail="Configuration not found")
617
+ return {"message": "Configuration deleted"}
618
+
619
+
620
+ @router.post("/search/{config_id}/active")
621
+ async def set_active_search_config(config_id: str):
622
+ """Set a search configuration as active."""
623
+ manager = get_config_manager()
624
+ success = manager.set_active_config(ConfigType.SEARCH, config_id)
625
+ if not success:
626
+ raise HTTPException(status_code=404, detail="Configuration not found")
627
+ return {"message": "Configuration activated", "active_id": config_id}
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter, HTTPException
2
+
3
+ from src.api.utils.history import history_manager
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/recent")
9
+ async def get_recent_history(limit: int = 10, type: str | None = None):
10
+ return history_manager.get_recent(limit, type)
11
+
12
+
13
+ @router.get("/{entry_id}")
14
+ async def get_history_entry(entry_id: str):
15
+ entry = history_manager.get_entry(entry_id)
16
+ if not entry:
17
+ raise HTTPException(status_code=404, detail="Entry not found")
18
+ return entry