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