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,611 @@
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
+ """Model wiring through LangChain's `init_chat_model`.
16
+
17
+ `MODEL_PROVIDER` / `MODEL_NAME` select the agent model; `JUDGE_MODEL_PROVIDER`,
18
+ `JUDGE_MODEL_NAME`, `JUDGE_BASE_URL` and `JUDGE_API_KEY` select the judge model
19
+ and default to the agent's values. The agent code never names a provider.
20
+
21
+ Every provider model gets a request timeout and a retry budget:
22
+ `MODEL_TIMEOUT_S` (default 60 seconds per model request; `0` leaves the
23
+ provider SDK's own default) and `MODEL_MAX_RETRIES` (default 2). The run as a
24
+ whole is bounded separately by `RUN_TIMEOUT_S` (see `limits.py`).
25
+
26
+ OpenAI-API models (`openai`, `openai-compatible`) take two more settings,
27
+ checked at startup like the limits:
28
+
29
+ * `MODEL_REASONING_EFFORT`: `none`, `minimal`, `low`, `medium`, `high` or
30
+ `xhigh` (what a model accepts varies); unset leaves the model's default.
31
+ Sent as `reasoning_effort` on Chat Completions and as `reasoning.effort` on
32
+ the Responses API.
33
+ * `MODEL_USE_RESPONSES_API`: `true` sends every request to the Responses API
34
+ (`/v1/responses`), `false` to Chat Completions (`/v1/chat/completions`).
35
+ Unset lets langchain-openai choose: Chat Completions, except for the models
36
+ its own list says need the Responses API and for requests that use a
37
+ Responses-only feature. Some models refuse function tools with a reasoning
38
+ effort on Chat Completions ("use /v1/responses"): set `true` (or
39
+ `MODEL_REASONING_EFFORT=none`). An OpenAI-compatible server that has no
40
+ `/v1/responses` needs `false`, or unset.
41
+
42
+ Either set for another provider stops startup: it would be ignored. The judge
43
+ takes `JUDGE_REASONING_EFFORT` and `JUDGE_USE_RESPONSES_API`; unset, it keeps
44
+ the agent's values when it is an OpenAI-API model too, and drops them when it
45
+ is not.
46
+
47
+ Provider `fake` is a deterministic in-process chat model for tests and CI. It
48
+ is never offered by `graph-agents-cli create`.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ import os
55
+ import re
56
+ from collections.abc import Iterator, Sequence
57
+ from typing import Any
58
+
59
+ from langchain_core.language_models import BaseChatModel
60
+ from langchain_core.messages import (
61
+ AIMessage,
62
+ AIMessageChunk,
63
+ BaseMessage,
64
+ ToolMessage,
65
+ )
66
+ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
67
+ from pydantic import Field
68
+
69
+ from {{cookiecutter.agent_directory}}.app_utils.content import (
70
+ unfence_agent_request,
71
+ unfence_tool_output,
72
+ )
73
+ from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
74
+ from {{cookiecutter.agent_directory}}.app_utils.structured import ANSWER_TOOL, validate
75
+
76
+ # graph-agents-cli provider name -> LangChain `model_provider`.
77
+ PROVIDER_TO_LANGCHAIN: dict[str, str] = {
78
+ "openai": "openai",
79
+ "anthropic": "anthropic",
80
+ "gemini": "google_genai",
81
+ "openai-compatible": "openai",
82
+ }
83
+
84
+ # Provider -> the environment variable holding its key.
85
+ PROVIDER_KEY_VARS: dict[str, str] = {
86
+ "openai": "OPENAI_API_KEY",
87
+ "anthropic": "ANTHROPIC_API_KEY",
88
+ "gemini": "GOOGLE_API_KEY",
89
+ "openai-compatible": "MODEL_API_KEY",
90
+ }
91
+
92
+ FAKE_PROVIDER = "fake"
93
+
94
+ DEFAULT_MODEL_TIMEOUT_S = 60.0
95
+ DEFAULT_MODEL_MAX_RETRIES = 2
96
+
97
+ # The providers that speak the OpenAI API (langchain-openai's ChatOpenAI): the only ones
98
+ # the reasoning-effort and Responses-API settings apply to.
99
+ OPENAI_API_PROVIDERS = frozenset({"openai", "openai-compatible"})
100
+ # `MODEL_REASONING_EFFORT` / `JUDGE_REASONING_EFFORT`: what OpenAI's reasoning models accept
101
+ # (each model takes a subset; the API refuses the rest).
102
+ REASONING_EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh")
103
+ _TRUE = ("1", "true", "yes", "on")
104
+ _FALSE = ("0", "false", "no", "off")
105
+
106
+
107
+ def model_timeout_s() -> float | None:
108
+ """`MODEL_TIMEOUT_S` (default 60); `0` means the provider SDK's own default."""
109
+ raw = (os.environ.get("MODEL_TIMEOUT_S") or "").strip()
110
+ if not raw:
111
+ return DEFAULT_MODEL_TIMEOUT_S
112
+ try:
113
+ value = float(raw)
114
+ except ValueError:
115
+ raise SettingsError(f"MODEL_TIMEOUT_S={raw!r} is not a number.") from None
116
+ if value < 0 or value != value or value == float("inf"):
117
+ raise SettingsError(f"MODEL_TIMEOUT_S={raw} must be a finite number >= 0.")
118
+ return value or None
119
+
120
+
121
+ def model_max_retries() -> int:
122
+ """`MODEL_MAX_RETRIES` (default 2): retries of a failed model request."""
123
+ raw = (os.environ.get("MODEL_MAX_RETRIES") or "").strip()
124
+ if not raw:
125
+ return DEFAULT_MODEL_MAX_RETRIES
126
+ try:
127
+ value = int(raw)
128
+ except ValueError:
129
+ raise SettingsError(f"MODEL_MAX_RETRIES={raw!r} is not an integer.") from None
130
+ if value < 0:
131
+ raise SettingsError(f"MODEL_MAX_RETRIES={value} must be >= 0.")
132
+ return value
133
+
134
+
135
+ def model_limits() -> tuple[float | None, int]:
136
+ """Both limits, validated (startup check)."""
137
+ return model_timeout_s(), model_max_retries()
138
+
139
+
140
+ def _effort(name: str) -> str | None:
141
+ raw = (os.environ.get(name) or "").strip().lower()
142
+ if not raw:
143
+ return None
144
+ if raw not in REASONING_EFFORTS:
145
+ raise SettingsError(f"{name}={raw!r} must be one of {', '.join(REASONING_EFFORTS)}.")
146
+ return raw
147
+
148
+
149
+ def _switch(name: str) -> bool | None:
150
+ raw = (os.environ.get(name) or "").strip().lower()
151
+ if not raw:
152
+ return None
153
+ if raw in _TRUE:
154
+ return True
155
+ if raw in _FALSE:
156
+ return False
157
+ raise SettingsError(f"{name}={raw!r} must be true or false (unset: langchain-openai chooses).")
158
+
159
+
160
+ def model_api_options(*, judge: bool = False) -> dict[str, Any]:
161
+ """`{"reasoning_effort", "use_responses_api"}` of the agent's (or the judge's) model.
162
+
163
+ Only the options that are set; `SettingsError` for a bad value, or for one
164
+ set explicitly for a provider that is not an OpenAI-API one (`fake` takes
165
+ and ignores them). The judge's own `JUDGE_*` values win; unset, it keeps the
166
+ agent's when its provider is an OpenAI-API one too.
167
+ """
168
+ provider = model_settings(judge=judge)["provider"] or ""
169
+ options: dict[str, Any] = {}
170
+ for key, variable, read in (
171
+ ("reasoning_effort", "REASONING_EFFORT", _effort),
172
+ ("use_responses_api", "USE_RESPONSES_API", _switch),
173
+ ):
174
+ own = f"JUDGE_{variable}" if judge else f"MODEL_{variable}"
175
+ value = read(own)
176
+ explicit = value is not None
177
+ if judge and value is None:
178
+ value = read(f"MODEL_{variable}")
179
+ if value is None:
180
+ continue
181
+ if provider in OPENAI_API_PROVIDERS:
182
+ options[key] = value
183
+ elif provider != FAKE_PROVIDER and explicit:
184
+ raise SettingsError(
185
+ f"{own} applies to an OpenAI-API model (MODEL_PROVIDER openai or "
186
+ f"openai-compatible), not {provider!r}: unset it."
187
+ )
188
+ return options
189
+
190
+
191
+ def model_options() -> tuple[dict[str, Any], dict[str, Any]]:
192
+ """The agent's and the judge's options, validated (startup check)."""
193
+ return model_api_options(), model_api_options(judge=True)
194
+
195
+
196
+ def model_settings(*, judge: bool = False) -> dict[str, str | None]:
197
+ """Resolve provider, model name, base URL and key from the environment."""
198
+ provider = os.environ.get("MODEL_PROVIDER", "openai").strip().lower()
199
+ name = os.environ.get("MODEL_NAME", "").strip()
200
+ base_url = os.environ.get("OPENAI_BASE_URL") or None
201
+ api_key = os.environ.get(PROVIDER_KEY_VARS.get(provider, "MODEL_API_KEY")) or None
202
+ if judge:
203
+ provider = (os.environ.get("JUDGE_MODEL_PROVIDER") or provider).strip().lower()
204
+ name = (os.environ.get("JUDGE_MODEL_NAME") or name).strip()
205
+ base_url = os.environ.get("JUDGE_BASE_URL") or base_url
206
+ api_key = (
207
+ os.environ.get("JUDGE_API_KEY")
208
+ or os.environ.get(PROVIDER_KEY_VARS.get(provider, "MODEL_API_KEY"))
209
+ or api_key
210
+ )
211
+ return {"provider": provider, "name": name, "base_url": base_url, "api_key": api_key}
212
+
213
+
214
+ def model_label(*, judge: bool = False) -> str:
215
+ """`<provider>/<model>` as recorded in run records and eval traces."""
216
+ s = model_settings(judge=judge)
217
+ return f"{s['provider']}/{s['name'] or 'fake' if s['provider'] == FAKE_PROVIDER else s['name']}"
218
+
219
+
220
+ def build_model(
221
+ provider: str,
222
+ name: str,
223
+ *,
224
+ base_url: str | None = None,
225
+ api_key: str | None = None,
226
+ **kwargs: Any,
227
+ ) -> BaseChatModel:
228
+ """Build a chat model for `provider`; only `fake` avoids a provider package."""
229
+ if provider == FAKE_PROVIDER:
230
+ return FakeChatModel(**kwargs)
231
+ lc_provider = PROVIDER_TO_LANGCHAIN.get(provider)
232
+ if lc_provider is None:
233
+ raise ValueError(
234
+ f"Unknown MODEL_PROVIDER {provider!r}; expected one of "
235
+ f"{', '.join(PROVIDER_TO_LANGCHAIN)} (or 'fake' in tests)."
236
+ )
237
+ if not name:
238
+ raise ValueError("MODEL_NAME is not set; put the model name in .env or the chart values.")
239
+ params: dict[str, Any] = dict(kwargs)
240
+ timeout = model_timeout_s()
241
+ if timeout is not None:
242
+ params.setdefault("timeout", timeout)
243
+ params.setdefault("max_retries", model_max_retries())
244
+ if api_key:
245
+ params["api_key"] = api_key
246
+ if provider == "openai-compatible":
247
+ if not base_url:
248
+ raise ValueError("OPENAI_BASE_URL is required when MODEL_PROVIDER=openai-compatible.")
249
+ params["base_url"] = base_url
250
+ if lc_provider == "openai":
251
+ # Ask for token usage on streamed responses (stream_options.include_usage).
252
+ # langchain-openai does this by itself only for api.openai.com, so without it an
253
+ # OpenAI-compatible endpoint (a proxy, gateway or self-hosted server) records zero
254
+ # usage in run records and eval traces, and `expect.max_tokens` passes vacuously.
255
+ params.setdefault("stream_usage", True)
256
+ from langchain.chat_models import init_chat_model
257
+
258
+ return init_chat_model(name, model_provider=lc_provider, **params)
259
+
260
+
261
+ def get_model(**kwargs: Any) -> BaseChatModel:
262
+ """The agent's chat model, from MODEL_PROVIDER / MODEL_NAME / OPENAI_BASE_URL (and, for an
263
+ OpenAI-API model, MODEL_REASONING_EFFORT / MODEL_USE_RESPONSES_API)."""
264
+ s = model_settings()
265
+ return build_model(
266
+ s["provider"] or "",
267
+ s["name"] or "",
268
+ base_url=s["base_url"],
269
+ api_key=s["api_key"],
270
+ **{**model_api_options(), **kwargs},
271
+ )
272
+
273
+
274
+ def get_judge_model(**kwargs: Any) -> BaseChatModel:
275
+ """The judge model, from JUDGE_* with the agent's values as defaults."""
276
+ s = model_settings(judge=True)
277
+ return build_model(
278
+ s["provider"] or "",
279
+ s["name"] or "",
280
+ base_url=s["base_url"],
281
+ api_key=s["api_key"],
282
+ **{**model_api_options(judge=True), **kwargs},
283
+ )
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Deterministic fake model (tests and CI only)
288
+ # ---------------------------------------------------------------------------
289
+
290
+ _GREETINGS = ("hi", "hello", "hey", "good morning", "good afternoon", "good evening")
291
+
292
+ # Words of a tool name that say nothing about what the tool is for: `list_orders`
293
+ # is picked by "orders", never by "list".
294
+ _GENERIC_NAME_WORDS = frozenset(
295
+ "all and api call create delete fetch find for from get list look lookup make new "
296
+ "query run search set the tool update with".split()
297
+ )
298
+ # The subject of a request: the text after its last "in", "for" or "about".
299
+ _SUBJECT_MARKER = re.compile(r"\b(?:in|for|about)\s+", re.IGNORECASE)
300
+ # A value per JSON-schema type for the arguments a fake tool call must fill.
301
+ _PLACEHOLDER_BY_TYPE: dict[str, Any] = {
302
+ "integer": 1,
303
+ "number": 1,
304
+ "boolean": False,
305
+ "array": [],
306
+ "object": {},
307
+ }
308
+
309
+
310
+ def _tool_spec(tool: Any) -> dict[str, Any] | None:
311
+ """``{"name", "description", "parameters"}`` of a bound tool, or None when unreadable."""
312
+ from langchain_core.utils.function_calling import convert_to_openai_tool
313
+
314
+ try:
315
+ function = convert_to_openai_tool(tool).get("function") or {}
316
+ except Exception: # an unusual tool object: bound, but never called by the fake model
317
+ return None
318
+ name = function.get("name")
319
+ if not name:
320
+ return None
321
+ return {
322
+ "name": str(name),
323
+ "description": str(function.get("description") or ""),
324
+ "parameters": function.get("parameters") or {},
325
+ }
326
+
327
+
328
+ def _name_words(name: str) -> set[str]:
329
+ words = {w for w in re.split(r"[^a-z0-9]+", name.lower()) if w}
330
+ return {w for w in words if w not in _GENERIC_NAME_WORDS and len(w) > 2}
331
+
332
+
333
+ def _mentions(prompt: str, name: str) -> bool:
334
+ """The prompt names the tool, or a distinctive word of its name (a plural counts)."""
335
+ lowered = prompt.lower()
336
+ if name.lower() in lowered:
337
+ return True
338
+ words = set(re.findall(r"[a-z0-9]+", lowered))
339
+ for word in _name_words(name):
340
+ singular = word[:-1] if word.endswith("s") else word
341
+ if {word, singular, f"{singular}s"} & words:
342
+ return True
343
+ return False
344
+
345
+
346
+ def _subject(prompt: str) -> str:
347
+ text = prompt.strip()
348
+ last = None
349
+ for match in _SUBJECT_MARKER.finditer(text):
350
+ last = match
351
+ subject = text[last.end() :] if last is not None else text
352
+ return subject.strip().rstrip("?.!").strip() or text
353
+
354
+
355
+ def _fake_args(parameters: dict[str, Any], prompt: str) -> dict[str, Any]:
356
+ """A value for every required argument: the request's subject for text, a fixed one else."""
357
+ properties = parameters.get("properties") or {}
358
+ args: dict[str, Any] = {}
359
+ for name in parameters.get("required") or []:
360
+ schema = properties.get(name) or {}
361
+ if "const" in schema: # a Literal of one value
362
+ args[name] = schema["const"]
363
+ elif schema.get("enum"):
364
+ args[name] = schema["enum"][0]
365
+ else:
366
+ args[name] = _PLACEHOLDER_BY_TYPE.get(schema.get("type"), _subject(prompt))
367
+ return args
368
+
369
+
370
+ def _misfit(schema: Any, value: Any, root: Any) -> bool:
371
+ """Whether `value` breaks `schema` (a part of the response schema `root`)."""
372
+ return bool(validate(schema, value, root=root))
373
+
374
+
375
+ def _fake_answer(schema: Any, text: str, root: Any = None) -> Any:
376
+ """A value that fits `schema` (a response schema), built from `text`: deterministic.
377
+
378
+ Every property is filled (as strict structured output does): a string with
379
+ `text` (cut to `maxLength`), a number with its `minimum` (else 1), a flag
380
+ with false, a list with `minItems` items, an enum or a choice with its
381
+ first option. A `pattern` is not followed: a string that breaks it takes the
382
+ first later choice that fits (of an `anyOf` or `oneOf`, or `null` in a type
383
+ list), so an optional id with a pattern is null; with no such choice the
384
+ answer does not fit (the tests' failing path).
385
+ """
386
+ root = schema if root is None else root
387
+ if not isinstance(schema, dict):
388
+ return text
389
+ if isinstance(schema.get("$ref"), str) and schema["$ref"].startswith("#"):
390
+ node: Any = root
391
+ for key in [k for k in schema["$ref"][1:].split("/") if k]:
392
+ node = node.get(key, {}) if isinstance(node, dict) else {}
393
+ return _fake_answer(node, text, root)
394
+ if "const" in schema:
395
+ return schema["const"]
396
+ if schema.get("enum"):
397
+ return schema["enum"][0]
398
+ for key in ("anyOf", "oneOf"):
399
+ if schema.get(key):
400
+ values = [_fake_answer(option, text, root) for option in schema[key]]
401
+ fits = (v for v, o in zip(values, schema[key], strict=True) if not _misfit(o, v, root))
402
+ return next(fits, values[0])
403
+ if schema.get("allOf"):
404
+ return _fake_answer(schema["allOf"][0], text, root)
405
+ types = schema.get("type")
406
+ types = types if isinstance(types, list) else [types]
407
+ kind = next((t for t in types if t != "null"), None if "null" in types else "string")
408
+ if kind is not None and "null" in types:
409
+ value = _fake_answer({**schema, "type": kind}, text, root)
410
+ return None if _misfit(schema, value, root) else value
411
+ if kind == "object":
412
+ properties = schema.get("properties") or {}
413
+ return {name: _fake_answer(sub, text, root) for name, sub in properties.items()}
414
+ if kind == "array":
415
+ return [_fake_answer(schema.get("items") or {}, text, root)] * int(
416
+ schema.get("minItems") or 0
417
+ )
418
+ if kind in ("integer", "number"):
419
+ return schema.get("minimum", 1)
420
+ if kind == "boolean":
421
+ return False
422
+ if kind is None:
423
+ return None
424
+ limit = schema.get("maxLength")
425
+ return text[:limit] if isinstance(limit, int) else text
426
+
427
+
428
+ def _first_sentence(text: str) -> str:
429
+ return text.strip().split("\n")[0].split(". ")[0].rstrip(".")
430
+
431
+
432
+ def _text_of(message: BaseMessage) -> str:
433
+ content = message.content
434
+ if isinstance(content, str):
435
+ return content
436
+ if isinstance(content, list):
437
+ return "".join(
438
+ block.get("text", "") if isinstance(block, dict) else str(block) for block in content
439
+ )
440
+ return str(content)
441
+
442
+
443
+ def _usage(prompt: str, completion: str) -> dict[str, int]:
444
+ inp = max(1, len(prompt.split()))
445
+ out = max(1, len(completion.split()))
446
+ return {"input_tokens": inp, "output_tokens": out, "total_tokens": inp + out}
447
+
448
+
449
+ class FakeChatModel(BaseChatModel):
450
+ """Deterministic chat model for tests: the reply depends only on the input.
451
+
452
+ It knows no tool by name: it calls whichever bound tool the request
453
+ mentions, so the tests and the eval smoke cases keep working when the
454
+ project's tools change. Replies (all stable across calls and safe under
455
+ concurrency):
456
+ * after a tool result: ``Here is what I found: <tool result>`` (the tool's own
457
+ text: the `<tool_output>` fence the agent adds is taken off; so is the
458
+ `<agent_request>` fence of a request another agent presents)
459
+ * a judge prompt (mentions "score" and "JSON"): ``{"score": 5, "explanation": ...}``
460
+ * a request that mentions a bound tool (its name, or a distinctive word of
461
+ it: "weather" for `get_weather`, "orders" for `list_orders`): a call of
462
+ the first such tool. Every required argument is filled: text with the
463
+ request's subject (what follows its last "in", "for" or "about", else
464
+ the whole request; "Paris" in "What is the weather in Paris?"), an enum
465
+ with its first value (a constant with its value), a number with 1, a
466
+ flag with false, a list or an object empty
467
+ * a greeting: ``Hello! How can I help you today?``
468
+ * anything else: ``I am a fake model. I can use these tools: <name> (<first
469
+ sentence of its description>), ... You said: <text>`` (without the tools
470
+ part when none is bound)
471
+
472
+ Structured answers (a response schema, `structured.py`): bound with a
473
+ provider `response_format`, a reply that would be text is instead that text
474
+ as a JSON answer (`_fake_answer`); bound with a forced `tool_choice` and the
475
+ answer tool (the tool strategy), it is a call of the answer tool with that
476
+ answer. It never calls the answer tool for a request that names it.
477
+ """
478
+
479
+ bound_tools: list[str] = Field(default_factory=list)
480
+ tool_specs: list[dict[str, Any]] = Field(default_factory=list)
481
+ # The response schema of a provider `response_format`, when one is bound.
482
+ answer_schema: dict[str, Any] | None = None
483
+ # A bound `tool_choice` that forces a tool call ("any", "required", true).
484
+ forced_tool: bool = False
485
+
486
+ @property
487
+ def _llm_type(self) -> str:
488
+ return "fake"
489
+
490
+ @property
491
+ def _identifying_params(self) -> dict[str, Any]:
492
+ return {"bound_tools": list(self.bound_tools)}
493
+
494
+ def bind_tools(self, tools: Sequence[Any], **kwargs: Any) -> Any: # type: ignore[override]
495
+ specs = [spec for spec in (_tool_spec(t) for t in tools) if spec is not None]
496
+ response_format = kwargs.get("response_format")
497
+ schema = None
498
+ if isinstance(response_format, dict):
499
+ schema = (response_format.get("json_schema") or {}).get("schema")
500
+ return self.model_copy(
501
+ update={
502
+ "bound_tools": [s["name"] for s in specs],
503
+ "tool_specs": specs,
504
+ "answer_schema": schema if isinstance(schema, dict) else None,
505
+ "forced_tool": kwargs.get("tool_choice") in ("any", "required", True),
506
+ }
507
+ )
508
+
509
+ def _structured(self, reply: AIMessage) -> AIMessage:
510
+ """A text reply as a structured answer, when one is bound; else the reply itself."""
511
+ if reply.tool_calls:
512
+ return reply
513
+ text = str(reply.content)
514
+ if self.answer_schema is not None:
515
+ answer = _fake_answer(self.answer_schema, text)
516
+ content = json.dumps(answer, ensure_ascii=False)
517
+ return AIMessage(content=content, usage_metadata=reply.usage_metadata)
518
+ spec = next((s for s in self.tool_specs if s["name"] == ANSWER_TOOL), None)
519
+ if self.forced_tool and spec is not None:
520
+ answer = _fake_answer(spec["parameters"], text)
521
+ return AIMessage(
522
+ content="",
523
+ tool_calls=[{"name": ANSWER_TOOL, "args": answer, "id": f"call_{ANSWER_TOOL}"}],
524
+ usage_metadata=reply.usage_metadata,
525
+ )
526
+ return reply
527
+
528
+ def _tool_call(self, prompt: str) -> AIMessage | None:
529
+ for spec in self.tool_specs:
530
+ if spec["name"] != ANSWER_TOOL and _mentions(prompt, spec["name"]):
531
+ args = _fake_args(spec["parameters"], prompt)
532
+ return AIMessage(
533
+ content="",
534
+ tool_calls=[{"name": spec["name"], "args": args, "id": f"call_{spec['name']}"}],
535
+ usage_metadata=_usage(prompt, spec["name"]),
536
+ )
537
+ return None
538
+
539
+ def _reply(self, messages: list[BaseMessage]) -> AIMessage:
540
+ return self._structured(self._text_reply(messages))
541
+
542
+ def _text_reply(self, messages: list[BaseMessage]) -> AIMessage:
543
+ last = messages[-1]
544
+ if isinstance(last, ToolMessage):
545
+ # The agent fences tool results (`UntrustedToolResults`); echo the tool's own text.
546
+ found = unfence_tool_output(_text_of(last))
547
+ text = f"Here is what I found: {found}"
548
+ return AIMessage(content=text, usage_metadata=_usage(found, text))
549
+ # A delegated run's request is fenced as the calling agent's: read the request itself.
550
+ prompt = unfence_agent_request(_text_of(last))
551
+ lowered = prompt.lower()
552
+ if "score" in lowered and "json" in lowered:
553
+ text = json.dumps({"score": 5, "explanation": "fake judge: deterministic pass"})
554
+ return AIMessage(content=text, usage_metadata=_usage(prompt, text))
555
+ call = self._tool_call(prompt)
556
+ if call is not None:
557
+ return call
558
+ if lowered.strip(" !.?,") in _GREETINGS or lowered.startswith(_GREETINGS):
559
+ text = "Hello! How can I help you today?"
560
+ return AIMessage(content=text, usage_metadata=_usage(prompt, text))
561
+ tools = ", ".join(
562
+ f"{s['name']} ({_first_sentence(s['description'])})" if s["description"] else s["name"]
563
+ for s in self.tool_specs
564
+ )
565
+ can = f" I can use these tools: {tools}." if tools else ""
566
+ text = f"I am a fake model.{can} You said: {prompt}"
567
+ return AIMessage(content=text, usage_metadata=_usage(prompt, text))
568
+
569
+ def _generate(
570
+ self,
571
+ messages: list[BaseMessage],
572
+ stop: list[str] | None = None,
573
+ run_manager: Any = None,
574
+ **kwargs: Any,
575
+ ) -> ChatResult:
576
+ return ChatResult(generations=[ChatGeneration(message=self._reply(messages))])
577
+
578
+ def _stream(
579
+ self,
580
+ messages: list[BaseMessage],
581
+ stop: list[str] | None = None,
582
+ run_manager: Any = None,
583
+ **kwargs: Any,
584
+ ) -> Iterator[ChatGenerationChunk]:
585
+ reply = self._reply(messages)
586
+ if reply.tool_calls:
587
+ call = reply.tool_calls[0]
588
+ chunk = AIMessageChunk(
589
+ content="",
590
+ tool_call_chunks=[
591
+ {
592
+ "name": call["name"],
593
+ "args": json.dumps(call["args"]),
594
+ "id": call["id"],
595
+ "index": 0,
596
+ }
597
+ ],
598
+ usage_metadata=reply.usage_metadata,
599
+ )
600
+ yield ChatGenerationChunk(message=chunk)
601
+ return
602
+ words = str(reply.content).split(" ")
603
+ for i, word in enumerate(words):
604
+ last = i == len(words) - 1
605
+ piece = word if last else word + " "
606
+ chunk = AIMessageChunk(
607
+ content=piece, usage_metadata=reply.usage_metadata if last else None
608
+ )
609
+ if run_manager is not None:
610
+ run_manager.on_llm_new_token(piece, chunk=ChatGenerationChunk(message=chunk))
611
+ yield ChatGenerationChunk(message=chunk)