10xgraph-api 0.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.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Run the graph for one AG-UI request and stream the result as AG-UI events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ag_ui.core import RunAgentInput
|
|
9
|
+
from ag_ui.encoder import EventEncoder
|
|
10
|
+
from tenxgraph.utils import ResponseGranularity
|
|
11
|
+
|
|
12
|
+
from tenxgraph_api.src.app.core import logger
|
|
13
|
+
from tenxgraph_api.src.app.core.auth.request_config import tool_result_ids
|
|
14
|
+
from tenxgraph_api.src.app.routers.graph.schemas.graph_schemas import GraphInputSchema
|
|
15
|
+
from tenxgraph_api.src.app.routers.graph.services.graph_service import GraphService
|
|
16
|
+
|
|
17
|
+
from .converter import select_new_messages, to_tenxgraph_messages
|
|
18
|
+
from .event_mapper import AgUiEventMapper
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Graph config key holding the AG-UI request extras (frontend context, tools, forwarded props),
|
|
22
|
+
# so graph nodes and tools can read them from ``config["ag_ui"]``.
|
|
23
|
+
AG_UI_CONFIG_KEY = "ag_ui"
|
|
24
|
+
|
|
25
|
+
# Client state keys that would overwrite the conversation or execution metadata.
|
|
26
|
+
_RESERVED_STATE_KEYS = frozenset({"context", "context_summary", "execution_meta"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def stream_ag_ui(
|
|
30
|
+
service: GraphService,
|
|
31
|
+
run_input: RunAgentInput,
|
|
32
|
+
user: dict[str, Any],
|
|
33
|
+
encoder: EventEncoder,
|
|
34
|
+
allow_client_tools: bool = True,
|
|
35
|
+
) -> AsyncIterator[str]:
|
|
36
|
+
"""Stream one AG-UI run as encoded events.
|
|
37
|
+
|
|
38
|
+
With ``allow_client_tools`` the tools the client sends are offered to the model for this run
|
|
39
|
+
(see ``ag_ui.allow_client_tools``); without it only ``remote_tools`` from 10xgraph.json are.
|
|
40
|
+
|
|
41
|
+
The thread's checkpoint is the record of the conversation: only client messages it has not
|
|
42
|
+
seen are sent to the graph. A run with nothing new (a client syncing state) still gets a
|
|
43
|
+
complete ``RUN_STARTED`` / ``RUN_FINISHED`` pair, with the thread's state.
|
|
44
|
+
"""
|
|
45
|
+
initial_state = _client_state(run_input.state)
|
|
46
|
+
mapper = AgUiEventMapper(run_input.thread_id, run_input.run_id, initial_state=initial_state)
|
|
47
|
+
for event in mapper.start():
|
|
48
|
+
yield encoder.encode(event)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
async for event in _run_events(
|
|
52
|
+
service, run_input, user, mapper, initial_state, allow_client_tools
|
|
53
|
+
):
|
|
54
|
+
yield encoder.encode(event)
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
logger.warning("AG-UI run rejected: %s", exc)
|
|
57
|
+
for event in mapper.fail(str(exc), "INVALID_INPUT"):
|
|
58
|
+
yield encoder.encode(event)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
# The response has already started, so the failure has to travel in the stream.
|
|
61
|
+
logger.error("AG-UI run failed: %s", exc)
|
|
62
|
+
for event in mapper.fail("AG-UI run failed", "AG_UI_ERROR"):
|
|
63
|
+
yield encoder.encode(event)
|
|
64
|
+
|
|
65
|
+
for event in mapper.finish():
|
|
66
|
+
yield encoder.encode(event)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _run_events(
|
|
70
|
+
service: GraphService,
|
|
71
|
+
run_input: RunAgentInput,
|
|
72
|
+
user: dict[str, Any],
|
|
73
|
+
mapper: AgUiEventMapper,
|
|
74
|
+
initial_state: dict[str, Any] | None,
|
|
75
|
+
allow_client_tools: bool,
|
|
76
|
+
) -> AsyncIterator[Any]:
|
|
77
|
+
"""The events between ``RUN_STARTED`` and the terminal event of one run."""
|
|
78
|
+
checkpoint_state = await _checkpoint_state(service, run_input.thread_id, user)
|
|
79
|
+
context = list(getattr(checkpoint_state, "context", None) or [])
|
|
80
|
+
new_messages = select_new_messages(
|
|
81
|
+
run_input.messages,
|
|
82
|
+
known_ids={str(m.message_id) for m in context},
|
|
83
|
+
answered_call_ids=set(tool_result_ids(context)),
|
|
84
|
+
)
|
|
85
|
+
messages = to_tenxgraph_messages(new_messages)
|
|
86
|
+
paused_at = _pending_interrupt(checkpoint_state)
|
|
87
|
+
resume = _resume_value(paused_at, run_input.resume or [])
|
|
88
|
+
|
|
89
|
+
if paused_at is not None and resume is _NO_RESUME:
|
|
90
|
+
# A thread paused at interrupt() only moves on with an answer; report the pause again
|
|
91
|
+
# instead of running the graph.
|
|
92
|
+
mapper.report_interrupt(paused_at.model_dump(mode="json"))
|
|
93
|
+
return
|
|
94
|
+
if not messages and resume is _NO_RESUME:
|
|
95
|
+
if checkpoint_state is not None:
|
|
96
|
+
for event in mapper.map_state(checkpoint_state):
|
|
97
|
+
yield event
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
graph_input, server_config = _graph_request(run_input, messages, initial_state, resume)
|
|
101
|
+
if not allow_client_tools:
|
|
102
|
+
server_config = {}
|
|
103
|
+
async for chunk in service.stream_chunks(graph_input, user, server_config):
|
|
104
|
+
for event in mapper.map(chunk):
|
|
105
|
+
yield event
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _client_state(state: Any) -> dict[str, Any] | None:
|
|
109
|
+
if not isinstance(state, dict):
|
|
110
|
+
return None
|
|
111
|
+
cleaned = {k: v for k, v in state.items() if k not in _RESERVED_STATE_KEYS}
|
|
112
|
+
return cleaned or None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _checkpoint_state(service: GraphService, thread_id: str, user: dict[str, Any]) -> Any:
|
|
116
|
+
if service.checkpointer is None:
|
|
117
|
+
return None
|
|
118
|
+
config = {
|
|
119
|
+
"thread_id": thread_id,
|
|
120
|
+
"user": user,
|
|
121
|
+
"user_id": user.get("user_id", "anonymous"),
|
|
122
|
+
}
|
|
123
|
+
return await service.checkpointer.aget_state(config)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Marks "the client sent no answer for the open interrupt" (a None answer means cancelled).
|
|
127
|
+
_NO_RESUME: Any = object()
|
|
128
|
+
|
|
129
|
+
# ``remote_tools`` run-config key read by the tenxgraph ToolNode for per-run client tools.
|
|
130
|
+
RUN_REMOTE_TOOLS_KEY = "remote_tools"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _pending_interrupt(state: Any) -> Any:
|
|
134
|
+
"""The interrupt() the thread is paused at, or ``None`` (and on cores without interrupts)."""
|
|
135
|
+
if state is None:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
from tenxgraph.utils.interrupt import pending_interrupt
|
|
139
|
+
except ImportError: # tenxgraph core older than interrupt()
|
|
140
|
+
return None
|
|
141
|
+
return pending_interrupt(state)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _resume_value(paused_at: Any, entries: list[Any]) -> Any:
|
|
145
|
+
"""The graph resume value for the AG-UI ``resume`` entries.
|
|
146
|
+
|
|
147
|
+
Returns ``_NO_RESUME`` when nothing answers the open interrupt. A ``cancelled`` entry resumes
|
|
148
|
+
with ``None``.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: ``resume`` was sent but the thread is not paused at that interrupt.
|
|
152
|
+
"""
|
|
153
|
+
if not entries:
|
|
154
|
+
return _NO_RESUME
|
|
155
|
+
if paused_at is None:
|
|
156
|
+
raise ValueError("Nothing to resume: this thread is not paused at an interrupt")
|
|
157
|
+
for entry in entries:
|
|
158
|
+
if entry.interrupt_id == paused_at.id:
|
|
159
|
+
return entry.payload if entry.status == "resolved" else None
|
|
160
|
+
raise ValueError(f"resume does not answer the open interrupt '{paused_at.id}'")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _graph_request(
|
|
164
|
+
run_input: RunAgentInput,
|
|
165
|
+
messages: list[Any],
|
|
166
|
+
initial_state: dict[str, Any] | None,
|
|
167
|
+
resume: Any,
|
|
168
|
+
) -> tuple[GraphInputSchema, dict[str, Any]]:
|
|
169
|
+
"""The graph input for one AG-UI run, and the server-owned run config that goes with it."""
|
|
170
|
+
extra: dict[str, Any] = {} if resume is _NO_RESUME else {"resume": resume}
|
|
171
|
+
graph_input = GraphInputSchema(
|
|
172
|
+
messages=messages,
|
|
173
|
+
initial_state=initial_state,
|
|
174
|
+
config={
|
|
175
|
+
"thread_id": run_input.thread_id,
|
|
176
|
+
"run_id": run_input.run_id,
|
|
177
|
+
AG_UI_CONFIG_KEY: {
|
|
178
|
+
"context": [c.model_dump(mode="json") for c in run_input.context or []],
|
|
179
|
+
"tools": [t.model_dump(mode="json") for t in run_input.tools or []],
|
|
180
|
+
"forwarded_props": run_input.forwarded_props,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
response_granularity=ResponseGranularity.FULL,
|
|
184
|
+
**extra,
|
|
185
|
+
)
|
|
186
|
+
# The client's own tools, offered to the model for this run only.
|
|
187
|
+
server_config = {
|
|
188
|
+
RUN_REMOTE_TOOLS_KEY: [
|
|
189
|
+
{"name": t.name, "description": t.description, "parameters": t.parameters}
|
|
190
|
+
for t in run_input.tools or []
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
return graph_input, server_config
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Checkpointer module
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Checkpointer router module."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
6
|
+
from injectq.integrations import InjectAPI
|
|
7
|
+
from tenxgraph.core.state import Message
|
|
8
|
+
|
|
9
|
+
from tenxgraph_api.src.app.core.auth.permissions import RequirePermission
|
|
10
|
+
from tenxgraph_api.src.app.core.auth.request_config import client_config
|
|
11
|
+
from tenxgraph_api.src.app.utils.response_helper import success_response
|
|
12
|
+
from tenxgraph_api.src.app.utils.swagger_helper import generate_swagger_responses
|
|
13
|
+
|
|
14
|
+
from .schemas.checkpointer_schemas import (
|
|
15
|
+
ConfigSchema,
|
|
16
|
+
MessagesListResponseSchema,
|
|
17
|
+
PutMessagesSchema,
|
|
18
|
+
ResponseSchema,
|
|
19
|
+
StateResponseSchema,
|
|
20
|
+
StateSchema,
|
|
21
|
+
ThreadResponseSchema,
|
|
22
|
+
ThreadsListResponseSchema,
|
|
23
|
+
)
|
|
24
|
+
from .services.checkpointer_service import CheckpointerService
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
router = APIRouter(tags=["checkpointer"])
|
|
28
|
+
|
|
29
|
+
# Bound pagination so an unbounded/huge ``limit`` (or ``limit=None``) can never ask the
|
|
30
|
+
# backend to load a whole table into memory. Applied server-side regardless of client input.
|
|
31
|
+
DEFAULT_PAGE_LIMIT = 100
|
|
32
|
+
MAX_PAGE_LIMIT = 1000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _clamp_limit(limit: int | None) -> int:
|
|
36
|
+
"""Return a bounded page size: default when unset, capped at MAX_PAGE_LIMIT."""
|
|
37
|
+
if limit is None:
|
|
38
|
+
return DEFAULT_PAGE_LIMIT
|
|
39
|
+
return min(limit, MAX_PAGE_LIMIT)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_thread_id(thread_id: int | str) -> None:
|
|
43
|
+
if isinstance(thread_id, str):
|
|
44
|
+
if not thread_id.strip():
|
|
45
|
+
raise HTTPException(status_code=422, detail="thread_id cannot be empty or whitespace")
|
|
46
|
+
elif isinstance(thread_id, int):
|
|
47
|
+
if thread_id < 1:
|
|
48
|
+
raise HTTPException(status_code=422, detail="thread_id must be a non-negative integer")
|
|
49
|
+
else:
|
|
50
|
+
raise HTTPException(status_code=422, detail="thread_id must be a string or integer")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get(
|
|
54
|
+
"/v1/threads/{thread_id}/state",
|
|
55
|
+
status_code=status.HTTP_200_OK,
|
|
56
|
+
responses=generate_swagger_responses(StateResponseSchema),
|
|
57
|
+
summary="Get state from checkpointer",
|
|
58
|
+
description="Retrieve state data from the checkpointer using configuration.",
|
|
59
|
+
)
|
|
60
|
+
async def get_state(
|
|
61
|
+
request: Request,
|
|
62
|
+
thread_id: int | str,
|
|
63
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
64
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "read")),
|
|
65
|
+
):
|
|
66
|
+
"""Get state from checkpointer.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
request: State schema with configuration
|
|
70
|
+
checkpointer: Injected checkpointer instance
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
State response with state data or error
|
|
74
|
+
"""
|
|
75
|
+
validate_thread_id(thread_id)
|
|
76
|
+
|
|
77
|
+
config = {"thread_id": thread_id}
|
|
78
|
+
|
|
79
|
+
result = await service.get_state(
|
|
80
|
+
config,
|
|
81
|
+
user,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return success_response(
|
|
85
|
+
result,
|
|
86
|
+
request,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.put(
|
|
91
|
+
"/v1/threads/{thread_id}/state",
|
|
92
|
+
status_code=status.HTTP_200_OK,
|
|
93
|
+
responses=generate_swagger_responses(StateResponseSchema),
|
|
94
|
+
summary="Put state to checkpointer",
|
|
95
|
+
description="Store state data in the checkpointer using configuration.",
|
|
96
|
+
)
|
|
97
|
+
async def put_state(
|
|
98
|
+
request: Request,
|
|
99
|
+
thread_id: str | int,
|
|
100
|
+
payload: StateSchema,
|
|
101
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
102
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "write")),
|
|
103
|
+
):
|
|
104
|
+
"""Put state to checkpointer.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
request: Request object
|
|
108
|
+
payload: Put state schema with configuration and state data
|
|
109
|
+
service: Injected checkpointer service
|
|
110
|
+
user: Current authenticated user
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Success response or error
|
|
114
|
+
"""
|
|
115
|
+
validate_thread_id(thread_id)
|
|
116
|
+
# The path thread_id is the one RequirePermission checked; it is applied last so the
|
|
117
|
+
# body config cannot redirect the call to another thread.
|
|
118
|
+
config = {**client_config(payload.config), "thread_id": thread_id}
|
|
119
|
+
|
|
120
|
+
res = await service.put_state(
|
|
121
|
+
config,
|
|
122
|
+
user,
|
|
123
|
+
payload.state,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return success_response(
|
|
127
|
+
res,
|
|
128
|
+
request,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.delete(
|
|
133
|
+
"/v1/threads/{thread_id}/state",
|
|
134
|
+
status_code=status.HTTP_200_OK,
|
|
135
|
+
responses=generate_swagger_responses(ResponseSchema),
|
|
136
|
+
summary="Clear state from checkpointer",
|
|
137
|
+
description="Clear state data from the checkpointer using configuration.",
|
|
138
|
+
)
|
|
139
|
+
async def clear_state(
|
|
140
|
+
request: Request,
|
|
141
|
+
thread_id: int | str,
|
|
142
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
143
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "delete")),
|
|
144
|
+
):
|
|
145
|
+
"""Clear state from checkpointer.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
request: Request object
|
|
149
|
+
payload: Clear state schema with configuration
|
|
150
|
+
service: Injected checkpointer service
|
|
151
|
+
user: Current authenticated user
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Success response or error
|
|
155
|
+
"""
|
|
156
|
+
validate_thread_id(thread_id)
|
|
157
|
+
config = {"thread_id": thread_id}
|
|
158
|
+
|
|
159
|
+
res = await service.clear_state(
|
|
160
|
+
config,
|
|
161
|
+
user,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return success_response(
|
|
165
|
+
res,
|
|
166
|
+
request,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Now Handle Messages
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@router.post(
|
|
174
|
+
"/v1/threads/{thread_id}/messages",
|
|
175
|
+
status_code=status.HTTP_200_OK,
|
|
176
|
+
responses=generate_swagger_responses(ResponseSchema),
|
|
177
|
+
summary="Put messages to checkpointer",
|
|
178
|
+
description="Store messages in the checkpointer using configuration.",
|
|
179
|
+
)
|
|
180
|
+
async def put_messages(
|
|
181
|
+
request: Request,
|
|
182
|
+
thread_id: str | int,
|
|
183
|
+
payload: PutMessagesSchema,
|
|
184
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
185
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "write")),
|
|
186
|
+
):
|
|
187
|
+
"""Put messages to checkpointer.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
request: Request object
|
|
191
|
+
payload: Put messages schema with configuration and messages
|
|
192
|
+
service: Injected checkpointer service
|
|
193
|
+
user: Current authenticated user
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Success response or error
|
|
197
|
+
"""
|
|
198
|
+
validate_thread_id(thread_id)
|
|
199
|
+
if not payload.messages:
|
|
200
|
+
raise HTTPException(status_code=422, detail="messages must not be empty")
|
|
201
|
+
|
|
202
|
+
# The path thread_id is the one RequirePermission checked; it is applied last so the
|
|
203
|
+
# body config cannot redirect the call to another thread.
|
|
204
|
+
config = {**client_config(payload.config), "thread_id": thread_id}
|
|
205
|
+
|
|
206
|
+
res = await service.put_messages(
|
|
207
|
+
config,
|
|
208
|
+
user,
|
|
209
|
+
payload.messages,
|
|
210
|
+
payload.metadata,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return success_response(
|
|
214
|
+
res,
|
|
215
|
+
request,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@router.get(
|
|
220
|
+
"/v1/threads/{thread_id}/messages/{message_id}",
|
|
221
|
+
status_code=status.HTTP_200_OK,
|
|
222
|
+
responses=generate_swagger_responses(Message),
|
|
223
|
+
summary="Get message from checkpointer",
|
|
224
|
+
description=(
|
|
225
|
+
"Retrieve a specific message from the checkpointer using configuration and message ID."
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
async def get_message(
|
|
229
|
+
request: Request,
|
|
230
|
+
thread_id: str | int,
|
|
231
|
+
message_id: str | int,
|
|
232
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
233
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "read")),
|
|
234
|
+
):
|
|
235
|
+
"""Get message from checkpointer.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
request: Request object
|
|
239
|
+
payload: Get message schema with configuration and message ID
|
|
240
|
+
service: Injected checkpointer service
|
|
241
|
+
user: Current authenticated user
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Message response with message data or error
|
|
245
|
+
"""
|
|
246
|
+
validate_thread_id(thread_id)
|
|
247
|
+
if not message_id or (isinstance(message_id, str) and not str(message_id).strip()):
|
|
248
|
+
raise HTTPException(status_code=422, detail="message_id is required and cannot be empty")
|
|
249
|
+
|
|
250
|
+
config = {"thread_id": thread_id}
|
|
251
|
+
|
|
252
|
+
result = await service.get_message(
|
|
253
|
+
config,
|
|
254
|
+
user,
|
|
255
|
+
message_id,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return success_response(
|
|
259
|
+
result,
|
|
260
|
+
request,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@router.get(
|
|
265
|
+
"/v1/threads/{thread_id}/messages",
|
|
266
|
+
status_code=status.HTTP_200_OK,
|
|
267
|
+
responses=generate_swagger_responses(MessagesListResponseSchema),
|
|
268
|
+
summary="List messages from checkpointer",
|
|
269
|
+
description="Retrieve a list of messages from the checkpointer using configuration "
|
|
270
|
+
"and optional filters.",
|
|
271
|
+
)
|
|
272
|
+
async def list_messages(
|
|
273
|
+
request: Request,
|
|
274
|
+
thread_id: int | str,
|
|
275
|
+
search: str | None = None,
|
|
276
|
+
offset: int | None = None,
|
|
277
|
+
limit: int | None = None,
|
|
278
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
279
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "read")),
|
|
280
|
+
):
|
|
281
|
+
"""List messages from checkpointer.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
request: Request object
|
|
285
|
+
payload: List messages schema with configuration and optional filters
|
|
286
|
+
service: Injected checkpointer service
|
|
287
|
+
user: Current authenticated user
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Messages list response with messages data or error
|
|
291
|
+
"""
|
|
292
|
+
validate_thread_id(thread_id)
|
|
293
|
+
if offset is not None and offset < 0:
|
|
294
|
+
raise HTTPException(status_code=422, detail="offset must be >= 0")
|
|
295
|
+
if limit is not None and limit <= 0:
|
|
296
|
+
raise HTTPException(status_code=422, detail="limit must be > 0")
|
|
297
|
+
|
|
298
|
+
config = {"thread_id": thread_id}
|
|
299
|
+
|
|
300
|
+
result = await service.get_messages(
|
|
301
|
+
config,
|
|
302
|
+
user,
|
|
303
|
+
search,
|
|
304
|
+
offset,
|
|
305
|
+
_clamp_limit(limit),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return success_response(
|
|
309
|
+
result,
|
|
310
|
+
request,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@router.delete(
|
|
315
|
+
"/v1/threads/{thread_id}/messages/{message_id}",
|
|
316
|
+
status_code=status.HTTP_200_OK,
|
|
317
|
+
responses=generate_swagger_responses(ResponseSchema),
|
|
318
|
+
summary="Delete message from checkpointer",
|
|
319
|
+
description="Delete a specific message from the checkpointer using configuration and ID.",
|
|
320
|
+
)
|
|
321
|
+
async def delete_message(
|
|
322
|
+
request: Request,
|
|
323
|
+
message_id: str | int,
|
|
324
|
+
thread_id: str | int,
|
|
325
|
+
payload: ConfigSchema,
|
|
326
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
327
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "delete")),
|
|
328
|
+
):
|
|
329
|
+
"""Delete message from checkpointer.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
request: Request object
|
|
333
|
+
payload: Delete message schema with configuration and message ID
|
|
334
|
+
service: Injected checkpointer service
|
|
335
|
+
user: Current authenticated user
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Success response or error
|
|
339
|
+
"""
|
|
340
|
+
validate_thread_id(thread_id)
|
|
341
|
+
if not message_id or (isinstance(message_id, str) and not str(message_id).strip()):
|
|
342
|
+
raise HTTPException(status_code=422, detail="message_id is required and cannot be empty")
|
|
343
|
+
|
|
344
|
+
# The path thread_id is the one RequirePermission checked; it is applied last so the
|
|
345
|
+
# body config cannot redirect the call to another thread.
|
|
346
|
+
config = {**client_config(payload.config), "thread_id": thread_id}
|
|
347
|
+
|
|
348
|
+
await service.delete_message(
|
|
349
|
+
config,
|
|
350
|
+
user,
|
|
351
|
+
message_id,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return success_response(
|
|
355
|
+
{"success": True, "message": "Message deleted successfully"},
|
|
356
|
+
request,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# Handle Threads
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.get(
|
|
364
|
+
"/v1/threads/{thread_id}",
|
|
365
|
+
status_code=status.HTTP_200_OK,
|
|
366
|
+
responses=generate_swagger_responses(ThreadResponseSchema),
|
|
367
|
+
summary="Get thread from checkpointer",
|
|
368
|
+
description="Retrieve a specific thread from the checkpointer using configuration.",
|
|
369
|
+
)
|
|
370
|
+
async def get_thread(
|
|
371
|
+
request: Request,
|
|
372
|
+
thread_id: str | int,
|
|
373
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
374
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "read")),
|
|
375
|
+
):
|
|
376
|
+
"""Get thread from checkpointer.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
request: Request object
|
|
380
|
+
payload: Get thread schema with configuration and thread ID
|
|
381
|
+
service: Injected checkpointer service
|
|
382
|
+
user: Current authenticated user
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Thread response with thread data or error
|
|
386
|
+
"""
|
|
387
|
+
validate_thread_id(thread_id)
|
|
388
|
+
result = await service.get_thread(
|
|
389
|
+
{"thread_id": thread_id},
|
|
390
|
+
user,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return success_response(
|
|
394
|
+
{"thread_data": result},
|
|
395
|
+
request,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@router.get(
|
|
400
|
+
"/v1/threads",
|
|
401
|
+
status_code=status.HTTP_200_OK,
|
|
402
|
+
responses=generate_swagger_responses(ThreadsListResponseSchema),
|
|
403
|
+
summary="List threads from checkpointer",
|
|
404
|
+
description="Retrieve a list of threads from the checkpointer with optional filters.",
|
|
405
|
+
)
|
|
406
|
+
async def list_threads(
|
|
407
|
+
request: Request,
|
|
408
|
+
search: str | None = None,
|
|
409
|
+
offset: int | None = None,
|
|
410
|
+
limit: int | None = None,
|
|
411
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
412
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "read")),
|
|
413
|
+
):
|
|
414
|
+
"""List threads from checkpointer.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
request: Request object
|
|
418
|
+
payload: List threads schema with configuration and optional filters
|
|
419
|
+
service: Injected checkpointer service
|
|
420
|
+
user: Current authenticated user
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Threads list response with threads data or error
|
|
424
|
+
"""
|
|
425
|
+
if limit is not None and limit <= 0:
|
|
426
|
+
raise HTTPException(status_code=422, detail="limit must be > 0")
|
|
427
|
+
if offset is not None and offset < 0:
|
|
428
|
+
raise HTTPException(status_code=422, detail="offset must be >= 0")
|
|
429
|
+
|
|
430
|
+
result = await service.list_threads(
|
|
431
|
+
user,
|
|
432
|
+
search,
|
|
433
|
+
offset,
|
|
434
|
+
_clamp_limit(limit),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return success_response(
|
|
438
|
+
result,
|
|
439
|
+
request,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@router.delete(
|
|
444
|
+
"/v1/threads/{thread_id}",
|
|
445
|
+
status_code=status.HTTP_200_OK,
|
|
446
|
+
responses=generate_swagger_responses(ResponseSchema),
|
|
447
|
+
summary="Delete thread from checkpointer",
|
|
448
|
+
description="Delete a specific thread from the checkpointer using configuration and thread ID.",
|
|
449
|
+
)
|
|
450
|
+
async def delete_thread(
|
|
451
|
+
request: Request,
|
|
452
|
+
thread_id: str | int,
|
|
453
|
+
payload: ConfigSchema,
|
|
454
|
+
service: CheckpointerService = InjectAPI(CheckpointerService),
|
|
455
|
+
user: dict[str, Any] = Depends(RequirePermission("checkpointer", "delete")),
|
|
456
|
+
):
|
|
457
|
+
"""Delete thread from checkpointer.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
request: Request object
|
|
461
|
+
payload: Delete thread schema with configuration and thread ID
|
|
462
|
+
service: Injected checkpointer service
|
|
463
|
+
user: Current authenticated user
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Success response or error
|
|
467
|
+
"""
|
|
468
|
+
validate_thread_id(thread_id)
|
|
469
|
+
# The path thread_id is the one RequirePermission checked; it is applied last so the
|
|
470
|
+
# body config cannot redirect the call to another thread.
|
|
471
|
+
config = {**client_config(payload.config), "thread_id": thread_id}
|
|
472
|
+
|
|
473
|
+
res = await service.delete_thread(
|
|
474
|
+
config,
|
|
475
|
+
user,
|
|
476
|
+
thread_id,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return success_response(
|
|
480
|
+
res,
|
|
481
|
+
request,
|
|
482
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Checkpointer schemas
|