realtimex-deeptutor 0.5.0.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- realtimex_deeptutor/__init__.py +67 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
- src/__init__.py +40 -0
- src/agents/__init__.py +24 -0
- src/agents/base_agent.py +657 -0
- src/agents/chat/__init__.py +24 -0
- src/agents/chat/chat_agent.py +435 -0
- src/agents/chat/prompts/en/chat_agent.yaml +35 -0
- src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
- src/agents/chat/session_manager.py +311 -0
- src/agents/co_writer/__init__.py +0 -0
- src/agents/co_writer/edit_agent.py +260 -0
- src/agents/co_writer/narrator_agent.py +423 -0
- src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
- src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
- src/agents/guide/__init__.py +16 -0
- src/agents/guide/agents/__init__.py +11 -0
- src/agents/guide/agents/chat_agent.py +104 -0
- src/agents/guide/agents/interactive_agent.py +223 -0
- src/agents/guide/agents/locate_agent.py +149 -0
- src/agents/guide/agents/summary_agent.py +150 -0
- src/agents/guide/guide_manager.py +500 -0
- src/agents/guide/prompts/en/chat_agent.yaml +41 -0
- src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
- src/agents/guide/prompts/en/locate_agent.yaml +68 -0
- src/agents/guide/prompts/en/summary_agent.yaml +157 -0
- src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
- src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
- src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
- src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
- src/agents/ideagen/__init__.py +12 -0
- src/agents/ideagen/idea_generation_workflow.py +426 -0
- src/agents/ideagen/material_organizer_agent.py +173 -0
- src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
- src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
- src/agents/question/__init__.py +24 -0
- src/agents/question/agents/__init__.py +18 -0
- src/agents/question/agents/generate_agent.py +381 -0
- src/agents/question/agents/relevance_analyzer.py +207 -0
- src/agents/question/agents/retrieve_agent.py +239 -0
- src/agents/question/coordinator.py +718 -0
- src/agents/question/example.py +109 -0
- src/agents/question/prompts/en/coordinator.yaml +75 -0
- src/agents/question/prompts/en/generate_agent.yaml +77 -0
- src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
- src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
- src/agents/question/prompts/zh/coordinator.yaml +75 -0
- src/agents/question/prompts/zh/generate_agent.yaml +77 -0
- src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
- src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
- src/agents/research/agents/__init__.py +23 -0
- src/agents/research/agents/decompose_agent.py +507 -0
- src/agents/research/agents/manager_agent.py +228 -0
- src/agents/research/agents/note_agent.py +180 -0
- src/agents/research/agents/rephrase_agent.py +263 -0
- src/agents/research/agents/reporting_agent.py +1333 -0
- src/agents/research/agents/research_agent.py +714 -0
- src/agents/research/data_structures.py +451 -0
- src/agents/research/main.py +188 -0
- src/agents/research/prompts/en/decompose_agent.yaml +89 -0
- src/agents/research/prompts/en/manager_agent.yaml +24 -0
- src/agents/research/prompts/en/note_agent.yaml +121 -0
- src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/en/reporting_agent.yaml +380 -0
- src/agents/research/prompts/en/research_agent.yaml +173 -0
- src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
- src/agents/research/prompts/zh/manager_agent.yaml +24 -0
- src/agents/research/prompts/zh/note_agent.yaml +121 -0
- src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
- src/agents/research/prompts/zh/research_agent.yaml +173 -0
- src/agents/research/research_pipeline.py +1309 -0
- src/agents/research/utils/__init__.py +60 -0
- src/agents/research/utils/citation_manager.py +799 -0
- src/agents/research/utils/json_utils.py +98 -0
- src/agents/research/utils/token_tracker.py +297 -0
- src/agents/solve/__init__.py +80 -0
- src/agents/solve/analysis_loop/__init__.py +14 -0
- src/agents/solve/analysis_loop/investigate_agent.py +414 -0
- src/agents/solve/analysis_loop/note_agent.py +190 -0
- src/agents/solve/main_solver.py +862 -0
- src/agents/solve/memory/__init__.py +34 -0
- src/agents/solve/memory/citation_memory.py +353 -0
- src/agents/solve/memory/investigate_memory.py +226 -0
- src/agents/solve/memory/solve_memory.py +340 -0
- src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
- src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
- src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
- src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
- src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
- src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
- src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
- src/agents/solve/solve_loop/__init__.py +22 -0
- src/agents/solve/solve_loop/citation_manager.py +74 -0
- src/agents/solve/solve_loop/manager_agent.py +274 -0
- src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
- src/agents/solve/solve_loop/response_agent.py +301 -0
- src/agents/solve/solve_loop/solve_agent.py +325 -0
- src/agents/solve/solve_loop/tool_agent.py +470 -0
- src/agents/solve/utils/__init__.py +64 -0
- src/agents/solve/utils/config_validator.py +313 -0
- src/agents/solve/utils/display_manager.py +223 -0
- src/agents/solve/utils/error_handler.py +363 -0
- src/agents/solve/utils/json_utils.py +98 -0
- src/agents/solve/utils/performance_monitor.py +407 -0
- src/agents/solve/utils/token_tracker.py +541 -0
- src/api/__init__.py +0 -0
- src/api/main.py +240 -0
- src/api/routers/__init__.py +1 -0
- src/api/routers/agent_config.py +69 -0
- src/api/routers/chat.py +296 -0
- src/api/routers/co_writer.py +337 -0
- src/api/routers/config.py +627 -0
- src/api/routers/dashboard.py +18 -0
- src/api/routers/guide.py +337 -0
- src/api/routers/ideagen.py +436 -0
- src/api/routers/knowledge.py +821 -0
- src/api/routers/notebook.py +247 -0
- src/api/routers/question.py +537 -0
- src/api/routers/research.py +394 -0
- src/api/routers/settings.py +164 -0
- src/api/routers/solve.py +305 -0
- src/api/routers/system.py +252 -0
- src/api/run_server.py +61 -0
- src/api/utils/history.py +172 -0
- src/api/utils/log_interceptor.py +21 -0
- src/api/utils/notebook_manager.py +415 -0
- src/api/utils/progress_broadcaster.py +72 -0
- src/api/utils/task_id_manager.py +100 -0
- src/config/__init__.py +0 -0
- src/config/accessors.py +18 -0
- src/config/constants.py +34 -0
- src/config/defaults.py +18 -0
- src/config/schema.py +38 -0
- src/config/settings.py +50 -0
- src/core/errors.py +62 -0
- src/knowledge/__init__.py +23 -0
- src/knowledge/add_documents.py +606 -0
- src/knowledge/config.py +65 -0
- src/knowledge/example_add_documents.py +236 -0
- src/knowledge/extract_numbered_items.py +1039 -0
- src/knowledge/initializer.py +621 -0
- src/knowledge/kb.py +22 -0
- src/knowledge/manager.py +782 -0
- src/knowledge/progress_tracker.py +182 -0
- src/knowledge/start_kb.py +535 -0
- src/logging/__init__.py +103 -0
- src/logging/adapters/__init__.py +17 -0
- src/logging/adapters/lightrag.py +184 -0
- src/logging/adapters/llamaindex.py +141 -0
- src/logging/config.py +80 -0
- src/logging/handlers/__init__.py +20 -0
- src/logging/handlers/console.py +75 -0
- src/logging/handlers/file.py +201 -0
- src/logging/handlers/websocket.py +127 -0
- src/logging/logger.py +709 -0
- src/logging/stats/__init__.py +16 -0
- src/logging/stats/llm_stats.py +179 -0
- src/services/__init__.py +56 -0
- src/services/config/__init__.py +61 -0
- src/services/config/knowledge_base_config.py +210 -0
- src/services/config/loader.py +260 -0
- src/services/config/unified_config.py +603 -0
- src/services/embedding/__init__.py +45 -0
- src/services/embedding/adapters/__init__.py +22 -0
- src/services/embedding/adapters/base.py +106 -0
- src/services/embedding/adapters/cohere.py +127 -0
- src/services/embedding/adapters/jina.py +99 -0
- src/services/embedding/adapters/ollama.py +116 -0
- src/services/embedding/adapters/openai_compatible.py +96 -0
- src/services/embedding/client.py +159 -0
- src/services/embedding/config.py +156 -0
- src/services/embedding/provider.py +119 -0
- src/services/llm/__init__.py +152 -0
- src/services/llm/capabilities.py +313 -0
- src/services/llm/client.py +302 -0
- src/services/llm/cloud_provider.py +530 -0
- src/services/llm/config.py +200 -0
- src/services/llm/error_mapping.py +103 -0
- src/services/llm/exceptions.py +152 -0
- src/services/llm/factory.py +450 -0
- src/services/llm/local_provider.py +347 -0
- src/services/llm/providers/anthropic.py +95 -0
- src/services/llm/providers/base_provider.py +93 -0
- src/services/llm/providers/open_ai.py +83 -0
- src/services/llm/registry.py +71 -0
- src/services/llm/telemetry.py +40 -0
- src/services/llm/types.py +27 -0
- src/services/llm/utils.py +333 -0
- src/services/prompt/__init__.py +25 -0
- src/services/prompt/manager.py +206 -0
- src/services/rag/__init__.py +64 -0
- src/services/rag/components/__init__.py +29 -0
- src/services/rag/components/base.py +59 -0
- src/services/rag/components/chunkers/__init__.py +18 -0
- src/services/rag/components/chunkers/base.py +34 -0
- src/services/rag/components/chunkers/fixed.py +71 -0
- src/services/rag/components/chunkers/numbered_item.py +94 -0
- src/services/rag/components/chunkers/semantic.py +97 -0
- src/services/rag/components/embedders/__init__.py +14 -0
- src/services/rag/components/embedders/base.py +32 -0
- src/services/rag/components/embedders/openai.py +63 -0
- src/services/rag/components/indexers/__init__.py +18 -0
- src/services/rag/components/indexers/base.py +35 -0
- src/services/rag/components/indexers/graph.py +172 -0
- src/services/rag/components/indexers/lightrag.py +156 -0
- src/services/rag/components/indexers/vector.py +146 -0
- src/services/rag/components/parsers/__init__.py +18 -0
- src/services/rag/components/parsers/base.py +35 -0
- src/services/rag/components/parsers/markdown.py +52 -0
- src/services/rag/components/parsers/pdf.py +115 -0
- src/services/rag/components/parsers/text.py +86 -0
- src/services/rag/components/retrievers/__init__.py +18 -0
- src/services/rag/components/retrievers/base.py +34 -0
- src/services/rag/components/retrievers/dense.py +200 -0
- src/services/rag/components/retrievers/hybrid.py +164 -0
- src/services/rag/components/retrievers/lightrag.py +169 -0
- src/services/rag/components/routing.py +286 -0
- src/services/rag/factory.py +234 -0
- src/services/rag/pipeline.py +215 -0
- src/services/rag/pipelines/__init__.py +32 -0
- src/services/rag/pipelines/academic.py +44 -0
- src/services/rag/pipelines/lightrag.py +43 -0
- src/services/rag/pipelines/llamaindex.py +313 -0
- src/services/rag/pipelines/raganything.py +384 -0
- src/services/rag/service.py +244 -0
- src/services/rag/types.py +73 -0
- src/services/search/__init__.py +284 -0
- src/services/search/base.py +87 -0
- src/services/search/consolidation.py +398 -0
- src/services/search/providers/__init__.py +128 -0
- src/services/search/providers/baidu.py +188 -0
- src/services/search/providers/exa.py +194 -0
- src/services/search/providers/jina.py +161 -0
- src/services/search/providers/perplexity.py +153 -0
- src/services/search/providers/serper.py +209 -0
- src/services/search/providers/tavily.py +161 -0
- src/services/search/types.py +114 -0
- src/services/setup/__init__.py +34 -0
- src/services/setup/init.py +285 -0
- src/services/tts/__init__.py +16 -0
- src/services/tts/config.py +99 -0
- src/tools/__init__.py +91 -0
- src/tools/code_executor.py +536 -0
- src/tools/paper_search_tool.py +171 -0
- src/tools/query_item_tool.py +310 -0
- src/tools/question/__init__.py +15 -0
- src/tools/question/exam_mimic.py +616 -0
- src/tools/question/pdf_parser.py +211 -0
- src/tools/question/question_extractor.py +397 -0
- src/tools/rag_tool.py +173 -0
- src/tools/tex_chunker.py +339 -0
- src/tools/tex_downloader.py +253 -0
- src/tools/web_search.py +71 -0
- src/utils/config_manager.py +206 -0
- src/utils/document_validator.py +168 -0
- src/utils/error_rate_tracker.py +111 -0
- src/utils/error_utils.py +82 -0
- src/utils/json_parser.py +110 -0
- src/utils/network/circuit_breaker.py +79 -0
|
@@ -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
|