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,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,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."""
|