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.
Files changed (48) hide show
  1. deeptutor/api/__init__.py +0 -0
  2. deeptutor/api/contracts/__init__.py +21 -0
  3. deeptutor/api/contracts/export.py +110 -0
  4. deeptutor/api/contracts/turn_protocol.py +313 -0
  5. deeptutor/api/main.py +547 -0
  6. deeptutor/api/module_routes.py +42 -0
  7. deeptutor/api/router_index.json +5752 -0
  8. deeptutor/api/router_loader.py +139 -0
  9. deeptutor/api/routers/__init__.py +1 -0
  10. deeptutor/api/routers/agent_config.py +59 -0
  11. deeptutor/api/routers/attachments.py +79 -0
  12. deeptutor/api/routers/auth.py +1937 -0
  13. deeptutor/api/routers/capabilities.py +44 -0
  14. deeptutor/api/routers/capabilities_settings.py +38 -0
  15. deeptutor/api/routers/dashboard.py +292 -0
  16. deeptutor/api/routers/file_library.py +169 -0
  17. deeptutor/api/routers/file_preview.py +205 -0
  18. deeptutor/api/routers/imports.py +146 -0
  19. deeptutor/api/routers/learning_journal.py +18 -0
  20. deeptutor/api/routers/mcp_settings.py +133 -0
  21. deeptutor/api/routers/modules.py +12 -0
  22. deeptutor/api/routers/multi_user.py +943 -0
  23. deeptutor/api/routers/outputs.py +97 -0
  24. deeptutor/api/routers/personas.py +154 -0
  25. deeptutor/api/routers/plugins.py +416 -0
  26. deeptutor/api/routers/quiz_judge.py +464 -0
  27. deeptutor/api/routers/sessions.py +801 -0
  28. deeptutor/api/routers/settings.py +2503 -0
  29. deeptutor/api/routers/skills.py +315 -0
  30. deeptutor/api/routers/space_cli_apps.py +222 -0
  31. deeptutor/api/routers/space_mcp.py +460 -0
  32. deeptutor/api/routers/subagents.py +306 -0
  33. deeptutor/api/routers/system.py +633 -0
  34. deeptutor/api/routers/task_board.py +139 -0
  35. deeptutor/api/routers/tools.py +268 -0
  36. deeptutor/api/routers/unified_ws.py +385 -0
  37. deeptutor/api/routers/workspace.py +371 -0
  38. deeptutor/api/run_server.py +99 -0
  39. deeptutor/api/security_guard.py +91 -0
  40. deeptutor/api/utils/http_headers.py +27 -0
  41. deeptutor/api/utils/progress_broadcaster.py +73 -0
  42. deeptutor/api/utils/task_id_manager.py +120 -0
  43. deeptutor/api/utils/task_log_stream.py +362 -0
  44. deeptutor/api/utils/tool_options.py +133 -0
  45. deeptutor_server-1.7.0.dist-info/METADATA +14 -0
  46. deeptutor_server-1.7.0.dist-info/RECORD +48 -0
  47. deeptutor_server-1.7.0.dist-info/WHEEL +5 -0
  48. 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)