graph-agents-cli 0.3.1__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 (291) hide show
  1. graph_agents_cli/__init__.py +26 -0
  2. graph_agents_cli/_api_policy.py +2145 -0
  3. graph_agents_cli/_approvals.py +400 -0
  4. graph_agents_cli/_build.py +186 -0
  5. graph_agents_cli/_build_info.json +7 -0
  6. graph_agents_cli/_chat_client.py +462 -0
  7. graph_agents_cli/_click.py +157 -0
  8. graph_agents_cli/_defaults.py +139 -0
  9. graph_agents_cli/_experiments.py +64 -0
  10. graph_agents_cli/_http.py +192 -0
  11. graph_agents_cli/_output.py +83 -0
  12. graph_agents_cli/_project.py +462 -0
  13. graph_agents_cli/_remote.py +220 -0
  14. graph_agents_cli/_response_schema.py +264 -0
  15. graph_agents_cli/_runner.py +319 -0
  16. graph_agents_cli/_skills_check.py +274 -0
  17. graph_agents_cli/_tools.py +189 -0
  18. graph_agents_cli/_trust.py +66 -0
  19. graph_agents_cli/api/__init__.py +15 -0
  20. graph_agents_cli/api/_changes.py +506 -0
  21. graph_agents_cli/api/_files.py +658 -0
  22. graph_agents_cli/api/cmd_api.py +2480 -0
  23. graph_agents_cli/deploy/__init__.py +15 -0
  24. graph_agents_cli/deploy/_config.py +171 -0
  25. graph_agents_cli/deploy/_image.py +128 -0
  26. graph_agents_cli/deploy/_kube.py +286 -0
  27. graph_agents_cli/deploy/_modes.py +234 -0
  28. graph_agents_cli/deploy/_preflight.py +370 -0
  29. graph_agents_cli/deploy/_values.py +168 -0
  30. graph_agents_cli/deploy/cmd_deploy.py +1866 -0
  31. graph_agents_cli/deploy/gitops.py +562 -0
  32. graph_agents_cli/deploy/local_load.py +273 -0
  33. graph_agents_cli/dev/__init__.py +13 -0
  34. graph_agents_cli/dev/cmd_build.py +131 -0
  35. graph_agents_cli/dev/cmd_install.py +78 -0
  36. graph_agents_cli/dev/cmd_lint.py +119 -0
  37. graph_agents_cli/dev/cmd_playground.py +297 -0
  38. graph_agents_cli/dev/policy_check.py +1287 -0
  39. graph_agents_cli/eval/__init__.py +22 -0
  40. graph_agents_cli/eval/_client.py +670 -0
  41. graph_agents_cli/eval/_common.py +177 -0
  42. graph_agents_cli/eval/_judge.py +168 -0
  43. graph_agents_cli/eval/_judge_runner.py +238 -0
  44. graph_agents_cli/eval/_paths.py +212 -0
  45. graph_agents_cli/eval/checks.py +581 -0
  46. graph_agents_cli/eval/cmd_analyze.py +278 -0
  47. graph_agents_cli/eval/cmd_compare.py +284 -0
  48. graph_agents_cli/eval/cmd_eval_group.py +80 -0
  49. graph_agents_cli/eval/cmd_generate.py +558 -0
  50. graph_agents_cli/eval/cmd_grade.py +466 -0
  51. graph_agents_cli/eval/cmd_metric.py +156 -0
  52. graph_agents_cli/eval/cmd_run.py +370 -0
  53. graph_agents_cli/eval/cmd_submit.py +400 -0
  54. graph_agents_cli/eval/config.py +435 -0
  55. graph_agents_cli/eval/dataset.py +350 -0
  56. graph_agents_cli/eval/gate.py +420 -0
  57. graph_agents_cli/eval/transcript.py +192 -0
  58. graph_agents_cli/extension/__init__.py +13 -0
  59. graph_agents_cli/extension/_compat.py +86 -0
  60. graph_agents_cli/extension/_loader.py +293 -0
  61. graph_agents_cli/extension/_manifest.py +135 -0
  62. graph_agents_cli/extension/_overrides.py +195 -0
  63. graph_agents_cli/extension/_paths.py +91 -0
  64. graph_agents_cli/extension/_refs.py +193 -0
  65. graph_agents_cli/extension/_resolver.py +453 -0
  66. graph_agents_cli/extension/_schema.py +106 -0
  67. graph_agents_cli/extension/_spec.py +253 -0
  68. graph_agents_cli/extension/_sync.py +102 -0
  69. graph_agents_cli/extension/_trust.py +58 -0
  70. graph_agents_cli/extension/cmd_extension_add.py +259 -0
  71. graph_agents_cli/extension/cmd_extension_group.py +57 -0
  72. graph_agents_cli/extension/cmd_extension_list.py +56 -0
  73. graph_agents_cli/extension/cmd_extension_remove.py +61 -0
  74. graph_agents_cli/extension/cmd_extension_update.py +195 -0
  75. graph_agents_cli/info/__init__.py +13 -0
  76. graph_agents_cli/info/cmd_info.py +222 -0
  77. graph_agents_cli/infra/__init__.py +15 -0
  78. graph_agents_cli/infra/checks.py +1169 -0
  79. graph_agents_cli/infra/cmd_infra.py +103 -0
  80. graph_agents_cli/main.py +591 -0
  81. graph_agents_cli/peer/__init__.py +15 -0
  82. graph_agents_cli/peer/_generate.py +254 -0
  83. graph_agents_cli/peer/cmd_peer.py +1151 -0
  84. graph_agents_cli/run/__init__.py +13 -0
  85. graph_agents_cli/run/_local_server.py +1157 -0
  86. graph_agents_cli/run/_signals.py +141 -0
  87. graph_agents_cli/run/cmd_approvals.py +530 -0
  88. graph_agents_cli/run/cmd_run.py +1421 -0
  89. graph_agents_cli/scaffold/__init__.py +19 -0
  90. graph_agents_cli/scaffold/agents/README.md +24 -0
  91. graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
  92. graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
  93. graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
  94. graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
  95. graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
  96. graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
  97. graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
  98. graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
  99. graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
  100. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
  101. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
  102. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
  103. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
  104. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
  105. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
  106. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
  107. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
  108. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
  109. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
  110. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
  111. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
  112. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
  113. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
  114. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
  115. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
  116. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
  117. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
  118. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
  119. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
  120. graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
  121. graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
  122. graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
  123. graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
  124. graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
  125. graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
  126. graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
  127. graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
  128. graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
  129. graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
  130. graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
  131. graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
  132. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
  133. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
  134. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
  135. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
  136. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
  137. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
  138. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
  139. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
  140. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
  141. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
  142. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
  143. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
  144. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
  145. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
  146. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
  147. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
  148. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
  149. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
  150. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
  151. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
  152. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
  153. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
  154. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
  155. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
  156. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
  157. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
  158. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
  159. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
  160. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
  161. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
  162. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
  163. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
  164. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
  165. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
  166. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
  167. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
  168. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
  169. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
  170. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
  171. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
  172. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
  173. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
  174. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
  175. graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
  176. graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
  177. graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
  178. graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
  179. graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
  180. graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
  181. graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
  182. graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
  183. graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
  184. graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
  185. graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
  186. graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
  187. graph_agents_cli/scaffold/commands/__init__.py +13 -0
  188. graph_agents_cli/scaffold/commands/create.py +1424 -0
  189. graph_agents_cli/scaffold/commands/enhance.py +1652 -0
  190. graph_agents_cli/scaffold/commands/upgrade.py +570 -0
  191. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
  192. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
  193. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
  194. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
  195. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
  196. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
  197. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
  198. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
  199. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
  200. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
  201. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
  202. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
  203. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
  204. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
  205. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
  206. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
  207. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
  208. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
  209. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
  210. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
  211. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
  212. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
  213. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
  214. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
  215. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
  216. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
  217. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
  218. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
  219. graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
  220. graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
  221. graph_agents_cli/scaffold/utils/__init__.py +13 -0
  222. graph_agents_cli/scaffold/utils/backup.py +212 -0
  223. graph_agents_cli/scaffold/utils/build_record.py +257 -0
  224. graph_agents_cli/scaffold/utils/cli_options.py +184 -0
  225. graph_agents_cli/scaffold/utils/fs.py +83 -0
  226. graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
  227. graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
  228. graph_agents_cli/scaffold/utils/keyedit.py +768 -0
  229. graph_agents_cli/scaffold/utils/keymerge.py +537 -0
  230. graph_agents_cli/scaffold/utils/language.py +138 -0
  231. graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
  232. graph_agents_cli/scaffold/utils/logging.py +77 -0
  233. graph_agents_cli/scaffold/utils/manifest.py +292 -0
  234. graph_agents_cli/scaffold/utils/merge.py +970 -0
  235. graph_agents_cli/scaffold/utils/merge3.py +216 -0
  236. graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
  237. graph_agents_cli/scaffold/utils/remote_template.py +376 -0
  238. graph_agents_cli/scaffold/utils/template.py +1352 -0
  239. graph_agents_cli/scaffold/utils/upgrade.py +894 -0
  240. graph_agents_cli/scaffold/utils/version.py +438 -0
  241. graph_agents_cli/secrets/__init__.py +15 -0
  242. graph_agents_cli/secrets/_apply.py +954 -0
  243. graph_agents_cli/secrets/_required.py +188 -0
  244. graph_agents_cli/secrets/cmd_secrets.py +211 -0
  245. graph_agents_cli/setup/__init__.py +13 -0
  246. graph_agents_cli/setup/_antigravity.py +221 -0
  247. graph_agents_cli/setup/cmd_auth.py +1030 -0
  248. graph_agents_cli/setup/cmd_dev_token.py +513 -0
  249. graph_agents_cli/setup/cmd_setup.py +428 -0
  250. graph_agents_cli/setup/cmd_update.py +140 -0
  251. graph_agents_cli/skills/__init__.py +13 -0
  252. graph_agents_cli/skills/_bundle.py +65 -0
  253. graph_agents_cli/skills/data/README.md +19 -0
  254. graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
  255. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
  256. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
  257. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
  258. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
  259. graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
  260. graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
  261. graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
  262. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
  263. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
  264. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
  265. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
  266. graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
  267. graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
  268. graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
  269. graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
  270. graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
  271. graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
  272. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
  273. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
  274. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
  275. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
  276. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
  277. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
  278. graph_agents_cli/system/__init__.py +15 -0
  279. graph_agents_cli/system/_apply.py +519 -0
  280. graph_agents_cli/system/_checks.py +1023 -0
  281. graph_agents_cli/system/_deploy.py +215 -0
  282. graph_agents_cli/system/_model.py +363 -0
  283. graph_agents_cli/system/_system.py +664 -0
  284. graph_agents_cli/system/_views.py +208 -0
  285. graph_agents_cli/system/cmd_system.py +423 -0
  286. graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
  287. graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
  288. graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
  289. graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
  290. graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
  291. graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
