deeptutor-server 1.7.0__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.
- deeptutor/api/__init__.py +0 -0
- deeptutor/api/contracts/__init__.py +21 -0
- deeptutor/api/contracts/export.py +110 -0
- deeptutor/api/contracts/turn_protocol.py +313 -0
- deeptutor/api/main.py +547 -0
- deeptutor/api/module_routes.py +42 -0
- deeptutor/api/router_index.json +5752 -0
- deeptutor/api/router_loader.py +139 -0
- deeptutor/api/routers/__init__.py +1 -0
- deeptutor/api/routers/agent_config.py +59 -0
- deeptutor/api/routers/attachments.py +79 -0
- deeptutor/api/routers/auth.py +1937 -0
- deeptutor/api/routers/capabilities.py +44 -0
- deeptutor/api/routers/capabilities_settings.py +38 -0
- deeptutor/api/routers/dashboard.py +292 -0
- deeptutor/api/routers/file_library.py +169 -0
- deeptutor/api/routers/file_preview.py +205 -0
- deeptutor/api/routers/imports.py +146 -0
- deeptutor/api/routers/learning_journal.py +18 -0
- deeptutor/api/routers/mcp_settings.py +133 -0
- deeptutor/api/routers/modules.py +12 -0
- deeptutor/api/routers/multi_user.py +943 -0
- deeptutor/api/routers/outputs.py +97 -0
- deeptutor/api/routers/personas.py +154 -0
- deeptutor/api/routers/plugins.py +416 -0
- deeptutor/api/routers/quiz_judge.py +464 -0
- deeptutor/api/routers/sessions.py +801 -0
- deeptutor/api/routers/settings.py +2503 -0
- deeptutor/api/routers/skills.py +315 -0
- deeptutor/api/routers/space_cli_apps.py +222 -0
- deeptutor/api/routers/space_mcp.py +460 -0
- deeptutor/api/routers/subagents.py +306 -0
- deeptutor/api/routers/system.py +633 -0
- deeptutor/api/routers/task_board.py +139 -0
- deeptutor/api/routers/tools.py +268 -0
- deeptutor/api/routers/unified_ws.py +385 -0
- deeptutor/api/routers/workspace.py +371 -0
- deeptutor/api/run_server.py +99 -0
- deeptutor/api/security_guard.py +91 -0
- deeptutor/api/utils/http_headers.py +27 -0
- deeptutor/api/utils/progress_broadcaster.py +73 -0
- deeptutor/api/utils/task_id_manager.py +120 -0
- deeptutor/api/utils/task_log_stream.py +362 -0
- deeptutor/api/utils/tool_options.py +133 -0
- deeptutor_server-1.7.0.dist-info/METADATA +14 -0
- deeptutor_server-1.7.0.dist-info/RECORD +48 -0
- deeptutor_server-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_server-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Defer unrelated API schemas in the experimental native-engine profile.
|
|
2
|
+
|
|
3
|
+
The generated index contains only routing patterns. Authentication, dependency
|
|
4
|
+
injection and handlers always come from the original FastAPI router. A stale
|
|
5
|
+
index disables deferral; OpenAPI expands every router before schema generation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from copy import copy
|
|
11
|
+
import hashlib
|
|
12
|
+
from importlib import import_module
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, FastAPI
|
|
18
|
+
from starlette.routing import BaseRoute, Match, Route, WebSocketRoute
|
|
19
|
+
|
|
20
|
+
INDEX_PATH = Path(__file__).with_name("router_index.json")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def source_fingerprint() -> str:
|
|
24
|
+
digest = hashlib.sha256()
|
|
25
|
+
root = Path(__file__).with_name("routers")
|
|
26
|
+
for path in sorted(root.rglob("*.py")):
|
|
27
|
+
digest.update(str(path.relative_to(root)).encode())
|
|
28
|
+
digest.update(path.read_bytes())
|
|
29
|
+
return digest.hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _unreachable(*_args: Any) -> None:
|
|
33
|
+
raise RuntimeError("Deferred routing pattern cannot execute a request")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DeferredRouter(BaseRoute):
|
|
37
|
+
def __init__(
|
|
38
|
+
self, app: FastAPI, reference: str, options: dict[str, Any], patterns: list[dict]
|
|
39
|
+
) -> None:
|
|
40
|
+
self.app = app
|
|
41
|
+
self.reference = reference
|
|
42
|
+
self.options = options
|
|
43
|
+
self.loaded: APIRouter | None = None
|
|
44
|
+
prefix = options.get("prefix", "")
|
|
45
|
+
self.patterns: list[BaseRoute] = []
|
|
46
|
+
for pattern in patterns:
|
|
47
|
+
path = prefix + pattern["path"]
|
|
48
|
+
if pattern["kind"] == "websocket":
|
|
49
|
+
route = WebSocketRoute(path, _unreachable, name=pattern["name"])
|
|
50
|
+
else:
|
|
51
|
+
route = Route(path, _unreachable, methods=pattern["methods"], name=pattern["name"])
|
|
52
|
+
# Starlette implicitly adds HEAD; FastAPI APIRoute does not.
|
|
53
|
+
route.methods = set(pattern["methods"])
|
|
54
|
+
self.patterns.append(route)
|
|
55
|
+
|
|
56
|
+
def load(self) -> APIRouter:
|
|
57
|
+
if self.loaded is None:
|
|
58
|
+
module, attribute = self.reference.split(":", 1)
|
|
59
|
+
router = APIRouter(redirect_slashes=False, dependency_overrides_provider=self.app)
|
|
60
|
+
router.include_router(getattr(import_module(module), attribute), **self.options)
|
|
61
|
+
# Publish only a fully constructed router. There is no await during
|
|
62
|
+
# initialization, so concurrent requests cannot observe half a load.
|
|
63
|
+
self.loaded = router
|
|
64
|
+
return self.loaded
|
|
65
|
+
|
|
66
|
+
def matches(self, scope: dict) -> tuple[Match, dict]:
|
|
67
|
+
partial: tuple[Match, dict] = (Match.NONE, {})
|
|
68
|
+
for route in self.patterns:
|
|
69
|
+
match, child = route.matches(scope)
|
|
70
|
+
if match == Match.FULL:
|
|
71
|
+
return match, child
|
|
72
|
+
if match == Match.PARTIAL and partial[0] == Match.NONE:
|
|
73
|
+
partial = (match, child)
|
|
74
|
+
return partial
|
|
75
|
+
|
|
76
|
+
async def handle(self, scope: dict, receive: Any, send: Any) -> None:
|
|
77
|
+
await self.load()(scope, receive, send)
|
|
78
|
+
|
|
79
|
+
def url_path_for(self, name: str, /, **path_params: Any) -> Any:
|
|
80
|
+
from starlette.routing import NoMatchFound
|
|
81
|
+
|
|
82
|
+
for route in self.patterns:
|
|
83
|
+
try:
|
|
84
|
+
return route.url_path_for(name, **path_params)
|
|
85
|
+
except NoMatchFound:
|
|
86
|
+
pass
|
|
87
|
+
raise NoMatchFound(name, path_params)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RouterLoader:
|
|
91
|
+
def __init__(self, app: FastAPI, *, lazy: bool = False) -> None:
|
|
92
|
+
self.app = app
|
|
93
|
+
self.records: dict[str, APIRouter] = {}
|
|
94
|
+
self.index: dict[str, list[dict]] = {}
|
|
95
|
+
if lazy:
|
|
96
|
+
try:
|
|
97
|
+
index = json.loads(INDEX_PATH.read_text())
|
|
98
|
+
if index["fingerprint"] == source_fingerprint():
|
|
99
|
+
self.index = index["routers"]
|
|
100
|
+
except (OSError, ValueError, KeyError):
|
|
101
|
+
pass
|
|
102
|
+
original_openapi = app.openapi
|
|
103
|
+
|
|
104
|
+
def openapi() -> dict:
|
|
105
|
+
self.expand()
|
|
106
|
+
return original_openapi()
|
|
107
|
+
|
|
108
|
+
app.openapi = openapi
|
|
109
|
+
|
|
110
|
+
def include(
|
|
111
|
+
self, reference: str, *, exclude_paths: tuple[str, ...] = (), **options: Any
|
|
112
|
+
) -> None:
|
|
113
|
+
if reference in self.index and not exclude_paths:
|
|
114
|
+
self.app.router.routes.append(
|
|
115
|
+
DeferredRouter(self.app, reference, options, self.index[reference])
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
module, attribute = reference.split(":", 1)
|
|
119
|
+
router = getattr(import_module(module), attribute)
|
|
120
|
+
if exclude_paths:
|
|
121
|
+
router = copy(router)
|
|
122
|
+
prefix = options.get("prefix", "")
|
|
123
|
+
router.routes = [
|
|
124
|
+
route
|
|
125
|
+
for route in router.routes
|
|
126
|
+
if not any(
|
|
127
|
+
prefix + getattr(route, "path", "") == path
|
|
128
|
+
or (prefix + getattr(route, "path", "")).startswith(path + "/")
|
|
129
|
+
for path in exclude_paths
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
self.records[reference] = router
|
|
133
|
+
self.app.include_router(router, **options)
|
|
134
|
+
|
|
135
|
+
def expand(self) -> None:
|
|
136
|
+
routes: list[BaseRoute] = []
|
|
137
|
+
for route in self.app.router.routes:
|
|
138
|
+
routes.extend(route.load().routes if isinstance(route, DeferredRouter) else [route])
|
|
139
|
+
self.app.router.routes[:] = routes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init file for routers
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Agent Configuration API - Provides agent metadata for data-driven UI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
# Agent registry - single source of truth for agent UI metadata
|
|
11
|
+
AGENT_REGISTRY = {
|
|
12
|
+
"solve": {
|
|
13
|
+
"icon": "HelpCircle",
|
|
14
|
+
"color": "blue",
|
|
15
|
+
"label_key": "Problem Solved",
|
|
16
|
+
},
|
|
17
|
+
"question": {
|
|
18
|
+
"icon": "FileText",
|
|
19
|
+
"color": "purple",
|
|
20
|
+
"label_key": "Question Generated",
|
|
21
|
+
},
|
|
22
|
+
"research": {
|
|
23
|
+
"icon": "Search",
|
|
24
|
+
"color": "emerald",
|
|
25
|
+
"label_key": "Research Report",
|
|
26
|
+
},
|
|
27
|
+
"co_writer": {
|
|
28
|
+
"icon": "PenTool",
|
|
29
|
+
"color": "amber",
|
|
30
|
+
"label_key": "Co-Writer",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/agents")
|
|
36
|
+
async def get_agent_config():
|
|
37
|
+
"""
|
|
38
|
+
Get agent UI configuration.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dict mapping agent type to UI metadata (icon, color, label_key)
|
|
42
|
+
"""
|
|
43
|
+
return AGENT_REGISTRY
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("/agents/{agent_type}")
|
|
47
|
+
async def get_single_agent_config(agent_type: str):
|
|
48
|
+
"""
|
|
49
|
+
Get UI configuration for a specific agent.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
agent_type: Agent type (solve, question, research, etc.)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Agent UI metadata or 404 if not found
|
|
56
|
+
"""
|
|
57
|
+
if agent_type in AGENT_REGISTRY:
|
|
58
|
+
return AGENT_REGISTRY[agent_type]
|
|
59
|
+
return {"error": f"Agent type '{agent_type}' not found"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""HTTP endpoint for chat attachment downloads / previews.
|
|
2
|
+
|
|
3
|
+
The chat turn runtime persists every uploaded attachment to the
|
|
4
|
+
:class:`~deeptutor.services.storage.AttachmentStore` and records the public
|
|
5
|
+
URL on the message. The frontend preview drawer loads files via this
|
|
6
|
+
router, which only serves paths the store hands back — every component is
|
|
7
|
+
sanitised to defend against directory traversal.
|
|
8
|
+
|
|
9
|
+
URL shape::
|
|
10
|
+
|
|
11
|
+
GET /files/attachments/{session_id}/{attachment_id}/{filename}
|
|
12
|
+
|
|
13
|
+
The session id functions as the ACL boundary, mirroring how the rest of
|
|
14
|
+
the app treats sessions today (single-tenant, session ownership is local
|
|
15
|
+
trust). Once multi-user auth lands we should swap this for signed URLs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import mimetypes
|
|
22
|
+
|
|
23
|
+
from fastapi import APIRouter, HTTPException
|
|
24
|
+
from fastapi.responses import FileResponse
|
|
25
|
+
|
|
26
|
+
from deeptutor.api.utils.http_headers import content_disposition
|
|
27
|
+
from deeptutor.services.storage import (
|
|
28
|
+
LocalDiskAttachmentStore,
|
|
29
|
+
get_attachment_store,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
router = APIRouter()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_content_disposition = content_disposition
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("/{session_id}/{attachment_id}/{filename:path}")
|
|
41
|
+
async def get_attachment(
|
|
42
|
+
session_id: str,
|
|
43
|
+
attachment_id: str,
|
|
44
|
+
filename: str,
|
|
45
|
+
):
|
|
46
|
+
"""Serve a previously uploaded chat attachment.
|
|
47
|
+
|
|
48
|
+
Responds with ``Content-Disposition: inline`` so browsers preview PDFs
|
|
49
|
+
and images directly in an ``<iframe>`` / ``<img>``. For unknown types
|
|
50
|
+
the browser still falls back to download, which is fine for the
|
|
51
|
+
drawer's "Download" button path.
|
|
52
|
+
"""
|
|
53
|
+
store = get_attachment_store()
|
|
54
|
+
if not isinstance(store, LocalDiskAttachmentStore):
|
|
55
|
+
# Future remote backends should issue a redirect to the signed URL
|
|
56
|
+
# here. Local-disk is the only backend today, so this branch just
|
|
57
|
+
# guards against an unexpected configuration.
|
|
58
|
+
raise HTTPException(status_code=501, detail="Attachment backend not servable")
|
|
59
|
+
|
|
60
|
+
target = store.resolve_path(
|
|
61
|
+
session_id=session_id,
|
|
62
|
+
attachment_id=attachment_id,
|
|
63
|
+
filename=filename,
|
|
64
|
+
)
|
|
65
|
+
if target is None:
|
|
66
|
+
raise HTTPException(status_code=404, detail="Attachment not found")
|
|
67
|
+
|
|
68
|
+
media_type, _ = mimetypes.guess_type(target.name)
|
|
69
|
+
if not media_type:
|
|
70
|
+
media_type = "application/octet-stream"
|
|
71
|
+
|
|
72
|
+
# ``inline`` lets the browser preview the file when possible while still
|
|
73
|
+
# honouring the suggested filename for the drawer's download action.
|
|
74
|
+
headers = {
|
|
75
|
+
"Content-Disposition": _content_disposition(target.name),
|
|
76
|
+
# User-uploaded data; do not let intermediaries cache it.
|
|
77
|
+
"Cache-Control": "private, max-age=0, must-revalidate",
|
|
78
|
+
}
|
|
79
|
+
return FileResponse(path=str(target), media_type=media_type, headers=headers)
|