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,681 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
# Modifications Copyright 2026 graph-agents-cli contributors
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Logging and tracing setup.
|
|
17
|
+
|
|
18
|
+
Logging (`setup_logging()`, fastapi runtime): one handler on the root
|
|
19
|
+
logger, level `LOG_LEVEL` (default INFO), format `LOG_FORMAT=json|text`
|
|
20
|
+
(default `json`, `text` under `APP_ENV=dev`). Every record carries the
|
|
21
|
+
request id, and inside a run the run id, the thread id and the hashed
|
|
22
|
+
principal, from context variables set by the HTTP middleware and the chat
|
|
23
|
+
runtime. Nothing logs headers, bodies or credentials:
|
|
24
|
+
|
|
25
|
+
* uvicorn's access lines keep the path and drop the query string (a client
|
|
26
|
+
that puts a token in the URL, `?access_token=...`, never gets it logged);
|
|
27
|
+
* the HTTP client libraries (`httpx`, `httpcore`, and `httpx2`/`httpcore2`
|
|
28
|
+
that model provider SDKs use) log at WARNING only, since their INFO lines
|
|
29
|
+
carry every outbound URL with its query and path values (tool arguments,
|
|
30
|
+
customer ids); `app_utils.api_client` logs each outbound call itself with
|
|
31
|
+
the API, method, operation id and path template instead;
|
|
32
|
+
* Python warnings are captured into the same handler (one JSON record each,
|
|
33
|
+
logger `py.warnings`), and the value a pydantic serializer warning echoes
|
|
34
|
+
(`input_value=...`, which can be a run context holding a forwarded
|
|
35
|
+
credential) is redacted.
|
|
36
|
+
|
|
37
|
+
LangGraph Server configures its own handlers, format and level (its
|
|
38
|
+
`LOG_LEVEL`, `LOG_JSON`). Under `langgraph-server`, `setup_server_logging()`
|
|
39
|
+
applies the same three rules to them: the server's access lines
|
|
40
|
+
(`langgraph_api.server`) lose their `query_string` field, the HTTP client
|
|
41
|
+
libraries log at WARNING only (`quiet_client_loggers()`; `api_client`
|
|
42
|
+
does the same when imported, in every process that runs the graph), and
|
|
43
|
+
warnings are captured with their values redacted.
|
|
44
|
+
|
|
45
|
+
Tracing: explicit opt-in, LangSmith or OpenTelemetry.
|
|
46
|
+
|
|
47
|
+
Nothing is configured unless `TRACING_ENABLED=true`. Then:
|
|
48
|
+
|
|
49
|
+
* with `LANGSMITH_API_KEY` set, LangChain's native LangSmith tracing is enabled
|
|
50
|
+
through the environment (`LANGSMITH_TRACING`, `LANGSMITH_PROJECT`,
|
|
51
|
+
`LANGSMITH_ENDPOINT`);
|
|
52
|
+
* otherwise spans go over OTLP/HTTP to `OTEL_EXPORTER_OTLP_ENDPOINT` using
|
|
53
|
+
OpenInference's LangChain instrumentation.
|
|
54
|
+
|
|
55
|
+
`TRACE_CAPTURE=metadata` (default) records structure, timing, model and tool
|
|
56
|
+
names, token counts, error types and identifiers (thread, run, hashed
|
|
57
|
+
principal) but no prompt or completion text, tool arguments or results, or
|
|
58
|
+
error messages. `TRACE_CAPTURE=full` records everything. The same policy is
|
|
59
|
+
applied to LangSmith (`hide_inputs`/`hide_outputs` plus a client anonymizer
|
|
60
|
+
that reduces a run's `error` to the exception class), to OpenInference (its
|
|
61
|
+
masking config plus an exporter that strips exception messages, stack traces
|
|
62
|
+
and the span status description) and to the run records the app keeps.
|
|
63
|
+
|
|
64
|
+
Correlation across services (`PROPAGATE_TRACE_HEADERS`, `trace_scope`: `peers`
|
|
65
|
+
by default, `all` or `off`; `true` reads as `peers`, `false` as `off`): under
|
|
66
|
+
`peers` every call of the policy client to another agent (`protocol: a2a`), or
|
|
67
|
+
to an `auth: forward` or `auth: exchange` API (reached with the caller's own
|
|
68
|
+
credential or a token exchanged for it), carries this request's `X-Request-ID`
|
|
69
|
+
and, when spans go over OTLP, the W3C trace context of the current span
|
|
70
|
+
(`outbound_trace_headers`, installed with `api_client.set_outbound_headers`;
|
|
71
|
+
`api_client.propagates` names the APIs), and a call to any other API carries
|
|
72
|
+
neither; an incoming `traceparent` on the A2A routes is attached to the
|
|
73
|
+
request's context (`attach_trace_context`, in
|
|
74
|
+
`middleware.RequestContextMiddleware`), so an agent this one calls over A2A
|
|
75
|
+
logs the same request id and its spans join this trace, while a caller of the
|
|
76
|
+
public routes (`/chat`) cannot choose its trace ids. `all` sends them to every
|
|
77
|
+
API and continues a trace on every path (behind a tracing gateway); `off`
|
|
78
|
+
neither. Under LangSmith tracing only the request id is passed on.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
from __future__ import annotations
|
|
82
|
+
|
|
83
|
+
import json
|
|
84
|
+
import logging
|
|
85
|
+
import os
|
|
86
|
+
import re
|
|
87
|
+
import sys
|
|
88
|
+
from collections.abc import Iterable
|
|
89
|
+
from contextvars import ContextVar
|
|
90
|
+
from datetime import UTC, datetime
|
|
91
|
+
from pathlib import Path
|
|
92
|
+
from typing import Any
|
|
93
|
+
|
|
94
|
+
from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
|
|
95
|
+
|
|
96
|
+
logger = logging.getLogger(__name__)
|
|
97
|
+
|
|
98
|
+
_initialized = False
|
|
99
|
+
# Set once spans are exported over OTLP: the W3C trace context is then passed on and taken.
|
|
100
|
+
_otlp_active = False
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Logging
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
# Correlation ids attached to every log record of the current request / run.
|
|
107
|
+
LOG_CONTEXT: dict[str, ContextVar[str | None]] = {
|
|
108
|
+
"request_id": ContextVar("request_id", default=None),
|
|
109
|
+
"run_id": ContextVar("run_id", default=None),
|
|
110
|
+
"thread_id": ContextVar("thread_id", default=None),
|
|
111
|
+
"principal_hash": ContextVar("principal_hash", default=None),
|
|
112
|
+
# The agent presenting a delegated request (a client name, not personal data).
|
|
113
|
+
"actor": ContextVar("actor", default=None),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
117
|
+
_HANDLER_FLAG = "_graph_agents_handler"
|
|
118
|
+
_STANDARD_ATTRS = set(logging.LogRecord("", 0, "", 0, "", None, None).__dict__) | {
|
|
119
|
+
"message",
|
|
120
|
+
"asctime",
|
|
121
|
+
"taskName",
|
|
122
|
+
"color_message", # uvicorn's ANSI-coloured duplicate of the message
|
|
123
|
+
"ids", # set by TextFormatter
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def bind_log_context(**values: str | None) -> None:
|
|
128
|
+
"""Set correlation ids (`request_id`, `run_id`, `thread_id`, `principal_hash`, `actor`)."""
|
|
129
|
+
for name, value in values.items():
|
|
130
|
+
LOG_CONTEXT[name].set(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def log_level() -> str:
|
|
134
|
+
"""`LOG_LEVEL` (default INFO); an unknown level is a startup error."""
|
|
135
|
+
level = (os.environ.get("LOG_LEVEL") or "INFO").strip().upper()
|
|
136
|
+
if level not in LOG_LEVELS:
|
|
137
|
+
raise SettingsError(f"LOG_LEVEL={level!r} is not one of {', '.join(LOG_LEVELS)}.")
|
|
138
|
+
return level
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def log_format() -> str:
|
|
142
|
+
"""`LOG_FORMAT` (`json` or `text`); defaults to `text` under APP_ENV=dev, else `json`."""
|
|
143
|
+
fmt = (os.environ.get("LOG_FORMAT") or "").strip().lower()
|
|
144
|
+
if not fmt:
|
|
145
|
+
return "text" if os.environ.get("APP_ENV") == "dev" else "json"
|
|
146
|
+
if fmt not in ("json", "text"):
|
|
147
|
+
raise SettingsError(f"LOG_FORMAT={fmt!r} must be 'json' or 'text'.")
|
|
148
|
+
return fmt
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ContextFilter(logging.Filter):
|
|
152
|
+
"""Copy the correlation ids of the current context onto each record."""
|
|
153
|
+
|
|
154
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
155
|
+
for name, var in LOG_CONTEXT.items():
|
|
156
|
+
if getattr(record, name, None) is None:
|
|
157
|
+
setattr(record, name, var.get())
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class JsonFormatter(logging.Formatter):
|
|
162
|
+
"""One JSON object per line: ts, level, logger, message, correlation ids, extras."""
|
|
163
|
+
|
|
164
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
165
|
+
payload: dict[str, Any] = {
|
|
166
|
+
"ts": datetime.fromtimestamp(record.created, tz=UTC).isoformat(timespec="milliseconds"),
|
|
167
|
+
"level": record.levelname,
|
|
168
|
+
"logger": record.name,
|
|
169
|
+
"message": record.getMessage(),
|
|
170
|
+
}
|
|
171
|
+
for name in LOG_CONTEXT:
|
|
172
|
+
value = getattr(record, name, None)
|
|
173
|
+
if value:
|
|
174
|
+
payload[name] = value
|
|
175
|
+
for key, value in record.__dict__.items():
|
|
176
|
+
if key in _STANDARD_ATTRS or key in payload or key in LOG_CONTEXT or value is None:
|
|
177
|
+
continue
|
|
178
|
+
if key.startswith("_"):
|
|
179
|
+
continue
|
|
180
|
+
payload[key] = value if isinstance(value, str | int | float | bool) else repr(value)
|
|
181
|
+
if record.exc_info:
|
|
182
|
+
payload["exc_type"] = record.exc_info[0].__name__ if record.exc_info[0] else None
|
|
183
|
+
payload["exception"] = self.formatException(record.exc_info)
|
|
184
|
+
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TextFormatter(logging.Formatter):
|
|
188
|
+
def __init__(self) -> None:
|
|
189
|
+
super().__init__("%(asctime)s %(levelname)s %(name)s%(ids)s: %(message)s")
|
|
190
|
+
|
|
191
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
192
|
+
ids = " ".join(
|
|
193
|
+
f"{name}={getattr(record, name)}"
|
|
194
|
+
for name in ("request_id", "run_id")
|
|
195
|
+
if getattr(record, name, None)
|
|
196
|
+
)
|
|
197
|
+
record.ids = f" [{ids}]" if ids else ""
|
|
198
|
+
return super().format(record)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _StderrHandler(logging.StreamHandler):
|
|
202
|
+
"""Writes to whatever `sys.stderr` is at emit time (test runners swap it)."""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
logging.Handler.__init__(self)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def stream(self) -> Any: # type: ignore[override]
|
|
209
|
+
return sys.stderr
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class AccessLogFilter(logging.Filter):
|
|
213
|
+
"""Drop the query string from uvicorn's access lines.
|
|
214
|
+
|
|
215
|
+
uvicorn logs `'%s - "%s %s HTTP/%s" %d'` with the full path and query as
|
|
216
|
+
the third argument. Query strings carry whatever a client put there,
|
|
217
|
+
credentials included (`?access_token=...`), so only the path is kept.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
221
|
+
args = record.args
|
|
222
|
+
if isinstance(args, tuple) and len(args) >= 3 and isinstance(args[2], str):
|
|
223
|
+
path = args[2]
|
|
224
|
+
if "?" in path:
|
|
225
|
+
record.args = (*args[:2], path.split("?", 1)[0], *args[3:])
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# A pydantic serializer warning names the value it could not serialize:
|
|
230
|
+
# `[field_name='context', input_value=AgentContext(...), input_type=...]`. The
|
|
231
|
+
# value's repr can hold a forwarded credential (a run context's attributes),
|
|
232
|
+
# so everything between `input_value=` and the last `, input_type=` of the line goes.
|
|
233
|
+
_WARNING_VALUE = re.compile(r"input_value=.*, input_type=")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class WarningRedactionFilter(logging.Filter):
|
|
237
|
+
"""Redact the values pydantic warnings echo (see `_WARNING_VALUE`)."""
|
|
238
|
+
|
|
239
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
240
|
+
message = record.getMessage()
|
|
241
|
+
if "input_value=" in message:
|
|
242
|
+
record.msg = _WARNING_VALUE.sub("input_value=<redacted>, input_type=", message)
|
|
243
|
+
record.args = None
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Loggers whose INFO lines carry full outbound URLs (query and path values):
|
|
248
|
+
# httpx, and httpx2, which the model provider SDKs use.
|
|
249
|
+
QUIET_LOGGERS = ("httpx", "httpcore", "httpx2", "httpcore2")
|
|
250
|
+
# LangGraph Server's access logger (a structlog event dict per request).
|
|
251
|
+
SERVER_ACCESS_LOGGER = "langgraph_api.server"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ServerAccessLogFilter(logging.Filter):
|
|
255
|
+
"""Drop the query string from LangGraph Server's access lines.
|
|
256
|
+
|
|
257
|
+
The server logs each request through structlog with a `query_string`
|
|
258
|
+
field holding the raw query (credentials a client put in the URL
|
|
259
|
+
included). Its records carry the event dict as `msg`; the field is
|
|
260
|
+
removed before any handler formats it. The line's text holds the path only.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
264
|
+
event = record.msg
|
|
265
|
+
if isinstance(event, dict) and "query_string" in event:
|
|
266
|
+
record.msg = {k: v for k, v in event.items() if k != "query_string"}
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def quiet_client_loggers() -> None:
|
|
271
|
+
"""`QUIET_LOGGERS` at WARNING: their INFO lines carry full outbound URLs."""
|
|
272
|
+
for name in QUIET_LOGGERS:
|
|
273
|
+
logging.getLogger(name).setLevel(logging.WARNING)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def setup_server_logging() -> None:
|
|
277
|
+
"""langgraph-server: the logging rules of `setup_logging()` on the server's own handlers."""
|
|
278
|
+
_add_filter_once(logging.getLogger(SERVER_ACCESS_LOGGER), ServerAccessLogFilter)
|
|
279
|
+
_add_filter_once(logging.getLogger("uvicorn.access"), AccessLogFilter)
|
|
280
|
+
quiet_client_loggers()
|
|
281
|
+
logging.captureWarnings(True)
|
|
282
|
+
_add_filter_once(logging.getLogger("py.warnings"), WarningRedactionFilter)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def setup_logging() -> None:
|
|
286
|
+
"""Route every logger (uvicorn's included) through one handler; idempotent."""
|
|
287
|
+
root = logging.getLogger()
|
|
288
|
+
for handler in list(root.handlers):
|
|
289
|
+
if getattr(handler, _HANDLER_FLAG, False):
|
|
290
|
+
root.removeHandler(handler)
|
|
291
|
+
handler = _StderrHandler()
|
|
292
|
+
setattr(handler, _HANDLER_FLAG, True)
|
|
293
|
+
handler.addFilter(ContextFilter())
|
|
294
|
+
handler.setFormatter(JsonFormatter() if log_format() == "json" else TextFormatter())
|
|
295
|
+
root.addHandler(handler)
|
|
296
|
+
root.setLevel(log_level())
|
|
297
|
+
# uvicorn installs its own handlers before the app loads; send its records
|
|
298
|
+
# (startup, errors, access lines) through the same handler instead.
|
|
299
|
+
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
|
|
300
|
+
uv_logger = logging.getLogger(name)
|
|
301
|
+
uv_logger.handlers = []
|
|
302
|
+
uv_logger.propagate = True
|
|
303
|
+
_add_filter_once(logging.getLogger("uvicorn.access"), AccessLogFilter)
|
|
304
|
+
quiet_client_loggers()
|
|
305
|
+
# Warnings become records of this handler (JSON under LOG_FORMAT=json)
|
|
306
|
+
# instead of raw multi-line text on stderr.
|
|
307
|
+
logging.captureWarnings(True)
|
|
308
|
+
_add_filter_once(logging.getLogger("py.warnings"), WarningRedactionFilter)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _add_filter_once(target: logging.Logger, kind: type[logging.Filter]) -> None:
|
|
312
|
+
if not any(isinstance(f, kind) for f in target.filters):
|
|
313
|
+
target.addFilter(kind())
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# Tracing
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def tracing_enabled() -> bool:
|
|
322
|
+
return (os.environ.get("TRACING_ENABLED") or "false").strip().lower() in ("1", "true", "yes")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
CAPTURE_MODES = ("metadata", "full")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def trace_capture() -> str:
|
|
329
|
+
"""`TRACE_CAPTURE` (`metadata`, the default, or `full`); anything else is a startup error."""
|
|
330
|
+
mode = (os.environ.get("TRACE_CAPTURE") or "metadata").strip().lower()
|
|
331
|
+
if mode not in CAPTURE_MODES:
|
|
332
|
+
raise SettingsError(f"TRACE_CAPTURE={mode!r} must be 'metadata' or 'full'.")
|
|
333
|
+
return mode
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def capture_mode() -> str:
|
|
337
|
+
"""The capture mode for a trace. Startup refuses a bad `TRACE_CAPTURE`
|
|
338
|
+
(`trace_capture`); should one be read anyway, it records metadata only."""
|
|
339
|
+
mode = (os.environ.get("TRACE_CAPTURE") or "metadata").strip().lower()
|
|
340
|
+
return "full" if mode == "full" else "metadata"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def project_name() -> str:
|
|
344
|
+
return (
|
|
345
|
+
os.environ.get("LANGSMITH_PROJECT")
|
|
346
|
+
or os.environ.get("OTEL_SERVICE_NAME")
|
|
347
|
+
or Path.cwd().name
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def setup_telemetry() -> None:
|
|
352
|
+
"""Configure tracing once, per the environment. Safe to call repeatedly."""
|
|
353
|
+
global _initialized
|
|
354
|
+
if _initialized:
|
|
355
|
+
return
|
|
356
|
+
_initialized = True
|
|
357
|
+
|
|
358
|
+
if not tracing_enabled():
|
|
359
|
+
# A stray LANGSMITH_TRACING=true must not create a client: TRACING_ENABLED is the switch.
|
|
360
|
+
os.environ["LANGSMITH_TRACING"] = "false"
|
|
361
|
+
os.environ["LANGCHAIN_TRACING_V2"] = "false"
|
|
362
|
+
logger.info("Tracing disabled (TRACING_ENABLED != true).")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
full = capture_mode() == "full"
|
|
366
|
+
if os.environ.get("LANGSMITH_API_KEY"):
|
|
367
|
+
_setup_langsmith(full)
|
|
368
|
+
else:
|
|
369
|
+
_setup_otlp(full)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
_ERROR_TYPE = re.compile(r"\s*([A-Za-z_][\w.]*)")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def redact_error(payload: Any) -> Any:
|
|
376
|
+
"""LangSmith anonymizer: keep the exception class, drop the message and the stack trace.
|
|
377
|
+
|
|
378
|
+
LangChain records `run.error` as `repr(exc)` plus the formatted traceback,
|
|
379
|
+
and `LANGSMITH_HIDE_*` never touch it; only a client anonymizer does. The
|
|
380
|
+
client applies it to the `{"error": <str>}` wrapper (and to run metadata,
|
|
381
|
+
which passes through untouched), so only that exact shape is rewritten.
|
|
382
|
+
"""
|
|
383
|
+
if (
|
|
384
|
+
isinstance(payload, dict)
|
|
385
|
+
and set(payload) == {"error"}
|
|
386
|
+
and isinstance(payload["error"], str)
|
|
387
|
+
):
|
|
388
|
+
m = _ERROR_TYPE.match(payload["error"])
|
|
389
|
+
return {"error": (m.group(1) if m else "Error") + ": <redacted>"}
|
|
390
|
+
return payload
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _setup_langsmith(full: bool) -> None:
|
|
394
|
+
os.environ["LANGSMITH_TRACING"] = "true"
|
|
395
|
+
os.environ.setdefault("LANGSMITH_PROJECT", project_name())
|
|
396
|
+
hide = "false" if full else "true"
|
|
397
|
+
# Kept for any client created elsewhere from the environment.
|
|
398
|
+
os.environ["LANGSMITH_HIDE_INPUTS"] = hide
|
|
399
|
+
os.environ["LANGSMITH_HIDE_OUTPUTS"] = hide
|
|
400
|
+
try:
|
|
401
|
+
import langsmith
|
|
402
|
+
except ImportError as exc: # pragma: no cover - dependency is in pyproject
|
|
403
|
+
logger.warning("LangSmith tracing unavailable: %s", exc)
|
|
404
|
+
return
|
|
405
|
+
# One process-wide client: every LangChainTracer (env-driven, /chat, A2A,
|
|
406
|
+
# the playground) picks it up, so the error redaction cannot be bypassed.
|
|
407
|
+
client = langsmith.Client(
|
|
408
|
+
hide_inputs=not full,
|
|
409
|
+
hide_outputs=not full,
|
|
410
|
+
anonymizer=None if full else redact_error,
|
|
411
|
+
)
|
|
412
|
+
langsmith.configure(client=client)
|
|
413
|
+
logger.info(
|
|
414
|
+
"Tracing to LangSmith project %r (capture=%s, endpoint=%s).",
|
|
415
|
+
os.environ["LANGSMITH_PROJECT"],
|
|
416
|
+
"full" if full else "metadata",
|
|
417
|
+
os.environ.get("LANGSMITH_ENDPOINT", "default"),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _setup_otlp(full: bool) -> None:
|
|
422
|
+
global _otlp_active
|
|
423
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip()
|
|
424
|
+
if not endpoint:
|
|
425
|
+
logger.warning(
|
|
426
|
+
"TRACING_ENABLED=true but neither LANGSMITH_API_KEY nor OTEL_EXPORTER_OTLP_ENDPOINT is set; "
|
|
427
|
+
"no exporter configured."
|
|
428
|
+
)
|
|
429
|
+
return
|
|
430
|
+
try:
|
|
431
|
+
from openinference.instrumentation import TraceConfig
|
|
432
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
433
|
+
from opentelemetry import trace
|
|
434
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
435
|
+
from opentelemetry.sdk.resources import Resource
|
|
436
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
437
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
438
|
+
except ImportError as exc: # pragma: no cover - dependencies are in pyproject
|
|
439
|
+
logger.warning("OpenTelemetry tracing unavailable: %s", exc)
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
resource = Resource.create(
|
|
443
|
+
{
|
|
444
|
+
"service.name": os.environ.get("OTEL_SERVICE_NAME") or project_name(),
|
|
445
|
+
"service.version": os.environ.get("AGENT_VERSION", "0.1.0"),
|
|
446
|
+
"deployment.environment": os.environ.get("APP_ENV", "dev"),
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
provider = TracerProvider(resource=resource)
|
|
450
|
+
exporter: Any = (
|
|
451
|
+
OTLPSpanExporter()
|
|
452
|
+
) # reads OTEL_EXPORTER_OTLP_ENDPOINT (+ /v1/traces) and headers
|
|
453
|
+
if not full:
|
|
454
|
+
exporter = _MetadataOnlyExporter(exporter)
|
|
455
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
456
|
+
trace.set_tracer_provider(provider)
|
|
457
|
+
|
|
458
|
+
hide = not full
|
|
459
|
+
config = TraceConfig(
|
|
460
|
+
hide_inputs=hide,
|
|
461
|
+
hide_outputs=hide,
|
|
462
|
+
hide_input_messages=hide,
|
|
463
|
+
hide_output_messages=hide,
|
|
464
|
+
hide_input_text=hide,
|
|
465
|
+
hide_output_text=hide,
|
|
466
|
+
hide_prompts=hide,
|
|
467
|
+
hide_choices=hide,
|
|
468
|
+
hide_llm_invocation_parameters=False,
|
|
469
|
+
hide_llm_tools=False,
|
|
470
|
+
)
|
|
471
|
+
LangChainInstrumentor().instrument(tracer_provider=provider, config=config)
|
|
472
|
+
_otlp_active = True
|
|
473
|
+
logger.info("Tracing over OTLP to %s (capture=%s).", endpoint, "full" if full else "metadata")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
# Correlation across services
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
TRACE_CONTEXT_HEADERS = ("traceparent", "tracestate")
|
|
481
|
+
# `PROPAGATE_TRACE_HEADERS`: how far the request id and the trace context go.
|
|
482
|
+
TRACE_SCOPE_PEERS = "peers"
|
|
483
|
+
TRACE_SCOPE_ALL = "all"
|
|
484
|
+
TRACE_SCOPE_OFF = "off"
|
|
485
|
+
TRACE_SCOPES = (TRACE_SCOPE_PEERS, TRACE_SCOPE_ALL, TRACE_SCOPE_OFF)
|
|
486
|
+
DEFAULT_TRACE_SCOPE = TRACE_SCOPE_PEERS
|
|
487
|
+
# The 0.2 spellings: true (the 0.2 default) reads as peers, false as off.
|
|
488
|
+
_TRUE = ("1", "true", "yes", "on")
|
|
489
|
+
_FALSE = ("0", "false", "no", "off")
|
|
490
|
+
# Where an incoming `traceparent` is continued under the default scope (0.3:
|
|
491
|
+
# `PROPAGATE_TRACE_HEADERS=peers`): the A2A routes only, so a caller of the public routes
|
|
492
|
+
# (`/chat`) cannot choose this agent's trace ids. `all` continues it on every path.
|
|
493
|
+
PEERS_INBOUND_TRACE_PREFIX = "/a2a/"
|
|
494
|
+
_said_true_is_peers = False
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def trace_scope() -> str:
|
|
498
|
+
"""`PROPAGATE_TRACE_HEADERS`: `peers` (the default), `all` or `off`.
|
|
499
|
+
|
|
500
|
+
`peers`: the request id and trace context go to other agents (`protocol:
|
|
501
|
+
a2a`) and to `auth: forward` and `auth: exchange` APIs (`api_client.propagates`),
|
|
502
|
+
and an incoming `traceparent` is continued on the A2A routes only. `all`: to
|
|
503
|
+
every API, and continued on every path (behind a tracing gateway). `off`:
|
|
504
|
+
neither. `true` (the 0.2 default) reads as `peers`, logged once at INFO, and
|
|
505
|
+
`false` as `off`. Anything else raises `SettingsError` (startup refuses it).
|
|
506
|
+
"""
|
|
507
|
+
global _said_true_is_peers
|
|
508
|
+
raw = (os.environ.get("PROPAGATE_TRACE_HEADERS") or "").strip().lower()
|
|
509
|
+
if not raw:
|
|
510
|
+
return DEFAULT_TRACE_SCOPE
|
|
511
|
+
if raw in _FALSE:
|
|
512
|
+
return TRACE_SCOPE_OFF
|
|
513
|
+
if raw in _TRUE:
|
|
514
|
+
if not _said_true_is_peers:
|
|
515
|
+
_said_true_is_peers = True
|
|
516
|
+
logger.info(
|
|
517
|
+
"PROPAGATE_TRACE_HEADERS=%s reads as peers: other agents and the APIs that act "
|
|
518
|
+
"for the user get the request id and trace context, and an incoming trace is "
|
|
519
|
+
"continued on the A2A routes only (all: every API and path)",
|
|
520
|
+
raw,
|
|
521
|
+
)
|
|
522
|
+
return TRACE_SCOPE_PEERS
|
|
523
|
+
if raw in TRACE_SCOPES:
|
|
524
|
+
return raw
|
|
525
|
+
raise SettingsError(
|
|
526
|
+
"PROPAGATE_TRACE_HEADERS must be peers, all or off (true reads as peers, false as off)."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _scope_or_off() -> str:
|
|
531
|
+
"""The scope, or `off` for a value startup would refuse (never propagate on a guess)."""
|
|
532
|
+
try:
|
|
533
|
+
return trace_scope()
|
|
534
|
+
except SettingsError:
|
|
535
|
+
return TRACE_SCOPE_OFF
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def propagate_trace_headers() -> bool:
|
|
539
|
+
"""Whether the request id and trace context go anywhere (`PROPAGATE_TRACE_HEADERS` is
|
|
540
|
+
not `off`): to the APIs `api_client.propagates` names, or every API under `all`."""
|
|
541
|
+
return _scope_or_off() != TRACE_SCOPE_OFF
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def propagate_to_every_api() -> bool:
|
|
545
|
+
"""`PROPAGATE_TRACE_HEADERS=all`: every API gets them, not only other agents and the APIs
|
|
546
|
+
that act for the user (`api_client.set_outbound_headers` asks it)."""
|
|
547
|
+
return _scope_or_off() == TRACE_SCOPE_ALL
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _trace_context_propagator() -> Any:
|
|
551
|
+
# W3C Trace Context only, whatever OTEL_PROPAGATORS says: no baggage leaves or enters.
|
|
552
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
553
|
+
|
|
554
|
+
return TraceContextTextMapPropagator()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def outbound_trace_headers() -> dict[str, str]:
|
|
558
|
+
"""The headers that carry this request's correlation to the agents it calls.
|
|
559
|
+
|
|
560
|
+
`X-Request-ID`, the id every log record of this request carries (a service
|
|
561
|
+
built from this template takes a caller's id as its own), and, when spans are
|
|
562
|
+
exported over OTLP, the W3C trace context of the current span (`traceparent`,
|
|
563
|
+
`tracestate`), so the callee's spans join this trace. The policy client adds
|
|
564
|
+
them only to calls of other agents (`protocol: a2a`) and of `auth: forward` and
|
|
565
|
+
`auth: exchange` APIs (`api_client.propagates`). Empty
|
|
566
|
+
under `PROPAGATE_TRACE_HEADERS=false`.
|
|
567
|
+
"""
|
|
568
|
+
if not propagate_trace_headers():
|
|
569
|
+
return {}
|
|
570
|
+
headers: dict[str, str] = {}
|
|
571
|
+
request_id = LOG_CONTEXT["request_id"].get()
|
|
572
|
+
if request_id:
|
|
573
|
+
headers["X-Request-ID"] = request_id
|
|
574
|
+
if _otlp_active:
|
|
575
|
+
carrier: dict[str, str] = {}
|
|
576
|
+
_trace_context_propagator().inject(carrier, context=_current_span_context())
|
|
577
|
+
headers.update({k: v for k, v in carrier.items() if k in TRACE_CONTEXT_HEADERS})
|
|
578
|
+
return headers
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _current_span_context() -> Any:
|
|
582
|
+
"""The OTel context to pass on: the span of the LangChain run in progress (a tool's).
|
|
583
|
+
|
|
584
|
+
OpenInference does not make its spans current in the OTel context (attaching
|
|
585
|
+
one from a callback could leak it to later spans), so inside a tool the OTel
|
|
586
|
+
current span is only what the request itself carried: nothing on a `/chat`
|
|
587
|
+
call that came without a `traceparent`. OpenInference keeps the run's span,
|
|
588
|
+
though, and the callee's spans then nest under the tool span that called it.
|
|
589
|
+
None (the OTel current context) outside a LangChain run.
|
|
590
|
+
"""
|
|
591
|
+
try:
|
|
592
|
+
from openinference.instrumentation.langchain import get_current_span
|
|
593
|
+
from opentelemetry import trace
|
|
594
|
+
|
|
595
|
+
span = get_current_span()
|
|
596
|
+
except Exception:
|
|
597
|
+
return None
|
|
598
|
+
if span is None or not span.get_span_context().is_valid:
|
|
599
|
+
return None
|
|
600
|
+
return trace.set_span_in_context(span)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def attach_trace_context(headers: Iterable[tuple[bytes, bytes]], path: str | None = None) -> Any:
|
|
604
|
+
"""Continue the caller's trace: attach the W3C trace context of an incoming request.
|
|
605
|
+
|
|
606
|
+
`headers` are the ASGI scope's, `path` the request's path (without the
|
|
607
|
+
app's root path). Returns the token `detach_trace_context` takes, or None
|
|
608
|
+
when nothing was attached: no `traceparent`, spans not exported over
|
|
609
|
+
OTLP, `PROPAGATE_TRACE_HEADERS=off`, or under `peers` (the default) a
|
|
610
|
+
path outside the A2A routes (`PEERS_INBOUND_TRACE_PREFIX`), so a caller
|
|
611
|
+
of the public routes cannot choose this agent's trace ids. A malformed
|
|
612
|
+
`traceparent` starts a new trace, as if there were none.
|
|
613
|
+
"""
|
|
614
|
+
if not _otlp_active:
|
|
615
|
+
return None
|
|
616
|
+
scope = _scope_or_off()
|
|
617
|
+
if scope == TRACE_SCOPE_OFF:
|
|
618
|
+
return None
|
|
619
|
+
if scope == TRACE_SCOPE_PEERS and not (path or "").startswith(PEERS_INBOUND_TRACE_PREFIX):
|
|
620
|
+
return None
|
|
621
|
+
carrier: dict[str, str] = {}
|
|
622
|
+
for key, value in headers:
|
|
623
|
+
name = key.decode("latin-1").lower()
|
|
624
|
+
if name in TRACE_CONTEXT_HEADERS:
|
|
625
|
+
carrier[name] = value.decode("latin-1")
|
|
626
|
+
if "traceparent" not in carrier:
|
|
627
|
+
return None
|
|
628
|
+
from opentelemetry import context as otel_context
|
|
629
|
+
|
|
630
|
+
return otel_context.attach(_trace_context_propagator().extract(carrier))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def detach_trace_context(token: Any) -> None:
|
|
634
|
+
"""Undo `attach_trace_context` (a None token: nothing was attached)."""
|
|
635
|
+
if token is not None:
|
|
636
|
+
from opentelemetry import context as otel_context
|
|
637
|
+
|
|
638
|
+
otel_context.detach(token)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class _MetadataOnlyExporter:
|
|
642
|
+
"""Wraps an exporter and strips exception messages and stack traces from spans.
|
|
643
|
+
|
|
644
|
+
Under `metadata` capture only the error *type* leaves the process: the
|
|
645
|
+
`exception.message` / `exception.stacktrace` event attributes are dropped
|
|
646
|
+
(`exception.type` stays) and an ERROR status loses its description, which
|
|
647
|
+
OpenInference fills with `repr(exc)` plus the traceback (OTLP `status.message`).
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
_DROP = ("exception.message", "exception.stacktrace")
|
|
651
|
+
|
|
652
|
+
def __init__(self, inner: Any) -> None:
|
|
653
|
+
self._inner = inner
|
|
654
|
+
|
|
655
|
+
def export(self, spans: Any) -> Any:
|
|
656
|
+
return self._inner.export([self._redact(s) for s in spans])
|
|
657
|
+
|
|
658
|
+
def shutdown(self) -> None:
|
|
659
|
+
self._inner.shutdown()
|
|
660
|
+
|
|
661
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
662
|
+
return bool(self._inner.force_flush(timeout_millis))
|
|
663
|
+
|
|
664
|
+
def _redact(self, span: Any) -> Any:
|
|
665
|
+
try:
|
|
666
|
+
from opentelemetry.sdk.trace import Event
|
|
667
|
+
from opentelemetry.trace import Status, StatusCode
|
|
668
|
+
|
|
669
|
+
status = getattr(span, "_status", None)
|
|
670
|
+
if status is not None and status.status_code is StatusCode.ERROR and status.description:
|
|
671
|
+
span._status = Status(StatusCode.ERROR)
|
|
672
|
+
events = getattr(span, "_events", None)
|
|
673
|
+
if events:
|
|
674
|
+
redacted = []
|
|
675
|
+
for ev in events:
|
|
676
|
+
attrs = {k: v for k, v in (ev.attributes or {}).items() if k not in self._DROP}
|
|
677
|
+
redacted.append(Event(name=ev.name, attributes=attrs, timestamp=ev.timestamp))
|
|
678
|
+
span._events = redacted
|
|
679
|
+
except Exception: # never fail export over redaction
|
|
680
|
+
logger.debug("span redaction skipped", exc_info=True)
|
|
681
|
+
return span
|