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,531 @@
1
+ """Init command implementation."""
2
+
3
+ import contextlib
4
+ import json
5
+ import re
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from string import Template
9
+ from typing import Any
10
+
11
+ from tenxgraph_api.cli.commands import BaseCommand
12
+ from tenxgraph_api.cli.core.prompts import Choice
13
+ from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError
14
+
15
+
16
+ _TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
17
+ _SKIP_DIRS = {"__pycache__", ".ruff_cache"}
18
+
19
+ # Directories inside prod/ that are only included based on user choices
20
+ _AUTH_DIR = "auth"
21
+
22
+
23
+ def _slugify(name: str) -> str:
24
+ """Convert 'WeatherBot' → 'weather-bot'."""
25
+ s = re.sub(r"([A-Z])", r"-\1", name).lstrip("-").lower()
26
+ s = re.sub(r"[^a-z0-9]+", "-", s)
27
+ return s.strip("-")
28
+
29
+
30
+ def _strip_env_blocks(content: str, *, keep_redis: bool, keep_jwt: bool) -> str:
31
+ """Remove conditional marker blocks that are not needed."""
32
+ if not keep_redis:
33
+ content = re.sub(r"##\[IF_REDIS\]##.*?##\[/IF_REDIS\]##\n?", "", content, flags=re.DOTALL)
34
+ else:
35
+ content = re.sub(r"##\[IF_REDIS\]##\n?", "", content)
36
+ content = re.sub(r"##\[/IF_REDIS\]##\n?", "", content)
37
+
38
+ if not keep_jwt:
39
+ content = re.sub(r"##\[IF_JWT\]##.*?##\[/IF_JWT\]##\n?", "", content, flags=re.DOTALL)
40
+ else:
41
+ content = re.sub(r"##\[IF_JWT\]##\n?", "", content)
42
+ content = re.sub(r"##\[/IF_JWT\]##\n?", "", content)
43
+
44
+ return content
45
+
46
+
47
+ class InitCommand(BaseCommand):
48
+ """Command to initialize a new agent project interactively."""
49
+
50
+ def execute(
51
+ self,
52
+ path: str = ".",
53
+ force: bool = False,
54
+ agent_name: str | None = None,
55
+ template: str | None = None,
56
+ auth: str | None = None,
57
+ rate_limit: str | None = None,
58
+ yes: bool = False,
59
+ non_interactive: bool = False,
60
+ dry_run: bool = False,
61
+ **kwargs: Any,
62
+ ) -> int:
63
+ try:
64
+ self.output.command_header(
65
+ "init", "Create a new 10xGraph agent project", color="magenta"
66
+ )
67
+
68
+ context: dict[str, Any] | None
69
+ if yes or non_interactive:
70
+ context = self._non_interactive_context(
71
+ path=path,
72
+ agent_name=agent_name,
73
+ template=template,
74
+ auth=auth,
75
+ rate_limit=rate_limit,
76
+ )
77
+ else:
78
+ context = self._prompt_user()
79
+ if context is None:
80
+ self.output.info("Cancelled.", emoji=False)
81
+ return 0
82
+
83
+ self._print_summary(context)
84
+
85
+ base_path = Path(path)
86
+ # Capture this BEFORE any writes: a config file present now is the
87
+ # developer's own (a prior init or a hand edit), and must not be
88
+ # clobbered unless they passed --force.
89
+ config_path = base_path / "10xgraph.json"
90
+ config_pre_existed = config_path.exists()
91
+
92
+ is_prod = context["setup_type"] == "production"
93
+ template_dir = _TEMPLATES_DIR / ("prod" if is_prod else "dev")
94
+
95
+ if dry_run:
96
+ planned = [
97
+ str(src.relative_to(template_dir))
98
+ for src in sorted(template_dir.rglob("*"))
99
+ if src.is_file() and not self._should_skip(src, template_dir, context, is_prod)
100
+ ]
101
+ if "10xgraph.json" not in planned:
102
+ planned.append("10xgraph.json")
103
+ self.output.print_list(planned, title="Files that would be created", bullet="→")
104
+ self.output.info("Dry run complete; no files were written.", emoji=False)
105
+ return 0
106
+
107
+ timeline = self.output.timeline(
108
+ f'Scaffolding "{context["agent_name"]}"',
109
+ steps=(
110
+ ("workspace", "Preparing the project directory"),
111
+ ("files", "Writing template files"),
112
+ ("config", "Generating 10xgraph.json"),
113
+ ),
114
+ )
115
+ with timeline:
116
+ with timeline.step("workspace") as step:
117
+ base_path.mkdir(parents=True, exist_ok=True)
118
+ step.detail(str(base_path.resolve()))
119
+
120
+ with timeline.step("files") as step:
121
+ created = self._copy_template_dir(
122
+ template_dir,
123
+ base_path,
124
+ context,
125
+ force=force,
126
+ is_prod=is_prod,
127
+ on_file=step.detail,
128
+ )
129
+ step.detail(f"{len(created)} files from the {template_dir.name} template")
130
+
131
+ with timeline.step("config") as step:
132
+ # Regenerate 10xgraph.json from the built config. Force is safe only
133
+ # to override the copy this run just made; honor --force for a file
134
+ # that was already there.
135
+ config = self._build_config(context, is_prod)
136
+ self._write_file(
137
+ config_path,
138
+ json.dumps(config, indent=2) + "\n",
139
+ force=force or not config_pre_existed,
140
+ )
141
+ step.detail(str(config_path))
142
+
143
+ agent_name = context["agent_name"]
144
+ self.output.completion_screen(
145
+ "Project ready",
146
+ f'"{agent_name}" was created successfully',
147
+ details={
148
+ "Location": base_path.resolve(),
149
+ "Template": "production" if is_prod else "quick-start",
150
+ "Auth": context["auth"],
151
+ },
152
+ next_steps=[
153
+ f"cd {base_path}",
154
+ "Copy .env.example to .env and add your model API key.",
155
+ "10xgraph play",
156
+ ],
157
+ )
158
+
159
+ return 0
160
+
161
+ except (FileOperationError, ValidationError) as e:
162
+ return self.handle_error(e)
163
+ except Exception as e:
164
+ return self.handle_error(FileOperationError(f"Failed to initialize project: {e}"))
165
+
166
+ def _non_interactive_context(
167
+ self,
168
+ *,
169
+ path: str,
170
+ agent_name: str | None,
171
+ template: str | None,
172
+ auth: str | None,
173
+ rate_limit: str | None,
174
+ ) -> dict[str, Any]:
175
+ """Resolve a complete, reproducible scaffold recipe without prompting."""
176
+ resolved_template = (template or "quick-start").strip().lower().replace("_", "-")
177
+ if resolved_template not in {"quick-start", "production"}:
178
+ raise ValidationError(
179
+ f"Invalid template '{resolved_template}'. Choose quick-start or production.",
180
+ field="template",
181
+ )
182
+
183
+ inferred_name = Path(path).resolve().name if path not in {"", "."} else "MyAgent"
184
+ resolved_name = (agent_name or inferred_name).strip()
185
+ slug = _slugify(resolved_name)
186
+ if not resolved_name or not slug:
187
+ raise ValidationError(
188
+ "Agent name must contain at least one letter or number.",
189
+ field="name",
190
+ )
191
+
192
+ resolved_auth = (auth or "none").strip().lower()
193
+ if resolved_auth not in {"none", "jwt", "custom"}:
194
+ raise ValidationError(
195
+ f"Invalid auth mode '{resolved_auth}'. Choose none, jwt, or custom.",
196
+ field="auth",
197
+ )
198
+
199
+ resolved_rate_limit = (rate_limit or "none").strip().lower()
200
+ if resolved_rate_limit not in {"none", "memory", "redis"}:
201
+ raise ValidationError(
202
+ f"Invalid rate-limit mode '{resolved_rate_limit}'. "
203
+ "Choose none, memory, or redis.",
204
+ field="rate_limit",
205
+ )
206
+
207
+ if resolved_template == "quick-start" and (
208
+ resolved_auth != "none" or resolved_rate_limit != "none"
209
+ ):
210
+ raise ValidationError(
211
+ "--auth and --rate-limit require --template production.",
212
+ field="template",
213
+ )
214
+
215
+ return {
216
+ "agent_name": resolved_name,
217
+ "agent_name_slug": slug,
218
+ "setup_type": "production" if resolved_template == "production" else "quick_start",
219
+ "auth": resolved_auth,
220
+ "rate_limit": resolved_rate_limit,
221
+ "rl_requests": 100,
222
+ "rl_window": 60,
223
+ "rl_by": "ip",
224
+ "rl_trusted_proxy": False,
225
+ }
226
+
227
+ # ------------------------------------------------------------------
228
+ # Prompts
229
+ # ------------------------------------------------------------------
230
+
231
+ def _prompt_user(self) -> dict | None: # noqa: PLR0911
232
+ prompts = self.output.prompts()
233
+ prompts.require_interactive(
234
+ field="template",
235
+ alternatives=(
236
+ "Re-run with --yes to accept the defaults, or --non-interactive "
237
+ "with --name/--template for a reproducible recipe."
238
+ ),
239
+ )
240
+
241
+ agent_name = prompts.text("What is your agent name?", default="MyAgent")
242
+ if agent_name is None:
243
+ return None
244
+
245
+ template = prompts.select(
246
+ "Which project template?",
247
+ [
248
+ Choice(
249
+ "quick_start",
250
+ "Quick Start",
251
+ "Minimal graph, no auth — fastest path to a running agent",
252
+ ),
253
+ Choice(
254
+ "production",
255
+ "Production",
256
+ "Auth, rate limiting, evals, and tests scaffolded in",
257
+ ),
258
+ ],
259
+ default="quick_start",
260
+ )
261
+ if template is None:
262
+ return None
263
+
264
+ is_prod = template == "production"
265
+ context: dict[str, Any] = {
266
+ "agent_name": agent_name,
267
+ "agent_name_slug": _slugify(agent_name),
268
+ "setup_type": template,
269
+ "auth": "none",
270
+ "rate_limit": "none",
271
+ }
272
+
273
+ if not is_prod:
274
+ return context
275
+
276
+ # --- Production questions ---
277
+
278
+ auth = prompts.select(
279
+ "How should requests be authenticated?",
280
+ [
281
+ Choice("none", "None", "Open endpoints — development only"),
282
+ Choice("jwt", "JWT", "Requires JWT_SECRET_KEY and JWT_ALGORITHM"),
283
+ Choice("custom", "Custom", "Scaffolds a BaseAuth subclass under auth/"),
284
+ ],
285
+ default="none",
286
+ )
287
+ if auth is None:
288
+ return None
289
+ context["auth"] = auth
290
+
291
+ if auth == "none":
292
+ return context
293
+
294
+ rate_limit = prompts.select(
295
+ "Rate limiting backend?",
296
+ [
297
+ Choice("none", "None", "No request throttling"),
298
+ Choice("memory", "Memory", "Per-process counters — single instance only"),
299
+ Choice("redis", "Redis", "Shared counters — requires REDIS_URL"),
300
+ ],
301
+ default="none",
302
+ )
303
+ if rate_limit is None:
304
+ return None
305
+ context["rate_limit"] = rate_limit
306
+
307
+ if rate_limit == "none":
308
+ return context
309
+
310
+ rl_requests = prompts.text(
311
+ "Max requests per window?",
312
+ default="100",
313
+ validate=lambda v: (v.isdigit() and int(v) > 0) or "Enter a positive integer",
314
+ )
315
+ if rl_requests is None:
316
+ return None
317
+ context["rl_requests"] = int(rl_requests)
318
+
319
+ rl_window = prompts.text(
320
+ "Window size (seconds)?",
321
+ default="60",
322
+ validate=lambda v: (v.isdigit() and int(v) > 0) or "Enter a positive integer",
323
+ )
324
+ if rl_window is None:
325
+ return None
326
+ context["rl_window"] = int(rl_window)
327
+
328
+ rl_by = prompts.select(
329
+ "Count requests per?",
330
+ [
331
+ Choice("ip", "Per IP", "Recommended — each client gets its own budget"),
332
+ Choice("global", "Global", "One shared budget across all clients"),
333
+ ],
334
+ default="ip",
335
+ )
336
+ if rl_by is None:
337
+ return None
338
+ context["rl_by"] = rl_by
339
+
340
+ rl_proxy = prompts.confirm(
341
+ "Behind a reverse proxy? (reads the real IP from forwarded headers)",
342
+ default=False,
343
+ )
344
+ if rl_proxy is None:
345
+ return None
346
+ context["rl_trusted_proxy"] = rl_proxy
347
+
348
+ return context
349
+
350
+ # ------------------------------------------------------------------
351
+ # Output helpers
352
+ # ------------------------------------------------------------------
353
+
354
+ def _print_summary(self, context: dict) -> None:
355
+ is_prod = context["setup_type"] == "production"
356
+ setup_label = "Production" if is_prod else "Quick Start"
357
+
358
+ rows: list[tuple[str, str]] = [
359
+ ("Agent name", context["agent_name"]),
360
+ ("Package name", context["agent_name_slug"]),
361
+ ("Setup", setup_label),
362
+ ]
363
+ if is_prod:
364
+ auth = context["auth"]
365
+ rows.append(("Auth", "None" if auth == "none" else auth.upper()))
366
+
367
+ rl = context["rate_limit"]
368
+ if rl == "none":
369
+ rows.append(("Rate limit", "None"))
370
+ else:
371
+ rl_by = "Global" if context.get("rl_by") == "global" else "Per IP"
372
+ proxy = " · proxy headers on" if context.get("rl_trusted_proxy") else ""
373
+ rows.append(
374
+ (
375
+ "Rate limit",
376
+ f"{rl.capitalize()} · {context.get('rl_requests', 100)} req / "
377
+ f"{context.get('rl_window', 60)}s · {rl_by}{proxy}",
378
+ )
379
+ )
380
+
381
+ self.output.print_table(
382
+ ["Setting", "Value"],
383
+ [[label, value] for label, value in rows],
384
+ title="Project summary",
385
+ )
386
+
387
+ def _print_next_steps(self, context: dict, is_prod: bool) -> None:
388
+ steps: list[tuple[str, str]] = []
389
+
390
+ steps.append(("10xgraph skills", "Install coding agent skills"))
391
+
392
+ if is_prod:
393
+ steps.append(("pre-commit install", "Set up Git hooks (ruff, bandit, etc.)"))
394
+
395
+ steps.append(("pip install google-genai", "Add the AI provider library"))
396
+ steps.append(("cp .env.example .env", "Copy env template, then fill in your API keys"))
397
+
398
+ if context["auth"] == "jwt":
399
+ steps.append(("# Set JWT_SECRET_KEY in .env", "Required for JWT auth to work"))
400
+
401
+ if context["rate_limit"] == "redis":
402
+ steps.append(("# Set REDIS_URL in .env", "Required for Redis rate limiting"))
403
+
404
+ steps.append(("10xgraph play", "Launch your agent"))
405
+
406
+ self.output.print_table(
407
+ ["#", "Command", "Purpose"],
408
+ [
409
+ [str(index), command, description]
410
+ for index, (command, description) in enumerate(steps, 1)
411
+ ],
412
+ title="Next steps",
413
+ )
414
+
415
+ # ------------------------------------------------------------------
416
+ # Config generation
417
+ # ------------------------------------------------------------------
418
+
419
+ def _build_config(self, context: dict, is_prod: bool) -> dict:
420
+ config: dict = {
421
+ "agent": "graph.agent:app",
422
+ "env": ".env",
423
+ "auth": None,
424
+ "thread_name_generator": None,
425
+ # AG-UI endpoint for clients such as CopilotKit. Off until turned on here.
426
+ "ag_ui": {"enabled": False},
427
+ }
428
+
429
+ if not is_prod:
430
+ return config
431
+
432
+ auth = context["auth"]
433
+ if auth == "jwt":
434
+ # JWT is configured with the bare string. The dict form requires a "path"
435
+ # (see GraphConfig.auth_config) and is only valid for method "custom".
436
+ config["auth"] = "jwt"
437
+ elif auth == "custom":
438
+ config["auth"] = {"method": "custom", "path": "auth.agent_auth:AgentAuth"}
439
+
440
+ if auth in ("jwt", "custom"):
441
+ # Secure by default in production: a thread is accessible only to its owner.
442
+ # Change to "allow_all" to let any authenticated user access any thread, or
443
+ # point at a custom AuthorizationBackend ("module:attr").
444
+ config["authorization"] = "ownership"
445
+
446
+ config["thread_name_generator"] = "graph.thread_name_generator:MyNameGenerator"
447
+ config["injectq"] = "graph.agent:container"
448
+
449
+ rate_limit = context["rate_limit"]
450
+ if rate_limit != "none":
451
+ config["rate_limit"] = {
452
+ "enabled": True,
453
+ "backend": rate_limit,
454
+ "requests": context.get("rl_requests", 100),
455
+ "window": context.get("rl_window", 60),
456
+ "by": context.get("rl_by", "ip"),
457
+ "trusted_proxy_headers": context.get("rl_trusted_proxy", False),
458
+ "exclude_paths": ["/ping", "/docs", "/redoc", "/openapi.json"],
459
+ }
460
+
461
+ return config
462
+
463
+ # ------------------------------------------------------------------
464
+ # File helpers
465
+ # ------------------------------------------------------------------
466
+
467
+ def _should_skip(self, src: Path, template_dir: Path, context: dict, is_prod: bool) -> bool:
468
+ """Return True if this template file should not be copied."""
469
+ if any(part in _SKIP_DIRS for part in src.parts):
470
+ return True
471
+ if not is_prod:
472
+ return False
473
+ # auth/ is only included for custom auth; skip for none and jwt
474
+ rel_parts = src.relative_to(template_dir).parts
475
+ return rel_parts[0] == _AUTH_DIR and context["auth"] != "custom"
476
+
477
+ def _render(self, src: Path, context: dict, is_prod: bool) -> str:
478
+ content = src.read_text(encoding="utf-8")
479
+
480
+ # Strip conditional blocks in .env.example before substitution
481
+ if src.name == ".env.example" and is_prod:
482
+ content = _strip_env_blocks(
483
+ content,
484
+ keep_redis=context["rate_limit"] == "redis",
485
+ keep_jwt=context["auth"] == "jwt",
486
+ )
487
+
488
+ with contextlib.suppress(Exception):
489
+ content = Template(content).safe_substitute(context)
490
+
491
+ return content
492
+
493
+ def _copy_template_dir(
494
+ self,
495
+ template_dir: Path,
496
+ dest_dir: Path,
497
+ context: dict,
498
+ *,
499
+ force: bool,
500
+ is_prod: bool,
501
+ on_file: Callable[[str], None] | None = None,
502
+ ) -> set[Path]:
503
+ created: set[Path] = set()
504
+ for src in sorted(template_dir.rglob("*")):
505
+ if src.is_dir():
506
+ continue
507
+ if self._should_skip(src, template_dir, context, is_prod):
508
+ continue
509
+ rel = src.relative_to(template_dir)
510
+ dest = dest_dir / rel
511
+ content = self._render(src, context, is_prod)
512
+ self._write_file(dest, content, force=force)
513
+ if on_file is not None:
514
+ on_file(str(rel).replace("\\", "/"))
515
+ created.add(dest)
516
+ return created
517
+
518
+ def _write_file(self, path: Path, content: str, *, force: bool) -> None:
519
+ try:
520
+ path.parent.mkdir(parents=True, exist_ok=True)
521
+ if path.exists() and not force:
522
+ raise FileOperationError(
523
+ f"File already exists: {path}. Use --force to overwrite.",
524
+ file_path=str(path),
525
+ )
526
+ path.write_text(content, encoding="utf-8")
527
+ self.logger.debug("Wrote file: %s", path)
528
+ except OSError as e:
529
+ raise FileOperationError(
530
+ f"Failed to write file {path}: {e}", file_path=str(path)
531
+ ) from e