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,844 @@
|
|
|
1
|
+
"""Default templates for CLI initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# How long a worker gets to finish in-flight work after SIGTERM.
|
|
10
|
+
#
|
|
11
|
+
# An agent run is not a typical web request: the LLM client alone allows up to
|
|
12
|
+
# 600s, and tools add more on top. Gunicorn's default graceful timeout is 30s, so
|
|
13
|
+
# every rolling deploy was hard-killing runs that were still in progress, ~30s in.
|
|
14
|
+
# Matching the LLM budget means a run in flight gets to finish rather than being
|
|
15
|
+
# truncated. Kubernetes needs a slightly larger window than the app, so the pod is
|
|
16
|
+
# not killed while the app is still draining.
|
|
17
|
+
GRACEFUL_TIMEOUT_SECONDS: Final[int] = 600
|
|
18
|
+
|
|
19
|
+
# Gunicorn kills a worker it considers hung. Long LLM calls must not look hung.
|
|
20
|
+
WORKER_TIMEOUT_SECONDS: Final[int] = 660
|
|
21
|
+
|
|
22
|
+
# terminationGracePeriodSeconds must exceed the app's graceful timeout, plus the
|
|
23
|
+
# preStop sleep that lets the load balancer stop sending new traffic first.
|
|
24
|
+
K8S_TERMINATION_GRACE_SECONDS: Final[int] = GRACEFUL_TIMEOUT_SECONDS + 60
|
|
25
|
+
K8S_PRESTOP_SLEEP_SECONDS: Final[int] = 15
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Default configuration template
|
|
29
|
+
DEFAULT_CONFIG_JSON: Final[str] = json.dumps(
|
|
30
|
+
{
|
|
31
|
+
"agent": "graph.react:app",
|
|
32
|
+
"env": ".env",
|
|
33
|
+
"auth": None,
|
|
34
|
+
"checkpointer": None,
|
|
35
|
+
"injectq": None,
|
|
36
|
+
"store": None,
|
|
37
|
+
"remote_tools": [],
|
|
38
|
+
"thread_name_generator": None,
|
|
39
|
+
"observability": None,
|
|
40
|
+
},
|
|
41
|
+
indent=2,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Template for the default react agent graph
|
|
45
|
+
DEFAULT_REACT_PY: Final[str] = '''
|
|
46
|
+
"""
|
|
47
|
+
Graph-based React Agent Implementation
|
|
48
|
+
|
|
49
|
+
This module implements a reactive agent system using 10xGraph's StateGraph.
|
|
50
|
+
The agent can interact with tools (like weather checking) and maintain conversation
|
|
51
|
+
state through a checkpointer. The graph orchestrates the flow between the main
|
|
52
|
+
agent logic and tool execution.
|
|
53
|
+
|
|
54
|
+
Key Components:
|
|
55
|
+
- Weather tool: Demonstrates tool calling with dependency injection
|
|
56
|
+
- Main agent: AI-powered assistant that can use tools
|
|
57
|
+
- Graph flow: Conditional routing based on tool usage
|
|
58
|
+
- Checkpointer: Maintains conversation state across interactions
|
|
59
|
+
|
|
60
|
+
Architecture:
|
|
61
|
+
The system uses a state graph with two main nodes:
|
|
62
|
+
1. MAIN: Processes user input and generates AI responses
|
|
63
|
+
2. TOOL: Executes tool calls when requested by the AI
|
|
64
|
+
|
|
65
|
+
The graph conditionally routes between these nodes based on whether
|
|
66
|
+
the AI response contains tool calls. Conversation history is maintained
|
|
67
|
+
through the checkpointer, allowing for multi-turn conversations.
|
|
68
|
+
|
|
69
|
+
Tools are defined as functions with JSON schema docstrings that describe
|
|
70
|
+
their interface for the AI model. The ToolNode automatically extracts
|
|
71
|
+
these schemas for tool selection.
|
|
72
|
+
|
|
73
|
+
Dependencies:
|
|
74
|
+
- 10xGraph: For graph and state management
|
|
75
|
+
- LiteLLM: For AI model interactions
|
|
76
|
+
- InjectQ: For dependency injection
|
|
77
|
+
- Python logging: For debug and info messages
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
import logging
|
|
81
|
+
from typing import Any
|
|
82
|
+
|
|
83
|
+
from tenxgraph.adapters.llm.model_response_converter import ModelResponseConverter
|
|
84
|
+
from tenxgraph.checkpointer import InMemoryCheckpointer
|
|
85
|
+
from tenxgraph.graph import StateGraph, ToolNode
|
|
86
|
+
from tenxgraph.state.agent_state import AgentState
|
|
87
|
+
from tenxgraph.utils.callbacks import CallbackManager
|
|
88
|
+
from tenxgraph.utils.constants import END
|
|
89
|
+
from tenxgraph.utils.converter import convert_messages
|
|
90
|
+
from dotenv import load_dotenv
|
|
91
|
+
from injectq import Inject
|
|
92
|
+
from litellm import acompletion
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Configure logging for the module
|
|
96
|
+
logging.basicConfig(
|
|
97
|
+
level=logging.INFO,
|
|
98
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
99
|
+
handlers=[logging.StreamHandler()],
|
|
100
|
+
)
|
|
101
|
+
logger = logging.getLogger(__name__)
|
|
102
|
+
|
|
103
|
+
# Load environment variables from .env file
|
|
104
|
+
load_dotenv()
|
|
105
|
+
|
|
106
|
+
# Initialize in-memory checkpointer for maintaining conversation state
|
|
107
|
+
checkpointer = InMemoryCheckpointer()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
Note: The docstring below will be used as the tool description and it will be
|
|
112
|
+
passed to the AI model for tool selection, so keep it relevant and concise.
|
|
113
|
+
This function will be converted to a tool with the following schema:
|
|
114
|
+
[
|
|
115
|
+
{
|
|
116
|
+
'type': 'function',
|
|
117
|
+
'function': {
|
|
118
|
+
'name': 'get_weather',
|
|
119
|
+
'description': 'Retrieve current weather information for a specified location.',
|
|
120
|
+
'parameters': {
|
|
121
|
+
'type': 'object',
|
|
122
|
+
'properties': {
|
|
123
|
+
'location': {'type': 'string'}
|
|
124
|
+
},
|
|
125
|
+
'required': ['location']
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
Parameters like tool_call_id, state, and checkpointer are injected automatically
|
|
132
|
+
by InjectQ when the tool is called by the agent.
|
|
133
|
+
Available injected parameters:
|
|
134
|
+
The following parameters are automatically injected by InjectQ when the tool is called,
|
|
135
|
+
but need to keep them as same name and type for proper injection:
|
|
136
|
+
- tool_call_id: Unique ID for the tool call
|
|
137
|
+
- state: Current AgentState containing conversation context
|
|
138
|
+
- config: Configuration dictionary passed during graph invocation
|
|
139
|
+
|
|
140
|
+
Below fields need to be used with Inject[] to get the instances:
|
|
141
|
+
- context_manager: ContextManager instance for managing context, like trimming
|
|
142
|
+
- publisher: Publisher instance for publishing events and logs
|
|
143
|
+
- checkpointer: InMemoryCheckpointer instance for state management
|
|
144
|
+
- store: InMemoryStore instance for temporary data storage
|
|
145
|
+
- callback: CallbackManager instance for handling callbacks
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_weather(
|
|
151
|
+
location: str,
|
|
152
|
+
tool_call_id: str,
|
|
153
|
+
state: AgentState,
|
|
154
|
+
checkpointer: InMemoryCheckpointer = Inject[InMemoryCheckpointer],
|
|
155
|
+
) -> str:
|
|
156
|
+
"""Retrieve current weather information for a specified location."""
|
|
157
|
+
# Demonstrate access to injected parameters
|
|
158
|
+
logger.debug("***** Checkpointer instance: %s", checkpointer)
|
|
159
|
+
if tool_call_id:
|
|
160
|
+
logger.debug("Tool call ID: %s", tool_call_id)
|
|
161
|
+
if state and hasattr(state, "context"):
|
|
162
|
+
logger.debug("Number of messages in context: %d", len(state.context))
|
|
163
|
+
|
|
164
|
+
# Mock weather response - in production, this would call a real weather API
|
|
165
|
+
return f"The weather in {location} is sunny"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Create a tool node containing all available tools
|
|
169
|
+
tool_node = ToolNode([get_weather])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def main_agent(
|
|
173
|
+
state: AgentState,
|
|
174
|
+
config: dict,
|
|
175
|
+
checkpointer: InMemoryCheckpointer = Inject[InMemoryCheckpointer],
|
|
176
|
+
callback: CallbackManager = Inject[CallbackManager],
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""
|
|
179
|
+
Main agent logic that processes user messages and generates responses.
|
|
180
|
+
|
|
181
|
+
This function implements the core AI agent behavior, handling both regular
|
|
182
|
+
conversation and tool-augmented responses. It uses LiteLLM for AI completion
|
|
183
|
+
and can access conversation history through the checkpointer.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
state: Current agent state containing conversation context
|
|
187
|
+
config: Configuration dictionary containing thread_id and other settings
|
|
188
|
+
checkpointer: Checkpointer for retrieving conversation history (injected)
|
|
189
|
+
callback: Callback manager for handling events (injected)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
dict: AI completion response containing the agent's reply
|
|
193
|
+
|
|
194
|
+
The agent follows this logic:
|
|
195
|
+
1. If the last message was a tool result, generate a final response without tools
|
|
196
|
+
2. Otherwise, generate a response with available tools for potential tool usage
|
|
197
|
+
"""
|
|
198
|
+
# System prompt defining the agent's role and capabilities
|
|
199
|
+
system_prompt = """
|
|
200
|
+
You are a helpful assistant.
|
|
201
|
+
Your task is to assist the user in finding information and answering questions.
|
|
202
|
+
You have access to various tools that can help you provide accurate information.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
# Convert state messages to the format expected by the AI model
|
|
206
|
+
messages = convert_messages(
|
|
207
|
+
system_prompts=[{"role": "system", "content": system_prompt}],
|
|
208
|
+
state=state,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Retrieve conversation history from checkpointer
|
|
212
|
+
try:
|
|
213
|
+
thread_messages = await checkpointer.aget_thread({"thread_id": config["thread_id"]})
|
|
214
|
+
logger.debug("Messages from checkpointer: %s", thread_messages)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning("Could not retrieve thread messages: %s", e)
|
|
217
|
+
thread_messages = []
|
|
218
|
+
|
|
219
|
+
# Log injected dependencies for debugging
|
|
220
|
+
logger.debug("Checkpointer in main_agent: %s", checkpointer)
|
|
221
|
+
logger.debug("CallbackManager in main_agent: %s", callback)
|
|
222
|
+
|
|
223
|
+
# Placeholder for MCP (Model Context Protocol) tools
|
|
224
|
+
# These would be additional tools from external sources
|
|
225
|
+
mcp_tools = []
|
|
226
|
+
is_stream = config.get("is_stream", False)
|
|
227
|
+
|
|
228
|
+
# Determine response strategy based on conversation context
|
|
229
|
+
if state.context and len(state.context) > 0 and state.context[-1].role == "tool":
|
|
230
|
+
# Last message was a tool result - generate final response without tools
|
|
231
|
+
logger.info("Generating final response after tool execution")
|
|
232
|
+
response = await acompletion(
|
|
233
|
+
model="gemini/gemini-2.0-flash-exp", # Updated model name
|
|
234
|
+
messages=messages,
|
|
235
|
+
stream=is_stream,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
# Regular response with tools available for potential usage
|
|
239
|
+
logger.info("Generating response with tools available")
|
|
240
|
+
tools = await tool_node.all_tools()
|
|
241
|
+
response = await acompletion(
|
|
242
|
+
model="gemini/gemini-2.0-flash-exp", # Updated model name
|
|
243
|
+
messages=messages,
|
|
244
|
+
tools=tools + mcp_tools,
|
|
245
|
+
stream=is_stream,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return ModelResponseConverter(
|
|
249
|
+
response,
|
|
250
|
+
converter="litellm",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def should_use_tools(state: AgentState) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Determine the next step in the graph execution based on the current state.
|
|
257
|
+
|
|
258
|
+
This routing function decides whether to continue with tool execution,
|
|
259
|
+
end the conversation, or proceed with the main agent logic.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
state: Current agent state containing the conversation context
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
str: Next node to execute ("TOOL" or END constant)
|
|
266
|
+
|
|
267
|
+
Routing Logic:
|
|
268
|
+
- If last message is from assistant and contains tool calls -> "TOOL"
|
|
269
|
+
- If last message is a tool result -> END (conversation complete)
|
|
270
|
+
- Otherwise -> END (default fallback)
|
|
271
|
+
"""
|
|
272
|
+
if not state.context or len(state.context) == 0:
|
|
273
|
+
return END
|
|
274
|
+
|
|
275
|
+
last_message = state.context[-1]
|
|
276
|
+
if not last_message:
|
|
277
|
+
return END
|
|
278
|
+
|
|
279
|
+
# Check if assistant wants to use tools
|
|
280
|
+
if (
|
|
281
|
+
hasattr(last_message, "tools_calls")
|
|
282
|
+
and last_message.tools_calls
|
|
283
|
+
and len(last_message.tools_calls) > 0
|
|
284
|
+
and last_message.role == "assistant"
|
|
285
|
+
):
|
|
286
|
+
logger.debug("Routing to TOOL node for tool execution")
|
|
287
|
+
return "TOOL"
|
|
288
|
+
|
|
289
|
+
# Check if we just received tool results
|
|
290
|
+
if last_message.role == "tool":
|
|
291
|
+
logger.info("Tool execution complete, ending conversation")
|
|
292
|
+
return END
|
|
293
|
+
|
|
294
|
+
# Default case: end conversation
|
|
295
|
+
logger.debug("Default routing: ending conversation")
|
|
296
|
+
return END
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Initialize the state graph for orchestrating agent flow
|
|
300
|
+
graph = StateGraph()
|
|
301
|
+
|
|
302
|
+
# Add nodes to the graph
|
|
303
|
+
graph.add_node("MAIN", main_agent) # Main agent processing node
|
|
304
|
+
graph.add_node("TOOL", tool_node) # Tool execution node
|
|
305
|
+
|
|
306
|
+
# Define conditional edges from MAIN node
|
|
307
|
+
# Routes to TOOL if tools should be used, otherwise ends
|
|
308
|
+
graph.add_conditional_edges(
|
|
309
|
+
"MAIN",
|
|
310
|
+
should_use_tools,
|
|
311
|
+
{"TOOL": "TOOL", END: END},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Define edge from TOOL back to MAIN for continued conversation
|
|
315
|
+
graph.add_edge("TOOL", "MAIN")
|
|
316
|
+
|
|
317
|
+
# Set the entry point for graph execution
|
|
318
|
+
graph.set_entry_point("MAIN")
|
|
319
|
+
|
|
320
|
+
# Compile the graph with checkpointer for state management
|
|
321
|
+
app = graph.compile(
|
|
322
|
+
checkpointer=checkpointer,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
'''
|
|
328
|
+
|
|
329
|
+
# Production templates (mirroring root repo tooling for convenience)
|
|
330
|
+
DEFAULT_PRE_COMMIT: Final[str] = """repos:
|
|
331
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
332
|
+
rev: v6.0.0
|
|
333
|
+
hooks:
|
|
334
|
+
- id: check-yaml
|
|
335
|
+
exclude: ^(tests|docs|examples)/
|
|
336
|
+
- id: trailing-whitespace
|
|
337
|
+
exclude: ^(tests|docs|examples)/
|
|
338
|
+
- id: check-added-large-files
|
|
339
|
+
args: [--maxkb=100]
|
|
340
|
+
exclude: ^(tests|docs|examples)/
|
|
341
|
+
- id: check-ast
|
|
342
|
+
exclude: ^(tests|docs|examples)/
|
|
343
|
+
- id: check-builtin-literals
|
|
344
|
+
exclude: ^(tests|docs|examples)/
|
|
345
|
+
- id: check-case-conflict
|
|
346
|
+
exclude: ^(tests|docs|examples)/
|
|
347
|
+
- id: check-docstring-first
|
|
348
|
+
exclude: ^(tests|docs|examples)/
|
|
349
|
+
- id: check-merge-conflict
|
|
350
|
+
exclude: ^(tests|docs|examples)/
|
|
351
|
+
- id: debug-statements
|
|
352
|
+
exclude: ^(tests|docs|examples)/
|
|
353
|
+
- id: detect-private-key
|
|
354
|
+
exclude: ^(tests|docs|examples)/
|
|
355
|
+
|
|
356
|
+
- repo: https://github.com/asottile/pyupgrade
|
|
357
|
+
rev: v3.17.0
|
|
358
|
+
hooks:
|
|
359
|
+
- id: pyupgrade
|
|
360
|
+
args: [--py310-plus]
|
|
361
|
+
exclude: ^(tests|docs|examples)/
|
|
362
|
+
|
|
363
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
364
|
+
rev: v0.5.7
|
|
365
|
+
hooks:
|
|
366
|
+
- id: ruff-format
|
|
367
|
+
exclude: ^(tests|docs|examples)/
|
|
368
|
+
|
|
369
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
370
|
+
rev: v0.5.7
|
|
371
|
+
hooks:
|
|
372
|
+
- id: ruff
|
|
373
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
374
|
+
exclude: ^(tests|docs|examples)/
|
|
375
|
+
|
|
376
|
+
- repo: https://github.com/PyCQA/bandit
|
|
377
|
+
rev: 1.7.9
|
|
378
|
+
hooks:
|
|
379
|
+
- id: bandit
|
|
380
|
+
args: [-c, pyproject.toml]
|
|
381
|
+
additional_dependencies: ["bandit[toml]"]
|
|
382
|
+
exclude: ^(tests|docs|examples)/
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
DEFAULT_PYPROJECT: Final[str] = """[build-system]
|
|
386
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
387
|
+
build-backend = "setuptools.build_meta"
|
|
388
|
+
|
|
389
|
+
[project]
|
|
390
|
+
name = "10xgraph-api-app"
|
|
391
|
+
version = "0.1.0"
|
|
392
|
+
description = "10xGraph API application"
|
|
393
|
+
readme = "README.md"
|
|
394
|
+
license = {text = "MIT"}
|
|
395
|
+
requires-python = ">=3.10"
|
|
396
|
+
authors = [
|
|
397
|
+
{name = "Your Name", email = "you@example.com"},
|
|
398
|
+
]
|
|
399
|
+
maintainers = [
|
|
400
|
+
{name = "Your Name", email = "you@example.com"},
|
|
401
|
+
]
|
|
402
|
+
keywords = ["10xgraph", "api", "fastapi", "cli"]
|
|
403
|
+
classifiers = [
|
|
404
|
+
"Development Status :: 4 - Beta",
|
|
405
|
+
"Intended Audience :: Developers",
|
|
406
|
+
"License :: OSI Approved :: MIT License",
|
|
407
|
+
"Operating System :: OS Independent",
|
|
408
|
+
"Programming Language :: Python :: 3",
|
|
409
|
+
"Programming Language :: Python :: 3.10",
|
|
410
|
+
"Programming Language :: Python :: 3.11",
|
|
411
|
+
"Programming Language :: Python :: 3.12",
|
|
412
|
+
"Programming Language :: Python :: 3.13",
|
|
413
|
+
]
|
|
414
|
+
dependencies = [
|
|
415
|
+
"10xgraph-api",
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
[project.scripts]
|
|
419
|
+
10xgraph = "tenxgraph_api.cli.main:main"
|
|
420
|
+
|
|
421
|
+
[tool.ruff]
|
|
422
|
+
line-length = 100
|
|
423
|
+
target-version = "py312"
|
|
424
|
+
lint.fixable = ["ALL"]
|
|
425
|
+
lint.select = [
|
|
426
|
+
"E", "W", "F", "PL", "I", "B", "A", "S", "ISC", "ICN", "PIE", "Q",
|
|
427
|
+
"RET", "SIM", "TID", "RUF", "YTT", "UP", "C4", "PTH", "G", "INP", "T20",
|
|
428
|
+
]
|
|
429
|
+
lint.ignore = [
|
|
430
|
+
"UP006", "UP007", "RUF012", "G004", "B904", "B008", "ISC001",
|
|
431
|
+
]
|
|
432
|
+
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
433
|
+
exclude = [
|
|
434
|
+
"venv/*",
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
[tool.ruff.lint.mccabe]
|
|
438
|
+
max-complexity = 10
|
|
439
|
+
|
|
440
|
+
[tool.ruff.lint.per-file-ignores]
|
|
441
|
+
"bin/*.py" = ["E402", "S603", "T201", "S101"]
|
|
442
|
+
"*/tests/*.py" = ["E402", "S603", "T201", "S101"]
|
|
443
|
+
"*/test/*.py" = ["E402", "S603", "T201", "S101"]
|
|
444
|
+
"scripts/*.py" = ["E402", "S603", "T201", "S101", "INP001"]
|
|
445
|
+
"*/__init__.py" = ["E402", "S603", "T201", "S101"]
|
|
446
|
+
"*/migrations/*.py" = ["E402", "S603", "T201", "S101"]
|
|
447
|
+
|
|
448
|
+
[tool.ruff.lint.isort]
|
|
449
|
+
lines-after-imports = 2
|
|
450
|
+
|
|
451
|
+
[tool.ruff.lint.pylint]
|
|
452
|
+
max-args = 10
|
|
453
|
+
|
|
454
|
+
[tool.ruff.format]
|
|
455
|
+
quote-style = "double"
|
|
456
|
+
indent-style = "space"
|
|
457
|
+
skip-magic-trailing-comma = false
|
|
458
|
+
line-ending = "auto"
|
|
459
|
+
docstring-code-format = true
|
|
460
|
+
|
|
461
|
+
[tool.ruff.lint.pydocstyle]
|
|
462
|
+
convention = "google"
|
|
463
|
+
|
|
464
|
+
[tool.bandit]
|
|
465
|
+
exclude_dirs = ["*/tests/*", "*/tenxgraph_api/tests/*"]
|
|
466
|
+
skips = ["B101", "B611", "B601", "B608"]
|
|
467
|
+
|
|
468
|
+
[tool.pytest.ini_options]
|
|
469
|
+
env = ["ENVIRONMENT=pytest"]
|
|
470
|
+
testpaths = ["tests"]
|
|
471
|
+
pythonpath = ["."]
|
|
472
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
473
|
+
addopts = [
|
|
474
|
+
"--cov=tenxgraph_api", "--cov-report=html", "--cov-report=term-missing",
|
|
475
|
+
"--cov-report=xml", "--cov-fail-under=0", "--strict-markers", "-v"
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
[tool.coverage.run]
|
|
479
|
+
source = ["tenxgraph_api"]
|
|
480
|
+
branch = true
|
|
481
|
+
omit = [
|
|
482
|
+
"*/__init__.py", "*/tests/*", "*/migrations/*", "*/scripts/*", "*/venv/*", "*/.venv/*",
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
[tool.coverage.report]
|
|
486
|
+
exclude_lines = [
|
|
487
|
+
"if __name__ == '__main__':", "pragma: no cover", "@abc.abstractmethod", "@abstractmethod",
|
|
488
|
+
"raise NotImplementedError",
|
|
489
|
+
]
|
|
490
|
+
show_missing = true
|
|
491
|
+
|
|
492
|
+
[tool.coverage.paths]
|
|
493
|
+
source = ["tenxgraph_api", "*/site-packages/tenxgraph_api"]
|
|
494
|
+
|
|
495
|
+
[tool.pytest-env]
|
|
496
|
+
ENVIRONMENT = "pytest"
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# Docker templates
|
|
501
|
+
# The published name on PyPI. The bare name "10xgraph-api" belongs to someone else.
|
|
502
|
+
CLI_PACKAGE = "10xgraph-api"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# Directories under ``tenxgraph_api/cli`` that only the CLI uses on a developer machine.
|
|
506
|
+
# The server imports nothing from them (it needs ``cli/constants.py`` only).
|
|
507
|
+
CLI_ONLY_ASSET_DIRS: Final[tuple[str, ...]] = ("templates", "config_editor")
|
|
508
|
+
|
|
509
|
+
# One-liner run in the image after install. ``find_spec`` locates the package without
|
|
510
|
+
# importing it, and a missing package is skipped rather than failing the build. Run with
|
|
511
|
+
# ``python -I`` so only the installed copy in site-packages can match, never a checkout on
|
|
512
|
+
# the working directory or PYTHONPATH.
|
|
513
|
+
_STRIP_CLI_ASSETS = (
|
|
514
|
+
"import importlib.util, pathlib, shutil; "
|
|
515
|
+
"spec = importlib.util.find_spec('tenxgraph_api'); "
|
|
516
|
+
"cli = pathlib.Path(spec.origin).parent / 'cli' if spec and spec.origin else None; "
|
|
517
|
+
f"[shutil.rmtree(cli / d, ignore_errors=True) for d in {CLI_ONLY_ASSET_DIRS!r} if cli]"
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def generate_dockerfile_content(
|
|
522
|
+
python_version: str,
|
|
523
|
+
port: int,
|
|
524
|
+
requirements_file: str,
|
|
525
|
+
has_requirements: bool,
|
|
526
|
+
omit_cmd: bool = False,
|
|
527
|
+
has_pyproject: bool = False,
|
|
528
|
+
) -> str:
|
|
529
|
+
"""Generate the content for the Dockerfile.
|
|
530
|
+
|
|
531
|
+
Dependencies come from ``requirements_file`` when there is one, otherwise from the
|
|
532
|
+
``[project].dependencies`` of ``pyproject.toml`` (what ``10xgraph init`` scaffolds), and
|
|
533
|
+
only as a last resort from the published CLI package alone.
|
|
534
|
+
"""
|
|
535
|
+
dockerfile_lines = [
|
|
536
|
+
"# Dockerfile for 10xGraph API",
|
|
537
|
+
"# Generated by 10xgraph-api CLI",
|
|
538
|
+
"",
|
|
539
|
+
f"FROM python:{python_version}-slim",
|
|
540
|
+
"",
|
|
541
|
+
"# Set environment variables",
|
|
542
|
+
"ENV PYTHONDONTWRITEBYTECODE=1",
|
|
543
|
+
"ENV PYTHONUNBUFFERED=1",
|
|
544
|
+
"ENV PYTHONPATH=/app",
|
|
545
|
+
"# Default to production; overridable via .env or runtime env.",
|
|
546
|
+
"# In production the local in-memory telemetry store stays disabled",
|
|
547
|
+
"# (use OTEL / a publisher for observability instead).",
|
|
548
|
+
"ENV MODE=production",
|
|
549
|
+
"ENV IS_DEBUG=false",
|
|
550
|
+
"# Gunicorn worker count. Gunicorn reads WEB_CONCURRENCY natively; a sane default",
|
|
551
|
+
"# here beats gunicorn's built-in default of 1 worker. Tune to your CPU/memory",
|
|
552
|
+
"# at deploy time, e.g. `docker run -e WEB_CONCURRENCY=8 ...`.",
|
|
553
|
+
"ENV WEB_CONCURRENCY=2",
|
|
554
|
+
"",
|
|
555
|
+
"# Set work directory",
|
|
556
|
+
"WORKDIR /app",
|
|
557
|
+
"",
|
|
558
|
+
"# Install system dependencies",
|
|
559
|
+
"RUN apt-get update \\",
|
|
560
|
+
" && apt-get install -y --no-install-recommends \\",
|
|
561
|
+
" build-essential \\",
|
|
562
|
+
" curl \\",
|
|
563
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
564
|
+
"",
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
if has_requirements:
|
|
568
|
+
dockerfile_lines.extend(
|
|
569
|
+
[
|
|
570
|
+
"# Install Python dependencies",
|
|
571
|
+
f"COPY {requirements_file} .",
|
|
572
|
+
"RUN pip install --no-cache-dir --upgrade pip \\",
|
|
573
|
+
f" && pip install --no-cache-dir -r {requirements_file} \\",
|
|
574
|
+
" && pip install --no-cache-dir gunicorn uvicorn",
|
|
575
|
+
"",
|
|
576
|
+
]
|
|
577
|
+
)
|
|
578
|
+
elif has_pyproject:
|
|
579
|
+
dockerfile_lines.extend(
|
|
580
|
+
[
|
|
581
|
+
"# Install the project's dependencies from pyproject.toml",
|
|
582
|
+
"COPY pyproject.toml .",
|
|
583
|
+
"RUN python -c \"import tomllib; deps = tomllib.load(open('pyproject.toml', 'rb'))"
|
|
584
|
+
"['project'].get('dependencies', []); print('\\\\n'.join(deps))\" "
|
|
585
|
+
"> /tmp/requirements.txt \\",
|
|
586
|
+
" && pip install --no-cache-dir --upgrade pip \\",
|
|
587
|
+
" && pip install --no-cache-dir -r /tmp/requirements.txt \\",
|
|
588
|
+
f" && pip install --no-cache-dir {CLI_PACKAGE} gunicorn uvicorn",
|
|
589
|
+
"",
|
|
590
|
+
]
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
dockerfile_lines.extend(
|
|
594
|
+
[
|
|
595
|
+
"# No requirements.txt or pyproject.toml: install the 10xGraph CLI only",
|
|
596
|
+
"RUN pip install --no-cache-dir --upgrade pip \\",
|
|
597
|
+
f" && pip install --no-cache-dir {CLI_PACKAGE} \\",
|
|
598
|
+
" && pip install --no-cache-dir gunicorn uvicorn",
|
|
599
|
+
"",
|
|
600
|
+
]
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
dockerfile_lines.extend(
|
|
604
|
+
[
|
|
605
|
+
"# Strip CLI-only assets the server never loads: project scaffolds, the bundled",
|
|
606
|
+
"# agent skill, and the local config editor (HTML/JS). Only the server code and",
|
|
607
|
+
"# its dependencies stay in the image.",
|
|
608
|
+
f'RUN python -I -c "{_STRIP_CLI_ASSETS}"',
|
|
609
|
+
"",
|
|
610
|
+
"# Copy application code",
|
|
611
|
+
"COPY . .",
|
|
612
|
+
"",
|
|
613
|
+
"# Create a non-root user",
|
|
614
|
+
"RUN groupadd -r appuser && useradd -r -g appuser appuser \\",
|
|
615
|
+
" && chown -R appuser:appuser /app",
|
|
616
|
+
"USER appuser",
|
|
617
|
+
"",
|
|
618
|
+
"# Expose port",
|
|
619
|
+
f"EXPOSE {port}",
|
|
620
|
+
"",
|
|
621
|
+
"# Health check",
|
|
622
|
+
"HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \\",
|
|
623
|
+
f" CMD curl -f http://localhost:{port}/ping || exit 1",
|
|
624
|
+
"",
|
|
625
|
+
]
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if not omit_cmd:
|
|
629
|
+
dockerfile_lines.extend(
|
|
630
|
+
[
|
|
631
|
+
"# Run the application (production)",
|
|
632
|
+
"# Use Gunicorn with Uvicorn workers for better performance and multi-core",
|
|
633
|
+
"# utilization",
|
|
634
|
+
"#",
|
|
635
|
+
"# --graceful-timeout is critical: an agent run can legitimately take",
|
|
636
|
+
"# minutes (the LLM client alone allows up to 600s). Gunicorn's default",
|
|
637
|
+
"# graceful timeout is 30s, so on every rolling deploy SIGTERM would",
|
|
638
|
+
"# hard-kill runs that were still mid-flight, truncating them. Give",
|
|
639
|
+
"# in-flight runs time to finish draining instead.",
|
|
640
|
+
(
|
|
641
|
+
'CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", '
|
|
642
|
+
f'"-b", "0.0.0.0:{port}", '
|
|
643
|
+
f'"--graceful-timeout", "{GRACEFUL_TIMEOUT_SECONDS}", '
|
|
644
|
+
f'"--timeout", "{WORKER_TIMEOUT_SECONDS}", '
|
|
645
|
+
'"tenxgraph_api.src.app.main:app"]'
|
|
646
|
+
),
|
|
647
|
+
"",
|
|
648
|
+
]
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return "\n".join(dockerfile_lines)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def generate_k8s_manifest_content(service_name: str, port: int) -> str:
|
|
655
|
+
"""Generate a Kubernetes Deployment + Service manifest.
|
|
656
|
+
|
|
657
|
+
Exists because rolling deploys were the main way in-flight agent runs got
|
|
658
|
+
truncated, and no manifest was generated at all -- so every user hand-rolled
|
|
659
|
+
one, typically with the default 30s grace period that kills a run mid-LLM-call.
|
|
660
|
+
|
|
661
|
+
The three settings that matter here:
|
|
662
|
+
|
|
663
|
+
- ``terminationGracePeriodSeconds`` must exceed the app's own graceful timeout,
|
|
664
|
+
or the kubelet SIGKILLs the pod while it is still draining.
|
|
665
|
+
- The ``preStop`` sleep gives the load balancer time to notice the pod is
|
|
666
|
+
terminating and stop routing NEW requests to it. Without it, Kubernetes sends
|
|
667
|
+
SIGTERM and removes the endpoint concurrently, so requests can still arrive
|
|
668
|
+
at a pod that has already begun shutting down.
|
|
669
|
+
- The readiness probe is what takes the pod out of rotation; the liveness probe
|
|
670
|
+
must be slack enough that a busy worker is not restarted mid-run.
|
|
671
|
+
"""
|
|
672
|
+
return "\n".join(
|
|
673
|
+
[
|
|
674
|
+
"apiVersion: apps/v1",
|
|
675
|
+
"kind: Deployment",
|
|
676
|
+
"metadata:",
|
|
677
|
+
f" name: {service_name}",
|
|
678
|
+
"spec:",
|
|
679
|
+
" replicas: 2",
|
|
680
|
+
" selector:",
|
|
681
|
+
" matchLabels:",
|
|
682
|
+
f" app: {service_name}",
|
|
683
|
+
" template:",
|
|
684
|
+
" metadata:",
|
|
685
|
+
" labels:",
|
|
686
|
+
f" app: {service_name}",
|
|
687
|
+
" spec:",
|
|
688
|
+
# Must be > the app's graceful timeout, else the kubelet SIGKILLs a
|
|
689
|
+
# pod that is still finishing a run.
|
|
690
|
+
f" terminationGracePeriodSeconds: {K8S_TERMINATION_GRACE_SECONDS}",
|
|
691
|
+
" containers:",
|
|
692
|
+
f" - name: {service_name}",
|
|
693
|
+
" image: 10xgraph-api:latest",
|
|
694
|
+
" ports:",
|
|
695
|
+
f" - containerPort: {port}",
|
|
696
|
+
" env:",
|
|
697
|
+
" - name: MODE",
|
|
698
|
+
' value: "production"',
|
|
699
|
+
" - name: IS_DEBUG",
|
|
700
|
+
' value: "false"',
|
|
701
|
+
" # CORS: production refuses wildcard origins with credentials.",
|
|
702
|
+
" - name: ORIGINS",
|
|
703
|
+
' value: "https://your-frontend.example.com"',
|
|
704
|
+
" lifecycle:",
|
|
705
|
+
" preStop:",
|
|
706
|
+
" exec:",
|
|
707
|
+
" # Let the load balancer stop sending new traffic before",
|
|
708
|
+
" # the app starts shutting down.",
|
|
709
|
+
" command:",
|
|
710
|
+
' - "/bin/sh"',
|
|
711
|
+
' - "-c"',
|
|
712
|
+
f' - "sleep {K8S_PRESTOP_SLEEP_SECONDS}"',
|
|
713
|
+
" readinessProbe:",
|
|
714
|
+
" httpGet:",
|
|
715
|
+
" path: /ping",
|
|
716
|
+
f" port: {port}",
|
|
717
|
+
" initialDelaySeconds: 5",
|
|
718
|
+
" periodSeconds: 10",
|
|
719
|
+
" livenessProbe:",
|
|
720
|
+
" httpGet:",
|
|
721
|
+
" path: /ping",
|
|
722
|
+
f" port: {port}",
|
|
723
|
+
" # Deliberately slack: a worker busy with a long agent run",
|
|
724
|
+
" # must not be mistaken for a hung one and restarted.",
|
|
725
|
+
" initialDelaySeconds: 30",
|
|
726
|
+
" periodSeconds: 30",
|
|
727
|
+
" failureThreshold: 5",
|
|
728
|
+
" resources:",
|
|
729
|
+
" requests:",
|
|
730
|
+
' cpu: "500m"',
|
|
731
|
+
' memory: "512Mi"',
|
|
732
|
+
" limits:",
|
|
733
|
+
' cpu: "2"',
|
|
734
|
+
' memory: "2Gi"',
|
|
735
|
+
"---",
|
|
736
|
+
"apiVersion: v1",
|
|
737
|
+
"kind: Service",
|
|
738
|
+
"metadata:",
|
|
739
|
+
f" name: {service_name}",
|
|
740
|
+
"spec:",
|
|
741
|
+
" selector:",
|
|
742
|
+
f" app: {service_name}",
|
|
743
|
+
" ports:",
|
|
744
|
+
" - protocol: TCP",
|
|
745
|
+
f" port: 80",
|
|
746
|
+
f" targetPort: {port}",
|
|
747
|
+
"",
|
|
748
|
+
]
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def generate_docker_compose_content(service_name: str, port: int) -> str:
|
|
753
|
+
"""Generate a simple docker-compose.yml content for the API service."""
|
|
754
|
+
return "\n".join(
|
|
755
|
+
[
|
|
756
|
+
"services:",
|
|
757
|
+
f" {service_name}:",
|
|
758
|
+
" build: .",
|
|
759
|
+
" image: 10xgraph-api:latest",
|
|
760
|
+
" environment:",
|
|
761
|
+
" - PYTHONUNBUFFERED=1",
|
|
762
|
+
" - PYTHONDONTWRITEBYTECODE=1",
|
|
763
|
+
# Default to production; local in-memory telemetry stays disabled.
|
|
764
|
+
" - MODE=production",
|
|
765
|
+
" - IS_DEBUG=false",
|
|
766
|
+
" ports:",
|
|
767
|
+
f" - '{port}:{port}'",
|
|
768
|
+
(
|
|
769
|
+
f" command: [ 'gunicorn', '-k', 'uvicorn.workers.UvicornWorker', "
|
|
770
|
+
f"'-b', '0.0.0.0:{port}', "
|
|
771
|
+
f"'--graceful-timeout', '{GRACEFUL_TIMEOUT_SECONDS}', "
|
|
772
|
+
f"'--timeout', '{WORKER_TIMEOUT_SECONDS}', "
|
|
773
|
+
"'tenxgraph_api.src.app.main:app' ]"
|
|
774
|
+
),
|
|
775
|
+
# Give in-flight agent runs time to drain on `docker compose down`
|
|
776
|
+
# instead of being killed after the default 10s.
|
|
777
|
+
f" stop_grace_period: {GRACEFUL_TIMEOUT_SECONDS}s",
|
|
778
|
+
" restart: unless-stopped",
|
|
779
|
+
" # Consider adding resource limits and deploy configurations in a swarm/stack",
|
|
780
|
+
" # deploy:",
|
|
781
|
+
" # replicas: 2",
|
|
782
|
+
" # resources:",
|
|
783
|
+
" # limits:",
|
|
784
|
+
" # cpus: '1.0'",
|
|
785
|
+
" # memory: 512M",
|
|
786
|
+
]
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def generate_dockerignore_content() -> str:
|
|
791
|
+
"""Generate a standard .dockerignore file content."""
|
|
792
|
+
return "\n".join(
|
|
793
|
+
[
|
|
794
|
+
"# Exclude environment secrets and local configs",
|
|
795
|
+
".env",
|
|
796
|
+
".env.*",
|
|
797
|
+
"secrets/",
|
|
798
|
+
"configs/",
|
|
799
|
+
"*.env",
|
|
800
|
+
"",
|
|
801
|
+
"# Exclude Python artifacts and caches",
|
|
802
|
+
"__pycache__/",
|
|
803
|
+
"*.py[cod]",
|
|
804
|
+
"*$py.class",
|
|
805
|
+
".pytest_cache/",
|
|
806
|
+
".coverage",
|
|
807
|
+
"htmlcov/",
|
|
808
|
+
".mypy_cache/",
|
|
809
|
+
".ruff_cache/",
|
|
810
|
+
"",
|
|
811
|
+
"# Exclude virtual environments",
|
|
812
|
+
".venv/",
|
|
813
|
+
"venv/",
|
|
814
|
+
"env/",
|
|
815
|
+
"",
|
|
816
|
+
"# Exclude version control and IDE files",
|
|
817
|
+
".git/",
|
|
818
|
+
".gitignore",
|
|
819
|
+
".idea/",
|
|
820
|
+
".vscode/",
|
|
821
|
+
"*.swp",
|
|
822
|
+
"*.swo",
|
|
823
|
+
"",
|
|
824
|
+
"# Exclude built assets and distribution files",
|
|
825
|
+
"build/",
|
|
826
|
+
"dist/",
|
|
827
|
+
"*.egg-info/",
|
|
828
|
+
"",
|
|
829
|
+
"# Exclude local data directories",
|
|
830
|
+
"data/",
|
|
831
|
+
"db.sqlite3",
|
|
832
|
+
"store/",
|
|
833
|
+
"uploads/",
|
|
834
|
+
"eval_reports/",
|
|
835
|
+
"",
|
|
836
|
+
"# Exclude dev-only project folders: tests, evals, and installed agent skills",
|
|
837
|
+
"tests/",
|
|
838
|
+
"evals/",
|
|
839
|
+
".claude/",
|
|
840
|
+
".agents/",
|
|
841
|
+
".github/",
|
|
842
|
+
"",
|
|
843
|
+
]
|
|
844
|
+
)
|