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.
- graph_agents_cli/__init__.py +26 -0
- graph_agents_cli/_api_policy.py +2145 -0
- graph_agents_cli/_approvals.py +400 -0
- graph_agents_cli/_build.py +186 -0
- graph_agents_cli/_build_info.json +7 -0
- graph_agents_cli/_chat_client.py +462 -0
- graph_agents_cli/_click.py +157 -0
- graph_agents_cli/_defaults.py +139 -0
- graph_agents_cli/_experiments.py +64 -0
- graph_agents_cli/_http.py +192 -0
- graph_agents_cli/_output.py +83 -0
- graph_agents_cli/_project.py +462 -0
- graph_agents_cli/_remote.py +220 -0
- graph_agents_cli/_response_schema.py +264 -0
- graph_agents_cli/_runner.py +319 -0
- graph_agents_cli/_skills_check.py +274 -0
- graph_agents_cli/_tools.py +189 -0
- graph_agents_cli/_trust.py +66 -0
- graph_agents_cli/api/__init__.py +15 -0
- graph_agents_cli/api/_changes.py +506 -0
- graph_agents_cli/api/_files.py +658 -0
- graph_agents_cli/api/cmd_api.py +2480 -0
- graph_agents_cli/deploy/__init__.py +15 -0
- graph_agents_cli/deploy/_config.py +171 -0
- graph_agents_cli/deploy/_image.py +128 -0
- graph_agents_cli/deploy/_kube.py +286 -0
- graph_agents_cli/deploy/_modes.py +234 -0
- graph_agents_cli/deploy/_preflight.py +370 -0
- graph_agents_cli/deploy/_values.py +168 -0
- graph_agents_cli/deploy/cmd_deploy.py +1866 -0
- graph_agents_cli/deploy/gitops.py +562 -0
- graph_agents_cli/deploy/local_load.py +273 -0
- graph_agents_cli/dev/__init__.py +13 -0
- graph_agents_cli/dev/cmd_build.py +131 -0
- graph_agents_cli/dev/cmd_install.py +78 -0
- graph_agents_cli/dev/cmd_lint.py +119 -0
- graph_agents_cli/dev/cmd_playground.py +297 -0
- graph_agents_cli/dev/policy_check.py +1287 -0
- graph_agents_cli/eval/__init__.py +22 -0
- graph_agents_cli/eval/_client.py +670 -0
- graph_agents_cli/eval/_common.py +177 -0
- graph_agents_cli/eval/_judge.py +168 -0
- graph_agents_cli/eval/_judge_runner.py +238 -0
- graph_agents_cli/eval/_paths.py +212 -0
- graph_agents_cli/eval/checks.py +581 -0
- graph_agents_cli/eval/cmd_analyze.py +278 -0
- graph_agents_cli/eval/cmd_compare.py +284 -0
- graph_agents_cli/eval/cmd_eval_group.py +80 -0
- graph_agents_cli/eval/cmd_generate.py +558 -0
- graph_agents_cli/eval/cmd_grade.py +466 -0
- graph_agents_cli/eval/cmd_metric.py +156 -0
- graph_agents_cli/eval/cmd_run.py +370 -0
- graph_agents_cli/eval/cmd_submit.py +400 -0
- graph_agents_cli/eval/config.py +435 -0
- graph_agents_cli/eval/dataset.py +350 -0
- graph_agents_cli/eval/gate.py +420 -0
- graph_agents_cli/eval/transcript.py +192 -0
- graph_agents_cli/extension/__init__.py +13 -0
- graph_agents_cli/extension/_compat.py +86 -0
- graph_agents_cli/extension/_loader.py +293 -0
- graph_agents_cli/extension/_manifest.py +135 -0
- graph_agents_cli/extension/_overrides.py +195 -0
- graph_agents_cli/extension/_paths.py +91 -0
- graph_agents_cli/extension/_refs.py +193 -0
- graph_agents_cli/extension/_resolver.py +453 -0
- graph_agents_cli/extension/_schema.py +106 -0
- graph_agents_cli/extension/_spec.py +253 -0
- graph_agents_cli/extension/_sync.py +102 -0
- graph_agents_cli/extension/_trust.py +58 -0
- graph_agents_cli/extension/cmd_extension_add.py +259 -0
- graph_agents_cli/extension/cmd_extension_group.py +57 -0
- graph_agents_cli/extension/cmd_extension_list.py +56 -0
- graph_agents_cli/extension/cmd_extension_remove.py +61 -0
- graph_agents_cli/extension/cmd_extension_update.py +195 -0
- graph_agents_cli/info/__init__.py +13 -0
- graph_agents_cli/info/cmd_info.py +222 -0
- graph_agents_cli/infra/__init__.py +15 -0
- graph_agents_cli/infra/checks.py +1169 -0
- graph_agents_cli/infra/cmd_infra.py +103 -0
- graph_agents_cli/main.py +591 -0
- graph_agents_cli/peer/__init__.py +15 -0
- graph_agents_cli/peer/_generate.py +254 -0
- graph_agents_cli/peer/cmd_peer.py +1151 -0
- graph_agents_cli/run/__init__.py +13 -0
- graph_agents_cli/run/_local_server.py +1157 -0
- graph_agents_cli/run/_signals.py +141 -0
- graph_agents_cli/run/cmd_approvals.py +530 -0
- graph_agents_cli/run/cmd_run.py +1421 -0
- graph_agents_cli/scaffold/__init__.py +19 -0
- graph_agents_cli/scaffold/agents/README.md +24 -0
- graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
- graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
- graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
- graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
- graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
- graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
- graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
- graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
- graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
- graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
- graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
- graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
- graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
- graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
- graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
- graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
- graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
- graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
- graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
- graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
- graph_agents_cli/scaffold/commands/__init__.py +13 -0
- graph_agents_cli/scaffold/commands/create.py +1424 -0
- graph_agents_cli/scaffold/commands/enhance.py +1652 -0
- graph_agents_cli/scaffold/commands/upgrade.py +570 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
- graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
- graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
- graph_agents_cli/scaffold/utils/__init__.py +13 -0
- graph_agents_cli/scaffold/utils/backup.py +212 -0
- graph_agents_cli/scaffold/utils/build_record.py +257 -0
- graph_agents_cli/scaffold/utils/cli_options.py +184 -0
- graph_agents_cli/scaffold/utils/fs.py +83 -0
- graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
- graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
- graph_agents_cli/scaffold/utils/keyedit.py +768 -0
- graph_agents_cli/scaffold/utils/keymerge.py +537 -0
- graph_agents_cli/scaffold/utils/language.py +138 -0
- graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
- graph_agents_cli/scaffold/utils/logging.py +77 -0
- graph_agents_cli/scaffold/utils/manifest.py +292 -0
- graph_agents_cli/scaffold/utils/merge.py +970 -0
- graph_agents_cli/scaffold/utils/merge3.py +216 -0
- graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
- graph_agents_cli/scaffold/utils/remote_template.py +376 -0
- graph_agents_cli/scaffold/utils/template.py +1352 -0
- graph_agents_cli/scaffold/utils/upgrade.py +894 -0
- graph_agents_cli/scaffold/utils/version.py +438 -0
- graph_agents_cli/secrets/__init__.py +15 -0
- graph_agents_cli/secrets/_apply.py +954 -0
- graph_agents_cli/secrets/_required.py +188 -0
- graph_agents_cli/secrets/cmd_secrets.py +211 -0
- graph_agents_cli/setup/__init__.py +13 -0
- graph_agents_cli/setup/_antigravity.py +221 -0
- graph_agents_cli/setup/cmd_auth.py +1030 -0
- graph_agents_cli/setup/cmd_dev_token.py +513 -0
- graph_agents_cli/setup/cmd_setup.py +428 -0
- graph_agents_cli/setup/cmd_update.py +140 -0
- graph_agents_cli/skills/__init__.py +13 -0
- graph_agents_cli/skills/_bundle.py +65 -0
- graph_agents_cli/skills/data/README.md +19 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
- graph_agents_cli/system/__init__.py +15 -0
- graph_agents_cli/system/_apply.py +519 -0
- graph_agents_cli/system/_checks.py +1023 -0
- graph_agents_cli/system/_deploy.py +215 -0
- graph_agents_cli/system/_model.py +363 -0
- graph_agents_cli/system/_system.py +664 -0
- graph_agents_cli/system/_views.py +208 -0
- graph_agents_cli/system/cmd_system.py +423 -0
- graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
- graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
- graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
- graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
- graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
- graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
|
@@ -0,0 +1,479 @@
|
|
|
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
|
+
"""An agent asks another for the user, and relays the user's approval (end to end).
|
|
16
|
+
|
|
17
|
+
One app plays both agents, in process: the concierge (alice talks to it over
|
|
18
|
+
`/chat` with her own token; its tools are `peer_tools`, calling the other agent
|
|
19
|
+
over the ASGI transport, with a token the fake issuer mints in exchange for
|
|
20
|
+
alice's) and orders (its A2A endpoint, and a `cancelOrder` call to a backend
|
|
21
|
+
that records every request, gated so that the concierge may relay alice's
|
|
22
|
+
decision). The loop check's idea of "this agent" is pinned to the concierge
|
|
23
|
+
(both agents are one process here).
|
|
24
|
+
|
|
25
|
+
* The relay: alice asks the concierge to cancel; orders pauses for approval;
|
|
26
|
+
alice approves at the concierge, seeing orders' call as the `effect`; the
|
|
27
|
+
decision reaches orders once, the order is cancelled once, orders records
|
|
28
|
+
`decided_via: concierge`, its waiting task follows, and the token was
|
|
29
|
+
exchanged once for the pair (alice, orders).
|
|
30
|
+
* When orders checks the order against the user's own words, the run alice's
|
|
31
|
+
relayed approval resumes there checks the words she asked with, not those she
|
|
32
|
+
said when approving at the concierge.
|
|
33
|
+
* When orders lost the task before alice decided, the relay reads its approvals
|
|
34
|
+
ledger instead and still sends exactly the decision she approved.
|
|
35
|
+
* With orders' gate at `decide_with: direct`, the concierge relays nothing: it
|
|
36
|
+
says alice must approve at orders, and she does, with her own token there.
|
|
37
|
+
|
|
38
|
+
Every test runs with the in-memory checkpointer, and on Postgres when
|
|
39
|
+
`TEST_POSTGRES_DSN` is set (its sessions in a time zone other than UTC).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import asyncio
|
|
45
|
+
import json
|
|
46
|
+
import os
|
|
47
|
+
import re
|
|
48
|
+
import time
|
|
49
|
+
import uuid
|
|
50
|
+
from collections.abc import AsyncIterator, Iterator
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import Any
|
|
53
|
+
from urllib.parse import urlsplit
|
|
54
|
+
|
|
55
|
+
# The environment must be in place before the app (and the graph) is imported.
|
|
56
|
+
os.environ.update(
|
|
57
|
+
{
|
|
58
|
+
"MODEL_PROVIDER": "fake",
|
|
59
|
+
"MODEL_NAME": "fake",
|
|
60
|
+
"CHECKPOINTER": "memory",
|
|
61
|
+
"AUTH_POLICY": "shared-bearer",
|
|
62
|
+
"API_KEY": "test-key",
|
|
63
|
+
"APP_ENV": "dev",
|
|
64
|
+
"TRACING_ENABLED": "false",
|
|
65
|
+
"RUNTIME": "fastapi",
|
|
66
|
+
"APP_URL": "http://testserver",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
import httpx
|
|
71
|
+
import jwt
|
|
72
|
+
import pytest
|
|
73
|
+
from fake_issuer import FakeIssuer
|
|
74
|
+
from langchain.tools import ToolRuntime
|
|
75
|
+
from langchain_core.tools import tool
|
|
76
|
+
|
|
77
|
+
from {{cookiecutter.agent_directory}}.app_utils import a2a as a2a_module
|
|
78
|
+
from {{cookiecutter.agent_directory}}.app_utils import token_exchange
|
|
79
|
+
from {{cookiecutter.agent_directory}}.app_utils.a2a_client import (
|
|
80
|
+
context_id_for,
|
|
81
|
+
peer_tools,
|
|
82
|
+
reset_a2a_client,
|
|
83
|
+
)
|
|
84
|
+
from {{cookiecutter.agent_directory}}.app_utils.api_client import (
|
|
85
|
+
get_client,
|
|
86
|
+
require_user_mentioned,
|
|
87
|
+
)
|
|
88
|
+
from {{cookiecutter.agent_directory}}.app_utils.api_client import (
|
|
89
|
+
reset_policy_cache as reset_api_policy,
|
|
90
|
+
)
|
|
91
|
+
from {{cookiecutter.agent_directory}}.app_utils.auth import reset_policy_cache
|
|
92
|
+
from {{cookiecutter.agent_directory}}.fast_api_app import app
|
|
93
|
+
|
|
94
|
+
A2A_PATH = "/a2a/{{cookiecutter.agent_directory}}"
|
|
95
|
+
SECRET = "s3cret"
|
|
96
|
+
|
|
97
|
+
POLICY = """
|
|
98
|
+
apis:
|
|
99
|
+
orders_agent:
|
|
100
|
+
description: "Orders agent: cancels the caller's orders after approval."
|
|
101
|
+
protocol: a2a
|
|
102
|
+
a2a: {path: /a2a/{{cookiecutter.agent_directory}}}
|
|
103
|
+
base_url_env: ORDERS_AGENT_URL
|
|
104
|
+
auth: exchange
|
|
105
|
+
exchange: {audience: orders}
|
|
106
|
+
allowed_methods: [GET, POST]
|
|
107
|
+
allowed_operations:
|
|
108
|
+
- operationId: getAgentCard
|
|
109
|
+
methods: [GET]
|
|
110
|
+
path: /a2a/{{cookiecutter.agent_directory}}/.well-known/agent-card.json
|
|
111
|
+
- {rpc_method: SendMessage, methods: [POST], path: /a2a/{{cookiecutter.agent_directory}}}
|
|
112
|
+
- {rpc_method: GetTask, methods: [POST], path: /a2a/{{cookiecutter.agent_directory}}}
|
|
113
|
+
- {operationId: listContextApprovals, methods: [GET], path: "/threads/{context_id}/approvals"}
|
|
114
|
+
approval:
|
|
115
|
+
- required_for: {operations: [{a2a_operation: approve}]}
|
|
116
|
+
approvers: [requester]
|
|
117
|
+
timeout_s: 900
|
|
118
|
+
limits: {max_calls_per_run: 12, max_response_bytes: 1048576}
|
|
119
|
+
timeouts_ms: {connect: 2000, read: 120000}
|
|
120
|
+
shop:
|
|
121
|
+
base_url_env: SHOP_API_BASE_URL
|
|
122
|
+
auth: none
|
|
123
|
+
allowed_methods: [GET, POST]
|
|
124
|
+
approval:
|
|
125
|
+
required_for:
|
|
126
|
+
operations:
|
|
127
|
+
- operationId: cancelOrder
|
|
128
|
+
path: /orders/{order_id}/cancel
|
|
129
|
+
methods: [POST]
|
|
130
|
+
approvers: [requester]
|
|
131
|
+
timeout_s: 60
|
|
132
|
+
"""
|
|
133
|
+
RELAYED = " decide_with: relayed\n relayers: [concierge]\n"
|
|
134
|
+
PEERS = {
|
|
135
|
+
"orders": {
|
|
136
|
+
"api": "orders_agent",
|
|
137
|
+
"approvals": "relay",
|
|
138
|
+
"description": "Orders agent: cancels the caller's orders after approval.",
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# What orders' cancel sends: an integer a double cannot hold, which the person must see exactly.
|
|
143
|
+
CANCEL_BODY = {"reason": "the customer asked", "amount": 12345678901234567}
|
|
144
|
+
# What the backend received.
|
|
145
|
+
SENT: list[httpx.Request] = []
|
|
146
|
+
# When it holds True, orders cancels only an order the user's own words name.
|
|
147
|
+
CHECK_MENTION: list[bool] = []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _backend(request: httpx.Request) -> httpx.Response:
|
|
151
|
+
SENT.append(request)
|
|
152
|
+
return httpx.Response(200, json={"cancelled": request.url.path})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@tool
|
|
156
|
+
async def cancel_order(order_id: int, runtime: ToolRuntime[Any]) -> str:
|
|
157
|
+
"""Cancel an order by its number."""
|
|
158
|
+
if CHECK_MENTION:
|
|
159
|
+
require_user_mentioned(order_id, runtime)
|
|
160
|
+
context = getattr(runtime, "context", None)
|
|
161
|
+
client = get_client("shop", context=context, transport=httpx.MockTransport(_backend))
|
|
162
|
+
data = await client.post(
|
|
163
|
+
"/orders/{order_id}/cancel",
|
|
164
|
+
operation_id="cancelOrder",
|
|
165
|
+
path_params={"order_id": order_id},
|
|
166
|
+
json_body=CANCEL_BODY,
|
|
167
|
+
)
|
|
168
|
+
return json.dumps(data)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.fixture(scope="module")
|
|
172
|
+
def issuer() -> Iterator[FakeIssuer]:
|
|
173
|
+
server = FakeIssuer({"concierge": (SECRET, {"orders"})})
|
|
174
|
+
try:
|
|
175
|
+
yield server
|
|
176
|
+
finally:
|
|
177
|
+
server.close()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.fixture(params=["memory", "postgres"])
|
|
184
|
+
async def database(
|
|
185
|
+
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
|
|
186
|
+
) -> AsyncIterator[str]:
|
|
187
|
+
if request.param == "memory":
|
|
188
|
+
yield "memory"
|
|
189
|
+
return
|
|
190
|
+
if not ADMIN_DSN:
|
|
191
|
+
pytest.skip("TEST_POSTGRES_DSN is not set")
|
|
192
|
+
import psycopg
|
|
193
|
+
|
|
194
|
+
name = f"gac_test_{uuid.uuid4().hex[:12]}"
|
|
195
|
+
async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
|
|
196
|
+
await admin.execute(f'CREATE DATABASE "{name}"')
|
|
197
|
+
# Sessions answer in a zone other than UTC (as a server set up on a laptop does).
|
|
198
|
+
await admin.execute(f"ALTER DATABASE \"{name}\" SET timezone TO 'America/New_York'")
|
|
199
|
+
monkeypatch.setenv("CHECKPOINTER", "postgres")
|
|
200
|
+
monkeypatch.setenv("POSTGRES_DSN", urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
|
|
201
|
+
yield "postgres"
|
|
202
|
+
async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
|
|
203
|
+
await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _agents(
|
|
207
|
+
issuer: FakeIssuer,
|
|
208
|
+
tmp_path: Path,
|
|
209
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
210
|
+
use_test_tools: Any,
|
|
211
|
+
decide: str,
|
|
212
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
213
|
+
policy = tmp_path / "api-policy.yaml"
|
|
214
|
+
policy.write_text(POLICY + decide, encoding="utf-8")
|
|
215
|
+
for name, value in {
|
|
216
|
+
"API_POLICY_PATH": str(policy),
|
|
217
|
+
"SHOP_API_BASE_URL": "http://shop.test",
|
|
218
|
+
"ORDERS_AGENT_URL": "http://testserver",
|
|
219
|
+
"AUTH_POLICY": "jwt",
|
|
220
|
+
"AUTH_JWT_JWKS_URL": issuer.jwks_url,
|
|
221
|
+
"AUTH_JWT_ISSUER": issuer.issuer,
|
|
222
|
+
"AUTH_JWT_AUDIENCE": "concierge,orders",
|
|
223
|
+
"AUTH_ALLOWED_ACTORS": "concierge",
|
|
224
|
+
"TOKEN_EXCHANGE_URL": issuer.token_url,
|
|
225
|
+
"TOKEN_EXCHANGE_CLIENT_ID": "concierge",
|
|
226
|
+
"TOKEN_EXCHANGE_CLIENT_SECRET": SECRET,
|
|
227
|
+
}.items():
|
|
228
|
+
monkeypatch.setenv(name, value)
|
|
229
|
+
for name in ("AUTH_JWT_PUBLIC_KEY", "TRACE_CAPTURE", "PRINCIPAL_HASH_SALT", "A2A_NAME"):
|
|
230
|
+
monkeypatch.delenv(name, raising=False)
|
|
231
|
+
# One process plays both agents: "this agent", for the loop check, is the concierge.
|
|
232
|
+
monkeypatch.setattr(token_exchange, "own_names", lambda env=None: {"concierge"})
|
|
233
|
+
reset_policy_cache()
|
|
234
|
+
reset_api_policy()
|
|
235
|
+
reset_a2a_client()
|
|
236
|
+
token_exchange.reset_token_exchange()
|
|
237
|
+
SENT.clear()
|
|
238
|
+
CHECK_MENTION.clear()
|
|
239
|
+
issuer.requests.clear()
|
|
240
|
+
user = f"alice-{uuid.uuid4().hex[:8]}"
|
|
241
|
+
async with app.router.lifespan_context(app):
|
|
242
|
+
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
|
243
|
+
use_test_tools(*reversed(peer_tools(PEERS, transport=transport)), cancel_order)
|
|
244
|
+
async with httpx.AsyncClient(
|
|
245
|
+
transport=transport, base_url="http://testserver", timeout=60
|
|
246
|
+
) as client:
|
|
247
|
+
started = asyncio.get_running_loop().time()
|
|
248
|
+
while (await client.get("/ready")).status_code != 200: # postgres: the schema
|
|
249
|
+
assert asyncio.get_running_loop().time() - started < 30, "not ready"
|
|
250
|
+
await asyncio.sleep(0.1)
|
|
251
|
+
yield {
|
|
252
|
+
"http": client,
|
|
253
|
+
"issuer": issuer,
|
|
254
|
+
"at_concierge": issuer.login(user, audience="concierge"),
|
|
255
|
+
"at_orders": issuer.login(user, audience="orders"),
|
|
256
|
+
}
|
|
257
|
+
reset_policy_cache()
|
|
258
|
+
reset_api_policy()
|
|
259
|
+
reset_a2a_client()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@pytest.fixture
|
|
263
|
+
async def relayed(
|
|
264
|
+
database: str,
|
|
265
|
+
issuer: FakeIssuer,
|
|
266
|
+
tmp_path: Path,
|
|
267
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
268
|
+
use_test_tools: Any,
|
|
269
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
270
|
+
"""orders' cancel gate relays the concierge's decisions (`decide_with: relayed`)."""
|
|
271
|
+
async for agents in _agents(issuer, tmp_path, monkeypatch, use_test_tools, RELAYED):
|
|
272
|
+
yield {**agents, "database": database}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.fixture
|
|
276
|
+
async def direct(
|
|
277
|
+
database: str,
|
|
278
|
+
issuer: FakeIssuer,
|
|
279
|
+
tmp_path: Path,
|
|
280
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
281
|
+
use_test_tools: Any,
|
|
282
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
283
|
+
"""orders' cancel gate at the default, `decide_with: direct`."""
|
|
284
|
+
async for agents in _agents(issuer, tmp_path, monkeypatch, use_test_tools, ""):
|
|
285
|
+
yield agents
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _events(text: str) -> list[tuple[str, dict[str, Any]]]:
|
|
289
|
+
events = []
|
|
290
|
+
for block in text.split("\n\n"):
|
|
291
|
+
lines = dict(line.split(": ", 1) for line in block.splitlines() if ": " in line)
|
|
292
|
+
if "event" in lines and "data" in lines:
|
|
293
|
+
events.append((lines["event"], json.loads(lines["data"])))
|
|
294
|
+
return events
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def _chat(agents: dict[str, Any], message: str, thread_id: str | None = None) -> Any:
|
|
298
|
+
body: dict[str, Any] = {"message": message}
|
|
299
|
+
if thread_id:
|
|
300
|
+
body["thread_id"] = thread_id
|
|
301
|
+
r = await agents["http"].post(
|
|
302
|
+
"/chat", json=body, headers={"Authorization": f"Bearer {agents['at_concierge']}"}
|
|
303
|
+
)
|
|
304
|
+
assert r.status_code == 200, r.text
|
|
305
|
+
return _events(r.text)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _end(events: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
|
|
309
|
+
ends = [data for event, data in events if event == "message.end"]
|
|
310
|
+
assert ends, events
|
|
311
|
+
return ends[-1]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _results(events: list[tuple[str, dict[str, Any]]]) -> str:
|
|
315
|
+
return json.dumps([data for event, data in events if event == "tool.result"])
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def _ask(
|
|
319
|
+
agents: dict[str, Any], status: str, message: str = "Ask orders about cancelling the order"
|
|
320
|
+
) -> tuple[str, str]:
|
|
321
|
+
"""alice asks the concierge to cancel; orders waits for approval. (thread, orders task)"""
|
|
322
|
+
events = await _chat(agents, message)
|
|
323
|
+
end = _end(events)
|
|
324
|
+
assert end["status"] == "ok", events
|
|
325
|
+
assert f'\\"status\\": \\"{status}\\"' in _results(events), _results(events)
|
|
326
|
+
found = re.search(r'\\"task_id\\": \\"([0-9a-f-]+)\\"', _results(events))
|
|
327
|
+
assert found, _results(events)
|
|
328
|
+
assert SENT == []
|
|
329
|
+
return end["thread_id"], found.group(1)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
async def test_the_concierge_relays_the_persons_approval(relayed: dict[str, Any]) -> None:
|
|
333
|
+
agents = relayed
|
|
334
|
+
thread, task_id = await _ask(agents, "needs_user_approval")
|
|
335
|
+
# alice asks the concierge to approve: it pauses for her, showing what happens at orders.
|
|
336
|
+
paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
|
|
337
|
+
assert paused["status"] == "awaiting_approval"
|
|
338
|
+
approval = paused["approval"]
|
|
339
|
+
assert (approval["api"], approval["a2a_operation"]) == ("orders_agent", "approve")
|
|
340
|
+
effect = approval["effect"]
|
|
341
|
+
assert (effect["agent"], effect["via"], effect["method"], effect["path"]) == (
|
|
342
|
+
"orders",
|
|
343
|
+
["orders"],
|
|
344
|
+
"POST",
|
|
345
|
+
"/orders/1/cancel",
|
|
346
|
+
)
|
|
347
|
+
assert approval["nested"]["decide_with"] == "relayed" and SENT == []
|
|
348
|
+
# Exactly what orders will send (read from its approval_json, not the protobuf Struct,
|
|
349
|
+
# whose numbers are doubles).
|
|
350
|
+
assert effect["body"] == CANCEL_BODY and approval["nested"]["call"]["body"] == CANCEL_BODY
|
|
351
|
+
# She approves: the decision reaches orders once, and the order is cancelled once.
|
|
352
|
+
r = await agents["http"].post(
|
|
353
|
+
f"/threads/{thread}/approvals/{approval['approval_id']}",
|
|
354
|
+
json={"decision": "approve"},
|
|
355
|
+
headers={"Authorization": f"Bearer {agents['at_concierge']}"},
|
|
356
|
+
)
|
|
357
|
+
assert r.status_code == 200, r.text
|
|
358
|
+
resumed = _events(r.text)
|
|
359
|
+
assert _end(resumed)["status"] == "ok", resumed
|
|
360
|
+
assert "cancelled" in _results(resumed)
|
|
361
|
+
assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
|
|
362
|
+
# At orders (alice's own token): its approval names the concierge as the relayer, and the
|
|
363
|
+
# task that waited follows the run the decision resumed.
|
|
364
|
+
subject = jwt.decode(agents["at_orders"], options={"verify_signature": False})["sub"]
|
|
365
|
+
context = context_id_for(thread, "orders_agent", subject)
|
|
366
|
+
mine = {"Authorization": f"Bearer {agents['at_orders']}"}
|
|
367
|
+
[record] = (await agents["http"].get(f"/threads/{context}/approvals", headers=mine)).json()
|
|
368
|
+
assert (record["status"], record["decided_via"], record["requester_actor"]) == (
|
|
369
|
+
"approved",
|
|
370
|
+
"concierge",
|
|
371
|
+
"concierge",
|
|
372
|
+
)
|
|
373
|
+
got = await agents["http"].post(
|
|
374
|
+
A2A_PATH,
|
|
375
|
+
json={"jsonrpc": "2.0", "id": "1", "method": "GetTask", "params": {"id": task_id}},
|
|
376
|
+
headers={**mine, "A2A-Version": "1.0"},
|
|
377
|
+
)
|
|
378
|
+
waited = got.json()["result"]
|
|
379
|
+
assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
|
|
380
|
+
assert waited["status"]["message"]["parts"][0]["text"].startswith("Continued in task ")
|
|
381
|
+
# Exchanged once for the pair (alice, orders), then reused (the card, the ask, the reads
|
|
382
|
+
# and the decision all went out with it).
|
|
383
|
+
exchanges = [r for r in agents["issuer"].requests if r["form"].get("audience") == "orders"]
|
|
384
|
+
assert len(exchanges) == 1
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def _approve_relayed(agents: dict[str, Any], thread: str, task_id: str) -> list[Any]:
|
|
388
|
+
"""alice has the concierge relay orders' approval, and approves it there: the resumed run."""
|
|
389
|
+
paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
|
|
390
|
+
assert paused["status"] == "awaiting_approval", paused
|
|
391
|
+
r = await agents["http"].post(
|
|
392
|
+
f"/threads/{thread}/approvals/{paused['approval']['approval_id']}",
|
|
393
|
+
json={"decision": "approve"},
|
|
394
|
+
headers={"Authorization": f"Bearer {agents['at_concierge']}"},
|
|
395
|
+
)
|
|
396
|
+
assert r.status_code == 200, r.text
|
|
397
|
+
return _events(r.text)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
async def test_the_run_alice_approves_checks_the_words_she_asked_with(
|
|
401
|
+
relayed: dict[str, Any],
|
|
402
|
+
) -> None:
|
|
403
|
+
"""orders cancels only an order the user's own words name (`require_user_mentioned`).
|
|
404
|
+
alice's words reach it with the request; when she approves at the concierge, having
|
|
405
|
+
said "approve_agent_action for <task>" there, the run the relayed decision resumes at
|
|
406
|
+
orders checks the words she asked with, and the order is cancelled once."""
|
|
407
|
+
agents = relayed
|
|
408
|
+
CHECK_MENTION.append(True)
|
|
409
|
+
thread, task_id = await _ask(
|
|
410
|
+
agents, "needs_user_approval", "Ask orders about cancelling order 1"
|
|
411
|
+
)
|
|
412
|
+
resumed = await _approve_relayed(agents, thread, task_id)
|
|
413
|
+
assert _end(resumed)["status"] == "ok", resumed
|
|
414
|
+
assert "refused" not in _results(resumed), _results(resumed)
|
|
415
|
+
assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def test_when_orders_lost_the_task_the_relay_reads_its_ledger_and_sends_once(
|
|
419
|
+
relayed: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
|
420
|
+
) -> None:
|
|
421
|
+
"""orders loses the task between the relay and alice's decision (A2A_TASK_TTL_S is far
|
|
422
|
+
shorter than an approval may wait): the resumed relay reads what orders waits on from
|
|
423
|
+
its approvals ledger instead, which gives the times the task gave, whatever the
|
|
424
|
+
database's time zone (America/New_York here), so the decision is the one alice approved
|
|
425
|
+
and the order is cancelled once."""
|
|
426
|
+
agents = relayed
|
|
427
|
+
thread, task_id = await _ask(agents, "needs_user_approval")
|
|
428
|
+
paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
|
|
429
|
+
assert paused["status"] == "awaiting_approval", paused
|
|
430
|
+
if agents["database"] == "postgres":
|
|
431
|
+
import psycopg
|
|
432
|
+
|
|
433
|
+
async with await psycopg.AsyncConnection.connect(os.environ["POSTGRES_DSN"]) as conn:
|
|
434
|
+
cursor = await conn.execute("DELETE FROM a2a_tasks WHERE task_id = %s", (task_id,))
|
|
435
|
+
assert cursor.rowcount == 1
|
|
436
|
+
await conn.commit()
|
|
437
|
+
else:
|
|
438
|
+
# The tasks in process memory: past A2A_TASK_TTL_S, as if the hour went by.
|
|
439
|
+
for store in a2a_module._STORES:
|
|
440
|
+
later = time.monotonic() + store.memory.ttl_s + 1
|
|
441
|
+
monkeypatch.setattr(store.memory, "_clock", lambda later=later: later)
|
|
442
|
+
gone = await agents["http"].post( # alice's own token reads her agents' tasks
|
|
443
|
+
A2A_PATH,
|
|
444
|
+
json={"jsonrpc": "2.0", "id": "1", "method": "GetTask", "params": {"id": task_id}},
|
|
445
|
+
headers={"Authorization": f"Bearer {agents['at_orders']}", "A2A-Version": "1.0"},
|
|
446
|
+
)
|
|
447
|
+
assert gone.json()["error"]["code"] == -32001, gone.text
|
|
448
|
+
r = await agents["http"].post(
|
|
449
|
+
f"/threads/{thread}/approvals/{paused['approval']['approval_id']}",
|
|
450
|
+
json={"decision": "approve"},
|
|
451
|
+
headers={"Authorization": f"Bearer {agents['at_concierge']}"},
|
|
452
|
+
)
|
|
453
|
+
assert r.status_code == 200, r.text
|
|
454
|
+
resumed = _events(r.text)
|
|
455
|
+
assert "differs from the request that was approved" not in _results(resumed), resumed
|
|
456
|
+
assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
async def test_under_direct_the_concierge_relays_nothing_and_alice_approves_at_orders(
|
|
460
|
+
direct: dict[str, Any],
|
|
461
|
+
) -> None:
|
|
462
|
+
agents = direct
|
|
463
|
+
thread, task_id = await _ask(agents, "needs_direct_approval")
|
|
464
|
+
events = await _chat(agents, f"approve_agent_action for {task_id}", thread)
|
|
465
|
+
assert _end(events)["status"] == "ok" # no approval asked at the concierge
|
|
466
|
+
result = _results(events)
|
|
467
|
+
assert '\\"status\\": \\"needs_direct_approval\\"' in result
|
|
468
|
+
assert "/orders/1/cancel" in result and SENT == []
|
|
469
|
+
subject = jwt.decode(agents["at_orders"], options={"verify_signature": False})["sub"]
|
|
470
|
+
context = context_id_for(thread, "orders_agent", subject)
|
|
471
|
+
mine = {"Authorization": f"Bearer {agents['at_orders']}"}
|
|
472
|
+
[record] = (await agents["http"].get(f"/threads/{context}/approvals", headers=mine)).json()
|
|
473
|
+
r = await agents["http"].post(
|
|
474
|
+
f"/threads/{context}/approvals/{record['approval_id']}",
|
|
475
|
+
json={"decision": "approve"},
|
|
476
|
+
headers=mine,
|
|
477
|
+
)
|
|
478
|
+
assert r.status_code == 200, r.text
|
|
479
|
+
assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
|