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.
Files changed (203) hide show
  1. 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
  2. 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
  3. 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
  4. 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
  5. 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
  7. agentflow_cli/__init__.py +69 -0
  8. tenxgraph_api/__init__.py +23 -0
  9. tenxgraph_api/cli/__init__.py +6 -0
  10. tenxgraph_api/cli/capabilities.py +127 -0
  11. tenxgraph_api/cli/commands/__init__.py +49 -0
  12. tenxgraph_api/cli/commands/api.py +262 -0
  13. tenxgraph_api/cli/commands/audit.py +213 -0
  14. tenxgraph_api/cli/commands/build.py +305 -0
  15. tenxgraph_api/cli/commands/config.py +71 -0
  16. tenxgraph_api/cli/commands/demo.py +93 -0
  17. tenxgraph_api/cli/commands/eval.py +909 -0
  18. tenxgraph_api/cli/commands/init.py +531 -0
  19. tenxgraph_api/cli/commands/skills.py +532 -0
  20. tenxgraph_api/cli/commands/test.py +113 -0
  21. tenxgraph_api/cli/commands/version.py +47 -0
  22. tenxgraph_api/cli/config_editor/__init__.py +15 -0
  23. tenxgraph_api/cli/config_editor/schema.py +566 -0
  24. tenxgraph_api/cli/config_editor/server.py +240 -0
  25. tenxgraph_api/cli/config_editor/static/app.css +2 -0
  26. tenxgraph_api/cli/config_editor/static/app.js +3 -0
  27. tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
  28. tenxgraph_api/cli/config_editor/static/index.html +20 -0
  29. tenxgraph_api/cli/config_editor/store.py +120 -0
  30. tenxgraph_api/cli/config_editor/validation.py +377 -0
  31. tenxgraph_api/cli/constants.py +112 -0
  32. tenxgraph_api/cli/context.py +46 -0
  33. tenxgraph_api/cli/core/__init__.py +0 -0
  34. tenxgraph_api/cli/core/animations.py +550 -0
  35. tenxgraph_api/cli/core/config.py +279 -0
  36. tenxgraph_api/cli/core/output.py +625 -0
  37. tenxgraph_api/cli/core/prompts.py +220 -0
  38. tenxgraph_api/cli/core/screen.py +287 -0
  39. tenxgraph_api/cli/core/steps.py +621 -0
  40. tenxgraph_api/cli/core/theme.py +242 -0
  41. tenxgraph_api/cli/core/validation.py +251 -0
  42. tenxgraph_api/cli/exceptions.py +129 -0
  43. tenxgraph_api/cli/logger.py +84 -0
  44. tenxgraph_api/cli/main.py +936 -0
  45. tenxgraph_api/cli/templates/__init__.py +0 -0
  46. tenxgraph_api/cli/templates/defaults.py +844 -0
  47. tenxgraph_api/cli/templates/dev/.env.example +1 -0
  48. tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
  49. tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
  50. tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
  51. tenxgraph_api/cli/templates/prod/.env.example +75 -0
  52. tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
  53. tenxgraph_api/cli/templates/prod/.python-version +1 -0
  54. tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
  55. tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
  56. tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
  57. tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
  58. tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
  59. tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
  60. tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
  61. tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
  62. tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
  63. tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
  64. tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
  65. tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
  66. tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
  67. tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
  68. tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
  69. tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
  70. tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
  71. tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
  72. tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
  73. tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
  74. tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
  75. tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
  76. tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
  77. tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
  78. tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
  79. tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
  80. tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
  81. tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
  82. tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
  83. tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
  84. tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
  85. tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
  86. tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
  87. tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
  88. tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
  89. tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
  90. tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
  91. tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
  92. tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
  93. tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
  94. tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
  95. tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
  96. tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
  97. tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
  98. tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
  99. tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
  100. tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
  101. tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
  102. tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
  103. tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
  104. tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
  105. tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
  106. tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
  107. tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
  108. tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
  109. tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
  110. tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
  111. tenxgraph_api/py.typed +0 -0
  112. tenxgraph_api/src/__init__.py +0 -0
  113. tenxgraph_api/src/app/__init__.py +13 -0
  114. tenxgraph_api/src/app/core/__init__.py +15 -0
  115. tenxgraph_api/src/app/core/auth/__init__.py +0 -0
  116. tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
  117. tenxgraph_api/src/app/core/auth/authorization.py +379 -0
  118. tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
  119. tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
  120. tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
  121. tenxgraph_api/src/app/core/auth/permissions.py +354 -0
  122. tenxgraph_api/src/app/core/auth/request_config.py +130 -0
  123. tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
  124. tenxgraph_api/src/app/core/config/__init__.py +0 -0
  125. tenxgraph_api/src/app/core/config/graph_config.py +532 -0
  126. tenxgraph_api/src/app/core/config/media_settings.py +72 -0
  127. tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
  128. tenxgraph_api/src/app/core/config/settings.py +202 -0
  129. tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
  130. tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
  131. tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
  132. tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
  133. tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
  134. tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
  135. tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
  136. tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
  137. tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
  138. tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
  139. tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
  140. tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
  141. tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
  142. tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
  143. tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
  144. tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
  145. tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
  146. tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
  147. tenxgraph_api/src/app/core/utils/__init__.py +14 -0
  148. tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
  149. tenxgraph_api/src/app/loader.py +516 -0
  150. tenxgraph_api/src/app/main.py +180 -0
  151. tenxgraph_api/src/app/routers/__init__.py +4 -0
  152. tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
  153. tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
  154. tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
  155. tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
  156. tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
  157. tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
  158. tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
  159. tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
  160. tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
  161. tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
  162. tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
  163. tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
  164. tenxgraph_api/src/app/routers/evals/router.py +38 -0
  165. tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
  166. tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
  167. tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
  168. tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
  169. tenxgraph_api/src/app/routers/graph/router.py +821 -0
  170. tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
  171. tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
  172. tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
  173. tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
  174. tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
  175. tenxgraph_api/src/app/routers/media/__init__.py +506 -0
  176. tenxgraph_api/src/app/routers/media/router.py +242 -0
  177. tenxgraph_api/src/app/routers/media/schemas.py +39 -0
  178. tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
  179. tenxgraph_api/src/app/routers/ping/router.py +28 -0
  180. tenxgraph_api/src/app/routers/setup_router.py +60 -0
  181. tenxgraph_api/src/app/routers/store/__init__.py +4 -0
  182. tenxgraph_api/src/app/routers/store/router.py +200 -0
  183. tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
  184. tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
  185. tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
  186. tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
  187. tenxgraph_api/src/app/tasks/__init__.py +0 -0
  188. tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
  189. tenxgraph_api/src/app/utils/__init__.py +12 -0
  190. tenxgraph_api/src/app/utils/callable_helper.py +26 -0
  191. tenxgraph_api/src/app/utils/media/__init__.py +15 -0
  192. tenxgraph_api/src/app/utils/media/extractor.py +172 -0
  193. tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
  194. tenxgraph_api/src/app/utils/parse_output.py +17 -0
  195. tenxgraph_api/src/app/utils/response_helper.py +129 -0
  196. tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
  197. tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
  198. tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
  199. tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
  200. tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
  201. tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
  202. tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
  203. 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