10xgraph-api 0.7.0__py3-none-any.whl

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