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,262 @@
|
|
|
1
|
+
"""API server command implementation."""
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
|
|
14
|
+
import uvicorn
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
from tenxgraph_api.cli.commands import BaseCommand
|
|
18
|
+
from tenxgraph_api.cli.constants import (
|
|
19
|
+
DEFAULT_CONFIG_FILE,
|
|
20
|
+
DEFAULT_HOST,
|
|
21
|
+
DEFAULT_PLAYGROUND_URL,
|
|
22
|
+
DEFAULT_PORT,
|
|
23
|
+
LEGACY_CONFIG_FILE,
|
|
24
|
+
)
|
|
25
|
+
from tenxgraph_api.cli.core.config import ConfigManager
|
|
26
|
+
from tenxgraph_api.cli.core.validation import validate_cli_options
|
|
27
|
+
from tenxgraph_api.cli.exceptions import ConfigurationError, ServerError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class APICommand(BaseCommand):
|
|
31
|
+
"""Command to start the 10xGraph API server."""
|
|
32
|
+
|
|
33
|
+
_PLAYGROUND_WAIT_TIMEOUT_SECONDS = 30.0
|
|
34
|
+
_PLAYGROUND_WAIT_INTERVAL_SECONDS = 0.25
|
|
35
|
+
|
|
36
|
+
def execute(
|
|
37
|
+
self,
|
|
38
|
+
config: str = DEFAULT_CONFIG_FILE,
|
|
39
|
+
host: str = DEFAULT_HOST,
|
|
40
|
+
port: int = DEFAULT_PORT,
|
|
41
|
+
reload: bool = True,
|
|
42
|
+
open_playground: bool = False,
|
|
43
|
+
playground_url: str = DEFAULT_PLAYGROUND_URL,
|
|
44
|
+
**kwargs: Any,
|
|
45
|
+
) -> int:
|
|
46
|
+
"""Execute the API server command.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config: Path to config file
|
|
50
|
+
host: Host to bind to
|
|
51
|
+
port: Port to bind to
|
|
52
|
+
reload: Enable auto-reload
|
|
53
|
+
open_playground: Open the hosted playground after the API becomes reachable
|
|
54
|
+
playground_url: Hosted playground base URL
|
|
55
|
+
**kwargs: Additional arguments
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Exit code
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
self.output.command_header(
|
|
62
|
+
"play" if open_playground else "api",
|
|
63
|
+
"Starting development server via Uvicorn. Not for production use.",
|
|
64
|
+
hint="Ctrl+C to stop the development server",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
timeline = self.output.timeline(
|
|
68
|
+
"Preparing the 10xGraph runtime",
|
|
69
|
+
steps=(
|
|
70
|
+
("config", "Validating project configuration"),
|
|
71
|
+
("runtime", "Loading environment and graph runtime"),
|
|
72
|
+
("port", f"Reserving port {port}"),
|
|
73
|
+
("launch", "Starting the development server"),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
with timeline:
|
|
77
|
+
with timeline.step("config") as step:
|
|
78
|
+
validated_options = validate_cli_options(host, port, config)
|
|
79
|
+
config_manager = ConfigManager()
|
|
80
|
+
actual_config_path = config_manager.find_config_file(
|
|
81
|
+
validated_options["config"]
|
|
82
|
+
)
|
|
83
|
+
config_manager.load_config(str(actual_config_path))
|
|
84
|
+
detail = str(actual_config_path)
|
|
85
|
+
if actual_config_path.name == LEGACY_CONFIG_FILE:
|
|
86
|
+
detail += f" (deprecated name; rename it to {DEFAULT_CONFIG_FILE})"
|
|
87
|
+
step.detail(detail)
|
|
88
|
+
|
|
89
|
+
with timeline.step("runtime") as step:
|
|
90
|
+
env_file_path = config_manager.resolve_env_file()
|
|
91
|
+
if env_file_path:
|
|
92
|
+
self.logger.info("Loading environment from: %s", env_file_path)
|
|
93
|
+
load_dotenv(env_file_path)
|
|
94
|
+
step.detail(f"environment: {env_file_path}")
|
|
95
|
+
else:
|
|
96
|
+
load_dotenv()
|
|
97
|
+
step.detail("environment: process defaults")
|
|
98
|
+
|
|
99
|
+
os.environ["GRAPH_PATH"] = str(actual_config_path)
|
|
100
|
+
|
|
101
|
+
# Add project root to sys.path for importing graph modules
|
|
102
|
+
sys.path.insert(0, str(actual_config_path.parent))
|
|
103
|
+
|
|
104
|
+
# Ensure we're using the correct module path
|
|
105
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
106
|
+
|
|
107
|
+
browser_host = self._normalize_browser_host(validated_options["host"])
|
|
108
|
+
with timeline.step("port") as step:
|
|
109
|
+
# Reported rather than raised: Uvicorn owns the bind, and its
|
|
110
|
+
# own error is the authoritative one if the port is taken.
|
|
111
|
+
if self._port_in_use(browser_host, validated_options["port"]):
|
|
112
|
+
step.detail("already in use — Uvicorn will report the conflict")
|
|
113
|
+
else:
|
|
114
|
+
step.detail("available")
|
|
115
|
+
|
|
116
|
+
with timeline.step("launch") as step:
|
|
117
|
+
self.logger.info(
|
|
118
|
+
"Starting API with config: %s, host: %s, port: %d",
|
|
119
|
+
actual_config_path,
|
|
120
|
+
validated_options["host"],
|
|
121
|
+
validated_options["port"],
|
|
122
|
+
)
|
|
123
|
+
launch_url = None
|
|
124
|
+
if open_playground:
|
|
125
|
+
launch_url = self._schedule_playground_launch(
|
|
126
|
+
host=validated_options["host"],
|
|
127
|
+
port=validated_options["port"],
|
|
128
|
+
playground_base_url=playground_url,
|
|
129
|
+
)
|
|
130
|
+
step.detail(
|
|
131
|
+
f"playground opens at {launch_url} once the API responds"
|
|
132
|
+
if launch_url
|
|
133
|
+
else f"http://{browser_host}:{validated_options['port']}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.output.completion_screen(
|
|
137
|
+
"Ready to serve",
|
|
138
|
+
"10xGraph runtime configured successfully",
|
|
139
|
+
details={
|
|
140
|
+
"API": f"http://{browser_host}:{validated_options['port']}",
|
|
141
|
+
"Docs": f"http://{browser_host}:{validated_options['port']}/docs",
|
|
142
|
+
"Config": actual_config_path,
|
|
143
|
+
"Reload": "enabled" if reload else "disabled",
|
|
144
|
+
},
|
|
145
|
+
next_steps=(
|
|
146
|
+
["The playground will open automatically when the API becomes reachable."]
|
|
147
|
+
if open_playground
|
|
148
|
+
else ["Press Ctrl+C to stop the development server."]
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
uvicorn.run(
|
|
153
|
+
"tenxgraph_api.src.app.main:app",
|
|
154
|
+
host=validated_options["host"],
|
|
155
|
+
port=validated_options["port"],
|
|
156
|
+
reload=reload,
|
|
157
|
+
workers=1,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
except (ConfigurationError, ServerError) as e:
|
|
163
|
+
return self.handle_error(e)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
server_error = ServerError(
|
|
166
|
+
f"Failed to start API server: {e}",
|
|
167
|
+
host=host,
|
|
168
|
+
port=port,
|
|
169
|
+
)
|
|
170
|
+
return self.handle_error(server_error)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _port_in_use(host: str, port: int) -> bool:
|
|
174
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
175
|
+
probe.settimeout(0.2)
|
|
176
|
+
return probe.connect_ex((host, port)) == 0
|
|
177
|
+
|
|
178
|
+
def _schedule_playground_launch(
|
|
179
|
+
self,
|
|
180
|
+
host: str,
|
|
181
|
+
port: int,
|
|
182
|
+
playground_base_url: str,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Start the readiness watcher and return the URL it will open."""
|
|
185
|
+
browser_host = self._normalize_browser_host(host)
|
|
186
|
+
launch_url = self._build_playground_url(browser_host, port, playground_base_url)
|
|
187
|
+
launch_thread = threading.Thread(
|
|
188
|
+
target=self._open_playground_when_ready,
|
|
189
|
+
args=(launch_url, browser_host, port),
|
|
190
|
+
daemon=True,
|
|
191
|
+
name="10xgraph-playground-launcher",
|
|
192
|
+
)
|
|
193
|
+
launch_thread.start()
|
|
194
|
+
return launch_url
|
|
195
|
+
|
|
196
|
+
def _open_playground_when_ready(
|
|
197
|
+
self,
|
|
198
|
+
launch_url: str,
|
|
199
|
+
host: str,
|
|
200
|
+
port: int,
|
|
201
|
+
) -> None:
|
|
202
|
+
if not self._wait_for_server(host, port):
|
|
203
|
+
self.logger.warning(
|
|
204
|
+
"API server did not become reachable in time. Open the playground manually: %s",
|
|
205
|
+
launch_url,
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
opened = webbrowser.open_new_tab(launch_url)
|
|
210
|
+
if opened:
|
|
211
|
+
self.logger.info("Opened playground URL: %s", launch_url)
|
|
212
|
+
# A plain line, not a panel: this fires from the watcher thread while
|
|
213
|
+
# Uvicorn is already streaming logs, and a boxed reveal there would
|
|
214
|
+
# interleave with them.
|
|
215
|
+
self.output.success(f"Playground opened: {launch_url}")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
message = f"Browser launch returned false. Open the playground manually: {launch_url}"
|
|
219
|
+
self.logger.warning(message)
|
|
220
|
+
self.output.warning(message)
|
|
221
|
+
|
|
222
|
+
def _wait_for_server(
|
|
223
|
+
self,
|
|
224
|
+
host: str,
|
|
225
|
+
port: int,
|
|
226
|
+
) -> bool:
|
|
227
|
+
deadline = time.monotonic() + self._PLAYGROUND_WAIT_TIMEOUT_SECONDS
|
|
228
|
+
while time.monotonic() < deadline:
|
|
229
|
+
try:
|
|
230
|
+
with socket.create_connection((host, port), timeout=1):
|
|
231
|
+
return True
|
|
232
|
+
except OSError:
|
|
233
|
+
time.sleep(self._PLAYGROUND_WAIT_INTERVAL_SECONDS)
|
|
234
|
+
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
def _build_playground_url(
|
|
238
|
+
self,
|
|
239
|
+
host: str,
|
|
240
|
+
port: int,
|
|
241
|
+
playground_base_url: str,
|
|
242
|
+
) -> str:
|
|
243
|
+
backend_host = host
|
|
244
|
+
if ":" in backend_host and not backend_host.startswith("["):
|
|
245
|
+
backend_host = f"[{backend_host}]"
|
|
246
|
+
|
|
247
|
+
backend_url = f"http://{backend_host}:{port}"
|
|
248
|
+
return f"{playground_base_url}?{urlencode({'backendUrl': backend_url})}"
|
|
249
|
+
|
|
250
|
+
def _normalize_browser_host(self, host: str) -> str:
|
|
251
|
+
if not host:
|
|
252
|
+
return "127.0.0.1"
|
|
253
|
+
|
|
254
|
+
normalized_host = host[1:-1] if host.startswith("[") and host.endswith("]") else host
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
if ipaddress.ip_address(normalized_host).is_unspecified:
|
|
258
|
+
return "127.0.0.1"
|
|
259
|
+
except ValueError:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
return normalized_host
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Environment and project audit for the 10xGraph CLI.
|
|
2
|
+
|
|
3
|
+
Backs ``10xgraph audit``: a read-only pass over everything that has to be true
|
|
4
|
+
before ``10xgraph dev``, ``eval``, or ``build`` can work in the current
|
|
5
|
+
directory. Nothing here mutates the project, the environment, or the user
|
|
6
|
+
configuration, so it is always safe to run.
|
|
7
|
+
|
|
8
|
+
Each check returns a :class:`Diagnostic` with one of three statuses:
|
|
9
|
+
|
|
10
|
+
``pass``
|
|
11
|
+
The requirement is satisfied.
|
|
12
|
+
``warn``
|
|
13
|
+
Not fatal for every workflow, but likely to surprise you. A missing
|
|
14
|
+
``10xgraph.json`` is fine until you run ``10xgraph dev``; a busy port is
|
|
15
|
+
fine until you try to bind it.
|
|
16
|
+
``fail``
|
|
17
|
+
A hard requirement is unmet and commands that depend on it will not run.
|
|
18
|
+
|
|
19
|
+
The command's exit code is the aggregate of those statuses (see
|
|
20
|
+
:meth:`AuditCommand.execute`), which is what makes it usable as a CI gate.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import importlib
|
|
26
|
+
import json
|
|
27
|
+
import socket
|
|
28
|
+
import sys
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from tenxgraph_api.cli.commands import BaseCommand
|
|
36
|
+
from tenxgraph_api.cli.constants import DEFAULT_CONFIG_FILE, DEFAULT_PORT
|
|
37
|
+
from tenxgraph_api.cli.core.config import resolve_default_config
|
|
38
|
+
from tenxgraph_api.src.app.core.config.graph_config import validate_remote_tools
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Diagnostic:
|
|
43
|
+
"""One audit result.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
name: Human-readable label for the thing that was checked.
|
|
47
|
+
status: One of ``"pass"``, ``"warn"``, or ``"fail"``.
|
|
48
|
+
detail: Supporting evidence — a version, a path, or an error message.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
status: str
|
|
53
|
+
detail: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AuditCommand(BaseCommand):
|
|
57
|
+
"""Audit the local CLI, core package, project config, and default port.
|
|
58
|
+
|
|
59
|
+
Runs six checks in a fixed order and reports them twice: live, through the
|
|
60
|
+
step timeline, and again as a summary table once every check has finished.
|
|
61
|
+
|
|
62
|
+
============ ==========================================================
|
|
63
|
+
Check What it asserts
|
|
64
|
+
============ ==========================================================
|
|
65
|
+
python The interpreter running the CLI (reported, never failed).
|
|
66
|
+
cli ``10xgraph-api`` is installed and resolvable.
|
|
67
|
+
core ``10xgraph`` is installed and resolvable.
|
|
68
|
+
evaluation The installed core exposes the evaluation symbols that
|
|
69
|
+
``10xgraph eval`` imports, catching a CLI/core version
|
|
70
|
+
skew before it becomes an ImportError mid-run.
|
|
71
|
+
config ``10xgraph.json`` exists here, parses, declares an
|
|
72
|
+
``agent`` key in ``module:attribute`` form, and contains
|
|
73
|
+
valid ``remote_tools`` definitions if present.
|
|
74
|
+
port The default API port is free to bind.
|
|
75
|
+
============ ==========================================================
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def execute(self, *, config: str = DEFAULT_CONFIG_FILE, **kwargs: Any) -> int:
|
|
79
|
+
"""Run every check and report the outcome.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
``1`` if any check failed, otherwise ``0`` — warnings are surfaced
|
|
83
|
+
but do not fail the run, so ``10xgraph audit`` can gate CI on
|
|
84
|
+
hard breakage without tripping on an absent project config.
|
|
85
|
+
"""
|
|
86
|
+
self.output.command_header(
|
|
87
|
+
"audit",
|
|
88
|
+
"Auditing the current 10xGraph development environment.",
|
|
89
|
+
color="cyan",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
checks: tuple[tuple[str, str, Callable[[], Diagnostic]], ...] = (
|
|
93
|
+
(
|
|
94
|
+
"python",
|
|
95
|
+
"Python interpreter",
|
|
96
|
+
lambda: Diagnostic("Python", "pass", sys.version.split()[0]),
|
|
97
|
+
),
|
|
98
|
+
("cli", "CLI package", lambda: self._package_check("10xgraph-api")),
|
|
99
|
+
("core", "Core framework", lambda: self._package_check("10xgraph")),
|
|
100
|
+
("evaluation", "Evaluation API", self._evaluation_api_check),
|
|
101
|
+
("config", "Project configuration", lambda: self._config_check(config)),
|
|
102
|
+
("port", f"Port {DEFAULT_PORT}", lambda: self._port_check(DEFAULT_PORT)),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
diagnostics: list[Diagnostic] = []
|
|
106
|
+
timeline = self.output.timeline(
|
|
107
|
+
"Running environment audit",
|
|
108
|
+
steps=tuple((key, title) for key, title, _ in checks),
|
|
109
|
+
)
|
|
110
|
+
with timeline:
|
|
111
|
+
for key, _title, run in checks:
|
|
112
|
+
with timeline.step(key) as step:
|
|
113
|
+
result = run()
|
|
114
|
+
diagnostics.append(result)
|
|
115
|
+
step.detail(f"{self._status_label(result.status)} {result.detail}")
|
|
116
|
+
|
|
117
|
+
self.output.print_table(
|
|
118
|
+
["Check", "Status", "Details"],
|
|
119
|
+
[[item.name, self._status_label(item.status), item.detail] for item in diagnostics],
|
|
120
|
+
title="Audit results",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
failures = [item for item in diagnostics if item.status == "fail"]
|
|
124
|
+
warnings = [item for item in diagnostics if item.status == "warn"]
|
|
125
|
+
if failures:
|
|
126
|
+
self.output.error(f"{len(failures)} required check(s) failed.")
|
|
127
|
+
return 1
|
|
128
|
+
if warnings:
|
|
129
|
+
self.output.warning(f"{len(warnings)} check(s) need attention.")
|
|
130
|
+
else:
|
|
131
|
+
self.output.completion_screen(
|
|
132
|
+
"Environment ready",
|
|
133
|
+
"All required 10xGraph checks passed",
|
|
134
|
+
details={"Checks": len(diagnostics), "Project": Path.cwd()},
|
|
135
|
+
)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _package_check(distribution: str) -> Diagnostic:
|
|
140
|
+
"""Report the installed version of ``distribution``, or fail if absent."""
|
|
141
|
+
try:
|
|
142
|
+
installed = version(distribution)
|
|
143
|
+
except PackageNotFoundError:
|
|
144
|
+
return Diagnostic(distribution, "fail", "not installed")
|
|
145
|
+
return Diagnostic(distribution, "pass", installed)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _evaluation_api_check() -> Diagnostic:
|
|
149
|
+
"""Fail when the installed core lacks the symbols ``10xgraph eval`` needs.
|
|
150
|
+
|
|
151
|
+
This is the CLI/core version-skew check: both packages can be installed
|
|
152
|
+
and still be incompatible, and without this the mismatch only surfaces
|
|
153
|
+
as an ImportError partway through an evaluation run.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
evaluation = importlib.import_module("tenxgraph.qa.evaluation")
|
|
157
|
+
except ImportError as exc:
|
|
158
|
+
return Diagnostic("Evaluation API", "fail", str(exc))
|
|
159
|
+
|
|
160
|
+
required = ("CriteriaConfig", "CriterionConfig", "EvalConfig")
|
|
161
|
+
missing = [name for name in required if not hasattr(evaluation, name)]
|
|
162
|
+
if missing:
|
|
163
|
+
return Diagnostic(
|
|
164
|
+
"Evaluation API",
|
|
165
|
+
"fail",
|
|
166
|
+
"core package is missing: " + ", ".join(missing),
|
|
167
|
+
)
|
|
168
|
+
return Diagnostic("Evaluation API", "pass", "compatible")
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _config_check(config: str = DEFAULT_CONFIG_FILE) -> Diagnostic:
|
|
172
|
+
"""Validate ``10xgraph.json`` (or legacy ``agentflow.json``) in the working directory.
|
|
173
|
+
|
|
174
|
+
Warns when the default file is absent, because audit is also useful
|
|
175
|
+
outside a project directory. A missing explicit path, invalid JSON,
|
|
176
|
+
missing ``agent``, or invalid remote tool definition is a hard failure.
|
|
177
|
+
"""
|
|
178
|
+
config_path = resolve_default_config(config).resolve()
|
|
179
|
+
if not config_path.exists():
|
|
180
|
+
status = "warn" if config == DEFAULT_CONFIG_FILE else "fail"
|
|
181
|
+
return Diagnostic("Project config", status, f"not found at {config_path}")
|
|
182
|
+
try:
|
|
183
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
184
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
185
|
+
return Diagnostic("Project config", "fail", str(exc))
|
|
186
|
+
if not isinstance(data, dict):
|
|
187
|
+
return Diagnostic("Project config", "fail", f"{config_path.name} must be an object")
|
|
188
|
+
agent = data.get("agent")
|
|
189
|
+
if not isinstance(agent, str) or ":" not in agent:
|
|
190
|
+
return Diagnostic("Project config", "fail", "'agent' must be a module:attribute string")
|
|
191
|
+
try:
|
|
192
|
+
tools = validate_remote_tools(data.get("remote_tools", []))
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
return Diagnostic("Project config", "fail", str(exc))
|
|
195
|
+
return Diagnostic("Project config", "pass", f"{config_path} ({len(tools)} remote tools)")
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _port_check(port: int) -> Diagnostic:
|
|
199
|
+
"""Warn when ``port`` on loopback already has a listener.
|
|
200
|
+
|
|
201
|
+
A short connect probe, not a bind attempt, so the audit never steals
|
|
202
|
+
the port from a server that is about to start.
|
|
203
|
+
"""
|
|
204
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
205
|
+
sock.settimeout(0.2)
|
|
206
|
+
if sock.connect_ex(("127.0.0.1", port)) == 0:
|
|
207
|
+
return Diagnostic(f"Port {port}", "warn", "already in use")
|
|
208
|
+
return Diagnostic(f"Port {port}", "pass", "available")
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _status_label(status: str) -> str:
|
|
212
|
+
"""Render a status as the uppercase token used in the timeline and table."""
|
|
213
|
+
return {"pass": "PASS", "warn": "WARN", "fail": "FAIL"}[status]
|