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,121 @@
1
+ """Checkpointer API schemas."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field, field_validator
6
+ from tenxgraph.core.state import Message
7
+
8
+ from tenxgraph_api.src.app.core.auth.request_config import client_tool_call_error
9
+
10
+
11
+ class ConfigSchema(BaseModel):
12
+ """Schema for state data."""
13
+
14
+ config: dict[str, Any] | None = Field(
15
+ default_factory=dict, description="Configuration for the state"
16
+ )
17
+
18
+
19
+ class StateResponseSchema(BaseModel):
20
+ """Schema for state response."""
21
+
22
+ state: dict[str, Any] | None = Field(None, description="State data")
23
+
24
+
25
+ class StateSchema(ConfigSchema):
26
+ """Schema for putting state."""
27
+
28
+ state: dict[str, Any] = Field(..., description="State data")
29
+
30
+
31
+ class PutMessagesSchema(ConfigSchema):
32
+ """Schema for putting messages."""
33
+
34
+ messages: list[Message] = Field(..., description="List of messages to store")
35
+ metadata: dict[str, Any] | None = Field(None, description="Optional metadata")
36
+
37
+ @field_validator("messages")
38
+ @classmethod
39
+ def messages_must_not_call_tools(cls, v: list[Message]) -> list[Message]:
40
+ if error := client_tool_call_error(v):
41
+ raise ValueError(error)
42
+ return v
43
+
44
+
45
+ class GetMessageSchema(ConfigSchema):
46
+ """Schema for getting a single message."""
47
+
48
+ message_id: str = Field(..., description="Message ID to retrieve")
49
+
50
+
51
+ class ListMessagesSchema(ConfigSchema):
52
+ """Schema for listing messages."""
53
+
54
+ search: str | None = Field(None, description="Search query")
55
+ offset: int | None = Field(None, description="Number of messages to skip")
56
+ limit: int | None = Field(None, description="Maximum number of messages to return")
57
+
58
+
59
+ class DeleteMessageSchema(ConfigSchema):
60
+ """Schema for deleting a message."""
61
+
62
+ message_id: str = Field(..., description="Message ID to delete")
63
+
64
+
65
+ class PutThreadSchema(ConfigSchema):
66
+ """Schema for putting thread info."""
67
+
68
+ thread_info: dict[str, Any] = Field(..., description="Thread information to store")
69
+
70
+
71
+ class GetThreadSchema(ConfigSchema):
72
+ """Schema for getting a thread."""
73
+
74
+ thread_id: str = Field(..., description="Thread ID to retrieve")
75
+
76
+
77
+ class ListThreadsSchema(ConfigSchema):
78
+ """Schema for listing threads."""
79
+
80
+ search: str | None = Field(None, description="Search query")
81
+ offset: int | None = Field(None, description="Number of threads to skip")
82
+ limit: int | None = Field(None, description="Maximum number of threads to return")
83
+
84
+
85
+ class DeleteThreadSchema(ConfigSchema):
86
+ """Schema for deleting a thread."""
87
+
88
+ thread_id: str = Field(..., description="Thread ID to delete")
89
+
90
+
91
+ # Response schemas
92
+ class ResponseSchema(BaseModel):
93
+ """Base response schema for checkpointer operations."""
94
+
95
+ success: bool = Field(..., description="Whether the operation was successful")
96
+ message: str = Field(..., description="Response message")
97
+ data: Any | None = Field(None, description="Response data")
98
+
99
+
100
+ class MessageResponseSchema(BaseModel):
101
+ """Response schema for message operations."""
102
+
103
+ message: Message | None = Field(None, description="Message data")
104
+
105
+
106
+ class MessagesListResponseSchema(BaseModel):
107
+ """Response schema for message list operations."""
108
+
109
+ messages: list[Message] | None = Field(None, description="List of messages")
110
+
111
+
112
+ class ThreadResponseSchema(BaseModel):
113
+ """Response schema for thread operations."""
114
+
115
+ thread: dict[str, Any] | None = Field(None, description="Thread data")
116
+
117
+
118
+ class ThreadsListResponseSchema(BaseModel):
119
+ """Response schema for thread list operations."""
120
+
121
+ threads: list[dict[str, Any]] | None = Field(None, description="List of threads")
@@ -0,0 +1 @@
1
+ # Checkpointer services
@@ -0,0 +1,300 @@
1
+ from typing import Any
2
+
3
+ from fastapi import HTTPException
4
+ from injectq import inject, singleton
5
+ from tenxgraph.core.state import AgentState, Message
6
+ from tenxgraph.storage.checkpointer import BaseCheckpointer
7
+
8
+ from tenxgraph_api.src.app.core import logger
9
+ from tenxgraph_api.src.app.core.auth.request_config import (
10
+ client_config,
11
+ client_tool_call_error,
12
+ )
13
+ from tenxgraph_api.src.app.core.config.settings import get_settings
14
+ from tenxgraph_api.src.app.core.utils.log_sanitizer import sanitize_for_logging
15
+ from tenxgraph_api.src.app.routers.checkpointer.schemas.checkpointer_schemas import (
16
+ MessagesListResponseSchema,
17
+ ResponseSchema,
18
+ StateResponseSchema,
19
+ ThreadResponseSchema,
20
+ ThreadsListResponseSchema,
21
+ )
22
+ from tenxgraph_api.src.app.routers.graph.services.multimodal_preprocessor import (
23
+ check_media_references,
24
+ )
25
+ from tenxgraph_api.src.app.utils.parse_output import parse_state_output
26
+
27
+
28
+ class CheckpointerUnavailableError(HTTPException, ValueError):
29
+ """Raised when the checkpointer service has not been configured."""
30
+
31
+ def __init__(self) -> None:
32
+ detail = "Checkpointer is not configured"
33
+ HTTPException.__init__(self, status_code=503, detail=detail)
34
+ ValueError.__init__(self, detail)
35
+
36
+
37
+ @singleton
38
+ class CheckpointerService:
39
+ @inject
40
+ def __init__(self, checkpointer: BaseCheckpointer):
41
+ self.checkpointer = checkpointer
42
+ self.settings = get_settings()
43
+
44
+ def _config(self, config: dict[str, Any] | None, user: dict) -> dict[str, Any]:
45
+ if not self.checkpointer:
46
+ raise CheckpointerUnavailableError()
47
+
48
+ # Client keys pass through; server-owned keys (authz, user, internals) never do.
49
+ cfg: dict[str, Any] = client_config(config)
50
+ cfg["user"] = user
51
+ cfg["user_id"] = user.get("user_id", "anonymous")
52
+ return cfg
53
+
54
+ async def _check_media_references(self, messages: list[Any], user: dict) -> None:
55
+ """Messages written into a thread are resolved on the next run, so the files they
56
+ reference must belong to the caller, exactly as for graph input."""
57
+ user_id = user.get("user_id")
58
+ if not user_id or not messages:
59
+ return
60
+ await check_media_references(messages, self._media_service(), str(user_id))
61
+
62
+ @staticmethod
63
+ def _media_service() -> Any:
64
+ try:
65
+ from injectq import InjectQ
66
+
67
+ from tenxgraph_api.src.app.routers.media import MediaService
68
+
69
+ return InjectQ.get_instance().try_get(MediaService)
70
+ except Exception:
71
+ return None
72
+
73
+ async def get_state(self, config: dict[str, Any], user: dict) -> StateResponseSchema:
74
+ cfg = self._config(config, user)
75
+
76
+ # this will return base pydantic model
77
+ res = await self.checkpointer.aget_state(cfg)
78
+ if not res:
79
+ rs = await self.checkpointer.aget_state_cache(cfg)
80
+ return StateResponseSchema(state=rs)
81
+
82
+ if res:
83
+ return StateResponseSchema(
84
+ state=parse_state_output(
85
+ self.settings,
86
+ res,
87
+ ),
88
+ )
89
+
90
+ return StateResponseSchema(state=res)
91
+
92
+ async def put_state(
93
+ self,
94
+ config: dict[str, Any],
95
+ user: dict,
96
+ state: dict[str, Any],
97
+ ) -> StateResponseSchema:
98
+ cfg = self._config(config, user)
99
+ # context is appended, so every message here is new and client-authored. A tool call
100
+ # in it would run on the next resume without the model ever requesting it.
101
+ if error := client_tool_call_error(state.get("context")):
102
+ raise HTTPException(status_code=422, detail=error)
103
+ await self._check_media_references(state.get("context") or [], user)
104
+ old_state: AgentState | None = await self.checkpointer.aget_state(cfg)
105
+ if not old_state:
106
+ old_state = await self.checkpointer.aget_state_cache(cfg)
107
+
108
+ # say two states are being merged
109
+ # How to merge to pydantic model
110
+ # Merge incoming state dict into existing Pydantic state, then rebuild the model.
111
+
112
+ merged = self._merge_states(old_state, state)
113
+ to_store = self._reconstruct_state(old_state, merged)
114
+ res = await self.checkpointer.aput_state(cfg, to_store) # type: ignore[arg-type]
115
+ # update cache as well
116
+ await self.checkpointer.aput_state_cache(cfg, to_store) # type: ignore
117
+
118
+ return StateResponseSchema(
119
+ state=parse_state_output(
120
+ self.settings,
121
+ res,
122
+ ),
123
+ )
124
+
125
+ async def clear_state(self, config: dict[str, Any], user: dict) -> ResponseSchema:
126
+ cfg = self._config(config, user)
127
+ res = await self.checkpointer.aclear_state(cfg)
128
+ return ResponseSchema(success=True, message="State cleared successfully", data=res)
129
+
130
+ # Messages
131
+ async def put_messages(
132
+ self,
133
+ config: dict[str, Any],
134
+ user: dict,
135
+ messages: list[Message],
136
+ metadata: dict[str, Any] | None = None,
137
+ ) -> ResponseSchema:
138
+ cfg = self._config(config, user)
139
+ await self._check_media_references(messages, user)
140
+ res = await self.checkpointer.aput_messages(cfg, messages, metadata)
141
+ return ResponseSchema(success=True, message="Messages put successfully", data=res)
142
+
143
+ async def get_message(
144
+ self,
145
+ config: dict[str, Any],
146
+ user: dict,
147
+ message_id: Any,
148
+ ) -> Message:
149
+ cfg = self._config(config, user)
150
+ return await self.checkpointer.aget_message(cfg, message_id)
151
+
152
+ async def get_messages(
153
+ self,
154
+ config: dict[str, Any],
155
+ user: dict,
156
+ search: str | None = None,
157
+ offset: int | None = None,
158
+ limit: int | None = None,
159
+ ) -> MessagesListResponseSchema:
160
+ cfg = self._config(config, user)
161
+ res = await self.checkpointer.alist_messages(cfg, search, offset, limit)
162
+ return MessagesListResponseSchema(messages=res)
163
+
164
+ async def delete_message(
165
+ self,
166
+ config: dict[str, Any],
167
+ user: dict,
168
+ message_id: Any,
169
+ ) -> ResponseSchema:
170
+ cfg = self._config(config, user)
171
+ res = await self.checkpointer.adelete_message(cfg, message_id)
172
+ return ResponseSchema(success=True, message="Message deleted successfully", data=res)
173
+
174
+ # Threads
175
+ async def get_thread(self, config: dict[str, Any], user: dict) -> ThreadResponseSchema:
176
+ cfg = self._config(config, user)
177
+ logger.debug(f"User info: {sanitize_for_logging(user)} and thread config: {cfg}")
178
+ res = await self.checkpointer.aget_thread(cfg)
179
+ return ThreadResponseSchema(thread=res.model_dump() if res else None)
180
+
181
+ async def list_threads(
182
+ self,
183
+ user: dict,
184
+ search: str | None = None,
185
+ offset: int | None = None,
186
+ limit: int | None = None,
187
+ ) -> ThreadsListResponseSchema:
188
+ cfg = self._config({}, user)
189
+ res = await self.checkpointer.alist_threads(cfg, search, offset, limit)
190
+ return ThreadsListResponseSchema(threads=[t.model_dump() for t in res])
191
+
192
+ async def delete_thread(
193
+ self,
194
+ config: dict[str, Any],
195
+ user: dict,
196
+ thread_id: Any,
197
+ ) -> ResponseSchema:
198
+ cfg = self._config(config, user)
199
+ logger.debug(f"User info: {sanitize_for_logging(user)} and thread ID: {thread_id}")
200
+ res = await self.checkpointer.aclean_thread(cfg)
201
+
202
+ # Clean telemetry traces if TelemetryStore is bound in InjectQ
203
+ try:
204
+ from injectq import InjectQ
205
+
206
+ from tenxgraph_api.src.app.utils.telemetry_store import TelemetryStore
207
+
208
+ telemetry_store = InjectQ.get_instance().try_get(TelemetryStore)
209
+ if telemetry_store:
210
+ telemetry_store.delete_thread(str(thread_id))
211
+ except Exception as exc:
212
+ logger.debug("Telemetry store cleanup failed for thread %s: %s", thread_id, exc)
213
+
214
+ # Invalidate any cached ownership for this thread. Ownership is otherwise
215
+ # immutable, so deletion is the only event that must evict the cache.
216
+ try:
217
+ from injectq import InjectQ
218
+
219
+ from tenxgraph_api.src.app.core.auth.authorization import AuthorizationBackend
220
+
221
+ authz = InjectQ.get_instance().try_get(AuthorizationBackend)
222
+ evict = getattr(authz, "evict", None)
223
+ if callable(evict):
224
+ pending = evict(str(thread_id))
225
+ if pending is not None:
226
+ await pending
227
+ except Exception as exc:
228
+ logger.debug("Ownership cache eviction failed for thread %s: %s", thread_id, exc)
229
+
230
+ return ResponseSchema(success=True, message="Thread deleted successfully", data=res)
231
+
232
+ # -------------------------------------------------
233
+ # Internal helpers
234
+ # -------------------------------------------------
235
+ def _merge_states(
236
+ self, old_state: AgentState | None, updates: dict[str, Any]
237
+ ) -> dict[str, Any]:
238
+ """Merge a partial state dict into an existing AgentState.
239
+
240
+ Rules:
241
+ - Preserve execution_meta from old_state unless explicitly provided (discouraged).
242
+ - Append context messages if both sides provide them; otherwise set from updates.
243
+ - Deep-merge dictionaries; non-dict values from updates overwrite.
244
+ - None values in updates do not overwrite existing values.
245
+ """
246
+
247
+ base: dict[str, Any] = {}
248
+ if old_state is not None:
249
+ # Keep full dump so we can preserve existing fields
250
+ # Use serialize_as_any=True to include subclass fields
251
+ base = old_state.model_dump(serialize_as_any=True)
252
+
253
+ merged: dict[str, Any] = {**base}
254
+
255
+ # Handle context specially (append)
256
+ if "context" in updates and updates["context"] is not None:
257
+ old_ctx = base.get("context", []) if base else []
258
+ new_ctx = updates.get("context") or []
259
+ # Simply concatenate; Pydantic will validate/convert dicts to Message
260
+ merged["context"] = list(old_ctx) + list(new_ctx)
261
+
262
+ # execution_meta: keep from old unless explicitly provided as dict/model
263
+ if old_state is not None:
264
+ merged["execution_meta"] = old_state.execution_meta
265
+
266
+ # Apply remaining fields with deep merge
267
+ for k, v in updates.items():
268
+ if k in ("context", "execution_meta"):
269
+ continue
270
+ if v is None:
271
+ # Do not erase existing values with None by default
272
+ continue
273
+ if isinstance(v, dict) and isinstance(merged.get(k), dict):
274
+ merged[k] = self._deep_merge_dicts(merged[k], v)
275
+ else:
276
+ merged[k] = v
277
+
278
+ return merged
279
+
280
+ def _deep_merge_dicts(self, base: dict[str, Any], updates: dict[str, Any]) -> dict[str, Any]:
281
+ """Deep merge two dictionaries without mutating inputs."""
282
+ out: dict[str, Any] = {**base}
283
+ for k, v in updates.items():
284
+ if v is None:
285
+ continue
286
+ if k in out and isinstance(out[k], dict) and isinstance(v, dict):
287
+ out[k] = self._deep_merge_dicts(out[k], v)
288
+ else:
289
+ out[k] = v
290
+ return out
291
+
292
+ def _reconstruct_state(self, old_state: AgentState | None, data: dict[str, Any]) -> AgentState:
293
+ """Rebuild the appropriate AgentState (or subclass) from merged dict."""
294
+ state_cls = type(old_state) if isinstance(old_state, AgentState) else AgentState
295
+
296
+ # Ensure execution_meta stays as model if present
297
+ if old_state is not None:
298
+ data["execution_meta"] = old_state.execution_meta
299
+
300
+ return state_cls.model_validate(data)
@@ -0,0 +1,6 @@
1
+ """Evals router package."""
2
+
3
+ from .router import router
4
+
5
+
6
+ __all__ = ["router"]
@@ -0,0 +1,38 @@
1
+ """
2
+ Evals router — serves eval-report data over HTTP so the playground's Evals
3
+ inspector can exercise a real API. Reports are read from `10xgraph eval`'s
4
+ JSON output (`eval_reports/*.json`); see EvalReportService for how the
5
+ `EvalReport` schema (no run-number, no dollar cost, no built-in regression)
6
+ is adapted into the run/detail shape the UI expects.
7
+
8
+ Endpoints:
9
+ GET /v1/evals/runs -> list of runs (summary rows)
10
+ GET /v1/evals/runs/{run_id} -> full drilldown for one run
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from fastapi import APIRouter, Request
16
+
17
+ from tenxgraph_api.src.app.routers.evals.services.eval_report_service import EvalReportService
18
+ from tenxgraph_api.src.app.utils.response_helper import success_response
19
+
20
+
21
+ # Evals is a dev-only report viewer over local `eval_reports/*.json` files -- it exposes no
22
+ # user data and is not a production surface, so it carries no authorization (the paths are
23
+ # on the route-guard's public allowlist). See core/auth/route_guard.py.
24
+ router = APIRouter(tags=["evals"])
25
+
26
+
27
+ @router.get("/v1/evals/runs", summary="List eval runs")
28
+ async def list_eval_runs(request: Request):
29
+ """List all eval runs (summary rows) found under eval_reports/."""
30
+ service = EvalReportService()
31
+ return success_response({"runs": service.list_runs()}, request)
32
+
33
+
34
+ @router.get("/v1/evals/runs/{run_id}", summary="Get eval run detail")
35
+ async def get_eval_run(run_id: str, request: Request):
36
+ """Return the full drilldown for one eval run."""
37
+ service = EvalReportService()
38
+ return success_response(service.get_run_detail(run_id), request)
@@ -0,0 +1 @@
1
+ """Evals services package."""