@@ -0,0 +1,376 @@
1
+ # Copyright 2026 graph-agents-cli contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Shared test setup: the tests see neither `.env` nor the developer's app settings,
16
+ and the server tests bring their own tool.
17
+
18
+ `{{cookiecutter.agent_directory}}/agent.py` calls `load_dotenv()` when it is imported, which
19
+ would pull a developer's `.env` (a provider key, `AUTH_JWT_*`, ...) into the
20
+ test process: a test could then pass in CI and fail on a laptop, or the other
21
+ way round. This file is loaded before any test module imports the app, so it
22
+ switches `.env` loading off (in this process and in the subprocesses tests
23
+ start) and removes every app setting from the environment: each setting
24
+ `.env.example` documents, plus the provider, auth, tracing and LangChain
25
+ variables and the other settings the app reads (`A2A_NAME`, `RUNTIME`, ...).
26
+ Each test module then sets exactly what it needs. Opt-ins the tests read
27
+ themselves (`TEST_*`, such as `TEST_POSTGRES_DSN`) are kept.
28
+
29
+ The project's response schema (`<agent directory>/response_schema.json`,
30
+ structured final answers) is switched off the same way
31
+ (`RESPONSE_SCHEMA_PATH=none`): the tests exercise the runtime with text
32
+ answers, whatever shape the project declares. The tests of structured answers
33
+ set a schema of their own, and `tests/unit/test_structured.py` checks the
34
+ project's own schema and that the agent answers in it.
35
+
36
+ The server tests exercise the plumbing (tool events, redaction, a run stopped
37
+ mid-call) with a test-only tool through the `use_test_tools` fixture, never
38
+ with the project's own tools, which are yours to replace or delete: while a
39
+ test module has imported the agent, each test serves a graph built like
40
+ `agent.py`'s but with no tools (the fake model would otherwise call a project
41
+ tool whose name a test prompt happens to mention). A test that needs the
42
+ project's own graph is marked `@pytest.mark.project_graph`.
43
+
44
+ `openai_compatible` is an OpenAI-compatible server in process (behind
45
+ `httpx.MockTransport`, no network) that refuses a chat history the way OpenAI
46
+ does: the tests that keep a thread valid for providers run the agent on
47
+ `ChatOpenAI` against it.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import os
53
+ import re
54
+ import sys
55
+ from collections.abc import Callable, Iterator, Sequence
56
+ from pathlib import Path
57
+ from typing import Any, ClassVar
58
+
59
+ import dotenv
60
+ import dotenv.main
61
+ import pytest
62
+
63
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
64
+ # Settings the app reads that a developer's shell may carry, beyond `.env.example`.
65
+ _SETTING_PREFIXES = (
66
+ "AUTH_",
67
+ "MODEL_",
68
+ "JUDGE_",
69
+ "LANGSMITH_",
70
+ "LANGCHAIN_",
71
+ "OTEL_",
72
+ "TRACE_",
73
+ "TRACING_",
74
+ "TOKEN_EXCHANGE_",
75
+ )
76
+ _PROVIDER_VARIABLES = {"OPENAI_API_KEY", "OPENAI_BASE_URL", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY"}
77
+ # Settings the app reads that `.env.example` leaves out (the runtime and the
78
+ # server set some of them; a developer's shell may carry any).
79
+ _APP_VARIABLES = {
80
+ "A2A_NAME",
81
+ "A2A_DESCRIPTION",
82
+ "AGENT_VERSION",
83
+ "DATABASE_URI",
84
+ "REDIS_URI",
85
+ "HOST",
86
+ "LANGGRAPH_SERVER",
87
+ "LANGGRAPH_SERVER_URL",
88
+ "LANGSERVE_GRAPHS",
89
+ "PGCONNECT_TIMEOUT",
90
+ "RUNTIME",
91
+ }
92
+ _ASSIGNMENT = re.compile(r"^\s*#?\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=")
93
+
94
+
95
+ def _documented_settings() -> set[str]:
96
+ """Every variable `.env.example` assigns, commented-out examples included."""
97
+ try:
98
+ text = (PROJECT_ROOT / ".env.example").read_text(encoding="utf-8")
99
+ except OSError:
100
+ return set()
101
+ return {m.group(1) for line in text.splitlines() if (m := _ASSIGNMENT.match(line))}
102
+
103
+
104
+ def _is_app_setting(name: str) -> bool:
105
+ if name.startswith("TEST_"):
106
+ return False
107
+ return (
108
+ name in _PROVIDER_VARIABLES or name in _APP_VARIABLES or name.startswith(_SETTING_PREFIXES)
109
+ )
110
+
111
+
112
+ def _no_dotenv(*args: Any, **kwargs: Any) -> bool:
113
+ return False
114
+
115
+
116
+ # python-dotenv >= 1.2 honours PYTHON_DOTENV_DISABLED (inherited by subprocesses);
117
+ # replacing load_dotenv covers older versions in this process.
118
+ os.environ["PYTHON_DOTENV_DISABLED"] = "1"
119
+ dotenv.load_dotenv = _no_dotenv
120
+ dotenv.main.load_dotenv = _no_dotenv
121
+ for _name in _documented_settings() | {n for n in list(os.environ) if _is_app_setting(n)}:
122
+ if not _name.startswith("TEST_") and _name != "PYTHON_DOTENV_DISABLED":
123
+ os.environ.pop(_name, None)
124
+ # No response schema unless a test sets one (see the module docstring); subprocesses inherit it.
125
+ os.environ["RESPONSE_SCHEMA_PATH"] = "none"
126
+
127
+
128
+ def build_test_graph(tools: Sequence[Any]) -> Any:
129
+ """The agent's graph as `agent.py` builds it, with `tools` instead of the project's."""
130
+ from langchain.agents import create_agent
131
+
132
+ from {{cookiecutter.agent_directory}} import agent
133
+ from {{cookiecutter.agent_directory}}.app_utils.limits import recursion_limit
134
+ from {{cookiecutter.agent_directory}}.app_utils.model import get_model
135
+ from {{cookiecutter.agent_directory}}.app_utils.structured import response_format
136
+
137
+ model = get_model()
138
+ return create_agent(
139
+ model=model,
140
+ tools=list(tools),
141
+ system_prompt=agent.SYSTEM_PROMPT,
142
+ middleware=agent.middleware(),
143
+ context_schema=agent.AgentContext,
144
+ response_format=response_format(model, list(tools)),
145
+ name="test-agent",
146
+ ).with_config({"recursion_limit": recursion_limit()})
147
+
148
+
149
+ AGENT_MODULE = "{{cookiecutter.agent_directory}}.agent"
150
+ APP_MODULE = "{{cookiecutter.agent_directory}}.fast_api_app"
151
+
152
+
153
+ def pytest_configure(config: pytest.Config) -> None:
154
+ config.addinivalue_line(
155
+ "markers", "project_graph: serve the project's own graph (its tools included)"
156
+ )
157
+
158
+
159
+ @pytest.fixture(autouse=True)
160
+ def _no_project_tools(
161
+ request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
162
+ ) -> Iterator[None]:
163
+ """Serve a graph without the project's tools (see the module docstring).
164
+
165
+ Only where the test module already imported the agent or the app (with its
166
+ settings); the app binds its checkpointer to whichever graph `agent.graph`
167
+ is at startup, and reads `agent.graph` again for every run.
168
+ """
169
+ agent = sys.modules.get(AGENT_MODULE)
170
+ if (agent is not None or APP_MODULE in sys.modules) and not os.environ.get("MODEL_PROVIDER"):
171
+ # A module that imported the app without setting a model (a test of
172
+ # its routes): the graph runs on the fake model.
173
+ monkeypatch.setenv("MODEL_PROVIDER", "fake")
174
+ if agent is None and APP_MODULE in sys.modules:
175
+ import importlib
176
+
177
+ agent = importlib.import_module(AGENT_MODULE)
178
+ if agent is not None and request.node.get_closest_marker("project_graph") is None:
179
+ graph = build_test_graph([])
180
+ graph.checkpointer = agent.graph.checkpointer
181
+ monkeypatch.setattr(agent, "graph", graph)
182
+ yield
183
+
184
+
185
+ @pytest.fixture
186
+ def use_test_tools(monkeypatch: pytest.MonkeyPatch) -> Callable[..., Any]:
187
+ """Serve a graph with only the given tools for this test (call it once the app has started).
188
+
189
+ The fake model calls a bound tool when the message mentions it (its name, or
190
+ a distinctive word of it), so `probe` is called for "Run the probe for Paris"
191
+ with `query="Paris"`. The graph keeps the checkpointer the app bound at startup.
192
+ """
193
+
194
+ def install(*tools: Any) -> Any:
195
+ from {{cookiecutter.agent_directory}} import agent
196
+
197
+ graph = build_test_graph(tools)
198
+ graph.checkpointer = agent.graph.checkpointer
199
+ monkeypatch.setattr(agent, "graph", graph)
200
+ return graph
201
+
202
+ return install
203
+
204
+
205
+ # --- an OpenAI-compatible server that checks the history, in process ----------------------
206
+
207
+
208
+ def openai_history_problem(messages: list[dict[str, Any]]) -> str | None:
209
+ """Why OpenAI would refuse this chat history (a 400), or None.
210
+
211
+ An assistant message with `tool_calls` must be followed by one tool message
212
+ per call before anything else, and a tool message must answer such a call.
213
+ """
214
+ pending: list[str] = []
215
+ for message in messages:
216
+ role = message.get("role")
217
+ if pending and role != "tool":
218
+ return (
219
+ "An assistant message with 'tool_calls' must be followed by tool messages "
220
+ "responding to each 'tool_call_id'. The following tool_call_ids did not have "
221
+ f"response messages: {', '.join(pending)}"
222
+ )
223
+ if role == "tool":
224
+ if message.get("tool_call_id") not in pending:
225
+ return "messages with role 'tool' must be a response to a preceding 'tool_calls'"
226
+ pending.remove(message.get("tool_call_id"))
227
+ elif role == "assistant":
228
+ pending = [call.get("id") for call in message.get("tool_calls") or []]
229
+ if pending:
230
+ return f"tool_call_ids did not have response messages: {', '.join(pending)}"
231
+ return None
232
+
233
+
234
+ class OpenAICompatibleFake:
235
+ """A scripted OpenAI-compatible chat server behind `httpx.MockTransport`.
236
+
237
+ It refuses a history OpenAI refuses (`openai_history_problem`, a 400), and
238
+ answers the last message: a user message naming a script word gets that
239
+ reply (see `SCRIPTS`: arguments that are not valid JSON, one id for two
240
+ calls), a tool result gets `Found: <results>` (unless the user asked for
241
+ `ALWAYSBAD`: invalid arguments again), anything else `ok`. Tool calls go
242
+ to the first tool of the request. Replies put in `queue` come first, in
243
+ order: `(text, [(call id, tool name, raw arguments), ...])`, text and
244
+ calls together in one message when both are given. `requests` keeps every
245
+ request body; `refusals` every 400.
246
+ """
247
+
248
+ SCRIPTS: ClassVar[dict[str, list[tuple[str, str]]]] = {
249
+ # (call id, raw arguments) per tool call
250
+ "BADARGS": [("call_0", "{'query': 'SF'}")],
251
+ "TRAILINGCOMMA": [("call_0", '{"query": "SF",}')],
252
+ "DUPIDS": [("dup", '{"query": "SF"}'), ("dup", '{"query": "Rome"}')],
253
+ "BADANDGOOD": [("call_0", '{"query": "SF"}'), ("call_1", "query=Rome")],
254
+ }
255
+
256
+ def __init__(self) -> None:
257
+ self.requests: list[dict[str, Any]] = []
258
+ self.refusals: list[str] = []
259
+ self.queue: list[tuple[str | None, list[tuple[str, str, str]]]] = []
260
+
261
+ def model(self) -> Any:
262
+ import httpx
263
+ from langchain_openai import ChatOpenAI
264
+
265
+ transport = httpx.MockTransport(self._handle)
266
+ return ChatOpenAI(
267
+ model="fake-gpt",
268
+ api_key="test",
269
+ base_url="http://openai-compatible.test/v1",
270
+ max_retries=0,
271
+ http_client=httpx.Client(transport=transport),
272
+ http_async_client=httpx.AsyncClient(transport=transport),
273
+ )
274
+
275
+ def _reply(self, body: dict[str, Any]) -> tuple[str | None, list[dict[str, Any]]]:
276
+ if self.queue:
277
+ text, queued = self.queue.pop(0)
278
+ return text, [
279
+ {
280
+ "index": i,
281
+ "id": call_id,
282
+ "type": "function",
283
+ "function": {"name": name, "arguments": arguments},
284
+ }
285
+ for i, (call_id, name, arguments) in enumerate(queued)
286
+ ]
287
+ messages = body.get("messages") or []
288
+ last = messages[-1] if messages else {}
289
+ asked = next((str(m.get("content")) for m in reversed(messages) if m["role"] == "user"), "")
290
+ tools = [t["function"]["name"] for t in body.get("tools") or []]
291
+ if "ALWAYSBAD" in asked and tools:
292
+ call_id = f"call_{sum(m['role'] == 'assistant' for m in messages)}"
293
+ function = {"name": tools[0], "arguments": "{query: SF}"}
294
+ return None, [{"index": 0, "id": call_id, "type": "function", "function": function}]
295
+ if last.get("role") == "tool":
296
+ results = []
297
+ for message in reversed(messages):
298
+ if message.get("role") != "tool":
299
+ break
300
+ content = message.get("content")
301
+ if isinstance(content, list):
302
+ content = "".join(str(b.get("text", "")) for b in content)
303
+ results.append(str(content))
304
+ return "Found: " + " | ".join(reversed(results)), []
305
+ text = str(last.get("content") or "")
306
+ for word, calls in self.SCRIPTS.items():
307
+ if word in text and tools:
308
+ return None, [
309
+ {
310
+ "index": i,
311
+ "id": call_id,
312
+ "type": "function",
313
+ "function": {"name": tools[0], "arguments": arguments},
314
+ }
315
+ for i, (call_id, arguments) in enumerate(calls)
316
+ ]
317
+ return "ok", []
318
+
319
+ def _handle(self, request: Any) -> Any:
320
+ import json
321
+
322
+ import httpx
323
+
324
+ body = json.loads(request.content)
325
+ self.requests.append(body)
326
+ problem = openai_history_problem(body.get("messages") or [])
327
+ if problem:
328
+ self.refusals.append(problem)
329
+ error = {"message": problem, "type": "invalid_request_error", "param": "messages"}
330
+ return httpx.Response(400, json={"error": error})
331
+ text, calls = self._reply(body)
332
+ usage = {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
333
+ head = {"id": "chatcmpl-fake", "created": 0, "model": "fake-gpt"}
334
+ if not body.get("stream"):
335
+ message: dict[str, Any] = {"role": "assistant", "content": text}
336
+ if calls:
337
+ message["tool_calls"] = [
338
+ {k: v for k, v in c.items() if k != "index"} for c in calls
339
+ ]
340
+ choice = {
341
+ "index": 0,
342
+ "message": message,
343
+ "finish_reason": "tool_calls" if calls else "stop",
344
+ }
345
+ return httpx.Response(
346
+ 200, json={**head, "object": "chat.completion", "choices": [choice], "usage": usage}
347
+ )
348
+
349
+ def chunk(delta: dict[str, Any], finish: str | None = None) -> str:
350
+ choice = {"index": 0, "delta": delta, "finish_reason": finish}
351
+ return "data: " + json.dumps(
352
+ {**head, "object": "chat.completion.chunk", "choices": [choice]}
353
+ )
354
+
355
+ if calls:
356
+ lines = [chunk({"role": "assistant", "content": text, "tool_calls": calls})]
357
+ lines.append(chunk({}, "tool_calls"))
358
+ else:
359
+ lines = [chunk({"role": "assistant", "content": ""})]
360
+ lines += [
361
+ chunk({"content": (text or "")[i : i + 20]}) for i in range(0, len(text or ""), 20)
362
+ ]
363
+ lines.append(chunk({}, "stop"))
364
+ final = {**head, "object": "chat.completion.chunk", "choices": [], "usage": usage}
365
+ lines += ["data: " + json.dumps(final), "data: [DONE]"]
366
+ return httpx.Response(
367
+ 200,
368
+ content="\n\n".join(lines).encode() + b"\n\n",
369
+ headers={"content-type": "text/event-stream"},
370
+ )
371
+
372
+
373
+ @pytest.fixture
374
+ def openai_compatible() -> OpenAICompatibleFake:
375
+ """An in-process OpenAI-compatible server that refuses invalid histories (see its class)."""
376
+ return OpenAICompatibleFake()
@@ -0,0 +1,53 @@
1
+ {
2
+ "cases": [
3
+ {
4
+ "id": "greeting",
5
+ "messages": [{"role": "user", "content": "hi"}],
6
+ "expect": {"contains": ["Hello"], "no_tool_calls": true},
7
+ "judge": {"response_quality": {"threshold": 4}},
8
+ "reference": "A short, friendly greeting that offers help.",
9
+ "metadata": {"category": "smoke"}
10
+ },
11
+ {
12
+ "id": "weather",
13
+ "messages": [{"role": "user", "content": "What is the weather in Paris?"}],
14
+ "expect": {
15
+ "contains": ["sunny"],
16
+ "tool_calls": [{"name": "get_weather", "args_subset": {"query": "Paris"}}]
17
+ },
18
+ "reference": "It's 90 degrees and sunny in Paris.",
19
+ "context": "get_weather returns \"It's 90 degrees and sunny.\" for every place except San Francisco.",
20
+ "metadata": {"category": "tools"}
21
+ },
22
+ {
23
+ "id": "capabilities",
24
+ "messages": [{"role": "user", "content": "What can you do?"}],
25
+ "expect": {
26
+ "regex": "(?i)\\bweather\\b",
27
+ "not_contains": ["Traceback", "PolicyViolation"],
28
+ "no_tool_calls": true
29
+ },
30
+ "judge": {"response_quality": {"threshold": 4}},
31
+ "reference": "The agent explains that it can look up the weather for a place.",
32
+ "metadata": {"category": "smoke"}
33
+ },
34
+ {
35
+ "id": "weather-follow-up",
36
+ "messages": [
37
+ {"role": "user", "content": "What is the weather in Paris?"},
38
+ {"role": "user", "content": "And what is the weather in Berlin?"}
39
+ ],
40
+ "expect": {
41
+ "scope": "all_turns",
42
+ "contains": ["sunny"],
43
+ "tool_calls": [
44
+ {"name": "get_weather", "args_subset": {"query": "Paris"}},
45
+ {"name": "get_weather", "args_subset": {"query": "Berlin"}}
46
+ ],
47
+ "ordered": true
48
+ },
49
+ "reference": "Reports the weather in Paris, then in Berlin: 90 degrees and sunny in both.",
50
+ "metadata": {"category": "multi-turn"}
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,32 @@
1
+ # Grading configuration for `graph-agents-cli eval grade` and `eval run`.
2
+ #
3
+ # Mandatory, no threshold: complete case accounting, every deterministic
4
+ # `expect` check, and every judge metric a case declares that is NOT listed
5
+ # under `quality_metrics`. Only quality metrics may pass at a rate below 100%.
6
+
7
+ judge:
8
+ provider: null # null = JUDGE_MODEL_PROVIDER, else the agent's MODEL_PROVIDER
9
+ model: null # null = JUDGE_MODEL_NAME, else MODEL_NAME
10
+ # Characters of one tool result a judge sees (default 50000; null = never
11
+ # cut). A longer result is cut with a TRUNCATED marker the judge is told
12
+ # about, and `eval grade` warns which cases were affected.
13
+ # max_tool_result_chars: 50000
14
+
15
+ quality_metrics:
16
+ # Only judge metrics listed here may score below their threshold on a bounded
17
+ # fraction of cases; every other judge metric and every deterministic check is
18
+ # mandatory. The rate counts only the cases that declare the metric.
19
+ response_quality: { threshold: 4, min_pass_rate: 0.9 }
20
+
21
+ # Override the built-in rubrics (response_quality, task_success, groundedness)
22
+ # or add your own: {scale, rubric, prompt_template}. The prompt template may use
23
+ # {metric}, {rubric}, {scale}, {conversation}, {transcript}, {response},
24
+ # {reference}, {context}, {reference_section}, {context_section} and
25
+ # {tool_calls_section}. On a multi-turn case {conversation} holds every earlier
26
+ # turn in full (user message, tool calls with results, agent reply) and the
27
+ # latest user message; {transcript} adds the final turn's tool calls and reply.
28
+ judges: {}
29
+
30
+ # module:function callables run in this project's environment:
31
+ # fn(case, trace) -> bool | number | {score, reasoning}
32
+ custom_metrics: []
@@ -0,0 +1,137 @@
1
+ # Copyright 2026 graph-agents-cli contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """The graph `tests/integration/test_approvals_server.py` serves with `langgraph dev`.
16
+
17
+ The agent as `agent.py` builds it (prompt, middleware, context schema), with
18
+ a test tool that cancels an order through `api_client`: a call the test's
19
+ `api-policy.yaml` gates. The JSON body comes from the file named by
20
+ `TEST_APPROVAL_BODY_FILE`, so a test can change the request between the pause
21
+ and the decision. Two more tools place and amend orders, for a policy whose
22
+ approval rules ask other approvers for other calls, and one reads a gauge whose
23
+ reading holds a lone surrogate. `whoami` reports the caller its run acts for
24
+ (`current_caller`) and whether `require_direct_caller` lets it through. With
25
+ `RESPONSE_SCHEMA_PATH` set it answers in that shape (`test_structured_server.py`).
26
+ Not collected by pytest (no `test_` prefix).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import os
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from langchain.agents import create_agent
37
+ from langchain.tools import ToolRuntime
38
+ from langchain_core.tools import tool
39
+
40
+ from {{cookiecutter.agent_directory}} import agent
41
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import (
42
+ ApiPolicyError,
43
+ current_caller,
44
+ get_client,
45
+ require_direct_caller,
46
+ )
47
+ from {{cookiecutter.agent_directory}}.app_utils.limits import recursion_limit
48
+ from {{cookiecutter.agent_directory}}.app_utils.model import get_model
49
+ from {{cookiecutter.agent_directory}}.app_utils.structured import response_format
50
+
51
+
52
+ def _principal(context: Any) -> str:
53
+ if isinstance(context, dict):
54
+ return str(context.get("principal_id") or "")
55
+ return str(getattr(context, "principal_id", "") or "")
56
+
57
+
58
+ @tool
59
+ async def cancel_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
60
+ """Cancel an order by its id."""
61
+ context = getattr(runtime, "context", None)
62
+ body = json.loads(Path(os.environ["TEST_APPROVAL_BODY_FILE"]).read_text(encoding="utf-8"))
63
+ client = get_client("shop", context=context)
64
+ data = await client.post(
65
+ "/orders/{order_id}/cancel",
66
+ operation_id="cancelOrder",
67
+ path_params={"order_id": order_id},
68
+ json_body=body,
69
+ )
70
+ return json.dumps({"upstream": data, "acting_as": _principal(context)})
71
+
72
+
73
+ @tool
74
+ async def place_order(item: str, runtime: ToolRuntime[Any]) -> str:
75
+ """Place a new purchase of an item."""
76
+ context = getattr(runtime, "context", None)
77
+ client = get_client("shop", context=context)
78
+ data = await client.post("/orders", operation_id="createOrder", json_body={"item": item})
79
+ return json.dumps({"upstream": data, "acting_as": _principal(context)})
80
+
81
+
82
+ @tool
83
+ async def amend_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
84
+ """Amend an existing purchase with a gift note."""
85
+ context = getattr(runtime, "context", None)
86
+ client = get_client("shop", context=context)
87
+ data = await client.patch(
88
+ "/orders/{order_id}",
89
+ operation_id="updateOrder",
90
+ path_params={"order_id": order_id},
91
+ json_body={"note": "gift"},
92
+ )
93
+ return json.dumps({"upstream": data, "acting_as": _principal(context)})
94
+
95
+
96
+ @tool
97
+ def gauge_reading(place: str) -> str:
98
+ """Read the gauge at a place (its reading holds a lone surrogate, which UTF-8 cannot encode)."""
99
+ return f"gauge at {place}:\ud80042"
100
+
101
+
102
+ @tool
103
+ def whoami(topic: str, runtime: ToolRuntime[Any]) -> str:
104
+ """Say whoami: the caller this run acts for, and whether only a person may ask here."""
105
+ context = getattr(runtime, "context", None)
106
+ caller = current_caller(context)
107
+ try:
108
+ require_direct_caller(context)
109
+ direct_only = "allowed"
110
+ except ApiPolicyError as exc:
111
+ direct_only = str(exc)
112
+ return json.dumps(
113
+ {
114
+ "principal_id": caller.principal_id,
115
+ "roles": sorted(caller.roles),
116
+ "actor": caller.actor,
117
+ "actor_chain": list(caller.actor_chain),
118
+ "direct_only": direct_only,
119
+ }
120
+ )
121
+
122
+
123
+ model = get_model()
124
+ # The fake model calls the first tool a message names: "Cancel ..." cancels,
125
+ # "Place ..." places, "Amend ..." amends, "... gauge ..." reads the gauge and
126
+ # "whoami" reports the caller.
127
+ tools = [cancel_order, place_order, amend_order, gauge_reading, whoami]
128
+ graph = create_agent(
129
+ model=model,
130
+ tools=tools,
131
+ system_prompt=agent.SYSTEM_PROMPT,
132
+ middleware=agent.middleware(),
133
+ context_schema=agent.AgentContext,
134
+ # Structured answers when the server runs with RESPONSE_SCHEMA_PATH (as agent.py).
135
+ response_format=response_format(model, tools),
136
+ name="approval-test",
137
+ ).with_config({"recursion_limit": recursion_limit()})