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
|
+
"""Each workspace's task board, kept in one account store.
|
|
2
|
+
|
|
3
|
+
The router sits outside request scope installation (see ``auth.py``) so its
|
|
4
|
+
event stream never holds a workspace activity lease; it resolves the board's
|
|
5
|
+
workspace from the same header/query itself.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
13
|
+
from fastapi.responses import StreamingResponse
|
|
14
|
+
|
|
15
|
+
from deeptutor.services.task_board import (
|
|
16
|
+
CreateCard,
|
|
17
|
+
LinkStatus,
|
|
18
|
+
LinkTasks,
|
|
19
|
+
StatusColors,
|
|
20
|
+
TaskBoard,
|
|
21
|
+
UpdateCard,
|
|
22
|
+
get_task_board_store,
|
|
23
|
+
workspace_board,
|
|
24
|
+
)
|
|
25
|
+
from deeptutor.services.workspace.context import workspace_context
|
|
26
|
+
from deeptutor.services.workspace.models import WorkspaceError
|
|
27
|
+
|
|
28
|
+
router = APIRouter()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def board_workspace(request: Request) -> str:
|
|
32
|
+
raw = request.headers.get("x-deeptutor-workspace") or request.query_params.get("dt_workspace")
|
|
33
|
+
try:
|
|
34
|
+
with workspace_context(raw or "") as scope:
|
|
35
|
+
return scope.workspace_id
|
|
36
|
+
except WorkspaceError as exc:
|
|
37
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("", response_model=TaskBoard)
|
|
41
|
+
def get_board(request: Request) -> TaskBoard:
|
|
42
|
+
return workspace_board(get_task_board_store().read(), board_workspace(request))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/events")
|
|
46
|
+
async def board_events(request: Request):
|
|
47
|
+
# Resolve once before streaming; each connection retains its account identity.
|
|
48
|
+
workspace_id = board_workspace(request)
|
|
49
|
+
store = await asyncio.to_thread(get_task_board_store)
|
|
50
|
+
|
|
51
|
+
async def events():
|
|
52
|
+
revision = -1
|
|
53
|
+
heartbeat = 0
|
|
54
|
+
while not await request.is_disconnected():
|
|
55
|
+
current_revision = await asyncio.to_thread(store.revision)
|
|
56
|
+
if current_revision != revision:
|
|
57
|
+
board = workspace_board(await asyncio.to_thread(store.read), workspace_id)
|
|
58
|
+
revision = board.revision
|
|
59
|
+
yield f"id: {revision}\ndata: {board.model_dump_json()}\n\n"
|
|
60
|
+
elif heartbeat % 30 == 0:
|
|
61
|
+
yield ": heartbeat\n\n"
|
|
62
|
+
heartbeat += 1
|
|
63
|
+
await asyncio.sleep(0.5)
|
|
64
|
+
|
|
65
|
+
return StreamingResponse(
|
|
66
|
+
events(),
|
|
67
|
+
media_type="text/event-stream",
|
|
68
|
+
headers={
|
|
69
|
+
# Next's proxy must flush each event instead of gzip-buffering it.
|
|
70
|
+
"Cache-Control": "no-cache, no-transform",
|
|
71
|
+
"Content-Encoding": "identity",
|
|
72
|
+
"X-Accel-Buffering": "no",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.post("/cards", response_model=TaskBoard, status_code=201)
|
|
78
|
+
def create_card(payload: CreateCard, request: Request) -> TaskBoard:
|
|
79
|
+
workspace_id = board_workspace(request)
|
|
80
|
+
return workspace_board(get_task_board_store().create(payload, workspace_id), workspace_id)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.patch("/cards/{card_id}", response_model=TaskBoard)
|
|
84
|
+
def update_card(card_id: str, payload: UpdateCard, request: Request) -> TaskBoard:
|
|
85
|
+
workspace_id = board_workspace(request)
|
|
86
|
+
try:
|
|
87
|
+
if "workspace_id" in payload.model_fields_set and payload.workspace_id is not None:
|
|
88
|
+
with workspace_context(payload.workspace_id) as scope:
|
|
89
|
+
payload.workspace_id = scope.workspace_id
|
|
90
|
+
if scope.archived:
|
|
91
|
+
raise WorkspaceError("Restore this workspace before assigning tasks.")
|
|
92
|
+
return workspace_board(
|
|
93
|
+
get_task_board_store().update(card_id, payload, workspace_id=workspace_id), workspace_id
|
|
94
|
+
)
|
|
95
|
+
except WorkspaceError as exc:
|
|
96
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
97
|
+
except KeyError as exc:
|
|
98
|
+
raise HTTPException(status_code=404, detail="Task card not found.") from exc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.put("/colors", response_model=TaskBoard)
|
|
102
|
+
def update_colors(payload: StatusColors, request: Request) -> TaskBoard:
|
|
103
|
+
workspace_id = board_workspace(request)
|
|
104
|
+
return workspace_board(get_task_board_store().set_colors(payload), workspace_id)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def require_session(session_id: str, workspace_id: str, board_workspace_id: str) -> str:
|
|
108
|
+
from deeptutor.services.session import get_session_store
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
with workspace_context(workspace_id) as scope:
|
|
112
|
+
if scope.workspace_id != board_workspace_id:
|
|
113
|
+
raise HTTPException(status_code=404, detail="Conversation not found.")
|
|
114
|
+
if scope.archived:
|
|
115
|
+
raise WorkspaceError("Restore this workspace before changing its conversations.")
|
|
116
|
+
if await get_session_store().get_session(session_id) is None:
|
|
117
|
+
raise HTTPException(status_code=404, detail="Conversation not found.")
|
|
118
|
+
return scope.workspace_id
|
|
119
|
+
except WorkspaceError as exc:
|
|
120
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@router.put("/sessions/{session_id}", response_model=TaskBoard)
|
|
124
|
+
async def link_tasks(session_id: str, payload: LinkTasks, request: Request) -> TaskBoard:
|
|
125
|
+
workspace_id = board_workspace(request)
|
|
126
|
+
payload.workspace_id = await require_session(session_id, payload.workspace_id, workspace_id)
|
|
127
|
+
try:
|
|
128
|
+
board = await asyncio.to_thread(get_task_board_store().link_tasks, session_id, payload)
|
|
129
|
+
return workspace_board(board, workspace_id)
|
|
130
|
+
except KeyError as exc:
|
|
131
|
+
raise HTTPException(status_code=404, detail="Task not found or archived.") from exc
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.patch("/sessions/{session_id}/status", response_model=TaskBoard)
|
|
135
|
+
async def link_status(session_id: str, payload: LinkStatus, request: Request) -> TaskBoard:
|
|
136
|
+
workspace_id = board_workspace(request)
|
|
137
|
+
payload.workspace_id = await require_session(session_id, payload.workspace_id, workspace_id)
|
|
138
|
+
board = await asyncio.to_thread(get_task_board_store().link_status, session_id, payload)
|
|
139
|
+
return workspace_board(board, workspace_id)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools API Router
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
Read-only listing of the chat agent's built-in tools, used by the Settings UI
|
|
6
|
+
to render the "Tools" sub-page. Returns each tool's definition (name,
|
|
7
|
+
description, parameters) alongside its bilingual prompt hints, so the frontend
|
|
8
|
+
can show authoritative copy without duplicating the catalog.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolPromptHints
|
|
20
|
+
from deeptutor.i18n.metadata_i18n import tool_description_i18n
|
|
21
|
+
from deeptutor.services.config import resolve_search_runtime_config
|
|
22
|
+
from deeptutor.services.settings.interface_settings import get_enabled_optional_tools
|
|
23
|
+
from deeptutor.tools.builtin import (
|
|
24
|
+
BUILTIN_TOOL_TYPES,
|
|
25
|
+
COMING_SOON_TOOL_TYPES,
|
|
26
|
+
TOOL_ALIASES,
|
|
27
|
+
USER_TOGGLEABLE_TOOL_NAMES,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
router = APIRouter()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ToolParameterPayload(BaseModel):
|
|
36
|
+
name: str
|
|
37
|
+
type: str
|
|
38
|
+
description: str = ""
|
|
39
|
+
required: bool = True
|
|
40
|
+
default: Any = None
|
|
41
|
+
enum: list[str] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolAliasPayload(BaseModel):
|
|
45
|
+
name: str
|
|
46
|
+
description: str = ""
|
|
47
|
+
input_format: str = ""
|
|
48
|
+
when_to_use: str = ""
|
|
49
|
+
phase: str = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolHintsPayload(BaseModel):
|
|
53
|
+
short_description: str = ""
|
|
54
|
+
when_to_use: str = ""
|
|
55
|
+
input_format: str = ""
|
|
56
|
+
guideline: str = ""
|
|
57
|
+
note: str = ""
|
|
58
|
+
phase: str = ""
|
|
59
|
+
aliases: list[ToolAliasPayload] = []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BuiltinToolPayload(BaseModel):
|
|
63
|
+
name: str
|
|
64
|
+
description: str
|
|
65
|
+
description_i18n: dict[Literal["en", "zh"], str] = {}
|
|
66
|
+
parameters: list[ToolParameterPayload]
|
|
67
|
+
hints: dict[Literal["en", "zh"], ToolHintsPayload]
|
|
68
|
+
aliases: list[str] = []
|
|
69
|
+
# True iff the user is allowed to switch this tool on/off from the
|
|
70
|
+
# /settings/tools UI. Locked-on tools (auto-mounted by the chat
|
|
71
|
+
# pipeline under context gates) report ``False`` and the UI renders
|
|
72
|
+
# them as informational entries only.
|
|
73
|
+
toggleable: bool = False
|
|
74
|
+
# Whether the tool is currently on. For toggleable tools this
|
|
75
|
+
# reflects the user's saved preference; for locked-on tools this is
|
|
76
|
+
# always ``True``.
|
|
77
|
+
enabled: bool = True
|
|
78
|
+
# ``coming_soon`` tools are NOT registered with the runtime — the chat
|
|
79
|
+
# agent cannot invoke them. They are listed here only so the settings
|
|
80
|
+
# page can render a placeholder card explaining the capability is on
|
|
81
|
+
# the roadmap. The frontend should lock the toggle and show a badge.
|
|
82
|
+
coming_soon: bool = False
|
|
83
|
+
# The capability that owns this tool (e.g. ``solve`` / ``mastery``), or
|
|
84
|
+
# ``None`` for a plain system built-in. Owned tools are reused by their
|
|
85
|
+
# capability on top of the shared built-in surface; the settings UI groups
|
|
86
|
+
# them under their owner, below the built-in section.
|
|
87
|
+
capability: str | None = None
|
|
88
|
+
# Runtime readiness is separate from the saved on/off preference. A tool
|
|
89
|
+
# may be enabled in the composer while its backing service still needs
|
|
90
|
+
# configuration (notably web_search with provider="none").
|
|
91
|
+
available: bool = True
|
|
92
|
+
unavailable_reason: str | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ToolsListResponse(BaseModel):
|
|
96
|
+
tools: list[BuiltinToolPayload]
|
|
97
|
+
enabled_optional_tools: list[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _serialise_definition(
|
|
101
|
+
definition: ToolDefinition,
|
|
102
|
+
) -> tuple[str, str, list[ToolParameterPayload]]:
|
|
103
|
+
params = [
|
|
104
|
+
ToolParameterPayload(
|
|
105
|
+
name=p.name,
|
|
106
|
+
type=p.type,
|
|
107
|
+
description=p.description,
|
|
108
|
+
required=p.required,
|
|
109
|
+
default=p.default,
|
|
110
|
+
enum=p.enum,
|
|
111
|
+
)
|
|
112
|
+
for p in definition.parameters
|
|
113
|
+
]
|
|
114
|
+
if not params and isinstance(definition.raw_parameters, dict):
|
|
115
|
+
properties = definition.raw_parameters.get("properties")
|
|
116
|
+
required = definition.raw_parameters.get("required")
|
|
117
|
+
required_names = set(required) if isinstance(required, list) else set()
|
|
118
|
+
if isinstance(properties, dict):
|
|
119
|
+
for name, raw in properties.items():
|
|
120
|
+
schema = raw if isinstance(raw, dict) else {}
|
|
121
|
+
enum = schema.get("enum")
|
|
122
|
+
params.append(
|
|
123
|
+
ToolParameterPayload(
|
|
124
|
+
name=str(name),
|
|
125
|
+
type=str(schema.get("type") or "object"),
|
|
126
|
+
description=str(schema.get("description") or ""),
|
|
127
|
+
required=str(name) in required_names,
|
|
128
|
+
default=schema.get("default"),
|
|
129
|
+
enum=([str(value) for value in enum] if isinstance(enum, list) else None),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return definition.name, definition.description, params
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _serialise_hints(hints: ToolPromptHints) -> ToolHintsPayload:
|
|
136
|
+
return ToolHintsPayload(
|
|
137
|
+
short_description=hints.short_description,
|
|
138
|
+
when_to_use=hints.when_to_use,
|
|
139
|
+
input_format=hints.input_format,
|
|
140
|
+
guideline=hints.guideline,
|
|
141
|
+
note=hints.note,
|
|
142
|
+
phase=hints.phase,
|
|
143
|
+
aliases=[
|
|
144
|
+
ToolAliasPayload(
|
|
145
|
+
name=alias.name,
|
|
146
|
+
description=alias.description,
|
|
147
|
+
input_format=alias.input_format,
|
|
148
|
+
when_to_use=alias.when_to_use,
|
|
149
|
+
phase=alias.phase,
|
|
150
|
+
)
|
|
151
|
+
for alias in hints.aliases
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _collect_aliases_for(tool_name: str) -> list[str]:
|
|
157
|
+
return sorted(alias for alias, (target, _) in TOOL_ALIASES.items() if target == tool_name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _build_tool_payload(
|
|
161
|
+
tool: BaseTool,
|
|
162
|
+
*,
|
|
163
|
+
enabled_optional: set[str],
|
|
164
|
+
coming_soon: bool = False,
|
|
165
|
+
capability: str | None = None,
|
|
166
|
+
) -> BuiltinToolPayload:
|
|
167
|
+
name, description, parameters = _serialise_definition(tool.get_definition())
|
|
168
|
+
descriptions = tool_description_i18n(name, description)
|
|
169
|
+
toggleable = (not coming_soon) and (name in USER_TOGGLEABLE_TOOL_NAMES)
|
|
170
|
+
if coming_soon:
|
|
171
|
+
enabled = False
|
|
172
|
+
elif toggleable:
|
|
173
|
+
enabled = name in enabled_optional
|
|
174
|
+
else:
|
|
175
|
+
enabled = True
|
|
176
|
+
available, unavailable_reason = _runtime_availability(name, coming_soon=coming_soon)
|
|
177
|
+
return BuiltinToolPayload(
|
|
178
|
+
name=name,
|
|
179
|
+
description=descriptions.get("en") or description,
|
|
180
|
+
description_i18n=descriptions,
|
|
181
|
+
parameters=parameters,
|
|
182
|
+
hints={
|
|
183
|
+
"en": _serialise_hints(tool.get_prompt_hints(language="en")),
|
|
184
|
+
"zh": _serialise_hints(tool.get_prompt_hints(language="zh")),
|
|
185
|
+
},
|
|
186
|
+
aliases=_collect_aliases_for(name),
|
|
187
|
+
toggleable=toggleable,
|
|
188
|
+
enabled=enabled,
|
|
189
|
+
coming_soon=coming_soon,
|
|
190
|
+
capability=capability,
|
|
191
|
+
available=available,
|
|
192
|
+
unavailable_reason=unavailable_reason,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _runtime_availability(name: str, *, coming_soon: bool = False) -> tuple[bool, str | None]:
|
|
197
|
+
if coming_soon:
|
|
198
|
+
return False, "coming_soon"
|
|
199
|
+
if name != "web_search":
|
|
200
|
+
return True, None
|
|
201
|
+
try:
|
|
202
|
+
config = resolve_search_runtime_config()
|
|
203
|
+
except Exception:
|
|
204
|
+
logger.exception("Failed to resolve web_search runtime availability")
|
|
205
|
+
return False, "search_configuration_error"
|
|
206
|
+
if config.provider == "none":
|
|
207
|
+
return False, "search_provider_not_configured"
|
|
208
|
+
if config.unsupported_provider or config.deprecated_provider:
|
|
209
|
+
return False, "search_provider_unsupported"
|
|
210
|
+
if config.missing_credentials:
|
|
211
|
+
return False, "search_credentials_missing"
|
|
212
|
+
return True, None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@router.get("", response_model=ToolsListResponse)
|
|
216
|
+
async def list_builtin_tools() -> ToolsListResponse:
|
|
217
|
+
"""Return all built-in tools the chat agent can invoke, plus any
|
|
218
|
+
coming-soon placeholders for the settings page."""
|
|
219
|
+
from deeptutor.capabilities import capability_tool_owners
|
|
220
|
+
|
|
221
|
+
enabled_optional = set(get_enabled_optional_tools())
|
|
222
|
+
owners = capability_tool_owners()
|
|
223
|
+
payloads: list[BuiltinToolPayload] = []
|
|
224
|
+
for tool_type in BUILTIN_TOOL_TYPES:
|
|
225
|
+
try:
|
|
226
|
+
instance = tool_type()
|
|
227
|
+
payloads.append(
|
|
228
|
+
_build_tool_payload(
|
|
229
|
+
instance,
|
|
230
|
+
enabled_optional=enabled_optional,
|
|
231
|
+
capability=owners.get(instance.name),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.exception("Failed to serialise tool %s", tool_type.__name__)
|
|
236
|
+
for tool_type in COMING_SOON_TOOL_TYPES:
|
|
237
|
+
try:
|
|
238
|
+
instance = tool_type()
|
|
239
|
+
payloads.append(
|
|
240
|
+
_build_tool_payload(
|
|
241
|
+
instance,
|
|
242
|
+
enabled_optional=enabled_optional,
|
|
243
|
+
coming_soon=True,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
except Exception:
|
|
247
|
+
logger.exception("Failed to serialise coming-soon tool %s", tool_type.__name__)
|
|
248
|
+
# Guard against the unlikely case of name collision (e.g. someone
|
|
249
|
+
# accidentally registers the same tool both as built-in and coming-soon).
|
|
250
|
+
seen: set[str] = set()
|
|
251
|
+
deduped: list[BuiltinToolPayload] = []
|
|
252
|
+
for payload in payloads:
|
|
253
|
+
if payload.name in seen:
|
|
254
|
+
continue
|
|
255
|
+
seen.add(payload.name)
|
|
256
|
+
deduped.append(payload)
|
|
257
|
+
# Toggleable tools outside the user's admin grant don't exist for them:
|
|
258
|
+
# hidden here so the settings page and composer match what turn_runtime
|
|
259
|
+
# will actually allow.
|
|
260
|
+
from deeptutor.multi_user.tool_access import allowed_optional_tools
|
|
261
|
+
|
|
262
|
+
allowed = allowed_optional_tools()
|
|
263
|
+
if allowed is not None:
|
|
264
|
+
deduped = [p for p in deduped if not p.toggleable or p.name in allowed]
|
|
265
|
+
return ToolsListResponse(
|
|
266
|
+
tools=deduped,
|
|
267
|
+
enabled_optional_tools=sorted(enabled_optional),
|
|
268
|
+
)
|