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,216 @@
|
|
|
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
|
+
"""A fake OIDC issuer for tests of agents calling agents: a JWKS and RFC 8693 token exchange.
|
|
16
|
+
|
|
17
|
+
It signs RS256 tokens with one key, serves the key set at `/jwks` and exchanges
|
|
18
|
+
tokens at `/token` (`grant_type=urn:ietf:params:oauth:grant-type:token-exchange`)
|
|
19
|
+
for the clients it knows (HTTP Basic client authentication, or the client id and
|
|
20
|
+
secret in the form: `client_secret_post`). An exchanged token
|
|
21
|
+
keeps the subject token's `sub`, names the calling client in `act` (the subject
|
|
22
|
+
token's own `act` nested inside it, RFC 8693 section 4.1), has the requested
|
|
23
|
+
`audience`, `azp` set to the client and `expires_in` of at most 300 s. It refuses
|
|
24
|
+
an audience the client may not act for (`may_act`), a service token (one whose
|
|
25
|
+
`sub` is its own client) and a token it did not issue. Every exchange request is
|
|
26
|
+
recorded (`requests`: the form, secrets included, and how the client
|
|
27
|
+
authenticated), and a test can make it misbehave: `hold` (a `threading.Event`
|
|
28
|
+
cleared: every `/token` request waits until it is set, a hung issuer), `answers`
|
|
29
|
+
(queued `(status, body)` answers returned instead of an exchange),
|
|
30
|
+
`expires_in` (the lifetime it grants; None leaves it out of the answer) and
|
|
31
|
+
`act` (False: exchanged tokens name no actor, only `azp`, as some issuers do).
|
|
32
|
+
Loopback only, on a free port; nothing leaves the process.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import base64
|
|
38
|
+
import json
|
|
39
|
+
import threading
|
|
40
|
+
import time
|
|
41
|
+
import uuid
|
|
42
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
43
|
+
from typing import Any
|
|
44
|
+
from urllib.parse import parse_qs, unquote_plus
|
|
45
|
+
|
|
46
|
+
import jwt
|
|
47
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
48
|
+
|
|
49
|
+
TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"
|
|
50
|
+
ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
|
51
|
+
EXCHANGED_TTL_S = 300
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FakeIssuer:
|
|
55
|
+
"""The issuer: `login` mints a person's token, `/token` exchanges it for an agent."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, clients: dict[str, tuple[str, set[str]]]) -> None:
|
|
58
|
+
"""`clients`: client id -> (secret, the audiences it may exchange tokens for)."""
|
|
59
|
+
self.key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
60
|
+
self.kid = uuid.uuid4().hex[:8]
|
|
61
|
+
self.clients = clients
|
|
62
|
+
self.exchanges: list[dict[str, Any]] = []
|
|
63
|
+
self.requests: list[dict[str, Any]] = []
|
|
64
|
+
self.hold = threading.Event()
|
|
65
|
+
self.hold.set()
|
|
66
|
+
self.answers: list[tuple[int, dict[str, Any]]] = []
|
|
67
|
+
self.expires_in: int | None = EXCHANGED_TTL_S
|
|
68
|
+
self.act = True
|
|
69
|
+
issuer = self
|
|
70
|
+
|
|
71
|
+
class Handler(BaseHTTPRequestHandler):
|
|
72
|
+
def do_GET(self) -> None:
|
|
73
|
+
if self.path != "/jwks":
|
|
74
|
+
self._answer(404, {"error": "not_found"})
|
|
75
|
+
return
|
|
76
|
+
self._answer(200, issuer.jwks())
|
|
77
|
+
|
|
78
|
+
def do_POST(self) -> None:
|
|
79
|
+
if self.path != "/token":
|
|
80
|
+
self._answer(404, {"error": "not_found"})
|
|
81
|
+
return
|
|
82
|
+
length = int(self.headers.get("content-length") or 0)
|
|
83
|
+
form = {k: v[0] for k, v in parse_qs(self.rfile.read(length).decode()).items()}
|
|
84
|
+
authorization = self.headers.get("authorization") or ""
|
|
85
|
+
issuer.requests.append({"form": form, "authorization": authorization})
|
|
86
|
+
issuer.hold.wait(timeout=120)
|
|
87
|
+
if issuer.answers:
|
|
88
|
+
status, body = issuer.answers.pop(0)
|
|
89
|
+
else:
|
|
90
|
+
status, body = issuer.exchange(authorization, form)
|
|
91
|
+
self._answer(status, body)
|
|
92
|
+
|
|
93
|
+
def _answer(self, status: int, body: dict[str, Any]) -> None:
|
|
94
|
+
data = json.dumps(body).encode()
|
|
95
|
+
self.send_response(status)
|
|
96
|
+
self.send_header("Content-Type", "application/json")
|
|
97
|
+
self.send_header("Content-Length", str(len(data)))
|
|
98
|
+
self.end_headers()
|
|
99
|
+
self.wfile.write(data)
|
|
100
|
+
|
|
101
|
+
def log_message(self, *args: Any) -> None:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
105
|
+
self.url = f"http://127.0.0.1:{self.httpd.server_address[1]}"
|
|
106
|
+
self.issuer = f"{self.url}/"
|
|
107
|
+
self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
|
|
108
|
+
self.thread.start()
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def jwks_url(self) -> str:
|
|
112
|
+
return f"{self.url}/jwks"
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def token_url(self) -> str:
|
|
116
|
+
return f"{self.url}/token"
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
self.hold.set()
|
|
120
|
+
self.httpd.shutdown()
|
|
121
|
+
self.httpd.server_close()
|
|
122
|
+
|
|
123
|
+
def jwks(self) -> dict[str, Any]:
|
|
124
|
+
public = jwt.algorithms.RSAAlgorithm.to_jwk(self.key.public_key(), as_dict=True)
|
|
125
|
+
return {"keys": [{**public, "kid": self.kid, "use": "sig", "alg": "RS256"}]}
|
|
126
|
+
|
|
127
|
+
def sign(self, claims: dict[str, Any]) -> str:
|
|
128
|
+
return jwt.encode(claims, self.key, algorithm="RS256", headers={"kid": self.kid})
|
|
129
|
+
|
|
130
|
+
def login(
|
|
131
|
+
self,
|
|
132
|
+
sub: str,
|
|
133
|
+
*,
|
|
134
|
+
audience: str,
|
|
135
|
+
client: str = "web",
|
|
136
|
+
roles: tuple[str, ...] = (),
|
|
137
|
+
ttl_s: int = 3600,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""A person's own token for `audience`, as a sign-in client obtains it."""
|
|
140
|
+
now = int(time.time())
|
|
141
|
+
return self.sign(
|
|
142
|
+
{
|
|
143
|
+
"iss": self.issuer,
|
|
144
|
+
"sub": sub,
|
|
145
|
+
"aud": audience,
|
|
146
|
+
"azp": client,
|
|
147
|
+
"roles": list(roles),
|
|
148
|
+
"iat": now,
|
|
149
|
+
"exp": now + ttl_s,
|
|
150
|
+
"jti": uuid.uuid4().hex,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def exchange(self, authorization: str, form: dict[str, str]) -> tuple[int, dict[str, Any]]:
|
|
155
|
+
"""RFC 8693 token exchange: `(status, body)`."""
|
|
156
|
+
scheme, _, credentials = authorization.partition(" ")
|
|
157
|
+
if scheme.lower() == "basic":
|
|
158
|
+
try:
|
|
159
|
+
pair = base64.b64decode(credentials).decode()
|
|
160
|
+
except Exception:
|
|
161
|
+
pair = ""
|
|
162
|
+
# RFC 6749 section 2.3.1: each part form-encoded, then joined and base64-encoded.
|
|
163
|
+
client, _, secret = (unquote_plus(part) for part in pair.partition(":"))
|
|
164
|
+
elif not authorization and "client_id" in form:
|
|
165
|
+
client, secret = form.get("client_id") or "", form.get("client_secret") or ""
|
|
166
|
+
else:
|
|
167
|
+
client, secret = "", ""
|
|
168
|
+
known = self.clients.get(client)
|
|
169
|
+
if known is None or known[0] != secret:
|
|
170
|
+
return 401, {"error": "invalid_client"}
|
|
171
|
+
if form.get("grant_type") != TOKEN_EXCHANGE:
|
|
172
|
+
return 400, {"error": "unsupported_grant_type"}
|
|
173
|
+
audience = form.get("audience") or ""
|
|
174
|
+
if audience not in known[1]:
|
|
175
|
+
return 400, {"error": "invalid_target"}
|
|
176
|
+
try:
|
|
177
|
+
subject = jwt.decode(
|
|
178
|
+
form.get("subject_token") or "",
|
|
179
|
+
self.key.public_key(),
|
|
180
|
+
algorithms=["RS256"],
|
|
181
|
+
issuer=self.issuer,
|
|
182
|
+
options={"verify_aud": False},
|
|
183
|
+
)
|
|
184
|
+
except jwt.PyJWTError:
|
|
185
|
+
return 400, {"error": "invalid_grant"}
|
|
186
|
+
if subject.get("sub") == subject.get("azp"):
|
|
187
|
+
return 400, {"error": "invalid_grant"} # no exchange of a service's own token
|
|
188
|
+
actor: dict[str, Any] = {"sub": client}
|
|
189
|
+
if isinstance(subject.get("act"), dict):
|
|
190
|
+
actor["act"] = subject["act"]
|
|
191
|
+
now = int(time.time())
|
|
192
|
+
exchanged = {
|
|
193
|
+
"iss": self.issuer,
|
|
194
|
+
"sub": subject["sub"],
|
|
195
|
+
"aud": audience,
|
|
196
|
+
"azp": client,
|
|
197
|
+
"act": actor,
|
|
198
|
+
"roles": subject.get("roles") or [],
|
|
199
|
+
"iat": now,
|
|
200
|
+
"exp": min(now + (self.expires_in or 60), int(subject.get("exp") or now)),
|
|
201
|
+
"jti": uuid.uuid4().hex,
|
|
202
|
+
}
|
|
203
|
+
if not self.act:
|
|
204
|
+
del exchanged["act"]
|
|
205
|
+
for claim in ("scope", "resource"):
|
|
206
|
+
if form.get(claim):
|
|
207
|
+
exchanged[claim] = form[claim]
|
|
208
|
+
self.exchanges.append({"client": client, "audience": audience, "sub": subject["sub"]})
|
|
209
|
+
answer: dict[str, Any] = {
|
|
210
|
+
"access_token": self.sign(exchanged),
|
|
211
|
+
"issued_token_type": ACCESS_TOKEN_TYPE,
|
|
212
|
+
"token_type": "Bearer",
|
|
213
|
+
}
|
|
214
|
+
if self.expires_in is not None:
|
|
215
|
+
answer["expires_in"] = exchanged["exp"] - now
|
|
216
|
+
return 200, answer
|
|
@@ -0,0 +1,357 @@
|
|
|
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
|
+
"""A fake OpenAI API over HTTP for tests of the model settings: Chat Completions and Responses.
|
|
16
|
+
|
|
17
|
+
It serves `POST /v1/chat/completions` and `POST /v1/responses`, streamed
|
|
18
|
+
(server-sent events, as OpenAI sends them) or not, and answers from the
|
|
19
|
+
conversation: after a tool result, `Found: <the result>`; a request whose last
|
|
20
|
+
user text names one of the request's function tools (a word of its name), a
|
|
21
|
+
call of that tool with every required string argument set to the text after the
|
|
22
|
+
last "in "/"for "; anything else, `ok`. It refuses what OpenAI refuses (400,
|
|
23
|
+
OpenAI's error body), so a test notices a request sent to the wrong API:
|
|
24
|
+
`stream_options` or a top-level `reasoning_effort` on the Responses API, and
|
|
25
|
+
`reasoning` on Chat Completions. With `effort_refuses_tools` (a model like
|
|
26
|
+
the one Track C met: F15) Chat Completions also refuses function tools unless
|
|
27
|
+
`reasoning_effort` is `none`, naming `/v1/responses`. Streamed Chat Completions
|
|
28
|
+
carry usage only when `stream_options.include_usage` asks for it; Responses
|
|
29
|
+
always carry it (`response.completed`). Every request is recorded (`requests`:
|
|
30
|
+
the path from `/v1/` on, and the JSON body). Any prefix before `/v1` is
|
|
31
|
+
accepted: `base_url()` gives each test one of its own, since langchain-openai
|
|
32
|
+
keeps one HTTP client per base URL, bound to the event loop that made it.
|
|
33
|
+
Loopback only, on a free port; nothing leaves the process.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
import threading
|
|
41
|
+
import time
|
|
42
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
CHAT_USAGE = {"prompt_tokens": 11, "completion_tokens": 5, "total_tokens": 16}
|
|
46
|
+
RESPONSES_USAGE = {
|
|
47
|
+
"input_tokens": 13,
|
|
48
|
+
"input_tokens_details": {"cached_tokens": 0},
|
|
49
|
+
"output_tokens": 7,
|
|
50
|
+
"output_tokens_details": {"reasoning_tokens": 2},
|
|
51
|
+
"total_tokens": 20,
|
|
52
|
+
}
|
|
53
|
+
F15 = (
|
|
54
|
+
"Function tools with reasoning_effort are not supported for {model} in "
|
|
55
|
+
"/v1/chat/completions. Please use /v1/responses instead, or set reasoning_effort to 'none'."
|
|
56
|
+
)
|
|
57
|
+
_SUBJECT = re.compile(r"\b(?:in|for)\s+([^?.!]+)", re.IGNORECASE)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _error(message: str, param: str | None = None, code: str | None = None) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"error": {"message": message, "type": "invalid_request_error", "param": param, "code": code}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _call_for(text: str, tools: list[dict[str, Any]]) -> tuple[str, str] | None:
|
|
67
|
+
"""(tool name, JSON arguments) when `text` names a tool (a word of its name), else None."""
|
|
68
|
+
words = set(re.findall(r"[a-z0-9]+", text.lower()))
|
|
69
|
+
for tool in tools:
|
|
70
|
+
name = str(tool["name"])
|
|
71
|
+
if not {w for w in name.lower().split("_") if len(w) > 2} & words:
|
|
72
|
+
continue
|
|
73
|
+
matches = list(_SUBJECT.finditer(text))
|
|
74
|
+
subject = matches[-1].group(1).strip() if matches else text
|
|
75
|
+
schema = tool.get("parameters") or {}
|
|
76
|
+
properties = schema.get("properties") or {}
|
|
77
|
+
args = {
|
|
78
|
+
key: subject
|
|
79
|
+
for key in schema.get("required") or []
|
|
80
|
+
if (properties.get(key) or {}).get("type", "string") == "string"
|
|
81
|
+
}
|
|
82
|
+
return name, json.dumps(args)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _text_of(content: Any) -> str:
|
|
87
|
+
if isinstance(content, str):
|
|
88
|
+
return content
|
|
89
|
+
if isinstance(content, list):
|
|
90
|
+
return "".join(str(block.get("text", "")) for block in content if isinstance(block, dict))
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FakeOpenAI:
|
|
95
|
+
"""The server: `base_url` is what OPENAI_BASE_URL names; `requests` what it received."""
|
|
96
|
+
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
self.requests: list[dict[str, Any]] = []
|
|
99
|
+
self.effort_refuses_tools = False
|
|
100
|
+
self._ids = 0
|
|
101
|
+
server = self
|
|
102
|
+
|
|
103
|
+
class Handler(BaseHTTPRequestHandler):
|
|
104
|
+
protocol_version = "HTTP/1.1"
|
|
105
|
+
|
|
106
|
+
def do_POST(self) -> None:
|
|
107
|
+
length = int(self.headers.get("content-length") or 0)
|
|
108
|
+
body = json.loads(self.rfile.read(length) or b"{}")
|
|
109
|
+
path = self.path[self.path.find("/v1/") :]
|
|
110
|
+
server.requests.append({"path": path, "body": body})
|
|
111
|
+
if self.path.endswith("/chat/completions"):
|
|
112
|
+
status, payload, events = server.chat(body)
|
|
113
|
+
elif self.path.endswith("/responses"):
|
|
114
|
+
status, payload, events = server.responses(body)
|
|
115
|
+
else:
|
|
116
|
+
status, payload, events = 404, _error(f"no route {self.path}"), None
|
|
117
|
+
if events is None:
|
|
118
|
+
data = json.dumps(payload).encode()
|
|
119
|
+
self.send_response(status)
|
|
120
|
+
self.send_header("content-type", "application/json")
|
|
121
|
+
else:
|
|
122
|
+
data = "".join(f"{event}\n\n" for event in events).encode()
|
|
123
|
+
self.send_response(200)
|
|
124
|
+
self.send_header("content-type", "text/event-stream")
|
|
125
|
+
self.send_header("content-length", str(len(data)))
|
|
126
|
+
self.end_headers()
|
|
127
|
+
self.wfile.write(data)
|
|
128
|
+
|
|
129
|
+
def log_message(self, *args: Any) -> None:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
133
|
+
self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
|
|
134
|
+
self.thread.start()
|
|
135
|
+
|
|
136
|
+
def base_url(self) -> str:
|
|
137
|
+
"""A base URL of this server no earlier call returned (OPENAI_BASE_URL)."""
|
|
138
|
+
self._ids += 1
|
|
139
|
+
return f"http://127.0.0.1:{self.httpd.server_address[1]}/c{self._ids}/v1"
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
self.httpd.shutdown()
|
|
143
|
+
self.httpd.server_close()
|
|
144
|
+
|
|
145
|
+
def paths(self) -> list[str]:
|
|
146
|
+
return [r["path"] for r in self.requests]
|
|
147
|
+
|
|
148
|
+
def _id(self, prefix: str) -> str:
|
|
149
|
+
self._ids += 1
|
|
150
|
+
return f"{prefix}_{self._ids}"
|
|
151
|
+
|
|
152
|
+
# --- Chat Completions ----------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def chat(self, body: dict[str, Any]) -> tuple[int, dict[str, Any], list[str] | None]:
|
|
155
|
+
if "reasoning" in body:
|
|
156
|
+
return 400, _error("Unknown parameter: 'reasoning'.", "reasoning", "unknown"), None
|
|
157
|
+
tools = [t["function"] for t in body.get("tools") or [] if t.get("type") == "function"]
|
|
158
|
+
effort = body.get("reasoning_effort")
|
|
159
|
+
if self.effort_refuses_tools and tools and effort != "none":
|
|
160
|
+
return 400, _error(F15.format(model=body.get("model")), "reasoning_effort"), None
|
|
161
|
+
messages = body.get("messages") or []
|
|
162
|
+
last = messages[-1] if messages else {}
|
|
163
|
+
text, call = "ok", None
|
|
164
|
+
if last.get("role") == "tool":
|
|
165
|
+
text = f"Found: {_text_of(last.get('content'))}"
|
|
166
|
+
else:
|
|
167
|
+
asked = next(
|
|
168
|
+
(_text_of(m.get("content")) for m in reversed(messages) if m["role"] == "user"), ""
|
|
169
|
+
)
|
|
170
|
+
call = _call_for(asked, tools)
|
|
171
|
+
head = {"id": self._id("chatcmpl"), "created": int(time.time()), "model": body["model"]}
|
|
172
|
+
call_id = self._id("call")
|
|
173
|
+
if not body.get("stream"):
|
|
174
|
+
message: dict[str, Any] = {"role": "assistant", "content": None if call else text}
|
|
175
|
+
if call:
|
|
176
|
+
message["tool_calls"] = [
|
|
177
|
+
{
|
|
178
|
+
"id": call_id,
|
|
179
|
+
"type": "function",
|
|
180
|
+
"function": {"name": call[0], "arguments": call[1]},
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
choice = {
|
|
184
|
+
"index": 0,
|
|
185
|
+
"message": message,
|
|
186
|
+
"finish_reason": "tool_calls" if call else "stop",
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
200,
|
|
190
|
+
{**head, "object": "chat.completion", "choices": [choice], "usage": CHAT_USAGE},
|
|
191
|
+
None,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def chunk(delta: dict[str, Any], finish: str | None = None) -> str:
|
|
195
|
+
choice = {"index": 0, "delta": delta, "finish_reason": finish}
|
|
196
|
+
return "data: " + json.dumps(
|
|
197
|
+
{**head, "object": "chat.completion.chunk", "choices": [choice]}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if call:
|
|
201
|
+
calls = [
|
|
202
|
+
{
|
|
203
|
+
"index": 0,
|
|
204
|
+
"id": call_id,
|
|
205
|
+
"type": "function",
|
|
206
|
+
"function": {"name": call[0], "arguments": call[1]},
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
events = [chunk({"role": "assistant", "content": None, "tool_calls": calls})]
|
|
210
|
+
events.append(chunk({}, "tool_calls"))
|
|
211
|
+
else:
|
|
212
|
+
events = [chunk({"role": "assistant", "content": ""})]
|
|
213
|
+
events += [chunk({"content": text[i : i + 8]}) for i in range(0, len(text), 8)]
|
|
214
|
+
events.append(chunk({}, "stop"))
|
|
215
|
+
if (body.get("stream_options") or {}).get("include_usage"):
|
|
216
|
+
usage = {**head, "object": "chat.completion.chunk", "choices": [], "usage": CHAT_USAGE}
|
|
217
|
+
events.append("data: " + json.dumps(usage))
|
|
218
|
+
events.append("data: [DONE]")
|
|
219
|
+
return 200, {}, events
|
|
220
|
+
|
|
221
|
+
# --- Responses -----------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
def responses(self, body: dict[str, Any]) -> tuple[int, dict[str, Any], list[str] | None]:
|
|
224
|
+
for unknown in ("stream_options", "reasoning_effort", "messages"):
|
|
225
|
+
if unknown in body:
|
|
226
|
+
return 400, _error(f"Unknown parameter: '{unknown}'.", unknown, "unknown"), None
|
|
227
|
+
tools = [t for t in body.get("tools") or [] if t.get("type") == "function"]
|
|
228
|
+
items = body.get("input") or []
|
|
229
|
+
if isinstance(items, str):
|
|
230
|
+
items = [{"role": "user", "content": items}]
|
|
231
|
+
last = items[-1] if items else {}
|
|
232
|
+
text, call = "ok", None
|
|
233
|
+
if last.get("type") == "function_call_output":
|
|
234
|
+
text = f"Found: {last.get('output')}"
|
|
235
|
+
else:
|
|
236
|
+
asked = next(
|
|
237
|
+
(
|
|
238
|
+
_text_of(i.get("content"))
|
|
239
|
+
for i in reversed(items)
|
|
240
|
+
if i.get("role") == "user" and i.get("type", "message") == "message"
|
|
241
|
+
),
|
|
242
|
+
"",
|
|
243
|
+
)
|
|
244
|
+
call = _call_for(asked, tools)
|
|
245
|
+
response_id = self._id("resp")
|
|
246
|
+
if call:
|
|
247
|
+
item: dict[str, Any] = {
|
|
248
|
+
"id": self._id("fc"),
|
|
249
|
+
"type": "function_call",
|
|
250
|
+
"call_id": self._id("call"),
|
|
251
|
+
"name": call[0],
|
|
252
|
+
"arguments": call[1],
|
|
253
|
+
"status": "completed",
|
|
254
|
+
}
|
|
255
|
+
else:
|
|
256
|
+
item = {
|
|
257
|
+
"id": self._id("msg"),
|
|
258
|
+
"type": "message",
|
|
259
|
+
"role": "assistant",
|
|
260
|
+
"status": "completed",
|
|
261
|
+
"content": [{"type": "output_text", "text": text, "annotations": []}],
|
|
262
|
+
}
|
|
263
|
+
response = {
|
|
264
|
+
"id": response_id,
|
|
265
|
+
"object": "response",
|
|
266
|
+
"created_at": int(time.time()),
|
|
267
|
+
"model": body["model"],
|
|
268
|
+
"status": "completed",
|
|
269
|
+
"error": None,
|
|
270
|
+
"incomplete_details": None,
|
|
271
|
+
"instructions": None,
|
|
272
|
+
"metadata": {},
|
|
273
|
+
"output": [item],
|
|
274
|
+
"parallel_tool_calls": True,
|
|
275
|
+
"temperature": None,
|
|
276
|
+
"tool_choice": "auto",
|
|
277
|
+
"tools": body.get("tools") or [],
|
|
278
|
+
"top_p": None,
|
|
279
|
+
"reasoning": body.get("reasoning"),
|
|
280
|
+
"usage": RESPONSES_USAGE,
|
|
281
|
+
}
|
|
282
|
+
if not body.get("stream"):
|
|
283
|
+
return 200, response, None
|
|
284
|
+
sequence = iter(range(1000))
|
|
285
|
+
|
|
286
|
+
def event(kind: str, **fields: Any) -> str:
|
|
287
|
+
data = {"type": kind, "sequence_number": next(sequence), **fields}
|
|
288
|
+
return f"event: {kind}\ndata: {json.dumps(data)}"
|
|
289
|
+
|
|
290
|
+
started = {**response, "status": "in_progress", "output": [], "usage": None}
|
|
291
|
+
events = [
|
|
292
|
+
event("response.created", response=started),
|
|
293
|
+
event("response.in_progress", response=started),
|
|
294
|
+
]
|
|
295
|
+
if call:
|
|
296
|
+
added = {**item, "arguments": "", "status": "in_progress"}
|
|
297
|
+
events += [
|
|
298
|
+
event("response.output_item.added", output_index=0, item=added),
|
|
299
|
+
event(
|
|
300
|
+
"response.function_call_arguments.delta",
|
|
301
|
+
item_id=item["id"],
|
|
302
|
+
output_index=0,
|
|
303
|
+
delta=item["arguments"],
|
|
304
|
+
),
|
|
305
|
+
event(
|
|
306
|
+
"response.function_call_arguments.done",
|
|
307
|
+
item_id=item["id"],
|
|
308
|
+
output_index=0,
|
|
309
|
+
arguments=item["arguments"],
|
|
310
|
+
),
|
|
311
|
+
]
|
|
312
|
+
else:
|
|
313
|
+
added = {**item, "status": "in_progress", "content": []}
|
|
314
|
+
part = {"type": "output_text", "text": "", "annotations": []}
|
|
315
|
+
events += [
|
|
316
|
+
event("response.output_item.added", output_index=0, item=added),
|
|
317
|
+
event(
|
|
318
|
+
"response.content_part.added",
|
|
319
|
+
item_id=item["id"],
|
|
320
|
+
output_index=0,
|
|
321
|
+
content_index=0,
|
|
322
|
+
part=part,
|
|
323
|
+
),
|
|
324
|
+
]
|
|
325
|
+
events += [
|
|
326
|
+
event(
|
|
327
|
+
"response.output_text.delta",
|
|
328
|
+
item_id=item["id"],
|
|
329
|
+
output_index=0,
|
|
330
|
+
content_index=0,
|
|
331
|
+
delta=text[i : i + 8],
|
|
332
|
+
logprobs=[],
|
|
333
|
+
)
|
|
334
|
+
for i in range(0, len(text), 8)
|
|
335
|
+
]
|
|
336
|
+
events += [
|
|
337
|
+
event(
|
|
338
|
+
"response.output_text.done",
|
|
339
|
+
item_id=item["id"],
|
|
340
|
+
output_index=0,
|
|
341
|
+
content_index=0,
|
|
342
|
+
text=text,
|
|
343
|
+
logprobs=[],
|
|
344
|
+
),
|
|
345
|
+
event(
|
|
346
|
+
"response.content_part.done",
|
|
347
|
+
item_id=item["id"],
|
|
348
|
+
output_index=0,
|
|
349
|
+
content_index=0,
|
|
350
|
+
part={**part, "text": text},
|
|
351
|
+
),
|
|
352
|
+
]
|
|
353
|
+
events += [
|
|
354
|
+
event("response.output_item.done", output_index=0, item=item),
|
|
355
|
+
event("response.completed", response=response),
|
|
356
|
+
]
|
|
357
|
+
return 200, {}, events
|