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