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,305 @@
1
+ """Build command implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from tenxgraph_api.cli.commands import BaseCommand
7
+ from tenxgraph_api.cli.constants import DEFAULT_PORT, DEFAULT_PYTHON_VERSION, DEFAULT_SERVICE_NAME
8
+ from tenxgraph_api.cli.core.validation import Validator
9
+ from tenxgraph_api.cli.exceptions import DockerError, FileOperationError, ValidationError
10
+ from tenxgraph_api.cli.templates.defaults import (
11
+ generate_docker_compose_content,
12
+ generate_dockerfile_content,
13
+ generate_dockerignore_content,
14
+ generate_k8s_manifest_content,
15
+ )
16
+
17
+
18
+ class BuildCommand(BaseCommand):
19
+ """Command to generate Dockerfile and docker-compose.yml for the application."""
20
+
21
+ def execute( # noqa: PLR0915
22
+ self,
23
+ output_file: str = "Dockerfile",
24
+ force: bool = False,
25
+ python_version: str = DEFAULT_PYTHON_VERSION,
26
+ port: int = DEFAULT_PORT,
27
+ docker_compose: bool = False,
28
+ k8s: bool = False,
29
+ service_name: str = DEFAULT_SERVICE_NAME,
30
+ **kwargs: Any,
31
+ ) -> int:
32
+ """Execute the build command.
33
+
34
+ Args:
35
+ output_file: Output Dockerfile path
36
+ force: Overwrite existing files
37
+ python_version: Python version to use
38
+ port: Port to expose
39
+ docker_compose: Generate docker-compose.yml
40
+ k8s: Generate a Kubernetes manifest (Deployment + Service)
41
+ service_name: Service name for docker-compose
42
+ **kwargs: Additional arguments
43
+
44
+ Returns:
45
+ Exit code
46
+ """
47
+ try:
48
+ self.output.command_header(
49
+ "build",
50
+ "Generate Dockerfile (and optional docker-compose.yml) for production image",
51
+ color="yellow",
52
+ )
53
+
54
+ steps: list[tuple[str, str]] = [
55
+ ("validate", "Validating build options"),
56
+ ("sources", "Discovering project sources"),
57
+ ("dockerfile", "Generating Dockerfile"),
58
+ ("dockerignore", "Generating .dockerignore"),
59
+ ]
60
+ if docker_compose:
61
+ steps.append(("compose", "Generating docker-compose.yml"))
62
+ if k8s:
63
+ steps.append(("k8s", "Generating k8s.yaml"))
64
+
65
+ generated = ["Dockerfile", ".dockerignore"]
66
+ timeline = self.output.timeline("Assembling deployment assets", steps=tuple(steps))
67
+ with timeline:
68
+ with timeline.step("validate") as step:
69
+ validated_port = Validator.validate_port(port)
70
+ validated_python_version = Validator.validate_python_version(python_version)
71
+ validated_service_name = Validator.validate_service_name(service_name)
72
+ output_path = Validator.validate_path(output_file)
73
+ if output_path.exists() and not force:
74
+ raise FileOperationError(
75
+ f"Dockerfile already exists at {output_path}. "
76
+ "Use --force to overwrite.",
77
+ file_path=str(output_path),
78
+ )
79
+ step.detail(
80
+ f"python {validated_python_version} · port {validated_port} · "
81
+ f"service {validated_service_name}"
82
+ )
83
+
84
+ with timeline.step("sources") as step:
85
+ requirements_files, requirements_file = self._discover_requirements(Path.cwd())
86
+ has_pyproject = (Path.cwd() / "pyproject.toml").is_file()
87
+ if requirements_files:
88
+ step.detail(f"requirements: {requirements_files[0]}")
89
+ elif has_pyproject:
90
+ step.detail("dependencies: pyproject.toml")
91
+ else:
92
+ step.detail(
93
+ "no requirements.txt or pyproject.toml; installing "
94
+ "10xgraph-api from PyPI"
95
+ )
96
+
97
+ with timeline.step("dockerfile") as step:
98
+ dockerfile_content = generate_dockerfile_content(
99
+ python_version=validated_python_version,
100
+ port=validated_port,
101
+ requirements_file=requirements_file,
102
+ has_requirements=bool(requirements_files),
103
+ omit_cmd=docker_compose,
104
+ has_pyproject=has_pyproject,
105
+ )
106
+ self._write_dockerfile(output_path, dockerfile_content)
107
+ step.detail(str(output_path))
108
+
109
+ dockerignore_path = output_path.parent / ".dockerignore"
110
+ with timeline.step("dockerignore") as step:
111
+ if dockerignore_path.exists() and not force:
112
+ step.skip(f"{dockerignore_path} already exists")
113
+ else:
114
+ dockerignore_path.write_text(
115
+ generate_dockerignore_content(), encoding="utf-8"
116
+ )
117
+ step.detail(str(dockerignore_path))
118
+
119
+ if docker_compose:
120
+ with timeline.step("compose") as step:
121
+ self._write_docker_compose(
122
+ force=force,
123
+ service_name=validated_service_name,
124
+ port=validated_port,
125
+ )
126
+ generated.append("docker-compose.yml")
127
+ step.detail("docker-compose.yml")
128
+
129
+ if k8s:
130
+ with timeline.step("k8s") as step:
131
+ self._write_k8s_manifest(
132
+ force=force,
133
+ service_name=validated_service_name,
134
+ port=validated_port,
135
+ )
136
+ generated.append("k8s.yaml")
137
+ step.detail("k8s.yaml")
138
+
139
+ self.output.completion_screen(
140
+ "Build assets ready",
141
+ "Deployment files generated successfully",
142
+ details={
143
+ "Files": ", ".join(generated),
144
+ "Service": validated_service_name,
145
+ "Port": validated_port,
146
+ },
147
+ next_steps=(
148
+ ["docker compose up --build"]
149
+ if docker_compose
150
+ else [f"docker build -t {validated_service_name} ."]
151
+ ),
152
+ )
153
+
154
+ return 0
155
+
156
+ except (ValidationError, DockerError, FileOperationError) as e:
157
+ return self.handle_error(e)
158
+ except Exception as e:
159
+ docker_error = DockerError(f"Failed to generate Docker files: {e}")
160
+ return self.handle_error(docker_error)
161
+
162
+ def _discover_requirements(self, current_dir: Path) -> tuple[list[Path], str]:
163
+ """Discover requirements files in the project.
164
+
165
+ Args:
166
+ current_dir: Current directory to search in
167
+
168
+ Returns:
169
+ Tuple of (found_files_list, chosen_filename_str)
170
+ """
171
+ requirements_files = []
172
+ requirements_paths = [
173
+ current_dir / "requirements.txt",
174
+ current_dir / "requirements" / "requirements.txt",
175
+ current_dir / "requirements" / "base.txt",
176
+ current_dir / "requirements" / "production.txt",
177
+ ]
178
+
179
+ for req_path in requirements_paths:
180
+ if req_path.exists():
181
+ requirements_files.append(req_path)
182
+
183
+ if not requirements_files:
184
+ self.logger.warning("No requirements.txt file found in common locations")
185
+
186
+ requirements_file = "requirements.txt"
187
+ if requirements_files:
188
+ requirements_file = requirements_files[0].name
189
+ if len(requirements_files) > 1:
190
+ self.logger.info(f"Found multiple requirements files, using: {requirements_file}")
191
+
192
+ return requirements_files, requirements_file
193
+
194
+ def _write_dockerfile(self, output_path: Path, content: str) -> None:
195
+ """Write Dockerfile content to file.
196
+
197
+ Args:
198
+ output_path: Path to write to
199
+ content: Dockerfile content
200
+
201
+ Raises:
202
+ FileOperationError: If writing fails
203
+ """
204
+ try:
205
+ output_path.write_text(content, encoding="utf-8")
206
+ except OSError as e:
207
+ raise FileOperationError(
208
+ f"Failed to write Dockerfile: {e}", file_path=str(output_path)
209
+ ) from e
210
+
211
+ def _write_k8s_manifest(self, force: bool, service_name: str, port: int) -> None:
212
+ """Write k8s.yaml (Deployment + Service).
213
+
214
+ No manifest was generated before, so users hand-rolled one -- usually with
215
+ the default 30s termination grace period, which SIGKILLs an agent run
216
+ mid-LLM-call on every rolling deploy. The generated one sets a grace period
217
+ that matches how long a run can actually take, plus a preStop hook.
218
+
219
+ Raises:
220
+ FileOperationError: If writing fails
221
+ """
222
+ manifest_path = Path("k8s.yaml")
223
+
224
+ if manifest_path.exists() and not force:
225
+ raise FileOperationError(
226
+ f"k8s.yaml already exists at {manifest_path}. Use --force to overwrite.",
227
+ file_path=str(manifest_path),
228
+ )
229
+
230
+ content = generate_k8s_manifest_content(service_name, port)
231
+
232
+ try:
233
+ manifest_path.write_text(content, encoding="utf-8")
234
+ except OSError as e:
235
+ raise FileOperationError(
236
+ f"Failed to write k8s.yaml: {e}", file_path=str(manifest_path)
237
+ ) from e
238
+
239
+ def _write_docker_compose(self, force: bool, service_name: str, port: int) -> None:
240
+ """Write docker-compose.yml file.
241
+
242
+ Args:
243
+ force: Overwrite existing file
244
+ service_name: Service name to use
245
+ port: Port to expose
246
+
247
+ Raises:
248
+ FileOperationError: If writing fails
249
+ """
250
+ compose_path = Path("docker-compose.yml")
251
+
252
+ if compose_path.exists() and not force:
253
+ raise FileOperationError(
254
+ f"docker-compose.yml already exists at {compose_path}. Use --force to overwrite.",
255
+ file_path=str(compose_path),
256
+ )
257
+
258
+ compose_content = generate_docker_compose_content(service_name, port)
259
+
260
+ try:
261
+ compose_path.write_text(compose_content, encoding="utf-8")
262
+ except OSError as e:
263
+ raise FileOperationError(
264
+ f"Failed to write docker-compose.yml: {e}", file_path=str(compose_path)
265
+ ) from e
266
+
267
+ def _show_next_steps(self, docker_compose: bool) -> None:
268
+ """Show next steps to the user.
269
+
270
+ Args:
271
+ docker_compose: Whether docker-compose was generated
272
+ """
273
+ if docker_compose:
274
+ steps = [
275
+ "Review the generated Dockerfile and docker-compose.yml",
276
+ "Build and run with: docker compose up --build",
277
+ "Or build separately: docker build -t 10xgraph-api .",
278
+ "Access your API at: http://localhost:8000",
279
+ ]
280
+ else:
281
+ steps = [
282
+ "Review the generated Dockerfile",
283
+ "Build the image: docker build -t 10xgraph-api .",
284
+ "Run the container: docker run -p 8000:8000 10xgraph-api",
285
+ "Access your API at: http://localhost:8000",
286
+ ]
287
+
288
+ self.output.print_list(
289
+ [f"{index}. {step}" for index, step in enumerate(steps, 1)],
290
+ title="Next steps",
291
+ bullet="→",
292
+ )
293
+
294
+ production_tips = [
295
+ "Using a multi-stage build to reduce image size",
296
+ "Setting up proper environment variables",
297
+ "Configuring health checks and resource limits",
298
+ "Using a reverse proxy like nginx",
299
+ ]
300
+
301
+ self.output.print_list(
302
+ production_tips,
303
+ title="Production considerations",
304
+ bullet="•",
305
+ )
@@ -0,0 +1,71 @@
1
+ """Config command implementation: a browser editor for ``10xgraph.json``."""
2
+
3
+ import webbrowser
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from tenxgraph_api.cli.commands import BaseCommand
8
+ from tenxgraph_api.cli.config_editor import ConfigEditorServer
9
+ from tenxgraph_api.cli.constants import DEFAULT_CONFIG_FILE, LEGACY_CONFIG_FILE
10
+ from tenxgraph_api.cli.exceptions import ServerError
11
+
12
+
13
+ class ConfigCommand(BaseCommand):
14
+ """Serve the config editor until the user stops it with Ctrl+C."""
15
+
16
+ def execute(
17
+ self,
18
+ config: str = DEFAULT_CONFIG_FILE,
19
+ port: int = 0,
20
+ open_browser: bool = True,
21
+ **kwargs: Any,
22
+ ) -> int:
23
+ # The editor always writes 10xgraph.json. When only the legacy agentflow.json exists,
24
+ # it starts from that file's contents; the first save creates 10xgraph.json and
25
+ # leaves agentflow.json untouched.
26
+ config_path = Path(config).expanduser().resolve()
27
+ seed_path = None
28
+ if config == DEFAULT_CONFIG_FILE and not config_path.exists():
29
+ legacy = config_path.with_name(LEGACY_CONFIG_FILE)
30
+ seed_path = legacy if legacy.exists() else None
31
+ try:
32
+ server = ConfigEditorServer(config_path, port=port, seed_path=seed_path)
33
+ except OSError as exc:
34
+ return self.handle_error(
35
+ ServerError(
36
+ f"Could not start the config editor: {exc}", host="127.0.0.1", port=port
37
+ )
38
+ )
39
+
40
+ self.output.command_header(
41
+ "config",
42
+ f"Edit {config_path.name} in your browser",
43
+ hint="Ctrl+C to stop the config editor",
44
+ )
45
+ self.output.completion_screen(
46
+ "Config editor running",
47
+ "Open the link below to edit, validate, and save your configuration.",
48
+ details={
49
+ "Editor": server.url,
50
+ "File": f"{config_path}{_file_note(config_path, server.store.seeded, seed_path)}",
51
+ },
52
+ next_steps=["Press Ctrl+C to stop the editor when you are done."],
53
+ )
54
+ if open_browser:
55
+ webbrowser.open_new_tab(server.url)
56
+
57
+ try:
58
+ server.serve_forever()
59
+ except KeyboardInterrupt:
60
+ pass
61
+ finally:
62
+ server.shutdown()
63
+ return 0
64
+
65
+
66
+ def _file_note(config_path: Path, seeded: bool, seed_path: Path | None) -> str:
67
+ if config_path.exists():
68
+ return ""
69
+ if seeded and seed_path is not None:
70
+ return f" (new, starts from {seed_path.name}; saving creates it)"
71
+ return " (new)"
@@ -0,0 +1,93 @@
1
+ """Safe interactive showcase for 10xGraph terminal motion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ from tenxgraph_api.cli.commands import BaseCommand
9
+ from tenxgraph_api.cli.exceptions import ValidationError
10
+
11
+
12
+ class DemoCommand(BaseCommand):
13
+ """Preview command-specific animations without external side effects."""
14
+
15
+ _STYLES = ("typing", "network", "init", "build", "eval")
16
+ _ALIASES = {"play": "typing", "api": "network"}
17
+
18
+ _SUBTITLES = {
19
+ "typing": "Full-screen 10xGraph identity and workspace transition",
20
+ "network": "Live agent network and playground connection",
21
+ "init": "Project scaffold assembly",
22
+ "build": "Container delivery pipeline",
23
+ "eval": "Evaluation scan and completion states",
24
+ }
25
+ _COMMAND_NAMES = {
26
+ "typing": "typing",
27
+ "network": "api",
28
+ "init": "init",
29
+ "build": "build",
30
+ "eval": "eval",
31
+ }
32
+
33
+ def execute(self, style: str = "all", **kwargs: Any) -> int:
34
+ normalized = self._ALIASES.get(style.strip().lower(), style.strip().lower())
35
+ if normalized != "all" and normalized not in self._STYLES:
36
+ return self.handle_error(
37
+ ValidationError(
38
+ f"Unknown animation style '{style}'. "
39
+ f"Choose all, {', '.join(self._STYLES)}.",
40
+ field="style",
41
+ )
42
+ )
43
+
44
+ selected = self._STYLES if normalized == "all" else (normalized,)
45
+ for theme in selected:
46
+ self.output.command_header(self._COMMAND_NAMES[theme], self._SUBTITLES[theme])
47
+
48
+ self._preview_timeline()
49
+ self._preview_progress()
50
+
51
+ self.output.completion_screen(
52
+ "Animation showcase",
53
+ "All preview states rendered successfully",
54
+ details={
55
+ "Themes": ", ".join(selected),
56
+ "Side effects": "none",
57
+ "Fallback": "automatic for CI, pipes, and JSON",
58
+ },
59
+ next_steps=[
60
+ "Run `10xgraph play` to see the network theme in a real workflow.",
61
+ "Use `10xgraph --no-animation COMMAND` for static accessible output.",
62
+ "Use `10xgraph --fullscreen COMMAND` to hold a dedicated screen.",
63
+ ],
64
+ )
65
+ return 0
66
+
67
+ def _preview_timeline(self) -> None:
68
+ stages = (
69
+ ("topology", "Resolving graph topology", "3 nodes, 2 edges"),
70
+ ("pipeline", "Assembling runtime pipeline", "checkpointer + store bound"),
71
+ ("experience", "Connecting developer experience", "playground reachable"),
72
+ )
73
+ timeline = self.output.timeline(
74
+ "Timeline preview",
75
+ steps=tuple((key, title) for key, title, _ in stages),
76
+ )
77
+ with timeline:
78
+ for key, _title, detail in stages:
79
+ with timeline.step(key) as step:
80
+ time.sleep(0.35)
81
+ step.detail(detail)
82
+
83
+ def _preview_progress(self) -> None:
84
+ samples = (
85
+ ("weather_eval::forecast_tokyo", "passed"),
86
+ ("weather_eval::forecast_oslo", "passed"),
87
+ ("weather_eval::unknown_city", "failed"),
88
+ ("weather_eval::rate_limited", "passed"),
89
+ )
90
+ with self.output.progress_run("Progress preview", total=len(samples)) as progress:
91
+ for label, status in samples:
92
+ time.sleep(0.18)
93
+ progress.record(label, status=status, detail="0.42s")