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,376 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Shared test setup: the tests see neither `.env` nor the developer's app settings,
|
|
16
|
+
and the server tests bring their own tool.
|
|
17
|
+
|
|
18
|
+
`{{cookiecutter.agent_directory}}/agent.py` calls `load_dotenv()` when it is imported, which
|
|
19
|
+
would pull a developer's `.env` (a provider key, `AUTH_JWT_*`, ...) into the
|
|
20
|
+
test process: a test could then pass in CI and fail on a laptop, or the other
|
|
21
|
+
way round. This file is loaded before any test module imports the app, so it
|
|
22
|
+
switches `.env` loading off (in this process and in the subprocesses tests
|
|
23
|
+
start) and removes every app setting from the environment: each setting
|
|
24
|
+
`.env.example` documents, plus the provider, auth, tracing and LangChain
|
|
25
|
+
variables and the other settings the app reads (`A2A_NAME`, `RUNTIME`, ...).
|
|
26
|
+
Each test module then sets exactly what it needs. Opt-ins the tests read
|
|
27
|
+
themselves (`TEST_*`, such as `TEST_POSTGRES_DSN`) are kept.
|
|
28
|
+
|
|
29
|
+
The project's response schema (`<agent directory>/response_schema.json`,
|
|
30
|
+
structured final answers) is switched off the same way
|
|
31
|
+
(`RESPONSE_SCHEMA_PATH=none`): the tests exercise the runtime with text
|
|
32
|
+
answers, whatever shape the project declares. The tests of structured answers
|
|
33
|
+
set a schema of their own, and `tests/unit/test_structured.py` checks the
|
|
34
|
+
project's own schema and that the agent answers in it.
|
|
35
|
+
|
|
36
|
+
The server tests exercise the plumbing (tool events, redaction, a run stopped
|
|
37
|
+
mid-call) with a test-only tool through the `use_test_tools` fixture, never
|
|
38
|
+
with the project's own tools, which are yours to replace or delete: while a
|
|
39
|
+
test module has imported the agent, each test serves a graph built like
|
|
40
|
+
`agent.py`'s but with no tools (the fake model would otherwise call a project
|
|
41
|
+
tool whose name a test prompt happens to mention). A test that needs the
|
|
42
|
+
project's own graph is marked `@pytest.mark.project_graph`.
|
|
43
|
+
|
|
44
|
+
`openai_compatible` is an OpenAI-compatible server in process (behind
|
|
45
|
+
`httpx.MockTransport`, no network) that refuses a chat history the way OpenAI
|
|
46
|
+
does: the tests that keep a thread valid for providers run the agent on
|
|
47
|
+
`ChatOpenAI` against it.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import os
|
|
53
|
+
import re
|
|
54
|
+
import sys
|
|
55
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from typing import Any, ClassVar
|
|
58
|
+
|
|
59
|
+
import dotenv
|
|
60
|
+
import dotenv.main
|
|
61
|
+
import pytest
|
|
62
|
+
|
|
63
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
64
|
+
# Settings the app reads that a developer's shell may carry, beyond `.env.example`.
|
|
65
|
+
_SETTING_PREFIXES = (
|
|
66
|
+
"AUTH_",
|
|
67
|
+
"MODEL_",
|
|
68
|
+
"JUDGE_",
|
|
69
|
+
"LANGSMITH_",
|
|
70
|
+
"LANGCHAIN_",
|
|
71
|
+
"OTEL_",
|
|
72
|
+
"TRACE_",
|
|
73
|
+
"TRACING_",
|
|
74
|
+
"TOKEN_EXCHANGE_",
|
|
75
|
+
)
|
|
76
|
+
_PROVIDER_VARIABLES = {"OPENAI_API_KEY", "OPENAI_BASE_URL", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY"}
|
|
77
|
+
# Settings the app reads that `.env.example` leaves out (the runtime and the
|
|
78
|
+
# server set some of them; a developer's shell may carry any).
|
|
79
|
+
_APP_VARIABLES = {
|
|
80
|
+
"A2A_NAME",
|
|
81
|
+
"A2A_DESCRIPTION",
|
|
82
|
+
"AGENT_VERSION",
|
|
83
|
+
"DATABASE_URI",
|
|
84
|
+
"REDIS_URI",
|
|
85
|
+
"HOST",
|
|
86
|
+
"LANGGRAPH_SERVER",
|
|
87
|
+
"LANGGRAPH_SERVER_URL",
|
|
88
|
+
"LANGSERVE_GRAPHS",
|
|
89
|
+
"PGCONNECT_TIMEOUT",
|
|
90
|
+
"RUNTIME",
|
|
91
|
+
}
|
|
92
|
+
_ASSIGNMENT = re.compile(r"^\s*#?\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _documented_settings() -> set[str]:
|
|
96
|
+
"""Every variable `.env.example` assigns, commented-out examples included."""
|
|
97
|
+
try:
|
|
98
|
+
text = (PROJECT_ROOT / ".env.example").read_text(encoding="utf-8")
|
|
99
|
+
except OSError:
|
|
100
|
+
return set()
|
|
101
|
+
return {m.group(1) for line in text.splitlines() if (m := _ASSIGNMENT.match(line))}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_app_setting(name: str) -> bool:
|
|
105
|
+
if name.startswith("TEST_"):
|
|
106
|
+
return False
|
|
107
|
+
return (
|
|
108
|
+
name in _PROVIDER_VARIABLES or name in _APP_VARIABLES or name.startswith(_SETTING_PREFIXES)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _no_dotenv(*args: Any, **kwargs: Any) -> bool:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# python-dotenv >= 1.2 honours PYTHON_DOTENV_DISABLED (inherited by subprocesses);
|
|
117
|
+
# replacing load_dotenv covers older versions in this process.
|
|
118
|
+
os.environ["PYTHON_DOTENV_DISABLED"] = "1"
|
|
119
|
+
dotenv.load_dotenv = _no_dotenv
|
|
120
|
+
dotenv.main.load_dotenv = _no_dotenv
|
|
121
|
+
for _name in _documented_settings() | {n for n in list(os.environ) if _is_app_setting(n)}:
|
|
122
|
+
if not _name.startswith("TEST_") and _name != "PYTHON_DOTENV_DISABLED":
|
|
123
|
+
os.environ.pop(_name, None)
|
|
124
|
+
# No response schema unless a test sets one (see the module docstring); subprocesses inherit it.
|
|
125
|
+
os.environ["RESPONSE_SCHEMA_PATH"] = "none"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_test_graph(tools: Sequence[Any]) -> Any:
|
|
129
|
+
"""The agent's graph as `agent.py` builds it, with `tools` instead of the project's."""
|
|
130
|
+
from langchain.agents import create_agent
|
|
131
|
+
|
|
132
|
+
from {{cookiecutter.agent_directory}} import agent
|
|
133
|
+
from {{cookiecutter.agent_directory}}.app_utils.limits import recursion_limit
|
|
134
|
+
from {{cookiecutter.agent_directory}}.app_utils.model import get_model
|
|
135
|
+
from {{cookiecutter.agent_directory}}.app_utils.structured import response_format
|
|
136
|
+
|
|
137
|
+
model = get_model()
|
|
138
|
+
return create_agent(
|
|
139
|
+
model=model,
|
|
140
|
+
tools=list(tools),
|
|
141
|
+
system_prompt=agent.SYSTEM_PROMPT,
|
|
142
|
+
middleware=agent.middleware(),
|
|
143
|
+
context_schema=agent.AgentContext,
|
|
144
|
+
response_format=response_format(model, list(tools)),
|
|
145
|
+
name="test-agent",
|
|
146
|
+
).with_config({"recursion_limit": recursion_limit()})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
AGENT_MODULE = "{{cookiecutter.agent_directory}}.agent"
|
|
150
|
+
APP_MODULE = "{{cookiecutter.agent_directory}}.fast_api_app"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
154
|
+
config.addinivalue_line(
|
|
155
|
+
"markers", "project_graph: serve the project's own graph (its tools included)"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.fixture(autouse=True)
|
|
160
|
+
def _no_project_tools(
|
|
161
|
+
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
|
|
162
|
+
) -> Iterator[None]:
|
|
163
|
+
"""Serve a graph without the project's tools (see the module docstring).
|
|
164
|
+
|
|
165
|
+
Only where the test module already imported the agent or the app (with its
|
|
166
|
+
settings); the app binds its checkpointer to whichever graph `agent.graph`
|
|
167
|
+
is at startup, and reads `agent.graph` again for every run.
|
|
168
|
+
"""
|
|
169
|
+
agent = sys.modules.get(AGENT_MODULE)
|
|
170
|
+
if (agent is not None or APP_MODULE in sys.modules) and not os.environ.get("MODEL_PROVIDER"):
|
|
171
|
+
# A module that imported the app without setting a model (a test of
|
|
172
|
+
# its routes): the graph runs on the fake model.
|
|
173
|
+
monkeypatch.setenv("MODEL_PROVIDER", "fake")
|
|
174
|
+
if agent is None and APP_MODULE in sys.modules:
|
|
175
|
+
import importlib
|
|
176
|
+
|
|
177
|
+
agent = importlib.import_module(AGENT_MODULE)
|
|
178
|
+
if agent is not None and request.node.get_closest_marker("project_graph") is None:
|
|
179
|
+
graph = build_test_graph([])
|
|
180
|
+
graph.checkpointer = agent.graph.checkpointer
|
|
181
|
+
monkeypatch.setattr(agent, "graph", graph)
|
|
182
|
+
yield
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.fixture
|
|
186
|
+
def use_test_tools(monkeypatch: pytest.MonkeyPatch) -> Callable[..., Any]:
|
|
187
|
+
"""Serve a graph with only the given tools for this test (call it once the app has started).
|
|
188
|
+
|
|
189
|
+
The fake model calls a bound tool when the message mentions it (its name, or
|
|
190
|
+
a distinctive word of it), so `probe` is called for "Run the probe for Paris"
|
|
191
|
+
with `query="Paris"`. The graph keeps the checkpointer the app bound at startup.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def install(*tools: Any) -> Any:
|
|
195
|
+
from {{cookiecutter.agent_directory}} import agent
|
|
196
|
+
|
|
197
|
+
graph = build_test_graph(tools)
|
|
198
|
+
graph.checkpointer = agent.graph.checkpointer
|
|
199
|
+
monkeypatch.setattr(agent, "graph", graph)
|
|
200
|
+
return graph
|
|
201
|
+
|
|
202
|
+
return install
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# --- an OpenAI-compatible server that checks the history, in process ----------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def openai_history_problem(messages: list[dict[str, Any]]) -> str | None:
|
|
209
|
+
"""Why OpenAI would refuse this chat history (a 400), or None.
|
|
210
|
+
|
|
211
|
+
An assistant message with `tool_calls` must be followed by one tool message
|
|
212
|
+
per call before anything else, and a tool message must answer such a call.
|
|
213
|
+
"""
|
|
214
|
+
pending: list[str] = []
|
|
215
|
+
for message in messages:
|
|
216
|
+
role = message.get("role")
|
|
217
|
+
if pending and role != "tool":
|
|
218
|
+
return (
|
|
219
|
+
"An assistant message with 'tool_calls' must be followed by tool messages "
|
|
220
|
+
"responding to each 'tool_call_id'. The following tool_call_ids did not have "
|
|
221
|
+
f"response messages: {', '.join(pending)}"
|
|
222
|
+
)
|
|
223
|
+
if role == "tool":
|
|
224
|
+
if message.get("tool_call_id") not in pending:
|
|
225
|
+
return "messages with role 'tool' must be a response to a preceding 'tool_calls'"
|
|
226
|
+
pending.remove(message.get("tool_call_id"))
|
|
227
|
+
elif role == "assistant":
|
|
228
|
+
pending = [call.get("id") for call in message.get("tool_calls") or []]
|
|
229
|
+
if pending:
|
|
230
|
+
return f"tool_call_ids did not have response messages: {', '.join(pending)}"
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class OpenAICompatibleFake:
|
|
235
|
+
"""A scripted OpenAI-compatible chat server behind `httpx.MockTransport`.
|
|
236
|
+
|
|
237
|
+
It refuses a history OpenAI refuses (`openai_history_problem`, a 400), and
|
|
238
|
+
answers the last message: a user message naming a script word gets that
|
|
239
|
+
reply (see `SCRIPTS`: arguments that are not valid JSON, one id for two
|
|
240
|
+
calls), a tool result gets `Found: <results>` (unless the user asked for
|
|
241
|
+
`ALWAYSBAD`: invalid arguments again), anything else `ok`. Tool calls go
|
|
242
|
+
to the first tool of the request. Replies put in `queue` come first, in
|
|
243
|
+
order: `(text, [(call id, tool name, raw arguments), ...])`, text and
|
|
244
|
+
calls together in one message when both are given. `requests` keeps every
|
|
245
|
+
request body; `refusals` every 400.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
SCRIPTS: ClassVar[dict[str, list[tuple[str, str]]]] = {
|
|
249
|
+
# (call id, raw arguments) per tool call
|
|
250
|
+
"BADARGS": [("call_0", "{'query': 'SF'}")],
|
|
251
|
+
"TRAILINGCOMMA": [("call_0", '{"query": "SF",}')],
|
|
252
|
+
"DUPIDS": [("dup", '{"query": "SF"}'), ("dup", '{"query": "Rome"}')],
|
|
253
|
+
"BADANDGOOD": [("call_0", '{"query": "SF"}'), ("call_1", "query=Rome")],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def __init__(self) -> None:
|
|
257
|
+
self.requests: list[dict[str, Any]] = []
|
|
258
|
+
self.refusals: list[str] = []
|
|
259
|
+
self.queue: list[tuple[str | None, list[tuple[str, str, str]]]] = []
|
|
260
|
+
|
|
261
|
+
def model(self) -> Any:
|
|
262
|
+
import httpx
|
|
263
|
+
from langchain_openai import ChatOpenAI
|
|
264
|
+
|
|
265
|
+
transport = httpx.MockTransport(self._handle)
|
|
266
|
+
return ChatOpenAI(
|
|
267
|
+
model="fake-gpt",
|
|
268
|
+
api_key="test",
|
|
269
|
+
base_url="http://openai-compatible.test/v1",
|
|
270
|
+
max_retries=0,
|
|
271
|
+
http_client=httpx.Client(transport=transport),
|
|
272
|
+
http_async_client=httpx.AsyncClient(transport=transport),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _reply(self, body: dict[str, Any]) -> tuple[str | None, list[dict[str, Any]]]:
|
|
276
|
+
if self.queue:
|
|
277
|
+
text, queued = self.queue.pop(0)
|
|
278
|
+
return text, [
|
|
279
|
+
{
|
|
280
|
+
"index": i,
|
|
281
|
+
"id": call_id,
|
|
282
|
+
"type": "function",
|
|
283
|
+
"function": {"name": name, "arguments": arguments},
|
|
284
|
+
}
|
|
285
|
+
for i, (call_id, name, arguments) in enumerate(queued)
|
|
286
|
+
]
|
|
287
|
+
messages = body.get("messages") or []
|
|
288
|
+
last = messages[-1] if messages else {}
|
|
289
|
+
asked = next((str(m.get("content")) for m in reversed(messages) if m["role"] == "user"), "")
|
|
290
|
+
tools = [t["function"]["name"] for t in body.get("tools") or []]
|
|
291
|
+
if "ALWAYSBAD" in asked and tools:
|
|
292
|
+
call_id = f"call_{sum(m['role'] == 'assistant' for m in messages)}"
|
|
293
|
+
function = {"name": tools[0], "arguments": "{query: SF}"}
|
|
294
|
+
return None, [{"index": 0, "id": call_id, "type": "function", "function": function}]
|
|
295
|
+
if last.get("role") == "tool":
|
|
296
|
+
results = []
|
|
297
|
+
for message in reversed(messages):
|
|
298
|
+
if message.get("role") != "tool":
|
|
299
|
+
break
|
|
300
|
+
content = message.get("content")
|
|
301
|
+
if isinstance(content, list):
|
|
302
|
+
content = "".join(str(b.get("text", "")) for b in content)
|
|
303
|
+
results.append(str(content))
|
|
304
|
+
return "Found: " + " | ".join(reversed(results)), []
|
|
305
|
+
text = str(last.get("content") or "")
|
|
306
|
+
for word, calls in self.SCRIPTS.items():
|
|
307
|
+
if word in text and tools:
|
|
308
|
+
return None, [
|
|
309
|
+
{
|
|
310
|
+
"index": i,
|
|
311
|
+
"id": call_id,
|
|
312
|
+
"type": "function",
|
|
313
|
+
"function": {"name": tools[0], "arguments": arguments},
|
|
314
|
+
}
|
|
315
|
+
for i, (call_id, arguments) in enumerate(calls)
|
|
316
|
+
]
|
|
317
|
+
return "ok", []
|
|
318
|
+
|
|
319
|
+
def _handle(self, request: Any) -> Any:
|
|
320
|
+
import json
|
|
321
|
+
|
|
322
|
+
import httpx
|
|
323
|
+
|
|
324
|
+
body = json.loads(request.content)
|
|
325
|
+
self.requests.append(body)
|
|
326
|
+
problem = openai_history_problem(body.get("messages") or [])
|
|
327
|
+
if problem:
|
|
328
|
+
self.refusals.append(problem)
|
|
329
|
+
error = {"message": problem, "type": "invalid_request_error", "param": "messages"}
|
|
330
|
+
return httpx.Response(400, json={"error": error})
|
|
331
|
+
text, calls = self._reply(body)
|
|
332
|
+
usage = {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
|
|
333
|
+
head = {"id": "chatcmpl-fake", "created": 0, "model": "fake-gpt"}
|
|
334
|
+
if not body.get("stream"):
|
|
335
|
+
message: dict[str, Any] = {"role": "assistant", "content": text}
|
|
336
|
+
if calls:
|
|
337
|
+
message["tool_calls"] = [
|
|
338
|
+
{k: v for k, v in c.items() if k != "index"} for c in calls
|
|
339
|
+
]
|
|
340
|
+
choice = {
|
|
341
|
+
"index": 0,
|
|
342
|
+
"message": message,
|
|
343
|
+
"finish_reason": "tool_calls" if calls else "stop",
|
|
344
|
+
}
|
|
345
|
+
return httpx.Response(
|
|
346
|
+
200, json={**head, "object": "chat.completion", "choices": [choice], "usage": usage}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def chunk(delta: dict[str, Any], finish: str | None = None) -> str:
|
|
350
|
+
choice = {"index": 0, "delta": delta, "finish_reason": finish}
|
|
351
|
+
return "data: " + json.dumps(
|
|
352
|
+
{**head, "object": "chat.completion.chunk", "choices": [choice]}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if calls:
|
|
356
|
+
lines = [chunk({"role": "assistant", "content": text, "tool_calls": calls})]
|
|
357
|
+
lines.append(chunk({}, "tool_calls"))
|
|
358
|
+
else:
|
|
359
|
+
lines = [chunk({"role": "assistant", "content": ""})]
|
|
360
|
+
lines += [
|
|
361
|
+
chunk({"content": (text or "")[i : i + 20]}) for i in range(0, len(text or ""), 20)
|
|
362
|
+
]
|
|
363
|
+
lines.append(chunk({}, "stop"))
|
|
364
|
+
final = {**head, "object": "chat.completion.chunk", "choices": [], "usage": usage}
|
|
365
|
+
lines += ["data: " + json.dumps(final), "data: [DONE]"]
|
|
366
|
+
return httpx.Response(
|
|
367
|
+
200,
|
|
368
|
+
content="\n\n".join(lines).encode() + b"\n\n",
|
|
369
|
+
headers={"content-type": "text/event-stream"},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.fixture
|
|
374
|
+
def openai_compatible() -> OpenAICompatibleFake:
|
|
375
|
+
"""An in-process OpenAI-compatible server that refuses invalid histories (see its class)."""
|
|
376
|
+
return OpenAICompatibleFake()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cases": [
|
|
3
|
+
{
|
|
4
|
+
"id": "greeting",
|
|
5
|
+
"messages": [{"role": "user", "content": "hi"}],
|
|
6
|
+
"expect": {"contains": ["Hello"], "no_tool_calls": true},
|
|
7
|
+
"judge": {"response_quality": {"threshold": 4}},
|
|
8
|
+
"reference": "A short, friendly greeting that offers help.",
|
|
9
|
+
"metadata": {"category": "smoke"}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "weather",
|
|
13
|
+
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
|
|
14
|
+
"expect": {
|
|
15
|
+
"contains": ["sunny"],
|
|
16
|
+
"tool_calls": [{"name": "get_weather", "args_subset": {"query": "Paris"}}]
|
|
17
|
+
},
|
|
18
|
+
"reference": "It's 90 degrees and sunny in Paris.",
|
|
19
|
+
"context": "get_weather returns \"It's 90 degrees and sunny.\" for every place except San Francisco.",
|
|
20
|
+
"metadata": {"category": "tools"}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "capabilities",
|
|
24
|
+
"messages": [{"role": "user", "content": "What can you do?"}],
|
|
25
|
+
"expect": {
|
|
26
|
+
"regex": "(?i)\\bweather\\b",
|
|
27
|
+
"not_contains": ["Traceback", "PolicyViolation"],
|
|
28
|
+
"no_tool_calls": true
|
|
29
|
+
},
|
|
30
|
+
"judge": {"response_quality": {"threshold": 4}},
|
|
31
|
+
"reference": "The agent explains that it can look up the weather for a place.",
|
|
32
|
+
"metadata": {"category": "smoke"}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "weather-follow-up",
|
|
36
|
+
"messages": [
|
|
37
|
+
{"role": "user", "content": "What is the weather in Paris?"},
|
|
38
|
+
{"role": "user", "content": "And what is the weather in Berlin?"}
|
|
39
|
+
],
|
|
40
|
+
"expect": {
|
|
41
|
+
"scope": "all_turns",
|
|
42
|
+
"contains": ["sunny"],
|
|
43
|
+
"tool_calls": [
|
|
44
|
+
{"name": "get_weather", "args_subset": {"query": "Paris"}},
|
|
45
|
+
{"name": "get_weather", "args_subset": {"query": "Berlin"}}
|
|
46
|
+
],
|
|
47
|
+
"ordered": true
|
|
48
|
+
},
|
|
49
|
+
"reference": "Reports the weather in Paris, then in Berlin: 90 degrees and sunny in both.",
|
|
50
|
+
"metadata": {"category": "multi-turn"}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Grading configuration for `graph-agents-cli eval grade` and `eval run`.
|
|
2
|
+
#
|
|
3
|
+
# Mandatory, no threshold: complete case accounting, every deterministic
|
|
4
|
+
# `expect` check, and every judge metric a case declares that is NOT listed
|
|
5
|
+
# under `quality_metrics`. Only quality metrics may pass at a rate below 100%.
|
|
6
|
+
|
|
7
|
+
judge:
|
|
8
|
+
provider: null # null = JUDGE_MODEL_PROVIDER, else the agent's MODEL_PROVIDER
|
|
9
|
+
model: null # null = JUDGE_MODEL_NAME, else MODEL_NAME
|
|
10
|
+
# Characters of one tool result a judge sees (default 50000; null = never
|
|
11
|
+
# cut). A longer result is cut with a TRUNCATED marker the judge is told
|
|
12
|
+
# about, and `eval grade` warns which cases were affected.
|
|
13
|
+
# max_tool_result_chars: 50000
|
|
14
|
+
|
|
15
|
+
quality_metrics:
|
|
16
|
+
# Only judge metrics listed here may score below their threshold on a bounded
|
|
17
|
+
# fraction of cases; every other judge metric and every deterministic check is
|
|
18
|
+
# mandatory. The rate counts only the cases that declare the metric.
|
|
19
|
+
response_quality: { threshold: 4, min_pass_rate: 0.9 }
|
|
20
|
+
|
|
21
|
+
# Override the built-in rubrics (response_quality, task_success, groundedness)
|
|
22
|
+
# or add your own: {scale, rubric, prompt_template}. The prompt template may use
|
|
23
|
+
# {metric}, {rubric}, {scale}, {conversation}, {transcript}, {response},
|
|
24
|
+
# {reference}, {context}, {reference_section}, {context_section} and
|
|
25
|
+
# {tool_calls_section}. On a multi-turn case {conversation} holds every earlier
|
|
26
|
+
# turn in full (user message, tool calls with results, agent reply) and the
|
|
27
|
+
# latest user message; {transcript} adds the final turn's tool calls and reply.
|
|
28
|
+
judges: {}
|
|
29
|
+
|
|
30
|
+
# module:function callables run in this project's environment:
|
|
31
|
+
# fn(case, trace) -> bool | number | {score, reasoning}
|
|
32
|
+
custom_metrics: []
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""The graph `tests/integration/test_approvals_server.py` serves with `langgraph dev`.
|
|
16
|
+
|
|
17
|
+
The agent as `agent.py` builds it (prompt, middleware, context schema), with
|
|
18
|
+
a test tool that cancels an order through `api_client`: a call the test's
|
|
19
|
+
`api-policy.yaml` gates. The JSON body comes from the file named by
|
|
20
|
+
`TEST_APPROVAL_BODY_FILE`, so a test can change the request between the pause
|
|
21
|
+
and the decision. Two more tools place and amend orders, for a policy whose
|
|
22
|
+
approval rules ask other approvers for other calls, and one reads a gauge whose
|
|
23
|
+
reading holds a lone surrogate. `whoami` reports the caller its run acts for
|
|
24
|
+
(`current_caller`) and whether `require_direct_caller` lets it through. With
|
|
25
|
+
`RESPONSE_SCHEMA_PATH` set it answers in that shape (`test_structured_server.py`).
|
|
26
|
+
Not collected by pytest (no `test_` prefix).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from langchain.agents import create_agent
|
|
37
|
+
from langchain.tools import ToolRuntime
|
|
38
|
+
from langchain_core.tools import tool
|
|
39
|
+
|
|
40
|
+
from {{cookiecutter.agent_directory}} import agent
|
|
41
|
+
from {{cookiecutter.agent_directory}}.app_utils.api_client import (
|
|
42
|
+
ApiPolicyError,
|
|
43
|
+
current_caller,
|
|
44
|
+
get_client,
|
|
45
|
+
require_direct_caller,
|
|
46
|
+
)
|
|
47
|
+
from {{cookiecutter.agent_directory}}.app_utils.limits import recursion_limit
|
|
48
|
+
from {{cookiecutter.agent_directory}}.app_utils.model import get_model
|
|
49
|
+
from {{cookiecutter.agent_directory}}.app_utils.structured import response_format
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _principal(context: Any) -> str:
|
|
53
|
+
if isinstance(context, dict):
|
|
54
|
+
return str(context.get("principal_id") or "")
|
|
55
|
+
return str(getattr(context, "principal_id", "") or "")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@tool
|
|
59
|
+
async def cancel_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
|
|
60
|
+
"""Cancel an order by its id."""
|
|
61
|
+
context = getattr(runtime, "context", None)
|
|
62
|
+
body = json.loads(Path(os.environ["TEST_APPROVAL_BODY_FILE"]).read_text(encoding="utf-8"))
|
|
63
|
+
client = get_client("shop", context=context)
|
|
64
|
+
data = await client.post(
|
|
65
|
+
"/orders/{order_id}/cancel",
|
|
66
|
+
operation_id="cancelOrder",
|
|
67
|
+
path_params={"order_id": order_id},
|
|
68
|
+
json_body=body,
|
|
69
|
+
)
|
|
70
|
+
return json.dumps({"upstream": data, "acting_as": _principal(context)})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@tool
|
|
74
|
+
async def place_order(item: str, runtime: ToolRuntime[Any]) -> str:
|
|
75
|
+
"""Place a new purchase of an item."""
|
|
76
|
+
context = getattr(runtime, "context", None)
|
|
77
|
+
client = get_client("shop", context=context)
|
|
78
|
+
data = await client.post("/orders", operation_id="createOrder", json_body={"item": item})
|
|
79
|
+
return json.dumps({"upstream": data, "acting_as": _principal(context)})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@tool
|
|
83
|
+
async def amend_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
|
|
84
|
+
"""Amend an existing purchase with a gift note."""
|
|
85
|
+
context = getattr(runtime, "context", None)
|
|
86
|
+
client = get_client("shop", context=context)
|
|
87
|
+
data = await client.patch(
|
|
88
|
+
"/orders/{order_id}",
|
|
89
|
+
operation_id="updateOrder",
|
|
90
|
+
path_params={"order_id": order_id},
|
|
91
|
+
json_body={"note": "gift"},
|
|
92
|
+
)
|
|
93
|
+
return json.dumps({"upstream": data, "acting_as": _principal(context)})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@tool
|
|
97
|
+
def gauge_reading(place: str) -> str:
|
|
98
|
+
"""Read the gauge at a place (its reading holds a lone surrogate, which UTF-8 cannot encode)."""
|
|
99
|
+
return f"gauge at {place}:\ud80042"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@tool
|
|
103
|
+
def whoami(topic: str, runtime: ToolRuntime[Any]) -> str:
|
|
104
|
+
"""Say whoami: the caller this run acts for, and whether only a person may ask here."""
|
|
105
|
+
context = getattr(runtime, "context", None)
|
|
106
|
+
caller = current_caller(context)
|
|
107
|
+
try:
|
|
108
|
+
require_direct_caller(context)
|
|
109
|
+
direct_only = "allowed"
|
|
110
|
+
except ApiPolicyError as exc:
|
|
111
|
+
direct_only = str(exc)
|
|
112
|
+
return json.dumps(
|
|
113
|
+
{
|
|
114
|
+
"principal_id": caller.principal_id,
|
|
115
|
+
"roles": sorted(caller.roles),
|
|
116
|
+
"actor": caller.actor,
|
|
117
|
+
"actor_chain": list(caller.actor_chain),
|
|
118
|
+
"direct_only": direct_only,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
model = get_model()
|
|
124
|
+
# The fake model calls the first tool a message names: "Cancel ..." cancels,
|
|
125
|
+
# "Place ..." places, "Amend ..." amends, "... gauge ..." reads the gauge and
|
|
126
|
+
# "whoami" reports the caller.
|
|
127
|
+
tools = [cancel_order, place_order, amend_order, gauge_reading, whoami]
|
|
128
|
+
graph = create_agent(
|
|
129
|
+
model=model,
|
|
130
|
+
tools=tools,
|
|
131
|
+
system_prompt=agent.SYSTEM_PROMPT,
|
|
132
|
+
middleware=agent.middleware(),
|
|
133
|
+
context_schema=agent.AgentContext,
|
|
134
|
+
# Structured answers when the server runs with RESPONSE_SCHEMA_PATH (as agent.py).
|
|
135
|
+
response_format=response_format(model, tools),
|
|
136
|
+
name="approval-test",
|
|
137
|
+
).with_config({"recursion_limit": recursion_limit()})
|