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,462 @@
|
|
|
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
|
+
"""Synchronous client for the chat API (``POST /chat``, SSE) of a scaffolded agent.
|
|
16
|
+
|
|
17
|
+
The server side is ``POST /chat`` streaming server-sent events, ``GET /health``,
|
|
18
|
+
``GET /threads``, ``GET /threads/{thread_id}/messages`` and the approval routes
|
|
19
|
+
(``GET /approvals``, ``GET /threads/{thread_id}/approvals``, ``POST
|
|
20
|
+
/threads/{thread_id}/approvals/{approval_id}``). This module is the only
|
|
21
|
+
client-side implementation of that surface; ``run``, ``approvals`` and
|
|
22
|
+
``eval generate`` build on it.
|
|
23
|
+
|
|
24
|
+
A run that reaches a call its API's policy gates pauses: its stream ends with
|
|
25
|
+
``message.end`` whose ``status`` is ``awaiting_approval`` and whose
|
|
26
|
+
``approval`` names the call. Deciding it (``decide_approval``) resumes the run
|
|
27
|
+
and streams the continuation with the same events as ``POST /chat``.
|
|
28
|
+
|
|
29
|
+
Only ``httpx`` is imported here: no model SDK, no framework. Clients come from
|
|
30
|
+
``graph_agents_cli._http``: a request to the local server never goes through a
|
|
31
|
+
proxy, and a remote one fails with one line when the environment names a proxy
|
|
32
|
+
httpx cannot use.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import re
|
|
39
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
40
|
+
from typing import Any, NamedTuple
|
|
41
|
+
from urllib.parse import quote
|
|
42
|
+
|
|
43
|
+
import httpx
|
|
44
|
+
|
|
45
|
+
from graph_agents_cli import _http
|
|
46
|
+
|
|
47
|
+
# Event names the scaffolded app streams from POST /chat.
|
|
48
|
+
EVENT_MESSAGE_START = "message.start"
|
|
49
|
+
EVENT_MESSAGE_DELTA = "message.delta"
|
|
50
|
+
EVENT_TOOL_CALL = "tool.call"
|
|
51
|
+
EVENT_TOOL_RESULT = "tool.result"
|
|
52
|
+
EVENT_MESSAGE_END = "message.end"
|
|
53
|
+
EVENT_ERROR = "error"
|
|
54
|
+
|
|
55
|
+
KNOWN_EVENTS = frozenset(
|
|
56
|
+
{
|
|
57
|
+
EVENT_MESSAGE_START,
|
|
58
|
+
EVENT_MESSAGE_DELTA,
|
|
59
|
+
EVENT_TOOL_CALL,
|
|
60
|
+
EVENT_TOOL_RESULT,
|
|
61
|
+
EVENT_MESSAGE_END,
|
|
62
|
+
EVENT_ERROR,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# `message.end` status of a run paused on a gated call (its payload has `approval`).
|
|
67
|
+
STATUS_AWAITING_APPROVAL = "awaiting_approval"
|
|
68
|
+
# The decisions `POST /threads/{thread_id}/approvals/{approval_id}` takes.
|
|
69
|
+
DECISION_APPROVE = "approve"
|
|
70
|
+
DECISION_REJECT = "reject"
|
|
71
|
+
DECISIONS = (DECISION_APPROVE, DECISION_REJECT)
|
|
72
|
+
# The 409 code of `POST /chat` on a thread whose run waits for an approval.
|
|
73
|
+
APPROVAL_PENDING = "approval_pending"
|
|
74
|
+
|
|
75
|
+
# Scalar timeout kept for callers that pass one number (every phase applies it).
|
|
76
|
+
DEFAULT_TIMEOUT = 120.0
|
|
77
|
+
# The streaming call: connecting must be quick, but a healthy agent may send no
|
|
78
|
+
# event for minutes while a tool or a non-streaming model phase runs. The read
|
|
79
|
+
# timeout bounds that gap without treating it as "unreachable"; ``post_chat``
|
|
80
|
+
# reads this at call time so a caller (or a test) may replace it.
|
|
81
|
+
STREAM_READ_TIMEOUT = 600.0
|
|
82
|
+
STREAM_TIMEOUT = httpx.Timeout(connect=10.0, read=STREAM_READ_TIMEOUT, write=10.0, pool=10.0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SseEvent(NamedTuple):
|
|
86
|
+
"""One parsed server-sent event.
|
|
87
|
+
|
|
88
|
+
``data`` is the JSON-decoded payload when the ``data:`` lines form valid
|
|
89
|
+
JSON, otherwise the raw joined string. ``raw`` is always the joined string.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
event: str
|
|
93
|
+
data: Any
|
|
94
|
+
raw: str = ""
|
|
95
|
+
id: str | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# `scheme://user:password@`: the userinfo of any URL in a text.
|
|
99
|
+
_URL_USERINFO = re.compile(r"(\b[A-Za-z][A-Za-z0-9+.-]*://)[^/\s@]+@")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def redact_credentials(text: str) -> str:
|
|
103
|
+
"""``text`` with the ``user:password@`` of every URL in it replaced by ``***@``."""
|
|
104
|
+
return _URL_USERINFO.sub(r"\1***@", text or "")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ChatClientError(Exception):
|
|
108
|
+
"""Base class for chat client failures."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ChatHTTPError(ChatClientError):
|
|
112
|
+
"""The server answered with an HTTP error status before streaming.
|
|
113
|
+
|
|
114
|
+
``url`` and the message never carry the URL's credentials (``***@``).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, status_code: int, body: str, url: str = "") -> None:
|
|
118
|
+
self.status_code = status_code
|
|
119
|
+
self.body = body
|
|
120
|
+
self.url = redact_credentials(url)
|
|
121
|
+
super().__init__(f"HTTP {status_code} from {self.url or 'server'}: {body}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _normalise_base(base_url: str) -> str:
|
|
125
|
+
return base_url.rstrip("/")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _decode_data(raw: str) -> Any:
|
|
129
|
+
if not raw:
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
return json.loads(raw)
|
|
133
|
+
except (json.JSONDecodeError, ValueError):
|
|
134
|
+
return raw
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def iter_sse(lines: Iterable[str]) -> Iterator[SseEvent]:
|
|
138
|
+
"""Parse a stream of text lines into :class:`SseEvent` objects.
|
|
139
|
+
|
|
140
|
+
Follows the WHATWG event-stream grammar: ``:`` lines are comments
|
|
141
|
+
(keep-alives) and are dropped, ``data:`` lines accumulate joined by
|
|
142
|
+
``\\n``, ``event:`` names the event (default ``message``), ``id:`` is kept,
|
|
143
|
+
``retry:`` and unknown fields are ignored, and a blank line dispatches the
|
|
144
|
+
pending event. A pending event at end of stream is dispatched as well, so a
|
|
145
|
+
server that closes without a trailing blank line still delivers its last
|
|
146
|
+
event. Events with no data are dropped, as the spec requires.
|
|
147
|
+
"""
|
|
148
|
+
event_name = ""
|
|
149
|
+
data_lines: list[str] = []
|
|
150
|
+
event_id: str | None = None
|
|
151
|
+
|
|
152
|
+
def flush() -> SseEvent | None:
|
|
153
|
+
nonlocal event_name, data_lines, event_id
|
|
154
|
+
if not data_lines:
|
|
155
|
+
event_name = ""
|
|
156
|
+
event_id = None
|
|
157
|
+
return None
|
|
158
|
+
raw = "\n".join(data_lines)
|
|
159
|
+
ev = SseEvent(event_name or "message", _decode_data(raw), raw, event_id)
|
|
160
|
+
event_name = ""
|
|
161
|
+
data_lines = []
|
|
162
|
+
event_id = None
|
|
163
|
+
return ev
|
|
164
|
+
|
|
165
|
+
for line in lines:
|
|
166
|
+
# Accept both bare and CRLF-terminated lines from any source.
|
|
167
|
+
line = line.rstrip("\r\n")
|
|
168
|
+
if line == "":
|
|
169
|
+
ev = flush()
|
|
170
|
+
if ev is not None:
|
|
171
|
+
yield ev
|
|
172
|
+
continue
|
|
173
|
+
if line.startswith(":"):
|
|
174
|
+
continue # comment / keep-alive
|
|
175
|
+
field, sep, value = line.partition(":")
|
|
176
|
+
if sep and value.startswith(" "):
|
|
177
|
+
value = value[1:]
|
|
178
|
+
if field == "data":
|
|
179
|
+
data_lines.append(value)
|
|
180
|
+
elif field == "event":
|
|
181
|
+
event_name = value
|
|
182
|
+
elif field == "id":
|
|
183
|
+
event_id = value
|
|
184
|
+
# retry: and unknown fields are ignored.
|
|
185
|
+
|
|
186
|
+
ev = flush()
|
|
187
|
+
if ev is not None:
|
|
188
|
+
yield ev
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _iter_stream_lines(chunks: Iterable[str]) -> Iterator[str]:
|
|
192
|
+
"""Split decoded text chunks into lines on LF, CR or CRLF only.
|
|
193
|
+
|
|
194
|
+
``httpx.Response.iter_lines`` uses ``str.splitlines`` semantics and also
|
|
195
|
+
breaks on U+0085, U+2028, U+2029, VT, FF, FS, GS and RS, which are legal
|
|
196
|
+
inside a ``data:`` payload (the scaffolded server encodes with
|
|
197
|
+
``ensure_ascii=False``); the event-stream grammar recognises only CR, LF
|
|
198
|
+
and CRLF as line terminators. A trailing CR is held back until the next
|
|
199
|
+
chunk shows whether an LF follows, so a CRLF split across chunks is one
|
|
200
|
+
line break, and a partial last line is flushed at end of stream.
|
|
201
|
+
"""
|
|
202
|
+
buf = ""
|
|
203
|
+
for chunk in chunks:
|
|
204
|
+
buf += chunk
|
|
205
|
+
while True:
|
|
206
|
+
i_n = buf.find("\n")
|
|
207
|
+
i_r = buf.find("\r")
|
|
208
|
+
if i_n == -1 and i_r == -1:
|
|
209
|
+
break
|
|
210
|
+
if i_r != -1 and (i_n == -1 or i_r < i_n):
|
|
211
|
+
if i_r == len(buf) - 1:
|
|
212
|
+
break # trailing CR: wait to see whether an LF follows
|
|
213
|
+
end = i_r + 2 if buf[i_r + 1] == "\n" else i_r + 1
|
|
214
|
+
yield buf[:i_r]
|
|
215
|
+
buf = buf[end:]
|
|
216
|
+
else:
|
|
217
|
+
yield buf[:i_n]
|
|
218
|
+
buf = buf[i_n + 1 :]
|
|
219
|
+
if buf:
|
|
220
|
+
yield buf.rstrip("\r")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _stream_headers(headers: Mapping[str, str] | None) -> dict[str, str]:
|
|
224
|
+
merged: dict[str, str] = {}
|
|
225
|
+
if headers:
|
|
226
|
+
merged.update(headers)
|
|
227
|
+
# Do not override an explicit Accept from the caller.
|
|
228
|
+
if not any(k.lower() == "accept" for k in merged):
|
|
229
|
+
merged["Accept"] = "text/event-stream"
|
|
230
|
+
return merged
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def post_chat(
|
|
234
|
+
base_url: str,
|
|
235
|
+
message: str,
|
|
236
|
+
*,
|
|
237
|
+
thread_id: str | None = None,
|
|
238
|
+
headers: Mapping[str, str] | None = None,
|
|
239
|
+
timeout: float | httpx.Timeout | None = None,
|
|
240
|
+
metadata: Mapping[str, Any] | None = None,
|
|
241
|
+
) -> Iterator[SseEvent]:
|
|
242
|
+
"""POST ``/chat`` and yield the streamed events as they arrive.
|
|
243
|
+
|
|
244
|
+
``timeout`` defaults to :data:`STREAM_TIMEOUT` (looked up at call time): a
|
|
245
|
+
short connect phase and a long per-read gap. A scalar applies to every
|
|
246
|
+
phase. Raises :class:`ChatHTTPError` when the response status is not 2xx
|
|
247
|
+
(the body is read fully so the caller can show it), and lets ``httpx``
|
|
248
|
+
transport errors propagate so callers can distinguish "unreachable"
|
|
249
|
+
(``ConnectError``) from a stalled stream (``ReadTimeout``) and from
|
|
250
|
+
"refused".
|
|
251
|
+
"""
|
|
252
|
+
base = _normalise_base(base_url)
|
|
253
|
+
body: dict[str, Any] = {"message": message, "metadata": dict(metadata or {})}
|
|
254
|
+
if thread_id:
|
|
255
|
+
body["thread_id"] = thread_id
|
|
256
|
+
yield from _post_stream(f"{base}/chat", body, headers=headers, timeout=timeout)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _post_stream(
|
|
260
|
+
url: str,
|
|
261
|
+
body: Mapping[str, Any],
|
|
262
|
+
*,
|
|
263
|
+
headers: Mapping[str, str] | None,
|
|
264
|
+
timeout: float | httpx.Timeout | None,
|
|
265
|
+
) -> Iterator[SseEvent]:
|
|
266
|
+
"""POST ``body`` as JSON and yield the SSE events of the answer (``post_chat``'s rules)."""
|
|
267
|
+
if timeout is None:
|
|
268
|
+
timeout = STREAM_TIMEOUT
|
|
269
|
+
with (
|
|
270
|
+
_http.client(url, timeout=timeout) as client,
|
|
271
|
+
client.stream("POST", url, json=dict(body), headers=_stream_headers(headers)) as resp,
|
|
272
|
+
):
|
|
273
|
+
if resp.status_code >= 400:
|
|
274
|
+
resp.read()
|
|
275
|
+
raise ChatHTTPError(resp.status_code, resp.text, url)
|
|
276
|
+
yield from iter_sse(_iter_stream_lines(resp.iter_text()))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def path_segment(value: str, what: str) -> str:
|
|
280
|
+
"""``value`` as one URL path segment: percent-encoded, so no id can reach another route.
|
|
281
|
+
|
|
282
|
+
An approval id or a thread id comes from a server payload or the command
|
|
283
|
+
line; ``../chat``, ``a/b``, ``.`` or ``..`` must stay one segment of the
|
|
284
|
+
approval route. Percent-encoding leaves dots alone, and a client drops a
|
|
285
|
+
``.``/``..`` segment (or the one before it), so their dots are encoded too.
|
|
286
|
+
"""
|
|
287
|
+
if not isinstance(value, str) or not value.strip():
|
|
288
|
+
raise ValueError(f"{what} must be a non-empty string")
|
|
289
|
+
segment = quote(value, safe="")
|
|
290
|
+
return segment.replace(".", "%2E") if segment in (".", "..") else segment
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def approval_url(base_url: str, thread_id: str, approval_id: str | None = None) -> str:
|
|
294
|
+
"""``<base>/threads/<thread_id>/approvals[/<approval_id>]`` with each id one segment."""
|
|
295
|
+
url = f"{_normalise_base(base_url)}/threads/{path_segment(thread_id, 'thread id')}/approvals"
|
|
296
|
+
if approval_id is not None:
|
|
297
|
+
url += f"/{path_segment(approval_id, 'approval id')}"
|
|
298
|
+
return url
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def decide_approval(
|
|
302
|
+
base_url: str,
|
|
303
|
+
thread_id: str,
|
|
304
|
+
approval_id: str,
|
|
305
|
+
decision: str,
|
|
306
|
+
*,
|
|
307
|
+
comment: str | None = None,
|
|
308
|
+
headers: Mapping[str, str] | None = None,
|
|
309
|
+
timeout: float | httpx.Timeout | None = None,
|
|
310
|
+
) -> Iterator[SseEvent]:
|
|
311
|
+
"""Approve or reject a pending approval; yield the events of the resumed run.
|
|
312
|
+
|
|
313
|
+
``POST /threads/{thread_id}/approvals/{approval_id}`` with
|
|
314
|
+
``{"decision": "approve"|"reject", "comment": ...}`` and nothing else: the
|
|
315
|
+
server binds the decision to the call it recorded, so the client never
|
|
316
|
+
sends (or could change) the call itself. The answer is an SSE stream of the
|
|
317
|
+
continuation, with the same events as ``POST /chat``. A refusal is a
|
|
318
|
+
:class:`ChatHTTPError`: 403 (not an allowed approver), 404 (no such
|
|
319
|
+
approval or thread), 409 (already decided), 410 (expired, which rejected
|
|
320
|
+
the call).
|
|
321
|
+
"""
|
|
322
|
+
if decision not in DECISIONS:
|
|
323
|
+
raise ValueError(f"decision must be one of {', '.join(DECISIONS)}")
|
|
324
|
+
body: dict[str, Any] = {"decision": decision}
|
|
325
|
+
if comment:
|
|
326
|
+
body["comment"] = comment
|
|
327
|
+
url = approval_url(base_url, thread_id, approval_id)
|
|
328
|
+
yield from _post_stream(url, body, headers=headers, timeout=timeout)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _json_list(resp: httpx.Response, url: str, key: str) -> list[dict[str, Any]]:
|
|
332
|
+
if resp.status_code >= 400:
|
|
333
|
+
raise ChatHTTPError(resp.status_code, resp.text, url)
|
|
334
|
+
try:
|
|
335
|
+
data = resp.json()
|
|
336
|
+
except ValueError:
|
|
337
|
+
raise ChatClientError(
|
|
338
|
+
f"unexpected answer from {redact_credentials(url)}: not JSON"
|
|
339
|
+
) from None
|
|
340
|
+
if isinstance(data, dict):
|
|
341
|
+
data = data.get(key, [])
|
|
342
|
+
if not isinstance(data, list):
|
|
343
|
+
raise ChatClientError(f"unexpected answer from {redact_credentials(url)}: not a list")
|
|
344
|
+
return [item for item in data if isinstance(item, dict)]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def list_approvals(
|
|
348
|
+
base_url: str,
|
|
349
|
+
thread_id: str,
|
|
350
|
+
*,
|
|
351
|
+
headers: Mapping[str, str] | None = None,
|
|
352
|
+
timeout: float = 30.0,
|
|
353
|
+
) -> list[dict[str, Any]]:
|
|
354
|
+
"""GET ``/threads/{thread_id}/approvals``: the thread's approvals, pending or decided.
|
|
355
|
+
|
|
356
|
+
The server may wrap the list as ``{"approvals": [...]}``; both shapes are accepted.
|
|
357
|
+
"""
|
|
358
|
+
url = approval_url(base_url, thread_id)
|
|
359
|
+
resp = _http.get(url, headers=dict(headers or {}), timeout=timeout)
|
|
360
|
+
return _json_list(resp, url, "approvals")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def list_visible_approvals(
|
|
364
|
+
base_url: str,
|
|
365
|
+
*,
|
|
366
|
+
status: str | None = None,
|
|
367
|
+
headers: Mapping[str, str] | None = None,
|
|
368
|
+
limit: int = 100,
|
|
369
|
+
offset: int = 0,
|
|
370
|
+
timeout: float = 30.0,
|
|
371
|
+
) -> list[dict[str, Any]]:
|
|
372
|
+
"""GET ``/approvals``: across threads, the approvals the caller may see (one page).
|
|
373
|
+
|
|
374
|
+
The caller's own, the ones naming one of its roles (it may decide them),
|
|
375
|
+
and every one for a read-across role; each row carries its ``thread_id``.
|
|
376
|
+
An agent without this route answers 404 or 405 (a :class:`ChatHTTPError`).
|
|
377
|
+
"""
|
|
378
|
+
url = f"{_normalise_base(base_url)}/approvals"
|
|
379
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
380
|
+
if status:
|
|
381
|
+
params["status"] = status
|
|
382
|
+
resp = _http.get(url, params=params, headers=dict(headers or {}), timeout=timeout)
|
|
383
|
+
return _json_list(resp, url, "approvals")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def list_threads(
|
|
387
|
+
base_url: str,
|
|
388
|
+
*,
|
|
389
|
+
headers: Mapping[str, str] | None = None,
|
|
390
|
+
limit: int = 100,
|
|
391
|
+
offset: int = 0,
|
|
392
|
+
timeout: float = 30.0,
|
|
393
|
+
) -> list[dict[str, Any]]:
|
|
394
|
+
"""GET ``/threads``: the caller's own threads (``{thread_id, ...}`` rows), one page."""
|
|
395
|
+
url = f"{_normalise_base(base_url)}/threads"
|
|
396
|
+
resp = _http.get(
|
|
397
|
+
url,
|
|
398
|
+
params={"limit": limit, "offset": offset},
|
|
399
|
+
headers=dict(headers or {}),
|
|
400
|
+
timeout=timeout,
|
|
401
|
+
)
|
|
402
|
+
return _json_list(resp, url, "threads")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def delete_thread(
|
|
406
|
+
base_url: str,
|
|
407
|
+
thread_id: str,
|
|
408
|
+
*,
|
|
409
|
+
headers: Mapping[str, str] | None = None,
|
|
410
|
+
timeout: float = 30.0,
|
|
411
|
+
) -> None:
|
|
412
|
+
"""DELETE ``/threads/{thread_id}``: the thread with its runs and its approvals.
|
|
413
|
+
|
|
414
|
+
Only its owner may. Raises :class:`ChatHTTPError` on any status but 2xx
|
|
415
|
+
(a 404 too: the route may be missing as well as the thread), and lets
|
|
416
|
+
transport errors propagate.
|
|
417
|
+
"""
|
|
418
|
+
url = f"{_normalise_base(base_url)}/threads/{path_segment(thread_id, 'thread id')}"
|
|
419
|
+
resp = _http.delete(url, headers=dict(headers or {}), timeout=timeout)
|
|
420
|
+
if resp.status_code >= 300:
|
|
421
|
+
raise ChatHTTPError(resp.status_code, resp.text, url)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_health(
|
|
425
|
+
base_url: str,
|
|
426
|
+
*,
|
|
427
|
+
headers: Mapping[str, str] | None = None,
|
|
428
|
+
timeout: float = 10.0,
|
|
429
|
+
) -> dict[str, Any]:
|
|
430
|
+
"""GET ``/health`` and return its JSON body.
|
|
431
|
+
|
|
432
|
+
Raises :class:`ChatHTTPError` on a non-2xx status and lets transport
|
|
433
|
+
errors propagate.
|
|
434
|
+
"""
|
|
435
|
+
url = f"{_normalise_base(base_url)}/health"
|
|
436
|
+
resp = _http.get(url, headers=dict(headers or {}), timeout=timeout)
|
|
437
|
+
if resp.status_code >= 400:
|
|
438
|
+
raise ChatHTTPError(resp.status_code, resp.text, url)
|
|
439
|
+
data = resp.json()
|
|
440
|
+
return data if isinstance(data, dict) else {"status": data}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def get_thread_messages(
|
|
444
|
+
base_url: str,
|
|
445
|
+
thread_id: str,
|
|
446
|
+
*,
|
|
447
|
+
headers: Mapping[str, str] | None = None,
|
|
448
|
+
timeout: float = 30.0,
|
|
449
|
+
) -> list[dict[str, Any]]:
|
|
450
|
+
"""GET ``/threads/{thread_id}/messages`` and return the ordered messages.
|
|
451
|
+
|
|
452
|
+
The server may wrap the list as ``{"messages": [...]}``; both shapes are
|
|
453
|
+
accepted.
|
|
454
|
+
"""
|
|
455
|
+
url = f"{_normalise_base(base_url)}/threads/{thread_id}/messages"
|
|
456
|
+
resp = _http.get(url, headers=dict(headers or {}), timeout=timeout)
|
|
457
|
+
if resp.status_code >= 400:
|
|
458
|
+
raise ChatHTTPError(resp.status_code, resp.text, url)
|
|
459
|
+
data = resp.json()
|
|
460
|
+
if isinstance(data, dict):
|
|
461
|
+
data = data.get("messages", [])
|
|
462
|
+
return list(data)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
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
|
+
"""Click helpers: lazy command loading and source-path display in --help."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import inspect
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from graph_agents_cli._experiments import resolve_experiment
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LazyGroup(click.Group):
|
|
29
|
+
"""Click group that defers importing subcommand modules until needed.
|
|
30
|
+
|
|
31
|
+
Register subcommands with `add_lazy_command(name, "module.path:obj",
|
|
32
|
+
short_help)`. The module is imported only when the command is actually
|
|
33
|
+
invoked (e.g. `tool name ...`) or when its own help is requested
|
|
34
|
+
(`tool name --help`). The parent group's --help (`tool --help`) renders
|
|
35
|
+
the supplied `short_help` strings directly without triggering any imports.
|
|
36
|
+
|
|
37
|
+
`short_help` must match the real command's docstring summary; the parity
|
|
38
|
+
test in tests/unittests/cli/test_click.py enforces this.
|
|
39
|
+
|
|
40
|
+
Pattern reference:
|
|
41
|
+
https://click.palletsprojects.com/en/stable/complex/#lazily-loading-subcommands
|
|
42
|
+
|
|
43
|
+
TODO: Python 3.15 introduces a native `lazy` import keyword (PEP 810). Once
|
|
44
|
+
our minimum Python version reaches 3.15, the `add_lazy_command` mechanism
|
|
45
|
+
here may become redundant — revisit and consider simplifying.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
49
|
+
super().__init__(*args, **kwargs)
|
|
50
|
+
self._lazy_commands: dict[str, tuple[str, str]] = {}
|
|
51
|
+
# name -> experiment label
|
|
52
|
+
self._experiment_gates: dict[str, str] = {}
|
|
53
|
+
self._overrides: dict[str, click.Command] = {}
|
|
54
|
+
|
|
55
|
+
def add_lazy_command(
|
|
56
|
+
self,
|
|
57
|
+
name: str,
|
|
58
|
+
import_path: str,
|
|
59
|
+
short_help: str,
|
|
60
|
+
experiment: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Register a subcommand for lazy loading.
|
|
63
|
+
|
|
64
|
+
If `experiment` is given, the command is only listed and invocable when
|
|
65
|
+
that experiment resolves truthy; otherwise it behaves as if it were
|
|
66
|
+
never registered (hidden from --help, "No such command" on invoke).
|
|
67
|
+
|
|
68
|
+
The experiment gate is resolved per invocation in `list_commands`/`get_command`,
|
|
69
|
+
not at this registration call: `main` is built once at import, so
|
|
70
|
+
guarding registration would freeze visibility before a unit test could
|
|
71
|
+
override the experiment.
|
|
72
|
+
"""
|
|
73
|
+
self._lazy_commands[name] = (import_path, short_help)
|
|
74
|
+
if experiment is not None:
|
|
75
|
+
self._experiment_gates[name] = experiment
|
|
76
|
+
|
|
77
|
+
def _is_visible(self, name: str) -> bool:
|
|
78
|
+
"""True if `name` is not hidden via experiment."""
|
|
79
|
+
label = self._experiment_gates.get(name)
|
|
80
|
+
if label is None:
|
|
81
|
+
return True
|
|
82
|
+
return bool(resolve_experiment(label))
|
|
83
|
+
|
|
84
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
85
|
+
names = set(super().list_commands(ctx)) | set(self._lazy_commands) | set(self._overrides)
|
|
86
|
+
return sorted(filter(self._is_visible, names))
|
|
87
|
+
|
|
88
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
89
|
+
# Overrides are consulted only after the visibility check, so an installed
|
|
90
|
+
# extension cannot expose a command that is gated off.
|
|
91
|
+
if not self._is_visible(cmd_name):
|
|
92
|
+
return None
|
|
93
|
+
if cmd_name in self._overrides:
|
|
94
|
+
return self._overrides[cmd_name]
|
|
95
|
+
if cmd_name in self._lazy_commands and cmd_name not in self.commands:
|
|
96
|
+
import_path, _ = self._lazy_commands[cmd_name]
|
|
97
|
+
module_path, attr = import_path.split(":")
|
|
98
|
+
cmd = getattr(importlib.import_module(module_path), attr)
|
|
99
|
+
patch_source_in_help(cmd)
|
|
100
|
+
self.commands[cmd_name] = cmd
|
|
101
|
+
return super().get_command(ctx, cmd_name)
|
|
102
|
+
|
|
103
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
104
|
+
rows: list[tuple[str, str]] = []
|
|
105
|
+
for name in self.list_commands(ctx):
|
|
106
|
+
if name in self._overrides:
|
|
107
|
+
rows.append((name, self._overrides[name].get_short_help_str(limit=1000)))
|
|
108
|
+
continue
|
|
109
|
+
if name in self.commands:
|
|
110
|
+
# Don't truncate — Click's default of 45 would cut our docstring summaries.
|
|
111
|
+
rows.append((name, self.commands[name].get_short_help_str(limit=1000)))
|
|
112
|
+
continue
|
|
113
|
+
lazy = self._lazy_commands.get(name)
|
|
114
|
+
if lazy is not None:
|
|
115
|
+
rows.append((name, lazy[1]))
|
|
116
|
+
if rows:
|
|
117
|
+
with formatter.section("Commands"):
|
|
118
|
+
formatter.write_dl(rows)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _source_path(cmd: Any) -> str | None:
|
|
122
|
+
"""Resolve the absolute file path of a command's callback module."""
|
|
123
|
+
cb = cmd.callback
|
|
124
|
+
if cb is None:
|
|
125
|
+
return None
|
|
126
|
+
try:
|
|
127
|
+
mod = importlib.import_module(cb.__module__)
|
|
128
|
+
return inspect.getfile(mod)
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def patch_source_in_help(cmd: Any) -> None:
|
|
134
|
+
"""Recursively patch all commands to show source location in --help epilog.
|
|
135
|
+
|
|
136
|
+
Idempotent: a command registered as lazy in two parents (e.g., the
|
|
137
|
+
`create` alias) would otherwise get its `format_epilog` wrapped twice
|
|
138
|
+
and render two `Source:` lines.
|
|
139
|
+
"""
|
|
140
|
+
if getattr(cmd, "_source_patched", False):
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
original = cmd.format_epilog
|
|
144
|
+
|
|
145
|
+
def _patched(ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
146
|
+
original(ctx, formatter)
|
|
147
|
+
path = _source_path(cmd)
|
|
148
|
+
if path:
|
|
149
|
+
formatter.write("\n")
|
|
150
|
+
formatter.write(f"Source: {path}\n")
|
|
151
|
+
|
|
152
|
+
cmd.format_epilog = _patched
|
|
153
|
+
cmd._source_patched = True
|
|
154
|
+
|
|
155
|
+
if isinstance(cmd, click.Group):
|
|
156
|
+
for sub in cmd.commands.values():
|
|
157
|
+
patch_source_in_help(sub)
|