From 2c9b2c9bdafb743ea6b672462f96ac2b64d64db2 Mon Sep 17 00:00:00 2001 From: Dawson Date: Tue, 18 Aug 2026 11:52:15 +0800 Subject: [PATCH 1/4] feat(labs): add AgentStream code --- .pre-commit-config.yaml | 3 + CHANGELOG.md | 2 + README.md | 2 + labs/AgentStream/LICENSE | 201 + labs/AgentStream/README.md | 95 + labs/AgentStream/exgentic/.dockerignore | 19 + labs/AgentStream/exgentic/.gitattributes | 1 + .../exgentic/.github/workflows/pre-commit.yml | 46 + .../.github/workflows/publish-pypi.yml | 59 + .../exgentic/.github/workflows/tests.yml | 64 + labs/AgentStream/exgentic/.gitignore | 121 + .../exgentic/.pre-commit-config.yaml | 66 + labs/AgentStream/exgentic/.whitesource | 9 + labs/AgentStream/exgentic/CODE_OF_CONDUCT.md | 128 + labs/AgentStream/exgentic/CONTRIBUTING.md | 78 + labs/AgentStream/exgentic/DCO.txt | 34 + labs/AgentStream/exgentic/DEVELOPMENT.md | 129 + labs/AgentStream/exgentic/LICENSE | 201 + labs/AgentStream/exgentic/README.md | 249 + labs/AgentStream/exgentic/SECURITY.md | 152 + labs/AgentStream/exgentic/docs/README.md | 46 + .../exgentic/docs/adding-agents.md | 384 + .../exgentic/docs/adding-benchmarks.md | 364 + labs/AgentStream/exgentic/docs/batch.md | 269 + .../exgentic/docs/cli-reference.md | 323 + .../exgentic/docs/custom-models.md | 250 + labs/AgentStream/exgentic/docs/huggingface.md | 35 + .../exgentic/docs/observability/quickstart.md | 151 + .../observability/semantic-conventions.md | 172 + labs/AgentStream/exgentic/docs/observers.md | 221 + .../exgentic/docs/output-format.md | 325 + labs/AgentStream/exgentic/docs/python-api.md | 361 + labs/AgentStream/exgentic/docs/releasing.md | 119 + .../exgentic/docs/replay-testing.md | 229 + labs/AgentStream/exgentic/docs/runners.md | 208 + .../exgentic/examples/run_appworld.py | 24 + .../exgentic/examples/run_browsecomp.py | 24 + .../examples/run_claude_code_on_gsm8k.py | 24 + .../examples/run_claude_code_on_tau2bench.py | 24 + .../exgentic/examples/run_cli_agents.py | 24 + .../exgentic/examples/run_gsm8k.py | 23 + .../exgentic/examples/run_hotpotqa.py | 24 + .../exgentic/examples/run_openai_mcp.py | 33 + .../AgentStream/exgentic/examples/run_smol.py | 24 + .../exgentic/examples/run_swebench.py | 24 + .../exgentic/examples/run_taubench.py | 24 + .../examples/simple_test_agent/adapter.py | 54 + .../examples/simple_test_agent/setup.sh | 3 + labs/AgentStream/exgentic/misc/assets/cli.png | Bin 0 -> 1046661 bytes .../misc/assets/exgentic_banner_black.png | Bin 0 -> 38420 bytes .../exgentic_banner_black_no_background.png | Bin 0 -> 31271 bytes .../misc/assets/exgentic_banner_white.png | Bin 0 -> 38083 bytes .../exgentic_banner_white_no_background.png | Bin 0 -> 33838 bytes .../exgentic/misc/assets/exgentic_light.png | Bin 0 -> 53953 bytes labs/AgentStream/exgentic/misc/assets/gui.png | Bin 0 -> 549586 bytes .../AgentStream/exgentic/misc/assets/icon.png | Bin 0 -> 6373 bytes .../exgentic/misc/assets/icon_black.png | Bin 0 -> 6897 bytes .../misc/security/requirements/README.md | 39 + .../requirements/appworld/requirements.txt | 195 + .../browsecompplus/requirements.txt | 261 + .../requirements/core/requirements.txt | 160 + .../requirements/gsm8k/requirements.txt | 165 + .../requirements/hotpotqa/requirements.txt | 190 + .../requirements/swebench/requirements.txt | 194 + .../requirements/tau2/requirements.txt | 182 + .../misc/security/setup_environments.sh | 303 + .../exgentic/misc/skills/add-agent/SKILL.md | 68 + .../misc/skills/add-benchmark/SKILL.md | 62 + .../exgentic/misc/utils/.secrets.baseline | 150 + .../misc/utils/enforce_dependency_caps.py | 124 + .../misc/utils/enforce_library_imports.py | 32 + .../misc/utils/enforce_relative_imports.py | 41 + .../misc/utils/enforce_spdx_header.py | 70 + labs/AgentStream/exgentic/pyproject.toml | 111 + labs/AgentStream/exgentic/renovate.json | 13 + labs/AgentStream/exgentic/ruff.toml | 78 + .../exgentic/scripts/a_mem/run_experiment.py | 416 + .../exgentic/scripts/a_mem/run_experiment.sh | 72 + .../exgentic/scripts/ace/run_experiment.py | 386 + .../exgentic/scripts/ace/run_experiment.sh | 60 + .../scripts/autoskill/run_experiment.py | 382 + .../scripts/autoskill/run_experiment.sh | 61 + .../scripts/harness/run_experiment.py | 386 + .../scripts/harness/run_experiment.sh | 57 + .../exgentic/scripts/litellm/run_baseline.py | 238 + .../exgentic/scripts/litellm/run_baseline.sh | 84 + .../scripts/reasoning_bank/run_experiment.py | 403 + .../scripts/reasoning_bank/run_experiment.sh | 70 + labs/AgentStream/exgentic/scripts/release.sh | 76 + .../exgentic/scripts/utils/task_ordering.py | 210 + .../exgentic/src/exgentic/__init__.py | 86 + .../src/exgentic/adapters/__init__.py | 2 + .../src/exgentic/adapters/actions/__init__.py | 4 + .../src/exgentic/adapters/actions/chat.py | 105 + .../exgentic/adapters/actions/functions.py | 94 + .../src/exgentic/adapters/agents/__init__.py | 4 + .../exgentic/adapters/agents/code_agent.py | 52 + .../exgentic/adapters/agents/coordinator.py | 321 + .../src/exgentic/adapters/agents/mcp_agent.py | 104 + .../exgentic/adapters/agents/mcp_server.py | 351 + .../exgentic/adapters/executors/__init__.py | 4 + .../src/exgentic/adapters/executors/proxy.py | 199 + .../src/exgentic/adapters/runners/__init__.py | 125 + .../src/exgentic/adapters/runners/_utils.py | 138 + .../src/exgentic/adapters/runners/direct.py | 35 + .../src/exgentic/adapters/runners/docker.py | 227 + .../src/exgentic/adapters/runners/process.py | 174 + .../src/exgentic/adapters/runners/service.py | 237 + .../src/exgentic/adapters/runners/thread.py | 92 + .../exgentic/adapters/runners/transport.py | 188 + .../src/exgentic/adapters/runners/venv.py | 215 + .../src/exgentic/adapters/schemas/__init__.py | 4 + .../exgentic/adapters/schemas/json_schema.py | 112 + .../src/exgentic/adapters/schemas/openai.py | 67 + .../exgentic/src/exgentic/agents/__init__.py | 4 + .../src/exgentic/agents/a_mem/__init__.py | 6 + .../src/exgentic/agents/a_mem/a_mem_agent.py | 71 + .../exgentic/agents/a_mem/a_mem_instance.py | 726 + .../src/exgentic/agents/a_mem/memory_note.py | 142 + .../src/exgentic/agents/a_mem/memory_store.py | 417 + .../src/exgentic/agents/a_mem/prompts.py | 405 + .../src/exgentic/agents/a_mem/retriever.py | 54 + .../src/exgentic/agents/ace/__init__.py | 7 + .../src/exgentic/agents/ace/ace_agent.py | 88 + .../src/exgentic/agents/ace/ace_instance.py | 775 + .../agents/ace/bulletpoint_analyzer.py | 200 + .../src/exgentic/agents/ace/playbook_store.py | 172 + .../src/exgentic/agents/ace/playbook_utils.py | 230 + .../exgentic/agents/ace/prompts/__init__.py | 10 + .../exgentic/agents/ace/prompts/curator.py | 67 + .../exgentic/agents/ace/prompts/reflector.py | 53 + .../src/exgentic/agents/autoskill/__init__.py | 7 + .../agents/autoskill/autoskill_agent.py | 81 + .../agents/autoskill/autoskill_instance.py | 636 + .../src/exgentic/agents/autoskill/prompts.py | 153 + .../agents/autoskill/skill_extraction.py | 141 + .../agents/autoskill/skill_maintenance.py | 207 + .../agents/autoskill/skill_retrieval.py | 159 + .../exgentic/agents/autoskill/skill_store.py | 208 + .../src/exgentic/agents/cli/__init__.py | 9 + .../exgentic/src/exgentic/agents/cli/base.py | 350 + .../exgentic/agents/cli/claude/__init__.py | 4 + .../src/exgentic/agents/cli/claude/agent.py | 93 + .../src/exgentic/agents/cli/claude/cli.py | 148 + .../src/exgentic/agents/cli/claude/setup.sh | 32 + .../src/exgentic/agents/cli/codex/__init__.py | 4 + .../src/exgentic/agents/cli/codex/agent.py | 71 + .../src/exgentic/agents/cli/codex/cli.py | 70 + .../src/exgentic/agents/cli/codex/setup.sh | 32 + .../src/exgentic/agents/cli/command_runner.py | 471 + .../exgentic/agents/cli/gemini/__init__.py | 4 + .../src/exgentic/agents/cli/gemini/agent.py | 81 + .../src/exgentic/agents/cli/gemini/cli.py | 112 + .../src/exgentic/agents/cli/gemini/setup.sh | 32 + .../src/exgentic/agents/cli/requirements.txt | 1 + .../src/exgentic/agents/harness/__init__.py | 6 + .../src/exgentic/agents/harness/evolver.py | 382 + .../exgentic/agents/harness/harness_agent.py | 70 + .../agents/harness/harness_instance.py | 579 + .../exgentic/agents/harness/harness_store.py | 320 + .../agents/harness/prompts/__init__.py | 6 + .../agents/harness/prompts/evolver.py | 52 + .../exgentic/agents/harness/prompts/inject.py | 30 + .../src/exgentic/agents/harness/retriever.py | 93 + .../agents/litellm_tool_calling/__init__.py | 2 + .../agents/litellm_tool_calling/instance.py | 486 + .../litellm_tool_calling_agent.py | 54 + .../agents/litellm_tool_calling/utils.py | 135 + .../src/exgentic/agents/openai/__init__.py | 2 + .../src/exgentic/agents/openai/instance.py | 307 + .../agents/openai/openai_mcp_agent.py | 68 + .../exgentic/agents/openai/requirements.txt | 1 + .../agents/reasoning_bank/__init__.py | 0 .../agents/reasoning_bank/evaluator.py | 72 + .../agents/reasoning_bank/induce_memory.py | 70 + .../reasoning_bank/memory_management.py | 112 + .../agents/reasoning_bank/prompts/__init__.py | 0 .../reasoning_bank/prompts/eval_prompts.py | 55 + .../prompts/memory_instruction.py | 70 + .../agents/reasoning_bank/rb_agent.py | 77 + .../agents/reasoning_bank/rb_instance.py | 552 + .../agents/reasoning_bank/rb_store.py | 164 + .../src/exgentic/agents/replay/__init__.py | 2 + .../exgentic/agents/replay/replay_agent.py | 107 + .../agents/replay/replay_benchmark.py | 92 + .../exgentic/agents/replay/replay_session.py | 139 + .../exgentic/agents/smolagents/__init__.py | 12 + .../exgentic/agents/smolagents/base_agent.py | 36 + .../agents/smolagents/base_instance.py | 152 + .../exgentic/agents/smolagents/code_agent.py | 21 + .../agents/smolagents/code_instance.py | 58 + .../agents/smolagents/requirements.txt | 1 + .../smolagents/structured_code_agent.yaml | 257 + .../agents/smolagents/tool_calling_agent.py | 21 + .../smolagents/tool_calling_instance.py | 37 + .../src/exgentic/agents/tool_shortlisting.py | 101 + .../src/exgentic/benchmarks/__init__.py | 2 + .../exgentic/benchmarks/appworld/__init__.py | 8 + .../benchmarks/appworld/appworld_benchmark.py | 57 + .../benchmarks/appworld/appworld_eval.py | 673 + .../benchmarks/appworld/requirements.txt | 1 + .../src/exgentic/benchmarks/appworld/setup.sh | 25 + .../src/exgentic/benchmarks/bfcl/__init__.py | 8 + .../benchmarks/bfcl/bfcl_benchmark.py | 93 + .../src/exgentic/benchmarks/bfcl/bfcl_eval.py | 576 + .../src/exgentic/benchmarks/bfcl/bfcl_shim.py | 68 + .../src/exgentic/benchmarks/bfcl/setup.sh | 30 + .../benchmarks/browsecompplus/__init__.py | 2 + .../browsecompplus/browsecomp_benchmark.py | 697 + .../browsecompplus/browsecomp_eval.py | 156 + .../browsecompplus/make_light_dataset.py | 25 + .../browsecompplus/requirements.txt | 2 + .../benchmarks/browsecompplus/retriever.py | 133 + .../browsecompplus/search_service.py | 173 + .../browsecompplus/search_tool_handler.py | 81 + .../browsecompplus/searcher_cache.py | 69 + .../benchmarks/browsecompplus/setup.sh | 93 + .../src/exgentic/benchmarks/gsm8k/__init__.py | 2 + .../benchmarks/gsm8k/gsm8k_benchmark.py | 351 + .../benchmarks/gsm8k/requirements.txt | 1 + .../src/exgentic/benchmarks/hle/__init__.py | 2 + .../exgentic/benchmarks/hle/hle_benchmark.py | 390 + .../exgentic/benchmarks/hle/requirements.txt | 3 + .../exgentic/benchmarks/hotpotqa/__init__.py | 2 + .../benchmarks/hotpotqa/hotpotqa_benchmark.py | 373 + .../benchmarks/hotpotqa/requirements.txt | 3 + .../exgentic/benchmarks/swebench/__init__.py | 2 + .../exgentic/benchmarks/swebench/config.yaml | 106 + .../exgentic/benchmarks/swebench/readme.md | 43 + .../benchmarks/swebench/requirements.txt | 2 + .../benchmarks/swebench/swebench_benchmark.py | 119 + .../benchmarks/swebench/swebench_eval.py | 466 + .../swebench/swebench_evaluation.py | 98 + .../benchmarks/swebench/swebench_logs.py | 178 + .../benchmarks/swebench/swebench_metrics.py | 31 + .../src/exgentic/benchmarks/tau2/__init__.py | 29 + .../exgentic/benchmarks/tau2/requirements.txt | 1 + .../src/exgentic/benchmarks/tau2/setup.sh | 24 + .../exgentic/benchmarks/tau2/system-deps.txt | 1 + .../benchmarks/tau2/tau2_benchmark.py | 61 + .../src/exgentic/benchmarks/tau2/tau2_eval.py | 679 + .../src/exgentic/benchmarks/tau2/tau2_shim.py | 82 + .../exgentic/src/exgentic/core/__init__.py | 45 + .../exgentic/src/exgentic/core/actions.py | 340 + .../exgentic/src/exgentic/core/agent.py | 89 + .../src/exgentic/core/agent_instance.py | 84 + .../exgentic/src/exgentic/core/benchmark.py | 94 + .../exgentic/src/exgentic/core/context.py | 335 + .../exgentic/src/exgentic/core/evaluator.py | 53 + .../exgentic/core/orchestrator/__init__.py | 41 + .../src/exgentic/core/orchestrator/cleanup.py | 23 + .../exgentic/core/orchestrator/controller.py | 114 + .../exgentic/core/orchestrator/execution.py | 418 + .../exgentic/core/orchestrator/observer.py | 68 + .../src/exgentic/core/orchestrator/run.py | 190 + .../src/exgentic/core/orchestrator/session.py | 129 + .../exgentic/core/orchestrator/termination.py | 71 + .../src/exgentic/core/orchestrator/tracker.py | 176 + .../src/exgentic/core/runner_mixin.py | 59 + .../exgentic/src/exgentic/core/session.py | 161 + .../src/exgentic/core/types/__init__.py | 75 + .../src/exgentic/core/types/action.py | 108 + .../src/exgentic/core/types/evaluation.py | 151 + .../src/exgentic/core/types/model_settings.py | 62 + .../src/exgentic/core/types/observation.py | 175 + .../exgentic/src/exgentic/core/types/run.py | 304 + .../src/exgentic/core/types/session.py | 223 + .../src/exgentic/environment/__init__.py | 6 + .../src/exgentic/environment/docker.py | 405 + .../src/exgentic/environment/helpers.py | 200 + .../src/exgentic/environment/instance.py | 19 + .../src/exgentic/environment/local.py | 68 + .../src/exgentic/environment/manager.py | 232 + .../src/exgentic/environment/protocol.py | 39 + .../exgentic/src/exgentic/environment/venv.py | 85 + .../src/exgentic/integrations/__init__.py | 4 + .../exgentic/integrations/litellm/__init__.py | 33 + .../integrations/litellm/cache/__init__.py | 26 + .../integrations/litellm/cache/core.py | 325 + .../integrations/litellm/cache/key.py | 289 + .../integrations/litellm/cache/log.py | 127 + .../integrations/litellm/cache_utils.py | 6 + .../exgentic/integrations/litellm/config.py | 114 + .../exgentic/integrations/litellm/health.py | 51 + .../exgentic/integrations/litellm/proxy.py | 296 + .../integrations/litellm/trace_cost.py | 61 + .../integrations/litellm/trace_logger.py | 670 + .../src/exgentic/interfaces/__init__.py | 4 + .../src/exgentic/interfaces/cli/__init__.py | 4 + .../interfaces/cli/commands/__init__.py | 4 + .../interfaces/cli/commands/analyze.py | 1223 ++ .../exgentic/interfaces/cli/commands/batch.py | 1031 + .../interfaces/cli/commands/compare.py | 1169 ++ .../interfaces/cli/commands/dashboard.py | 25 + .../interfaces/cli/commands/evaluate.py | 429 + .../interfaces/cli/commands/listing.py | 105 + .../interfaces/cli/commands/run_info.py | 212 + .../exgentic/interfaces/cli/commands/serve.py | 61 + .../exgentic/interfaces/cli/commands/setup.py | 166 + .../src/exgentic/interfaces/cli/main.py | 126 + .../src/exgentic/interfaces/cli/options.py | 474 + .../src/exgentic/interfaces/cli/render.py | 263 + .../src/exgentic/interfaces/cli/run.py | 9 + .../exgentic/interfaces/dashboard/__init__.py | 2 + .../src/exgentic/interfaces/dashboard/app.py | 243 + .../interfaces/dashboard/views/__init__.py | 19 + .../interfaces/dashboard/views/data.py | 529 + .../interfaces/dashboard/views/formatting.py | 99 + .../interfaces/dashboard/views/forms.py | 134 + .../interfaces/dashboard/views/panels.py | 505 + .../interfaces/dashboard/views/runtime.py | 895 + .../interfaces/dashboard/views/state.py | 113 + .../interfaces/dashboard/views/status.py | 87 + .../src/exgentic/interfaces/lib/__init__.py | 4 + .../src/exgentic/interfaces/lib/api.py | 613 + .../src/exgentic/interfaces/registry.py | 352 + .../src/exgentic/observers/__init__.py | 2 + .../exgentic/observers/handlers/__init__.py | 4 + .../exgentic/observers/handlers/configs.py | 48 + .../observers/handlers/dashboard_events.py | 360 + .../observers/handlers/file_logger.py | 337 + .../src/exgentic/observers/handlers/logger.py | 508 + .../src/exgentic/observers/handlers/otel.py | 451 + .../src/exgentic/observers/handlers/recap.py | 145 + .../exgentic/observers/handlers/results.py | 537 + .../observers/handlers/session_ledger.py | 66 + .../exgentic/observers/handlers/warnings.py | 27 + .../exgentic/observers/logging/__init__.py | 358 + .../exgentic/observers/tracing/__init__.py | 4 + .../exgentic/src/exgentic/testing/__init__.py | 42 + .../exgentic/src/exgentic/testing/agent.py | 145 + .../src/exgentic/testing/benchmark.py | 180 + .../src/exgentic/testing/calculator.py | 50 + .../src/exgentic/testing/docker_session.py | 68 + .../exgentic/src/exgentic/utils/__init__.py | 2 + .../exgentic/src/exgentic/utils/cost.py | 222 + .../exgentic/src/exgentic/utils/disk_cache.py | 168 + .../exgentic/src/exgentic/utils/logging.py | 87 + .../exgentic/src/exgentic/utils/otel.py | 532 + .../exgentic/src/exgentic/utils/paths.py | 192 + .../exgentic/src/exgentic/utils/settings.py | 142 + .../exgentic/src/exgentic/utils/sync.py | 48 + labs/AgentStream/exgentic/tests/__init__.py | 4 + .../tests/adapters/runners/__init__.py | 2 + .../tests/adapters/runners/conftest.py | 36 + .../tests/adapters/runners/test_docker.py | 69 + .../adapters/runners/test_e2e_session.py | 289 + .../tests/adapters/runners/test_process.py | 43 + .../tests/adapters/runners/test_thread.py | 41 + .../tests/adapters/runners/test_transport.py | 101 + .../tests/adapters/runners/test_utils.py | 80 + .../tests/adapters/runners/test_venv.py | 69 + .../agents/cli/test_claude_cli_config.py | 30 + .../tests/agents/cli/test_cli_context_env.py | 54 + .../agents/cli/test_cli_error_surfacing.py | 59 + .../tests/agents/test_tool_calling_utils.py | 31 + .../exgentic/tests/api/__init__.py | 4 + .../exgentic/tests/api/conftest.py | 37 + .../exgentic/tests/api/fixtures/__init__.py | 4 + .../exgentic/tests/api/fixtures/test_agent.py | 27 + .../tests/api/fixtures/test_benchmark.py | 15 + .../tests/api/test_agent_package_integrity.py | 142 + .../exgentic/tests/api/test_api_errors.py | 50 + .../exgentic/tests/api/test_api_files.py | 34 + .../exgentic/tests/api/test_api_instances.py | 66 + .../exgentic/tests/api/test_api_limits.py | 50 + .../tests/api/test_api_missing_results.py | 43 + .../exgentic/tests/api/test_api_random.py | 30 + .../exgentic/tests/api/test_api_reuse.py | 42 + .../exgentic/tests/api/test_api_run_config.py | 87 + .../exgentic/tests/api/test_api_runners.py | 165 + .../tests/api/test_api_session_config.py | 41 + .../exgentic/tests/api/test_cli_batch.py | 132 + .../exgentic/tests/api/test_cli_commands.py | 102 + .../exgentic/tests/api/test_cli_compare.py | 1638 ++ .../exgentic/tests/api/test_cli_version.py | 24 + .../tests/api/test_package_exports.py | 30 + .../exgentic/tests/benchmarks/__init__.py | 2 + .../recordings/appworld/recording.json | 6 + .../recordings/appworld/results.json | 79 + .../recordings/appworld/session.json | 17009 ++++++++++++++++ .../recordings/appworld/trajectory.jsonl | 63 + .../recordings/browsecompplus/recording.json | 5 + .../recordings/browsecompplus/results.json | 206 + .../recordings/browsecompplus/session.json | 79 + .../browsecompplus/trajectory.jsonl | 55 + .../recordings/swebench/recording.json | 5 + .../recordings/swebench/results.json | 119 + .../recordings/swebench/session.json | 48 + .../recordings/swebench/trajectory.jsonl | 65 + .../benchmarks/recordings/tau2/recording.json | 5 + .../benchmarks/recordings/tau2/results.json | 77 + .../benchmarks/recordings/tau2/session.json | 642 + .../recordings/tau2/trajectory.jsonl | 17 + .../tests/benchmarks/test_benchmark_replay.py | 153 + .../tests/benchmarks/test_tau2_data_dir.py | 67 + .../exgentic/tests/core/test_actions.py | 117 + .../exgentic/tests/core/test_context_env.py | 45 + .../tests/core/test_run_results_version.py | 20 + .../exgentic/tests/environment/__init__.py | 2 + .../tests/environment/test_integration.py | 159 + .../tests/environment/test_manager.py | 1967 ++ .../litellm/cache/test_async_key.py | 49 + .../cache/test_cache_logger_context.py | 24 + .../integrations/litellm/cache/test_key.py | 91 + .../litellm/cache/test_settings.py | 155 + .../litellm/cache/test_sync_key.py | 76 + .../integrations/litellm/proxy/conftest.py | 77 + .../proxy/test_proxy_cache_execution.py | 51 + .../proxy/test_proxy_callback_config.py | 54 + .../proxy/test_proxy_callback_execution.py | 157 + .../litellm/proxy/test_proxy_env.py | 39 + .../test_proxy_subprocess_integration.py | 110 + .../litellm/proxy/test_trace_logger_env.py | 162 + .../tests/integrations/litellm/test_health.py | 85 + .../integrations/litellm/test_trace_cost.py | 58 + .../litellm/test_trace_logger_context.py | 33 + .../tests/integrations/test_mcp_agent.py | 214 + .../tests/integrations/test_mcp_server.py | 342 + .../exgentic/tests/setup/__init__.py | 4 + .../exgentic/tests/setup/test_tool_install.py | 103 + .../exgentic/tests/test_coordinator.py | 298 + .../exgentic/tests/test_env_loading.py | 33 + .../tests/test_integrations_functions.py | 91 + .../exgentic/tests/utils/test_cost.py | 87 + .../utils/test_litellm_cache_settings.py | 51 + labs/AgentStream/exgentic/uv.lock | 4101 ++++ labs/AgentStream/exgentic/whitesource.config | 2 + labs/AgentStream/figs/evaluation_compare.png | Bin 0 -> 311885 bytes 429 files changed, 82578 insertions(+) create mode 100644 labs/AgentStream/LICENSE create mode 100644 labs/AgentStream/exgentic/.dockerignore create mode 100644 labs/AgentStream/exgentic/.gitattributes create mode 100644 labs/AgentStream/exgentic/.github/workflows/pre-commit.yml create mode 100644 labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml create mode 100644 labs/AgentStream/exgentic/.github/workflows/tests.yml create mode 100644 labs/AgentStream/exgentic/.gitignore create mode 100644 labs/AgentStream/exgentic/.pre-commit-config.yaml create mode 100644 labs/AgentStream/exgentic/.whitesource create mode 100644 labs/AgentStream/exgentic/CODE_OF_CONDUCT.md create mode 100644 labs/AgentStream/exgentic/CONTRIBUTING.md create mode 100644 labs/AgentStream/exgentic/DCO.txt create mode 100644 labs/AgentStream/exgentic/DEVELOPMENT.md create mode 100644 labs/AgentStream/exgentic/LICENSE create mode 100644 labs/AgentStream/exgentic/README.md create mode 100644 labs/AgentStream/exgentic/SECURITY.md create mode 100644 labs/AgentStream/exgentic/docs/README.md create mode 100644 labs/AgentStream/exgentic/docs/adding-agents.md create mode 100644 labs/AgentStream/exgentic/docs/adding-benchmarks.md create mode 100644 labs/AgentStream/exgentic/docs/batch.md create mode 100644 labs/AgentStream/exgentic/docs/cli-reference.md create mode 100644 labs/AgentStream/exgentic/docs/custom-models.md create mode 100644 labs/AgentStream/exgentic/docs/huggingface.md create mode 100644 labs/AgentStream/exgentic/docs/observability/quickstart.md create mode 100644 labs/AgentStream/exgentic/docs/observability/semantic-conventions.md create mode 100644 labs/AgentStream/exgentic/docs/observers.md create mode 100644 labs/AgentStream/exgentic/docs/output-format.md create mode 100644 labs/AgentStream/exgentic/docs/python-api.md create mode 100644 labs/AgentStream/exgentic/docs/releasing.md create mode 100644 labs/AgentStream/exgentic/docs/replay-testing.md create mode 100644 labs/AgentStream/exgentic/docs/runners.md create mode 100644 labs/AgentStream/exgentic/examples/run_appworld.py create mode 100644 labs/AgentStream/exgentic/examples/run_browsecomp.py create mode 100644 labs/AgentStream/exgentic/examples/run_claude_code_on_gsm8k.py create mode 100644 labs/AgentStream/exgentic/examples/run_claude_code_on_tau2bench.py create mode 100644 labs/AgentStream/exgentic/examples/run_cli_agents.py create mode 100644 labs/AgentStream/exgentic/examples/run_gsm8k.py create mode 100644 labs/AgentStream/exgentic/examples/run_hotpotqa.py create mode 100644 labs/AgentStream/exgentic/examples/run_openai_mcp.py create mode 100644 labs/AgentStream/exgentic/examples/run_smol.py create mode 100644 labs/AgentStream/exgentic/examples/run_swebench.py create mode 100644 labs/AgentStream/exgentic/examples/run_taubench.py create mode 100644 labs/AgentStream/exgentic/examples/simple_test_agent/adapter.py create mode 100644 labs/AgentStream/exgentic/examples/simple_test_agent/setup.sh create mode 100644 labs/AgentStream/exgentic/misc/assets/cli.png create mode 100644 labs/AgentStream/exgentic/misc/assets/exgentic_banner_black.png create mode 100644 labs/AgentStream/exgentic/misc/assets/exgentic_banner_black_no_background.png create mode 100644 labs/AgentStream/exgentic/misc/assets/exgentic_banner_white.png create mode 100644 labs/AgentStream/exgentic/misc/assets/exgentic_banner_white_no_background.png create mode 100644 labs/AgentStream/exgentic/misc/assets/exgentic_light.png create mode 100644 labs/AgentStream/exgentic/misc/assets/gui.png create mode 100644 labs/AgentStream/exgentic/misc/assets/icon.png create mode 100644 labs/AgentStream/exgentic/misc/assets/icon_black.png create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/README.md create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/appworld/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/browsecompplus/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/core/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/gsm8k/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/hotpotqa/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/swebench/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/requirements/tau2/requirements.txt create mode 100644 labs/AgentStream/exgentic/misc/security/setup_environments.sh create mode 100644 labs/AgentStream/exgentic/misc/skills/add-agent/SKILL.md create mode 100644 labs/AgentStream/exgentic/misc/skills/add-benchmark/SKILL.md create mode 100644 labs/AgentStream/exgentic/misc/utils/.secrets.baseline create mode 100644 labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py create mode 100644 labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py create mode 100644 labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py create mode 100644 labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py create mode 100644 labs/AgentStream/exgentic/pyproject.toml create mode 100644 labs/AgentStream/exgentic/renovate.json create mode 100644 labs/AgentStream/exgentic/ruff.toml create mode 100644 labs/AgentStream/exgentic/scripts/a_mem/run_experiment.py create mode 100644 labs/AgentStream/exgentic/scripts/a_mem/run_experiment.sh create mode 100644 labs/AgentStream/exgentic/scripts/ace/run_experiment.py create mode 100644 labs/AgentStream/exgentic/scripts/ace/run_experiment.sh create mode 100644 labs/AgentStream/exgentic/scripts/autoskill/run_experiment.py create mode 100644 labs/AgentStream/exgentic/scripts/autoskill/run_experiment.sh create mode 100644 labs/AgentStream/exgentic/scripts/harness/run_experiment.py create mode 100644 labs/AgentStream/exgentic/scripts/harness/run_experiment.sh create mode 100644 labs/AgentStream/exgentic/scripts/litellm/run_baseline.py create mode 100644 labs/AgentStream/exgentic/scripts/litellm/run_baseline.sh create mode 100644 labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.py create mode 100644 labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.sh create mode 100644 labs/AgentStream/exgentic/scripts/release.sh create mode 100644 labs/AgentStream/exgentic/scripts/utils/task_ordering.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/actions/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/actions/chat.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/actions/functions.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/agents/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/agents/code_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/agents/coordinator.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_server.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/executors/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/executors/proxy.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/_utils.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/direct.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/docker.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/process.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/service.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/thread.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/transport.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/runners/venv.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/schemas/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/schemas/json_schema.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/adapters/schemas/openai.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_note.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_store.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/prompts.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/a_mem/retriever.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/bulletpoint_analyzer.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_store.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_utils.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/curator.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/reflector.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/prompts.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_extraction.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_maintenance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_retrieval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_store.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/base.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/cli.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/cli.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/command_runner.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/cli.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/cli/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/evolver.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_store.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/evolver.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/inject.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/harness/retriever.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/utils.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/openai/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/openai/instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/openai/openai_mcp_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/openai/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/evaluator.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/induce_memory.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/memory_management.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/eval_prompts.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/memory_instruction.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_store.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/replay/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_session.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/structured_code_agent.yaml create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/agents/tool_shortlisting.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_eval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_eval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_shim.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_eval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/make_light_dataset.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/retriever.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_service.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_tool_handler.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/searcher_cache.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/gsm8k_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/hle_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/hotpotqa_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/config.yaml create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/readme.md create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_eval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_evaluation.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_logs.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_metrics.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/requirements.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/setup.sh create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/system-deps.txt create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_eval.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_shim.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/actions.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/agent_instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/context.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/evaluator.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/cleanup.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/controller.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/execution.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/observer.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/run.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/session.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/termination.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/orchestrator/tracker.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/runner_mixin.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/session.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/action.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/evaluation.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/model_settings.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/observation.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/run.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/core/types/session.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/docker.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/helpers.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/instance.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/local.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/manager.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/protocol.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/environment/venv.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/core.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/key.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/log.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache_utils.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/config.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/health.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/proxy.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_cost.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_logger.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/analyze.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/batch.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/compare.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/dashboard.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/evaluate.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/listing.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/run_info.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/serve.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/setup.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/main.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/options.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/render.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/cli/run.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/app.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/data.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/formatting.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/forms.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/panels.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/runtime.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/state.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/status.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/lib/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/lib/api.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/interfaces/registry.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/configs.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/dashboard_events.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/file_logger.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/logger.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/otel.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/recap.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/results.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/session_ledger.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/handlers/warnings.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/logging/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/observers/tracing/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/testing/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/testing/agent.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/testing/benchmark.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/testing/calculator.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/testing/docker_session.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/__init__.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/cost.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/disk_cache.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/logging.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/otel.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/paths.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/settings.py create mode 100644 labs/AgentStream/exgentic/src/exgentic/utils/sync.py create mode 100644 labs/AgentStream/exgentic/tests/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/conftest.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_docker.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_e2e_session.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_process.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_thread.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_transport.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_utils.py create mode 100644 labs/AgentStream/exgentic/tests/adapters/runners/test_venv.py create mode 100644 labs/AgentStream/exgentic/tests/agents/cli/test_claude_cli_config.py create mode 100644 labs/AgentStream/exgentic/tests/agents/cli/test_cli_context_env.py create mode 100644 labs/AgentStream/exgentic/tests/agents/cli/test_cli_error_surfacing.py create mode 100644 labs/AgentStream/exgentic/tests/agents/test_tool_calling_utils.py create mode 100644 labs/AgentStream/exgentic/tests/api/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/api/conftest.py create mode 100644 labs/AgentStream/exgentic/tests/api/fixtures/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/api/fixtures/test_agent.py create mode 100644 labs/AgentStream/exgentic/tests/api/fixtures/test_benchmark.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_agent_package_integrity.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_errors.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_files.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_instances.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_limits.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_missing_results.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_random.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_reuse.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_run_config.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_runners.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_api_session_config.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_cli_batch.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_cli_commands.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_cli_compare.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_cli_version.py create mode 100644 labs/AgentStream/exgentic/tests/api/test_package_exports.py create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/recording.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/results.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/session.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/recording.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/results.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/session.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/trajectory.jsonl create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/recording.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/session.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/trajectory.jsonl create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/recording.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/results.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/session.json create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/trajectory.jsonl create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/test_benchmark_replay.py create mode 100644 labs/AgentStream/exgentic/tests/benchmarks/test_tau2_data_dir.py create mode 100644 labs/AgentStream/exgentic/tests/core/test_actions.py create mode 100644 labs/AgentStream/exgentic/tests/core/test_context_env.py create mode 100644 labs/AgentStream/exgentic/tests/core/test_run_results_version.py create mode 100644 labs/AgentStream/exgentic/tests/environment/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/environment/test_integration.py create mode 100644 labs/AgentStream/exgentic/tests/environment/test_manager.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_async_key.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_cache_logger_context.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_key.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_settings.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_sync_key.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/conftest.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_cache_execution.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_config.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_execution.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_env.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_subprocess_integration.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_trace_logger_env.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/test_health.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_cost.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_logger_context.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/test_mcp_agent.py create mode 100644 labs/AgentStream/exgentic/tests/integrations/test_mcp_server.py create mode 100644 labs/AgentStream/exgentic/tests/setup/__init__.py create mode 100644 labs/AgentStream/exgentic/tests/setup/test_tool_install.py create mode 100644 labs/AgentStream/exgentic/tests/test_coordinator.py create mode 100644 labs/AgentStream/exgentic/tests/test_env_loading.py create mode 100644 labs/AgentStream/exgentic/tests/test_integrations_functions.py create mode 100644 labs/AgentStream/exgentic/tests/utils/test_cost.py create mode 100644 labs/AgentStream/exgentic/tests/utils/test_litellm_cache_settings.py create mode 100644 labs/AgentStream/exgentic/uv.lock create mode 100644 labs/AgentStream/exgentic/whitesource.config create mode 100644 labs/AgentStream/figs/evaluation_compare.png diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb8dcce8..62a3f09a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,6 +59,9 @@ repos: - "**/build/**" - "-ignore" - "**/vendor/**" + # Separately licensed Apache-2.0 project + - "-ignore" + - "labs/AgentStream/**" # Generated protobuf / gRPC stubs - "-ignore" - "**/*.pb.go" diff --git a/CHANGELOG.md b/CHANGELOG.md index c282d64e..545112d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Guidelines for editors: ### Added +- **AgentStream:** add a streaming evaluation framework for self-evolving LLM agents under `labs/AgentStream`. + ### Changed ### Deprecated diff --git a/README.md b/README.md index 17426c92..36807707 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,8 @@ Contributions of all kinds are welcome: bug reports, feature ideas, documentatio Sico is licensed under the [MIT License](LICENSE). +The contents of [AgentStream](labs/AgentStream) are licensed separately under the [Apache License 2.0](labs/AgentStream/LICENSE). + ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. diff --git a/labs/AgentStream/LICENSE b/labs/AgentStream/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/labs/AgentStream/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/labs/AgentStream/README.md b/labs/AgentStream/README.md index 58fb6d24..a304f3a2 100644 --- a/labs/AgentStream/README.md +++ b/labs/AgentStream/README.md @@ -24,9 +24,14 @@ dapenghu@microsoft.com

+

+ arXiv +

+ ## 🚀 News +* **[2026/08]** Code is released! * **[2026/07]** Code is under preparation. Stay tuned! ## 📖 Overview @@ -38,3 +43,93 @@ Over these scenarios, we combinatorially evaluate five representative self-evolv Our results show that self-evolution reliability varies across streaming scenarios, the benefit of self-evolution is gated by model capability and non-monotonic in model strength, and no single method dominates across models and scenarios. These findings offer concrete guidance for selecting self-evolving methods across models and streaming scenarios. Overall, we advocate that self-evolving agents should be evaluated under realistic task streams rather than isolated single-task settings. + +
+ Framework of AgentStream +
+ +## ⚡️ Getting Started + +AgentStream is built on the [`Exgentic`](./exgentic) framework, which is bundled in this repository. The five self-evolving agents live under [`exgentic/src/exgentic/agents`](./exgentic/src/exgentic/agents), and the benchmarks are orchestrated through `exgentic`'s installation and runner infrastructure. + +### 1. Requirements + +- Python `>= 3.11` +- [`uv`](https://github.com/astral-sh/uv) +- Docker (optional) + +### 2. Install the local exgentic (agent side) + +Clone the repo and create an editable environment from the bundled `exgentic`: + +```bash +git clone https://github.com/microsoft/Sico.git +cd Sico/labs/AgentStream/exgentic + +# Install the local ./src/exgentic in editable mode into .venv/ +uv sync + +# Activate the environment +source .venv/bin/activate +``` + +Verify that the self-evolving agents are visible from the local install: + +```bash +uv run exgentic list agents +``` + +### 3. Install benchmarks (benchmark side) + +Each benchmark is installed into isolated **`venv`** environment: + +```bash +cd Sico/labs/AgentStream/exgentic + + +uv run exgentic install --benchmark tau2 +uv run exgentic install --benchmark bfcl +uv run exgentic install --benchmark hle +uv run exgentic install --benchmark appworld +uv run exgentic install --benchmark swebench +uv run exgentic install --benchmark browsecompplus +``` + +### 4. API credentials + +The runners call LLMs through [LiteLLM](https://docs.litellm.ai/). Set the credentials for your provider in the [`exgentic/scripts//run_experiment.sh`](./exgentic/scripts/ace/run_experiment.sh): + +```bash +export OPENAI_API_KEY="..." +export OPENAI_API_BASE="..." +``` + +### 5. Run the streaming experiments + +Each method has its own runner under [`exgentic/scripts/`](./exgentic/scripts). The shell script selects the streaming scenario via `MODE` (`isolated` | `sequential` | `interleaved`), the model, the seed, and the benchmark stream: + +```bash +cd Sico/labs/AgentStream/exgentic/scripts/ace + +bash run_experiment.sh +``` + + +## 🙏 Acknowledgement +This work is based on [Exgentic](https://github.com/Exgentic/exgentic). We sincerely thank the authors and contributors of these excellent open-source projects. + +## 📚 Citation +If you find our work helpful, please consider citing: + +```bibtex +@article{yan2026agentstream, + title={AgentStream: How Well Do Self-Evolving LLM Agents Perform Under Streaming Tasks?}, + author={Yan, Dong and Liang, Jian and Hu, Dapeng and He, Ran and Yuan, Nicholas Jing and Zhang, Qi and Tan, Tieniu}, + journal={arXiv preprint arXiv:2608.00155}, + year={2026} +} +``` + +## 📄 License + +The contents of this AgentStream directory are licensed separately under the [Apache License 2.0](./LICENSE). diff --git a/labs/AgentStream/exgentic/.dockerignore b/labs/AgentStream/exgentic/.dockerignore new file mode 100644 index 00000000..79975c4c --- /dev/null +++ b/labs/AgentStream/exgentic/.dockerignore @@ -0,0 +1,19 @@ +.venv/ +.git/ +outputs/ +__pycache__/ +*.pyc +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.exgentic/ +tests/ + +# Large benchmark data/assets — not needed in the base Docker image. +# Benchmarks that need these should use setup_script or volumes instead. +src/exgentic/benchmarks/browsecompplus/assets/ +# Tau2: exclude large pre-computed results and figures but keep domain +# data files (tasks.json, policy.md, db.json) which the session needs. +src/exgentic/benchmarks/tau2/installation/tau2-bench/data/tau2/results/ +src/exgentic/benchmarks/tau2/installation/tau2-bench/figs/ +src/exgentic/benchmarks/tau2/installation/tau2-bench/tests/ diff --git a/labs/AgentStream/exgentic/.gitattributes b/labs/AgentStream/exgentic/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/labs/AgentStream/exgentic/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml b/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..b76a8097 --- /dev/null +++ b/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml @@ -0,0 +1,46 @@ +name: Pre-commit Checks + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pre-commit + + - name: Run pre-commit hooks + run: pre-commit run --all-files --show-diff-on-failure + + - name: Upload pre-commit results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: pre-commit-results + path: | + **/*.log + .pre-commit-config.yaml + +# Made with Bob diff --git a/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml b/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml new file mode 100644 index 00000000..9d5d203e --- /dev/null +++ b/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml @@ -0,0 +1,59 @@ +name: Publish to PyPI + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Ensure tag commit is on main + run: | + git fetch origin main + tag_commit="$(git rev-list -n 1 "$GITHUB_REF_NAME")" + git merge-base --is-ancestor "$tag_commit" origin/main + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build distributions + run: | + python -m pip install --upgrade pip + python -m pip install build twine + python -m build + python -m twine check dist/* + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: pypi + url: https://pypi.org/project/exgentic/ + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/labs/AgentStream/exgentic/.github/workflows/tests.yml b/labs/AgentStream/exgentic/.github/workflows/tests.yml new file mode 100644 index 00000000..9316f916 --- /dev/null +++ b/labs/AgentStream/exgentic/.github/workflows/tests.yml @@ -0,0 +1,64 @@ +name: Tests + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --frozen --extra dev --extra analysis + + - name: Run core tests + run: uv run --frozen pytest tests -v --ignore=tests/integrations --ignore=tests/adapters/runners --tb=short + + - name: Run runner tests + run: uv run --frozen pytest tests/adapters/runners -v --tb=short -p no:faulthandler + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.python-version }} + path: "**/*.log" + + docker-integration: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: "3.12" + + - name: Install dependencies + run: uv sync --frozen --extra dev --extra analysis + + - name: Run Docker integration tests + run: uv run --frozen pytest tests/environment/test_manager.py -v -k "Integration" --tb=short diff --git a/labs/AgentStream/exgentic/.gitignore b/labs/AgentStream/exgentic/.gitignore new file mode 100644 index 00000000..2609c5fc --- /dev/null +++ b/labs/AgentStream/exgentic/.gitignore @@ -0,0 +1,121 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +!src/exgentic/interfaces/lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +src/exgentic/_version.py + +# Exgentic local data (venvs, caches, installations) +.exgentic/ +.exgentic_installations/ + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# AI/CLI agent metadata +.bob/ +.claude/ +.cursor/ +.aider* +.copilot/ +.continue/ + +# VS Code settings +.vscode/ +.history/ +*.code-workspace + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints +.jupyter/ +profile_default/ + +# PyCharm +.idea/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ +outputs/ +# OS files +.DS_Store +Thumbs.db +data/ +src/exgentic/benchmarks/appworld/data/ +src/exgentic/benchmarks/appworld/experiments/ +src/exgentic/benchmarks/tau2/installation/ + +# Benchmark caches +tau2_disk_cache/ + +old_outputs2/ +outputs +*outputs* +.litellm_cache +trace.jsonl + +t.* +node_modules +package-lock.json +package.json +src/exgentic/benchmarks/browsecompplus/assets +exgentic_session_cache/ +scripts/litellm/docker_vm_data/Ubuntu.qcow2.zip diff --git a/labs/AgentStream/exgentic/.pre-commit-config.yaml b/labs/AgentStream/exgentic/.pre-commit-config.yaml new file mode 100644 index 00000000..b7d44f52 --- /dev/null +++ b/labs/AgentStream/exgentic/.pre-commit-config.yaml @@ -0,0 +1,66 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.6 + hooks: + # Run the linter on all files except the specific one + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: [--baseline, misc/utils/.secrets.baseline] + exclude: misc/utils/.secrets.baseline + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.7.12 + hooks: + - id: uv-lock + args: [--locked] + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli + + - repo: local + hooks: + - id: enforce-spdx-header + name: Enforce SPDX Header + entry: python3 misc/utils/enforce_spdx_header.py + language: system + files: \.py$ + exclude: ^\.venv/ + types: [python] + - id: enforce-relative-imports + name: Enforce Relative Imports + entry: python3 misc/utils/enforce_relative_imports.py + language: system + # Adjust the files pattern to match your needs + files: ^src/.*\.py$ + # Optional: Specify types or exclude files + types: [python] + - id: enforce-dependency-caps + name: Enforce Dependency Version Caps + entry: python3 misc/utils/enforce_dependency_caps.py + language: system + files: ^pyproject\.toml$ + pass_filenames: false + + - repo: local + hooks: + - id: enforce-library-imports + name: Enforce Library Imports + entry: python3 misc/utils/enforce_library_imports.py + language: system + # Adjust the files pattern to match your needs + exclude: (^src/.*\.py$)|misc/utils/enforce_library_imports.py|misc/utils/enforce_relative_imports.py + # Optional: Specify types or exclude files + types: [python] diff --git a/labs/AgentStream/exgentic/.whitesource b/labs/AgentStream/exgentic/.whitesource new file mode 100644 index 00000000..5e1a3914 --- /dev/null +++ b/labs/AgentStream/exgentic/.whitesource @@ -0,0 +1,9 @@ +{ + "settingsInheritedFrom": "whitesource-config/whitesource-config@master", + "scanSettingsSAST": { + "enableScan": true + }, + "scanSettings": { + "configMode": "LOCAL" + } +} diff --git a/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md b/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..c8e52a2e --- /dev/null +++ b/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socioeconomic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/labs/AgentStream/exgentic/CONTRIBUTING.md b/labs/AgentStream/exgentic/CONTRIBUTING.md new file mode 100644 index 00000000..2a03a7c2 --- /dev/null +++ b/labs/AgentStream/exgentic/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# How to contribute to Exgentic + +Thank you for your interest in contributing! + +## Development Setup + +```bash +# Install dependencies using the pinned lock file — never plain `uv sync` +uv sync --frozen --extra dev --extra analysis + +# To intentionally upgrade a specific package: +uv lock --upgrade-package +# Review the uv.lock diff carefully before committing +``` + +> **Security note:** Always use `uv sync --frozen` locally. Running plain `uv sync` may silently +> upgrade packages and introduce untested or malicious versions. Dependency upgrades should be +> explicit, reviewed, and go through a PR. + +## How to Contribute + +1. Fork the [repository](https://github.com/exgentic/exgentic). +2. Create a new branch for your changes. +3. Sign your commits using the `-s` flag (see [Legal](#legal)) +4. Submit a pull request to the `main` branch with a clear title and description. +Reference any issues fixed, for example `Fixes #1234`. +Ensure your PR title follows [semantic commit conventions](https://www.conventionalcommits.org/). +5. A maintainer will review your PR and may request changes. + +## Legal + +### License + +This project is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE). + +Each source code file must include the following SPDX headers at the top of the file: + +**For Python files:** +```python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2025, The Exgentic organization and its contributors. + +"""Module docstring here.""" +import ... +``` + +**For other file types:** Use the appropriate comment syntax for that language. + +### Developer Certificate of Origin (DCO) + +We require all commits to be **signed off** to indicate agreement with the [DCO](DCO.txt). + +By signing off a commit, you certify: + +> “I have the right to submit this contribution under the Apache License, Version 2.0 (or the open source license indicated in the file), and understand this project and my contribution are public.” + +### How to sign off your commits + +The easiest way is to use the `-s` flag when committing: + +```bash +git commit -s -m "Fix: Correct spelling in README" +``` + +This uses your Git configuration. Make sure your name and email are set: +```bash +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" +``` + +Alternatively you can manually sign your commit by adding this line to the commit message: +``` +Signed-off-by: Your Name +``` + +## Development Environment Setup + +For detailed instructions on setting up your local development environment, see [DEVELOPMENT.md](./DEVELOPMENT.md). diff --git a/labs/AgentStream/exgentic/DCO.txt b/labs/AgentStream/exgentic/DCO.txt new file mode 100644 index 00000000..49b8cb05 --- /dev/null +++ b/labs/AgentStream/exgentic/DCO.txt @@ -0,0 +1,34 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/labs/AgentStream/exgentic/DEVELOPMENT.md b/labs/AgentStream/exgentic/DEVELOPMENT.md new file mode 100644 index 00000000..814ea061 --- /dev/null +++ b/labs/AgentStream/exgentic/DEVELOPMENT.md @@ -0,0 +1,129 @@ +# Development Guide + +This guide covers setting up exgentic for local development, editing, and debugging. + +## Setup + +```bash +git clone https://github.com/Exgentic/exgentic.git +cd exgentic +uv sync +``` + +## Setup Benchmarks & Agents + +Benchmarks and agents declare their dependencies through two mechanisms: + +- **`requirements.txt`** — pip packages installed automatically via `uv pip install` +- **`setup.sh`** — shell script for non-pip setup (apt packages, git clones, data downloads) + +Both are auto-discovered next to the benchmark/agent module directory. The `exgentic install` command runs both: + +```bash +# Benchmarks +uv run exgentic install --benchmark tau2 +uv run exgentic install --benchmark appworld +uv run exgentic install --benchmark gsm8k +uv run exgentic install --benchmark hotpotqa +uv run exgentic install --benchmark swebench +uv run exgentic install --benchmark browsecompplus + +# Agents +uv run exgentic install --agent litellm_tool_calling +uv run exgentic install --agent smolagents +uv run exgentic install --agent openai +uv run exgentic install --agent claude +uv run exgentic install --agent codex +uv run exgentic install --agent gemini +``` + +> **Note:** `exgentic setup` still works but is deprecated. Use `install`/`uninstall` instead. + +### Isolated Runners (venv / docker) + +By default, benchmarks run with the `venv` runner, which creates an isolated `uv` virtual environment per benchmark under `.exgentic//venv/`. This means **no local setup is needed** — dependencies are installed automatically in the venv on first run. + +You can also use the `docker` runner for full container isolation: + +```bash +uv run exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --model gpt-4o \ + --set benchmark.runner=docker \ + --set benchmark.user_simulator_model="gpt-4o" +``` + +Both isolated runners follow the same pattern: + +1. Install `requirements.txt` and run `setup.sh` in the isolated environment +2. Start `exgentic serve --cls --kwargs ` inside the venv/container +3. Communicate over HTTP via the runner transport layer + +Setup scripts can check the `EXGENTIC_DOCKER_BUILD` environment variable to distinguish a Docker build from a local setup (e.g., to skip interactive prompts or large downloads that are handled differently in containers). + +## API Credentials + +```bash +export OPENAI_API_KEY=... +# or +export ANTHROPIC_API_KEY=... +``` + +Or create a `.env` file in the project root — Exgentic loads it automatically. + +## Running Evaluations + +```bash +uv run exgentic list benchmarks +uv run exgentic list agents + +uv run exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --model gpt-4o \ + --set benchmark.user_simulator_model="gpt-4o" +``` + +## Tests + +```bash +# Core tests (no Docker or external services required) +uv run pytest tests/ --ignore=tests/integrations --ignore=tests/adapters/runners + +# Runner/transport tests (includes Docker tests on matching Python version) +uv run pytest tests/adapters/runners -v -p no:faulthandler + +# API-level tests only +uv run pytest tests/api + +# Skip tests requiring external services +uv run pytest tests/ -k "not litellm and not mcp" +``` + +The test suite includes **replay tests** that re-run recorded benchmark sessions without any external dependencies. Recordings are stored under `tests/benchmarks/recordings/` and use `ReplayBenchmark` + `ReplayAgent` to verify the execution loop end-to-end. + +## Linting + +```bash +pip install pre-commit +pre-commit install +pre-commit run --all-files +``` + +## OpenTelemetry Tracing + +```bash +uv sync --extra otel + +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +export EXGENTIC_OTEL_ENABLED=true +``` + +See [`OTEL_SEMANTIC_CONVENTIONS.md`](./OTEL_SEMANTIC_CONVENTIONS.md) for details. + +## Releases + +- Release process guide: `docs/releasing.md` +- Benchmark adapter design guide: `docs/adding-benchmarks.md` +- Create and push a release tag: `scripts/release.sh 0.2.0 --push` +- After PyPI publish succeeds, create the GitHub Release manually: `gh release create v0.2.0 --generate-notes --title "v0.2.0"` +- Release versions come from Git tags via `hatch-vcs` +- PyPI publishing uses GitHub Actions Trusted Publishing diff --git a/labs/AgentStream/exgentic/LICENSE b/labs/AgentStream/exgentic/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/labs/AgentStream/exgentic/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/labs/AgentStream/exgentic/README.md b/labs/AgentStream/exgentic/README.md new file mode 100644 index 00000000..11de4d8b --- /dev/null +++ b/labs/AgentStream/exgentic/README.md @@ -0,0 +1,249 @@ +Exgentic Banner + +

+ Evaluate any agent on any benchmark in the simplest way possible +

+ +--- + +## What is Exgentic? + +Exgentic is a universal evaluation framework that enables standardized testing of AI agents across diverse benchmarks and domains. It provides a consistent interface for evaluating any agent on any benchmark, making it easy to compare performance, reproduce results, and ensure your agent works reliably across different tasks and environments. + +## Who is it for? + +1. **General Audience** - Visit [www.exgentic.ai](https://www.exgentic.ai) to explore the first general agent leaderboard comparing leading agents and frontier models across varied tasks. +2. **Agent Builders** - Evaluate your agents comprehensively across multiple domains and benchmarks. +3. **Researchers & Component Developers** - Test agentic components (memory, context compression, planning) across different agents and domains. +4. **Benchmark Builders** - Evaluate your benchmark across multiple agents to ensure meaningful differentiation. + +--- + +## Quick Start + +### Installation + +```bash +uv tool install exgentic +``` + +### API Credentials + +```bash +export OPENAI_API_KEY=... +# or +export ANTHROPIC_API_KEY=... +``` + +### Run an Evaluation + +```bash +# List available benchmarks and agents +exgentic list benchmarks +exgentic list agents + +# Evaluate an agent on a benchmark +exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --model gpt-4o \ + --set benchmark.user_simulator_model="gpt-4o" +``` + +Benchmarks are automatically installed on first run — no manual installation needed. You can also install them explicitly: + +```bash +exgentic install --benchmark tau2 # install deps + data (default) +exgentic install --agent tool_calling +exgentic install --benchmark tau2 --docker # build Docker image +exgentic install --benchmark tau2 --local # install into local environment +exgentic uninstall --benchmark tau2 # remove installed environment +``` + +> **Note:** `exgentic setup` still works but is deprecated in favor of `install`/`uninstall`. + +For full container isolation, use the Docker runner (`--set benchmark.runner=docker`). You only need Docker installed and running: + +```bash +exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --model gpt-4o \ + --set benchmark.runner=docker \ + --set benchmark.user_simulator_model="gpt-4o" +``` + +### Python API + +To use exgentic as a library, install it first: + +```bash +uv add exgentic # or: pip install exgentic +``` + +```python +from exgentic import evaluate + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=2, + model="gpt-4o", + benchmark_kwargs={"user_simulator_model": "gpt-4o"}, +) +``` + +For more examples, see the [`examples/`](./examples/) directory. + +--- + +## Available Benchmarks + +```bash +exgentic list benchmarks +``` + +| Benchmark | Description | +|-----------|-------------| +| **tau2** | Simulated customer support tasks across multiple domains (mock, retail, airline, telecom) | +| **appworld** | Multi-app API environment testing agents' ability to interact with application interfaces | +| **browsecompplus** | Web search and browsing benchmark for information retrieval and navigation | +| **swebench** | Software engineering benchmark for resolving real-world GitHub issues | +| **hotpotqa** | Multi-hop question answering over Wikipedia | +| **gsm8k** | Grade school math word problems with optional calculator tool | +| **bfcl** | Berkeley Function Calling Leaderboard for evaluating tool-use capabilities | + +## Available Agents + +| Agent | Description | +|-------|-------------| +| **LiteLLM Tool Calling** | Generic tool-calling agent via LiteLLM | +| **SmolAgents** | HuggingFace SmolAgents framework | +| **OpenAI MCP** | OpenAI Responses API with MCP tools | +| **Claude Code** | Anthropic Claude Code agent | +| **Codex CLI** | OpenAI Codex CLI agent | +| **Gemini CLI** | Google Gemini CLI agent | + +--- + +## Dashboard + +Dashboard + +```bash +exgentic dashboard +``` + +--- + +## Output Structure + +Each run creates its own directory under `outputs//`: + +```text +outputs// +├── results.json # Overall scores, costs, per-session statistics +├── benchmark_results.json # Benchmark-specific aggregated results +├── run/ +│ ├── config.json # Snapshot of benchmark and agent configuration +│ ├── run.log # Main execution log +│ └── warnings.log # Warnings during execution +└── sessions// + ├── config.json # Session configuration + ├── results.json # Session results + ├── trajectory.jsonl # One JSON line per step (action + observation) + ├── agent/ + │ └── agent.log # Agent execution log + └── benchmark/ + ├── results.json # Benchmark-specific results + └── session.log # Benchmark session log +``` + +--- + +## CLI Reference + +CLI + +```bash +# Discover +exgentic list benchmarks +exgentic list subsets --benchmark tau2 +exgentic list tasks --benchmark tau2 --subset retail --limit 5 +exgentic list agents +exgentic install --benchmark tau2 +exgentic install --benchmark tau2 --docker +exgentic install --benchmark tau2 --local +exgentic uninstall --benchmark tau2 + +# Run +exgentic evaluate --benchmark tau2 --agent tool_calling --subset airline --num-tasks 10 +exgentic batch run --benchmark tau2 --agent tool_calling --subset airline --num-tasks 10 + +# Inspect +exgentic status --benchmark tau2 --agent tool_calling --subset airline --num-tasks 10 +exgentic preview --benchmark tau2 --agent tool_calling --subset airline --num-tasks 10 +exgentic results --benchmark tau2 --agent tool_calling --subset airline --num-tasks 10 + +# Analyze +exgentic compare --agents tool_calling openai --benchmark tau2 + +# Explore +exgentic dashboard +``` + +--- + +## Advanced + +### Model Configuration + +```bash +exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --set agent.model.temperature=0.2 +``` + +Supported fields: `temperature`, `top_p`, `max_tokens`, `reasoning_effort`, `num_retries`, `retry_after`, `retry_strategy` + +### Run Limits + +```bash +exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 2 \ + --max-steps 100 --max-actions 100 +``` + +Sessions stop at either limit and record `limit_reached` status. Default: 100 for both. + +### HuggingFace + +Use HuggingFace models or run evaluations on HuggingFace Jobs. See [docs/huggingface.md](./docs/huggingface.md). + +--- + +## How It Works + +To learn more about Exgentic's architecture and design, see our [arXiv paper](https://arxiv.org/abs/2602.22953). + +## Development + +For local development, editing, and contributing, see [DEVELOPMENT.md](./DEVELOPMENT.md). + +## Contributing + +We welcome issues and pull requests! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. + +## Citing Exgentic + +```bibtex +@misc{bandel2026generalagentevaluation, + title={General Agent Evaluation}, + author={Elron Bandel and Asaf Yehudai and Lilach Eden and Yehoshua Sagron and Yotam Perlitz and Elad Venezian and Natalia Razinkov and Natan Ergas and Shlomit Shachor Ifergan and Segev Shlomov and Michal Jacovi and Leshem Choshen and Liat Ein-Dor and Yoav Katz and Michal Shmueli-Scheuer}, + year={2026}, + url={https://arxiv.org/abs/2602.22953}, +} +``` + +## License + +Apache License 2.0 — see [LICENSE](LICENSE). + +## Support + +For questions and support, [open an issue](https://github.com/Exgentic/exgentic/issues) on GitHub. diff --git a/labs/AgentStream/exgentic/SECURITY.md b/labs/AgentStream/exgentic/SECURITY.md new file mode 100644 index 00000000..db6c7c92 --- /dev/null +++ b/labs/AgentStream/exgentic/SECURITY.md @@ -0,0 +1,152 @@ +# Exgentic Security Policy & Responsible Disclosure + +## Security Policy + +This security policy applies to all public projects under the Exgentic organization on GitHub. We prioritize security and continuously work to safeguard our systems. However, vulnerabilities can still exist. If you identify a security issue, please report it to us so we can address it promptly. + +### Security/Bugfix Versions + +- Fixes are released either as part of the next minor version (e.g., 1.3.0 → 1.4.0) or as an on-demand patch version (e.g., 1.3.0 → 1.3.1) +- Security fixes are given priority and might be enough to cause a new version to be released + +## Reporting a Vulnerability + +We encourage responsible disclosure of security vulnerabilities. If you find something suspicious, we encourage and appreciate your report! + +### How to Report + +Use the "Report a vulnerability" button under the "Security" tab of the [repository](https://github.com/exgentic/exgentic/security). This creates a private communication channel between you and the maintainers. + +### Reporting Guidelines + +- Provide clear details to help us reproduce and fix the issue quickly +- Include steps to reproduce, potential impact, and any suggested fixes +- Your report will be kept confidential, and your details will not be shared without your consent + +### Response Timeline + +- We will acknowledge your report within 5 business days +- We will provide an estimated resolution timeline +- We will keep you updated on our progress + +### Disclosure Guidelines + +- Do not publicly disclose vulnerabilities until we have assessed, resolved, and notified affected users +- If you plan to present your research (e.g., at a conference or in a blog), share a draft with us at least 30 days in advance for review +- Avoid including: + - Data from any customer projects + - User/customer information + - Details about employees, contractors, or partners + +We appreciate your efforts in helping us maintain a secure platform and look forward to working together to resolve any issues responsibly. + +## Dependency Management & Supply Chain Security + +### Version Capping Policy + +All direct dependencies in `pyproject.toml` are capped at the next major version (e.g., `litellm>=1.65.0,<2`). This policy limits the blast radius of supply chain attacks by preventing automatic upgrades to arbitrary future versions. + +**Why we cap dependencies:** +- **Supply chain attack mitigation**: Malicious packages can be uploaded to PyPI at any time. By capping at major versions, we limit exposure to known version ranges. +- **Controlled upgrades**: Major version bumps require explicit review and testing before adoption. +- **Stability**: Prevents breaking changes from being automatically pulled in. + +**Enforcement:** +- A pre-commit hook (`enforce-dependency-caps`) validates that all dependencies have upper bounds. +- CI will fail if any direct dependency lacks an upper bound. +- The hook runs automatically on every commit and in CI. + +### Automated Dependency Updates via Renovate + +We use Renovate to keep dependencies up to date while maintaining security: + +**14-Day Release Age Gate:** +- Renovate is configured with `minimumReleaseAge: 14 days` for all Python dependencies. +- New package versions are not proposed until 2 weeks after their PyPI release. +- This reduces exposure to day-zero malicious uploads and gives the community time to identify compromised packages. + +**Major Version Bumps:** +- Renovate uses `rangeStrategy: "bump"` to update both `uv.lock` and the upper bounds in `pyproject.toml` when a new major version is stable. +- Major version PRs require careful review of breaking changes and thorough testing. + +### Reviewing Renovate PRs + +When reviewing Renovate PRs that update `uv.lock`: + +1. **Check the PR description** for the list of updated packages and their version changes. +2. **Review the lockfile diff** to understand what's changing: + ```bash + gh pr diff -- uv.lock + ``` +3. **Verify the release age**: Ensure the new version has been available for at least 14 days. +4. **Check for security advisories**: Look for any CVEs or security issues in the changelog. +5. **Review changelogs**: For major updates, read the package's changelog for breaking changes. +6. **Test thoroughly**: Run the full test suite and any relevant integration tests. + +### Lockfile Integrity + +A `uv-lock --locked` pre-commit hook (added in PR #65) ensures `uv.lock` stays in sync with `pyproject.toml`: +- The hook rejects commits where the lockfile is out of sync. +- This prevents accidental lockfile drift and ensures reproducible builds. +- If the hook fails, run `uv lock` to regenerate the lockfile, review the changes, and commit. + +### Incident Response: Malicious Package Detected + +If a malicious package version is discovered in our dependencies: + +1. **Immediate containment:** + ```bash + # Pin the malicious version as excluded in pyproject.toml + # Example: "litellm>=1.65.0,!=1.82.7,!=1.82.8,<2" + ``` + +2. **Rotate credentials:** + - Assume any secrets or credentials accessible to the compromised environment may be compromised. + - Rotate API keys, tokens, and passwords that were accessible during the infection window. + +3. **Clean infected environments:** + ```bash + # Remove all virtual environments + rm -rf .venv venv .exgentic/ + + # Reinstall with the patched dependency specification + uv sync + ``` + +4. **Audit for data exfiltration:** + - Review logs and network traffic for suspicious outbound connections. + - Check for unauthorized access to systems or data. + +5. **Update lockfile:** + ```bash + uv lock + git add uv.lock pyproject.toml + git commit -m "Pin malicious package version as excluded" + ``` + +6. **Notify the team** and document the incident. + +### CVE Scanning with uv audit + +The `uv audit` command scans dependencies for known CVEs: + +```bash +uv audit +``` + +**Current status:** +- `uv audit` is temporarily unavailable due to the litellm quarantine (versions 1.82.7 and 1.82.8 are excluded). +- Once the quarantine is lifted and a clean version is available, re-enable regular `uv audit` checks. +- Consider adding `uv audit` to CI once it's operational again. + +### Best Practices + +- **Never commit lockfiles without review**: Always inspect `uv.lock` diffs before committing. +- **Keep dependencies minimal**: Only add dependencies that are truly necessary. +- **Monitor security advisories**: Subscribe to security mailing lists for critical dependencies. +- **Test updates thoroughly**: Don't merge Renovate PRs without running tests. +- **Document exceptions**: If you must exclude a version (e.g., `!=1.82.8`), document why in a comment or commit message. + +## Known Vulnerabilities + +There are currently no known vulnerabilities. diff --git a/labs/AgentStream/exgentic/docs/README.md b/labs/AgentStream/exgentic/docs/README.md new file mode 100644 index 00000000..225c3f78 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/README.md @@ -0,0 +1,46 @@ +# Exgentic Documentation + +Welcome to the Exgentic docs. Use the table below to find what you need. + +--- + +## Using Exgentic + +| Document | Description | +|----------|-------------| +| [CLI Reference](./cli-reference.md) | Every command, flag, and environment variable | +| [Python API](./python-api.md) | `evaluate()`, `execute()`, `aggregate()`, `status()`, `list_*()`, and all other library functions | +| [Custom Models](./custom-models.md) | Use any LLM provider (OpenAI, Anthropic, Azure, Bedrock, Ollama, and more) via LiteLLM | +| [Batch Runs](./batch.md) | Run parameter sweeps, manage large evaluations, export to CSV, publish to HuggingFace | +| [Runners](./runners.md) | `direct`, `venv`, `docker` — isolation levels, configuration, Docker-in-Docker | +| [Output Format](./output-format.md) | Schema for `results.json`, `trajectory.jsonl`, session results, and cost reports | +| [Observers](./observers.md) | Hook into the evaluation lifecycle for custom logging, monitoring, and early stopping | + +## Extending Exgentic + +| Document | Description | +|----------|-------------| +| [Adding Agents](./adding-agents.md) | Write a new agent adapter — design principles, required methods, file layout, and validation checklist | +| [Adding Benchmarks](./adding-benchmarks.md) | Write a new benchmark adapter — design principles, contract rules, and validation checklist | +| [Replay Testing](./replay-testing.md) | Test benchmark and agent adapters end-to-end without API calls, using recorded sessions | + +## Observability + +| Document | Description | +|----------|-------------| +| [Quick Start](./observability/quickstart.md) | Set up OpenTelemetry tracing with Jaeger in five minutes | +| [Semantic Conventions](./observability/semantic-conventions.md) | Full reference of every span and attribute Exgentic emits | + +## Maintainers + +| Document | Description | +|----------|-------------| +| [Releasing](./releasing.md) | Cut a release, publish to PyPI, and create a GitHub Release | + +--- + +## Other resources + +- [README.md](../README.md) — project overview, quick start, CLI reference, and available benchmarks/agents +- [DEVELOPMENT.md](../DEVELOPMENT.md) — local setup, running tests, linting, and the release process +- [CONTRIBUTING.md](../CONTRIBUTING.md) — contribution workflow, legal requirements, and PR guidelines diff --git a/labs/AgentStream/exgentic/docs/adding-agents.md b/labs/AgentStream/exgentic/docs/adding-agents.md new file mode 100644 index 00000000..2f6b3f2f --- /dev/null +++ b/labs/AgentStream/exgentic/docs/adding-agents.md @@ -0,0 +1,384 @@ +# Adding Agents + +This document defines the agent design principles for Exgentic. + +It is intentionally opinionated. An agent adapter should not just "work"; it should cleanly separate configuration from execution, isolate heavy dependencies, and adapt to any benchmark contract without requiring the benchmark to change. + +Use these existing adapters as reference points: +- `src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py` + `instance.py` (split pattern) +- `src/exgentic/agents/cli/claude/agent.py` (light pattern, single file) + +**Related docs:** +[docs/](./README.md) · [Adding Benchmarks](./adding-benchmarks.md) · [Custom Models](./custom-models.md) · [Runners](./runners.md) · [Replay Testing](./replay-testing.md) · [CONTRIBUTING.md](../CONTRIBUTING.md) + +## Core Principle + +The agent adapts to the benchmark contract, not the other way around. + +That means: +- the benchmark decides the task, context, actions, step flow, and scoring +- the agent receives these through `_get_instance_kwargs()` and must work within them +- the agent should not require benchmark modifications to function +- the agent should not impose protocol-specific assumptions on the benchmark + +The default goal should be the thinnest possible agent wrapper. + +That means: +- translate the benchmark's actions into whatever protocol your agent uses (tool calls, code generation, CLI commands) +- do not reshape the benchmark contract to match your model's preferred format +- keep the configuration surface small and explicit + +## Architecture + +Exgentic agents are split into two classes with distinct roles: + +### Agent (config, host-side) + +`Agent` is a lightweight Pydantic model that holds configuration. It lives on the host and is never sent into an isolated runner. It has no heavy dependencies. + +Responsibilities: +- declare `display_name` and `slug_name` as `ClassVar[str]` +- hold user-facing configuration fields (model name, max steps, feature flags) +- implement `_get_instance_class()` to resolve the execution class +- implement `_get_instance_kwargs()` to translate config + benchmark contract into constructor arguments +- optionally override `setup()` for non-pip setup (Docker builds, npm installs) +- optionally override `model_name` / `get_models_names()` for dashboard metadata + +### AgentInstance (execution, venv-side) + +`AgentInstance` is the execution class. It runs inside the runner (venv, Docker, or local) and may import heavy third-party libraries. + +Responsibilities: +- implement `react(observation) -> Action | None` as the core decision loop +- implement `close()` for resource cleanup +- optionally override `start()` for initialization that happens after construction +- optionally override `get_cost()` to report monetary cost + +The agent instance receives a single `session_id` in its constructor, which scopes all logs and artifacts. Additional kwargs come from `_get_instance_kwargs()`. + +## Key Pattern: Lazy Import for Dependency Isolation + +The `_get_instance_class()` classmethod must use a lazy import so that heavy dependencies are only loaded inside the runner environment, not on the host. + +```python +@classmethod +def _get_instance_class(cls): + from .instance import MyAgentInstance + + return MyAgentInstance +``` + +This is the same pattern that `Benchmark._get_session_class()` uses. It ensures the host process never imports libraries like `litellm`, `smolagents`, `openai`, or any other agent-specific SDK. + +## When to Split Files + +**Split into separate files** when your agent depends on heavy third-party libraries: + +``` +src/exgentic/agents/my_agent/ + __init__.py + my_agent.py # Agent subclass (light, no heavy imports) + instance.py # AgentInstance subclass (imports litellm, openai, etc.) + requirements.txt # Agent-specific pip dependencies + setup.sh # Optional non-pip setup + utils.py # Optional helpers +``` + +Examples: `litellm_tool_calling`, `smolagents`, `openai` + +**Keep everything in one file** when dependencies are light or already available in the base environment: + +``` +src/exgentic/agents/my_agent/ + __init__.py + agent.py # Both Agent and AgentInstance in one file +``` + +Example: `cli/claude` (the instance class is in the same file because it only depends on stdlib and core Exgentic types) + +The rule is simple: if importing the instance class would pull in packages that are not in the base `exgentic` install, split the files. + +## Required Methods + +### On the Agent class + +#### `_get_instance_class()` (classmethod, abstract) + +Returns the `AgentInstance` subclass. Must use a lazy import. + +```python +@classmethod +def _get_instance_class(cls): + from .instance import MyAgentInstance + + return MyAgentInstance +``` + +#### `_get_instance_kwargs()` (abstract) + +Translates the agent's configuration into constructor kwargs for the instance class. Task, context, and actions are passed separately via `start()`, not through the constructor. + +```python +def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "max_steps": self.max_steps, + } +``` + +The returned dict is passed directly to the instance class constructor. Every key must match a constructor parameter. + +### On the AgentInstance class + +#### `react(observation) -> Action | None` (abstract) + +The core decision loop. Receives an `Observation` (or `None` on the first call) and returns an `Action` to take, or `None` to signal that the agent is done. + +```python +def react(self, observation: Observation | None) -> Action | None: + # Process observation, decide next action + # Return None when the agent decides it is finished + ... +``` + +#### `close()` (abstract) + +Cleanup resources. Called when the session ends, whether or not the agent finished normally. + +```python +def close(self) -> None: + # Release connections, flush logs, etc. + pass +``` + +#### `start(task, context, actions)` (optional override) + +Called after construction but before the first `react()`. Receives the benchmark's task string, context dict, and list of action types. The base implementation stores these as `self.task`, `self.context`, and `self.actions`. Override to perform initialization that depends on these values (e.g., seeding a conversation with the task prompt). + +#### `get_cost()` (optional) + +Returns a `CostReport` with estimated monetary cost. Default returns an empty report. Override to track API costs. + +```python +def get_cost(self) -> CostReport: + return self._cost_data +``` + +## Registration + +Every agent must be registered in `src/exgentic/interfaces/registry.py` in the `AGENTS` dict. + +```python +AGENTS: dict[str, RegistryEntry] = { + # ...existing entries... + "my_agent": RegistryEntry( + slug_name="my_agent", + display_name="My Agent", + module="exgentic.agents.my_agent.my_agent", + attr="MyAgent", + kind="agent", + ), +} +``` + +Requirements: +- `slug_name` must match the `slug_name` ClassVar on the Agent class exactly +- `display_name` must match the `display_name` ClassVar on the Agent class exactly +- `module` is the dotted Python module path to the file containing the Agent class +- `attr` is the class name within that module +- `kind` must be `"agent"` + +The registry validates these constraints at load time. Mismatches will raise at startup. + +## Setup + +### `requirements.txt` + +List agent-specific pip dependencies. The runner installs these automatically into the isolated environment. + +``` +litellm>=1.50.0 +``` + +Place the file in the agent's package directory. The `RunnerMixin` auto-discovers it by walking up from the module file. + +### `setup.sh` + +Optional script for non-pip setup. Runs after dependencies are installed. + +```bash +#!/usr/bin/env bash +set -euo pipefail +# Build Docker images, install npm packages, download models, etc. +``` + +Place it next to the agent module. The `RunnerMixin` auto-discovers it. + +Both files are automatically found by the framework through `RunnerMixin.requirements_txt` and `RunnerMixin.setup_script`. No manual wiring is needed. + +## Recommended File Structure + +### Split pattern (heavy deps) + +``` +src/exgentic/agents/my_agent/ + __init__.py + my_agent.py # Agent subclass + instance.py # AgentInstance subclass + requirements.txt # e.g., litellm>=1.50.0 + setup.sh # optional + utils.py # optional helpers +``` + +**my_agent.py** (host-side, no heavy imports): + +```python +from __future__ import annotations + +from typing import Any, ClassVar + +from ...core.agent import Agent +from ...core.types import ActionType, ModelSettings + + +class MyAgent(Agent): + display_name: ClassVar[str] = "My Agent" + slug_name: ClassVar[str] = "my_agent" + + model: str = "gpt-4o" + max_steps: int = 100 + + @classmethod + def _get_instance_class(cls): + from .instance import MyAgentInstance + + return MyAgentInstance + + @property + def model_name(self) -> str: + return self.model + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "max_steps": self.max_steps, + } +``` + +**instance.py** (runner-side, may import heavy libs): + +```python +from __future__ import annotations + +from typing import Any, Optional + +import some_heavy_library # only loaded inside the runner + +from ...core.agent_instance import AgentInstance +from ...core.types import Action, ActionType, Observation + + +class MyAgentInstance(AgentInstance): + def __init__( + self, + session_id: str, + task: str, + context: dict[str, Any], + actions: list[ActionType], + model: str, + max_steps: int, + ): + super().__init__(session_id) + self.task = task + self.context = context + self.actions = actions + self.model = model + self.max_steps = max_steps + self._step_count = 0 + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + self._step_count += 1 + if self._step_count > self.max_steps: + return None + # Agent decision logic here + ... + + def close(self) -> None: + pass +``` + +### Light pattern (no heavy deps) + +``` +src/exgentic/agents/my_agent/ + __init__.py + agent.py # Both Agent and AgentInstance +``` + +Keep both classes in a single file when the instance has no third-party imports beyond what exgentic already provides. + +## Validation Checklist + +Before opening a PR for a new agent, validate all of the following. + +### Contract validation + +- Agent class declares `display_name` and `slug_name` as `ClassVar[str]` +- `_get_instance_class()` uses a lazy import +- `_get_instance_kwargs()` returns a dict whose keys match the instance constructor +- The instance implements `react()` and `close()` +- The instance calls `super().__init__(session_id)` in its constructor + +### Dependency isolation validation + +- The Agent file does not import heavy third-party libraries at module level +- Heavy imports only appear inside `_get_instance_class()` or in the instance module +- `requirements.txt` lists all agent-specific dependencies + +### Registry validation + +- `slug_name` in the registry entry matches the class `slug_name` exactly +- `display_name` in the registry entry matches the class `display_name` exactly +- `module` path resolves to the correct file +- `attr` matches the Agent class name +- `kind` is `"agent"` + +### Functional validation + +- Agent is discoverable through the registry (`load_agent("my_agent")` succeeds) +- Agent works with at least one benchmark end to end +- `react()` correctly returns `None` when the agent decides it is done +- `close()` does not raise +- `get_cost()` returns a valid `CostReport` + +### Quality validation + +- `py_compile` passes for all changed Python files +- `pre-commit` passes for changed files +- `git diff --check` passes + +## Practical Rule of Thumb + +When in doubt, ask: + +1. Does the agent adapt to the benchmark, or does it require the benchmark to change? +2. Are heavy dependencies isolated behind a lazy import? +3. Is the Agent file importable without installing agent-specific packages? +4. Does `_get_instance_kwargs()` faithfully pass the benchmark contract through? +5. Will this agent work with benchmarks that have very different action spaces? +6. Is the configuration surface minimal and explicit? + +If the answer to any of those is no, the adapter is probably too coupled or too leaky. + +--- + +## See also + +- [Adding Benchmarks](./adding-benchmarks.md) — the other side of the contract +- [Custom Models](./custom-models.md) — configuring LLM providers and sampling parameters for the `tool_calling` agent +- [Runners](./runners.md) — how setup.sh and requirements.txt are discovered and executed +- [Replay Testing](./replay-testing.md) — write end-to-end tests for your agent without API calls +- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR workflow and legal requirements +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/adding-benchmarks.md b/labs/AgentStream/exgentic/docs/adding-benchmarks.md new file mode 100644 index 00000000..4fd56287 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/adding-benchmarks.md @@ -0,0 +1,364 @@ +# Adding Benchmarks + +This document defines the benchmark design principles for Exgentic. + +It is intentionally opinionated. A benchmark adapter should not just "work"; it should preserve the benchmark's meaning while still fitting Exgentic's agent abstraction cleanly. + +Use these existing adapters as reference points: +- `src/exgentic/benchmarks/tau2/tau2_benchmark.py` +- `src/exgentic/benchmarks/bfcl/bfcl_benchmark.py` + +**Related docs:** +[docs/](./README.md) · [Adding Agents](./adding-agents.md) · [Runners](./runners.md) · [Replay Testing](./replay-testing.md) · [Output Format](./output-format.md) · [CONTRIBUTING.md](../CONTRIBUTING.md) + +## Core Principle + +The benchmark owns the contract. + +That means the benchmark decides: +- what the task is +- what context the agent receives +- what actions exist +- how steps progress +- when a session is finished +- how scoring works + +The agent should adapt to the benchmark contract through Exgentic's normal interfaces. The benchmark should not be shaped around one specific model protocol. + +The default goal should be the thinnest possible benchmark wrapper. + +That means: +- reuse the source benchmark wherever possible +- add only the translation layers that are actually necessary +- avoid reimplementing benchmark logic unless there is a clear reason +- avoid introducing runtime behavior that exists only to satisfy one agent or one model protocol + +The target is simple: +- make the benchmark accessible to any Exgentic agent +- while adding the minimum adapter surface necessary +- and without clashing with agent-specific assumptions + +## Principles + +### 1. Keep the agent-facing contract protocol-agnostic + +Do not define a benchmark in terms of OpenAI tool calls, raw assistant messages, or any other provider-specific response format. + +Define it in terms of: +- task semantics +- available actions +- observations +- finish conditions +- score + +Protocol-specific translation belongs in adapters, not in the benchmark contract. + +Bad: +- "The model must return all tool calls in one assistant message." + +Good: +- "The task is complete when the required actions have been taken and the benchmark-specific finish condition is met." + +### 2. The task should be the real task + +`task` should contain the actual task the agent is meant to solve. + +Do not wrap the task in fake chat scaffolding unless that scaffolding is genuinely part of the benchmark. + +If the benchmark is not about user interaction, do not invent a chat conversation just to make it look conversational. + +Bad: +- synthetic "user" messages when the benchmark is not actually testing user interaction +- generic wrapper prompts replacing the real benchmark task + +Good: +- the benchmark prompt itself is the `task` + +### 3. Context should contain only what the agent should know + +`context` is not a metadata dump. + +It should contain only information that is necessary for the agent to behave correctly on the task. + +Keep internal benchmark metadata out of `context`, including: +- subset names +- source dataset ids +- registry information +- adapter implementation details + +Good context: +- policy text +- execution constraints +- information the agent genuinely needs to act correctly + +Bad context: +- `"subset": "live_parallel_multiple"` +- `"benchmark": "bfcl"` + +### 4. Actions should represent semantic operations + +Actions are the benchmark's action space. + +Name and describe them in terms of what they do, not in terms of a transport protocol. + +Prefer "actions" over protocol-specific terms like "tool calls" in benchmark-facing language, because not all agents consume or produce actions through the same protocol. + +If the source benchmark exposes functions, commands, or tools, translate those into Exgentic actions at the boundary. + +### 5. Use `finish` only as part of the benchmark contract + +`finish` is valid when the benchmark needs an explicit end-of-step or end-of-task signal. + +It should exist because the benchmark contract needs it, not because a specific model API needs it. + +Use it when: +- the benchmark has multiple steps or turns and needs an explicit transition point +- the benchmark needs a clear "done with this step" signal +- the benchmark should allow completion without another normal action + +Do not force `finish` into a benchmark if the source benchmark's semantics are cleaner without it. + +### 6. Distinguish execution modes by contract, not by protocol + +If a benchmark has single-turn, live, or multi-turn variants, define those as execution contracts. + +The important differences are things like: +- whether more steps may follow +- whether the task ends after the current finish +- whether action outputs affect later state +- whether the benchmark continues after a step completes + +Do not define the mode in terms of how many assistant messages or tool-call payloads a model is allowed to emit. + +Important: +- single-turn does not necessarily mean a single action +- multi-action single-turn tasks are valid +- the distinction is about step structure, not about one specific model protocol + +### 7. Action outputs must be honest + +If the benchmark can produce real execution outputs, use them. + +If it cannot, do not fabricate realistic outputs that imply more runtime semantics than actually exist. + +Be explicit in the contract when actions are only being recorded rather than executed. + +Good: +- real execution results when the source benchmark exposes an official executor +- `Action recorded.` when there is no real runtime execution for that task family + +Bad: +- made-up outputs that look like real environment state changes when none were actually computed + +### 8. Reuse external harnesses as the source of truth where possible + +When adapting an external benchmark, prefer to reuse: +- dataset loading +- official assets +- ground-truth files +- official checkers or scorers +- official execution helpers + +Avoid copying large chunks of benchmark logic into Exgentic if the source repository already provides them. + +But there is an important boundary: +- external harnesses should be the source of truth for benchmark assets and scoring +- they should not automatically own the Exgentic runtime contract + +If the external harness assumes a model-specific interaction pattern, Exgentic should usually keep its own runtime and bridge to the harness at load/score time instead. + +When choosing between two valid integrations, prefer the thinner one. + +Use the more complex approach only when the thinner one would: +- distort benchmark meaning +- hard-code one agent's assumptions +- or force Exgentic to own logic that should stay with the source benchmark + +### 9. Be explicit about what is official and what is adapted + +If the adapter preserves official scoring but changes runtime behavior, document that clearly. + +If some subsets use official execution while others only use official scoring, document that too. + +Do not imply full equivalence when the integration is intentionally more abstract than the source benchmark. + +For each benchmark adapter, it should be easy to answer: +- What comes directly from the source benchmark? +- What is adapted by Exgentic? +- What is exact? +- What is approximate? + +### 10. Success, failure, and error must stay distinct + +Finished benchmark failures are not the same as runtime errors. + +The adapter should keep these states separate: +- success: benchmark completed and passed +- unsuccessful: benchmark completed and failed +- unfinished: benchmark did not complete +- error: adapter or runtime failure prevented a proper benchmark result + +Do not swallow real errors and report them as ordinary failures. + +If an exception happens, record it explicitly in session metadata. + +### 11. The benchmark should work for many agents, not just one + +A benchmark adapter should not depend on modifying one particular agent implementation. + +Prefer to build benchmark logic around Exgentic's shared abstractions: +- `task` +- `context` +- `actions` +- observations +- `Session.start()` +- `Session.step()` +- `Session.done()` +- `Session.score()` + +If the adapter only works because one agent has special behavior, the adapter is too coupled. + +### 12. Keep setup, runtime, and registration separate + +A well-structured benchmark adapter usually has three separate concerns: + +1. Setup +- external checkout or installation +- pinned dependencies +- benchmark-specific environment preparation + +2. Runtime +- session logic +- task loading +- action translation +- scoring + +3. Registration +- registry entry +- subset listing +- CLI discoverability + +Do not mix setup logic directly into the runtime path when it can be handled once in `setup.sh`. + +### 13. The main benchmark file must not import external dependencies + +The main benchmark file (`_benchmark.py`) defines the `Benchmark` subclass that Exgentic loads in the host process. This file **must be importable without any benchmark-specific dependencies installed**. + +External dependencies (benchmark harnesses, datasets, ML libraries, etc.) belong in **separate files** that are only loaded inside the runner subprocess through `_get_evaluator_class()` and `_get_session_class()`. + +**Rule:** The benchmark class file may only import from: +- Python standard library +- `pydantic` +- `exgentic` core modules + +All other imports must live in evaluator/session files that are accessed through the class getters. + +**Why:** Exgentic loads the benchmark class in the host process to read configuration (runner type, evaluator/session class names, kwargs). The actual benchmark execution happens inside an isolated runner (venv or Docker). If the main file imports heavy dependencies, the host process fails when those deps are only installed inside the runner environment. + +Bad: +```python +# _benchmark.py +from some_harness import HarnessRunner # ← breaks host import + +class MyBenchmark(Benchmark): + ... +``` + +Good: +```python +# _benchmark.py — no external deps +class MyBenchmark(Benchmark): + def _get_evaluator_class(self): + from ._eval import MyEvaluator # loaded inside runner + return MyEvaluator + +# _eval.py — external deps are fine here +from some_harness import HarnessRunner # ← only loaded in runner subprocess +``` + +## Required Structure + +For a benchmark package under `src/exgentic/benchmarks//`: + +- `_benchmark.py` **(required)** + - `Benchmark` subclass only + - no external dependency imports + - `_get_evaluator_class()` and `_get_session_class()` return classes from other files +- `_eval.py` or `_session.py` **(required if benchmark has external deps)** + - evaluator, session, and runtime logic + - may import external dependencies at module level + - only loaded inside the runner subprocess +- `setup.sh` + - benchmark installation/bootstrap +- optional shim module + - thin import boundary around an external harness +- optional helper modules + - action translation, scoring helpers, data parsing + +Then register it in: +- `src/exgentic/interfaces/registry.py` + +## Validation Checklist + +Before opening a PR for a new benchmark, validate all of the following. + +### Contract validation + +- `task` is the actual task, not fake wrapper chat +- `context` contains only agent-relevant information +- actions are semantically named +- `finish` exists only if the benchmark contract needs it +- success/failure/error semantics are distinct + +### Import validation + +- the main benchmark file (`_benchmark.py`) imports **no external dependencies** +- `_get_evaluator_class()` and `_get_session_class()` load from separate files +- `python -c "from exgentic.benchmarks.._benchmark import "` works without deps installed + +### Functional validation + +- benchmark is discoverable through the registry +- subsets list correctly +- tasks list correctly +- setup script works from a clean environment +- at least one happy-path task works end to end +- benchmark works with the default venv runner (not just direct) +- at least one failure-path task is represented correctly +- adapter errors surface as errors, not silent failures + +### Source-of-truth validation + +- official assets are reused where possible +- official scoring is reused where possible +- any remaining deviations from the source benchmark are documented explicitly + +### Quality validation + +- `py_compile` passes for changed Python files +- `pre-commit` passes for changed files +- `git diff --check` passes + +## Practical Rule Of Thumb + +When in doubt, ask: + +1. Is this benchmark contract describing the task, or just mirroring one model API? +2. Is this information something the agent should truly know? +3. Is this the thinnest adapter that still preserves the benchmark's meaning? +4. Am I reusing the source benchmark where it helps, without letting it dictate the wrong runtime shape? +5. Are the benchmark outputs honest about what was actually executed? +6. Will this adapter still make sense for a very different kind of Exgentic agent? + +If the answer to any of those is no, the adapter is probably too coupled or too misleading. + +--- + +## See also + +- [Adding Agents](./adding-agents.md) — the other side of the contract +- [Runners](./runners.md) — how setup.sh and requirements.txt are discovered and executed +- [Replay Testing](./replay-testing.md) — write end-to-end tests for your benchmark without API calls +- [Output Format](./output-format.md) — trajectory.jsonl and results.json schemas +- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR workflow and legal requirements +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/batch.md b/labs/AgentStream/exgentic/docs/batch.md new file mode 100644 index 00000000..86e19700 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/batch.md @@ -0,0 +1,269 @@ +# Batch Runs + +The `batch` commands let you manage large evaluations across multiple configurations — parameter sweeps, multi-benchmark comparisons, re-runs of failed sessions — without writing orchestration scripts. + +**Related docs:** +[docs/](./README.md) · [CLI Reference](./cli-reference.md) · [Python API](./python-api.md) · [Output Format](./output-format.md) + +--- + +## When to use batch vs evaluate + +| Scenario | Command | +|----------|---------| +| Single benchmark run | `exgentic evaluate` | +| Multiple models on one benchmark | `batch evaluate` with config files | +| Re-run only failed sessions | `batch evaluate` (skips completed by default) | +| Sweep temperature/model grids | `batch evaluate` + `batch patch` | +| Publish results to HuggingFace | `batch publish` | +| Export results to CSV | `batch extract` | + +--- + +## Config files + +Every batch command operates on **config files** — JSON files that describe a run. There are two kinds. + +### RunConfig + +Describes a full multi-task run. + +```json +{ + "benchmark": "tau2", + "agent": "tool_calling", + "subset": "retail", + "num_tasks": 10, + "model": "gpt-4o", + "benchmark_kwargs": { + "user_simulator_model": "gpt-4o" + }, + "agent_kwargs": { + "model_settings": { + "temperature": 0.2 + } + }, + "output_dir": "./outputs", + "max_steps": 100, + "max_actions": 100 +} +``` + +### SessionConfig + +Describes a single task. Used when you need per-task control or when replaying individual sessions. + +```json +{ + "benchmark": "tau2", + "agent": "tool_calling", + "task_id": "retail_1", + "subset": "retail", + "model": "gpt-4o", + "output_dir": "./outputs" +} +``` + +### Full field reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `benchmark` | string | required | Benchmark slug (e.g. `tau2`) | +| `agent` | string | required | Agent slug (e.g. `tool_calling`) | +| `subset` | string | null | Benchmark subset | +| `task_ids` | list[string] | null | Explicit task IDs to run | +| `num_tasks` | int | null | Number of tasks (randomly sampled if task_ids not set) | +| `task_id` | string | required (SessionConfig) | Single task ID | +| `model` | string | null | Model override | +| `output_dir` | string | `./outputs` | Where to write results | +| `cache_dir` | string | null | Cache directory | +| `run_id` | string | auto | Deterministic ID derived from config | +| `max_steps` | int | 100 | Steps per session | +| `max_actions` | int | 100 | Actions per session | +| `max_workers` | int | null | Parallel session workers | +| `overwrite_sessions` | bool | false | Re-run already-completed sessions | +| `benchmark_kwargs` | object | null | Extra kwargs passed to the benchmark | +| `agent_kwargs` | object | null | Extra kwargs passed to the agent | + +--- + +## Commands + +All batch commands accept one or more `--config` flags, each taking a file path or a glob pattern. + +```bash +exgentic batch --config path/to/config.json +exgentic batch --config "configs/*.json" +exgentic batch --config configs/run1.json --config configs/run2.json +``` + +--- + +### batch evaluate + +Run all configs sequentially, executing sessions and aggregating results. + +```bash +exgentic batch evaluate --config "configs/*.json" +``` + +Already-completed sessions are skipped unless `overwrite_sessions` is true in the config. This makes it safe to re-run after partial failures — only missing or failed sessions are executed. + +--- + +### batch execute + +Same as `batch evaluate` but skips the aggregation step. Use this when you want to run sessions and aggregate later. + +```bash +exgentic batch execute --config "configs/*.json" +exgentic batch aggregate --config "configs/*.json" # aggregate afterwards +``` + +--- + +### batch aggregate + +Aggregate results from already-completed sessions without running anything. + +```bash +exgentic batch aggregate --config "configs/*.json" +``` + +Useful when you have sessions from a previous run and want to recompute scores. + +--- + +### batch status + +Print a status table showing completion state for each config. + +```bash +exgentic batch status --config "configs/*.json" +``` + +--- + +### batch prepare + +Write session config files to disk without executing. Creates the session directory structure so you can inspect or modify configs before running. + +```bash +exgentic batch prepare --config run.json +exgentic batch prepare --config run.json --overwrite # overwrite existing session configs +``` + +--- + +### batch patch + +Modify existing run or session config files in bulk using dotted-key notation. + +```bash +# Preview what would change +exgentic batch patch --config "configs/*.json" \ + --set model=gpt-4o \ + --dry-run + +# Apply changes +exgentic batch patch --config "configs/*.json" \ + --set model=gpt-4o \ + --set agent_kwargs.model_settings.temperature=0.2 \ + --apply +``` + +Dotted paths are resolved into nested dicts. Values are parsed as JSON first; if that fails, treated as strings. This lets you do sweeps: + +```bash +# Change model across a whole grid of configs +exgentic batch patch --config "sweep_*.json" --set model=claude-3-5-sonnet-20241022 --apply +``` + +--- + +### batch extract + +Export results from multiple runs into a single CSV file. + +```bash +exgentic batch extract --config "configs/*.json" --output results.csv +exgentic batch extract --config "configs/*.json" --output - # print to stdout +``` + +Each row is one run. Columns include all `RunResults` fields (see [Output Format](./output-format.md)). + +--- + +### batch publish + +Push results to a [HuggingFace dataset](https://huggingface.co/docs/datasets/). + +```bash +exgentic batch publish \ + --config "configs/*.json" \ + --repo Exgentic/open-agent-leaderboard-results \ + --append +``` + +Flags: + +| Flag | Default | Description | +|------|---------|-------------| +| `--repo` | required | HuggingFace dataset repo ID | +| `--append` / `--overwrite` | `--append` | Append to or replace the existing dataset | +| `--private` / `--public` | `--private` | Dataset visibility | + +Deduplication: when appending, existing rows with the same `(benchmark, agent, model)` triple are replaced. New combinations are appended. + +Requires the `datasets` package (`pip install datasets`) and a HuggingFace token with write access: + +```bash +huggingface-cli login +# or +export HF_TOKEN=hf_... +``` + +--- + +## Typical workflows + +### Parameter sweep + +```bash +# Create one config per model +for model in gpt-4o claude-3-5-sonnet-20241022 gemini-2.0-flash; do + cp base_config.json "configs/${model}.json" + exgentic batch patch --config "configs/${model}.json" --set model=${model} --apply +done + +# Run all +exgentic batch evaluate --config "configs/*.json" + +# Export to CSV +exgentic batch extract --config "configs/*.json" --output sweep_results.csv +``` + +### Resume after partial failure + +```bash +# Just re-run — completed sessions are skipped automatically +exgentic batch evaluate --config "configs/*.json" +``` + +### Separate execute from aggregate + +```bash +# Run sessions in parallel across machines, then aggregate centrally +exgentic batch execute --config "configs/*.json" +# ... copy outputs to aggregation machine ... +exgentic batch aggregate --config "configs/*.json" +``` + +--- + +## See also + +- [CLI Reference](./cli-reference.md) — full flag reference for all commands +- [Output Format](./output-format.md) — RunResults schema, what batch extract produces +- [Python API](./python-api.md) — programmatic equivalents: `evaluate()`, `execute()`, `aggregate()` +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/cli-reference.md b/labs/AgentStream/exgentic/docs/cli-reference.md new file mode 100644 index 00000000..2c9235ba --- /dev/null +++ b/labs/AgentStream/exgentic/docs/cli-reference.md @@ -0,0 +1,323 @@ +# CLI Reference + +Complete reference for all `exgentic` CLI commands. + +**Related docs:** +[docs/](./README.md) · [Python API](./python-api.md) · [Batch Runs](./batch.md) · [Custom Models](./custom-models.md) · [Output Format](./output-format.md) + +--- + +## Global flags + +| Flag | Description | +|------|-------------| +| `--debug` | Enable debug logging | +| `--help` | Show help for any command | + +--- + +## Discovery + +### list benchmarks + +List all available benchmarks. + +```bash +exgentic list benchmarks +``` + +### list agents + +List all available agents. + +```bash +exgentic list agents +``` + +### list subsets + +List subsets for a benchmark. + +```bash +exgentic list subsets --benchmark tau2 +``` + +### list tasks + +List task IDs for a benchmark (or subset). + +```bash +exgentic list tasks --benchmark tau2 --subset retail +exgentic list tasks --benchmark tau2 --subset retail --limit 20 +``` + +| Flag | Description | +|------|-------------| +| `--benchmark` | Benchmark slug (required) | +| `--subset` | Subset name | +| `--limit` | Maximum tasks to show | + +--- + +## install + +Install a benchmark's or agent's dependencies (default: isolated venv). + +```bash +exgentic install --benchmark tau2 # install deps + data (default: venv) +exgentic install --agent tool_calling +exgentic install --benchmark tau2 --force # reinstall even if already set up +exgentic install --benchmark tau2 --docker # build Docker image +exgentic install --benchmark tau2 --local # install into local environment +``` + +| Flag | Description | +|------|-------------| +| `--benchmark` | Benchmark slug | +| `--agent` | Agent slug | +| `--force` | Force reinstall | +| `--docker` | Build a Docker image | +| `--local` | Install into the local environment instead of an isolated venv | + +See [Runners](./runners.md) for details on runner types. + +--- + +## uninstall + +Remove an installed benchmark's or agent's environment. + +```bash +exgentic uninstall --benchmark tau2 +exgentic uninstall --agent tool_calling +``` + +| Flag | Description | +|------|-------------| +| `--benchmark` | Benchmark slug | +| `--agent` | Agent slug | + +--- + +## setup (deprecated) + +> **Deprecated:** `exgentic setup` is an alias for `exgentic install` and will be removed in a future release. Use `install`/`uninstall` instead. + +--- + +## evaluate + +Run an evaluation end-to-end: execute sessions and aggregate results. + +```bash +exgentic evaluate \ + --benchmark tau2 \ + --agent tool_calling \ + --subset retail \ + --num-tasks 10 \ + --model gpt-4o \ + --set benchmark.user_simulator_model="gpt-4o" +``` + +| Flag | Description | +|------|-------------| +| `--benchmark` | Benchmark slug (required) | +| `--agent` | Agent slug (required) | +| `--subset` | Benchmark subset | +| `--task` | One or more specific task IDs (repeatable) | +| `--num-tasks` | Number of tasks to run | +| `--model` | Model override | +| `--max-steps` | Steps per session (default: 100) | +| `--max-actions` | Actions per session (default: 100) | +| `--max-workers` | Parallel session workers | +| `--overwrite` | Re-run already-completed sessions | +| `--output-dir` | Results output directory (default: `./outputs`) | +| `--run-id` | Override the auto-generated run ID | +| `--set KEY=VALUE` | Override any config field (repeatable) | +| `--debug` | Enable debug logging | + +### --set syntax + +`--set` accepts dotted key paths and JSON-compatible values: + +```bash +# Benchmark kwargs +--set benchmark.user_simulator_model="gpt-4o" +--set benchmark.runner=venv + +# Agent kwargs +--set agent.max_steps=200 + +# Model settings +--set agent.model.temperature=0.2 +--set agent.model.max_tokens=4096 +--set agent.model.top_p=0.9 +--set agent.model.reasoning_effort=high +--set agent.model.num_retries=3 +--set agent.model.retry_after=1.0 +--set agent.model.retry_strategy=constant +``` + +--- + +## status + +Show the execution status of a run (how many sessions are done, running, missing). + +```bash +exgentic status --benchmark tau2 --agent tool_calling --subset retail --num-tasks 10 +``` + +Accepts the same flags as `evaluate`. + +--- + +## preview + +Show which tasks would run without executing anything. + +```bash +exgentic preview --benchmark tau2 --agent tool_calling --subset retail --num-tasks 10 +``` + +Prints a plan showing which sessions would be new, which already exist, and which are currently running. + +--- + +## results + +Load and display results from a completed run. + +```bash +exgentic results --benchmark tau2 --agent tool_calling --subset retail --num-tasks 10 +``` + +Reads `results.json` from the run directory. Accepts the same config flags as `evaluate`. + +See [Output Format](./output-format.md) for the full results schema. + +--- + +## compare + +Statistical comparison between two run configurations. + +```bash +exgentic compare \ + --agents tool_calling openai_solo \ + --benchmark tau2 \ + --subset retail \ + --num-tasks 50 +``` + +Runs a Breslow-Day homogeneity test across subsets and reports whether the difference between agents is statistically significant. + +Requires the `analysis` extra: + +```bash +pip install "exgentic[analysis]" +``` + +--- + +## analyze + +Generate comparison plots for multiple benchmarks or agents. + +```bash +exgentic analyze \ + --agents tool_calling openai_solo \ + --benchmarks tau2 gsm8k \ + --output report.png +``` + +Requires the `analysis` extra: + +```bash +pip install "exgentic[analysis]" +``` + +--- + +## dashboard + +Launch the interactive web dashboard. + +```bash +exgentic dashboard +``` + +Opens a NiceGUI interface for exploring runs, browsing session trajectories, and monitoring live evaluations. + +--- + +## batch + +All batch subcommands. See [Batch Runs](./batch.md) for full documentation. + +```bash +exgentic batch evaluate --config "configs/*.json" +exgentic batch execute --config "configs/*.json" +exgentic batch aggregate --config "configs/*.json" +exgentic batch status --config "configs/*.json" +exgentic batch prepare --config run.json [--overwrite] +exgentic batch patch --config "configs/*.json" --set key=value [--apply | --dry-run] +exgentic batch extract --config "configs/*.json" --output results.csv +exgentic batch publish --config "configs/*.json" --repo org/dataset [--append | --overwrite] [--private | --public] +``` + +--- + +## Environment variables + +Exgentic reads the following environment variables. + +### Exgentic settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `EXGENTIC_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | +| `EXGENTIC_CACHE_DIR` | `.exgentic` | Cache directory for venvs and setup state | +| `EXGENTIC_DOTENV_PATH` | `.env` | Path to `.env` file loaded automatically | +| `EXGENTIC_OTEL_ENABLED` | `false` | Enable OpenTelemetry tracing | +| `EXGENTIC_OTEL_RECORD_CONTENT` | `false` | Include prompts/responses in traces (opt-in) | +| `EXGENTIC_LITELLM_CACHING` | `true` | Enable LiteLLM response caching | +| `EXGENTIC_LITELLM_CACHE_DIR` | `~/.cache/exgentic/litellm` | LiteLLM cache directory | +| `EXGENTIC_LITELLM_LOG_LEVEL` | `WARNING` | LiteLLM internal log level | + +### LLM provider credentials + +| Variable | Provider | +|----------|----------| +| `OPENAI_API_KEY` | OpenAI | +| `ANTHROPIC_API_KEY` | Anthropic | +| `AZURE_API_KEY` | Azure OpenAI | +| `AZURE_API_BASE` | Azure OpenAI endpoint | +| `AZURE_API_VERSION` | Azure OpenAI API version | +| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | AWS Bedrock | +| `AWS_REGION_NAME` | AWS Bedrock region | +| `VERTEXAI_PROJECT` / `VERTEXAI_LOCATION` | Google Vertex AI | +| `OPENAI_API_BASE` | Custom OpenAI-compatible endpoint | + +See [Custom Models](./custom-models.md) for full provider setup instructions. + +### OpenTelemetry + +| Variable | Description | +|----------|-------------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` or `grpc` | + +See [Observability Quick Start](./observability/quickstart.md) for tracing setup. + +--- + +## See also + +- [Python API](./python-api.md) — programmatic equivalents of all CLI commands +- [Batch Runs](./batch.md) — detailed guide for batch commands +- [Custom Models](./custom-models.md) — LLM provider and `--set agent.model.*` reference +- [Runners](./runners.md) — `--set benchmark.runner=*` options +- [Output Format](./output-format.md) — what `results` and `extract` produce +- [Observability Quick Start](./observability/quickstart.md) — tracing setup +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/custom-models.md b/labs/AgentStream/exgentic/docs/custom-models.md new file mode 100644 index 00000000..f81be751 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/custom-models.md @@ -0,0 +1,250 @@ +# Custom Models + +Exgentic routes all LLM calls through [LiteLLM](https://docs.litellm.ai/), which means any provider or deployment LiteLLM supports works out of the box — no code changes required. You pick the model, supply credentials, and optionally tune sampling parameters. + +**Related docs:** +[docs/](./README.md) · [CLI Reference](./cli-reference.md) · [Python API](./python-api.md) · [Adding Agents](./adding-agents.md) · [Observability Quick Start](./observability/quickstart.md) + +--- + +## Model string format + +The `--model` flag (and the `model` parameter in the Python API) accepts any model string that LiteLLM recognises. The general pattern is: + +``` +/ +``` + +For OpenAI-native models the provider prefix is optional: + +```bash +# These are equivalent +--model gpt-4o +--model openai/gpt-4o +``` + +For every other provider the prefix is required. See the provider examples below. + +--- + +## Supported providers + +### OpenAI + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model gpt-4o +``` + +Required environment variable: + +```bash +export OPENAI_API_KEY=sk-... +``` + +### Anthropic + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model claude-3-5-sonnet-20241022 +``` + +Required environment variable: + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +``` + +### Azure OpenAI + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model azure/ +``` + +Required environment variables: + +```bash +export AZURE_API_KEY=... +export AZURE_API_BASE=https://.openai.azure.com +export AZURE_API_VERSION=2024-02-01 # or whichever version your deployment uses +``` + +### AWS Bedrock + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0 +``` + +Required environment variables: + +```bash +export AWS_ACCESS_KEY_ID=... +export AWS_SECRET_ACCESS_KEY=... +export AWS_REGION_NAME=us-east-1 +``` + +### Google Vertex AI + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model vertex_ai/gemini-1.5-pro +``` + +Required environment variables: + +```bash +export VERTEXAI_PROJECT=my-gcp-project +export VERTEXAI_LOCATION=us-central1 +``` + +### Ollama (local) + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model ollama/llama3 +``` + +Required: Ollama running locally. Set the base URL if it differs from the default: + +```bash +export OLLAMA_API_BASE=http://localhost:11434 # default; only needed if different +``` + +### Any OpenAI-compatible endpoint + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model openai/ +``` + +Override the base URL: + +```bash +export OPENAI_API_BASE=http://localhost:8080/v1 +export OPENAI_API_KEY=any-non-empty-string # required by the client even if unused +``` + +This works with vLLM, LM Studio, LocalAI, Together AI, Fireworks, Anyscale, and any other OpenAI-compatible server. + +### LiteLLM proxy + +If you run a [LiteLLM proxy server](https://docs.litellm.ai/docs/proxy/quick_start) in front of your models: + +```bash +exgentic evaluate --benchmark gsm8k --agent tool_calling \ + --model openai/ +``` + +```bash +export OPENAI_API_BASE=http://localhost:4000 +export OPENAI_API_KEY= +``` + +--- + +## Sampling parameters + +Use `--set agent.model.*` to control sampling. These map to `ModelSettings` and are forwarded to LiteLLM on every completion call. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--set agent.model.temperature` | float ≥ 0 | `1.0` | Sampling temperature | +| `--set agent.model.top_p` | float 0–1 | `null` | Nucleus sampling | +| `--set agent.model.max_tokens` | int ≥ 0 | `null` | Maximum tokens in response | +| `--set agent.model.reasoning_effort` | string | `null` | Reasoning effort level (o1/o3 models) | +| `--set agent.model.num_retries` | int ≥ 0 | `5` | Retries on transient errors | +| `--set agent.model.retry_after` | float ≥ 0 | `0.5` | Initial retry delay in seconds | +| `--set agent.model.retry_strategy` | string | `exponential_backoff` | `exponential_backoff` or `constant` | + +Example — lower temperature and capped output: + +```bash +exgentic evaluate --benchmark tau2 --agent tool_calling \ + --model gpt-4o \ + --set benchmark.user_simulator_model="gpt-4o" \ + --set agent.model.temperature=0.2 \ + --set agent.model.max_tokens=2048 +``` + +--- + +## Python API + +```python +from exgentic import evaluate +from exgentic.core.types import ModelSettings + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=5, + model="azure/my-gpt-4o-deployment", + agent_kwargs={ + "model_settings": ModelSettings( + temperature=0.2, + max_tokens=2048, + num_retries=3, + ) + }, + benchmark_kwargs={"user_simulator_model": "gpt-4o"}, +) +``` + +--- + +## Reasoning models + +For models that support reasoning effort (OpenAI o1, o3, etc.): + +```bash +exgentic evaluate --benchmark swebench --agent tool_calling \ + --model o3 \ + --set agent.model.reasoning_effort=high \ + --set agent.model.max_tokens=32768 +``` + +Note: temperature is typically fixed at 1 for reasoning models and will be ignored if set. + +--- + +## Cost tracking + +Exgentic records token counts and estimated cost for every LiteLLM completion automatically. Results appear in: + +- `outputs//results.json` — aggregate cost across all sessions +- `outputs//sessions//results.json` — per-session cost + +Cost estimates are calculated using LiteLLM's built-in pricing database. For providers or custom deployments not in the database, cost will show as `0`. + +--- + +## Caching + +LiteLLM-level response caching is enabled by default. To disable it for a run: + +```bash +export EXGENTIC_LITELLM_CACHING=false +``` + +The cache directory defaults to `.litellm_cache` in the working directory. To move it: + +```bash +export EXGENTIC_LITELLM_CACHE_DIR=/path/to/cache +``` + +--- + +## Observability + +All LLM inference calls emit OpenTelemetry spans automatically when tracing is enabled. See [Observability Quick Start](./observability/quickstart.md) to set up tracing, and [Semantic Conventions](./observability/semantic-conventions.md) for the full attribute reference. + +--- + +## Further reading + +- [LiteLLM providers documentation](https://docs.litellm.ai/docs/providers) +- [Adding a new agent adapter](./adding-agents.md) — relevant when wrapping a framework that manages its own LLM calls +- [Observability Quick Start](./observability/quickstart.md) diff --git a/labs/AgentStream/exgentic/docs/huggingface.md b/labs/AgentStream/exgentic/docs/huggingface.md new file mode 100644 index 00000000..05ce9104 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/huggingface.md @@ -0,0 +1,35 @@ +# HuggingFace + +## Using HuggingFace Models + +Set your HF token and use the `huggingface///` model string format: + +```bash +export HF_TOKEN=hf_... +``` + +```bash +exgentic evaluate \ + --benchmark gsm8k \ + --agent tool_calling \ + --model huggingface/together/meta-llama/Llama-3.1-70B-Instruct +``` + +LiteLLM routes the call through HuggingFace's inference providers (billed to your HF account). Supported providers include `together`, `sambanova`, and others. Tool calling support depends on the provider and model. + +## Running on HuggingFace Jobs + +HuggingFace Jobs run containerized workloads on HF infrastructure (requires Pro/Team/Enterprise). + +```bash +hf jobs run astral-sh/uv:python3.12-bookworm sh -c " + uvx exgentic evaluate \ + --benchmark gsm8k \ + --agent tool_calling \ + --model huggingface/together/meta-llama/Llama-3.1-70B-Instruct \ + --output-dir /tmp/outputs && + uvx exgentic batch publish --repo-id your-org/eval-results /tmp/outputs +" --env HF_TOKEN=hf_... +``` + +Results are published to `https://huggingface.co/datasets/your-org/eval-results`. diff --git a/labs/AgentStream/exgentic/docs/observability/quickstart.md b/labs/AgentStream/exgentic/docs/observability/quickstart.md new file mode 100644 index 00000000..cfe52cde --- /dev/null +++ b/labs/AgentStream/exgentic/docs/observability/quickstart.md @@ -0,0 +1,151 @@ +# Observability Quick Start + +This guide gets you from zero to traces in five minutes using Jaeger as a local trace collector. + +For a full reference of every attribute Exgentic emits, see [Semantic Conventions](./semantic-conventions.md). + +--- + +## Prerequisites + +- Docker or Podman installed and running +- `exgentic` installed with the `otel` extra (see below) + +--- + +## Step 1 — Install the OTEL extra + +```bash +uv sync --extra otel +``` + +--- + +## Step 2 — Start Jaeger + +```bash +# Using Docker (or replace 'docker' with 'podman') +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + jaegertracing/all-in-one:latest +``` + +Default ports: + +| Port | Service | +|-------|--------------| +| 16686 | Jaeger UI | +| 4317 | OTLP gRPC | +| 4318 | OTLP HTTP | + +--- + +## Step 3 — Configure environment variables + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # or 'grpc' for port 4317 +export EXGENTIC_OTEL_ENABLED=true +``` + +To include task prompts, tool arguments, and LLM messages in traces (opt-in — may contain sensitive data): + +```bash +export EXGENTIC_OTEL_RECORD_CONTENT=true +``` + +--- + +## Step 4 — Set up and run an evaluation + +```bash +exgentic install --agent tool_calling +exgentic install --benchmark tau2 + +exgentic evaluate \ + --benchmark tau2 \ + --agent tool_calling \ + --model gpt-4o \ + --set benchmark.user_simulator_model="gpt-4o" \ + --task 1 \ + --max-steps 10 +``` + +--- + +## Step 5 — View traces + +Open [http://localhost:16686](http://localhost:16686), select the `exgentic` service, and click **Find Traces**. + +--- + +## Exporting traces + +### Via the Jaeger UI + +1. Open a trace. +2. Click the **JSON** button in the top-right corner. + +### Via the API + +```bash +# All recent traces +curl "http://localhost:16686/api/traces?service=exgentic&limit=100" | jq '.' > traces.json + +# A specific trace +curl "http://localhost:16686/api/traces/" | jq '.' > trace.json +``` + +--- + +## Troubleshooting + +### No traces appearing + +1. Verify environment variables are set: `env | grep OTEL` +2. Confirm Jaeger is running: `docker ps | grep jaeger` +3. Check Jaeger logs: `docker logs jaeger` +4. Ensure the evaluation completed successfully before looking for traces + +### Traces are incomplete + +- Wait a few seconds after the evaluation finishes — spans are flushed asynchronously +- Check session logs for OTEL-related errors +- Verify network connectivity to Jaeger + +### Jaeger not starting + +```bash +# Check for an existing container +docker ps -a | grep jaeger + +# Stop and remove, then restart +docker stop jaeger && docker rm jaeger +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 -p 4317:4317 -p 4318:4318 \ + jaegertracing/all-in-one:latest +``` + +--- + +## Cleanup + +```bash +docker stop jaeger +docker rm jaeger + +# Optional: remove the image +docker rmi jaegertracing/all-in-one:latest +``` + +--- + +## Further reading + +- [Semantic Conventions](./semantic-conventions.md) — full attribute reference +- [Jaeger documentation](https://www.jaegertracing.io/docs/) +- [OpenTelemetry GenAI conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) diff --git a/labs/AgentStream/exgentic/docs/observability/semantic-conventions.md b/labs/AgentStream/exgentic/docs/observability/semantic-conventions.md new file mode 100644 index 00000000..92384e25 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/observability/semantic-conventions.md @@ -0,0 +1,172 @@ +# Semantic Conventions + +This document maps Exgentic's core types to [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). It reflects the actual implementation in `src/exgentic/observers/handlers/otel.py` and `src/exgentic/integrations/litellm/trace_logger.py`. + +For setup instructions, see [Quick Start](./quickstart.md). + +--- + +## Span hierarchy + +``` +Session Span (ROOT) +├── execute_tool initial_observation +├── chat {model} ← LLM inference +├── execute_tool {tool_name} +├── chat {model} ← LLM inference +├── execute_tool {tool_name} +└── ... ← continues until session ends +``` + +--- + +## Attribute reference + +The table below documents every attribute actually emitted by the implementation, organised by span type. + +| Span type | OTel attribute | Exgentic source | Type | Requirement | Content-filtered | Notes | +|-----------|---------------|-----------------|------|-------------|-----------------|-------| +| **Session (ROOT)** | `exgentic.benchmark.slug_name` | `BenchmarkEntry.slug_name` | string | Custom | No | Heritable | +| **Session (ROOT)** | `exgentic.benchmark.subset` | `RunConfig.subset` | string | Custom | No | Heritable | +| **Session (ROOT)** | `exgentic.benchmark.agent.name` | `AgentEntry.display_name` | string | Custom | No | Heritable | +| **Session (ROOT)** | `exgentic.agent.slug` | `RunConfig.agent` | string | Custom | No | Heritable | +| **Session (ROOT)** | `exgentic.run.id` | `Context.run_id` | string | Custom | No | Heritable | +| **Session (ROOT)** | `gen_ai.request.model` | `RunConfig.model` | string | Recommended | No | Heritable; set when model is known at run start | +| **Session (ROOT)** | `gen_ai.conversation.id` | `Session.session_id` | string | Recommended | No | Heritable; primary correlation attribute | +| **Session (ROOT)** | `exgentic.session.id` | `Session.session_id` | string | Custom | No | Heritable; kept for backwards compatibility | +| **Session (ROOT)** | `exgentic.session.task_id` | `Session.task_id` | string | Custom | No | | +| **Session (ROOT)** | `exgentic.session.task` | `Session.task` | string | Opt-in | **Yes** | Task prompt; requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | +| **Session (ROOT)** | `exgentic.session.action.{name}.name` | `ActionType.name` | string | Custom | No | One entry per action in `Session.actions` | +| **Session (ROOT)** | `exgentic.session.action.{name}.description` | `ActionType.description` | string | Custom | No | | +| **Session (ROOT)** | `exgentic.session.action.{name}.is_message` | `ActionType.is_message` | bool | Custom | No | | +| **Session (ROOT)** | `exgentic.session.action.{name}.is_finish` | `ActionType.is_finish` | bool | Custom | No | | +| **Session (ROOT)** | `exgentic.context.{key}` | `Session.context[key]` | string | Custom | No | One entry per context key | +| **Session (ROOT)** | `exgentic.session.agent.id` | `AgentInstance.agent_id` | string | Custom | No | | +| **Session (ROOT)** | `exgentic.session.agent.path` | `AgentInstance.paths.agent_dir` | string | Custom | No | | +| **Session (ROOT)** | `exgentic.score.success` | `SessionScore.success` | bool | Custom | No | Set on session close | +| **Session (ROOT)** | `exgentic.score` | `SessionScore.score` | float | Custom | No | Set on session close | +| **Session (ROOT)** | `exgentic.score.is_finished` | `SessionScore.is_finished` | bool | Custom | No | Set on session close | +| **Session (ROOT)** | `exgentic.session.steps` | step counter | int | Custom | No | Set on session close | +| **Session (ROOT)** | `exgentic.agent.agent_cost` | `AgentInstance.get_cost()` | string (JSON) | Custom | No | Set on session close | +| **Session (ROOT)** | `exgentic.session.cost` | `Session.get_cost()` | string (JSON) | Custom | No | Set on session close | +| **execute_tool** | `gen_ai.operation.name` | `"execute_tool"` | string | Required | No | Constant value | +| **execute_tool** | `gen_ai.tool.name` | `Action.name` | string | Required | No | | +| **execute_tool** | `gen_ai.tool.id` | `Action.id` | string | Recommended | No | | +| **execute_tool** | `gen_ai.tool.description` | `ActionType.description` | string | Recommended | No | Looked up from `Session.actions` | +| **execute_tool** | `gen_ai.tool.parameters` | `Action.arguments` | string (JSON) | Opt-in | **Yes** | Requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | +| **execute_tool** | `gen_ai.tool.result` | `Observation` | string | Opt-in | **Yes** | Requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | +| **execute_tool** | `gen_ai.conversation.id` | `Session.session_id` | string | Recommended | No | Inherited from session span | +| **LLM inference** | `gen_ai.operation.name` | `"chat"` or `"text_completion"` | string | Required | No | | +| **LLM inference** | `gen_ai.provider.name` | `litellm_params.custom_llm_provider` | string | Required | No | Mapped to standard provider names | +| **LLM inference** | `gen_ai.request.model` | `LitellmKwargs.model` | string | Required | No | | +| **LLM inference** | `error.type` | exception class name | string | Required | No | Set on failure | +| **LLM inference** | `gen_ai.conversation.id` | `Context.session_id` | string | Recommended | No | | +| **LLM inference** | `gen_ai.request.max_tokens` | `optional_params.max_tokens` | int | Recommended | No | | +| **LLM inference** | `gen_ai.request.temperature` | `optional_params.temperature` | float | Recommended | No | | +| **LLM inference** | `gen_ai.request.top_p` | `optional_params.top_p` | float | Recommended | No | | +| **LLM inference** | `gen_ai.request.top_k` | `optional_params.top_k` | float | Recommended | No | | +| **LLM inference** | `gen_ai.request.frequency_penalty` | `optional_params.frequency_penalty` | float | Recommended | No | | +| **LLM inference** | `gen_ai.request.presence_penalty` | `optional_params.presence_penalty` | float | Recommended | No | | +| **LLM inference** | `gen_ai.request.stop_sequences` | `optional_params.stop` | string[] | Recommended | No | | +| **LLM inference** | `gen_ai.request.choice.count` | `optional_params.n` | int | Required | No | Only when `n != 1` | +| **LLM inference** | `gen_ai.request.seed` | `optional_params.seed` | int | Required | No | | +| **LLM inference** | `gen_ai.response.id` | `ResponseObject.id` | string | Recommended | No | | +| **LLM inference** | `gen_ai.response.model` | `ResponseObject.model` | string | Recommended | No | Actual model resolved by the provider | +| **LLM inference** | `gen_ai.usage.input_tokens` | `usage.prompt_tokens` | int | Recommended | No | | +| **LLM inference** | `gen_ai.usage.output_tokens` | `usage.completion_tokens` | int | Recommended | No | | +| **LLM inference** | `gen_ai.response.finish_reasons` | `choices[*].finish_reason` | string[] | Recommended | No | | +| **LLM inference** | `gen_ai.tool.definitions` | `LitellmKwargs.tools` | string (JSON) | Opt-in | **Yes** | Requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | +| **LLM inference** | `gen_ai.input.messages` | `LitellmKwargs.messages` | string (JSON) | Opt-in | **Yes** | Requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | +| **LLM inference** | `gen_ai.output.messages` | `choices[*].message` | string (JSON) | Opt-in | **Yes** | Requires `EXGENTIC_OTEL_RECORD_CONTENT=true` | + +--- + +## Span details + +### Session span (ROOT) + +- **Name**: `{benchmark_name} {subset} session` +- **Kind**: `INTERNAL` +- **Opened**: `OtelTracingObserver.on_session_creation` +- **Closed**: `OtelTracingObserver.on_session_success` or `on_session_error` + +### execute_tool span + +- **Name**: `execute_tool {tool_name}` or `execute_tool initial_observation` +- **Kind**: `CLIENT` +- **Opened**: `OtelTracingObserver.on_session_start` (initial), `on_react_success`, or `on_react_error` +- **Closed**: `OtelTracingObserver.on_step_success` or `on_step_error` +- **Reference**: [OTel GenAI execute_tool span](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span) + +### LLM inference span + +- **Name**: `{operation} {model}` (e.g., `chat gpt-4o`) +- **Kind**: `CLIENT` +- **Opened/Closed**: `TraceLogger._write_otel` (LiteLLM callback) +- **Parent**: session span (via OTEL context propagation) +- **Reference**: [OTel GenAI inference span](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#inference) + +--- + +## Attribute inheritance + +The following attributes are set on the session span and automatically propagated to all child spans via `SessionSpanManager.set_heritable_attribute()`: + +| Attribute | Source | +|-----------|--------| +| `gen_ai.conversation.id` | `Session.session_id` — primary correlation key | +| `exgentic.session.id` | `Session.session_id` — backwards compatibility | +| `gen_ai.request.model` | `RunConfig.model` (when available) | +| `exgentic.run.id` | `Context.run_id` | +| `exgentic.benchmark.slug_name` | `BenchmarkEntry.slug_name` | +| `exgentic.benchmark.subset` | `RunConfig.subset` | +| `exgentic.benchmark.agent.name` | `AgentEntry.display_name` | +| `exgentic.agent.slug` | `RunConfig.agent` | + +--- + +## Content filtering + +Attributes marked **Yes** in the content-filtered column contain user data (prompts, tool arguments, model responses). They are **not recorded by default** and must be explicitly enabled: + +```bash +export EXGENTIC_OTEL_RECORD_CONTENT=true +``` + +Attributes that are never filtered include all IDs, names, counters, scores, and static schemas — only runtime user content requires opt-in. + +--- + +## Implementation notes + +### Model name resolution + +Because `AgentInstance` does not expose model settings, the model name is extracted from `RunConfig` at run start: + +```python +model_name = run_config.model or (run_config.agent_kwargs or {}).get("model") +``` + +### Cost attributes + +`LiteLLMCostReport` and `UpdatableCostReport` are serialized to JSON strings for OTEL compatibility: + +- `exgentic.agent.agent_cost` — agent-level cost report +- `exgentic.session.cost` — full session cost report + +### LLM span parent context + +LLM inference spans are created inside the LiteLLM callback and attached to the session span via OTEL context propagation: + +1. The session span manager writes the current OTEL context into the `Context` ContextVar via `update_tracing_context()`. +2. The LiteLLM trace logger reads the OTEL context from that ContextVar. +3. LLM spans are created with the session span as their parent using `_get_parent_context()`. + +--- + +## References + +- [OTel GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) +- [execute_tool span spec](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span) +- [Inference span spec](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#inference) +- [Quick Start](./quickstart.md) diff --git a/labs/AgentStream/exgentic/docs/observers.md b/labs/AgentStream/exgentic/docs/observers.md new file mode 100644 index 00000000..bd29313b --- /dev/null +++ b/labs/AgentStream/exgentic/docs/observers.md @@ -0,0 +1,221 @@ +# Observers and Controllers + +Observers let you hook into the evaluation lifecycle to add custom logging, monitoring, alerting, or analytics — without modifying benchmarks or agents. Controllers extend this with the ability to stop a run early. + +**Related docs:** +[docs/](./README.md) · [Python API](./python-api.md) · [Output Format](./output-format.md) · [Observability](./observability/quickstart.md) + +--- + +## Observer interface + +All observers extend `exgentic.core.orchestrator.observer.Observer`. Every method has a default no-op implementation, so you only override what you need. + +```python +from exgentic.core.orchestrator.observer import Observer + +class Observer: + # Run-level callbacks + def on_run_start(self, run_config) -> None: ... + def on_run_success(self, results, run_config) -> None: ... + def on_run_error(self, error) -> None: ... + + # Session-level callbacks + def on_session_creation(self, session) -> None: ... + def on_session_start(self, session, agent, observation) -> None: ... + def on_session_scoring(self, session) -> None: ... + def on_session_success(self, session, score, agent) -> None: ... + def on_session_error(self, session, error) -> None: ... + def on_session_reuse(self, task_result) -> None: ... + + # Step-level: agent.react() returned an action + def on_react_success(self, session, action) -> None: ... + def on_react_error(self, session, error) -> None: ... + + # Step-level: session.step(action) returned an observation + def on_step_success(self, session, observation) -> None: ... + def on_step_error(self, session, error) -> None: ... +``` + +### Callback parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `run_config` | `RunConfig` | The run configuration | +| `results` | `RunResults` | Aggregated results (available in `on_run_success`) | +| `session` | `Session` | Session object with `session_id`, `task_id`, `paths` | +| `agent` | `Agent` | Agent config object with `get_cost()` | +| `observation` | `Observation \| None` | Observation returned by the benchmark (None on first step) | +| `action` | `Action \| None` | Action returned by the agent (None if agent is done) | +| `score` | `SessionScore` | Score with `score`, `success`, `is_finished`, `session_metrics`, `session_metadata` | +| `task_result` | `SessionResults` | Results for a session that was skipped/reused from cache | +| `error` | `Exception` | The exception that occurred | + +--- + +## Using observers + +Pass observers to `evaluate()`, `execute()`, or `aggregate()`: + +```python +from exgentic import evaluate + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + num_tasks=10, + observers=[MyObserver()], +) +``` + +Multiple observers are supported: + +```python +observers=[LoggingObserver(), MetricsObserver(), AlertObserver()] +``` + +--- + +## Examples + +### Print session results as they complete + +```python +from exgentic.core.orchestrator.observer import Observer + +class PrintObserver(Observer): + def on_session_success(self, session, score, agent): + status = "PASS" if score.success else "FAIL" + print(f"[{status}] {session.task_id} score={score.score:.2f} cost=${agent.get_cost().total_cost:.4f}") + + def on_session_error(self, session, error): + print(f"[ERROR] {session.task_id} {type(error).__name__}: {error}") +``` + +### Track costs in real time + +```python +from exgentic.core.orchestrator.observer import Observer + +class CostTracker(Observer): + def __init__(self): + self.total_cost = 0.0 + + def on_session_success(self, session, score, agent): + self.total_cost += agent.get_cost().total_cost + print(f"Running total: ${self.total_cost:.4f}") +``` + +### Write a custom log file + +```python +import json +from pathlib import Path +from exgentic.core.orchestrator.observer import Observer + +class JsonlLogger(Observer): + def __init__(self, path: str): + self.path = Path(path) + + def on_session_success(self, session, score, agent): + entry = { + "session_id": session.session_id, + "task_id": session.task_id, + "success": score.success, + "score": score.score, + } + with self.path.open("a") as f: + f.write(json.dumps(entry) + "\n") +``` + +### Send a Slack alert on failure + +```python +import requests +from exgentic.core.orchestrator.observer import Observer + +class SlackAlerter(Observer): + def __init__(self, webhook_url: str): + self.webhook_url = webhook_url + + def on_session_error(self, session, error): + requests.post(self.webhook_url, json={ + "text": f":x: Session `{session.task_id}` failed: `{error}`" + }) +``` + +--- + +## Controllers + +Controllers extend observers with the ability to raise errors that stop the run. Use them to implement early stopping — e.g. abort if too many consecutive failures occur. + +```python +from exgentic.core.orchestrator.observer import Observer + +class EarlyStopController(Observer): + def __init__(self, max_failures: int = 3): + self.max_failures = max_failures + self.failures = 0 + + def on_session_error(self, session, error): + self.failures += 1 + if self.failures >= self.max_failures: + raise RuntimeError(f"Stopping: {self.failures} consecutive session errors") +``` + +Pass controllers via the `controllers` parameter: + +```python +results = evaluate( + benchmark="tau2", + agent="tool_calling", + num_tasks=50, + controllers=[EarlyStopController(max_failures=5)], +) +``` + +--- + +## Built-in observers + +Exgentic uses these observers internally. They run automatically — you do not need to register them. + +| Observer | What it does | +|----------|-------------| +| `ResultsObserver` | Writes `trajectory.jsonl` and `results.json` for every session; computes `RunResults` | +| `LoggerObserver` | Logs run progress to the console | +| `FileLoggerObserver` | Writes `run.log` and per-session logs | +| `OtelTracingObserver` | Emits OpenTelemetry spans (active when `EXGENTIC_OTEL_ENABLED=true`) | +| `DashboardEventsObserver` | Streams events to the live dashboard | +| `WarningsObserver` | Captures and writes `warnings.log` | +| `RecapObserver` | Prints a summary table at the end of a run | + +--- + +## Thread safety + +Observers may be called from multiple threads when `max_workers > 1`. If your observer maintains shared state (counters, file handles, accumulators), protect it with a lock: + +```python +import threading +from exgentic.core.orchestrator.observer import Observer + +class ThreadSafeCounter(Observer): + def __init__(self): + self._lock = threading.Lock() + self.count = 0 + + def on_session_success(self, session, score, agent): + with self._lock: + self.count += 1 +``` + +--- + +## See also + +- [Python API](./python-api.md) — how to pass observers to `evaluate()` +- [Observability Quick Start](./observability/quickstart.md) — OpenTelemetry tracing (built-in observer) +- [Output Format](./output-format.md) — the data that `ResultsObserver` writes +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/output-format.md b/labs/AgentStream/exgentic/docs/output-format.md new file mode 100644 index 00000000..8d7e9eb7 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/output-format.md @@ -0,0 +1,325 @@ +# Output Format + +Every evaluation writes structured results to a directory under `outputs//`. This document covers the file layout and the schema of every results file. + +**Related docs:** +[docs/](./README.md) · [Python API](./python-api.md) · [Batch Runs](./batch.md) · [CLI Reference](./cli-reference.md) + +--- + +## Directory layout + +``` +outputs// +├── results.json # Aggregated run-level results +├── benchmark_results.json # Benchmark-specific aggregated results +├── run/ +│ ├── config.json # Snapshot of RunConfig used for this run +│ ├── run.log # Main execution log +│ └── warnings.log # Warnings captured during execution +└── sessions// + ├── config.json # SessionConfig for this task + ├── results.json # Session-level results + ├── trajectory.jsonl # One JSON line per step (action + observation) + ├── agent/ + │ └── agent.log # Agent execution log + └── benchmark/ + ├── results.json # Benchmark-specific session results + └── session.log # Benchmark session log +``` + +### run_id and session_id + +Both IDs are deterministic SHA256 hashes of the configuration: + +- `run_id` — 12-character prefix of the hash of `(benchmark, agent, subset, model, benchmark_kwargs, agent_kwargs)` — everything except the task list. +- `session_id` — 8-character prefix of the hash of `(benchmark, agent, subset, task_id, model, benchmark_kwargs, agent_kwargs)` — includes the task, so each task gets a stable ID. + +This means re-running the same config produces the same IDs. Completed sessions are skipped unless `overwrite_sessions` is set. + +--- + +## results.json + +The top-level aggregated results for the entire run. + +```json +{ + "benchmark_name": "Tau2Bench", + "benchmark_slug_name": "tau2", + "agent_name": "LiteLLM Tool Calling", + "agent_slug_name": "tool_calling", + "model_name": "gpt-4o", + "model_names": ["gpt-4o"], + "subset_name": "retail", + "total_sessions": 10, + "planned_sessions": 10, + "successful_sessions": 7, + + "benchmark_score": 0.72, + "average_score": 0.65, + + "average_agent_cost": 0.043, + "total_agent_cost": 0.43, + "average_benchmark_cost": 0.021, + "total_benchmark_cost": 0.21, + "total_run_cost": 0.64, + + "average_steps": 12.4, + "average_action_count": 15.1, + "average_invalid_action_count": 0.8, + "average_invalid_action_percent": 5.3, + + "percent_finished": 80.0, + "percent_successful": 70.0, + "percent_finished_successful": 70.0, + "percent_finished_unsuccessful": 10.0, + "percent_unfinished": 10.0, + "percent_error": 10.0, + + "aggregation_mode": "completed_only", + "completed_sessions": 10, + "incomplete_sessions": 0, + "missing_sessions": 0, + "aggregated_session_ids": ["a1b2c3d4", "..."], + "skipped_session_ids": [], + "skipped_session_reasons": {}, + + "exgentic_version": "0.3.0", + + "session_results": [ ... ] +} +``` + +### Field reference + +#### Identity + +| Field | Type | Description | +|-------|------|-------------| +| `benchmark_name` | string | Benchmark display name | +| `benchmark_slug_name` | string | Benchmark CLI identifier | +| `agent_name` | string | Agent display name | +| `agent_slug_name` | string | Agent CLI identifier | +| `model_name` | string \| null | Primary model used | +| `model_names` | list[string] \| null | All models used (if multiple) | +| `subset_name` | string \| null | Benchmark subset | +| `exgentic_version` | string \| null | Exgentic version that produced these results | + +#### Session counts + +| Field | Type | Description | +|-------|------|-------------| +| `total_sessions` | int | Sessions that were executed | +| `planned_sessions` | int \| null | Sessions originally planned (from `num_tasks` or `task_ids`) | +| `successful_sessions` | int | Sessions where `success=true` | + +#### Scores + +| Field | Type | Description | +|-------|------|-------------| +| `benchmark_score` | float \| null | Primary score from `Benchmark.aggregate_sessions()` — benchmark-specific | +| `average_score` | float \| null | Mean of per-session `score` values | + +#### Costs + +| Field | Type | Description | +|-------|------|-------------| +| `average_agent_cost` | float \| null | Mean agent API cost per session (USD) | +| `total_agent_cost` | float \| null | Total agent API cost across all sessions | +| `average_benchmark_cost` | float \| null | Mean benchmark API cost per session (e.g. simulator LLM) | +| `total_benchmark_cost` | float \| null | Total benchmark API cost | +| `total_run_cost` | float \| null | Total cost (agent + benchmark) | + +#### Performance statistics + +| Field | Type | Description | +|-------|------|-------------| +| `average_steps` | float \| null | Mean number of steps per session | +| `average_action_count` | float \| null | Mean number of actions per session | +| `average_invalid_action_count` | float \| null | Mean invalid actions per session | +| `average_invalid_action_percent` | float \| null | Invalid actions as percentage of total | + +#### Outcome breakdown + +| Field | Type | Description | +|-------|------|-------------| +| `percent_finished` | float \| null | Sessions that reached a terminal state (success or failure) | +| `percent_successful` | float \| null | Sessions with `success=true` | +| `percent_finished_successful` | float \| null | Sessions that finished successfully | +| `percent_finished_unsuccessful` | float \| null | Sessions that finished unsuccessfully | +| `percent_unfinished` | float \| null | Sessions that ran out of steps | +| `percent_error` | float \| null | Sessions that raised an exception | + +#### Aggregation provenance + +| Field | Type | Description | +|-------|------|-------------| +| `aggregation_mode` | string \| null | Always `"completed_only"` — only completed sessions are aggregated | +| `completed_sessions` | int \| null | Sessions with a `results.json` on disk | +| `incomplete_sessions` | int \| null | Sessions with a directory but no `results.json` | +| `missing_sessions` | int \| null | Planned sessions with no directory at all | +| `aggregated_session_ids` | list[string] \| null | Sessions included in score aggregation | +| `skipped_session_ids` | list[string] \| null | Sessions excluded from aggregation | +| `skipped_session_reasons` | dict \| null | Reason per skipped session ID | + +--- + +## sessions//results.json + +Per-session results. + +```json +{ + "session_id": "a1b2c3d4", + "task_id": "retail_1", + "success": true, + "score": 1.0, + "is_finished": true, + "status": "success", + "steps": 14, + "action_count": 17, + "invalid_action_count": 1, + "agent_cost": 0.038, + "benchmark_cost": 0.019, + "execution_time": 42.3, + "details": { ... }, + "cost_reports": { + "agent": { "model_name": "gpt-4o", "input_tokens": 9400, "output_tokens": 820, ... }, + "benchmark": { ... } + } +} +``` + +### Field reference + +| Field | Type | Description | +|-------|------|-------------| +| `session_id` | string | 8-char deterministic session ID | +| `task_id` | string \| null | Task identifier from the benchmark | +| `success` | bool | Whether the session ended successfully | +| `score` | float \| null | Benchmark-assigned score for this session (0–1 unless benchmark uses a different scale) | +| `is_finished` | bool \| null | Whether the agent signalled completion (as opposed to hitting a step limit) | +| `status` | string | Session outcome status (see below) | +| `steps` | int | Number of (action → observation) steps executed | +| `action_count` | int | Total individual actions taken | +| `invalid_action_count` | int | Actions that failed schema or contract validation | +| `agent_cost` | float | Estimated agent API cost (USD) | +| `benchmark_cost` | float | Estimated benchmark API cost (USD) | +| `execution_time` | float | Wall-clock time in seconds | +| `details` | object | Full `SessionScore` dump — benchmark-specific | +| `cost_reports` | object | Detailed cost breakdown keyed by `"agent"` and `"benchmark"` | + +### Session outcome status + +| Value | Meaning | +|-------|---------| +| `success` | Session finished and benchmark scored it as successful | +| `unsuccessful` | Session finished but benchmark scored it as unsuccessful | +| `unfinished` | Agent never returned `None` — ran out of steps | +| `limit_reached` | Hit `max_steps` or `max_actions` | +| `error` | An exception occurred during execution | +| `cancelled` | Run was cancelled before this session completed | +| `unknown` | Status could not be determined | + +--- + +## sessions//trajectory.jsonl + +A newline-delimited JSON file with one entry per step. Use this to replay or audit what the agent did. + +```json +{"event": "observation", "step": 0, "initial": true, "session_id": "a1b2c3d4", "task_id": "retail_1", "observation": {...}, "action": null} +{"event": "action", "step": 1, "initial": false, "session_id": "a1b2c3d4", "task_id": "retail_1", "observation": null, "action": {"name": "search_products", "arguments": {...}}} +{"event": "observation", "step": 1, "initial": false, "session_id": "a1b2c3d4", "task_id": "retail_1", "observation": {"content": [...]}, "action": null} +... +``` + +--- + +## benchmark_results.json + +Benchmark-specific aggregated results produced by `Benchmark.aggregate_sessions()`. Schema is benchmark-defined, but always includes at minimum: + +```json +{ + "benchmark_name": "Tau2Bench", + "total_tasks": 10, + "score": 0.72, + "metrics": { ... } +} +``` + +--- + +## cost_reports schema + +Within session `results.json`, the `cost_reports` dict contains detailed token and cost breakdowns. + +```json +{ + "agent": { + "model_name": "gpt-4o", + "input_tokens": 9400, + "output_tokens": 820, + "input_cost": 0.0235, + "output_cost": 0.0164, + "total_cost": 0.0399 + }, + "benchmark": { + "model_name": "gpt-4o", + "input_tokens": 4200, + "output_tokens": 340, + "input_cost": 0.0105, + "output_cost": 0.0068, + "total_cost": 0.0173 + } +} +``` + +Cost estimates come from LiteLLM's pricing database. For providers or deployments not in the database, costs show as `0`. + +--- + +## Reading results programmatically + +```python +import json +from pathlib import Path + +run_dir = Path("outputs/abc123def456") + +# Load run-level results +results = json.loads((run_dir / "results.json").read_text()) +print(f"Score: {results['benchmark_score']}") +print(f"Sessions: {results['total_sessions']}") + +# Load a specific session trajectory +session_dir = run_dir / "sessions" / "a1b2c3d4" +trajectory = [ + json.loads(line) + for line in (session_dir / "trajectory.jsonl").read_text().splitlines() +] +``` + +Or use the Python API to load and validate: + +```python +from exgentic import results +from exgentic.batch import RunConfig + +config = RunConfig(benchmark="tau2", agent="tool_calling", subset="retail") +run_results = results(config) +print(run_results.benchmark_score) +``` + +See [Python API](./python-api.md) for the full API reference. + +--- + +## See also + +- [Python API](./python-api.md) — `results()`, `status()`, `aggregate()` functions +- [Batch Runs](./batch.md) — `batch extract` to export results to CSV +- [CLI Reference](./cli-reference.md) — `exgentic results` command +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/python-api.md b/labs/AgentStream/exgentic/docs/python-api.md new file mode 100644 index 00000000..a8dc49b4 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/python-api.md @@ -0,0 +1,361 @@ +# Python API + +Exgentic can be used as a library. The public API is importable directly from the `exgentic` package. + +**Related docs:** +[docs/](./README.md) · [CLI Reference](./cli-reference.md) · [Output Format](./output-format.md) · [Batch Runs](./batch.md) · [Custom Models](./custom-models.md) + +--- + +## Installation + +```bash +uv add exgentic # or: pip install exgentic +``` + +--- + +## Quick example + +```python +from exgentic import evaluate + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=5, + model="gpt-4o", + benchmark_kwargs={"user_simulator_model": "gpt-4o"}, +) + +print(results.benchmark_score) +print(results.total_agent_cost) +``` + +--- + +## Core functions + +All functions share the same config parameters. You can pass them as keyword arguments or as a pre-built `RunConfig` object. + +### evaluate() + +Run sessions and aggregate results. The standard function for most use cases. + +```python +from exgentic import evaluate + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=10, + model="gpt-4o", + benchmark_kwargs={"user_simulator_model": "gpt-4o"}, + agent_kwargs={"model_settings": {"temperature": 0.2}}, + max_steps=100, + max_actions=100, + max_workers=4, +) +``` + +Returns: `RunResults` — see [Output Format](./output-format.md) for the full schema. + +### execute() + +Run sessions without aggregating results. Use this when you want to separate execution from aggregation (e.g. run on multiple machines, aggregate centrally). + +```python +from exgentic import execute, aggregate + +execute(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=10) +# ... copy outputs to central machine ... +results = aggregate(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=10) +``` + +Returns: `RunResults` with aggregation fields empty. + +### aggregate() + +Aggregate already-completed sessions without running anything. Reads `results.json` from each session directory and computes run-level statistics. + +```python +from exgentic import aggregate + +results = aggregate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=10, +) +``` + +Returns: `RunResults`. + +### status() + +Get the current execution status of a run without running anything. + +```python +from exgentic import status + +run_status = status(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=10) +print(run_status.completed) # number of completed sessions +print(run_status.running) # number of currently-running sessions +print(run_status.missing) # number of not-yet-started sessions +``` + +Returns: `RunStatus`. + +### preview() + +Get the execution plan for a run — which sessions would run, which would be reused, etc. — without executing. + +```python +from exgentic import preview +from exgentic.interfaces.lib.api import RunConfig + +config = RunConfig(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=10) +plan = preview(config) + +print(plan.to_run) # list of session configs that would run +print(plan.reuse) # list of already-completed sessions +print(plan.missing) # list of sessions with no output directory +``` + +Returns: `RunPlan`. + +### results() + +Load aggregated results from a completed run's `results.json` on disk. + +```python +from exgentic import results +from exgentic.interfaces.lib.api import RunConfig + +config = RunConfig(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=10) +run_results = results(config) +``` + +Returns: `RunResults`. + +--- + +## Parameters + +All core functions accept the same parameters (as kwargs or as a `RunConfig`/`SessionConfig` object). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `benchmark` | string \| Benchmark | required | Benchmark slug or instance | +| `agent` | string \| Agent | required | Agent slug or instance | +| `subset` | string | null | Benchmark subset | +| `task_ids` | list[string] | null | Explicit task IDs | +| `num_tasks` | int | null | Number of tasks to run | +| `model` | string | null | Model override (forwarded to the agent) | +| `output_dir` | string | `./outputs` | Results directory | +| `cache_dir` | string | null | Cache directory | +| `run_id` | string | auto | Deterministic run ID derived from config | +| `max_steps` | int | 100 | Maximum steps per session | +| `max_actions` | int | 100 | Maximum actions per session | +| `max_workers` | int | null | Parallel session workers | +| `overwrite_sessions` | bool | false | Re-run completed sessions | +| `benchmark_kwargs` | dict | null | Extra kwargs for the benchmark constructor | +| `agent_kwargs` | dict | null | Extra kwargs for the agent constructor | +| `observers` | list | null | Custom observers (see [Observers](./observers.md)) | +| `controllers` | list | null | Custom controllers (see [Observers](./observers.md)) | + +--- + +## Discovery functions + +### list_benchmarks() + +```python +from exgentic import list_benchmarks + +for b in list_benchmarks(): + print(b["slug_name"], b["display_name"], b["installed"]) +``` + +Returns: `list[dict]` with keys `slug_name`, `display_name`, `installed`, `installed_at`. + +### list_agents() + +```python +from exgentic import list_agents + +for a in list_agents(): + print(a["slug_name"], a["display_name"]) +``` + +Returns: `list[dict]` with keys `slug_name`, `display_name`, `installed`, `installed_at`. + +### list_subsets() + +```python +from exgentic import list_subsets + +subsets = list_subsets("tau2") +# ["retail", "airline", "banking"] +``` + +Returns: `list[str]`. + +### list_tasks() + +```python +from exgentic import list_tasks + +tasks = list_tasks(benchmark="tau2", subset="retail") +# ["retail_1", "retail_2", ...] +``` + +Returns: `list[str]`. + +--- + +## Setup functions + +### setup_benchmark() + +Install a benchmark's dependencies and run its `setup.sh`. Equivalent to `exgentic install --benchmark `. + +```python +from exgentic.interfaces.lib.api import setup_benchmark + +setup_benchmark("tau2") +setup_benchmark("tau2", force=True) # reinstall even if already set up +setup_benchmark("tau2", runner="venv") # install into isolated venv +``` + +### setup_agent() + +```python +from exgentic.interfaces.lib.api import setup_agent + +setup_agent("tool_calling") +setup_agent("tool_calling", force=True) +``` + +--- + +## Config objects + +Use config objects when you want to construct a run programmatically, save configs to disk, or pass them around. + +### RunConfig + +```python +from exgentic.interfaces.lib.api import RunConfig + +config = RunConfig( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=10, + model="gpt-4o", + max_steps=100, + benchmark_kwargs={"user_simulator_model": "gpt-4o"}, + agent_kwargs={"model_settings": {"temperature": 0.2}}, +) + +# Save to disk +import json +Path("my_run.json").write_text(config.model_dump_json(indent=2)) + +# Load from disk +config2 = RunConfig.model_validate_json(Path("my_run.json").read_text()) +``` + +### SessionConfig + +For single-task runs: + +```python +from exgentic.interfaces.lib.api import SessionConfig + +config = SessionConfig( + benchmark="tau2", + agent="tool_calling", + task_id="retail_1", + subset="retail", + model="gpt-4o", +) +``` + +--- + +## Model settings + +Pass model settings through `agent_kwargs`: + +```python +from exgentic import evaluate +from exgentic.core.types import ModelSettings + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + subset="retail", + num_tasks=5, + model="gpt-4o", + agent_kwargs={ + "model_settings": ModelSettings( + temperature=0.2, + max_tokens=4096, + num_retries=3, + ) + }, +) +``` + +Or as a plain dict (equivalent): + +```python +agent_kwargs={ + "model_settings": { + "temperature": 0.2, + "max_tokens": 4096, + "num_retries": 3, + } +} +``` + +See [Custom Models](./custom-models.md) for the full `ModelSettings` reference. + +--- + +## Custom observers + +Pass observers to receive live callbacks during a run: + +```python +from exgentic import evaluate +from exgentic.core.orchestrator.observer import Observer + +class PrintObserver(Observer): + def on_session_success(self, session, score, agent): + print(f"Session {session.session_id}: score={score.score}") + +results = evaluate( + benchmark="tau2", + agent="tool_calling", + num_tasks=5, + observers=[PrintObserver()], +) +``` + +See [Observers](./observers.md) for the full interface reference. + +--- + +## See also + +- [Custom Models](./custom-models.md) — LLM provider setup and `ModelSettings` +- [Output Format](./output-format.md) — `RunResults` and `SessionResults` schema +- [Observers](./observers.md) — custom event callbacks +- [Batch Runs](./batch.md) — programmatic equivalents of batch commands +- [CLI Reference](./cli-reference.md) — CLI alternative to the Python API +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/releasing.md b/labs/AgentStream/exgentic/docs/releasing.md new file mode 100644 index 00000000..9ef4c0a3 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/releasing.md @@ -0,0 +1,119 @@ +# Releasing Exgentic + +**Related docs:** +[docs/](./README.md) · [DEVELOPMENT.md](../DEVELOPMENT.md) · [CONTRIBUTING.md](../CONTRIBUTING.md) + +## Release model + +Exgentic uses Git tags as the single source of truth for released versions. +There is no manually maintained version string in the source tree. + +- A release tag must look like `vX.Y.Z`, for example `v0.2.0` +- Package versions are derived from Git tags via `hatch-vcs` +- PyPI publishing runs from GitHub Actions only for pushed `v*` tags +- The publishing workflow verifies that the tagged commit is reachable from `main` +- GitHub Releases are created manually after PyPI publish succeeds + +## One-time repository setup + +1. In PyPI, create the `exgentic` project if it does not exist yet. +2. In PyPI project settings, add a Trusted Publisher for this GitHub repository. +3. Use these GitHub values when configuring the publisher: + - Owner: `Exgentic` + - Repository: `exgentic` + - Workflow name: `publish-pypi.yml` + - Environment name: `pypi` +4. In GitHub, keep the `pypi` environment enabled for this workflow if you want environment-level protections. + +See the official docs for the exact PyPI setup steps: +- https://docs.pypi.org/trusted-publishers/using-a-publisher/ +- https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + +## Release steps + +1. Make sure the release commit is already merged to `main`. +2. Update local refs: + ```bash + git checkout main + git pull --ff-only origin main + ``` +3. Create the annotated tag: + ```bash + scripts/release.sh 0.2.0 + ``` +4. Push the tag: + ```bash + git push origin v0.2.0 + ``` + +Or do steps 3 and 4 in one command: + +```bash +scripts/release.sh 0.2.0 --push +``` + +5. After the PyPI workflow succeeds, create the GitHub Release manually: + ```bash + gh release create v0.2.0 --generate-notes --title "v0.2.0" + ``` + +## GitHub Release notes + +Use generated notes as the base, then edit the release text to keep it short and useful. + +The release description should include: + +- What changed for users +- Any packaging, CLI, or behavior changes worth calling out +- Any migration or upgrade note if behavior changed +- A short verification note when helpful, for example that the version is on PyPI + +Good default structure: + +```md +## Summary +- Short user-facing change 1 +- Short user-facing change 2 + +## Notes +- Optional upgrade or compatibility note +``` + +Avoid: + +- Raw internal implementation details unless they affect users +- Huge changelogs pasted into the release body +- Empty releases with only the tag name when there was a meaningful change + +## What happens after the tag is pushed + +1. GitHub Actions checks out the tagged commit. +2. The workflow confirms that commit belongs to `main`. +3. The package is built from that exact tag. +4. GitHub exchanges its OIDC identity with PyPI using Trusted Publishing. +5. The distribution is uploaded to PyPI. +6. After that succeeds, create the GitHub Release page for the same tag. + +## Verifying the release locally + +You can inspect the version derived from a tag before pushing: + +```bash +git tag -a v0.2.0 -m "Release v0.2.0" +python -m build +``` + +The built wheel and sdist should report version `0.2.0`. +If you created a test tag by mistake, delete it locally before pushing: + +```bash +git tag -d v0.2.0 +``` + +--- + +## See also + +- [DEVELOPMENT.md](../DEVELOPMENT.md) — local setup and testing +- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR workflow and legal requirements +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/docs/replay-testing.md b/labs/AgentStream/exgentic/docs/replay-testing.md new file mode 100644 index 00000000..996be3b0 --- /dev/null +++ b/labs/AgentStream/exgentic/docs/replay-testing.md @@ -0,0 +1,229 @@ +# Replay Testing + +Replay tests let you verify benchmark and agent integration end-to-end without making real API calls or running external services. A recording captures a live session; the test replays it deterministically. + +This is the primary testing pattern for contributors adding new benchmarks or agents. + +**Related docs:** +[docs/](./README.md) · [Adding Benchmarks](./adding-benchmarks.md) · [Adding Agents](./adding-agents.md) · [Runners](./runners.md) + +--- + +## How it works + +1. Run a live evaluation and capture the session trajectory. +2. Store the trajectory in `tests/benchmarks/recordings//`. +3. Write a test that replays the trajectory using `ReplayAgent` and `ReplayBenchmark`. +4. The test verifies the session ends with the expected score — no network, no API keys, no benchmark installation required. + +Replay tests can be parametrized across all runner types (`direct`, `venv`, `docker`) to verify that runner isolation doesn't change session outcomes. + +--- + +## Recording format + +Each recording lives in its own directory: + +``` +tests/benchmarks/recordings// +├── recording.json # Metadata: task_id, benchmark slug, expected score +├── trajectory.jsonl # Recorded actions and observations +├── session.json # Session manifest +└── results.json # Recorded session results +``` + +### recording.json + +```json +{ + "task_id": "retail_1", + "benchmark_slug": "tau2", + "expected_score": 1.0 +} +``` + +Set `expected_score` to `null` if you only want to verify that the session completes without asserting on score. + +### trajectory.jsonl + +Newline-delimited JSON. Each line is one event — either an observation (benchmark → agent) or an action (agent → benchmark): + +```json +{"event": "observation", "step": 0, "initial": true, "session_id": "...", "task_id": "retail_1", "observation": {...}, "action": null} +{"event": "action", "step": 1, "initial": false, "session_id": "...", "task_id": "retail_1", "observation": null, "action": {"name": "search", "arguments": {...}}} +{"event": "observation", "step": 1, "initial": false, "session_id": "...", "task_id": "retail_1", "observation": {...}, "action": null} +``` + +--- + +## Creating a recording + +Run a live evaluation and save the trajectory. The trajectory file is written automatically to the session output directory: + +``` +outputs//sessions//trajectory.jsonl +outputs//sessions//results.json +``` + +Copy the relevant files into your recording directory: + +```bash +mkdir -p tests/benchmarks/recordings/my_benchmark + +cp outputs//sessions//trajectory.jsonl \ + tests/benchmarks/recordings/my_benchmark/ + +cp outputs//sessions//results.json \ + tests/benchmarks/recordings/my_benchmark/ + +# Write recording.json manually +cat > tests/benchmarks/recordings/my_benchmark/recording.json <=0.1.0 +some-other-dep==1.2.3 +``` + +Git LFS objects are automatically skipped during install (`GIT_LFS_SKIP_SMUDGE=1`). + +### setup.sh + +Shell script for setup that can't be expressed as pip packages: cloning repositories, compiling binaries, downloading model weights, etc. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Example: clone a dependency +git clone --depth 1 https://github.com/example/repo /opt/repo +``` + +Place it in the same directory as your benchmark module. It runs after `requirements.txt` is installed. + +--- + +## See also + +- [Adding Benchmarks](./adding-benchmarks.md) — how benchmarks declare their setup +- [Adding Agents](./adding-agents.md) — how agents declare their setup +- [Custom Models](./custom-models.md) — configuring the LLM behind the agent +- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/examples/run_appworld.py b/labs/AgentStream/exgentic/examples/run_appworld.py new file mode 100644 index 00000000..03600313 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_appworld.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark appworld --agent tool_calling --subset test_normal --num-tasks 3 \ +# --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="appworld", agent="tool_calling", subset="test_normal", num_tasks=3, +# model="gpt-4o")) +## Direct class usage (this script): +# AppWorldBenchmark + LiteLLMToolCallingAgent + +from exgentic import AppWorldBenchmark, LiteLLMToolCallingAgent, evaluate + + +def main() -> None: + benchmark = AppWorldBenchmark(subset="test_normal") + agent = LiteLLMToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=3) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_browsecomp.py b/labs/AgentStream/exgentic/examples/run_browsecomp.py new file mode 100644 index 00000000..26176885 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_browsecomp.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark browsecompplus --agent tool_calling --subset main --num-tasks 3 \ +# --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="browsecompplus", agent="tool_calling", subset="main", num_tasks=3, +# model="gpt-4o")) +## Direct class usage (this script): +# BrowseCompPlusBenchmark + LiteLLMToolCallingAgent + +from exgentic import BrowseCompPlusBenchmark, LiteLLMToolCallingAgent, evaluate + + +def main() -> None: + benchmark = BrowseCompPlusBenchmark() + agent = LiteLLMToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=3) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_claude_code_on_gsm8k.py b/labs/AgentStream/exgentic/examples/run_claude_code_on_gsm8k.py new file mode 100644 index 00000000..6e3cecb8 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_claude_code_on_gsm8k.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark gsm8k --agent claude_code --num-tasks 1 \ +# --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="gsm8k", agent="claude_code", num_tasks=1, +# model="gpt-4o")) +## Direct class usage (this script): +# GSM8kBenchmark + ClaudeCodeAgent + +from exgentic import ClaudeCodeAgent, GSM8kBenchmark, evaluate + + +def main() -> None: + benchmark = GSM8kBenchmark() + agent = ClaudeCodeAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=1) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_claude_code_on_tau2bench.py b/labs/AgentStream/exgentic/examples/run_claude_code_on_tau2bench.py new file mode 100644 index 00000000..b8e7b66e --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_claude_code_on_tau2bench.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark tau2 --agent claude_code --subset telecom --num-tasks 1 \ +# --model gpt-4o --set benchmark.user_simulator_model=gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="tau2", agent="claude_code", subset="telecom", num_tasks=1, +# model="gpt-4o", benchmark_kwargs={"user_simulator_model": "gpt-4o"})) +## Direct class usage (this script): +# TAU2Benchmark + ClaudeCodeAgent + +from exgentic import ClaudeCodeAgent, TAU2Benchmark, evaluate + + +def main() -> None: + benchmark = TAU2Benchmark(subset="telecom", user_simulator_model="gpt-4o") + agent = ClaudeCodeAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=1) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_cli_agents.py b/labs/AgentStream/exgentic/examples/run_cli_agents.py new file mode 100644 index 00000000..cbf518c9 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_cli_agents.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark gsm8k --agent codex_cli --num-tasks 3 \ +# --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="gsm8k", agent="codex_cli", num_tasks=3, +# model="gpt-4o")) +## Direct class usage (this script): +# GSM8kBenchmark + CodexAgent + +from exgentic import CodexAgent, GSM8kBenchmark, evaluate + + +def main() -> None: + benchmark = GSM8kBenchmark() + agent = CodexAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=3) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_gsm8k.py b/labs/AgentStream/exgentic/examples/run_gsm8k.py new file mode 100644 index 00000000..93466296 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_gsm8k.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark gsm8k --agent tool_calling --num-tasks 3 --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="gsm8k", agent="tool_calling", num_tasks=3, +# model="gpt-4o")) +## Direct class usage (this script): +# GSM8kBenchmark + LiteLLMToolCallingAgent + +from exgentic import GSM8kBenchmark, LiteLLMToolCallingAgent, evaluate + + +def main() -> None: + benchmark = GSM8kBenchmark() + agent = LiteLLMToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=3) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_hotpotqa.py b/labs/AgentStream/exgentic/examples/run_hotpotqa.py new file mode 100644 index 00000000..eccf6563 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_hotpotqa.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark hotpotqa --agent smolagents_tool --subset distractor --num-tasks 3 \ +# --model gpt-4o --set benchmark.with_search_tools=true +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="hotpotqa", agent="smolagents_tool", subset="distractor", num_tasks=3, +# model="gpt-4o", benchmark_kwargs={"with_search_tools": True})) +## Direct class usage (this script): +# HotpotQABenchmark + SmolagentToolCallingAgent + +from exgentic import HotpotQABenchmark, SmolagentToolCallingAgent, evaluate + + +def main() -> None: + benchmark = HotpotQABenchmark(with_search_tools=True) + agent = SmolagentToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=3) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_openai_mcp.py b/labs/AgentStream/exgentic/examples/run_openai_mcp.py new file mode 100644 index 00000000..08a2486b --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_openai_mcp.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark tau2 --agent openai_solo --subset retail --task 4 \ +# --model gpt-4o --set benchmark.user_simulator_model=gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="tau2", agent="openai_solo", subset="retail", task_ids=["4"], +# model="gpt-4o", +# benchmark_kwargs={"user_simulator_model": "gpt-4o"})) +## Direct class usage (this script): +# TAU2Benchmark + OpenAIMCPAgent + +from exgentic import OpenAIMCPAgent, TAU2Benchmark, evaluate + + +def main() -> None: + benchmark = TAU2Benchmark( + subset="retail", + user_simulator_model="gpt-4o", + ) + agent = OpenAIMCPAgent(model="gpt-4o") + evaluate( + benchmark=benchmark, + agent=agent, + output_dir="./outputs", + task_ids=["4"], + ) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_smol.py b/labs/AgentStream/exgentic/examples/run_smol.py new file mode 100644 index 00000000..2b616b68 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_smol.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark tau2 --agent smolagents_code --subset retail --num-tasks 30 \ +# --model gpt-4o --set benchmark.user_simulator_model=gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="tau2", agent="smolagents_code", subset="retail", num_tasks=30, +# model="gpt-4o", benchmark_kwargs={"user_simulator_model": "gpt-4o"})) +## Direct class usage (this script): +# TAU2Benchmark + SmolagentCodeAgent + +from exgentic import SmolagentCodeAgent, TAU2Benchmark, evaluate + + +def main() -> None: + benchmark = TAU2Benchmark(subset="retail", user_simulator_model="gpt-4o") + agent = SmolagentCodeAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=30) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_swebench.py b/labs/AgentStream/exgentic/examples/run_swebench.py new file mode 100644 index 00000000..0d7d9492 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_swebench.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark swebench --agent tool_calling --num-tasks 30 \ +# --model gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="swebench", agent="tool_calling", num_tasks=30, +# model="gpt-4o")) +## Direct class usage (this script): +# SWEBenchBenchmark + LiteLLMToolCallingAgent + +from exgentic import LiteLLMToolCallingAgent, SWEBenchBenchmark, evaluate + + +def main() -> None: + benchmark = SWEBenchBenchmark() + agent = LiteLLMToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=30) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/run_taubench.py b/labs/AgentStream/exgentic/examples/run_taubench.py new file mode 100644 index 00000000..c2a2f1bd --- /dev/null +++ b/labs/AgentStream/exgentic/examples/run_taubench.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +## CLI usage: +# exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-tasks 1 \ +# --model gpt-4o --set benchmark.user_simulator_model=gpt-4o +## Python API usage: +# from exgentic import RunConfig, evaluate +# evaluate(RunConfig(benchmark="tau2", agent="tool_calling", subset="retail", num_tasks=1, +# model="gpt-4o", benchmark_kwargs={"user_simulator_model": "gpt-4o"})) +## Direct class usage (this script): +# TAU2Benchmark + LiteLLMToolCallingAgent + +from exgentic import LiteLLMToolCallingAgent, TAU2Benchmark, evaluate + + +def main() -> None: + benchmark = TAU2Benchmark(subset="retail", user_simulator_model="gpt-4o") + agent = LiteLLMToolCallingAgent(model="gpt-4o") + evaluate(benchmark=benchmark, agent=agent, output_dir="./outputs", num_tasks=1) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/examples/simple_test_agent/adapter.py b/labs/AgentStream/exgentic/examples/simple_test_agent/adapter.py new file mode 100644 index 00000000..1501ea4a --- /dev/null +++ b/labs/AgentStream/exgentic/examples/simple_test_agent/adapter.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from typing import Any, Dict, List, Optional + +from exgentic.core.agent import Agent +from exgentic.core.agent_instance import AgentInstance +from exgentic.core.types import ModelSettings + + +class SimpleTestAgentInstance(AgentInstance): + """Simple test agent that responds with basic actions.""" + + def __init__(self, session_id: str, task: str, context: Dict[str, Any], actions: List[str]): + super().__init__(session_id) + self.task = task + self.context = context or {} + self.actions = actions + self.step_count = 0 + + def react(self, observation: Optional[str]) -> Optional[str]: + """React to observation with simple response.""" + self.step_count += 1 + + if observation is None: + # First step + return f"Starting task: {self.task}" + + # Simple logic: respond a few times then finish + if self.step_count <= 2: + return f"Responding to: {observation}" + # Signal completion + return None + + def close(self): + pass + + +class SimpleTestAgent(Agent): + """Agent factory that creates simple test agents.""" + + display_name: str = "Simple Test Agent" + slug_name: str = "simple_test" + + def __init__(self, model_settings: ModelSettings | None = None) -> None: + if model_settings is not None and not isinstance(model_settings, ModelSettings): + raise ValueError("model_settings must be a ModelSettings instance.") + self.model_settings = model_settings + + def assign(self, task: str, context: Dict[str, Any], actions: List[str], session_id: str) -> AgentInstance: + return SimpleTestAgentInstance(session_id, task, context, actions) + + def get_models_names(self) -> List[str]: # type: ignore[override] + return [] diff --git a/labs/AgentStream/exgentic/examples/simple_test_agent/setup.sh b/labs/AgentStream/exgentic/examples/simple_test_agent/setup.sh new file mode 100644 index 00000000..5ddf8ed1 --- /dev/null +++ b/labs/AgentStream/exgentic/examples/simple_test_agent/setup.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# No additional dependencies needed for simple test agent +echo "Simple test agent - no setup required" diff --git a/labs/AgentStream/exgentic/misc/assets/cli.png b/labs/AgentStream/exgentic/misc/assets/cli.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4f5ad1de3f4b8cab4331ffcaa53a0a585db5e4 GIT binary patch literal 1046661 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

vR`lWgj{0&hcCJ%UPYW~H-Jr*KBovB+Wh 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 a70MG8AsFnN6%$Ukr|D4!y&zB8X&@G^fH_Z6 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�+zqiW$?q_@kb7Za~uJ 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�!%N76e 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

)J$1(1^g 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 z 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�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

318hT43(HZswXY9b!TV`TihX-l-7|Zfx z&;r{FMd`I1KKxA=#IJseM`EPuM-rPbo2Q0%w(+xRkN0eO(zR%m3^<$#I&AE_g-bx? zMfKkXtw#>3Yy{U=AjUN1gG|r{FncEt?7WvoVQ|c6s}?=5%P8?n9~$o%W(&m*afFn^J?T z@~*Bbi&qZ$DSaNLDI+P=^)ZEPh617~E_}vtzw_;4eg271Y{V!tn_!|YzN*or#sldu zZ6d~)doNzldAcX`Lf25hI1Of*#EdLjyKt>{yFI+JBOPn$9s^3!3S-w*!01+&y?_w9 zEcVSbF-Xwn!#bnTWuAIkzEx18z3FK1s9vh$2$OhR<*oL-Ia%hdc$lj@&^VJNGP}jX zq$4)E7N~m>a=a9bW;>+IA}LffZU-I}5v-ku6kQf-y>tzPJk(8UmliDyz!T0YIhtQz zSMeI9iQaG7n!VNb#K);MyK|nwtYlFXOBpABH5-Ks}JWFde4*^4!5eUd3!A4I@Mw=!}K_6zuhjE4eTx% zEI)QjF5ak|jfg_a{`_iMwwJnG_Cf;jHXBtL?-M4}X@o&m^iX9@p{Z*)7_@6QQ8cYz zkc_$aJSdKp2G!@_ajxRzwdE92Ks5wBbv{P8Mb8eP%hjWRJFPjKTZvqtt$=uSou^QSjr8 zJf|Us8@*Pu3PD}kFe%FU_&yp|$v7$W;kHAy%@F4vSliFC{q@nT^Oi%5i_nPj*5sX@ zTR5Qav#y6>mX&npMwrQCddY)}!Ve`!CDpPpP^xe^;oO8wjeI&Izfn8$Pq z=Cy&DZpOjg!i?zCv|yIsCD9Kiud!8re#n&K_9Y!xR1F-Q_LGG^aOfUP7Z=H>4m+VA z({QE$QG|(~l|?Utw^7WCH$@dgONEmlW8sc=YQM~Du+J03qPTYL!MRmq?*R7a;I52) zBdFP;OgyHOY4NuW;IAD|7L%5e+E??p14309zdti;t1w#k-N~?=$KDcV)46MsVmI?X zZ$1)Vi6@A6X~ceu5}mAM5I@>tYK#GF}qHNY$UMafaEnGD9s2b+sxyzt) z_e1}W{OaoQVZdE6sWIzTl11xXccs$ zZ@nW^a%FR|wYN~3P2rm({%Z)BOOmD~?19Q>sn9_8@F9qGk5G-dr{W0GHigjFU8!xK z*=laoZA3)8_wopc>lE+SGh$0$48HB3sOrWYRBZ;=qJc`nDMAxHk%L;=wPQ>xH z4!dNWElGCiOgj8H0Olb9Wme<25@hd{7;j!SAJ^iuT3`+06uWeTyrN3kAIph6ujET2 z@NhF*} z?*{ptnWjxM4GCnfnn-INMK^i1mb70vCE&Q8E#d;IMkSmzu&DMkZisdHHR@I&^s(qN z%qjd`$@0%$kRJ7n&mhPn|4TwDil`c7N=E%p2|9Zf9~A{YF{RXkJ1}MKh~ls7CF2r_ zWO-bl57)xfAj|WX)~YBMhG8t+6rFw-x| z$u-f3h3ng-xvNZ>{I&aN+2V2y71N2Y{J6vUQ(r+{AmHo|zwVQl_N8oo1khno+Uef? zdEDZEP0Q44IDK;KM(_g|$QK<}MAyF(drC&Pp_|w==sT^y-vZ+3ZcUvqy=yPe!yZj( z{tPu1zn5E>6QX}1PaTdQtF>1zuJ@h8eGy24u0S(2N{2CbqnqG=rs2HUoP3SzHmOSc za`ey2+>yKvM@3|IapuP@t69nSGYMu+XAA-yoCZ0BJwY@%W(0x?1<+I3mcC)0Z2kS+ zq*Powu-66#9{-wyxawE7*+SK9LtCFE2yIW=0o`pZ49|`3M2mU4K$Vi02PAGHXT~p5 z@IHr_F)#M06J6m$P-KLKRCvGB z8(5~>gr0iwt`+yF-+KV|PBR%7k8AVLi=TT=ie48aX$IP@f$aQ6J!S#WLx+#jfp71Q z17ffAK?&9l1pVOC0F{U{@$8eg5B=XKS(vr;Bm3c-P{)VFl*tG@+aqSZ!@fZvv_lx9 z%J5=mwYaKq>~2O@kv^k{%MFF*@e_ZRjP$3u%l^3xPLfwaHgRKG%Y1B^(Ka&_3B6W8 z3{bo>5G~vbr9rQta-)M*W(3BJLOOl(Qj8R$)7cDqkAIOb891obiFv#7$n6XVCy{=%9-(ekiO?0vrbh1SN$l zQObYc{yH&zddh_b4I@HQv*D1|uU&kP?-qEnR7ian&%aaP$-87EIS7fcc5q%jMYHEU zM**62MJ^e+kH6r)3Dkl~L2A_E5+t;oUI{zfF!Gz#c(Ht8`fGA5PTg`4`1V!AQ7$!C zwD@7p&*h7u?m@DXaIX4Rw`HZAAc3chMb zObntnoVV8&sIihD({>KQeU8jsMH{+=QRqIQWCEzI--VL%=gIkQDzab>?Z%ybv+Ng)VQ}sX=6b=(Ia{J| z@-U#MZlTHX{q5htRX?v5A#c2g(oEsveEZYx51!5iv)MI!4C$hgrq6hzwTy{mpsjXRkr`fTOqO4z!=8{3(vgXB$|Ed$DvH3 ze#?5-E}H7gm!veKB7Byg#X4QKce?k(ze!{D%xotI=~sqhmkbVq`#S1hJ5(c_tfpxNYsS$6CWN>4!-yJ8SjR(1bu9 za?CdmDohFg8TSws;K?A-S@s`qwH`zV3HEV;$l2SvYHL+NjjNNuc?Nba0&7#DsZ}z< zcl>?;$+!)$a%B?r9^btdG3kEEs~_0I6_H$x*s=>&33aqOv;yA>a!JuaC`Xt+=({Ej zb$XhMyQld7JOnd6JjCa**`LpK#up6%c7-t`8+j@m*?ZKVcH;ftZ4_7HvP!=qzh03_ zDcEy$+pkjkk6DuJCg`7w zV5=#aY>qNukpQv?1ZWtDuEJ@`<$oPOSVpS{)gLH$F|Do>;-T-B2N)}VZRr*15hNUk z>MAuPD8mM!h9Z!13!fueUD6LJnZgi5i0RZe=*07@>-{}nh*$6w#0e|^cyx@+cNgGG zz{a)%um2SBXoOYhwoyE;KQBBD$;&8hJCSr@$~0DR}HC#1h598eHBsZ$g_~84Nq)9bO{~! zCFj6fK43jSk5I7fDS_`^ssOUJLR=@)^++-7Lihv@lx)aJ#|8JFZRF8lc$yd?3Z9p6 z`y25X%g>YeeOFkig8P%Y@aPvN-+M9_fL;0|ueCK6K*m2mSrYWolL3W=@9VKTq!O_rJf8q=s`XlXN-c?>YGm8yn<+ zyoxpSyZ^`b{Ph+82K#GT{;llq-S}@C|FUZ6e`ofWDO3Gtpne^Yf7bYy2l?lE{`uMe zi8}s)ihrQu_qq7>;vcB^2P%GHiQnIl=}xN{(pdq|LKhWpWm6Ps@~aiX(PyK UPKtMJ7yP*_qkJj*;?=wV3;JT=+W-In literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/exgentic_banner_white_no_background.png b/labs/AgentStream/exgentic/misc/assets/exgentic_banner_white_no_background.png new file mode 100644 index 0000000000000000000000000000000000000000..8f272db3c8c7efaf3e4bf259e72e449d7b3622ff GIT binary patch literal 33838 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 SPE(#E{4`W`RVwb;M*lxZpbE+W literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/exgentic_light.png b/labs/AgentStream/exgentic/misc/assets/exgentic_light.png new file mode 100644 index 0000000000000000000000000000000000000000..844b23029f91f3064f12bc2427684992bd3338dd GIT binary patch literal 53953 zcmeEuiC0tC_HZ03`c%+IDglL)i*$4TsIUBsV284g4{i~ZKX?4O>F zz_!6pAFzMud|hL(`U^ElXZh8i6RxRCto~eIoglXQv&-rFb*n#Xs1K~3Fyi)3puy*d zbu2pg^Cuk6Y4`t4|Ct{DNBqyUz~TN+^j1^j{}gXEHU7`+tft2Q=SKI8RILHMe={yI zmk>6-YTYxeZ&KHJIT4bWm5v>~ zLo2~Vdy?%Ln;R2`o2i1&+S;#IoR&)0Hwe{omQnex5%Ki)DQ+O;INCB7^y0%yeot94 z<=3_rmxZQ^xmiQJL2g)+sqwwLDLKi+A$tC6{X2Q%)%A@TGt};30@50wdtjQUBTWdh*dy`MLmAxxcyCk9Szxu2t)_8EM?8+$J;nDW=9D&ZoI&Zr=>gz*Ds^#vUQG77V!#`2L?r2s8Qtf7U?l6Sj)%%5BCOE*GV&?t(lMu_Uzw)Y+o@}v zRZYrCcEmsGBCE>6@P?BD1t*Lk3>#wHCq^8UKOwXujaSOekIf*?bVpjZn+SNYjUYiR zqRQ_xWQKH6cGz9A9qB{wt?z@!(RjLqtQ?4t&(Q$4`hs1+;E`42KJ0BNJk57YHQs^F zAm)Tw1<_DHZ0Od{)8lX%W4{PJ_zo~`EC4aLRM68~kHht*?WNSOW4*D7`Glto!w;m; z{&1A+)eftrV!_oWMc|Eg4IugZs%h(Tvcl5HaZQS116oKNQQ?3H+!Y84t?z$ciq6af zG%Qs~yg8e`WJ&1-j^ZMQ_YbUbdff1dkla7mbVE1)>WifW{-0=UsxeHLbLw+2>NDA$ zDkyB)Zb@RJwuepNS3BqU8Uv!|8!c^HhAA$f=e$3BDSE_41CRn*N3{5x6nvK8Zqs)7 z_K7V7!r~m{lZ5tOrr7XR51YIvCtKR<>H%e0u(OM!}y|0;v%-*$0rPHEfV&OOO_|H*0>tRR`>Dl1M#*RpON37`A zn}0*=`w{}*CRK^?ZD6+HDG^!;f8&VwOIk_Y;6@cLFBW6DI0j+)VLkj%l)8@b!?-OI zMu2dembuggt6d0d~sYh*3J#)YyEf4lF9@o%|I6B}Dn;Ivp zqY6{FJOwLTMnl66FUjR0Im0o2w7P$akS+irTbYKTGp?o=A^FDtd+b$Wv^e(>{jk`U z$Fx|GCf?}uCrRwR)aB`qQ|{}cTOh@l*t1S?AF8u=_usr(2x_DbruZy{yycGBZ%pD7h!#i4YOj>8__n>p1?!R7B62Rm@TZqsnkaij11sa zgLHBa_Nqal;BwRUkbx6Okx2f4o;w2N2K-6N?Y|2BkT_TE4+ruXcYPxcD?{V?OS zKg(2y6Huc~7w0vnn3Bl;>sj@m4Kg;!t1%}wxbR&@6*+a*#UBUb+h31*0e0Ar%a3y3 zqoy_5r#J!KH<&>6wLYg>Y)iT{Y)kA{Mt~eh7SNrOJx_vD(D5ODb`6q~lTB+96XHeR zh}Wvb1s9T@K5e??B^5DM08`}@eVWS0!U1fY0EkB1h@VAp=}$77U|)rdOw| zbMiYiOkG_H>VP<7K%5Vr!~T!ytHAkz=2T&3GNBY%aslVnPB!2V#)tW?7ctxWJlDM!6dric+46qHOn6;`VGE-oE9NsI0I|>>^wpDKg?^^E(`JYL zfPMcN%-Cqns|~cZT-!`fY+R=n?^6 zAzCIZZHoVIWcaX&PgA26Pvq#M&h~&2Fue8#BWIrfaN&|QYPpit3V3mp-m_D#^3eRm zM88mV9OYBOGR`?CoCO2g3wJ&d@8k&>mNO|t3s^-wJ=1_9BJD^#;H`7jHbNw+bv=qHbEYB zktUdE+HRMpAo6aqX%yM1W&>pnk9pG}+|himx^F}OXB5p!jg{O zCEMv~6e1${Hp;!H)?hfT->qmgV`%+9aJ&7)gIhGZq?TW>qs6~9{&!&4QF^Yth^K`V zg{kJy@Hss7pu~R~=7Siwhr*V}_C5%;<<~(pawINu@gvo_w?LQ9yaGMaRS;vJSqbu3 zi@~1!Ui7b2;~>0hR=G3AA=W=@3<$&0Al!^c=BW`PEgGFB@hpfnA6SO$SFWEmYFqpotnf!pFa;u~o} z{z2(Cx%7FW>Ml{(ZjVJLmrOY_Pvcz5d)&vo@l!(i`-G__!G--fjkC`Ut+fY2Jn+4S z%jSfa^>|r1*MUaTd*nrcjE{l|BX4_=Rx->^t{9iL(c$f?6o@LuPi-^T@?*Hd);I#S-rF^hV|fWBsyvfFq?UB&C- zEG04)-Ta@g6^8O1kSP<w#aqWj|5^F3tH(c?9f z<8Owo%i9jRMD}6;7Iv}rodnyRc98vClKgTHo;7B3AgRXr`N}rbdBK5SiMSHOjTmQB zgh%)%5Gi7=Tqm&7$3gs5y#+_6Ch*j&{y*iNq(rzLob~?n?oB^z%XnN^-Fu5N)t<%) z>b@t)%-;ixlo~h=c2OoW`NIDs)Ea5|=@ZTVC)>%r!sTgMDuO9A6*j)%c*a?Uh5wN% z;B5yj22if|lHBWr^Z<9!oM!ZzWNekZY1TZf1$!dwXO&Eq8$vDkvyrv|O-mVuS>zP7 zIMNn=Q)#~!6MQ~!1O6T0J8xyGlw1(0uO*H74(wrl0M^&hL#!OFgwME%N$;mj$Mu7j z(2A{IKtjfuD4l`KE}3bahhp3-q<-`MJ)fAu*tv9^;zt@s*C6J^Tof(!a!aN?KN$&;b?nYA=}&kp!*53e>5fL_2I+!v zq|W3ptTWoF-Y=U!!B^QrK9B0Wl3#_> z#;&`pguh99!JaSGTD)Mxw>9lg>Y+LKKAi=Q=EX+`=XiJd|Bv04FWu^AX-}|E(ym?C zT6Dh1Y!&^v-|%QXy5*e-bu{gqG0U!;65#}Y)a(6JhgGiouD-JS>3w4_GdkXmm{W5*ZWPijCS8mk0$qS%f55i zp!C9N?t#WLpHu`!`E*AId$pD5AztMtgg->eTbB?xS6y4p`ZL|07l@q0G9I`*^XTBl zo}t9o>+0RRykdj2OyjN%=S#2r41ec&zcsB1aiN4#H23%ImX&i9fAb*Plyd`j>pY^_ z*LEoC{mvD|hS7NkY7C^jC`s2(uytSymG>1rZM+BJMpGY8*M9REYD9J^Y)9m6wVC-8 zq8x!YSrft)qo>^rYAmE0C!2EItpA1tJAwB)i*6y!$4Pm2jl~9?Cx_}Maq>Px#FRX_t8?$U1u+dKR=-@(UY|#?3g@jV0 zzD&33zmeG=f~6;h<=yWNnoILpg;^cK@g-BUevt2#Cm}|iJ(JxLXF-0MA!hcnq-!Zm zHGN{`SNK~$2HOL%GhNd)A1FTlp}O(ff~sGw39cPNMRuBA()db-1cpYmNl(?g+b5k5 zKa4-yypHD#i_;BR=(i6L%srCsTs5oDjp*PSYuDy=NAqB?f$JUlK&?pqRkMoUQD<8Z zr)hJ&K^AWe$C6u&k2)n1<~91-G?QSJ)K$R#g8&QWc-q`t>XrTTeOI!Fw{KS~AaGh2 z&wJDa>)#y5(wWOoL{i_GQrAq)iPaoi6T4Q%s0Ug-}~NYqGgnK z)m7v1as+|Fji8>bFX(7NGG{`X`yb2CG?WOXCz_|{oT^s=%TtaHo~bGk%Jcu^khv5( z>LFTmOi;@gqQrn~@ch}X{fF^p!>8i2Kp|b@!pnxWOqJxnVBd%$1l*6kmQ3&@;R*Kn zbDN^h{k451ti_Ph&=_d*9=@f47Q0yH%Tu4Ov3}yHlzgYWHfFU*?qDUjO1JEDZiY}A z@4==CWhOje&M(y^@oVSgvo_jW$upE@i`L{8zlJ<9l@^*%ZbIh^xjmJ$6EXz0J-s{= zrSHtRO%w0#{QjD3_m7Y>$TX<{1pT0vsUwg2g+1`N4tun0@Ap#=qsckNTQT<3S0(U#vf^^J?e)pl zmk6zk^NG5P?!yQ7Md!b~x=P0)@V-!0cPVn)wJi~CpQ!uA&hI3vtcRvvN4xty{sss|MGQC(_N_3Nzm@$KxPeuY(B_KAoN&Nv%Kwz6M(oXltK zP-*y4X(cYg0L>(-OSC>2Yd^vMXB6H4GH%(10u-6|sWk{b1srLKPakVk)TSgVm^7&`WJ`A75kQgupCI&#wFBWR|FK2nP*BFGT%|+*z zObpWF!ndrD8vaT>ONjEc2^`G;ReO6T%S3D>|4S5(W7fgC3|u)ncR};VxBt{RJJ^?X zCyYsS)=+tw_HIklDrj6qvw>48)S21GkcNuD7rZ>On&>{MiS<<*hx>;K^S&q9F8-`! z>!SIJTiRzii!t@9p%?>{2d1q?*}VGa3Dmsnrll_SN$y5b2Gqna386ukIC1?2pn4dNK< zhllMPr}}T|;RjI3k|avOd4V?rL(DXVu9Kn>xD{ZQlKBwQO_s zl&A832GO0Psd(Idq+9%FQqh-$R+Dj-zvTrE5N&_+a$hm|=)Tf3Us^4ZS5mua%E{wd zayGuLZ&)Z89_4K*pf$=nS{WZzX-w0jgtl$jw@TU~uJ@ji7jF{iwYDzI-H{CO@_Jjq zw$fcK`M4_@18-hs=lU_df}MZ=QjYR8qJw)+4mYh){+j_Ek#&~0zV)}k1<}Dy(pJ)| z9Vx3(W8k8&@C);To4KMcF_KQ0$M@-4e9Zl?$Pud(KvZOXp)e6l*nZ8Q`jQkQcxQR8 zC;bb_a8ietwX>;*4d%a`&+9VGcZ)Qy4}LTz=*#HDDNuTf-Bx-`Q?JM zA%Be9TMyfef;foKNLV0o63d^hJe%L0k=K!?ql0tg9*+zET~|< zcTybcfH3LW+11H;L6zg1=l>ay5#Ita1_&%X^6n3fJZT`PBGQHmShiHy(dJ|6w)G4?7%@1)59A)sX zv~DmGfq>-U*I*XLJPsQi_3^J;dd^?8NyY4njt$Y$+qP?VZZg1LzdbnB*T$AaE192o zhxhJF(IpBAt+AnD6@m(6iq@0kP|E+`GPs#ob%~=-#-48Ywv;vZwkBJ3GCZ&$cj?*U zI9QF|ZAs_#&hp9;eQ>R#bX$p$c0xK$8+K+s@27r4Td7y@OS5x=4~S6%ciDHc#P<)J za)Z5iEbKfspRV?sQaSr4?9X93M(}J__xRjoGNNx4xa;I&*`n<3;kgSjtprt%T{Cup zZ>R1T_b;Qhvo;~>Ax5d6hb2qo(x>6;|8&vFtvPbzWsdv-rAYBtXHC;(nH2tu)(^ws zEt=>D1eKyK5t!temGZUAsrssaCVEyJHlU^{vRyoC8f^pJ(kDA_SYR@sqZycGcvlc_ zT9q_Q^D=sTl{)EC8y}Zdo<_Fe_?fVC!U_eWr_L_plc)nuq^;zpT!=@f-0dN$U?)+S;Sx;~eI(hFv z{pcf<8LR3@U}g>J7Nlf{l?a5sZY!&k5V=U9-nH7RyuNJL@`}{{y4!1Jx5DqdVL6(Z zWr`(pw0B29-Cu}_oMU-`;34V;73B&UE~Ies=wmirB>(k3FOnUZ0siarro0GD&fwKw zLk{v$sE>_w=aK80wr`%4K*nQYox*AiMqp3;>huAv{xXmKeY%@c58Hl3xwVYM3F55@ z&i{RCw!AieJR^)oCi5L>#F%IxAy+Xj(~yGAQ(ht{0lg8%bNWsIRnWd|+2%fa?QbFZ2by^b-7U@c^*zs}{{i$zUJ6B%eE2AgDsA^x2)# z)qR~$1Z8MEP0A`RXAJ*%BaFd0$s3)(d-+#iN}?sVOdDwCg^qK^%31^WB-`Q7`mc9} zIOnL9sq(~#AaVg@(35fApS4x?iGs2*pqN+Dohiskv#?0EV>)uKJLH*qTj0|KOjL>< zWfdrYQe>X|JfiMHpjsE}$*kW&kXO^7C$aPn6ziE54bA6SH^?{h(GpBS;RTto0 zV^vGB?MSg`tI4|5Stxj%W)T#|BnMsjI@*^;UP7-Sj#g}%;oivoq9p`7*x>U7QCJRC z9jO8m+44P26?WCWn5#|ep)UIRZ|vVWrPf{|+&PixMpmkF@u~g8^G>;Zfafa?JtBeRhKV(bwsZ{)Spm4UY^d)95t=tV6+dXerHzCZD9Z z3nNwDmbh~QoJwJT7cEyo4(f*%hy|@QM%?B`PgXhfPLx(AMCoRCq=+_68ZBusS zs|bqEJi3|QLr=D1_H$)*wtHIwoBL5)y0#2eM z^1ofO#OIOQ2Db%7s!RA;6M1(wY%4u~b30qbRy#W+5Mu1+X%cf6mZM+=1!k6~4XwX9 zo-wAYC05w?GEK@Uyh5;uj@Axf=;Q1j^R$y*RRtu{!oF1wW|fDeqf`@(dsFJ6&X%Gac{}JAb4KQ) z8!&CdF@7-5Nx^nCUJ{T0f$;$$tS~5C#fJCcP#VZoVNZd=VnCd_gtBMXSR5qgoNw^CTU44#y7V6MGP;)9ff2p0 zJIe(x(OJ5Q7UjEd)bI+EJ+Z2@(o~VH_vv={1)o9Y_r`ud--6149!R~7q^n}URdeO7 zng0G8OTok4d~|--e%Y;K3|(s4M6Jr!|1dPE0mFYL6F9S#WDhKAP#*a$&73#im1cgg zWQr@xwNdy5Um=)5w_dr05ek=>;&@*0t!iO^0HQIq#w|~E>=*nKy#oyABju0D-LMEK zn8ib~(1FXBJkofky5P)kz;Np<sq5XZmMYImx)dM@(j5fZsLCx`qb+;z zB7-+Sx{mIcnM`0d^zPuA1iF(ec|^LzPOQ)X2G_5VAd$0(*&Y0S!7DIC4T9-33B9*x z%JKwdz+NrbFxPy{s`n3g+_@r`E*7X(LE=QXOC83Wj4t*9ei6eVQ>c-Zkt37WyTkfY zU}jEE*n<-IH#S2Ab&_c6eI~qX@W!ut1pE^+m;zSJyc80M;ZL-b$XW~o7BnP8ZO%Wx zYxjsi=gl$5(rPdw^IJx1?z$q=_>bu|);`94vL)XZsom;z@q0lvT0~7&D(>QbEt*A! zUx2g%#60cH@^Jt4@5w5;$qrqP1 zp&jfomoV;#&lTK4UsmqRJ5%INj(xjCNYTWqA|PHh25EwEFf}7ebT-%rJ2sIpuB~_a zAXF6^fka}?@?I}KlW2pCS^t|Fk2|r0oxfU;w_pKIK%ETHJUc9(oSwhO=Xm#h!JD*k zW}wV63+!_<4W9@~F+iVkMzLVlAI1X5G=!KO$*GPwXc z2cb8r^DM&CN)n$OinQ&$!3+n-*+J{rg4I+&b88d`n5l7sUcDdKo2$+dt;};F)AeXu zZkD_y))^F1oe(7hThu2VN|0>6>WbHj#20w!J#^L3a#k&`1sJzbIXcg>$Cg>i{rc{Z zE~=Wd7)a`=+=s!(wa(?E`e0`uDd-K^bg{7xdXYS+mE#HF%fXE^*RhNd+Rh0IK+@c* z`!(fNo(%=`|CwAM{Q&T*fA2`Y)4>o*dXx%sT8XrVkX(CW2|dB^l6c05omvB zbcbb+6~2^Qu#9kS2I#v7Ch8B#{Y~3_oVp*ApJH?tq6H=-xD3n_$zD~>GQ>y`O5i7g=&1tS$LB?GV}i_hn(_k$UJG|! z2}$mOVgUepzE6@SWw}K5-{|wgkSTbA3PqJr98PR>GYtx{`A$7{U4j1V_jUpgWr24_ z4wPZw9aWwEMi&fvjo)r1`5GH-IYovvwW%%fzn~C4>SM?8OtMw%m8kEF1NqkxXWG9v z6k!5^iyAtMtfNDP*e0XHifvH7!K~Yp=K3~dJ_fgkf>iu-AMrW=mP+`jm zhSieO+BaxHj-3?3`#y?e7Q+AW_kerPW|Wcr`9}02>5p0>o5mx=0ftWFmK5myv(E6& z=nnR;{;8~F=)h2goZQnzjRD;)t%)@S*R@QmHl^~KSnG?^(FO-c8teiExn$z62$%$ z*iFhJ-%-IaZh>#mG2P>X&x4gaNrn1A3>5>CP(`ukSJ2|J7JWRhqhvUr|16sD2f@Qc z?QM=CI9gKXd&Rk~5;=wt3jb7=CTK{Icyg}k$PgSy(_a<9kA#kqAmFm20(_}We;I-D7)tBhu8E<~*u>c;tDfZ-7Ar)-D zldR#8{xsfy6?B%VwKJCvl1>L}N}{4sw2l}6?stekjlP7hCOAlIT&&aWvWZhm*;I!I z4lxZucxqZ@)nL;)kE%4c2ZhpHpXO;Mu)}BLm=&}ETmxB53ZSB59ZQ^hnA}7NC!`Qp zOWuaSy@R4m0hU21R<>Qe@sP zE9o>+XaC8D0Jv(wZ7VFnQ~GH zJtVj45?qb4+ebNA@x{q%xF)=PxGl5?(5mw%04Ix}M`I*OOcU>VVRqRx9n?8bHjNye zTSVnkm7Hq3^2ub<2cZ1itSdexipP*F zapcz_6j6~Dhr2$DrnPE8@7$StV^*R7XI(&B;9KK=n!jXQI_m3PdIvyNp;R_9{)dM2 z#D7eCQqsk=cVen|)VFy$^91;awIj?B0&KxPc5dTXeZqtMJlI3bC?ZmEu@Q2x>5c?* zTAe~nIl=Vu5fg)Ik5Segv;MY+P~>xC3B5smu zQDqui)MNrFf$lggLONYpevjM=O77g9g_-Gb9nw{ z{THczB}2`$6IMuZz_2fb@x8ZTua%El!1}NI2lMAd9vv4kI{zAN1yzT}aaXG%8M(Fk zeU|)YR3%dXR^Ko!MM19e4RjKDLX3Ag#6J7m5#E>4p9?`#zYJPKH3MG#RF<-LtFPk zAH`psASYWhZAy5|=~uv~k>5friP~~PakZBRB9qxWKh}5mwi-c*>7WbZYRDkJ9o(4E z7G>qA1IrJ)C%DtJT~@_8xhwCy_7$jw)U4|$#1vpe)bhe`1>Z&iE2)V$S$vCfmObZn z2(pf1zP^<(Zm74`V3EWG>FljeB>H%%^n~ zyt%^?p*bHWx(c^HZ!9h&pX3CML@fs|b$%qye06yM#BJcx|3cY^qgb0t?n5FX3h{MY zOcOjt52PC3jSfC+tsQ__%t?<8TDV;3i}1w6Z|{q2nnE^omOpJ_GN5f|DI<*T_IOGR z`ZN#QUzQ>+&Cgvh4)of^FAL$?3hoE3FjT}}Neqv;t&Xad6pPkEPdP1SJ1Km0yqqo967zFUU|HSXhal(Z24>&YxaNLjHOX} zD~OKjdTYG~u_lnyT?|F6FNmv6O*6XdxuJ8G=4<>NSQp*$ashG|4f2z_~`OP8I+*YQG|HjT6SeME!ZRmnklo%=CnD*5A*`CG3z^;s4m%eM1P9!Wj zf*b3i$GMY#@BM%j``Uzzn$s1s7Crg95o2mHeX7T=Vu6ls67A%05r+gQ8jsD{p+)(x z^hm0C79?1!*Cx{Ce-Ta$ZeyrptuyC4W%}5fNgLOX6p{Kj;D^`DLIQ21Xr6cIcZy%m zkoAYC12LwCg#vTkLoPCq_sATo(jOyGY&ge%6TX*5Dnwn*AghgZniwJ62#liq>uE)o zJoD}{@&dTUxfxU$dcaN92ilSe{Z|aqr*?;0|3PxzfHw)=3+|w?r-R6*!6x!)@P`KU z-*h-cbR3^go_KY?YEl`|CXWBoRV1hh{f=umcLhxqbtXf^FcpIEAhyd>){0SbQm})l zfm{VK@%xH9hISGGx767IS-bS#wrwf#b_!`R?dkCoxt_*adx(O&oqWW!$Juo6>~1Ex zHtp0?g{}MUqhBFVVzIGDrtkM1HQC^M6HHr^=2KBdL+6N*CH@o$D)SV7y3F;C@clDH z45G7$3U@vJ2$E{tJ1`gHrXRbZlb5FVm!ibpwlwufJm!>o2Rdm2a|DimygAIw5yT-v zdQpZv0q=coyY@Rfa0XShkORWt1@EXGzRqyC?0(7ovNQB8e=xM}u1+vL@m1wmNGm!F zd}I})1vDes&NW5P6L@cvJO!rVX0E&ZPK&8km;&qQx5p*Heo8Fe8`9h!P)DbyYbCL! zHX-Z-TSK@KsyXr;s99L@rJ#;+F~n-gkRp?mk>4k+U2=gvLV1N>Mezyd$sz_G}WwRO}#T0y$H}> zTZEp4tizS^{2^ljLzC@B-YPS5NuB+|Mw{70F}q^m!yS;H1`owP;sg~V#DoB5!!b(ajywu!-RVR-I6x*1|G)^)1k~f|H^ZG@LpodXWth>VAxaWIs#u zyC9cj+xD+iY4Y}6biw47;9Fw{5DwMw_Gmw(7^uel1hoH?m=&Ad~&D9 zZ^=I8#iJ?E;b7o%G_ZC+_A!=I#^Q5~iF;r6@Gf8TB>zNWH*Loh=qBdtnMgPs(vW%y zJ$_L4F*26#t;wQ>zbd#hu?X>E*96`){gPVEbvoO7mw0Fh-`v=p`<0PBDBNuDiNHVq zO?S17htL9pzvEpzT~$FY|G4*YJbpaYB`eBG0fMSEi-G*_!FHdXGaY=LMeTlNCJ_TDmvwpaQmN{yN-pw%ztBhY`t zVTio$LUJdhQG$-}S?FJp`dAx?T^Z?$0dzbQskg`62}wyq<*ySk#d~B2XU0}(*4=dp2-&Fi*ZgT8V@iH-@5|!&nNLnw|lZ_u9jekAK_9g zgkysn8P?EW34GuZ(y4*33X(*COj}_C!XY>5f_Xy8woG9$M_kGm6Y`QlH zjEK{Xs+LP(m^C8syHb^Ob}rEbzhTS{r$iMiQuS~354iWMHRL)Qb-p%w^k za;8%h^s4KvSNThw{j1H{=g=gtq0zwex`|1&38BzaZ@8V;N@LAq`>*6Xbna&(#lnE!5>t>#!VTguFlq{T8H_tSSSrJ!r8k^P# z1(&<}ME3K{Js!2|?wCdNy${wN>OkpMV9Lbl-+U?08HZ9XHg4JI8(E@YB!XQ1_RVb*$YFr}x z4+Wp)$aE~7f>R0h-P2`bNr$8IHpXR{VN8f3RzOT_c1Qbc=Og~HS--U4?laJP%W0Uh z_S3>#8kg8}oY?@nIX{LvT7~N{*5yoSBd`H-Op@t*WI28ktnJ5wr|)tZk2IbpIriq4 zGFt})olWK$$y$1A3o8?N1KCubWwJ(t_O7)F!(ZnWn;ig)Vb(}Mq`wKN@Z*jtQR4N`EM~o?J-SPsC>q-=gjiJXN?r|h>rVVhG#HC z!^GifQ!pB3S`8Y5Ht$S-1W7f|$x8kuX_wri|11@e<4_Z^K-RtwFHk&otm@H!(rHV_ zyUzQI7LcdoXek_k5Jn(@*v7^>sZ#tf@tEnXqnYQxnU0NE!=|Y%+utbNwL~Fe_@p)) zHhQ8HPl?gqnPYgp(-)Jincn0kKhh!3KbpfxjyBNC}Amuc|x4LTxEAqKl%bQMlTQx2+9hj zr#czMR}6-w$HaOE>V5UNCpgLiP}9<(8Dth5S3*2d!B)tESaH2-CYX+A5M%j4DR2e) zbTw*%1(-x4l;zilyqTZ8=MWE}loW&!x;v-J%K2MnPl2WQqZq3jvAiH4#cbhQ1)2ms z49J`q5pSj7fDiG1{xtMFzkDTbC0p~wVA$X!#34yQ6pmr3KpUexsG~vom1i0Q#pR@_ zvNxglDhk}I0cRqpR=P>BXANlT5T$g#4d+a|Dk$I1``1$Q^iz&Gq&{v~Ac5~bz=2;( z$(RhtqN70f>08Jwu+|k$OE54SdKGOoF=5%2SqD-4AbC;mqkxk_{W}z<6T2oyfoW1> zrF2^?b7GXUNZ^+>Ddv5Zn`M|IXaZQ1@`&*#!W>12nBy1IN-7s`B|#>Y>IJbe8{*X0 zkTAHZ&VJIO(OG-@HW2=Fy}FhrWJbUrU;CJr>G6W}vJVJY6P<3JlWeGW=3sd_x$0wk zFY7yeq2M+q`FNxPjslw55XZ5j&QN3hDpkqJ3p-7v=$H7iAo-0U^za6UF?)Zq>skzt z^S-kI-?6ZGQqED!whRnX?=5>9N(UCA8zE5y>@b!F;yj+^O zx86P8BiJzdRiN&%s#EUpcOVYhma-eG8lp za(WP$g=O8>gxwds-x92IjCbeo53X1=`wlA*Vhh6U7{n%K3 zDyt}To+>@I&WWn!x#XYB-{B2O5=MEHHsya`OYzwBxD1E=jCBD`Y;AmvkDGkk^q!C8 z?s)GkO2uc4=X9U5zQF=Zg? z{22rXxHi*SKpTf0Klt(}ao|%v zSWqYwV0iBBmeNdmUG;)|9ZMvp!#Tx?Oz#Z4l>kR| zxyDQT%zvnQD98==9sVWMj07oi3>ND?GYf3)Zn)}IX?!fqER^)jvYcKnvH zh$R1W&hl`)*sjdfj0fhBa-jn4i3NzN8HVNLhQV#t5vrMskabQjc_ipt2HPh*&wq{? z!%?IIfc_Ec#ovytn`l`9Q9uBq7!%wQ5;l)3b?Qi;$^sh!YW&}FUpOK~KE#hmGrzx1 z1zaz~6to z``zyJZ zV`6fwS;C4a{ZSC=yG!XEL$WkeB%dImgEuzKKS9$$CoM;wCb;HTy6gWX#yy+v2{>cY z)<2~SIS-R#^=zvOxQ{8II+a$TAg6w?l?E4yti?o_yNPERNb6SDJ9DMHSWpRxengVS zmX%XK1^U}9sQUV%Gn}YAeM_cHi&bbV_o4mrp^uO!UIp~?lZBJ5x-T}0zcx8pv1uW{ zNl|jT7oGfs^9wGA>#4+OWmr|nl~mKLcb(jvIV=1H66 z!3gI-MnjC3_>o@n_FA z+y z-=o&?whbX+a^hVmzqBUAe{wM5+Jzq_SRb@LBr>?YPuAd^%yKM1K&meSVa>8sc1awe_Xap+ImUeZjoAfw>8`wzWhh#z41E}sPbRLX+-m6njsrNevSAU3G z12dCr*Wh}&1{o!ge7qLqIt;|hPqt@*;aX2w8pI&toYKp5bB}tx^rM->PPgAs)CRa| z-kKEEGG4K^_Ee*DGAx?e3HYVCAOjol*G0-5KkyR4gM>U+n!2>#BeFmws&{RPDLTvY zv8Tck4++*S_Nn9(Q|0j?)TCdSdHj%U*_5;VD4^}jfd$owXIFAUf{a_gN_b>ysI7+u zt+8-776IT^_lQzs3q_7u4&V+>tJp%bktnZ_=NHBhKtq9s|4FQJ zfWd%`NADdtn2A$)@kVQ^vs7pDNjvMwMIj$9O2Oqtdf9aO`&a)A=fG>=PO|=mgZvPv zkc;2G!OkqFk>4%nH`5m=!xCm8jlHqOx=+YQ_+KF*aNF6Xe_N{Ns6fr{zM)muJpojl z)x?zJ#JM-#0o4Ys@Tym|!Hl3F|&+I%bkjVH!|o%Gw^svajP zi>rZK2S63%;P$#^fcAa{YcPs9%72cgrYh}zI~i;Nmn6Vlcl}Htb{we6z4SuEhf%`7 z7-(`ol4`sg?V*V*WrWRbxFJJQi`2fJ)tRN`Z*Oc01;3uNv~e&?+rd>qsLHVP&Pj&g z|1@M*MC<0-gr5*TAG|6pxQtGR-tF0KkSc&)U`bK{)Uf3s@benb8L$C);QCT`8L0qN zIG}KUJS#_E@ATTmGBS9n-UNKP08_zYH~!fb`vjMiX&*MJe-&>9+emc{xHArH#$`yl zMna|5q{g7us~A{P-#}|fYAoQsB7tI`hnU9cJNe^D*zI;MOIAV@6w>#9SbGvq?PW32 zMGKb(hhp?g5_HV$hL9J2-!w1Onq4!VHw9R>VfW5OT+{V4ZpDA(_n)0vf|nRgUqcM5 z6oC(E2XpeF8r^Lr=Z^i^)03y}Cp*=7 zVO?uyGh9;Q_oV$5NlKHAdZ1BeI;-5&++W}@` zGNA=dv;~fz6DCi$ffz~E!@X0R=&)wV!gkgHDHBM>IWXWOtWm`ho2shKf0b^L7z04C!g={ZdrmO$+E%@A$F#tC6C%OR` zh}O3l8}#BDJ}>p<)DMuzi;}T|is9)xW-56CLr!>@4_k=cZ>^SZuZj(_Amw7#dbCBE zov6uhmZ5k<5_!8MUjgJZ4jj6@M!Gut^fFImD=<89I!;CG>yz;VZiorU^E^6&-o}9L z$nJSAs;+Z4sM4{v=_}>;1oxV@+YSg2h}Vxb7bEtQZ_cY-hCA)30pMBeJ%G=+yqGaH zJ*PT~n(C`6JAAGlsO!&(cCg*wh%qE$bI@_Pw`IG=G#A}$V&ZY_CXGjI-6^;Kgn7o| zEbK7|e!HeTp28(|tSw-Ay1;2cRj^~=|55ej0Zm@n+d-`8RH2SjQQ4-$jErMZl$JeE zYb(==pt3~vC;~2Q$|hlpr4=m*T3bNbR8$}#1X;qqxq!;1LLkVhtOf)D0}=$j=O)2eRhtcMDNgi}3AgVGCx+3%sEhLY(W2Z8!fbN>7Ap_IEkjx)ZK<5ju{=Zp?@boioF#E__{B<)Y@A` zWm%~gm>YQ2(oP(^fQFZA9DJV*ra%&eZr@o3X2n|jvDq|P=EYzaQQ-gm-rH&vth}+K zcQ>8T?XhsMaRwFUW=e}#cHP-(A~6RkkT!@;c@)oBk7ID9eZ;dQT;}5er54tH!<2FtcVLa0 z;B(0pv*X&#$n}_S08HEtg(LU8%i-un|B{Rs*U8$7I`#e6joj zSN0l*sW_#g&3Ke3C2L)fH3q^C`%!%8PL}1Ku=AZuVv|+>dfzS#C$Y6D&-kv}SjwR; z7>?!1_Fd(Sx?cI-WrIZKa&F@faL(QMQB_=UAYn>VVJU*#tJo z>AfkmWGNOJmxpG1IYNo>!FSXGF&_^7mxjaj#FNCsJXdAgVb?^haX@{O_fWEw&X3o= z%{dtz6w@VQZ(qt+eO4`$m$>)?0=+t$FWi`>uG3$_&x0LrNwtBb-?H~-r1G*ILnZup z;60}+wG1Hk=5B=?ZO0dgDaqndeE!DGPz82-u7WFiZ0bAJDYV1K9yVcjSgT=^gp`4C zsYB!%>E~>eO2%-CKcO%$S7>rOlpDhHD>Bstn(o#B(T{vo3J;7VcB%+7bgo|A!Y9CwKm|Ke2r0oz+ydzHmO|C{eaV zm~sH-{sv=+80XV4&h5{>nCs1BX~3mqq!)%ce=a!ay8#|f5(ZTnmz);y)6{vL+u`!h z%_c|B#_(N9rvf=H6rU{%k`#O+_zZW`)Xkq}!fS55h!bJLRmGF(m3(|vrVDQaS)DnL zXGX^VI%(PMR1VLfRUgc5BL;vg(PhI0SEG686^TA>2;o1IP)%?KqV&@OyTns;86^GN zdj5V@#)t4Qn-AZ;-DVKi{ID@AZti`8dUE@MiMQd6H56v??2BhI_G9sE5Y;KW%S(_D zoxkv&MT|K9YKISVoSQ$~gu6R^P;{@Ao4Huq8&?jH>HxkH+C7oFQsTYjrN@Q2_padi z$eWprBX^tHT*|EdkCA}dL*FGOZSEI1Qy*?NBE|>bb>1}4SN|`$K7B6qRteN?_|_{5 z>mQ3)L)JYy21gaLt;+vBQ>+&2Zf%0FOHao#{5PaI^qwzy?-(^(a^}|)(@$3mJt$hl z#yT?2a{Ag}*@m$uV#qY|7alxpi;)(l^j3Ihj5RYJRWHo8qHtvi$=S42mhnj(qw+h9 zDuiExeE|sxDMQY#gQKiOJ&xk@Ir7CXj8d;rKwlCiE{(PvYB4lUZUGWpC zongK{b7~~maU75A7M5#}M^K{pSBV7OOt?N{Q^+wk0zEFkQf=zXJ+Mc+B@T;8oh>2P z`~$X)<719@N5Z)I)z-wubWMqUvYMy+M+rBl?RM**gkg`jQrpyd`Um8VXg2$Y@^?r4?Zc{;) zZQ)IAS+<$z?M6uA35hXM&QG*T{mS4-RA>g^pOy5JAKHhRt7dNr&n?9O)5Bc0976CL zY8g-jeRZU>xI~0FNz4{TVs&cWg)S-f5)D9#Km(Fy^t$pAgOhL#UnkY)={hfIi1z9k zvBkHEFH91O)6}>1p5tG^>3llJ>gZp-H6bfAHfX(4(clTv;RmGEZrkz)beNn)FS` z@#r9C)MK*PFl+s$`n;0vy_z`?sFrroyZAKO&4cWv_$rC%iFX4zS*nu#NS=E13XYZFbof^W6v2<<>~2`iZ!4t6i~{{((~(_G`VoeMgXa7< z@^2?9vPJ*?r98+<TeBfvk?in|gNGq#T$%IPL7Cq6y-bwX4) z(H%Z=PSvIh!4ukve_!JeIOen;r*V1CM(Mr#9I{Jh5y;Ne{CW2!dE&u-+Ro3~y)|gt zv$J6nKNJg3%$;d)d^^LR$Nox$!~wZ=V}rc{qv)vlpiIo5=Kr~OqFSaCGDSBKxm%5$ z#%1`l@jg*NwH&*{23hGc544-(=T&;xSz0oZU#BAKuD`f+K1TBt+~4J_>ah-8G%Nk}L>iG_8#`!w&Ppkukcj!Kn>G3jwk&9|VHs9Fk zWaYUn1(l9-L1;HZ(CJEo&P0hRwIA2qI5qlk-;#v#;Ls=hnchN=O_zsr)Zn-X)O7?k_o2&j_r=hN=Sb$3xlj~1DR)LTaV$)Ge?tID(OTu5Nl^Zu zJ8y}*mXvvvd@5Vj?#v*Aa_dKrBwKEDV%*-!q^xI$_RI|-j~@)SATH}(VZ(p9_x;G+ z>;_!4YGSq%+1lGX_?G`HbM>ow(0Npfa#Wow7By(8CC_fo2HA@8vHaD!XO{O~?~+``;{wVoS~A|S9cK6J;pCzvAD*HWXu%Ex znpf|TF1kK+Ywv7Wk7L)VDgC_3bENRgjKJ2eQkHtMQ$9PJZo}%~NnG$G4xAEnG?MSh zTvHk>LLR9-Wx|$I9SC2%CEO_2vQtql+lz)QZKcm!slk!&{(-*_$xCtUK>nIuL5PoE z)pAKG0*9nDkXGnDpR#Ul=oG(U`ye;fidtDRSUqUgy_An{%_zT&@fo7b?jcyFW+E-6ZU764|XezEZ#cX~-BX z%=>hFprauFV|mGYk7QdX z8%PaYF9p2o_h<(zTePNF9VuCPXI7A2bds5Vo;q^c9HPunf{VbQ7!a;G`eJHVO;YJ8YmB#0uy zdqHWTjV}C?$Aj(GJ&2&(2J_e;Dwr{^1To9se^X-=40RZ?wxidecyAqU>*(q67sJ6c z;N**Fd+><8g0E7w%D|VLkI`rDu-Yp%rYOM^)i^+YnD%9r9#jB}>WtirI(4*C^6$thAYbhYvqzhVbk{^9GW~QnHqggN(+M zvJ$xwa+W~YJ@SYF?FzJTbkep5@AaP%b|U&+e+^R)DAFK57OBfc@>W`uPD9E4DfIZz zjFNp$&ThVqEHCHslYT36lB0u+yI(z0q|A9s06t#T8rajHbZ04C-fb>L#4OT+a{KHIbD~OsK8^$ zX9TWMWT|pKF)k2YkKBtxJNnS{e|xd&ML8S1!fKDQ0trs^8NC!StEKr-O2V%aC_}Ie zA5rTI$zJ$X_rdg-!O~*#9t2C%sC)gPStzYFq8Dg75e9GF`r_Bh^uH{vjWG}_FCzMIRbms8{=$pO84v%@Psq53At?L7LRPaxi6qli+s?3Jc zv8BzgIVpiIwQMULA-}L(O9kl>fi!kY^GZo5rRj4Y`(yGhUfQsi@$p$7Nia&ZU-W-C z=^x)(c@DQyh7#)4T}?+24AR3T_)(Rr{#7}q=h7fCpTpx>WBYNKet!J^(1^44A{bi< zamC=90aGAIV^y{-@EP_RN1(tu1Aw70X~W^;XIyV*U01!vtTxSeySk4ul4)CDc98&S znT~mqXw1I%FQim}-(YpfChL7Qe}>@fyzX%ufNRN>hn6jrA`y(G-=ECzT(%T3)DuD$kOgO{#M8m?cE6aNi zh14K|nH&d1fRnO!nkUTWds}I&@j6^8fgi)s)AssWGZg3MOaK9dOTxLT^*;CCbsORB zs^p8FeEc3W_9&aFsjA$w334DbALemGc%tRXcxJ6rQA_S5<2hI}_Hae^s+_&+k&2&?q~iNdA(HM`ik5nxhvp-lOL_Li z>X~VwIUk;v`8u&V!Tk!c+(nlpHdjv`QIiJ*xg;;=GF2n&PE+3?OcFM7 zDz*WVlkrL78F?x}O|U$r-FT-W=)XySH}hQBj^7PxuzJ8GsILXANzNet;;V4r{=7kX z5S?Ie%0)CQ6PVkC_+seXYDc6zyu6(qrh${z;p70u?CsP4eXs z9g;t@dS5m-v%=eNrIJX3v{D9N!~H1boY2-~Y~OiKMi2!AO;W3&P!j!;u>d}+wwXw) z6ZD&7)~LiSSMqfuMf<9dhEs?K0uKG}M7%zM&(bR$X950j>LSaFmi#qZkY<*kQ<=e& z+2~Nl*S>)>M2{hLhipk1LvYK2oRN6UH6ah$`@Xz{F^tAzJ^)fSN)5~EI~-sYvBB<= zQpGcz;m(#rK$LXeu>S8&lM*y+7K8nk&ke5PdjqT*^u`S#-m~GjB)jEBUkRcJY@~5E zMPsn+io=4Z-%5?qKS;kerIb_;_a%4xklm7SWgnHcO_vl|1YcpB@cvjjj2Gr+gfV=5 zG|G8N>;>BD1Z#Xjuz_JBMPt2dQak!BP38Z*50(#{;pXEh3c{sNhE}s8CWI zatUymxvmXw{&yG$rstBU%n4U@oJUz_dh`d-t70G=&?o>N0y!t@tE? zH9#ze;D~ey=!)GM9p>O?|#V zHl8gWMyGe-nelorq{#fq#Dbw)A|%JAH;s1H*3?1Db@XI4Yy)Dnl`L zTTF<|Xf9H`@+a7p%%GxrsTD*9nJ`CN)H3)f#c3XCDf3104hzpQx3~LJ_Q+^`k{F_D zO8oTHHtFCt@phT3nVsb2c(%&vl@_ac1DzbE}J-DC)WN-9~ZT1AIx z^R$s2eA5+yln;fDo2E$kUH^2m1-{8jY=8M{Ur54L3T3}2i}VYPUJtfLWG~~ht299S zF{>fzrfq=QR02h8UJ*D7NQYo#y*DwM#J8v+ zI%vIVg^g0tbQl?};pTGH5{P*3p*Jr^wo89n^{=@mVE|`*L{!a86kQX75k=Y7_&7~g zUchQtW2X~jX;{T;f9KnM`hFYMFrxpHGtwXS{^Vp$MNO{0yb$o>OEf+zx?_HdwY%4F z3H*p6z2i&WkWdhcC|bHp#}y`V-Vc0Sb+^&8e4={d@SCGDPTiR|gyoR4CeCxgDusWJ ziv^K;0~5RSgsuANB~ySkOCglXOe6D3;A*M=u%fCre-a;n~PB=Mo{VQf+6tU(_|)g_1$i;K%ONNp9oNJdju3ZT-#6dK5aE16Mb z7zuv`_l=(_Jtj`>?S{lnc5n;F;INnO!F%G$Zf3*e?euNSaypp2(_lH4+`ry3QR&^6 z;Phy1uakGnC^BN!!eOtK%-12mba?8*-VWA-`huykhqBC^u#zmaOkUc)0Xn z_AkYdp}Vptax?+OS6Um|aD7r7Ri}Q(`HXQ3`X)Bd;5s-O%mdiUOF!8#)-U{pUx1%v zj1(+P=-yh!yN8JAd8QB}S@{wsGmBRC&#@T4(BabjMl)osfZ?aeMd<)dvl4DRal4RZ{~s z6^2jYSQ0ZW`4c#{WjHw?ANrZK&9Xed{bNmIZ>cw_?eCDw9ur6Wg~At=EEo zecWI#GxMR zE_dWarO`pHHppTHo^>ZqF=J8rE3J7gjn*kS_QsWf*H^{})7gpeOpyO+n$-z4Xu=}N z>8-7kAN$+gea3CrCpsS|Z!x=0-4?zB&uF(EtBz*UBOdy+_nVDVN#1-niy{3Cr2(1g zBkk4lL2+{L4k6(8lqQB)hd`sdC695d$3s>Yhyg}bT~a>n=)bU*P+>gcUFCH9Nec)l z-T%82+Qw9oC|xPF3WW85ic`v`_MY#>*ebk-xe=k}25%^8s3P(%f3#2aJr`N&Ang?y zam1qt2gbApXCXNvGw4MgNoTeU3KEtRlJYHCt*SkaJjM_H{AXV%$8V)=XK&LbUbDtX z&Aa&Jh+(qahU4wk3V4(Eun#$$KX?4p^f2kJ_W9P!2~B*puVHyiS{N;|KKtWTK0Kz^$%f&J2;4q?b1e%3FBqUB zU~W*KpAHOhkQfj9QJ=)}iTV8nFM35Xjd`QJkw)n%Z~J5@RXpe!l+NHN3)TW-kn9Al z!+_*6vY}Bl?|)JFt~F9O#KYq+S|Gs1d)Rj6HY8uq)0^B-x5iQJIwoq!yJukoDWV}h zuq~r;sZ2_tm`?PPx=YSl=L6I`2!1}Jrb z2?`?vWq0MKFp6XZ@ypcFdS5@bPI$?KT|LJXM$fsP!>i`(`*U9yXFHYOGVn+E=YJZF z*(HdlXLk%1`z0DDyI)T1IXhoK-ji(_ugCwKU6uq; zH=Ra9_vvTpyoK4y8myTrsVeTml$>Ls-zro&C0!;v#G`ak_oKPe`@+N$9H-^GyRI|P zIVSx9D*|>`q4yLsNH0w^RkO=5iTrHzVaHPbg3{HSIaV;54YqccK&fOUT@0APt-ZRl zEDZ7(y~FD-s!V<>z8>=tD5XV5>Z0QP3uB><~8v6YMiXB9`!YI}Z24rkTcrtFo z@pdygUmdxtX#>*zb^d4ks$vdFrX5(myCI|^(TtQ{gHk){;~({Gt;g*qR*DTsX)Cq% zft8hz;VKun?D36GBZVwY?S7c_Exv@uPX5elb!`+>RH|)CcSG=FZPiyBr<5xrjDG3g zZyt5lv2c`bzPwaVvb7*ZCH)C~Q?@;ZMLr+?jMr_+0$q!8r#4~eJ;y+>t7+rLcc=lo zkx2}`^p68{I>!53cc+ZR$RGYI%5zzOp+2MAMF*MJ=ZZ3ht|whi6drE-Qu18Il1W>* z4fD;+bs;K*T3dA4VGy&_(yTGk7|WLpE8AUMtQzoeBN;DJmGtMn4JiGd^o5u`r?P>_ z$(Tj%Xj;%3mYMF@$UpMbMTz`lw?F^!bcvnlH{s3*!-Q;iS}OWx6IQsS%-=l`IyJXz z=kVDeyRQNs`P!w}!@64j`O?9VzC(1r`OOLbUhS2#F=ufHTOGHjzBMu?n{)J|Y7~NO zB2+<|3OM`mlX|3=TKDK0{ABw+SHdlO<6fUv3Bz>pZIrOHrt5>kXzcecwLy~A4f<%K zazefAh}vG>{FWOISb|KWfks3gwoCOP zodEo5@$DgbFQy~!7&Xg&H6vs{)WiplRh`LZy?9e}rbzwC(F zg}D{Iy_$BZkFnQMpk@TngaJX&)_;{^xI**3Cj z=<#@eX3;A2463~!H+zmVx>}ltS7vxwL9=DKR(EEaY(NW^SlxTlq$y}k-Q~6RNBIm& zbvN<_kE^bIVX<7h*cG%0Ra~v;s5_0zH4gWw{o9|{Q>(})!^GCTQ{&IYe3;;~PDby& zn%u#wsGUN=k9iiuG;Dcce>HaT*i)dYtc*joc-rHM4Ja}6$v9Zf4Y%NM0_G4t#r*Q| zplAqL{JpF?&;Sz_X`x=CM)mLoNc14WJR1JNL&RzwSCcgy26|!W#SiH63(qjnjZj4# zwHg+QnHJmy#H;Al$MkFS_-jIbGJD!(5dO$g6epCcxmDl#IXjh`t!1!$129a`3l4Vp zZ>qAoL>-pU}B>5Bi6oZ5kBO9y(iM_X{^=a zjNU>;p><%-P@H4nL6I*XzEMU8^F=qsd5*97u!lPO)Au7+&d9IvI8+cuvWvl&{-~+G zI``*`{%mRuHo}pg!U`ELR(+VSw4wTjAJlub&R(I}9Ke+a^ep_c?0OzmUQB=2!syR| z{Z{g+xy*f7b?&~zV|A7lFc$G)eLn88_`#EGQ!pd02hsvP=Wi@%yxPU@Nj)R4m~(7Y z?x`$MzhcDc-t}R~9=PM#J}r+KJ&~_%d8(6N*0@f!7wp$0iZ#r$OU;)|yR;|I%KWZSx3dmQH9B0O8f zq@UVc(RFtyS!YBu1UgZ?lMgIMzGy-R8<4EO^et~5CS@`OwWob(7=9x|ZOtw^EFx*w0{R^bT{}#Mx1_1+cs~n> zIV1c)wtPy%o(A^1f@-hJ*iYk`VJJNjNzp;?_OHxeXMP&X zh}*q4A?o`M!&jN2@x2mf=ZS_M`ZBMNDL?b&Hs@p~#~{B)%jXY>-pjM_s0SBh8vM?(JhBhIE#cQ=~)>s9g)ZlcUrwY-xI{gwRP zpa0H(imX@#fPVc5)kS^2u2qlz@WjyYU-)&b?U}|}MW5(_me8(QcgOcf3r_6@MI#R8 zC)|8vd|}8)Vj+XNtq9{WuJo{f@>0#7vKo@%a@Z&>vh46poj$QbXF1*ahJ7r;WHgW7 z(d1>P!#RNAQ$BuCcXqtmLSDDG50EQ!mCusBsPZb=HPY-TD0Swh=o;JTQW{ih5Bj!i>HsRQvdQ=>v?y+c&7aXksQv! zz{qV~5cQBQS#K5fX2}5fbtS%4=7k`oPTdsjq5U%Bhx& zTkLci)=CDLG5cN~{htld3ZLOrNI0t%AYq+%@k?z@f0q-U-Ol(Y8qPlJ6=3+!@iZR2Mq?G1Sn31b8Z8$cMM*k6i|L zyj17{#>gQ1+BCH%2f2fuOJemRr8p{t(i6_b|N_xz*ova)QGXulel)0``8$>O? zm{Ox|{HxTJJXpcP*WP5}ipM=QT#E^Ym-J%vp03$Pior8-oXGu_{^nqFn-ETvh^3fc z(WWFgSYwow;X1Vm4Hx3n0NMv=^7k*{gzs}VwQ zsxt`9l9nqXbbA_PM{D@_mQXu|!-Kuy)=l6E?>4c)ms;8TYU5N+Ph5k0i>0Ef=*!4B zv$mU}^Fz_<;YOdcTT|bUTBgE6MZCjy-rQ^W5d+BC9#$s42OA}t;scW!&6Bg@;`nN( z^Hl2{L;Y611v5kNyX%yaNRQ(_e#iI~#&5w01Uw@~Kq8qWc~zf#73HEc5#_=5{^QTq zD!r`+b;y(~UHU+ay5PqeBaaukK^GaAW9D1%gv_SdF#a{E;CU2p&o;)e0qKI(zPZ94 z;#E}sUant=f?#UeZk_%D{%%totw5U@cr1s69SU-;?Xzhq#jAspT76&NdFxv)xt>7P z-5Lh{NL_M^_@SHha(9M)7k0P-`{1gI(w}- zBYm5Z5dNxt8=E&X6eJlylauVU0}kS24tMAEVa|Am%lN_#2D+JKeZ2hBg+1gA2iWn} z=nGhuwQ)-8N$#hqt=fu_EbW*O&DuL27YBx!Q5Y*iwpyXH3IY&kCr4zT(&r04UB??w zxtFQOMjiYHBz^i-_>RPmND)q45cq$CcT3)*<5b1`H713UX*tZ}XF^G})*cXb>&Hk^ z&kc~f6(IMcH^Z_(O+7C=-9bpB^-q)SQ$v=oc7r^=ZNNf?``7tL!!a0a)MVyA>TnBm zMi?n|hqQe>=`roTEG0tSHpvVF#q#ea2Qj(&{6@wJ5h}^_HaybBI8LNuWJk<8$LYG8 zUB;ZD>*3pIlA%k@pzyz5F!kqrJmC(kU|JzLn`#+jhd@C%dMVbbFw+?7T5j16QhY?< zioKENs%Xq{HxHP|PXVsZl1>UT>*=w^R=hi4o(U?ggye`Es3eF8@~@k|E`)~cin+N~ z6Zz5YmAZ$uTvYEIX*a?d-xgxmbedkbOVHocBYj6F5`9!i7&v6%%vY4=QHaJCj`W0rZN9}YBio})&Vyv=csv3axj)J?J@v@C#qj>Mj+gFO+C0UM@ugW+~fEv8iVbO0-6EeX}wPR-&^f@8*HK>qsSj1R%wB-hiP z5eTD5?4r+4X3e0-{j|F^2J=9Q#0^c?hj2Ppj{e0(6^J`dIUmz%bVUc6?ylxYMs49k zepyi@dNxjp@884k1FBJZRrjXkx;iiIS!!@mhfN&O3Gm>%vb!bYc*qj-kzKJDWt1tE z&jg>>3G-X~E@Az&`3IU}N*#cbg? z%UN zI>moem>#3@lkIx?9$yH8ejn)?vLPFfS-f-bY*eAqq`#fS1=@ zh3oQ6FhLlx;Y}iTPi9K;l?w9ulx+=pYzusc!!zt|R5Uk(O+cG(>y6Q8gou8@4vxX& z4%;}s&rji3#m%42fDMPiuKMZ&S%vrYq&0?Oe|UTPq(cjpRoUs;?oZHUrB_-?WgyK+*QXnfIXT7kP6l z-Yw@&9OrL^G%!u?+26@UnHy(6kclswN8fO2_m5UOXQ76Fba@c|kC9ord#{0W?q*GuuRljoaa1XZC!lAFZ%?x?} zm(2|Pdx89THije}`3E%Vs(M%S_J-Ps)Rju6F@l}dX62l7qn2D1g{(rN&UWNp_kJoc zzL+!nMCZx#-fkVIIUn|0Sr6oSw9<9T@c%3X;Rr!g=LKD^lqmkSp!=MaHrdan(M$v`;0jrTFGHFJ> zf}P+M>?zV9Yw;g`8UcC+=U80o^;&}h(aED+FY1`&>Ir#uHrq3^(KB~X)p8y!LMpyq zXmL(PoIE_o9!JU267fchrk#SBd_h-w&6kNy$BBzMPQ@E5LxohiwA4Bk-FSi`i;OV0 zs=lyYXnff{$pBW05|MLkb_KfFf1bA`X53KthVKaX-jc^s!pYb&HTVxpo(HwxR(W>n z(}fA19a-=}j0i>apo}^_Dz%sj4#}DedbfAzzL%AQkrJ(-P7knBGZr$$S-T{Lf|58X zsWqArZ44_l=*`>@%VWGCX4DQzJ_-r+T(}R`lzCrqM%3KNjuY*_L{cpW^ASlfBSg=L zJKfxWQZgySqrQO+P2UW!1M}AYi&9-r%G>}z-olEe%Y9S5LO74%NJ+ca4cJ3;W14}C zWC=`fgRSyFxU;I+lL|YXSWI+1(~`G=yn~i^(-RF4BP?j7qB-5Qg|8hOQo1G^(+Nb8 zYeQcwO-WgI`ZT!B1`&pLMgv*tu`$dd1Twpj{-GvcDRiiHPl92-d^OS zvd4i9leg&I;7~5wH*0xj-rpWt&am_jH%r})6|8dZ3J71R5PS{1Jpbs*|csNEoIx%ZPlPILof|p8*Cc``m@UR>JQi-=s^2n^EX?D?HJog*G#|n1>mAsh-A%+MQfvwD zq_-%P=0EgoZS89teow)IJ9%`9KxZaH((a$!!T9H^^3pdmbNqEAaNYuR-2$|0OM3l% zo^Ad@?A3?HPE*mL72P_AbeaytN{rC-m@NNO;O6ywyV&bxIxCu|9usAxr0W)0VVR8M z{-UKxDnqkn!XL1q$5!zHu+@&PB`mM{ry8^-@6c3jpTBt>q9=Z7ui&k7*u;FceNSDZ znyB7WfEm;H1}-!2lw~Vvg`{P6nc&J5mZDCoOueUF>hmehwB6(#n8|?;!Grh<;;K=u zeeCXpm{^-a&-s@ZM!DV`((A}W`rAQ&#(0qTxVqtHQ0%vHrWJ#FOCRBO@a~bc-uA@% zMA(iJ17bIX!;hpoa>Wh8Ail?uR}UVhmP)OpWeg$m|F_`O7A zEH7?~F1eqg#QG02Ro!n%3C;ja;>Nn(w8{mv*ryrDF6B*Ky96Lo(P7>t>vRO6JK0)<5&Ki@nW^(i(huU2{{oLF3IX<*+Yf zLmK?aAaOjMID#}iK(htiz7CIm;Pm{r;`Q()8RImxIldO2UV4Wh1Bb37M9zjTY62NW zmzxENlp}Xi1YQ+AvZk&`LCG3vwMtS)T}5Tlmy%gq(J%c{`g|LNzi<{8cjl--;(Pkp zTG?cW`Te37!YH{yvObil$%`aCHtSquIf1$tI^)7mB^`EKZMGXuYWko9U+yq=knb^{ z+930~*P25gbHGKF6Z0cu(H}zarAr6+gol}s!Q~)PSiYY>V-<@bH2?DI6koh=N{e3_ zPGi>N&rYqAOG-Y!NP__5N}X3Okih|G_jjD$a~!x>2Kv$ zItBVYdVof5bVle5Ht@@F`dYeJ9nezn#JIQ3p3TRNA~6bay(IkZObWDX+L=ytS*NB; zgtxIPN8Q=u0MHDDB^(RI(pSTmH2w30O)S%V)%WBkph85jKr7sFswtaD4CsvACM}Ki zE!u1)s6~3hhHU=wMN0FT6Vz9>q;esNz;#CTTYj-1mMS4_H{TYw5%%L8Ln*T=rg)EN z4zjk$Y2rDmmgOvQlGKz``E}kT>cJQ0YFi`z9dJLCt&4j04U1#cxRTM~&o*E#*gVf8 znAD0~e#acs$M!ml&5TARg5r?p#9B7^ZS{z>-Sl1L%1 zn-6LQKG+UdcUmCJSj*sN*{KRZF|~Ul>z^vpOmJ%UXb&DXNq)xIaCeP-q4DKN&|{G8 zp1_PsUt-?1Ok=Z=pdYyh?_mza`3o&y8btm@O1h&B18l=343yWcP2!G(B18{I{g%s( zHL*x*k|jzZkk*REDD$vG`KsSwBYY@q-dO{>+=$u`#))GC&=cqFCF$wRYyM((HdOcB zgt`1h)J1St`xf@KUr#FyV`+NVCs zTVumX6=1AMfE0SsmH3ds-U$N5nRP1KR30M9v)kbBke33>VCUh*Lh-CL6|HJ5IZo6Y z)}hf&9$#|zhQXxP$`yb>D^LHm zo7klXt3xbBs7q?0Q>}>&oZ-;}>K#$ydGaC@MgL#0JAQ?gA3Mx90db}X7$8FBJ6>1k zPPapZQaSefkpuUSc+`X&YFfPNIC@p$qj(ML*kB6mkPYO9f!9<2z>;N%EmNdxwS7qw z(1Rl(GQh5c5!G3bi_Vf#;vH3|P|PFLJBE~zpDVgx)IsMnv~o8kRv-AXBN?Yql8wcT zh4N!@?d0GX z?6*=GGvZ_T1YvyI8#ATs75Wc_<{XcObWzf&{*(G-{6_CzscE8#@uZ^r9E;EO!9_Ex zA7D9Wpf>Yp^#>P7LrP`%Y%rkZDw4e3iXiWcu`|^|9mtJfyQ1wceumx-f`D?A>&WGM zM^I-s{$(hC_2xSs1XQv0ljan>$B}G=syozYJVVvzT`|z&JBQoiH^Nd}OHi18~@vo_!!9U5`SIJRqfoN=*=<*>`!jvaeKE!2dk*i8tYlz zI@gZ5nCfoA9Yo!M83Bv(FWY?Mi|+R4j3Vt7f)dhQr>LBv{gO?~tJTLOg*-gRr#)849c>?zxOS~4J*2&Ahqm*<@?7!nco|n?uISRx9?}Gw z%EELa@-7r!BVlq>^sa+Lp4nrnwQ)RS3<&ET<8lUcAOv46J|DcvoA@e)-z#5pXbD?f zbo5zDI7V5uQY2Z|i)vSd;SPeo7G^bOW$PLobyJPElvGcdpBiQ|!kl8M2Nw4eQIX9< zg>2NY5;DiL#{l{1JWhz5f`k8by^Hoj$IEEtL+~#C$*m+g`<)yU<_-WRX`)57!;2w5 zj;v!KL0Y*wpb|qe*=xoU5-T}QABE5i6tXHHxb`5~8p=dZhd83}Mpb@++dtLY<8fp7+pS$t6}!r#ivCP!m-k`88vDO)tR{!@BW$ zasdtcc4xRtl5W8#C}N;lb+u!rxQ7!SVzH@nncToRKAT7~Ud?P~o1o54Zszd^8@VPJ z3`bur8s^$<_o%xXexHTM`xhkdZLbgP2|;dO(d zcCiNt&bZ3tPcbaol2FqCalh)e?ZWivJkhVhbn-5WRwf8VF)^~q zOK_+847iR?wig@dwO=L<0p3U0C-L>ZZ*P*rHp<-2U0{`ndgb+0X_-(9EED%vYnJvZ z+EwyU=|p&*lA2{q6o@+6Bu8|6bAazm_Vb(#8Z2ezu-UrTbG+XOFIvD6Td2uMew>EJQ-g zo{X$I;E>6%6NR%ZR629_syYG=lw_hgU!$q>@#~}n$OwQ$m^@MQfzk2%Nh=5$#K@mr zceq~LBnpMZ-4TY3c_*-^FE%jK>~w%CN$)a&=>b@B* z;(~SZH6x^>hb$R|SCh%HxKdmL6-r6N8G7(We^<1E<_`Qot{C>!=O5qSUD_Ds;!GLl+s1j|hmT7XXnk@6dOKa(#b6WZp3X8fND*YZ^PHQ~dQWN-_J|^@prx z9{FmK8~Rj8X*4Usc%qSRpQmH(Vyk~o_v~hI;lyh(=gv6Jv*)-`-QA5o-^L}I2vJ+p zKNXeKf}nh~p@$dt@=An%giz7G^76al2z$Z4bgLQG!rc;Q^S+HQHCQG~B10Y*xtXdc zuC7?%L1A-cKx;4>ksRfHjjP3AN5cfq>Q2IpHiomKcTi(Lgp?1wC zz|gKR;yjyHIU(+vW*iwgyh_gdKsgWdIYoE3j9%ih7syof3oe!ct!9T(&HBd zf7Wekh*Bc$!V^A^o*r&ZQ=RV7wNk)h1`ljz9=FSP#Jj-?p~wp>v_&CU-g?N5tfoWm z46Ww?I<6kcm%53fh1=QOO`Z439LmM*LT|SB#>8)-q6|Dr@@||{i)8_&YEHKEn53rn zYP3~tl(cJV`xl8PVek}djGFU~m74bvqG##Jb|dqV9ppsC3@+LPZdBQ&%U;M~*owA9 z*eCF|+9(_g6*eoNYte2upG!lbT57*9Zf~Txnv`)%EFli{SrIcl8)4A{vt+ySe39IX z>VIySGOHttXP*?8xCp|-;0~cJkLt1u%NG2BOAHFqG;8lHWZdDnQ5@Swf!lqMin_8z z^voiw^rH)}(ZDf^hl+x%LRL2fr?%BJ(Y7P5v`n=l>Kw{jT~dxCQlC)X)Ojiem5Y1e zWO&>riOwB8Bs$_*H~k{4pTJn5?xak|t6Yx7UWTKngHMZXI9%sg3e8mx--UhhV#CX2 zjmV$NeSL67V3nyUc9^1fe&hQBQV{6<_gI@)_Jbh2=b5-xk(YvDNcPaBT8QQi-HJwZ z*e?^~)t+xLLeNUdNIQ?%JoY%JTgTw%PiIq!?x$@RPc$*HAE#jp$KqAcu@MX3qoqc- zKgw8CwH8Udk@njdf=BcoEUOd+2fKq>keQn01>*Czi=Z*S;H^b?civrJM|Sh^_ODPY zOnOc>hoF+yldSkh;j;$xFX>tsw zT@ju|i_Les87cec_l{U3iTm&BD_^Rk6`q{Gp&ahqh2Y}R-(!U8923v+`%8ck$7i-x zO)zk0YTZm3(Y~xw{a^+8>b2UUcD^M*HFDor*O9eNH>y<>{q=Mw?uVhcZNPaQ&5$o} zO#cNt&xOiikRBS)P$SskEGMhb#{y-Y{we#)RTI;#l^TlB;{Ewez>Lrv+(Ytb-={%{ zF9~O{EJ&q;{mcPrJCX>&oMWZjXc zu9OHq>9L{+Jd}wdcb@kt1T)!E}}y5OKNQU-_vv0KI>9nI3Fy8mT-)=FHTN zvp5$1#rC3KVDw6BX$q+P*Y;}LE$Y6>gk(g?DMI~U_tup3%T=x2?mfRqrQ3(@C0eOI zTgD7fv|p$Yja>108kQ@rgs=a@o(AddBP$xc=Fif?zbcZAY2%RD%R4N`xfMl>c^p9a zu^ljwFW@hWzQY@wireSX(iikoF?g%s-z0!32jaK^dH-V|KUU$ZqHmO7n^XxD>qJ>HTT#Q z))9IvWZn&LGY*_HntNHWuTM4~17b2nuOmh)o48?ae#;$xC*1J-d3D$UnQGVpp9cg` z>wp|S8IpM-^!Wow6rpL!idrVpI5^*fxjI8Jt6v;WZ4s|)3riQrwrXf}WtWH_{o7m+ zj-k}$;Y(9XzE*uE{w~10K9Up_AXqoo3t3+lQJT#RmWXO>8%A@IcA>|-nmx#L@j{=e z@-Mc;jvjQ`qzA)oHgXmf22T(}r`1RV z8MYQy98V5J@(;nlNyc7ZrpfK$B~v%aZCI}-MKI$+aUk^zEYY9^<45G zRjuzIpqjb`N2&L7f`V49Fe(AnkTi>z`IOzSID(j2u4z} zTV%lxm(Mgj6wfwh4GBA)^a6R0cIz0QP zb@w;ZZ3>0PT_Xc%dhkR~g7-5CnQXTz-6>A-ec$D@6uedx!jLWzXwr6-?`L5t^bVn4 zN`LX5a6$3?$ReN7;@jPpxb1DNwW;x;E1Do07C2-gmEJcB zCqpmYA5s`gNE@-*eqz3&Q6IJu@KJEyv1{oQ%8eIAIa|rvJR@6!URa(dE)o90UxTq) z<>r~j^N-rvl<&`&6pCJ3DC)k`A0LS@tr#DOo%ApM^uvTTt8T2$WKHRqGn<>e;JS2b zty3IAr(u?gf<@V|QcR@UJaH6(<_CvC<+h52V20PuLH&R_%#<~&om}QDH{V%MRONz$ zR{EyLu96~tA`~MyiM+zvsxteN=j5WD!gB;65+^G7PoVMS!wVRhWQa-xGaOUvsVE$q zCC|uA1DvmxHvli`yQF9DFrRT?4}(5$jEJKOuQ62_^Q6^iyxaF?oy9ngbP z1eAKA)|!haw(sqNBCjt{pq4@!TyC`&dKu5u-O)yO>jT-*hT|%oTc}a83}t*}@Ci!% zMHuu$(HNHH>sp6H+J)?9fkBTG6*8!1A8d&@L(cb&8LEKpYQp}p+e{uqJ!|cClAHe- zeTLarFiKxRwG1)_S|{H)Ze2*=zhyp2##W&xp}oJGT#U&F)1rXo@#(*{5o=H?qO>t02zY6-~~ z$K3JFm27e!AM;EEAb{nsp82mR92RiB0a4|wK&UOc%12d^G{!a-9-E6a8oGEU4m~%s zhj_EQns_FKFdr^uS>jZFFuH`4(-deu)(`}gB>FjjsM<*MyGiQ;jLH#j7HQe2@uHDp zEt<7`D?I1_`Otb~u?YMr9oP0j2toj-9r~P%+k|)W+0yM8>M2V6`1*czoqfg5-Cg3% zVqZjealr>h6!st0PI2f$JUE4}Gv3ABaG7hHj+-uQpkIme94mGsdLF1lyba`Qvf7}t zS+snylX2$#pG)f3(5lN%1zT_Pm?RB0dr;0QB-zte8xv;;tFD|vBW=am&>clDf3g8d z$;D&xd>au20Nix9dl#D7&Ir}kO{NF~h`NlM z(3gN}^ojt;L}kfCefJ31Y+Bt!reWzhCl-YE>>)g--mArY)lIO;(xogIbHtDt>?5=` z0N>ulWrczI6y233hTeC5GaBOepc#J){oc-O56;L{T?lk-LqS z#as1QA@y3iq@?HhfmxD$7bdcxT5cUHVFuTvSQDb_R9>A+%0N}ElmEgNBXvW_E=Yua zbEZ@+0o;-PVGU}9$oDDGdh*Upj{ktPncVHwW4;4n|ex9vM zw$c?=Dm&Z6;O8Jg!H%Zf2*W>yd`5b5*s=^G=6pU;NS7#@<|r+2#A=xkOhHk9_evqB!nqTIU$qo3(g)f7?3A$9v zx#Cu!YrC1YdW9Ih>d!Y^8n@j1Rzmu#e=x^3B+$g!s|+cnC%}&6PG&vZE(XP^)Mpx{ zlNNp~`zjb&$540CShZ%AUpZSmD7+k$wt+mDo=(p}pi}2J&fXvtQ2Jn4iP!{Ic=R$K zuxm4KuS-gVPmvv};`mD_cFy#&Nyd!Ggd`fxdH+0;ZSvpgJF}f3L4F(#=l`|$WMNHZ zNe5fSZl=Lj5f>82ZiP`$Xri(OP;nPfgd~70aRF2i!WLwcsBPm0i~>Sv78OBEfPlyv z2tlP$wul&ZB)AX>0xdDn1i`7B5JCTW`M+--try6-_trh9PF0;c=hpJk4GB5P{SM3@ zyK=C~Eg%5sw&YRcN{g19D?!rq8xIYyDkeljKCk*u73Cq(CF_{|#*=PFq{{m@njDV( za&G(POS%OMvKJVHoV7mTVA*81W$m0b2lD20{5*S^9``1}^2hz#&L6mZVczo(hc`D} zvg}gS+P_x3wfKj}mS=op`VH#de2AV{<>!{Lw6`v(Su#*C(jWKn-e?;)W@vJiyW2qi zXnU%-!$KEgr}8bt@v-@P^5C+&s;iGNP%zXB4lHExC--PaN}&RxPthgjLtYkswjUc5_e|s zua-jAl3H{6=#|J>b^K)B8=i`7Dkce*U)Y@G)Ixp^KvhA6&kghVb1^ny$NVp-hFArB zYFuge`^I>nd(ufFhT72!wPj!fz%MJ5^z&*J1*i<4lPrdj=?x>Sf%J4Z*VDxT{HtF9 zGx*@enXe9#!l5cj1g{NfDU2Fuu$$CDbCn|y5W?!{hYz}NN> zy8X&b4GfgXzAqf1#^h^ZO{G23N9vmpD{;$?2)nE9w%|}dn;hfp1ho<`sIpv;(u1wx z9_vKvmImV=e#=*A_Oh9PQiw!u*5nTH?^759P924S_LYR! z7Az+olx+DR3jHMEqz}Pn!%JiI&lRA&+5}*$rY=VxtxD-0+&I_knL{m(XaQk_IL=Di za6-{;!v%n|--#DUfGw*lc`^U;xSja6(3{}BY-(bnjlp4e5NGuG67Fs=_w9U6_V5Vh z6IUQ!nE6WX|{%n6`_3H!g6Q8LLQ2Jn% zTGE#&Ea5@mBhg8Jlk)!LY-d8(o!~vyTB@pKgpz9Jt7mF!avB(D6J)cSMCAV> zFYt_T_=(#Rx)D z@*n|XySjjjhKnfz2kM)#ZQ}Bhw>`07w?WyQckwXp3|}ex6eRZ;ZjzxK`@4(6G7AqB zv-7O^H_EcR@J}}JyrUz5wP5V6+o8xo#Rh}wL@4+d2cuW=&48nK=;k$8u~V0#f1AiF z)Z^N+jO+5Sb2#k8S4)a%J%;dTLyHdHrD-s%5hk9KI8h-~q>mGs@=2;#kpsR57gU4X z#avj-tR7j5B?#7)eXGe)8{>jDEo?ANByZxdjT3teGu9q~-Z*Azb(HHsZBo@X2aMIT zI9{}^i&OcHQa4Yqu0g&~xXdoWwWKSs+rg)z(X$PHYO#&ZGBp84fO9m^k?#~;Q*^Ax zeJ2-0uZ1~C^@8>c24(2VhxTGfJ$hPsmG9)Xx=C>o?zw>b{G)Gsu1>N4?3H? z`J0R2L-_@55yOg|^?12oWvCq^o={-`>TKc-!~ze@6_9cZVm}Z3nxpCa&~yu!*4?I# z6Sb7|-@=={1Q>osdAVdK&x9Us6Xttt8G@8Q-y}yej$n=BrO_EX6%{l@zfo8pbR#Qx zgD(TsbzM^qX8igOa{0+7jGF$!25<#VsBx!w3{$>Zo3y=Q+=@arK}K?qcFqvN3iO)xvf_x|m2bbLpy(81uZRNEHC&} z0G!Ozxsj3I#hfCFuTV6X!AGtj8BMAECS}2p4@66}kFEmauM?IESKQYU{Tew$Pl3&= z=%6`G@O+zJ(ZYIUHQ?udJS5)7U=^`0RE}5wc}8iZjl%lNvptS?`nDwP>KSD0D6iK{ z0(S-au&c8*gd{*^2KdNfX7s4Wgi#7VdkcQoVmkRkg@C;iN_aI5X$!m=a9# zOYaBo5}$olO~L}-RLGe$TI z92$7>QOXZO*JIH9S!l}*ftPeLxhISz<{N4G3wcxSWk zNE$*7YA44wLpG8sFwhmH~71=+E*6d%kZ;wu~{9L4>-0P`j(aXN!l z7?8j0QO!pS@HJdXE&r>HBG7Xob{!&LF9^0^4LxNaB`dp;fi={Q(UaYyG!m6^H+6jP zmXb(<69&$L(464ytgT!pMs-n0clxeMb<^&_qwf~dS$GxZ>2;qbqyk;*(?U^O;~)t< zGWw(gIdYiQss&Y?B{0#WHoWqk5v1VB%Hi`?NO zo*sB%=H#4wqA34|&en}Qk9O5l&iHZbQ?U4Kgyu##FoGg=R|lhNIqPrl&uSPj%?8IoJK~h3@G<$U11z6o+_G*`-9QGe3#8 zpB`!eik947em!ix;p7;au4Bk9F9{grdb>CgC;ANg>B}d^{_M{6D#IzRGD zn#Yd==dz!Nxt*nF*IZsTlv?vB@T*sQq)sPvlvuQovhR4Aimza#F+6A*=7A|BHuEgF|Uo z-TmUl>ti8hiH!jkQD1W4m^$pCxnnEq5Y^V&W~J+gMU<%=sP^mN1*r6=QuAKcs-1+O zY5klX6KTzjjtv4+>8rk_LEo-H&1g9`_{%vDTu54rtQ6DGst>)ja5b*1tA}1px4Y%X z!a{$RM!7QG1}cTHcZ~>@i-YCg3zoL&OS*1LxzQgm z-;r7l@+-PZ|1Sjhk#+l)Tu&iLVh()CGc-)h`Px|x~b?U zUBu;AIJ*!bmY!tDo8Ys?BY1<2!BpuYd3(eW&>CNkA4OgX9iGWx)a^E3{-&u1gOtq_ z1QaUb)yqbOhKCd`@(txPaMn2-pb47WQ~W;FS>h;zVV|$SKMD2EYH)WLHf#E3w&rdw zG7cGe_jblm;lN-%40h&SDCP6O95bM9n=G%ryvgmBJlmhF1#A7)7=Vdp0LKDEeg=>< zU_NGm?!y1Vn=`I|d;JVzpuQ1*CN;jX-VAC`kyT^{d=tDG#6W#hJ2R>AKUcaR!&Us= UDSdBWA~gzkZ@1c(vz2uAKT=do^#A|> literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/gui.png b/labs/AgentStream/exgentic/misc/assets/gui.png new file mode 100644 index 0000000000000000000000000000000000000000..5997c98a0785b87021a829b15ab6d55a4b51b235 GIT binary patch literal 549586 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 z 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൐?K9H$b>OdRISB@1 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 z zzf*UPD=&3i=C^d=@Q|B(w6hH8CZf|e6O%r{bHiv3Oj&PSE2(ME0d_C^KgiWa{j?VR z+Ln{T8W7=`zEpP)Hc5;5ojZ;}9X}l=^4Q2RZc#Y1vb-|f&1~arA7N6>ZmVvAN=8#Nr{^bJS(xaNZB!Xk20{zPIK#i zHJhvIn^(YF-XPl0h??S;B1Y-Q4qRGb|M@R@K`V*OtgN%Uc#iqm;X~jaa5RnL^~gpu zaD93vvMJh!V_Nv-CzoCge$zorQ|);%nsrs}e)dLkecpfdUtKeep3bg0o-e>d>{Bs3 zQi`Xsat~c3eFHr1^{rI~uQeEA`n_`Bh?KFDi4s-bym70jo@8S-`JJM72af1svww!N zJ91u19RkUmryl+`wrsFa3%09ko0)b3laJaRR)d+XPl>{3yb#H}rFNNKM=H}~1jpL2 zyY&d$yZ=P?@4=j-K+w;ndyAs{p5j^%F~C6e{Q+AgPZ@*1L7q6_iTR*d{y;tC{lWn z?@H#z!Nuv0B@Rc12M2?tNmQ(5S734k!%ScbVc3mQQ^GgYKSx!+DO6G3sw0 zez53+#cRjNhG*ESg81Aht(P}y+vNT|xK8;K7g;#(dGmcwW1F42>aeoQT(-}(pzCWl zE8`*V63-;L;V~lq@;_QbWaPyvMQNh#-;`2lyQSen`$DvhdwhIUTe$cO6tH>HFfdxJ zS|azbpYuCuc2!3D}_FZBpUv308kz#y=6r zMt@)^#_8`qY>l4AT;A<?Fn9lkflK{4y0BWcq3u9oDtj9Qa5LW6Ep=H$oN9YCXsjYFXVq0G_ARpg+rJ_ zb~jaXEao!srim>7qhZJSbYB)RFd}- zj_ku97sMkhyv=d%nYwzW9D~rauk;EQ6$8Cz`@a;OKj6!aBb_uz?s


+`UL+s`Ri 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 a&%NAf8DM;E;&JvLfu^b+vf|Ez=l=zv_)UKR literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/icon_black.png b/labs/AgentStream/exgentic/misc/assets/icon_black.png new file mode 100644 index 0000000000000000000000000000000000000000..72092af3da52754d847edcff2ecb1e4c87a006b7 GIT binary patch literal 6897 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/dev/null 2>&1; then + log_error "uv is not installed. Install it with: pip install uv" + exit 1 +fi + +# Parse command line arguments +REQUESTED_BENCHMARKS=() +if [ $# -gt 0 ]; then + REQUESTED_BENCHMARKS=("$@") +fi +ALL_BENCHMARKS=("gsm8k" "hotpotqa" "appworld" "browsecompplus" "swebench" "tau2") + +# Determine which benchmarks to create +if [ ${#REQUESTED_BENCHMARKS[@]} -eq 0 ]; then + # No arguments provided, create all + CREATE_CORE=true + BENCHMARKS_TO_CREATE=("${ALL_BENCHMARKS[@]}") + log_info "No benchmarks specified, creating all environments" +elif [ "${REQUESTED_BENCHMARKS[0]}" = "core" ] && [ ${#REQUESTED_BENCHMARKS[@]} -eq 1 ]; then + # Only 'core' requested + CREATE_CORE=true + BENCHMARKS_TO_CREATE=() + log_info "Creating only core environment" +else + # Specific benchmarks requested + CREATE_CORE=false + BENCHMARKS_TO_CREATE=() + + # Check if 'core' is in the list + for arg in "${REQUESTED_BENCHMARKS[@]}"; do + if [ "$arg" = "core" ]; then + CREATE_CORE=true + else + # Validate benchmark name + if [[ " ${ALL_BENCHMARKS[@]} " =~ " ${arg} " ]]; then + BENCHMARKS_TO_CREATE+=("$arg") + else + log_warning "Unknown benchmark: $arg (skipping)" + fi + fi + done + + if [ "$CREATE_CORE" = true ]; then + log_info "Creating core environment and benchmarks: ${BENCHMARKS_TO_CREATE[*]}" + else + log_info "Creating benchmark environments: ${BENCHMARKS_TO_CREATE[*]}" + fi +fi + +log_info "Starting environment setup..." +log_info "Project root: ${PROJECT_ROOT}" +log_info "Virtual environments will be created in: ${VENV_DIR}" + +mkdir -p "${VENV_DIR}" + +# 1. Create 'core' environment with all optional dependencies +if [ "$CREATE_CORE" = true ]; then + log_info "Creating 'core' environment with all optional dependencies..." + + uv venv "${VENV_DIR}/core" --python 3.11 + + # Activate and install + source "${VENV_DIR}/core/bin/activate" + + # Install project with all optional dependencies + log_info "Installing base project with all optional dependencies..." + uv pip install -e "${PROJECT_ROOT}[smolagents,openaimacp,otel,cli,dev]" + + # Generate requirements.txt for core environment + log_info "Generating requirements.txt for core environment..." + mkdir -p "${REQUIREMENTS_DIR}/core" + uv pip freeze | grep -v "^-e " | grep -v " @ file://" | grep -v "github.ibm.com" | grep -v " @ git+https://" | grep -v " @ git+ssh://" > "${REQUIREMENTS_DIR}/core/requirements.txt" + core_pkg_count=$(wc -l < "${REQUIREMENTS_DIR}/core/requirements.txt" | tr -d ' ') + log_success "Saved ${core_pkg_count} packages to ${REQUIREMENTS_DIR}/core/requirements.txt" + + deactivate + log_success "Core environment created at ${VENV_DIR}/core" +else + log_info "Skipping core environment creation" +fi + +# 2. Create benchmark-specific environments +if [ ${#BENCHMARKS_TO_CREATE[@]} -gt 0 ]; then + BENCHMARKS=("${BENCHMARKS_TO_CREATE[@]}") +else + BENCHMARKS=() +fi + +if [ ${#BENCHMARKS[@]} -gt 0 ]; then +for benchmark in "${BENCHMARKS[@]}"; do + log_info "Creating '${benchmark}' environment..." + + SETUP_SCRIPT="${PROJECT_ROOT}/src/exgentic/benchmarks/${benchmark}/setup.sh" + + if [ ! -f "${SETUP_SCRIPT}" ]; then + log_warning "Setup script not found: ${SETUP_SCRIPT}, skipping..." + continue + fi + + # Create virtual environment + uv venv "${VENV_DIR}/${benchmark}" --python 3.11 + + # Activate environment + source "${VENV_DIR}/${benchmark}/bin/activate" + + # Install base project with all optional dependencies + log_info "Installing base project with all optional dependencies for ${benchmark}..." + uv pip install -e "${PROJECT_ROOT}[smolagents,openaimacp,otel,cli,dev]" + + # Run benchmark-specific setup script + log_info "Running setup script for ${benchmark}..." + + # Change to project root before running setup script + # (some scripts expect to be run from project root) + cd "${PROJECT_ROOT}" + + SETUP_OK=true + if bash "${SETUP_SCRIPT}"; then + log_success "${benchmark} setup completed successfully" + SETUP_PASSED+=("${benchmark}") + else + log_warning "${benchmark} setup script encountered issues (exit code: $?)" + SETUP_FAILED+=("${benchmark}") + SETUP_OK=false + fi + + # Generate requirements.txt only when setup succeeded + if [ "${SETUP_OK}" = true ]; then + log_info "Generating requirements.txt for ${benchmark} environment..." + mkdir -p "${REQUIREMENTS_DIR}/${benchmark}" + uv pip freeze | grep -v "^-e " | grep -v " @ file://" | grep -v "github.ibm.com" | grep -v " @ git+https://" | grep -v " @ git+ssh://" > "${REQUIREMENTS_DIR}/${benchmark}/requirements.txt" + bench_pkg_count=$(wc -l < "${REQUIREMENTS_DIR}/${benchmark}/requirements.txt" | tr -d ' ') + log_success "Saved ${bench_pkg_count} packages to ${REQUIREMENTS_DIR}/${benchmark}/requirements.txt" + else + log_warning "Skipping requirements.txt generation for ${benchmark} due to setup failure" + fi + + deactivate + log_success "${benchmark} environment created at ${VENV_DIR}/${benchmark}" +done +fi + +# Generate README for requirements +log_info "Generating requirements README..." +cat > "${REQUIREMENTS_DIR}/README.md" << 'EOF' +# Requirements Files + +This directory contains frozen requirements for each virtual environment. + +## Directories + +Each environment has its own directory with a `requirements.txt` file: + +- `core/requirements.txt` - Core environment with all optional dependencies +- `gsm8k/requirements.txt` - GSM8K benchmark environment +- `hotpotqa/requirements.txt` - HotpotQA benchmark environment +- `appworld/requirements.txt` - AppWorld benchmark environment +- `browsecompplus/requirements.txt` - BrowseComp+ benchmark environment +- `swebench/requirements.txt` - SWE-bench benchmark environment +- `tau2/requirements.txt` - TAU-2 benchmark environment + +## Usage + +To recreate an environment from a requirements file: + +```bash +# Create a new virtual environment +uv venv .venv --python 3.11 + +# Activate it +source .venv/bin/activate + +# Install from requirements +uv pip install -r misc/security/requirements/core/requirements.txt +``` + +## Notes + +- These files are generated automatically by `misc/security/setup_environments.sh` +- They represent the exact package versions installed in each environment +- Only PyPI packages are included (no git or local installs) +- Regenerate by re-running the setup script + +## Generation Date + +EOF + +log_success "Requirements README created at ${REQUIREMENTS_DIR}/README.md" + +# Summary +echo "" +log_success "Environment setup completed!" +echo "" +echo "Created environments:" +if [ "$CREATE_CORE" = true ] && [ -d "${VENV_DIR}/core" ]; then + echo " ${VENV_DIR}/core - Core environment with all optional dependencies" + if [ -f "${REQUIREMENTS_DIR}/core/requirements.txt" ]; then + pkg_count=$(wc -l < "${REQUIREMENTS_DIR}/core/requirements.txt" | tr -d ' ') + echo " Requirements: ${REQUIREMENTS_DIR}/core/requirements.txt (${pkg_count} packages)" + fi +fi +if [ ${#BENCHMARKS[@]} -gt 0 ]; then +for benchmark in "${BENCHMARKS[@]}"; do + if [ -d "${VENV_DIR}/${benchmark}" ]; then + echo " ${VENV_DIR}/${benchmark} - ${benchmark} benchmark environment" + if [ -f "${REQUIREMENTS_DIR}/${benchmark}/requirements.txt" ]; then + pkg_count=$(wc -l < "${REQUIREMENTS_DIR}/${benchmark}/requirements.txt" | tr -d ' ') + echo " Requirements: ${REQUIREMENTS_DIR}/${benchmark}/requirements.txt (${pkg_count} packages)" + fi + fi +done +fi +echo "" +echo "To activate an environment, use:" +if [ "$CREATE_CORE" = true ]; then + echo " source ${VENV_DIR}/core/bin/activate" +fi +if [ ${#BENCHMARKS[@]} -gt 0 ]; then +for benchmark in "${BENCHMARKS[@]}"; do + if [ -d "${VENV_DIR}/${benchmark}" ]; then + echo " source ${VENV_DIR}/${benchmark}/bin/activate" + fi +done +fi +echo "" +echo "Note: Some benchmarks may require additional prerequisites:" +echo " - appworld, swebench: Git LFS" +echo " - browsecompplus: Java 21+, SSH access to IBM GitHub" +echo "" +echo "Usage examples:" +echo " ./misc/security/setup_environments.sh # Create all environments" +echo " ./misc/security/setup_environments.sh gsm8k hotpotqa # Create specific benchmarks" +echo " ./misc/security/setup_environments.sh core # Create only core environment" + +# Setup script pass/fail summary +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Setup Script Results" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +if [ ${#SETUP_PASSED[@]} -gt 0 ]; then + for b in "${SETUP_PASSED[@]}"; do + echo -e " ${GREEN}✔ PASSED${NC} ${b}" + done +else + echo " (no benchmark setup scripts ran)" +fi +if [ ${#SETUP_FAILED[@]} -gt 0 ]; then + for b in "${SETUP_FAILED[@]}"; do + echo -e " ${RED}✘ FAILED${NC} ${b}" + done +fi +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/labs/AgentStream/exgentic/misc/skills/add-agent/SKILL.md b/labs/AgentStream/exgentic/misc/skills/add-agent/SKILL.md new file mode 100644 index 00000000..047d8a16 --- /dev/null +++ b/labs/AgentStream/exgentic/misc/skills/add-agent/SKILL.md @@ -0,0 +1,68 @@ +--- +name: add-agent +description: Use when adding or updating an agent adapter in the Exgentic repository. Follow the repository agent principles, separate lightweight config from heavy execution logic, isolate third-party dependencies behind lazy imports, adapt to any benchmark contract without requiring benchmark changes, and validate the adapter with representative smoke tests before finishing. +--- + +# Add Agent + +Use this skill when working on agent adapters in the Exgentic repository. + +## First read + +Start with: +- `docs/adding-agents.md` +- `src/exgentic/core/agent.py` +- `src/exgentic/core/agent_instance.py` +- `src/exgentic/interfaces/registry.py` + +Then inspect the most relevant existing adapters: +- `src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py` + `instance.py` (split pattern: heavy deps in separate file) +- `src/exgentic/agents/cli/claude/agent.py` (light pattern: everything in one file) + +## Workflow + +1. Decide whether to split files. + If the agent depends on heavy third-party libraries (litellm, smolagents, openai SDK, etc.), put the Agent in one file and the AgentInstance in `instance.py`. If deps are light, keep both in a single file. + +2. Implement the Agent class first. + Subclass `Agent`. Declare `display_name` and `slug_name` as `ClassVar[str]`. Add user-facing config fields. Implement `_get_instance_class()` with a lazy import. Implement `_get_instance_kwargs()` to translate config and the benchmark contract into instance constructor kwargs. + +3. Implement the AgentInstance class. + Subclass `AgentInstance`. Accept `session_id` plus the kwargs from `_get_instance_kwargs()`. Call `super().__init__(session_id)`. Implement `react()` as the core decision loop and `close()` for cleanup. Optionally override `start()` and `get_cost()`. + +4. Add `requirements.txt` for agent-specific dependencies. + List only packages not already in the base exgentic install. Place the file in the agent's package directory; `RunnerMixin` discovers it automatically. + +5. Add `setup.sh` if non-pip setup is needed. + Place it next to the agent module; `RunnerMixin` discovers it automatically. + +6. Register the agent in `src/exgentic/interfaces/registry.py`. + Add a `RegistryEntry` to the `AGENTS` dict. Ensure `slug_name` and `display_name` match the class exactly. + +7. Validate the adapter as an agent, not just as code. + Check registry loading, dependency isolation, at least one end-to-end benchmark run, cost reporting, and cleanup. + +## Non-negotiable rules + +- The Agent file must be importable without installing agent-specific packages. +- `_get_instance_class()` must use a lazy import to isolate heavy deps. +- `_get_instance_kwargs()` must faithfully pass the benchmark contract (task, context, actions, session_id) through to the instance. +- The instance constructor must call `super().__init__(session_id)`. +- `react()` must return `None` when the agent decides it is done. +- `close()` must not raise exceptions. +- `slug_name` and `display_name` in the registry entry must exactly match the class values. +- The agent must adapt to the benchmark, never the other way around. + +## Validation + +Before finishing, run at least: +- `python -m py_compile` on changed agent files +- `pre-commit run --files ...` +- `git diff --check` + +Also confirm: +- `load_agent("slug_name")` succeeds from a Python shell +- The Agent file imports cleanly without agent-specific packages installed +- At least one benchmark runs end to end with the new agent +- `close()` completes without error +- `get_cost()` returns a valid report diff --git a/labs/AgentStream/exgentic/misc/skills/add-benchmark/SKILL.md b/labs/AgentStream/exgentic/misc/skills/add-benchmark/SKILL.md new file mode 100644 index 00000000..3e3da2f1 --- /dev/null +++ b/labs/AgentStream/exgentic/misc/skills/add-benchmark/SKILL.md @@ -0,0 +1,62 @@ +--- +name: add-benchmark +description: Use when adding or updating a benchmark adapter in the Exgentic repository. Follow the repository benchmark principles, keep the benchmark contract protocol-agnostic, prefer the thinnest possible wrapper that makes the benchmark accessible to any Exgentic agent, reuse external harness assets and scoring where possible, and validate the adapter with representative smoke tests before finishing. +--- + +# Add Benchmark + +Use this skill when working on benchmark adapters in the Exgentic repository. + +## First read + +Start with: +- `docs/adding-benchmarks.md` +- `src/exgentic/core/benchmark.py` +- `src/exgentic/interfaces/registry.py` + +Then inspect the most relevant existing adapters: +- `src/exgentic/benchmarks/tau2/tau2_benchmark.py` +- `src/exgentic/benchmarks/bfcl/bfcl_benchmark.py` + +## Workflow + +1. Define the benchmark contract before writing code. + Decide the real `task`, the agent-relevant `context`, the semantic `actions`, the finish condition, and the scoring boundary. + +2. Keep the agent-facing contract protocol-agnostic. + Do not define the benchmark in terms of one provider's chat or tool-calling format. + +3. Prefer the thinnest possible wrapper. + Make the benchmark accessible to any Exgentic agent with the minimum translation surface necessary. Do not add extra abstraction, copied logic, or runtime machinery unless it is needed to preserve benchmark meaning. + +4. Decide the source-of-truth boundary. + Reuse external benchmark assets, setup, and scoring where possible, but do not let an external harness dictate the wrong runtime contract for Exgentic. + +5. Implement runtime, setup, and registration separately. + Prefer a benchmark module, a `setup.sh`, and a registry entry with clear responsibilities. + +6. Validate the adapter as a benchmark, not just as code. + Check task listing, subset listing, happy-path scoring, failure-path scoring, and error semantics. + +## Non-negotiable rules + +- `task` must be the actual task. +- `context` must include only what the agent should know. +- subset names and internal metadata stay out of `context`. +- Prefer the thinnest wrapper that preserves the benchmark's meaning. +- Actions should describe semantic operations, not protocol artifacts. +- Use `finish` only when it is part of the benchmark contract. +- If outputs are not real execution results, say so plainly in the benchmark contract. +- Keep success, unsuccessful completion, unfinished runs, and errors distinct. + +## Validation + +Before finishing, run at least: +- `python -m py_compile` on changed benchmark files +- `pre-commit run --files ...` +- `git diff --check` + +Also run benchmark-specific smoke tests that prove: +- one passing case works +- one failing case is represented correctly +- one real error is surfaced as an error diff --git a/labs/AgentStream/exgentic/misc/utils/.secrets.baseline b/labs/AgentStream/exgentic/misc/utils/.secrets.baseline new file mode 100644 index 00000000..873f0f38 --- /dev/null +++ b/labs/AgentStream/exgentic/misc/utils/.secrets.baseline @@ -0,0 +1,150 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": "misc/utils/.secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "tests/benchmarks/recordings/appworld/trajectory.jsonl": [ + { + "type": "JSON Web Token", + "filename": "tests/benchmarks/recordings/appworld/trajectory.jsonl", + "hashed_secret": "0c677ddce015761585645772702fd153efbb08d9", + "is_verified": false, + "line_number": 7 + } + ], + "tests/benchmarks/recordings/swebench/results.json": [ + { + "type": "Secret Keyword", + "filename": "tests/benchmarks/recordings/swebench/results.json", + "hashed_secret": "d4e0e04792fd434b5dc9c4155c178f66edcf4ed3", + "is_verified": false, + "line_number": 46 + } + ] + }, + "generated_at": "2026-03-17T11:41:59Z" +} diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py new file mode 100644 index 00000000..1f78a16b --- /dev/null +++ b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +#!/usr/bin/env python3 +"""Enforce that all direct dependencies in pyproject.toml have upper version bounds. + +This script prevents supply chain attacks by ensuring no dependency can auto-upgrade +to an arbitrary future version. All dependencies must be capped at the next major +version (e.g., >=1.0.0,<2). + +Exit codes: + 0: All dependencies have upper bounds + 1: One or more dependencies lack upper bounds +""" + +import re +import sys +from pathlib import Path + + +def _extract_dependency_lines(content: str) -> list[str]: + """Extract lines that belong to dependency sections in pyproject.toml. + + Scopes extraction to [project.dependencies] and + [project.optional-dependencies.*] sections only, so that version-like + strings in other sections (e.g. build-system.requires) are ignored. + """ + lines: list[str] = [] + in_dep_section = False + in_dep_array = False + + for line in content.splitlines(): + stripped = line.strip() + + # Detect section headers + if stripped.startswith("["): + in_dep_section = stripped in ("[project]",) or stripped.startswith("[project.optional-dependencies") + in_dep_array = False + continue + + if not in_dep_section: + continue + + # Inside a relevant section, look for dependency array starts + if "dependencies" in stripped and "=" in stripped and "[" in stripped: + in_dep_array = True + continue + # Also handle bare list continuation under optional-dependencies groups + if stripped.startswith('"') and in_dep_section and not in_dep_array: + # We're likely in an optional-dep group list + in_dep_array = True + + if in_dep_array: + if stripped == "]": + in_dep_array = False + continue + lines.append(line) + + return lines + + +def check_dependency_caps(pyproject_path: Path) -> list[str]: + """Check all dependencies in pyproject.toml for upper version bounds. + + Args: + pyproject_path: Path to pyproject.toml file + + Returns: + List of dependency lines that lack upper bounds (empty if all are capped) + """ + content = pyproject_path.read_text() + uncapped = [] + + # Only check lines inside dependency sections + dep_lines = _extract_dependency_lines(content) + + # Pattern to match dependency specifications (supports extras like [extra]) + # Matches: "package>=1.0.0" or "package[extra]>=1.0.0,!=1.2.3" but not "package>=1.0.0,<2" + dep_pattern = re.compile( + r'^\s*"([a-zA-Z0-9_-]+(?:\[[a-zA-Z0-9_,\s-]+\])?)([><=!,.\d\s]+)"', + ) + + for line in dep_lines: + match = dep_pattern.match(line) + if match: + full_line = match.group(0).strip() + version_spec = match.group(2) + + # Check if there's an upper bound (< or <=) + if "<" not in version_spec: + uncapped.append(full_line) + + return uncapped + + +def main() -> int: + """Main entry point.""" + pyproject_path = Path("pyproject.toml") + + if not pyproject_path.exists(): + print("Error: pyproject.toml not found", file=sys.stderr) + return 1 + + uncapped = check_dependency_caps(pyproject_path) + + if uncapped: + print("❌ Dependencies without upper version bounds found:", file=sys.stderr) + print(file=sys.stderr) + for dep in uncapped: + print(f" {dep}", file=sys.stderr) + print(file=sys.stderr) + print( + "All dependencies must have upper bounds (e.g., >=1.0.0,<2) to limit supply chain attack exposure.", + file=sys.stderr, + ) + print("See SECURITY.md for the dependency management policy.", file=sys.stderr) + return 1 + + print("✅ All dependencies have upper version bounds") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py b/labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py new file mode 100644 index 00000000..6f1058d9 --- /dev/null +++ b/labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import logging +import sys + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def check_imports(file_path): + success = True + with open(file_path, encoding="utf-8") as file: + for lineno, line in enumerate(file, start=1): + if "from src import" in line or "from src." in line or "import src" in line: + logger.error(f"Non library import: {file_path}:{lineno}: {line.strip()[:30]}...") + success = False + return success + + +def main(): + success = True + for file_path in sys.argv[1:]: + if not check_imports(file_path): + success = False + + if not success: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py b/labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py new file mode 100644 index 00000000..8531541e --- /dev/null +++ b/labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import logging +import sys + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +library_name = "exgentic" + + +def check_imports(file_path): + success = True + with open(file_path, encoding="utf-8") as file: + for lineno, line in enumerate(file, start=1): + if ( + f"from {library_name} import" in line + or f"from {library_name}." in line + or f"import {library_name}" in line + or "from src import" in line + or "from src." in line + or "import src" in line + ): + logger.error(f"Non relative import: {file_path}:{lineno}: {line.strip()[:30]}...") + success = False + return success + + +def main(): + success = True + for file_path in sys.argv[1:]: + if not check_imports(file_path): + success = False + + if not success: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py b/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py new file mode 100644 index 00000000..dc39aaa2 --- /dev/null +++ b/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +#!/usr/bin/env python3 +"""Ensure SPDX+copyright header exists at top of Python files.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +HEADER_LINES = [ + "# SPDX-License-Identifier: Apache-2.0", + "# Copyright (C) 2026, The Exgentic organization and its contributors.", +] +HEADER_TEXT = "\n".join(HEADER_LINES) + "\n\n" + +COPYRIGHT_RE = re.compile(r"^# Copyright \(C\) (?P\d{4}), The Exgentic organization and its contributors\.$") + +SKIP_DIRS = { + ".git", + ".venv", + ".pytest_cache", + ".ruff_cache", + "__pycache__", +} + + +def should_skip(path: Path) -> bool: + return bool(set(path.parts) & SKIP_DIRS) + + +def update_file(path: Path) -> bool: + raw = path.read_bytes() + original = raw.decode("utf-8", errors="surrogateescape") + if not original: + path.write_text(HEADER_TEXT, encoding="utf-8") + return True + + lines = original.splitlines() + if len(lines) >= 2 and lines[0] == HEADER_LINES[0] and COPYRIGHT_RE.match(lines[1]): + if lines[1] != HEADER_LINES[1]: + lines[1] = HEADER_LINES[1] + updated = "\n".join(lines) + ("\n" if original.endswith("\n") else "") + path.write_text(updated, encoding="utf-8", errors="surrogateescape") + return True + return False + + updated = HEADER_TEXT + original + path.write_text(updated, encoding="utf-8", errors="surrogateescape") + return True + + +def main(argv: list[str]) -> int: + changed = False + for filename in argv: + path = Path(filename) + if not path.is_file() or should_skip(path): + continue + if update_file(path): + changed = True + if changed: + print("SPDX headers updated. Re-run pre-commit.") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/labs/AgentStream/exgentic/pyproject.toml b/labs/AgentStream/exgentic/pyproject.toml new file mode 100644 index 00000000..9f7f8692 --- /dev/null +++ b/labs/AgentStream/exgentic/pyproject.toml @@ -0,0 +1,111 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "exgentic" +dynamic = ["version"] +description = "Exgentic - General agent evaluation" +authors = [{name = "Exgentic Team"}] +license = {text = "Apache-2.0"} +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "typing-extensions>=4,<5", + "pydantic-settings>=2,<3", + "nicegui>=3,<4", + "cloudpickle>=3,<4", + "diskcache>=5,<6", + "filelock>=3,<4", + "click>=8.1.7,<9", + "json-schema-to-pydantic>=0.4,<1", + "litellm>=1.65.0,!=1.82.7,!=1.82.8,<2", + "mcp>=1.24,<2", + "pydantic>=2.9.2,<3", + "python-dotenv>=1,<2", + "rich>=13,<14", + "rich-click>=1,<2", +] + +[project.scripts] +exgentic = "exgentic.interfaces.cli.main:main" + +[project.optional-dependencies] +# Observability +otel = [ + "opentelemetry-api>=1,<2", + "opentelemetry-sdk>=1,<2", + "opentelemetry-exporter-otlp-proto-http>=1,<2", + "opentelemetry-exporter-otlp-proto-grpc>=1,<2", + "opentelemetry-semantic-conventions-ai>=0.4.0,<1", +] +analysis = [ + "matplotlib>=3,<4", + "numpy>=2,<3", + "pandas>=3,<4", + "scipy>=1,<2", + "statsmodels>=0.14,<1", +] +amem = [ + "sentence-transformers>=3,<5", + "scikit-learn>=1,<2", +] + +# Development +dev = [ + "pytest>=7.0.0,<10", + "pytest-asyncio>=0.21.0,<2", + "pytest-mock>=3.0.0,<4", + "pre-commit>=3.0.0,<5", + "ruff>=0.1.0,<1", + "codespell>=2.0.0,<3", + "detect-secrets>=1.0.0,<2", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.version] +source = "vcs" +tag-pattern = "^v(?P.*)$" +fallback-version = "0.0.0" + +[tool.hatch.build.hooks.vcs] +version-file = "src/exgentic/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/exgentic"] +include = [ + "src/exgentic/benchmarks/**/requirements.txt", + "src/exgentic/benchmarks/**/setup.sh", + "src/exgentic/benchmarks/**/system-deps.txt", + "src/exgentic/agents/**/requirements.txt", + "src/exgentic/agents/**/setup.sh", + "src/exgentic/agents/**/system-deps.txt", +] + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", + "/docs", + "/examples", +] + +[tool.uv] +override-dependencies = [ + "rich>=13.9.4,<14", +] + +[tool.codespell] +skip = "tests/benchmarks/recordings/*,uv.lock" +ignore-words-list = "requestor" + +[tool.pytest.ini_options] +asyncio_mode = "strict" +pythonpath = ["."] + +[dependency-groups] +dev = [ + "pytest>=9.0.1,<10", +] diff --git a/labs/AgentStream/exgentic/renovate.json b/labs/AgentStream/exgentic/renovate.json new file mode 100644 index 00000000..97380307 --- /dev/null +++ b/labs/AgentStream/exgentic/renovate.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "packageRules": [ + { + "matchManagers": ["uv"], + "minimumReleaseAge": "14 days", + "groupName": "python dependencies", + "rangeStrategy": "bump" + } + ], + "schedule": ["every weekend"] +} diff --git a/labs/AgentStream/exgentic/ruff.toml b/labs/AgentStream/exgentic/ruff.toml new file mode 100644 index 00000000..aba4f270 --- /dev/null +++ b/labs/AgentStream/exgentic/ruff.toml @@ -0,0 +1,78 @@ +# Ruff configuration for Exgentic project + +# Set line length +line-length = 120 + +# Exclude common directories +extend-exclude = [ + ".venv", + ".venvs", + "venv", + ".git", + "__pycache__", + ".pytest_cache", + ".exgentic", +] + +[lint] +# Enable specific rule sets +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "D", # pydocstyle + "RUF", # Ruff-specific rules + "RET", # flake8-return + "C90", # mccabe complexity + "N", # pep8-naming + "G", # flake8-logging-format +] + +# Ignore specific rules +ignore = [ + "TID252", # Relative imports from parent modules - conflicts with our enforce-relative-imports hook + "C901", # Function is too complex - will be addressed separately + "T201", # Print statements - will be addressed separately + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "G004", # Logging statement uses f-string + "UP007", # Use `X | Y` for type annotations + "RET504", # Unnecessary assignment to ... before return +] + +# Allow autofix for all enabled rules +fixable = ["ALL"] +unfixable = [] + +[lint.per-file-ignores] +# Allow print statements in test files and scripts +"tests/**/*.py" = ["T201"] +"examples/**/*.py" = ["T201"] +"misc/**/*.py" = ["T201"] + +[lint.pydocstyle] +# Use Google-style docstrings +convention = "google" + +[lint.mccabe] +# Set maximum complexity +max-complexity = 15 + +[format] +# Use double quotes for strings +quote-style = "double" + +# Indent with 4 spaces +indent-style = "space" + +# Use Unix line endings +line-ending = "lf" diff --git a/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.py b/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.py new file mode 100644 index 00000000..4b319465 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.py @@ -0,0 +1,416 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.agents.a_mem.memory_store import MemoryStore +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + "runner": "direct", + }, + "agent_kwargs": {}, + }, +} + + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def get_memory_stats(mode: str, bm_slug: str) -> tuple[int, int]: + stores = MemoryStore.list_stores() + if mode == "isolated": + store = stores.get(f"amem_isolated_{bm_slug}") + elif mode == "sequential": + store = stores.get("amem_sequential_global") + elif mode == "interleaved": + store = stores.get("amem_interleaved_global") + else: + return 0, 0 + if store is None: + return 0, 0 + stats = store.get_stats() + return stats["total_memories"], stats["total_evolutions"] + + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + mode: str, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + memory_count, total_evolutions = get_memory_stats(mode, bm_slug) + + record = { + "session_index": session_index, + "seed": seed, + "mode": mode, + "agent": "a_mem", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_count": memory_count, + "total_evolutions": total_evolutions, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + + + +def run_experiment(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" A-Mem Experiment: mode={args.mode} seed={args.seed}") + print(f" model={args.model} memory_model={args.memory_model}") + print(f" num_tasks={args.num_tasks} retrieve_k={args.retrieve_k}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, args.mode) + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": args.mode, + "seed": args.seed, + "agent": "a_mem", + "model": args.model, + "memory_model": args.memory_model, + "retrieve_k": args.retrieve_k, + "evo_threshold": args.evo_threshold, + "embedding_model": args.embedding_model, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + + MemoryStore.reset_all() + if args.mode == "interleaved": + _ckpt_ids = ["amem_interleaved_global"] + elif args.mode == "sequential": + _ckpt_ids = ["amem_sequential_global"] + else: + _ckpt_ids = [f"amem_isolated_{b}" for b in benchmarks_to_run] + + _restored = False + _restored_session_count = 0 + for sid in _ckpt_ids: + ckpt_path = output_dir / f"memory_{sid}.json" + if ckpt_path.exists(): + store = MemoryStore.get_or_create( + shuffle_mode=args.mode, + benchmark_id=sid.replace("amem_isolated_", "") if args.mode == "isolated" else None, + embedding_model=args.embedding_model, + evo_threshold=args.evo_threshold, + ) + store.load_checkpoint(str(ckpt_path)) + _restored_session_count = max(_restored_session_count, store.session_count) + _restored = True + if _restored: + print(f" Restored memory from checkpoint (session_count={_restored_session_count})") + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + if metrics_path.exists(): + kept_lines: list[str] = [] + with open(metrics_path, "r") as f: + for line in f: + if _restored and session_index >= _restored_session_count: + break + rec = json.loads(line) + all_scores.append(rec["score"]) + bm_scores[rec["benchmark_slug"]].append(rec["score"]) + kept_lines.append(line) + session_index += 1 + with open(metrics_path, "w") as f: + f.writelines(kept_lines) + if session_index > 0: + print(f" Restored {session_index} metrics records (cum_avg={sum(all_scores)/len(all_scores):.3f})") + + if args.mode in ("isolated", "sequential"): + _completed_benchmarks: set[str] = set() + if _restored and session_index > 0: + _bm_counts: dict[str, int] = defaultdict(int) + with open(metrics_path, "r") as f: + for line in f: + rec = json.loads(line) + _bm_counts[rec["benchmark_slug"]] += 1 + for bm_slug, task_ids in group_by_benchmark(task_order): + if _bm_counts.get(bm_slug, 0) >= len(task_ids): + _completed_benchmarks.add(bm_slug) + if _completed_benchmarks: + print(f" Skipping completed benchmarks: {sorted(_completed_benchmarks)}") + + for bm_slug, task_ids in group_by_benchmark(task_order): + if bm_slug in _completed_benchmarks: + continue + + print(f"\n{'=' * 60}") + print(f" {args.mode.upper()} — {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("a_mem")( + model=args.model, + memory_model=args.memory_model, + shuffle_mode=args.mode, + benchmark_id=bm_slug, + retrieve_k=args.retrieve_k, + evo_threshold=args.evo_threshold, + embedding_model=args.embedding_model, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=1, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + + stores = MemoryStore.list_stores() + store_key = (f"amem_isolated_{bm_slug}" if args.mode == "isolated" + else "amem_sequential_global") + store = stores.get(store_key) + if store: + stats = store.get_stats() + print(f" memory: sessions={store.session_count}, " + f"memories={stats['total_memories']}, " + f"evolutions={stats['total_evolutions']}") + store.save_checkpoint(str(output_dir / f"memory_{store_key}.json")) + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.mode, args.seed, args.model, + all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + elif args.mode == "interleaved": + for i, (bm_slug, task_id) in enumerate(task_order): + if _restored and i < _restored_session_count: + print(f" Skipping Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} (cached)") + continue + + print(f"\n--- Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} ---") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("a_mem")( + model=args.model, + memory_model=args.memory_model, + shuffle_mode="interleaved", + benchmark_id=bm_slug, + retrieve_k=args.retrieve_k, + evo_threshold=args.evo_threshold, + embedding_model=args.embedding_model, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + max_workers=1, + output_dir=str(output_dir), + ) + + sr = results.session_results[0] + rec = record_online_metrics( + metrics_path, session_index, bm_slug, task_id, sr, + "interleaved", args.seed, args.model, + all_scores, bm_scores, + ) + print(f" score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + + stores = MemoryStore.list_stores() + store = stores.get("amem_interleaved_global") + if store: + stats = store.get_stats() + print(f" memory: sessions={store.session_count}, " + f"memories={stats['total_memories']}, " + f"evolutions={stats['total_evolutions']}") + store.save_checkpoint(str(output_dir / "memory_amem_interleaved_global.json")) + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Final Summary") + print(f"{'=' * 70}") + if all_scores: + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + + stores = MemoryStore.list_stores() + for store_id, store in stores.items(): + stats = store.get_stats() + print(f" memory[{store_id}]: sessions={store.session_count}, " + f"memories={stats['total_memories']}, " + f"evolutions={stats['total_evolutions']}, " + f"avg_links={stats['avg_links']:.2f}") + + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description="Run A-Mem test-time learning experiment") + parser.add_argument("--mode", required=True, choices=["isolated", "sequential", "interleaved"]) + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--memory-model", default=None, + help="Model for memory evolution (defaults to --model)") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + # A-Mem specific hyperparameters + parser.add_argument("--retrieve-k", type=int, default=10, + help="Number of memories to retrieve per query") + parser.add_argument("--evo-threshold", type=int, default=100, + help="Consolidate embeddings every N evolutions") + parser.add_argument("--embedding-model", default="all-MiniLM-L6-v2", + help="SentenceTransformer model for memory retrieval") + args = parser.parse_args() + + if args.memory_model is None: + args.memory_model = args.model + + run_experiment(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.sh b/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.sh new file mode 100644 index 00000000..e9036dd0 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/a_mem/run_experiment.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + +cd "$(dirname "$0")" + +# ============================================================ +# Configuration +# ============================================================ +SEED=44 +NUM_TASKS=50 +MODEL="openai/gpt-5.4" +MEMORY_MODEL="openai/gpt-5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" +MODE="sequential" # isolated | sequential | interleaved + +# A-Mem specific hyperparameters +RETRIEVE_K=10 +EVO_THRESHOLD=100 +EMBEDDING_MODEL="all-MiniLM-L6-v2" + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|/|_|g') +RUN_TAG="amem_${MODE}_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "Mode: ${MODE}" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" +echo "A-Mem: retrieve_k=${RETRIEVE_K} evo_threshold=${EVO_THRESHOLD} embedding=${EMBEDDING_MODEL}" + +mkdir -p "$OUTPUT_BASE" + +ALL_BENCHMARKS="hle,bfcl,browsecompplus,appworld,swebench,tau2" +if [ "$MODE" = "isolated" ]; then + for BENCH in swebench tau2 browsecompplus appworld hle bfcl; do + echo "=== Running isolated ${BENCH} ===" + uv run python run_experiment.py \ + --mode isolated --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL --memory-model $MEMORY_MODEL \ + --retrieve-k $RETRIEVE_K --evo-threshold $EVO_THRESHOLD \ + --embedding-model $EMBEDDING_MODEL \ + $SETTINGS_ARGS \ + --benchmarks $BENCH \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_${BENCH} \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_${BENCH}.log + done +else + echo "=== Running ${MODE} (all benchmarks) ===" + uv run python run_experiment.py \ + --mode $MODE --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL --memory-model $MEMORY_MODEL \ + --retrieve-k $RETRIEVE_K --evo-threshold $EVO_THRESHOLD \ + --embedding-model $EMBEDDING_MODEL \ + $SETTINGS_ARGS \ + --benchmarks $ALL_BENCHMARKS \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_all \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_all.log +fi + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/ace/run_experiment.py b/labs/AgentStream/exgentic/scripts/ace/run_experiment.py new file mode 100644 index 00000000..b1ab9dba --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/ace/run_experiment.py @@ -0,0 +1,386 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +# --- ExGentic imports --- +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.agents.ace.playbook_store import PlaybookStore +from exgentic.agents.ace.playbook_utils import get_playbook_stats +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + "runner": "direct", + }, + "agent_kwargs": {}, + }, +} + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + """Extract total input/output tokens from cost_reports dict.""" + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def get_memory_tokens(mode: str, bm_slug: str) -> tuple[int, int]: + stores = PlaybookStore.list_stores() + if mode == "isolated": + store = stores.get(f"ace_isolated_{bm_slug}") + elif mode == "sequential": + store = stores.get("ace_sequential_global") + elif mode == "interleaved": + store = stores.get("ace_interleaved_global") + else: + return 0, 0 + if store is None: + return 0, 0 + stats = get_playbook_stats(store.playbook) + mem_tokens = len(store.playbook) // 4 + return mem_tokens, stats["total_bullets"] + + + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + mode: str, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + memory_tokens, playbook_bullets = get_memory_tokens(mode, bm_slug) + + record = { + "session_index": session_index, + "seed": seed, + "mode": mode, + "agent": "ace", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_tokens": memory_tokens, + "playbook_bullets": playbook_bullets, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + +def run_experiment(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" ACE Experiment: mode={args.mode} seed={args.seed}") + print(f" model={args.model} num_tasks={args.num_tasks}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, args.mode) + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": args.mode, + "seed": args.seed, + "agent": "ace", + "model": args.model, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + PlaybookStore.reset_all() + if args.mode == "interleaved": + _ckpt_ids = ["ace_interleaved_global"] + elif args.mode == "sequential": + _ckpt_ids = ["ace_sequential_global"] + else: + _ckpt_ids = [f"ace_isolated_{b}" for b in benchmarks_to_run] + + _restored = False + _restored_session_count = 0 + for sid in _ckpt_ids: + ckpt_path = output_dir / f"playbook_{sid}.json" + if ckpt_path.exists(): + store = PlaybookStore.get_or_create( + shuffle_mode=args.mode, + benchmark_id=sid.replace("ace_isolated_", "") if args.mode == "isolated" else None, + ) + store.load_checkpoint(str(ckpt_path)) + _restored_session_count = max(_restored_session_count, store.session_count) + _restored = True + if _restored: + print(f" ♻️ Restored playbook from checkpoint (session_count={_restored_session_count})") + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + if metrics_path.exists(): + kept_lines: list[str] = [] + with open(metrics_path, "r") as f: + for line in f: + if _restored and session_index >= _restored_session_count: + break + rec = json.loads(line) + all_scores.append(rec["score"]) + bm_scores[rec["benchmark_slug"]].append(rec["score"]) + kept_lines.append(line) + session_index += 1 + with open(metrics_path, "w") as f: + f.writelines(kept_lines) + if session_index > 0: + print(f" ♻️ Restored {session_index} metrics records (cum_avg={sum(all_scores)/len(all_scores):.3f})") + + if args.mode in ("isolated", "sequential"): + _completed_benchmarks: set[str] = set() + if _restored and session_index > 0: + _bm_counts: dict[str, int] = defaultdict(int) + with open(metrics_path, "r") as f: + for line in f: + rec = json.loads(line) + _bm_counts[rec["benchmark_slug"]] += 1 + for bm_slug, task_ids in group_by_benchmark(task_order): + if _bm_counts.get(bm_slug, 0) >= len(task_ids): + _completed_benchmarks.add(bm_slug) + if _completed_benchmarks: + print(f" ⏭️ Skipping completed benchmarks: {sorted(_completed_benchmarks)}") + + for bm_slug, task_ids in group_by_benchmark(task_order): + if bm_slug in _completed_benchmarks: + continue + + print(f"\n{'=' * 60}") + print(f" {args.mode.upper()} — {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("ace")( + model=args.model, + curator_model=args.model, + shuffle_mode=args.mode, + benchmark_id=bm_slug, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=1, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + + stores = PlaybookStore.list_stores() + store_key = (f"ace_isolated_{bm_slug}" if args.mode == "isolated" + else "ace_sequential_global") + store = stores.get(store_key) + if store: + stats = get_playbook_stats(store.playbook) + print(f" playbook: sessions={store.session_count}, " + f"bullets={stats['total_bullets']}") + store.save_checkpoint(str(output_dir / f"playbook_{store_key}.json")) + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.mode, args.seed, args.model, + all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + elif args.mode == "interleaved": + for i, (bm_slug, task_id) in enumerate(task_order): + if _restored and i < _restored_session_count: + print(f" ⏭️ Skipping Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} (cached)") + continue + + print(f"\n--- Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} ---") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("ace")( + model=args.model, + curator_model=args.model, + shuffle_mode="interleaved", + benchmark_id=bm_slug, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + max_workers=1, + output_dir=str(output_dir), + ) + + sr = results.session_results[0] + + rec = record_online_metrics( + metrics_path, session_index, bm_slug, task_id, sr, + "interleaved", args.seed, args.model, + all_scores, bm_scores, + ) + print(f" score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + stores = PlaybookStore.list_stores() + store = stores.get("ace_interleaved_global") + if store: + stats = get_playbook_stats(store.playbook) + print(f" playbook: sessions={store.session_count}, " + f"bullets={stats['total_bullets']}") + store.save_checkpoint(str(output_dir / "playbook_ace_interleaved_global.json")) + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Final Summary") + print(f"{'=' * 70}") + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + + stores = PlaybookStore.list_stores() + for store_id, store in stores.items(): + stats = get_playbook_stats(store.playbook) + print(f" playbook[{store_id}]: sessions={store.session_count}, " + f"bullets={stats['total_bullets']}") + + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description="Run ACE test-time learning experiment") + parser.add_argument("--mode", required=True, choices=["isolated", "sequential", "interleaved"]) + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + args = parser.parse_args() + run_experiment(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/ace/run_experiment.sh b/labs/AgentStream/exgentic/scripts/ace/run_experiment.sh new file mode 100644 index 00000000..c1ad04cd --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/ace/run_experiment.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + +cd "$(dirname "$0")" + +# ============================================================ +# Configuration +# ============================================================ +SEED=44 +NUM_TASKS=50 +MODEL="openai/gpt-5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" +MODE="sequential" # isolated | sequential | interleaved + + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|/|_|g') +RUN_TAG="ace_${MODE}_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "Mode: ${MODE}" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" + +mkdir -p "$OUTPUT_BASE" + +ALL_BENCHMARKS="hle,bfcl,browsecompplus,appworld,swebench,tau2" + +if [ "$MODE" = "isolated" ]; then + for BENCH in swebench tau2 browsecompplus appworld hle bfcl; do + echo "=== Running isolated ${BENCH} ===" + uv run python run_experiment.py \ + --mode isolated --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $BENCH \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_${BENCH} \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_${BENCH}.log + done +else + echo "=== Running ${MODE} (all benchmarks) ===" + uv run python run_experiment.py \ + --mode $MODE --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $ALL_BENCHMARKS \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_all \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_all.log +fi + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.py b/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.py new file mode 100644 index 00000000..0c4f756b --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.py @@ -0,0 +1,382 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.agents.autoskill.skill_store import SkillStore +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + "runner": "direct", + }, + "agent_kwargs": {}, + }, +} + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + """Extract total input/output tokens from cost_reports dict.""" + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def get_memory_tokens(mode: str, bm_slug: str) -> tuple[int, int]: + """Return (memory_tokens, skill_count) for current skill store state.""" + stores = SkillStore.list_stores() + if mode == "isolated": + store = stores.get(f"autoskill_isolated_{bm_slug}") + elif mode == "sequential": + store = stores.get("autoskill_sequential_global") + elif mode == "interleaved": + store = stores.get("autoskill_interleaved_global") + else: + return 0, 0 + if store is None: + return 0, 0 + skills = store.list_skills() + total_chars = sum(len(s.to_search_text()) for s in skills) + mem_tokens = total_chars // 4 + return mem_tokens, len(skills) + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + mode: str, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + memory_tokens, skill_count = get_memory_tokens(mode, bm_slug) + + record = { + "session_index": session_index, + "seed": seed, + "mode": mode, + "agent": "autoskill", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_tokens": memory_tokens, + "skill_count": skill_count, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + +def run_experiment(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" AutoSkill Experiment: mode={args.mode} seed={args.seed}") + print(f" model={args.model} num_tasks={args.num_tasks}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, args.mode) + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": args.mode, + "seed": args.seed, + "agent": "autoskill", + "model": args.model, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + SkillStore.reset_all() + if args.mode == "interleaved": + _ckpt_ids = ["autoskill_interleaved_global"] + elif args.mode == "sequential": + _ckpt_ids = ["autoskill_sequential_global"] + else: + _ckpt_ids = [f"autoskill_isolated_{b}" for b in benchmarks_to_run] + + _restored = False + _restored_session_count = 0 + for sid in _ckpt_ids: + ckpt_path = output_dir / f"skillstore_{sid}.json" + if ckpt_path.exists(): + store = SkillStore.get_or_create( + shuffle_mode=args.mode, + benchmark_id=sid.replace("autoskill_isolated_", "") if args.mode == "isolated" else None, + ) + store.load_checkpoint(str(ckpt_path)) + _restored_session_count = max(_restored_session_count, store.session_count) + _restored = True + if _restored: + print(f" ♻️ Restored skill store from checkpoint (session_count={_restored_session_count})") + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + if metrics_path.exists(): + kept_lines: list[str] = [] + with open(metrics_path, "r") as f: + for line in f: + if _restored and session_index >= _restored_session_count: + break + rec = json.loads(line) + all_scores.append(rec["score"]) + bm_scores[rec["benchmark_slug"]].append(rec["score"]) + kept_lines.append(line) + session_index += 1 + with open(metrics_path, "w") as f: + f.writelines(kept_lines) + if session_index > 0: + print(f" ♻️ Restored {session_index} metrics records (cum_avg={sum(all_scores)/len(all_scores):.3f})") + + if args.mode in ("isolated", "sequential"): + _completed_benchmarks: set[str] = set() + if _restored and session_index > 0: + _bm_counts: dict[str, int] = defaultdict(int) + with open(metrics_path, "r") as f: + for line in f: + rec = json.loads(line) + _bm_counts[rec["benchmark_slug"]] += 1 + for bm_slug, task_ids in group_by_benchmark(task_order): + if _bm_counts.get(bm_slug, 0) >= len(task_ids): + _completed_benchmarks.add(bm_slug) + if _completed_benchmarks: + print(f" ⏭️ Skipping completed benchmarks: {sorted(_completed_benchmarks)}") + + for bm_slug, task_ids in group_by_benchmark(task_order): + if bm_slug in _completed_benchmarks: + continue + + print(f"\n{'=' * 60}") + print(f" {args.mode.upper()} — {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("autoskill")( + model=args.model, + skill_model=args.model, + shuffle_mode=args.mode, + benchmark_id=bm_slug, + embedding_model="all-MiniLM-L6-v2", + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=1, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + stores = SkillStore.list_stores() + store_key = (f"autoskill_isolated_{bm_slug}" if args.mode == "isolated" + else "autoskill_sequential_global") + store = stores.get(store_key) + if store: + print(f" skillbank: sessions={store.session_count}, " + f"skills={store.skill_count}") + store.save_checkpoint(str(output_dir / f"skillstore_{store_key}.json")) + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.mode, args.seed, args.model, + all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + elif args.mode == "interleaved": + for i, (bm_slug, task_id) in enumerate(task_order): + if _restored and i < _restored_session_count: + print(f" ⏭️ Skipping Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} (cached)") + continue + + print(f"\n--- Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} ---") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("autoskill")( + model=args.model, + skill_model=args.model, + shuffle_mode="interleaved", + benchmark_id=bm_slug, + embedding_model="all-MiniLM-L6-v2", + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + max_workers=1, + output_dir=str(output_dir), + ) + + sr = results.session_results[0] + + rec = record_online_metrics( + metrics_path, session_index, bm_slug, task_id, sr, + "interleaved", args.seed, args.model, + all_scores, bm_scores, + ) + print(f" score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + + stores = SkillStore.list_stores() + store = stores.get("autoskill_interleaved_global") + if store: + print(f" skillbank: sessions={store.session_count}, " + f"skills={store.skill_count}") + store.save_checkpoint(str(output_dir / "skillstore_autoskill_interleaved_global.json")) + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Final Summary") + print(f"{'=' * 70}") + if all_scores: + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + + stores = SkillStore.list_stores() + for store_id, store in stores.items(): + print(f" skillbank[{store_id}]: sessions={store.session_count}, " + f"skills={store.skill_count}") + + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + +def main(): + parser = argparse.ArgumentParser(description="Run AutoSkill test-time learning experiment") + parser.add_argument("--mode", required=True, choices=["isolated", "sequential", "interleaved"]) + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + args = parser.parse_args() + run_experiment(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.sh b/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.sh new file mode 100644 index 00000000..86b59c1a --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/autoskill/run_experiment.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + +cd "$(dirname "$0")" + +# ============================================================ +# Configuration +# ============================================================ +SEED=42 +NUM_TASKS=50 +MODEL="openai/gpt5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" +MODE="sequential" # isolated | sequential | interleaved + + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|anthropic/||; s|/|_|g') +RUN_TAG="autoskill_${MODE}_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "Mode: ${MODE}" +echo "Model: ${MODEL}" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" + +mkdir -p "$OUTPUT_BASE" + +ALL_BENCHMARKS="hle,bfcl,browsecompplus,appworld,swebench,tau2" + +if [ "$MODE" = "isolated" ]; then + for BENCH in swebench tau2 browsecompplus appworld hle bfcl; do + echo "=== Running isolated ${BENCH} ===" + uv run python run_experiment.py \ + --mode isolated --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $BENCH \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_${BENCH} \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_${BENCH}.log + done +else + echo "=== Running ${MODE} (all benchmarks) ===" + uv run python run_experiment.py \ + --mode $MODE --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $ALL_BENCHMARKS \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_all \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_all.log +fi + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/harness/run_experiment.py b/labs/AgentStream/exgentic/scripts/harness/run_experiment.py new file mode 100644 index 00000000..b211007c --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/harness/run_experiment.py @@ -0,0 +1,386 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +# --- ExGentic imports --- +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.agents.harness.harness_store import HarnessStore +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + "runner": "direct", + }, + "agent_kwargs": {}, + }, +} + + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def get_memory_tokens(mode: str, bm_slug: str) -> tuple[int, int]: + stores = HarnessStore.list_stores() + if mode == "isolated": + store = stores.get(f"harness_isolated_{bm_slug}") + elif mode == "sequential": + store = stores.get("harness_sequential_global") + elif mode == "interleaved": + store = stores.get("harness_interleaved_global") + else: + return 0, 0 + if store is None: + return 0, 0 + total_chars = len(store.system_prompt) + len(store.memory) + for skill in store.list_skills(): + total_chars += len(skill.description) + len(skill.body) + mem_tokens = total_chars // 4 + return mem_tokens, store.skill_count + + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + mode: str, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + memory_tokens, skill_count = get_memory_tokens(mode, bm_slug) + + record = { + "session_index": session_index, + "seed": seed, + "mode": mode, + "agent": "harness", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_tokens": memory_tokens, + "skill_count": skill_count, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + +def run_experiment(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" Harness Experiment: mode={args.mode} seed={args.seed}") + print(f" model={args.model} num_tasks={args.num_tasks}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, args.mode) + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": args.mode, + "seed": args.seed, + "agent": "harness", + "model": args.model, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + HarnessStore.reset_all() + if args.mode == "interleaved": + _ckpt_ids = ["harness_interleaved_global"] + elif args.mode == "sequential": + _ckpt_ids = ["harness_sequential_global"] + else: + _ckpt_ids = [f"harness_isolated_{b}" for b in benchmarks_to_run] + + _restored = False + _restored_session_count = 0 + for sid in _ckpt_ids: + ckpt_path = output_dir / f"harness_{sid}.json" + if ckpt_path.exists(): + store = HarnessStore.get_or_create( + shuffle_mode=args.mode, + benchmark_id=sid.replace("harness_isolated_", "") if args.mode == "isolated" else None, + ) + store.load_checkpoint(str(ckpt_path)) + _restored_session_count = max(_restored_session_count, store.session_count) + _restored = True + if _restored: + print(f" ♻️ Restored harness store from checkpoint (session_count={_restored_session_count})") + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + if metrics_path.exists(): + kept_lines: list[str] = [] + with open(metrics_path, "r") as f: + for line in f: + if _restored and session_index >= _restored_session_count: + break + rec = json.loads(line) + all_scores.append(rec["score"]) + bm_scores[rec["benchmark_slug"]].append(rec["score"]) + kept_lines.append(line) + session_index += 1 + with open(metrics_path, "w") as f: + f.writelines(kept_lines) + if session_index > 0: + print(f" ♻️ Restored {session_index} metrics records (cum_avg={sum(all_scores)/len(all_scores):.3f})") + + if args.mode in ("isolated", "sequential"): + _completed_benchmarks: set[str] = set() + if _restored and session_index > 0: + _bm_counts: dict[str, int] = defaultdict(int) + with open(metrics_path, "r") as f: + for line in f: + rec = json.loads(line) + _bm_counts[rec["benchmark_slug"]] += 1 + for bm_slug, task_ids in group_by_benchmark(task_order): + if _bm_counts.get(bm_slug, 0) >= len(task_ids): + _completed_benchmarks.add(bm_slug) + if _completed_benchmarks: + print(f" ⏭️ Skipping completed benchmarks: {sorted(_completed_benchmarks)}") + + for bm_slug, task_ids in group_by_benchmark(task_order): + if bm_slug in _completed_benchmarks: + continue + + print(f"\n{'=' * 60}") + print(f" {args.mode.upper()} — {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("harness")( + model=args.model, + evolver_model=args.model, + shuffle_mode=args.mode, + benchmark_id=bm_slug, + embedding_model="all-MiniLM-L6-v2", + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=1, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + stores = HarnessStore.list_stores() + store_key = (f"harness_isolated_{bm_slug}" if args.mode == "isolated" + else "harness_sequential_global") + store = stores.get(store_key) + if store: + print(f" harness: sessions={store.session_count}, " + f"skills={store.skill_count}") + store.save_checkpoint(str(output_dir / f"harness_{store_key}.json")) + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.mode, args.seed, args.model, + all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + elif args.mode == "interleaved": + for i, (bm_slug, task_id) in enumerate(task_order): + if _restored and i < _restored_session_count: + print(f" ⏭️ Skipping Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} (cached)") + continue + + print(f"\n--- Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} ---") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("harness")( + model=args.model, + evolver_model=args.model, + shuffle_mode="interleaved", + benchmark_id=bm_slug, + embedding_model="all-MiniLM-L6-v2", + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + max_workers=1, + output_dir=str(output_dir), + ) + + sr = results.session_results[0] + + rec = record_online_metrics( + metrics_path, session_index, bm_slug, task_id, sr, + "interleaved", args.seed, args.model, + all_scores, bm_scores, + ) + print(f" score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + + stores = HarnessStore.list_stores() + store = stores.get("harness_interleaved_global") + if store: + print(f" harness: sessions={store.session_count}, " + f"skills={store.skill_count}") + store.save_checkpoint(str(output_dir / "harness_harness_interleaved_global.json")) + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Final Summary") + print(f"{'=' * 70}") + if all_scores: + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + + stores = HarnessStore.list_stores() + for store_id, store in stores.items(): + print(f" harness[{store_id}]: sessions={store.session_count}, " + f"skills={store.skill_count}") + + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description="Run Harness test-time learning experiment") + parser.add_argument("--mode", required=True, choices=["isolated", "sequential", "interleaved"]) + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + args = parser.parse_args() + run_experiment(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/harness/run_experiment.sh b/labs/AgentStream/exgentic/scripts/harness/run_experiment.sh new file mode 100644 index 00000000..71ab5454 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/harness/run_experiment.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + +cd "$(dirname "$0")" + +SEED=44 +NUM_TASKS=50 +MODEL="openai/gpt-5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" +MODE="sequential" # isolated | sequential | interleaved + + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|/|_|g') +RUN_TAG="harness_${MODE}_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "Mode: ${MODE}" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" + +mkdir -p "$OUTPUT_BASE" + +ALL_BENCHMARKS="hle,bfcl,browsecompplus,appworld,swebench,tau2" + +if [ "$MODE" = "isolated" ]; then + for BENCH in swebench tau2 browsecompplus appworld hle bfcl; do + echo "=== Running isolated ${BENCH} ===" + uv run python run_experiment.py \ + --mode isolated --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $BENCH \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_${BENCH} \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_${BENCH}.log + done +else + echo "=== Running ${MODE} (all benchmarks) ===" + uv run python run_experiment.py \ + --mode $MODE --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks $ALL_BENCHMARKS \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_all \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_all.log +fi + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/litellm/run_baseline.py b/labs/AgentStream/exgentic/scripts/litellm/run_baseline.py new file mode 100644 index 00000000..4432741e --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/litellm/run_baseline.py @@ -0,0 +1,238 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "ace")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, +} + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + + record = { + "session_index": session_index, + "seed": seed, + "mode": "baseline", + "agent": "tool_calling", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_tokens": 0, + "playbook_bullets": 0, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + +def run_baseline(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" Baseline: tool_calling agent (no learning)") + print(f" seed={args.seed} model={args.model} num_tasks={args.num_tasks}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, "isolated") + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": "baseline", + "seed": args.seed, + "agent": "tool_calling", + "model": args.model, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + for bm_slug, task_ids in group_by_benchmark(task_order): + print(f"\n{'=' * 60}") + print(f" BASELINE — {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("tool_calling")( + model=args.model, + runner="direct", + model_settings=model_settings, + allow_truncated_messages=True, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=5, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.seed, args.model, all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Baseline Summary") + print(f"{'=' * 70}") + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description="Run baseline (tool_calling agent, no learning)") + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + args = parser.parse_args() + run_baseline(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/litellm/run_baseline.sh b/labs/AgentStream/exgentic/scripts/litellm/run_baseline.sh new file mode 100644 index 00000000..382fa7e2 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/litellm/run_baseline.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + +cd "$(dirname "$0")" + +SEED=42 +NUM_TASKS=50 +MODEL="openai/gpt-5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|/|_|g') +RUN_TAG="baseline_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" + +# HLE +echo "=== Running HLE ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks hle \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_hle \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_hle.log + +# BFCL +echo "=== Running BFCL ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks bfcl \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_bfcl \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_bfcl.log + +# Tau2 +echo "=== Running Tau2 ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks tau2 \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_tau2 \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_tau2.log + +# BrowseCompPlus +echo "=== Running BrowseCompPlus ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks browsecompplus \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_browsecompplus \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_browsecompplus.log + +# AppWorld +echo "=== Running AppWorld ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks appworld \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_appworld \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_appworld.log + +# SWE-bench +echo "=== Running SWE-bench ===" +uv run python run_baseline.py \ + --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL $SETTINGS_ARGS \ + --benchmarks swebench \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_swebench \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_swebench.log + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.py b/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.py new file mode 100644 index 00000000..6c49a725 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.py @@ -0,0 +1,403 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "utils")) + +os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + +# --- ExGentic imports --- +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import load_agent, load_benchmark +from exgentic.agents.reasoning_bank.rb_store import ReasoningBankStore +from exgentic.core.types import ModelSettings + +from task_ordering import get_unified_task_order, group_by_benchmark + +BENCHMARK_REGISTRY: dict[str, dict[str, Any]] = { + "browsecompplus": { + "bm_kwargs": { + "searcher_type": "faiss", + "include_get_document": True, + "eval_model_id": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "swebench": { + "bm_kwargs": { + "subset": "princeton-nlp/SWE-bench_Verified", + }, + "agent_kwargs": {}, + }, + "appworld": { + "bm_kwargs": { + "subset": "test_challenge", + }, + "agent_kwargs": { + "enable_tool_shortlisting": True, + "max_selected_tools": 30, + }, + }, + "bfcl": { + "bm_kwargs": { + "subset": "multi_turn_base", + }, + "agent_kwargs": {}, + }, + "tau2": { + "bm_kwargs": { + "subset": "telecom", + "user_simulator_model": "openai/gpt-5.4", + }, + "agent_kwargs": {}, + }, + "hle": { + "bm_kwargs": { + "judge_model": "openai/gpt-5.4", + "runner": "direct", + }, + "agent_kwargs": {}, + }, +} + +def extract_token_counts(cost_reports: dict) -> tuple[int, int]: + total_in, total_out = 0, 0 + for report in cost_reports.values(): + if isinstance(report, dict): + total_in += report.get("input_tokens", 0) + total_out += report.get("output_tokens", 0) + elif hasattr(report, "input_tokens"): + total_in += report.input_tokens + total_out += report.output_tokens + return total_in, total_out + + +def get_memory_stats(mode: str, bm_slug: str) -> tuple[int, int]: + stores = ReasoningBankStore.list_stores() + if mode == "isolated": + store = stores.get(f"rb_isolated_{bm_slug}") + elif mode == "sequential": + store = stores.get("rb_sequential_global") + elif mode == "interleaved": + store = stores.get("rb_interleaved_global") + else: + return 0, 0 + if store is None: + return 0, 0 + entries = store.get_entries() + total_chars = sum(len("\n\n".join(e.memory_items)) for e in entries) + mem_tokens = total_chars // 4 + return len(entries), mem_tokens + + + +def record_online_metrics( + metrics_path: Path, + session_index: int, + bm_slug: str, + task_id: str, + sr: Any, + mode: str, + seed: int, + model: str, + all_scores: list[float], + bm_scores: dict[str, list[float]], +): + score = sr.score if sr.score is not None else (1.0 if sr.success else 0.0) + all_scores.append(score) + bm_scores[bm_slug].append(score) + + input_tokens, output_tokens = extract_token_counts(sr.cost_reports) + memory_entries, memory_tokens = get_memory_stats(mode, bm_slug) + + record = { + "session_index": session_index, + "seed": seed, + "mode": mode, + "agent": "reasoning_bank", + "model": model, + "benchmark_slug": bm_slug, + "task_id": task_id, + "score": score, + "cumulative_avg_score": sum(all_scores) / len(all_scores), + "benchmark_cumulative_avg_score": ( + sum(bm_scores[bm_slug]) / len(bm_scores[bm_slug]) + ), + "steps": sr.steps, + "action_count": sr.action_count, + "agent_cost": sr.agent_cost, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "memory_entries": memory_entries, + "memory_tokens": memory_tokens, + "execution_time": sr.execution_time, + "status": sr.status.value if hasattr(sr.status, "value") else str(sr.status), + "timestamp": datetime.now().isoformat(), + } + + with open(metrics_path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + + +def run_experiment(args): + benchmarks_to_run = [s.strip() for s in args.benchmarks.split(",")] + configs = {k: BENCHMARK_REGISTRY[k] for k in benchmarks_to_run} + + settings_kwargs = {} + if args.max_tokens is not None: + settings_kwargs["max_tokens"] = args.max_tokens + if args.reasoning_effort is not None: + settings_kwargs["reasoning_effort"] = args.reasoning_effort + model_settings = ModelSettings(**settings_kwargs) + + print(f"\n{'=' * 70}") + print(f" ReasoningBank Experiment: mode={args.mode} seed={args.seed}") + print(f" model={args.model} num_tasks={args.num_tasks}") + print(f" embedding_model={args.embedding_model}") + print(f" top_k_memories={args.top_k_memories} max_memory_items={args.max_memory_items}") + print(f" model_settings={settings_kwargs or 'default'}") + print(f" benchmarks={benchmarks_to_run}") + print(f" output_dir={args.output_dir}") + print(f"{'=' * 70}\n") + + task_order = get_unified_task_order(configs, args.num_tasks, args.seed, args.mode) + print(f"Total tasks: {len(task_order)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + metrics_path = output_dir / "online_metrics.jsonl" + + exp_config = { + "mode": args.mode, + "seed": args.seed, + "agent": "reasoning_bank", + "model": args.model, + "embedding_model": args.embedding_model, + "top_k_memories": args.top_k_memories, + "max_memory_items": args.max_memory_items, + "num_tasks": args.num_tasks, + "benchmarks": benchmarks_to_run, + "task_order": [(s, t) for s, t in task_order], + } + with open(output_dir / "experiment_config.json", "w") as f: + json.dump(exp_config, f, indent=2) + + ReasoningBankStore.reset_all() + if args.mode == "interleaved": + _ckpt_ids = ["rb_interleaved_global"] + elif args.mode == "sequential": + _ckpt_ids = ["rb_sequential_global"] + else: + _ckpt_ids = [f"rb_isolated_{b}" for b in benchmarks_to_run] + + _restored = False + _restored_session_count = 0 + for sid in _ckpt_ids: + ckpt_path = output_dir / f"store_{sid}.json" + if ckpt_path.exists(): + store = ReasoningBankStore.get_or_create( + shuffle_mode=args.mode, + benchmark_id=sid.replace("rb_isolated_", "") if args.mode == "isolated" else None, + ) + store.load_checkpoint(str(ckpt_path)) + _restored_session_count = max(_restored_session_count, store.session_count) + _restored = True + if _restored: + print(f" Restored store from checkpoint (session_count={_restored_session_count})") + + all_scores: list[float] = [] + bm_scores: defaultdict[str, list[float]] = defaultdict(list) + session_index = 0 + + if metrics_path.exists(): + kept_lines: list[str] = [] + with open(metrics_path, "r") as f: + for line in f: + if _restored and session_index >= _restored_session_count: + break + rec = json.loads(line) + all_scores.append(rec["score"]) + bm_scores[rec["benchmark_slug"]].append(rec["score"]) + kept_lines.append(line) + session_index += 1 + with open(metrics_path, "w") as f: + f.writelines(kept_lines) + if session_index > 0: + print(f" Restored {session_index} metrics records (cum_avg={sum(all_scores)/len(all_scores):.3f})") + + if args.mode in ("isolated", "sequential"): + _completed_benchmarks: set[str] = set() + if _restored and session_index > 0: + _bm_counts: dict[str, int] = defaultdict(int) + with open(metrics_path, "r") as f: + for line in f: + rec = json.loads(line) + _bm_counts[rec["benchmark_slug"]] += 1 + for bm_slug, task_ids in group_by_benchmark(task_order): + if _bm_counts.get(bm_slug, 0) >= len(task_ids): + _completed_benchmarks.add(bm_slug) + if _completed_benchmarks: + print(f" Skipping completed benchmarks: {sorted(_completed_benchmarks)}") + + for bm_slug, task_ids in group_by_benchmark(task_order): + if bm_slug in _completed_benchmarks: + continue + + print(f"\n{'=' * 60}") + print(f" {args.mode.upper()} -- {bm_slug} ({len(task_ids)} tasks)") + print(f"{'=' * 60}\n") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("reasoning_bank")( + model=args.model, + memory_model=args.model, + eval_model=args.model, + embedding_model=args.embedding_model, + top_k_memories=args.top_k_memories, + max_memory_items=args.max_memory_items, + shuffle_mode=args.mode, + benchmark_id=bm_slug, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=task_ids, + max_workers=1, + output_dir=str(output_dir), + ) + + print(f" {bm_slug} score={results.benchmark_score}") + + stores = ReasoningBankStore.list_stores() + store_key = (f"rb_isolated_{bm_slug}" if args.mode == "isolated" + else "rb_sequential_global") + store = stores.get(store_key) + if store: + print(f" store: sessions={store.session_count}, " + f"entries={store.entry_count}") + store.save_checkpoint(str(output_dir / f"store_{store_key}.json")) + + for i, sr in enumerate(results.session_results): + tid = task_ids[i] if i < len(task_ids) else sr.task_id or "?" + rec = record_online_metrics( + metrics_path, session_index, bm_slug, tid, sr, + args.mode, args.seed, args.model, + all_scores, bm_scores, + ) + print(f" [{session_index}] {bm_slug}::{tid} " + f"score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + session_index += 1 + + elif args.mode == "interleaved": + for i, (bm_slug, task_id) in enumerate(task_order): + if _restored and i < _restored_session_count: + print(f" Skipping Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} (cached)") + continue + + print(f"\n--- Interleaved [{i+1}/{len(task_order)}] {bm_slug}::{task_id} ---") + + bm_kwargs = configs[bm_slug]["bm_kwargs"] + agent_kwargs = configs[bm_slug].get("agent_kwargs", {}) + + benchmark = load_benchmark(bm_slug)(**bm_kwargs) + agent = load_agent("reasoning_bank")( + model=args.model, + memory_model=args.model, + eval_model=args.model, + embedding_model=args.embedding_model, + top_k_memories=args.top_k_memories, + max_memory_items=args.max_memory_items, + shuffle_mode="interleaved", + benchmark_id=bm_slug, + runner="direct", + model_settings=model_settings, + **agent_kwargs, + ) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + max_workers=1, + output_dir=str(output_dir), + ) + + sr = results.session_results[0] + + rec = record_online_metrics( + metrics_path, session_index, bm_slug, task_id, sr, + "interleaved", args.seed, args.model, + all_scores, bm_scores, + ) + print(f" score={rec['score']:.2f} cum={rec['cumulative_avg_score']:.3f} " + f"steps={rec['steps']}") + + stores = ReasoningBankStore.list_stores() + store = stores.get("rb_interleaved_global") + if store: + print(f" store: sessions={store.session_count}, " + f"entries={store.entry_count}") + store.save_checkpoint(str(output_dir / "store_rb_interleaved_global.json")) + session_index += 1 + + print(f"\n{'=' * 70}") + print(f" Final Summary") + print(f"{'=' * 70}") + if all_scores: + print(f" Overall avg score: {sum(all_scores)/len(all_scores):.3f}") + for bm, scores in sorted(bm_scores.items()): + print(f" {bm:20s}: avg={sum(scores)/len(scores):.3f} n={len(scores)}") + + stores = ReasoningBankStore.list_stores() + for store_id, store in stores.items(): + print(f" store[{store_id}]: sessions={store.session_count}, " + f"entries={store.entry_count}") + + print(f"\n Metrics: {metrics_path}") + print(f" Output: {output_dir}") + +def main(): + parser = argparse.ArgumentParser(description="Run ReasoningBank test-time learning experiment") + parser.add_argument("--mode", required=True, choices=["isolated", "sequential", "interleaved"]) + parser.add_argument("--seed", type=int, required=True) + parser.add_argument("--num-tasks", type=int, default=50, help="Tasks per benchmark") + parser.add_argument("--model", default="openai/gpt-5.4") + parser.add_argument("--embedding-model", default="all-MiniLM-L6-v2", + help="Local SentenceTransformer model for memory retrieval") + parser.add_argument("--benchmarks", default="browsecompplus,swebench,bfcl,tau2", + help="Comma-separated benchmark slugs") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--max-tokens", type=int, default=None, help="Max output tokens") + parser.add_argument("--reasoning-effort", default=None, help="Reasoning effort (low/medium/high)") + # ReasoningBank specific hyperparameters + parser.add_argument("--top-k-memories", type=int, default=1, + help="Number of memories to retrieve per query") + parser.add_argument("--max-memory-items", type=int, default=3, + help="Max memory items per induction") + args = parser.parse_args() + run_experiment(args) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.sh b/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.sh new file mode 100644 index 00000000..0ebab8a9 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/reasoning_bank/run_experiment.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -e + +export OPENAI_API_BASE="" +export OPENAI_API_KEY="" + + +export CUDA_VISIBLE_DEVICES=4 + +cd "$(dirname "$0")" + +SEED=44 +NUM_TASKS=50 +MODEL="openai/gpt-5.4" +MAX_TOKENS="default" +REASONING_EFFORT="default" +MODE="interleaved" # isolated | sequential | interleaved + +# ReasoningBank specific hyperparameters +TOP_K_MEMORIES=1 +MAX_MEMORY_ITEMS=3 +EMBEDDING_MODEL="all-MiniLM-L6-v2" + +OUTPUT_BASE="./outputs" +MODEL_SHORT=$(echo $MODEL | sed 's|openai/||; s|azure/||; s|/|_|g') +RUN_TAG="rb_${MODE}_s${SEED}_${MODEL_SHORT}_${MAX_TOKENS}_${REASONING_EFFORT}" + +SETTINGS_ARGS="" +[ "$MAX_TOKENS" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --max-tokens $MAX_TOKENS" +[ "$REASONING_EFFORT" != "default" ] && SETTINGS_ARGS="$SETTINGS_ARGS --reasoning-effort $REASONING_EFFORT" +echo "Mode: ${MODE}" +echo "ModelSettings args: ${SETTINGS_ARGS:-default (no overrides)}" +echo "ReasoningBank: top_k=${TOP_K_MEMORIES} max_items=${MAX_MEMORY_ITEMS} embedding=${EMBEDDING_MODEL}" + +mkdir -p "$OUTPUT_BASE" + +ALL_BENCHMARKS="hle,bfcl,browsecompplus,appworld,swebench,tau2" +if [ "$MODE" = "isolated" ]; then + for BENCH in swebench tau2 browsecompplus appworld hle bfcl; do + echo "=== Running isolated ${BENCH} ===" + uv run python run_experiment.py \ + --mode isolated --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL \ + --embedding-model $EMBEDDING_MODEL \ + --top-k-memories $TOP_K_MEMORIES --max-memory-items $MAX_MEMORY_ITEMS \ + $SETTINGS_ARGS \ + --benchmarks $BENCH \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_${BENCH} \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_${BENCH}.log + done +else + echo "=== Running ${MODE} (all benchmarks) ===" + uv run python run_experiment.py \ + --mode $MODE --seed $SEED --num-tasks $NUM_TASKS \ + --model $MODEL \ + --embedding-model $EMBEDDING_MODEL \ + --top-k-memories $TOP_K_MEMORIES --max-memory-items $MAX_MEMORY_ITEMS \ + $SETTINGS_ARGS \ + --benchmarks $ALL_BENCHMARKS \ + --output-dir ${OUTPUT_BASE}/${RUN_TAG}_all \ + 2>&1 | tee ${OUTPUT_BASE}/${RUN_TAG}_all.log +fi + +echo "" +echo "=== All benchmarks complete ===" +echo "Run tag: ${RUN_TAG}" +echo "Outputs in: ${OUTPUT_BASE}/${RUN_TAG}_*" diff --git a/labs/AgentStream/exgentic/scripts/release.sh b/labs/AgentStream/exgentic/scripts/release.sh new file mode 100644 index 00000000..67c3b826 --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/release.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/release.sh [--push] + +Creates an annotated release tag on the current main HEAD. +The Git tag is the single source of truth for package versioning. + +Examples: + scripts/release.sh 0.1.1 + scripts/release.sh 0.1.1 --push +EOF +} + +if [[ $# -lt 1 || $# -gt 2 ]]; then + usage + exit 1 +fi + +version="$1" +push_changes="${2:-}" + +if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){2}([A-Za-z0-9._-]+)?$ ]]; then + echo "Version must look like 1.2.3 or 1.2.3rc1" >&2 + exit 1 +fi + +if [[ -n "$push_changes" && "$push_changes" != "--push" ]]; then + usage + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +current_branch="$(git branch --show-current)" +if [[ "$current_branch" != "main" ]]; then + echo "Release tags must be created from main. Current branch: $current_branch" >&2 + exit 1 +fi + +if [[ -n "$(git status --short)" ]]; then + echo "Working tree is dirty. Commit or stash changes before releasing." >&2 + exit 1 +fi + +git fetch origin main --tags +local_head="$(git rev-parse HEAD)" +remote_head="$(git rev-parse origin/main)" +if [[ "$local_head" != "$remote_head" ]]; then + echo "Local main is not at origin/main. Pull or push before releasing." >&2 + exit 1 +fi + +tag="v$version" +if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists." >&2 + exit 1 +fi + +git tag -a "$tag" -m "Release $tag" + +if [[ "$push_changes" == "--push" ]]; then + git push origin "$tag" +fi + +echo "Created release tag $tag at $local_head" +if [[ "$push_changes" != "--push" ]]; then + echo "Push when ready with:" + echo " git push origin $tag" +fi diff --git a/labs/AgentStream/exgentic/scripts/utils/task_ordering.py b/labs/AgentStream/exgentic/scripts/utils/task_ordering.py new file mode 100644 index 00000000..6d944a9c --- /dev/null +++ b/labs/AgentStream/exgentic/scripts/utils/task_ordering.py @@ -0,0 +1,210 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +""" +Seed-controlled task selection and ordering for evaluation experiments. + +Guarantees: + - Same seed → same task set for every benchmark, regardless of mode. + - Within-benchmark task order is identical across isolated / sequential / interleaved. + - Interleaved only interleaves *between* benchmarks; within-benchmark order is preserved. +""" + +from __future__ import annotations + +import hashlib +import random +import sys +from collections import deque +from pathlib import Path +from typing import Any + +# Allow importing exgentic from the repo source tree +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from exgentic.interfaces.registry import load_benchmark + + +# ────────────────────────────────────────────────────────────── +# Public API +# ────────────────────────────────────────────────────────────── + +def get_unified_task_order( + benchmark_configs: dict[str, dict[str, Any]], + num_tasks_per_benchmark: int, + seed: int, + mode: str, +) -> list[tuple[str, str]]: + """Return a deterministic, mode-aware task ordering. + + Parameters + ---------- + benchmark_configs : dict + ``{slug: {"bm_kwargs": {...}, "agent_kwargs": {...}}}`` + num_tasks_per_benchmark : int + How many tasks to select from each benchmark (e.g. 50). + seed : int + Ordering seed. Controls within-benchmark task order and + interleaved interleaving. Task *selection* is always fixed at + seed=42 so all experiments use the same task set. + mode : str + ``"isolated"`` | ``"sequential"`` | ``"interleaved"``. + + Returns + ------- + list of (benchmark_slug, task_id) + Ordered task sequence. For isolated/sequential the tasks are grouped + by benchmark (sorted alphabetically by slug). For interleaved the + tasks are interleaved across benchmarks while preserving + within-benchmark order. + """ + # Always use seed=42 for task SELECTION (which tasks to pick), + # use the provided seed only for ORDERING (task sequence). + _SELECTION_SEED = 42 + per_bm_tasks = _select_tasks(benchmark_configs, num_tasks_per_benchmark, _SELECTION_SEED) + + # Re-shuffle within-benchmark order using the provided seed + if seed != _SELECTION_SEED: + for slug in per_bm_tasks: + order_seed = _derive_seed(seed, slug) + rng = random.Random(order_seed) + rng.shuffle(per_bm_tasks[slug]) + + if mode in ("isolated", "sequential"): + result: list[tuple[str, str]] = [] + for slug in sorted(per_bm_tasks): + for tid in per_bm_tasks[slug]: + result.append((slug, tid)) + return result + + if mode == "interleaved": + return _interleave_preserving_order(per_bm_tasks, seed) + + raise ValueError(f"Unknown mode: {mode!r}") + + +def select_tasks_only( + benchmark_configs: dict[str, dict[str, Any]], + num_tasks_per_benchmark: int, + seed: int, +) -> dict[str, list[str]]: + """Return the selected tasks per benchmark (no ordering applied).""" + return _select_tasks(benchmark_configs, num_tasks_per_benchmark, seed) + + +# ────────────────────────────────────────────────────────────── +# Internals +# ────────────────────────────────────────────────────────────── + +def _select_tasks( + benchmark_configs: dict[str, dict[str, Any]], + num_tasks: int, + seed: int, +) -> dict[str, list[str]]: + """Select *num_tasks* tasks per benchmark using per-benchmark derived seeds.""" + per_bm: dict[str, list[str]] = {} + for slug in sorted(benchmark_configs): + bm_kwargs = benchmark_configs[slug].get("bm_kwargs", {}) + bm = load_benchmark(slug)(**bm_kwargs) + evaluator = bm.get_evaluator() + try: + all_ids = [str(t) for t in evaluator.list_tasks()] + finally: + try: + evaluator.close() + except Exception: + pass + bm.close() + + bm_seed = _derive_seed(seed, slug) + rng = random.Random(bm_seed) + rng.shuffle(all_ids) + per_bm[slug] = all_ids[:num_tasks] + return per_bm + + +def _interleave_preserving_order( + per_bm_tasks: dict[str, list[str]], + seed: int, +) -> list[tuple[str, str]]: + """Interleave tasks across benchmarks, preserving within-benchmark order. + + At each step, randomly pick a non-empty benchmark queue and pop + its next task. This ensures the relative order within each + benchmark is the same as in isolated/sequential mode. + """ + queues = {slug: deque(tasks) for slug, tasks in per_bm_tasks.items()} + rng = random.Random(seed) + result: list[tuple[str, str]] = [] + while any(queues.values()): + available = sorted(s for s, q in queues.items() if q) + slug = rng.choice(available) + result.append((slug, queues[slug].popleft())) + return result + + +def _derive_seed(master_seed: int, slug: str) -> int: + """Derive a deterministic per-benchmark seed from master seed + slug.""" + h = hashlib.md5(f"{master_seed}_{slug}".encode()).hexdigest() + return int(h, 16) % (2**31) + + +# ────────────────────────────────────────────────────────────── +# Helpers +# ────────────────────────────────────────────────────────────── + +def group_by_benchmark( + task_order: list[tuple[str, str]], +) -> list[tuple[str, list[str]]]: + """Group a task order list into (slug, [task_ids]) preserving order.""" + groups: list[tuple[str, list[str]]] = [] + current_slug: str | None = None + current_ids: list[str] = [] + for slug, tid in task_order: + if slug != current_slug: + if current_slug is not None: + groups.append((current_slug, current_ids)) + current_slug = slug + current_ids = [tid] + else: + current_ids.append(tid) + if current_slug is not None: + groups.append((current_slug, current_ids)) + return groups + + +# ────────────────────────────────────────────────────────────── +# Self-test +# ────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + # Quick sanity check without requiring benchmark data + print("=== task_ordering.py self-test ===\n") + + # Simulate with fake data + fake_per_bm = { + "bfcl": ["b1", "b2", "b3", "b4", "b5"], + "tau2": ["t1", "t2", "t3", "t4", "t5"], + "browsecompplus": ["c1", "c2", "c3", "c4", "c5"], + } + + # Test interleave preserving order + interleaved = _interleave_preserving_order(fake_per_bm, seed=42) + print("Interleaved interleave (seed=42):") + for slug, tid in interleaved: + print(f" {slug}: {tid}") + + # Verify within-benchmark order is preserved + for slug in fake_per_bm: + original = fake_per_bm[slug] + fused = [tid for s, tid in interleaved if s == slug] + assert fused == original, f"{slug}: order changed! {original} → {fused}" + print(f" ✓ {slug} order preserved: {fused}") + + # Verify different seeds produce different interleaving + interleaved2 = _interleave_preserving_order(fake_per_bm, seed=123) + order1 = [(s, t) for s, t in interleaved] + order2 = [(s, t) for s, t in interleaved2] + print(f"\n seed=42 vs seed=123 differ: {order1 != order2}") + + print("\nAll checks passed.") diff --git a/labs/AgentStream/exgentic/src/exgentic/__init__.py b/labs/AgentStream/exgentic/src/exgentic/__init__.py new file mode 100644 index 00000000..285aa3dd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/__init__.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from importlib import import_module +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version +from typing import Any + +try: + from ._version import version as __version__ +except ImportError: + try: + __version__ = package_version("exgentic") + except PackageNotFoundError: + __version__ = "0+unknown" + +from .environment.manager import EnvironmentManager, EnvType +from .interfaces.registry import get_agent_entries, get_benchmark_entries + +_API_EXPORTS = { + "aggregate", + "evaluate", + "execute", + "list_agents", + "list_benchmarks", + "list_subsets", + "list_tasks", + "preview", + "results", + "status", +} + +__all__ = [ + "__version__", + "EnvironmentManager", + "EnvType", + "aggregate", + "evaluate", + "execute", + "list_agents", + "list_benchmarks", + "list_subsets", + "list_tasks", + "preview", + "results", + "status", +] + + +def _find_component_export(name: str): + matches = [ + entry + for entries in (get_benchmark_entries(), get_agent_entries()) + for entry in entries.values() + if entry.attr == name + ] + if not matches: + return None + if len(matches) > 1: + slugs = ", ".join(sorted(entry.slug_name for entry in matches)) + raise AttributeError(f"Ambiguous exgentic export '{name}' found in registry entries: {slugs}.") + return matches[0] + + +def __getattr__(name: str) -> Any: + if name in _API_EXPORTS: + value = getattr(import_module(".interfaces.lib.api", __name__), name) + globals()[name] = value + return value + + entry = _find_component_export(name) + if entry is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + value = entry.load() + globals()[name] = value + return value + + +def __dir__() -> list[str]: + # Keep dir() side-effect free. + # Some introspection libraries (e.g. freezegun) iterate over dir(module) + # and then call getattr() for each name. Exposing lazy registry exports here + # can trigger expensive imports during unrelated initialization paths. + return sorted(set(globals()) | _API_EXPORTS) diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/actions/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/actions/chat.py b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/chat.py new file mode 100644 index 00000000..0e1f5d86 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/chat.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Chat/tool-call helpers for translating between Exgentic actions and chat payloads.""" +from __future__ import annotations + +from typing import Any, Optional + +from pydantic import BaseModel + +from ...core.actions import extract_argument +from ...core.types import ( + MessageObservation, + MessagePayload, + MultiObservation, + SingleAction, + SingleObservation, +) + + +class ChatActionContext: + """Helper to map actions to chat content/tool calls and back to observations.""" + + def __init__(self) -> None: + self.message_actions: dict[str, SingleAction] = {} + self.tool_actions: dict[str, SingleAction] = {} + + @staticmethod + def action_to_tool_call_payload(action: SingleAction) -> dict[str, Any]: + arguments: Any = action.arguments + if isinstance(arguments, str): + arguments = {"error_parsing": arguments} + elif isinstance(arguments, BaseModel): + arguments = arguments.model_dump() + return {"name": action.name, "arguments": arguments, "id": action.id} + + def actions_to_chat_components(self, actions: list[SingleAction]) -> tuple[Optional[str], list[dict[str, Any]]]: + content: Optional[str] = None + tool_calls: list[dict[str, Any]] = [] + self.message_actions = {} + self.tool_actions = {} + + for act in actions: + if act.name == "message": + self.message_actions[act.id] = act + msg_val = extract_argument(act.arguments, "content", None) + if msg_val is None: + try: + msg_val = str(act.arguments) + except Exception: + msg_val = None + if msg_val is not None: + if content is None: + content = "" + content += str(msg_val) + continue + + self.tool_actions[act.id] = act + tool_calls.append(self.action_to_tool_call_payload(act)) + + return content, tool_calls + + def actions_to_assistant_message(self, actions: list[SingleAction]) -> dict[str, Any]: + """Convert actions into an assistant message dict with content and tool_calls.""" + content, tool_calls = self.actions_to_chat_components(actions) + message: dict[str, Any] = {"role": "assistant"} + if content is not None: + message["content"] = content + if tool_calls: + message["tool_calls"] = tool_calls + return message + + def message_to_observation(self, message: Any) -> SingleObservation | MultiObservation: + # Support a list of messages (e.g., multiple tool responses) + if isinstance(message, list): + items = [self.message_to_observation(m) for m in message] + flat: list[SingleObservation] = [] + for obs in items: + if isinstance(obs, MultiObservation): + flat.extend(obs.observations) + else: + flat.append(obs) + return MultiObservation(observations=flat) + + if isinstance(message, dict): + role = message.get("role") + if role == "user": + acts = list(self.message_actions.values()) + content = message.get("content") or "" + payload = MessagePayload(sender="user", message=content) + return MessageObservation(invoking_actions=acts, result=payload) + if role == "tool": + act = self.tool_actions.get(str(message.get("tool_call_id"))) + return SingleObservation( + invoking_actions=([act] if act else []), + result=message.get("content"), + ) + if "id" in message and "content" in message: + act = self.tool_actions.get(str(message.get("id"))) + return SingleObservation( + invoking_actions=([act] if act else []), + result=message.get("content"), + ) + + return SingleObservation(invoking_actions=[], result=str(message)) diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/actions/functions.py b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/functions.py new file mode 100644 index 00000000..f730d0bb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/actions/functions.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import inspect +from typing import Any, Callable + +from pydantic import BaseModel +from pydantic_core import PydanticUndefined + +from ...core.actions import build_action +from ...core.types import ActionType, MultiObservation, SingleAction, SingleObservation + + +def action_type_to_function( + action_type: "ActionType", internal_function: Callable[["SingleAction"], Any] +) -> Callable[..., Any]: + def function(*args, **kwargs: Any) -> Any: + all_kwargs = bind_arguments(cls=action_type.arguments, args=args, kwargs=kwargs) + action = build_action(action_type, all_kwargs) + observation = internal_function(action) + if observation is None: + return None + if isinstance(observation, SingleObservation): + return observation.result + if isinstance(observation, MultiObservation): + return [obs.result for obs in observation.observations] + raise TypeError(f"Unexpected observation type: {type(observation).__name__}") + + function.__name__ = action_type.name.replace(".", "__") + + docstring_parts = [action_type.description or action_type.name.replace("_", " ")] + + arguments_type = action_type.arguments + if not isinstance(arguments_type, type) or not issubclass(arguments_type, BaseModel): + raise TypeError(f"Action arguments must be a Pydantic BaseModel, got {arguments_type!r}") + + params = [] + annotations: dict[str, Any] = {} + + fields = arguments_type.model_fields + if fields: + docstring_parts.extend(["", "Args:"]) + for field_name, field_info in fields.items(): + anno = field_info.annotation or Any + + default = field_info.default + required = default is PydanticUndefined + if default is None: + required = False + + param = inspect.Parameter( + name=field_name, + kind=inspect.Parameter.KEYWORD_ONLY, + annotation=anno, + default=(inspect._empty if required else default), + ) + params.append(param) + annotations[field_name] = anno + + # Description and Google-style formatting + desc = field_info.description or field_name.replace("_", " ") + type_name = getattr(anno, "__name__", None) or str(anno).replace("typing.", "") + docstring_parts.append(f" {field_name} ({type_name}): {desc}") + + function.__signature__ = inspect.Signature(parameters=params) + function.__annotations__ = annotations + else: + function.__signature__ = inspect.Signature(parameters=[]) + function.__annotations__ = {} + + function.__annotations__["return"] = Any + function.__doc__ = "\n".join(docstring_parts) + + return function + + +def bind_arguments(cls: type[BaseModel], args: list[Any], kwargs: dict[str, Any]) -> dict[str, Any]: + """Bind positional args to Pydantic model fields by declaration order.""" + field_names = list(cls.model_fields.keys()) + if len(args) > len(field_names): + raise TypeError( + f"Too many positional arguments for {cls.__name__} " + f"(expected at most {len(field_names)}, got {len(args)})" + ) + + positional_names = field_names[: len(args)] + _duplicates = set(positional_names) & set(kwargs.keys()) + if _duplicates: + dup_list = ", ".join(sorted(_duplicates)) + raise TypeError(f"Multiple values for argument(s): {dup_list}") + + bound = dict(zip(positional_names, args)) + bound.update(kwargs) + return bound diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/agents/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/agents/code_agent.py b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/code_agent.py new file mode 100644 index 00000000..bf638121 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/code_agent.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from abc import abstractmethod +from typing import Callable, List, Optional + +from ...core import Observation +from ..actions.functions import action_type_to_function +from .coordinator import AgentCoordinator, CoordinatedAgent + + +class CodeAgentInstance(CoordinatedAgent, AgentCoordinator): + """Base class for code-based agents that inherits both roles.""" + + def __init__(self, session_id: str): + self.initial_observation: Optional[Observation] = None + # Initialize AgentCoordinator with self as the internal agent + AgentCoordinator.__init__(self, session_id, self) + + def run(self, adapter) -> None: + """Implementation of CoordinatedAgent.run that converts actions to functions. + + When the code agent calls one of the functions, what actually happens, is that the + AgentCoordinator.execute() method is called with the action. This places the action in an + action queue, creates a future for the result, and waits for it. + + The AgentCordinator, which is running in a different thread, waits for an action in the + queue, fetches it and passes it to the benchmark environment. When the AgentCordinator receives + the coressponding observation, it places it in the result future. + + This unblocks the CoordinateAgent, and cause the function to return the value. + The code agent then continues its run. + + """ + functions = [] + + for action_type in self.actions: + function = action_type_to_function(action_type, self.execute) + functions.append(function) + + # Block until the environment delivers the initial observation via adapter.react() + self.initial_observation = adapter.get_observation() + + try: + self.run_code_agent(functions) + finally: + self.execute(None) # Mark execution as done, by returning no action to the benchmark. + + @abstractmethod + def run_code_agent(self, functions: List[Callable]) -> None: + """Subclasses implement their code agent logic here.""" + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/agents/coordinator.py b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/coordinator.py new file mode 100644 index 00000000..ebd58c9d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/coordinator.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import contextvars +import threading +import time +import traceback +from abc import ABC, abstractmethod +from typing import List, Optional + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + MultiObservation, + Observation, + ParallelAction, + SingleAction, +) + + +class CoordinatedAgent(ABC): + """Internal agent that runs inside an AgentCoordinator. + + The agent: + - receives observations via get_observation() + - sends actions via execute() + - signals termination by execute(None) + """ + + @abstractmethod + def run(self, adapter) -> None: + pass + + +class AgentCoordinator(AgentInstance): + """Coordinates turn-based communication between threads. + + - an environment thread (react) + - an internal agent thread (run / execute) + """ + + def __init__( + self, + session_id, + internal_agent: CoordinatedAgent, + accumulate_window_seconds: float | None = None, + ): + super().__init__(session_id) + self.internal_agent = internal_agent + self._accumulate_window_seconds = accumulate_window_seconds + + self._condition = threading.Condition() + self._thread: Optional[threading.Thread] = None + self._started = False + self._closed = False + self._agent_error: Optional[BaseException] = None + self._agent_traceback: str | None = None + + self._turn = 0 + self._current_observation: Observation | None = None + self._agent_seen_turn = -1 + + self._pending_actions: List[Action | None] = [] + self._last_actions: List[SingleAction] = [] + + def start(self, task, context, actions) -> None: + """Receive work payload and start the internal agent thread (once).""" + super().start(task, context, actions) + with self._condition: + if self._started: + raise RuntimeError("AgentCoordinator already started") + self._started = True + ctx = contextvars.copy_context() + self._thread = threading.Thread( + target=ctx.run, + args=(self._run_internal_agent,), + name=f"AgentCoordinator[{self.session_id}]", + daemon=False, + ) + self._thread.start() + + def _run_internal_agent(self) -> None: + """Entry point for the internal agent thread.""" + try: + self.internal_agent.run(self) + except BaseException as exc: + with self._condition: + self._agent_error = exc + self._agent_traceback = traceback.format_exc() + self._closed = True + self._turn += 1 + self._current_observation = None + self._pending_actions.clear() + self._condition.notify_all() + self.logger.exception("Internal agent crashed") + finally: + self.close() + + def _raise_if_agent_failed(self) -> None: + if self._agent_error is not None: + if isinstance(self._agent_error, Exception): + tb = self._agent_traceback or "" + raise RuntimeError(f"{self._agent_error}\n\n{tb}") from self._agent_error + raise RuntimeError("Internal agent failed") from self._agent_error + + def _flush_actions(self) -> Action | None: + """Combine pending actions into a single Action or ParallelAction.""" + if not self._pending_actions: + return None + actions = self._pending_actions + self._pending_actions = [] + + if any(a is None for a in actions): + self._closed = True + return None + + return actions[0] if len(actions) == 1 else ParallelAction(actions=actions) + + def _remember_actions(self, action: Action | None) -> None: + if isinstance(action, Action): + actions = list(action.to_action_list()) + if all(isinstance(act, SingleAction) for act in actions): + self._last_actions = actions + return + else: + self._last_actions = [] + return + self._last_actions = [] + + def _rewire_observation(self, observation: Observation | None) -> Observation | None: + if observation is None or not self._last_actions: + return observation + if not isinstance(observation, Observation): + return observation + + obs_list = observation.to_observation_list() + if not obs_list: + return observation + + if len(obs_list) == 1 and len(self._last_actions) > 1: + obs = obs_list[0] + if not obs.invoking_actions: + obs.invoking_actions = list(self._last_actions) + return observation + + used_ids = {act.id for obs in obs_list for act in obs.invoking_actions if isinstance(act, SingleAction)} + remaining = [act for act in self._last_actions if act.id not in used_ids] + for obs in obs_list: + if obs.invoking_actions: + continue + if not remaining: + break + obs.invoking_actions = [remaining.pop(0)] + + if remaining: + self.logger.warning( + "Unassigned actions after rewiring observations (actions=%s, observations=%s)", + len(self._last_actions), + len(obs_list), + ) + return observation + + def _select_observation_for_action( + self, action: Action | None, observation: Observation | None + ) -> Observation | None: + if observation is None: + return observation + if not isinstance(observation, Observation): + return observation + if not isinstance(action, SingleAction): + return observation + + obs_list = observation.to_observation_list() + if not obs_list: + return observation + + matched = [ + obs + for obs in obs_list + if any(isinstance(inv, SingleAction) and inv.id == action.id for inv in obs.invoking_actions) + ] + if matched: + if len(matched) == 1: + return matched[0] + return MultiObservation(observations=matched) + + self.logger.warning( + "No matching observation for action id=%s (observations=%s)", + action.id, + len(obs_list), + ) + return observation + + def _publish_observation(self, observation: Observation | None) -> None: + self.logger.info("Publishing observation (turn=%s): %s", self._turn + 1, observation) + observation = self._rewire_observation(observation) + self._last_actions = [] + self._current_observation = observation + self._turn += 1 + self._condition.notify_all() + + def _wait_for_pending_actions(self) -> bool: + while not self._closed and not self._pending_actions: + self._condition.wait() + self._raise_if_agent_failed() + return not self._closed + + def _accumulate_pending_actions(self) -> None: + if not self._accumulate_window_seconds: + return + if any(a is None for a in self._pending_actions): + return + deadline = time.monotonic() + self._accumulate_window_seconds + while not self._closed: + remaining = deadline - time.monotonic() + if remaining <= 0: + return + if any(a is None for a in self._pending_actions): + return + self._condition.wait(timeout=remaining) + + def get_observation(self) -> Observation | None: + """Block until a new observation is available or the coordinator closes.""" + with self._condition: + self._raise_if_agent_failed() + if self._closed: + return None + + # Wait until a new observation is published by the environment thread. + # We also block if _current_observation is still None (initial call). + while not self._closed and (self._turn <= self._agent_seen_turn or self._current_observation is None): + self._condition.wait() + + self._raise_if_agent_failed() + if self._closed: + return None + + self._agent_seen_turn = self._turn + self.logger.info( + "Delivered observation (turn=%s): %s", + self._turn, + self._current_observation, + ) + return self._current_observation + + def execute(self, action: Action | None) -> Observation | None: + """Publish an action for the current turn. + + If action is None, signals agent termination. + """ + with self._condition: + self._raise_if_agent_failed() + if self._closed: + if action is not None: + raise RuntimeError("execute() called after close()") + return None + + self.logger.info("Received action (turn=%s): %s", self._turn, action) + self._pending_actions.append(action) + self._condition.notify_all() + + if action is None: + self._closed = True + return None + + my_turn = self._turn + while not self._closed and self._turn == my_turn: + self._condition.wait() + + self._raise_if_agent_failed() + if self._closed: + return None + + self._agent_seen_turn = self._turn + self.logger.info( + "Delivered observation after action (turn=%s): %s", + self._turn, + self._current_observation, + ) + return self._select_observation_for_action(action, self._current_observation) + + def react(self, observation: Observation | None) -> Action | None: + """Publish an observation and wait for the agent's action. + + If observation is None, signals environment termination. + """ + with self._condition: + self._raise_if_agent_failed() + if self._closed: + if observation is not None: + raise RuntimeError("react() called after close()") + return None + + self._publish_observation(observation) + + if observation is None: + self._closed = True + return None + + if not self._wait_for_pending_actions(): + return None + + self._accumulate_pending_actions() + + action = self._flush_actions() + self._remember_actions(action) + return action + + def close(self) -> None: + """Close the coordinator and wait for the agent thread to exit.""" + with self._condition: + if not self._closed: + self._closed = True + self._turn += 1 + self._current_observation = None + self._pending_actions.clear() + self._condition.notify_all() + t = self._thread + + if t and t.is_alive() and t is not threading.current_thread(): + t.join() diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_agent.py b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_agent.py new file mode 100644 index 00000000..424b20f7 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_agent.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import abc +from abc import abstractmethod +from typing import Any, Callable, List, Optional + +from ..actions.functions import action_type_to_function +from .code_agent import CodeAgentInstance +from .mcp_server import MCPServer + + +class MCPAgentInstance(CodeAgentInstance, abc.ABC): + """Sync-first base class. + + - run_code_agent(): sync + - run_mcp_agent(): ABSTRACT SYNC (subclass decides implementation strategy) + """ + + def __init__(self, session_id: str): + self._mcp_server: Optional[MCPServer] = None + super().__init__(session_id) + + def run(self, adapter) -> None: + functions: List[Callable[..., Any]] = [] + for action_type in self.actions: + if action_type.is_finish: + function = action_type_to_function(action_type, self._submit_finish_action) + else: + function = action_type_to_function(action_type, self.execute) + functions.append(function) + + self.initial_observation = adapter.get_observation() + + try: + self.run_code_agent(functions) + finally: + self.execute(None) + + def run_code_agent(self, functions: List[Callable[..., Any]]) -> Any: + """Fully synchronous entrypoint.""" + self.logger.info("Starting MCP server for agent tools") + server = MCPServer( + tools=functions, + log_dir=self.paths.agent_dir, + logger=self.logger, + stringify_empty_output=self._stringify_empty_output(), + ) + self._mcp_server = server + self.mcp = server.mcp + started = False + try: + with server: + started = True + self.logger.info( + "MCP server ready at http://%s:%s/mcp", + server.connect_host, + server.port, + ) + return self.run_mcp_agent(server.connect_host, server.port) + finally: + if started: + self.logger.info("MCP server stopped") + + def _stringify_empty_output(self) -> bool: + return False + + def _submit_finish_action(self, action) -> None: + with self._condition: + self._raise_if_agent_failed() + if self._closed: + if action is not None: + raise RuntimeError("execute() called after close()") + return + + self.logger.info("Received finish action (turn=%s): %s", self._turn, action) + self._pending_actions.append(action) + self._condition.notify_all() + return + + def close_mcp_agent(self) -> None: + self.close() + + def close(self) -> None: + super().close() + if self._mcp_server is not None: + self.logger.info("Stopping MCP server") + self._mcp_server.stop(raise_on_timeout=False) + + @abstractmethod + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> Any: + """ABSTRACT SYNC. + + Subclass may implement: + - purely sync logic, OR + - a sync wrapper over an async core (via run_sync, etc.) + """ + ... + + +class MCPAgent(MCPAgentInstance, abc.ABC): + """Backwards-compatible alias for MCPAgentInstance.""" + + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_server.py b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_server.py new file mode 100644 index 00000000..1a97ebb6 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/agents/mcp_server.py @@ -0,0 +1,351 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import functools +import inspect +import logging +import socket +import threading +import time +from typing import Any, Callable, Iterable, Optional + +import uvicorn +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client +from mcp.server.fastmcp.server import FastMCP +from pydantic import BaseModel + +from ...observers.logging import ( + configure_library_file_logging, + configure_uvicorn_file_logging, + get_logger, +) +from ...utils.sync import run_sync + + +class MCPServerConfig(BaseModel): + http_timeout_seconds: float | None = None # None means no timeout + sse_read_timeout_seconds: float | None = None # None means no timeout + http_connect_timeout_seconds: float | None = None # None means no timeout + headers: dict[str, str] | None = None + terminate_on_close: bool = True + + +class MCPServer: + _MAX_SAFE_SCHEMA_INT = 2_147_483_647 + + def __init__( + self, + mcp: FastMCP | None = None, + *, + host: str | None = None, + port: int | None = None, + tools: Iterable[Callable[..., Any]] | None = None, + log_dir, + logger: logging.Logger, + stringify_empty_output: bool = False, + ) -> None: + self._mcp = mcp or self._build_fastmcp() + self._host = host or "0.0.0.0" + self._port = port + self._log_dir = log_dir + self._logger = logger + self._stringify_empty_output = stringify_empty_output + self._mcp_log_dir = self._log_dir / "mcp" + self._mcp_log_dir.mkdir(parents=True, exist_ok=True) + self._server_logger = get_logger( + f"MCPServer_{id(self)}", + str(self._mcp_log_dir / "server.log"), + ) + ts = self._mcp.settings.transport_security + ts.allowed_hosts = [ + *ts.allowed_hosts, + "host.containers.internal:*", + "host.docker.internal:*", + ] + ts.allowed_origins = [ + *ts.allowed_origins, + "http://host.containers.internal:*", + "http://host.docker.internal:*", + ] + + tool_names: list[str] = [] + if tools: + for fn in tools: + tool_fn = self._wrap_tool(fn) if self._stringify_empty_output else fn + tool = self._mcp._tool_manager.add_tool(tool_fn) + tool.parameters = self._clamp_schema_ints(tool.parameters) + tool_names.append(fn.__name__) + self._log_tool_summary(tool_names) + + if self._logger is not None: + self._logger.info("MCP logs at %s", self._mcp_log_dir) + + self._server: Optional[uvicorn.Server] = None + self._thread: Optional[threading.Thread] = None + self._sock: Optional[socket.socket] = None + self._started = threading.Event() + + @classmethod + def _clamp_schema_ints(cls, obj: Any) -> Any: + if isinstance(obj, dict): + return {k: cls._clamp_schema_ints(v) for k, v in obj.items()} + if isinstance(obj, list): + return [cls._clamp_schema_ints(v) for v in obj] + if isinstance(obj, int) and not isinstance(obj, bool): + if obj > cls._MAX_SAFE_SCHEMA_INT: + return cls._MAX_SAFE_SCHEMA_INT + if isinstance(obj, float): + if obj > cls._MAX_SAFE_SCHEMA_INT: + return float(cls._MAX_SAFE_SCHEMA_INT) + return obj + + @staticmethod + def stringify_empty_output(result: Any) -> Any: + if result is None: + return "null" + if result == []: + return "[]" + return result + + @classmethod + def _wrap_tool(cls, fn: Callable[..., Any]) -> Callable[..., Any]: + signature = inspect.signature(fn) + + if inspect.iscoroutinefunction(fn): + + @functools.wraps(fn) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + result = await fn(*args, **kwargs) + return cls.stringify_empty_output(result) + + else: + + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + result = fn(*args, **kwargs) + return cls.stringify_empty_output(result) + + wrapper.__signature__ = signature # type: ignore[attr-defined] + return wrapper + + def _build_fastmcp(self) -> FastMCP: + root = logging.getLogger() + null_handler: logging.Handler | None = None + if not root.handlers: + null_handler = logging.NullHandler() + root.addHandler(null_handler) + try: + return FastMCP() + finally: + if null_handler is not None: + try: + root.removeHandler(null_handler) + except Exception: + pass + + @property + def server(self) -> Optional[uvicorn.Server]: + return self._server + + @property + def thread(self) -> Optional[threading.Thread]: + return self._thread + + @property + def mcp(self) -> FastMCP: + return self._mcp + + @property + def host(self) -> str: + return self._host + + @property + def connect_host(self) -> str: + # Use 127.0.0.1 for connecting, even if server binds to 0.0.0.0. + return "127.0.0.1" if self._host == "0.0.0.0" else self._host + + @property + def port(self) -> int: + if self._port is None: + raise RuntimeError("MCP server port not assigned yet.") + return self._port + + def start( + self, + timeout: float = 60.0, + *, + tcp_timeout: float = 60.0, + ping_timeout: float = 60.0, + ) -> None: + if self._thread and self._thread.is_alive(): + return + + self._ensure_socket() + + self._started.clear() + t = threading.Thread( + target=self._thread_entry, + name=f"mcp-native:{self._port}", + daemon=True, + ) + self._thread = t + t.start() + + if not self._started.wait(timeout=timeout): + raise RuntimeError("MCP uvicorn thread did not signal startup within timeout.") + + try: + started_at = time.time() + # Use 127.0.0.1 for connecting, even if server binds to 0.0.0.0 + connect_host = "127.0.0.1" if self._host == "0.0.0.0" else self._host + wait_for_tcp(connect_host, self.port, timeout=tcp_timeout) + tcp_elapsed = time.time() - started_at + ping_started = time.time() + run_sync( + wait_for_mcp_ping_async(connect_host, self.port, timeout=ping_timeout), + timeout=ping_timeout + 5.0, + ) + ping_elapsed = time.time() - ping_started + self._server_logger.info( + "MCP readiness OK (tcp=%.2fs, ping=%.2fs)", + tcp_elapsed, + ping_elapsed, + ) + except BaseException as exc: + self.stop(error=exc, raise_on_timeout=False) + raise + + def __enter__(self) -> "MCPServer": + self.start() + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self.stop(error=exc, raise_on_timeout=True) + + def stop( + self, + timeout: float = 60.0, + *, + error: BaseException | None = None, + raise_on_timeout: bool = True, + ) -> None: + if self._server is not None: + self._server.should_exit = True + + t = self._thread + if t and t.is_alive(): + t.join(timeout=timeout) + if t.is_alive(): + message = "MCP uvicorn server thread did not exit cleanly" + if raise_on_timeout and error is None: + raise RuntimeError(message) + self._server_logger.warning(message) + if self._sock is not None: + try: + self._sock.close() + finally: + self._sock = None + + def _thread_entry(self) -> None: + thread_id = threading.get_ident() + cleanup_uvicorn = configure_uvicorn_file_logging( + self._mcp_log_dir / "uvicorn.log", + thread_id=thread_id, + ) + cleanup_mcp = configure_library_file_logging( + self._mcp_log_dir / "server.log", + logger_names=["mcp", "mcp.server", "mcp.client"], + thread_id=thread_id, + ) + + server = self._build_server() + + self._server_logger.info( + "Starting MCP server on %s:%s (path=%s)", + self._host, + self.port, + self._mcp.settings.streamable_http_path, + ) + self._started.set() + + try: + sock = self._sock + if sock is not None: + self._server_logger.info("Using pre-bound socket on %s:%s", self._host, self.port) + server.run(sockets=[sock]) + else: + server.run() + finally: + self._server_logger.info("MCP server stopped") + cleanup_mcp() + cleanup_uvicorn() + + def _ensure_socket(self) -> None: + if self._sock is not None: + return + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((self._host, self._port or 0)) + sock.listen(1) + except Exception: + sock.close() + raise + self._sock = sock + self._port = int(sock.getsockname()[1]) + + def _build_server(self) -> uvicorn.Server: + app = self._mcp.streamable_http_app() + config = uvicorn.Config(app, host=self._host, port=self.port, log_config=None) + server = uvicorn.Server(config) + self._server = server + return server + + def _log_tool_summary(self, tool_names: list[str]) -> None: + count = len(tool_names) + if count == 0: + self._server_logger.info("Registered 0 MCP tools") + return + preview = ", ".join(tool_names[:6]) + if count > 6: + preview = f"{preview}, +{count - 6} more" + self._server_logger.info("Registered %s MCP tool(s): %s", count, preview) + + +def wait_for_tcp(host: str, port: int, timeout: float = 60.0) -> None: + deadline = time.time() + timeout + last_err: Optional[BaseException] = None + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.5): + return + except Exception as e: + last_err = e + time.sleep(0.1) + raise TimeoutError(f"Server at {host}:{port} did not open TCP port within {timeout}s. " f"Last error: {last_err!r}") + + +async def wait_for_mcp_ping_async(host: str, port: int, timeout: float = 60.0) -> None: + deadline = time.time() + timeout + last_err: Optional[BaseException] = None + url = f"http://{host}:{port}/mcp" + + import asyncio # local import to keep module sync-first + + while time.time() < deadline: + try: + async with streamable_http_client(url) as ( + read_stream, + write_stream, + _, + ): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + await session.send_ping() + return + except Exception as e: + last_err = e + await asyncio.sleep(0.1) + + raise TimeoutError(f"MCP ping at {host}:{port} failed within {timeout}s. " f"Last error: {last_err!r}") diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/executors/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/executors/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/executors/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/executors/proxy.py b/labs/AgentStream/exgentic/src/exgentic/adapters/executors/proxy.py new file mode 100644 index 00000000..e21e291b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/executors/proxy.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from argparse import Action +from queue import Queue +from threading import Event, Lock, Semaphore +from typing import Any, Generic, Optional, TypeVar + +from ...core.session import Session +from ...core.types import Observation + +DONE = object() + + +class BaseProxySession(Session, ABC): + """Generic queue-based proxy session. + + Implements a simple rendezvous between an external driver (agent/framework) + and a foreign environment adapter via two blocking queues. + """ + + def __init__(self): + super().__init__() + self.completed = False + self.step_count = 0 + self._to_agent: Queue = Queue() + self._from_agent: Queue = Queue() + self._last_observation: Optional[Any] = None + + def step(self, action: Action) -> Optional[Observation]: + self.step_count += 1 + self._from_agent.put(action) + next_obs = self._to_agent.get() + return next_obs + + def start(self) -> Optional[Observation]: + result = self._to_agent.get() + return result + + def done(self) -> bool: + result = self.completed + return result + + def score(self) -> dict: + return { + "success": self.completed, + "steps": self.step_count, + "score": max(0.0, 1.0 - (self.step_count - 1) * 0.1), + } + + def close(self): + self.completed = True + try: + self._from_agent.put_nowait(DONE) + except Exception: + pass + try: + self._to_agent.put_nowait(DONE) + except Exception: + pass + + # --- Hooks for subclasses --- + def put_observation(self, obs: Any) -> None: + self._last_observation = obs + self._to_agent.put(obs) + + def wait_for_action(self) -> Optional[Any]: + item = self._from_agent.get() + if item is DONE: + self.completed = True + return None + return item + + +_PAIRING_SEMAPHORE: Semaphore = Semaphore(1) # Only 1 session can be staged at a time +_PAIRING_LOCK: Lock = Lock() +_CURRENT_SESSION: Optional[PairableProxySession] = None + + +class PairableProxySession(BaseProxySession): + """Proxy session that can be staged and paired with a proxy agent automatically.""" + + def __init__(self): + super().__init__() + self._paired_event: Event = Event() + + # Pairing API + def stage_for_pairing(self) -> None: + _PAIRING_SEMAPHORE.acquire() # Blocks until slot available + with _PAIRING_LOCK: + global _CURRENT_SESSION + _CURRENT_SESSION = self + + def _mark_paired(self) -> None: + self._paired_event.set() + + def waiting_for_pairing(self): + return _CURRENT_SESSION == self and not self._paired_event.is_set() + + def unstage_for_pairing(self): + if self.waiting_for_pairing(): + with _PAIRING_LOCK: + global _CURRENT_SESSION + assert _CURRENT_SESSION == self + _CURRENT_SESSION = None + _PAIRING_SEMAPHORE.release() + + def pair_to_agent(self, timeout: Optional[float] = 10.0) -> None: + ok = self._paired_event.wait(timeout=timeout) + if not ok: + raise RuntimeError("Timed out waiting for proxy agent to pair with session") + + # Gate helpers + @classmethod + def block_pairing(cls) -> None: + pass # No longer needed with lock-based approach + + @classmethod + def allow_pairing(cls) -> None: + pass # No longer needed with lock-based approach + + @classmethod + def pairing_allowed(cls) -> bool: + return True # Always allowed with lock-based approach + + +SessionT = TypeVar("SessionT", bound=BaseProxySession) + + +class BaseProxyAgent(ABC, Generic[SessionT]): + """Base mixin providing generic step handling between a proxy session and an external environment. + + Uses core terms (session, observation, action). + + Adapters should call `handle_observation(observation, state)` from their + environment-specific entrypoint. + """ + + def _ensure_session(self, state: Optional[SessionT], observation: Any) -> SessionT: + if state is None: + return self.create_session(observation) + self.update_session_observation(state, observation) + return state + + def handle_observation(self, observation: Any, state: Optional[SessionT]): + """Generic step handler: ensure session, wait for action, translate response. + + - observation: an environment-specific observation object + - state: the proxy session instance (or None for a new session) + Returns (environment-specific response, new_state). + """ + session = self._ensure_session(state, observation) + action = session.wait_for_action() + response_obj, new_state = self.action_to_response(action, observation, session) + return response_obj, new_state + + # --- Subclass hooks --- + @abstractmethod + def create_session(self, first_observation: Any) -> SessionT: + pass + + @abstractmethod + def update_session_observation(self, session: SessionT, observation: Any) -> None: + pass + + @abstractmethod + def action_to_response(self, action: Any, observation: Any, session: SessionT): + pass + + +class PairableProxyAgent(BaseProxyAgent[SessionT]): + """Proxy agent that adopts the currently staged PairableProxySession.""" + + def adopt_staged_session(self) -> SessionT: + with _PAIRING_LOCK: + global _CURRENT_SESSION + sess = _CURRENT_SESSION + if sess is None: + raise RuntimeError("No staged session available for pairing") + _CURRENT_SESSION = None + sess._mark_paired() # type: ignore[attr-defined] + _PAIRING_SEMAPHORE.release() # Release slot for next session + return sess # type: ignore[return-value] + + # Gate helpers + @classmethod + def block_pairing(cls) -> None: + PairableProxySession.block_pairing() + + @classmethod + def allow_pairing(cls) -> None: + PairableProxySession.allow_pairing() + + @classmethod + def pairing_allowed(cls) -> bool: + return PairableProxySession.pairing_allowed() diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/__init__.py new file mode 100644 index 00000000..84d0360b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/__init__.py @@ -0,0 +1,125 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Runner & Transport abstractions for running objects in different isolation levels. + +Runners wrap any object and control where it executes: + +- ``direct`` — same thread, no isolation +- ``thread`` — separate thread, queue-based communication +- ``process`` — separate process, pipe-based communication with cloudpickle +- ``service`` — HTTP service in a background thread +- ``docker`` — HTTP service inside a Docker container +- ``venv`` — HTTP service in an isolated uv virtual environment + +Usage:: + + calc = with_runner(Calculator, runner="thread", value=10) +""" + +from __future__ import annotations + +from typing import Any, Literal + +from .direct import DirectTransport +from .transport import ObjectHost, ObjectProxy, Transport + +RunnerName = Literal["direct", "thread", "process", "service", "docker", "venv"] + + +def _resolve_cls(cls: type | str) -> type: + """Resolve a ``"module:qualname"`` string to the actual class.""" + if isinstance(cls, type): + return cls + module_path, qualname = cls.rsplit(":", 1) + import importlib + + mod = importlib.import_module(module_path) + obj = mod + for attr in qualname.split("."): + obj = getattr(obj, attr) + return obj # type: ignore[return-value] + + +def with_runner(cls: type | str, *args: Any, runner: RunnerName = "direct", **kwargs: Any) -> Any: + """Create an instance of *cls* running in the specified isolation level. + + *cls* may be a class or a ``"module:qualname"`` string. String + references are resolved lazily — for ``venv`` and ``docker`` runners + the string is forwarded directly so heavy imports never happen on the + host. + + Returns an ``ObjectProxy`` that transparently forwards all + attribute access and method calls to the real object. + """ + if runner == "direct": + cls = _resolve_cls(cls) + return ObjectProxy(DirectTransport(cls(*args, **kwargs))) + + if runner == "thread": + from .thread import ThreadTransport + + cls = _resolve_cls(cls) + t = ThreadTransport(cls, *args, **kwargs) + t.start() + return ObjectProxy(t) + + if runner == "process": + from .process import PipeTransport + + cls = _resolve_cls(cls) + t = PipeTransport(cls, *args, **kwargs) + t.start() + return ObjectProxy(t) + + if runner == "service": + from .service import ServiceRunner + + cls = _resolve_cls(cls) + return ServiceRunner(cls, *args, **kwargs).start() + + if runner == "docker": + from .docker import DockerRunner + + docker_kw = {} + for key in ( + "env_name", + "module_path", + "image", + "dockerfile", + "port", + "docker_args", + "dependencies", + "docker_socket", + "volumes", + ): + if key in kwargs: + docker_kw[key] = kwargs.pop(key) + return DockerRunner(cls, *args, **docker_kw, **kwargs).start() + + if runner == "venv": + from .venv import VenvRunner + + venv_kw = {} + for key in ( + "env_name", + "module_path", + "port", + "dependencies", + "health_timeout", + ): + if key in kwargs: + venv_kw[key] = kwargs.pop(key) + return VenvRunner(cls, *args, **venv_kw, **kwargs).start() + + raise ValueError(f"Unknown runner: {runner!r}") + + +__all__ = [ + "DirectTransport", + "ObjectHost", + "ObjectProxy", + "RunnerName", + "Transport", + "with_runner", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/_utils.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/_utils.py new file mode 100644 index 00000000..07dac32f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/_utils.py @@ -0,0 +1,138 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Shared utilities for runner implementations.""" + +from __future__ import annotations + +import base64 +import json +import socket +from pathlib import Path +from typing import Any + + +def find_project_root() -> Path: + """Return the project root directory. + + Walks up from the exgentic package looking for a ``pyproject.toml``. + When none is found (e.g. ``uv tool install exgentic``), falls back + to ``~/.exgentic/`` so that benchmark venvs and caches still have a + stable home directory. + """ + for parent in Path(__file__).resolve().parents: + if (parent / "pyproject.toml").exists(): + return parent + fallback = Path.home() / ".exgentic" + fallback.mkdir(parents=True, exist_ok=True) + return fallback + + +def find_free_port() -> int: + """Return an unused TCP port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] + + +def serialize_kwargs(kwargs: dict[str, Any]) -> tuple[str, str]: + """Serialize kwargs for the ``exgentic serve`` CLI. + + Returns ``(flag, value)`` — either ``("--kwargs", json_str)`` + or ``("--kwargs-b64", pickled_b64)`` for non-JSON-serializable values. + """ + try: + return "--kwargs", json.dumps(kwargs) + except TypeError: + import cloudpickle as cp + + return "--kwargs-b64", base64.b64encode(cp.dumps(kwargs)).decode("ascii") + + +_SYSTEM_ENV_BLOCKLIST = frozenset( + { + "PATH", + "HOME", + "USER", + "SHELL", + "HOSTNAME", + "LANG", + "TERM", + "PWD", + "OLDPWD", + "SHLVL", + "_", + "TMPDIR", + "VIRTUAL_ENV", + "CONDA_DEFAULT_ENV", + "CONDA_PREFIX", + } +) +_PREFIX_BLOCKLIST = ("VSCODE_", "UV_", "PIP_") + + +def prepare_subprocess_env() -> dict[str, str]: + """Build a filtered env dict for subprocess runners (venv, docker). + + Forwards API tokens and user config while excluding system-level + vars, IDE noise, and Python-path-manager prefixes that could + conflict with the isolated environment. + """ + import os + + root = find_project_root() + project_root = str(root) if (root / "pyproject.toml").exists() else "" + + env: dict[str, str] = { + k: v + for k, v in os.environ.items() + if k not in _SYSTEM_ENV_BLOCKLIST + and not any(k.startswith(p) for p in _PREFIX_BLOCKLIST) + and not v.startswith(project_root + "/src/") + } + return env + + +def inject_exgentic_env(env: dict[str, str]) -> None: + """Add exgentic context vars and resolved settings paths into *env*. + + Mutates *env* in-place. + """ + from ...core.context import context_env + from ...environment.instance import get_manager + from ...utils.settings import get_settings + + for k, v in context_env().items(): + env[k] = v + for key in ("EXGENTIC_CTX_OUTPUT_DIR", "EXGENTIC_CTX_CACHE_DIR"): + if key in env: + env[key] = str(Path(env[key]).resolve()) + + settings = get_settings() + # Use the EnvironmentManager's base_dir (~/.exgentic/) so that + # EXGENTIC_CACHE_DIR points to the same location where benchmark + # data is actually installed. The old settings.cache_dir default + # (".exgentic") resolved to a CWD-relative path that diverged from + # the manager's absolute ~/.exgentic/ path, breaking Docker mounts. + manager = get_manager() + env.setdefault("EXGENTIC_CACHE_DIR", str(manager.base_dir)) + env.setdefault("EXGENTIC_OUTPUT_DIR", str(Path(settings.output_dir).resolve())) + + +def make_close(transport: Any, stop_fn: Any) -> Any: + """Create a close function for an ObjectProxy. + + Attempts a graceful ``close()`` on the remote object, then shuts + down the transport and calls *stop_fn* to tear down the underlying + process/container. + """ + + def _close() -> None: + try: + transport.call("close") + except Exception: + pass + transport.close() + stop_fn() + + return _close diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/direct.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/direct.py new file mode 100644 index 00000000..c00787ae --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/direct.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""DirectTransport — no isolation, calls the object in the same thread.""" + +from __future__ import annotations + +from typing import Any + +from .transport import ObjectHost, Transport + + +class DirectTransport(Transport): + """Calls the object directly in the same thread and process. + + Useful as a baseline and as the default runner. + """ + + def __init__(self, obj: Any) -> None: + self._host = ObjectHost(obj) + + def call(self, method: str, *args: Any, **kwargs: Any) -> Any: + return self._host.handle("call", method, *args, **kwargs) + + def get(self, name: str) -> Any: + return self._host.handle("get", name) + + def set(self, name: str, value: Any) -> None: + self._host.handle("set", name, value) + + def close(self) -> None: + pass + + def __repr__(self) -> str: + return f"DirectTransport({self._host.obj!r})" diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/docker.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/docker.py new file mode 100644 index 00000000..4ac7822b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/docker.py @@ -0,0 +1,227 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""DockerRunner — runs the HTTP service inside a Docker container. + +Uses the same HTTPTransport as ServiceRunner, but the uvicorn server +runs inside a container instead of a local thread. + +The Docker image is managed by the EnvironmentManager — DockerRunner +only starts the container and wires up volumes, ports and env vars. +""" + +from __future__ import annotations + +import atexit +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from ._utils import ( + find_free_port, + inject_exgentic_env, + make_close, + prepare_subprocess_env, + serialize_kwargs, +) +from .service import HTTPTransport, _wait_for_health +from .transport import ObjectProxy + + +def _docker(*args: str, check: bool = True, **kwargs: Any) -> subprocess.CompletedProcess: + docker_bin = shutil.which("docker") + if docker_bin is None: + raise RuntimeError("docker CLI not found on PATH") + return subprocess.run([docker_bin, *args], check=check, **kwargs) + + +class DockerRunner: + """Start a containerised HTTP service and return an ObjectProxy. + + Parameters + ---------- + target_cls: Class to instantiate inside the container. + env_name: Environment name for EnvironmentManager (e.g. "benchmarks/bfcl"). + module_path: Dotted module path for locating package resources. + image: Pre-built image name (skips EM lookup). + dockerfile: Path to a Dockerfile to build from. + port: Host port to bind (auto-selected if None). + docker_args: Extra arguments forwarded to ``docker run``. + dependencies: Pip packages to install in the image. + docker_socket: Mount the host Docker socket into the container. + volumes: Host-to-container volume mappings (``{host: container}``). + """ + + def __init__( + self, + target_cls: type | str, + *args: Any, + env_name: str = "", + module_path: str = "", + image: str | None = None, + dockerfile: str | None = None, + port: int | None = None, + docker_args: list[str] | None = None, + dependencies: list[str] | None = None, + docker_socket: bool = False, + volumes: dict[str, str] | None = None, + **kwargs: Any, + ) -> None: + if args: + raise ValueError( + "DockerRunner requires keyword-only constructor arguments. " + "Pass all arguments as kwargs instead of positional args." + ) + self._target_cls = target_cls + self._kwargs = kwargs + self._env_name = env_name + self._module_path = module_path + self._image = image + self._dockerfile = dockerfile + self._port = port or find_free_port() + self._docker_args = docker_args or [] + self._dependencies = dependencies or [] + self._docker_socket = docker_socket + self._volumes = volumes or {} + self._container_id: str | None = None + + # ── image handling ─────────────────────────────────────────────── + + def _ensure_image(self) -> str: + if self._image: + return self._image + + if self._dockerfile: + tag = f"exgentic-runner-custom:{hash(self._dockerfile) & 0xFFFFFFFF:08x}" + path = Path(self._dockerfile) + _docker("build", "-t", tag, "-f", str(path), str(path.parent), capture_output=True) + return tag + + if not self._env_name: + raise RuntimeError( + "DockerRunner requires 'env_name' (and usually 'module_path') " + "when no 'image' or 'dockerfile' is provided." + ) + + # Use EM's pre-built image. + from ...environment.instance import get_manager + + mgr = get_manager() + image = mgr.docker_image(self._env_name) + if image: + return image + + # Not pre-installed — install via EM now. + from ...environment import EnvType + from ...environment.helpers import get_exgentic_install_target + + project_root, packages = get_exgentic_install_target() + all_packages = (packages or []) + list(self._dependencies) + mgr.install( + self._env_name, + env_type=EnvType.DOCKER, + module_path=self._module_path, + docker_socket=self._docker_socket, + project_root=project_root, + packages=all_packages or None, + ) + image = mgr.docker_image(self._env_name) + if not image: + raise RuntimeError(f"EM install succeeded but no Docker image found for {self._env_name}") + return image + + # ── container lifecycle ────────────────────────────────────────── + + def start(self) -> ObjectProxy: + image = self._ensure_image() + + if isinstance(self._target_cls, str): + cls_ref = self._target_cls + else: + cls_ref = f"{self._target_cls.__module__}:{self._target_cls.__qualname__}" + kwargs_flag, kwargs_value = serialize_kwargs(self._kwargs) + + run_args: list[str] = ["run", "-d", "-p", f"{self._port}:8080"] + + # Forward host environment into the container (API tokens, user + # config) while excluding system-level and IDE vars. + env = prepare_subprocess_env() + inject_exgentic_env(env) + cache_dir = env.get("EXGENTIC_CACHE_DIR", "") + + for k, v in env.items(): + run_args.extend(["-e", f"{k}={v}"]) + + # Mount Docker socket for sibling container access. + if self._docker_socket: + run_args.extend(["-v", "/var/run/docker.sock:/var/run/docker.sock"]) + + # Always mount the cache dir so benchmarks that skip data downloads + # during Docker build (e.g. browsecompplus) can access host-side data, + # and benchmarks that bake data into the image (e.g. appworld) can + # also work since the volume mount overlays the image path. + Path(cache_dir).mkdir(parents=True, exist_ok=True) + run_args.extend(["-v", f"{cache_dir}:{cache_dir}"]) + + # Mount volumes. Resolve to absolute paths (Docker requires them) + # and ensure source directories exist — Docker Desktop on macOS + # cannot create mount sources in some protected paths. + for host_path, container_path in self._volumes.items(): + host_path = str(Path(host_path).resolve()) + container_path = str(Path(container_path)) if Path(container_path).is_absolute() else container_path + Path(host_path).mkdir(parents=True, exist_ok=True) + run_args.extend(["-v", f"{host_path}:{container_path}"]) + + run_args.extend(self._docker_args) + run_args.extend( + [ + image, + "exgentic", + "serve", + "--cls", + cls_ref, + kwargs_flag, + kwargs_value, + "--host", + "0.0.0.0", + "--port", + "8080", + ] + ) + + result = _docker(*run_args, capture_output=True, text=True) + self._container_id = result.stdout.strip() + atexit.register(self._stop_container) + + url = f"http://127.0.0.1:{self._port}" + try: + _wait_for_health(url, timeout=60.0) + except TimeoutError: + cid = self._container_id or "" + logs = _docker("logs", cid, check=False, capture_output=True, text=True) + status = _docker( + "inspect", "--format", "{{.State.Status}}", cid, check=False, capture_output=True, text=True + ) + self._stop_container() + raise TimeoutError( + f"Container did not become healthy within 60s.\n" + f"Status: {status.stdout.strip()}\n" + f"Logs:\n{logs.stdout}\n{logs.stderr}" + ) from None + + transport = HTTPTransport(url, timeout=600.0) + proxy = ObjectProxy(transport) + object.__setattr__(proxy, "close", make_close(transport, self._stop_container)) + return proxy + + def _stop_container(self) -> None: + if self._container_id is None: + return + cid = self._container_id + self._container_id = None + try: + _docker("stop", "-t", "2", cid, check=False, capture_output=True) + _docker("rm", "-f", cid, check=False, capture_output=True) + except Exception: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/process.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/process.py new file mode 100644 index 00000000..ed9560b4 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/process.py @@ -0,0 +1,174 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""PipeTransport — runs the object in a subprocess via multiprocessing + cloudpickle.""" + +from __future__ import annotations + +import multiprocessing as mp +import weakref +from typing import Any + +import cloudpickle as cp + +from .transport import ObjectHost, Transport, deserialize_error, serialize_error + +# ── worker process ─────────────────────────────────────────────────── + + +def _worker(q_in: mp.Queue, q_out: mp.Queue) -> None: + """Subprocess entry point: create the object and serve RPC requests.""" + # Late imports — these run in the child process. + from ...core.context import init_context_from_env, set_context, try_get_context + from ...observers.logging import configure_warnings_logging + + configure_warnings_logging(replace_existing_file_handlers=False) + + try: + tag, target_cls, args, kwargs, ctx = cp.loads(q_in.get()) + assert tag == "init" + + # Restore context in child process. + if ctx is not None: + set_context(ctx) + else: + try: + init_context_from_env() + except RuntimeError: + pass # No context env vars — standalone worker + + obj = target_cls(*args, **kwargs) + + # If the object has a session_id, update context to include it. + session_id = getattr(obj, "session_id", None) + if session_id: + current_ctx = try_get_context() + if current_ctx is not None: + set_context(current_ctx.with_session(str(session_id))) + + q_out.put(cp.dumps(("ready", None))) + except Exception as exc: + q_out.put(cp.dumps(("error", serialize_error(exc)))) + return + + host = ObjectHost(obj) + while True: + try: + raw = q_in.get() + if raw is None: # shutdown sentinel + break + op, name, args, kwargs = cp.loads(raw) + try: + result = host.handle(op, name, *args, **kwargs) + q_out.put(cp.dumps(("ok", result))) + except Exception as exc: + q_out.put(cp.dumps(("error", serialize_error(exc)))) + except (EOFError, BrokenPipeError): + break + + +# ── transport ──────────────────────────────────────────────────────── + + +class PipeTransport(Transport): + """Runs the target in a child process with full memory isolation. + + Uses cloudpickle for serialization and multiprocessing queues + for communication. Propagates the exgentic Context to the child. + """ + + def __init__(self, target_cls: type, *args: Any, **kwargs: Any) -> None: + self._target_cls = target_cls + self._args = args + self._kwargs = kwargs + self._ctx = mp.get_context("spawn") + self._q_in: mp.Queue | None = None + self._q_out: mp.Queue | None = None + self._proc: mp.Process | None = None + + def start(self) -> None: + if self._proc is not None and self._proc.is_alive(): + return + + from ...core.context import context_env_scope, try_get_context + + self._q_in = self._ctx.Queue() + self._q_out = self._ctx.Queue() + self._proc = self._ctx.Process( + target=_worker, + args=(self._q_in, self._q_out), + daemon=True, + ) + # Ensure context env vars are in os.environ for the spawned process. + with context_env_scope(): + self._proc.start() + self._finalizer = weakref.finalize(self, _terminate, self._q_in, self._proc) + + # Send init payload with context. + ctx = try_get_context() + self._q_in.put(cp.dumps(("init", self._target_cls, self._args, self._kwargs, ctx))) + status, payload = self._recv() + if status == "error": + self.close() + raise deserialize_error(payload) + + # ── internal helpers ───────────────────────────────────────────── + + def _recv(self) -> tuple[str, Any]: + assert self._q_out is not None + if self._proc is not None and not self._proc.is_alive(): + raise RuntimeError(f"Worker process died (exit code: {self._proc.exitcode})") + return cp.loads(self._q_out.get()) + + def _rpc(self, op: str, name: str, *args: Any, **kwargs: Any) -> Any: + if self._proc is None or self._q_in is None or not self._proc.is_alive(): + raise RuntimeError("Worker process is not running") + self._q_in.put(cp.dumps((op, name, args, kwargs))) + status, payload = self._recv() + if status == "error": + raise deserialize_error(payload) + return payload + + # ── Transport API ──────────────────────────────────────────────── + + def call(self, method: str, *args: Any, **kwargs: Any) -> Any: + return self._rpc("call", method, *args, **kwargs) + + def get(self, name: str) -> Any: + return self._rpc("get", name) + + def set(self, name: str, value: Any) -> None: + self._rpc("set", name, value) + + def close(self) -> None: + _terminate(self._q_in, self._proc) + self._q_in = None + self._q_out = None + self._proc = None + try: + self._finalizer.detach() + except Exception: + pass + + def __repr__(self) -> str: + pid = self._proc.pid if self._proc else None + return f"PipeTransport({self._target_cls.__name__}, pid={pid})" + + +def _terminate(q_in: mp.Queue | None, proc: mp.Process | None) -> None: + """Shut down the worker process (used by both close() and the weak finalizer).""" + try: + if q_in is not None: + q_in.put(None) + except Exception: + pass + try: + if proc is not None: + proc.join(timeout=2.0) + except Exception: + pass + try: + if proc is not None and proc.is_alive(): + proc.terminate() + except Exception: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/service.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/service.py new file mode 100644 index 00000000..fa530755 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/service.py @@ -0,0 +1,237 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""HTTPTransport + serve() — run any object as an HTTP service.""" + +from __future__ import annotations + +import base64 +import threading +import time +from typing import Any, Optional + +import cloudpickle as cp +import httpx +from fastapi import FastAPI +from pydantic import BaseModel as PydanticBaseModel + +from .transport import ObjectHost, ObjectProxy, Transport, deserialize_error, serialize_error + +# ── HTTP models ────────────────────────────────────────────────────── + + +class CallRequest(PydanticBaseModel): + method: str + args: str # base64(cloudpickle) + kwargs: str # base64(cloudpickle) + + +class GetRequest(PydanticBaseModel): + name: str + + +class SetRequest(PydanticBaseModel): + name: str + value: str # base64(cloudpickle) + + +class RPCResponse(PydanticBaseModel): + status: str # "ok" | "error" + result: Optional[str] = None # base64(cloudpickle) + error_type: Optional[str] = None + error_msg: Optional[str] = None + error_tb: Optional[str] = None + error_pickled: Optional[str] = None # base64(cloudpickle'd exception) + + +# ── helpers ────────────────────────────────────────────────────────── + + +def _encode(obj: Any) -> str: + return base64.b64encode(cp.dumps(obj)).decode("ascii") + + +def _decode(data: str) -> Any: + return cp.loads(base64.b64decode(data)) + + +def _error_response(exc: Exception) -> RPCResponse: + data = serialize_error(exc) + pickled_b64 = None + if data["pickled"] is not None: + pickled_b64 = base64.b64encode(data["pickled"]).decode("ascii") + return RPCResponse( + status="error", + error_type=data["type"], + error_msg=data["msg"], + error_tb=data["tb"], + error_pickled=pickled_b64, + ) + + +# ── FastAPI app ────────────────────────────────────────────────────── + + +def create_app(host: ObjectHost) -> FastAPI: + app = FastAPI() + + @app.get("/health") + def health(): + return {"status": "ok"} + + @app.post("/call") + def handle_call(req: CallRequest) -> RPCResponse: + try: + result = host.handle("call", req.method, *_decode(req.args), **_decode(req.kwargs)) + return RPCResponse(status="ok", result=_encode(result)) + except Exception as exc: + return _error_response(exc) + + @app.post("/get") + def handle_get(req: GetRequest) -> RPCResponse: + try: + return RPCResponse(status="ok", result=_encode(host.handle("get", req.name))) + except Exception as exc: + return _error_response(exc) + + @app.post("/set") + def handle_set(req: SetRequest) -> RPCResponse: + try: + host.handle("set", req.name, _decode(req.value)) + return RPCResponse(status="ok") + except Exception as exc: + return _error_response(exc) + + return app + + +# ── serve() ────────────────────────────────────────────────────────── + + +def serve(obj: Any, host: str = "0.0.0.0", port: int = 8080) -> None: + """Serve an object over HTTP (blocking).""" + import uvicorn + + uvicorn.run(create_app(ObjectHost(obj)), host=host, port=port, log_level="warning") + + +# ── HTTPTransport — client side ────────────────────────────────────── + + +class HTTPTransport(Transport): + """Talks to an HTTP server hosting an ObjectHost.""" + + def __init__(self, base_url: str, timeout: float = 30.0) -> None: + self._base_url = base_url.rstrip("/") + self._client = httpx.Client(timeout=timeout) + + def _rpc(self, endpoint: str, payload: dict) -> Any: + resp = self._client.post(f"{self._base_url}{endpoint}", json=payload) + resp.raise_for_status() + data = RPCResponse(**resp.json()) + if data.status == "error": + pickled = base64.b64decode(data.error_pickled) if data.error_pickled else None + raise deserialize_error( + { + "type": data.error_type or "RuntimeError", + "msg": data.error_msg or "", + "tb": data.error_tb or "", + "pickled": pickled, + } + ) + return _decode(data.result) if data.result is not None else None + + def call(self, method: str, *args: Any, **kwargs: Any) -> Any: + return self._rpc( + "/call", + { + "method": method, + "args": _encode(args), + "kwargs": _encode(kwargs), + }, + ) + + def get(self, name: str) -> Any: + return self._rpc("/get", {"name": name}) + + def set(self, name: str, value: Any) -> None: + self._rpc("/set", {"name": name, "value": _encode(value)}) + + def close(self) -> None: + self._client.close() + + def __repr__(self) -> str: + return f"HTTPTransport({self._base_url!r})" + + +# ── Utilities ──────────────────────────────────────────────────────── + + +def _wait_for_health(url: str, timeout: float = 15.0) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + if httpx.get(f"{url}/health", timeout=2.0).status_code == 200: + return + except httpx.HTTPError: + pass + time.sleep(0.1) + raise TimeoutError(f"Service at {url} did not become healthy within {timeout}s") + + +# ── ServiceRunner ──────────────────────────────────────────────────── + + +class ServiceRunner: + """Starts an HTTP service in a background thread and returns an ObjectProxy.""" + + def __init__( + self, + target_cls: type, + *args: Any, + port: int | None = None, + **kwargs: Any, + ) -> None: + self._target_cls = target_cls + self._args = args + self._kwargs = kwargs + from ._utils import find_free_port + + self._port = port or find_free_port() + self._server = None + + def start(self) -> ObjectProxy: + import uvicorn + + from ...core.context import set_context_fallback, try_get_context + + # Set process-wide fallback so context is available in uvicorn's + # request handler threads (which don't inherit ContextVar). + set_context_fallback(try_get_context()) + + obj = self._target_cls(*self._args, **self._kwargs) + app = create_app(ObjectHost(obj)) + + config = uvicorn.Config(app, host="127.0.0.1", port=self._port, log_level="warning") + self._server = uvicorn.Server(config) + threading.Thread(target=self._server.run, daemon=True).start() + + url = f"http://127.0.0.1:{self._port}" + _wait_for_health(url) + + transport = HTTPTransport(url) + proxy = ObjectProxy(transport) + + server_ref = self._server + + def _close() -> None: + try: + transport.call("close") + except AttributeError: + pass + transport.close() + server_ref.should_exit = True + set_context_fallback(None) + + object.__setattr__(proxy, "close", _close) + return proxy diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/thread.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/thread.py new file mode 100644 index 00000000..dda6fa39 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/thread.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""ThreadTransport — runs the object in a dedicated thread, queue-based RPC.""" + +from __future__ import annotations + +import contextvars +import queue +import threading +from typing import Any + +from .transport import ObjectHost, Transport, deserialize_error, serialize_error + +_SHUTDOWN = object() + + +class ThreadTransport(Transport): + """Runs the target object in a dedicated daemon thread. + + Communication happens via two queues (request / response). + The object's methods never block the caller's thread except + while waiting for the result. + """ + + def __init__(self, target_cls: type, *args: Any, **kwargs: Any) -> None: + self._target_cls = target_cls + self._args = args + self._kwargs = kwargs + self._req: queue.Queue = queue.Queue() + self._resp: queue.Queue = queue.Queue() + self._thread: threading.Thread | None = None + + def start(self) -> None: + ctx = contextvars.copy_context() + self._thread = threading.Thread(target=ctx.run, args=(self._worker,), daemon=True) + self._thread.start() + status, payload = self._resp.get() + if status == "error": + raise deserialize_error(payload) + + # ── worker loop ────────────────────────────────────────────────── + + def _worker(self) -> None: + try: + obj = self._target_cls(*self._args, **self._kwargs) + except Exception as exc: + self._resp.put(("error", serialize_error(exc))) + return + + host = ObjectHost(obj) + self._resp.put(("ready", None)) + + while True: + msg = self._req.get() + if msg is _SHUTDOWN: + break + op, name, args, kwargs = msg + try: + result = host.handle(op, name, *args, **kwargs) + self._resp.put(("ok", result)) + except Exception as exc: + self._resp.put(("error", serialize_error(exc))) + + # ── Transport API ──────────────────────────────────────────────── + + def _rpc(self, op: str, name: str, *args: Any, **kwargs: Any) -> Any: + if self._thread is None or not self._thread.is_alive(): + raise RuntimeError("Worker thread is not running") + self._req.put((op, name, args, kwargs)) + status, payload = self._resp.get() + if status == "error": + raise deserialize_error(payload) + return payload + + def call(self, method: str, *args: Any, **kwargs: Any) -> Any: + return self._rpc("call", method, *args, **kwargs) + + def get(self, name: str) -> Any: + return self._rpc("get", name) + + def set(self, name: str, value: Any) -> None: + self._rpc("set", name, value) + + def close(self) -> None: + if self._thread is not None and self._thread.is_alive(): + self._req.put(_SHUTDOWN) + self._thread.join(timeout=5.0) + self._thread = None + + def __repr__(self) -> str: + return f"ThreadTransport({self._target_cls.__name__})" diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/transport.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/transport.py new file mode 100644 index 00000000..fe4887d2 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/transport.py @@ -0,0 +1,188 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Core transport abstractions: Transport, ObjectHost, ObjectProxy, and error helpers.""" + +from __future__ import annotations + +import builtins +import inspect +import traceback +from abc import ABC, abstractmethod +from typing import Any + +import cloudpickle as cp + +# Sentinel returned by ``get`` when the attribute is a bound method. +# The proxy checks for this to avoid serialising the entire instance. +CALLABLE_MARKER = {"__exgentic_callable__": True} + +# ── Transport interface ────────────────────────────────────────────── + + +class Transport(ABC): + """Communication channel between a proxy and a remote object. + + Every transport implements four operations so that ``ObjectProxy`` + can forward attribute access and method calls regardless of where + the real object lives. + """ + + @abstractmethod + def call(self, method: str, *args: Any, **kwargs: Any) -> Any: + ... + + @abstractmethod + def get(self, name: str) -> Any: + ... + + @abstractmethod + def set(self, name: str, value: Any) -> None: + ... + + @abstractmethod + def close(self) -> None: + ... + + +# ── ObjectHost — server side ───────────────────────────────────────── + + +class ObjectHost: + """Executes operations on a real object (the "server side"). + + Used identically whether the object lives in the same thread, + a subprocess, or an HTTP server. + """ + + def __init__(self, obj: Any) -> None: + self.obj = obj + + def handle(self, op: str, name: str, *args: Any, **kwargs: Any) -> Any: + if op == "call": + return getattr(self.obj, name)(*args, **kwargs) + if op == "get": + value = getattr(self.obj, name) + # Bound methods cannot be reliably serialised (the instance may + # contain locks, threads, etc.). Return a lightweight marker so + # the proxy knows to use ``call`` instead. + if inspect.ismethod(value) or inspect.isbuiltin(value): + return CALLABLE_MARKER + return value + if op == "set": + setattr(self.obj, name, args[0]) + return None + if op == "del": + delattr(self.obj, name) + return None + raise ValueError(f"Unknown operation: {op!r}") + + +# ── Error serialization ────────────────────────────────────────────── + + +def serialize_error(exc: BaseException) -> dict: + """Serialize an exception into a dict that can cross process/network boundaries. + + The dict always contains string fallbacks (``type``, ``msg``, ``tb``). + When possible it also includes a ``pickled`` copy of the original + exception so that custom exception types and attributes survive. + """ + pickled = None + try: + pickled = cp.dumps(exc) + except Exception: + pass + return { + "type": type(exc).__qualname__, + "msg": str(exc), + "tb": traceback.format_exc(), + "pickled": pickled, + } + + +def deserialize_error(data: dict) -> BaseException: + """Reconstruct an exception from a ``serialize_error`` dict. + + Strategy: try cloudpickle first (preserves custom types and state), + then fall back to reconstructing a builtin type from its name. + A ``__remote_traceback__`` attribute is always attached. + """ + tb = data.get("tb", "") + + # Fast path: unpickle the original exception. + pickled = data.get("pickled") + if pickled is not None: + try: + exc = cp.loads(pickled) + if isinstance(exc, BaseException): + exc.__remote_traceback__ = tb # type: ignore[attr-defined] + return exc + except Exception: + pass + + # Fallback: reconstruct from type name (builtins only) + message. + name = data.get("type", "RuntimeError") + msg = data.get("msg", "") + cls = getattr(builtins, name, None) + if not (isinstance(cls, type) and issubclass(cls, BaseException)): + cls = RuntimeError + try: + exc = cls(msg) + except TypeError: + exc = RuntimeError(f"{name}: {msg}") + exc.__remote_traceback__ = tb # type: ignore[attr-defined] + return exc + + +# ── ObjectProxy — client side ──────────────────────────────────────── + + +class ObjectProxy: + """Transparent proxy that forwards attribute access over a Transport. + + Behaves like the real object: attribute reads, writes, and method + calls are all forwarded through the transport. + """ + + def __init__(self, transport: Transport) -> None: + object.__setattr__(self, "_transport", transport) + + def __getattr__(self, name: str) -> Any: + transport: Transport = object.__getattribute__(self, "_transport") + value = transport.get(name) + if isinstance(value, dict) and value.get("__exgentic_callable__"): + + def method(*args: Any, **kwargs: Any) -> Any: + return transport.call(name, *args, **kwargs) + + method.__name__ = name # type: ignore[attr-defined] + return method + return value + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("_"): + object.__setattr__(self, name, value) + else: + transport: Transport = object.__getattribute__(self, "_transport") + transport.set(name, value) + + def close(self) -> None: + """Close the remote object, then tear down the transport.""" + transport: Transport = object.__getattribute__(self, "_transport") + try: + transport.call("close") + except AttributeError: + pass + transport.close() + + def __del__(self) -> None: + try: + transport: Transport = object.__getattribute__(self, "_transport") + transport.close() + except Exception: + pass + + def __repr__(self) -> str: + transport: Transport = object.__getattribute__(self, "_transport") + return f"ObjectProxy({transport!r})" diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/runners/venv.py b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/venv.py new file mode 100644 index 00000000..28ece872 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/runners/venv.py @@ -0,0 +1,215 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""VenvRunner — runs the HTTP service inside a uv virtual environment. + +Uses the same HTTPTransport as ServiceRunner and DockerRunner, but the +uvicorn server runs in a subprocess with its own isolated venv instead +of the host Python or a Docker container. + +The venv is created and managed by the EnvironmentManager — VenvRunner +only starts the subprocess and optionally installs extra runtime +dependencies. +""" + +from __future__ import annotations + +import atexit +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Any + +from ._utils import ( + find_free_port, + inject_exgentic_env, + make_close, + prepare_subprocess_env, + serialize_kwargs, +) +from .service import HTTPTransport, _wait_for_health +from .transport import ObjectProxy + +_HEALTH_TIMEOUT = 30.0 +_TRANSPORT_TIMEOUT = 1800.0 + + +def _uv(*args: str, check: bool = True, **kwargs: Any) -> subprocess.CompletedProcess: + uv_bin = shutil.which("uv") + if uv_bin is None: + raise RuntimeError("uv CLI not found on PATH") + result = subprocess.run([uv_bin, *args], check=False, **kwargs) + if check and result.returncode != 0: + stderr = getattr(result, "stderr", "") or "" + stdout = getattr(result, "stdout", "") or "" + raise RuntimeError(f"uv {' '.join(args[:3])} failed (exit {result.returncode}):\n{stderr}\n{stdout}") + return result + + +class VenvRunner: + """Start an HTTP service in an isolated uv venv and return an ObjectProxy. + + Parameters + ---------- + target_cls: Class to instantiate inside the venv subprocess. + env_name: Environment name for EnvironmentManager (e.g. "benchmarks/bfcl"). + module_path: Dotted module path for locating package resources. + port: Host port to bind (auto-selected if None). + dependencies: Extra pip packages to install in the venv at runtime. + health_timeout: Seconds to wait for the health endpoint. + """ + + def __init__( + self, + target_cls: type | str, + *args: Any, + env_name: str = "", + module_path: str = "", + port: int | None = None, + dependencies: list[str] | None = None, + health_timeout: float | None = None, + **kwargs: Any, + ) -> None: + if args: + raise ValueError( + "VenvRunner requires keyword-only constructor arguments. " + "Pass all arguments as kwargs instead of positional args." + ) + self._target_cls = target_cls + self._kwargs = kwargs + self._env_name = env_name + self._module_path = module_path + self._port = port or find_free_port() + self._dependencies = dependencies or [] + self._health_timeout = health_timeout or _HEALTH_TIMEOUT + self._process: subprocess.Popen | None = None + + # ── venv handling ───────────────────────────────────────────────── + + def _get_venv_dir(self) -> Path: + """Return the venv directory managed by EnvironmentManager.""" + from ...environment.instance import get_manager + + return get_manager().env_path(self._env_name) / "venv" + + def _venv_python(self) -> Path: + """Return the path to the Python binary inside the venv.""" + venv = self._get_venv_dir() + if sys.platform == "win32": + return venv / "Scripts" / "python.exe" + return venv / "bin" / "python" + + def _ensure_venv(self) -> Path: + """Ensure the venv exists via EnvironmentManager.""" + from ...environment import EnvType + from ...environment.helpers import get_exgentic_install_target + from ...environment.instance import get_manager + + mgr = get_manager() + project_root, packages = get_exgentic_install_target() + mgr.install( + self._env_name, + env_type=EnvType.VENV, + module_path=self._module_path, + project_root=project_root, + packages=packages, + ) + return self._get_venv_dir() + + def _install_deps(self) -> None: + """Install extra runtime dependencies into the venv.""" + if not self._dependencies: + return + python = self._venv_python() + _uv( + "pip", + "install", + "--python", + str(python), + "--no-cache", + *self._dependencies, + capture_output=True, + text=True, + ) + + # ── subprocess lifecycle ────────────────────────────────────────── + + def start(self) -> ObjectProxy: + venv = self._ensure_venv() + self._install_deps() + + if isinstance(self._target_cls, str): + cls_ref = self._target_cls + else: + cls_ref = f"{self._target_cls.__module__}:{self._target_cls.__qualname__}" + kwargs_flag, kwargs_value = serialize_kwargs(self._kwargs) + + # Build a filtered environment (same filtering as DockerRunner). + env = prepare_subprocess_env() + env["VIRTUAL_ENV"] = str(venv) + venv_bin = str(venv / "bin") + # Prepend venv bin to the *system* PATH so external tools (docker, + # podman, git, …) remain reachable from within the venv subprocess. + system_path = os.environ.get("PATH", "") + env["PATH"] = venv_bin + os.pathsep + system_path + inject_exgentic_env(env) + + exgentic_bin = self._get_venv_dir() / "bin" / "exgentic" + cmd = [ + str(exgentic_bin), + "serve", + "--cls", + cls_ref, + kwargs_flag, + kwargs_value, + "--host", + "127.0.0.1", + "--port", + str(self._port), + ] + + self._process = subprocess.Popen( + cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + atexit.register(self._stop_process) + + url = f"http://127.0.0.1:{self._port}" + try: + _wait_for_health(url, timeout=self._health_timeout) + except TimeoutError: + proc = self._process + if proc is not None: + proc.terminate() + stdout, stderr = proc.communicate(timeout=5) + else: + stdout, stderr = b"", b"" + self._stop_process() + raise TimeoutError( + f"Venv service did not become healthy within {self._health_timeout}s.\n" + f"stdout:\n{stdout.decode(errors='replace')}\n" + f"stderr:\n{stderr.decode(errors='replace')}" + ) from None + + transport = HTTPTransport(url, timeout=_TRANSPORT_TIMEOUT) + proxy = ObjectProxy(transport) + object.__setattr__(proxy, "close", make_close(transport, self._stop_process)) + return proxy + + def _stop_process(self) -> None: + if self._process is None: + return + proc = self._process + self._process = None + try: + proc.terminate() + proc.wait(timeout=5) + except Exception: + try: + proc.kill() + except Exception: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/__init__.py b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/json_schema.py b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/json_schema.py new file mode 100644 index 00000000..d2d6a7be --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/json_schema.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any, Literal + +from json_schema_to_pydantic import create_model as _schema_to_model +from pydantic import BaseModel + + +def make_args_model_from_json_schema(name: str, parameters: dict[str, Any]) -> type[BaseModel]: + """Build a Pydantic v2 model from JSON Schema and verify core semantics match. + + Verifies (type/required/properties), ignoring cosmetic keys like 'title'. + """ + # 1) build + model = _schema_to_model( + schema=parameters, + base_model_type=BaseModel, + root_schema=None, + allow_undefined_array_items=False, + allow_undefined_type=False, + ) + + return model + + +def _schema_to_type(schema: dict[str, Any]) -> Any: + """Best-effort map a JSON Schema fragment to a Python type annotation.""" + if not isinstance(schema, dict): + return Any + if isinstance(schema.get("enum"), list) and schema["enum"]: + return _enum_type(schema["enum"]) # type: ignore[arg-type] + t = schema.get("type") + items = schema.get("items") if isinstance(schema.get("items"), dict) else None + return _json_type_to_py(t, items) + + +def _json_type_to_py(t: Any, item_schema: dict[str, Any] | None = None): + """Map a JSON Schema "type" to a Python type annotation. + + Supports primitives and simple containers. For arrays/objects, uses generic + fallbacks unless an item schema is provided for arrays. + """ + from typing import Any as TAny + from typing import Dict as TDict + from typing import List as TList + + if t == "string": + return str + if t == "integer": + return int + if t == "number": + return float + if t == "boolean": + return bool + if t == "array": + # Try to infer item type if provided, otherwise default to list[Any] + if isinstance(item_schema, dict): + inner = _schema_to_type(item_schema) + return TList[inner] # type: ignore[index] + return TList[TAny] # type: ignore[index] + if t == "object": + return TDict[str, TAny] # type: ignore[index] + return Any + + +def _enum_type(values: list[Any]): + """Create a Literal type from enum values when feasible; otherwise Any.""" + try: + return Literal[tuple(values)] # type: ignore[misc] + except TypeError: + # Fallback if values contain unhashables or mixed unsupported types + return Any + + +# def make_args_model_from_param_list(name: str, params: List[Dict[str, Any]]) -> type[BaseModel]: +# """Create a Pydantic model from AppWorld 'standard' parameter list entries.""" +# fields: Dict[str, Tuple[Any, Any]] = {} + +# for p in params or []: +# pname = p["name"] +# ptype = p.get("type") +# required = bool(p.get("required", False)) +# default = p.get("default", ... if required else None) +# enum_vals = p.get("enum") +# field_kwargs: Dict[str, Any] = {} + +# if isinstance(enum_vals, list) and enum_vals: +# py_t = _enum_type(enum_vals) +# else: +# py_t = _json_type_to_py(ptype) + +# if ptype in ("number", "integer"): +# if "minimum" in p: +# field_kwargs["ge"] = p["minimum"] +# if "maximum" in p: +# field_kwargs["le"] = p["maximum"] +# if ptype == "string": +# if "minLength" in p: +# field_kwargs["min_length"] = p["minLength"] +# if "maxLength" in p: +# field_kwargs["max_length"] = p["maxLength"] + +# if field_kwargs: +# annotated = Annotated[py_t, Field(**field_kwargs)] # type: ignore[misc] +# fields[pname] = (annotated, default) +# else: +# fields[pname] = (py_t, default) + +# return create_model(f"{name}_Args", **fields) diff --git a/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/openai.py b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/openai.py new file mode 100644 index 00000000..2292e1ab --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/adapters/schemas/openai.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import create_model + +from ...core.types import ActionType, SingleAction +from .json_schema import make_args_model_from_json_schema + + +def openai_tools_to_action_types(tools: list[dict[str, Any]]) -> list[ActionType]: + """Translate OpenAI-style tools into ActionType definitions. + + Builds concrete argument models from each tool's parameter schema so no + information is lost when emitting tools back to the LLM via ActionType. + """ + actions: list[ActionType] = [] + for t in tools: + if not isinstance(t, dict) or t.get("type") != "function": + continue + fn = t.get("function") or {} + name = fn.get("name") + if not isinstance(name, str): + continue + desc = fn.get("description") or "" + params = fn.get("parameters") or {} + + args_model = make_args_model_from_json_schema(name, params) + + action_model = create_model( + f"{name}_Action", + __base__=SingleAction, + name=(Literal[name], name), + arguments=(args_model, ...), + ) + actions.append(ActionType(name=name, description=str(desc), cls=action_model)) + + if not actions: + raise ValueError("No OpenAI function tools provided to translate into ActionTypes") + return actions + + +def mcp_to_openai_tool(mcp_tool: Any) -> dict[str, Any]: + """Converts a tool definition from a 'mcp' format into OpenAI tool schema.""" + function_name = mcp_tool.name + description = mcp_tool.description or "" + parameters_schema = mcp_tool.inputSchema or {"type": "object", "properties": {}} + + if not function_name: + raise ValueError("MCP tool definition is missing a 'name'.") + + tool_schema = { + "type": "function", + "function": { + "name": function_name, + "description": description, + "parameters": parameters_schema, + }, + } + return tool_schema + + +def mcp_tools_to_openai_tools(mcp_tools: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [mcp_to_openai_tool(mcp_tool) for mcp_tool in mcp_tools] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/__init__.py new file mode 100644 index 00000000..d3ec05ef --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +# Bundled agents diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/__init__.py new file mode 100644 index 00000000..f787a20c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from .a_mem_agent import AMemAgent + +__all__ = ["AMemAgent"] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_agent.py new file mode 100644 index 00000000..398a108d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_agent.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar, Optional + +from pydantic import ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings +from ...utils.settings import RunnerName + + +class AMemAgent(Agent): + + display_name: ClassVar[str] = "A-Mem Agent" + slug_name: ClassVar[str] = "a_mem" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + model: str = "gpt-4o" + memory_model: Optional[str] = None + + retrieve_k: int = 10 + evo_threshold: int = 100 + embedding_model: str = "all-MiniLM-L6-v2" + + shuffle_mode: str = "isolated" + + benchmark_id: Optional[str] = None + + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + runner: RunnerName | None = None + model_settings: ModelSettings | None = None + + @classmethod + def _get_instance_class(cls): + from .a_mem_instance import AMemAgentInstance + return AMemAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.a_mem.a_mem_instance:AMemAgentInstance" + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "memory_model": self.memory_model or self.model, + "retrieve_k": self.retrieve_k, + "evo_threshold": self.evo_threshold, + "embedding_model": self.embedding_model, + "shuffle_mode": self.shuffle_mode, + "model_settings": self.model_settings, + "benchmark_id": self.benchmark_id, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + } + + @property + def model_name(self) -> str: + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: + names = [str(self.model)] + mm = self.memory_model or self.model + if mm != self.model: + names.append(str(mm)) + return names diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_instance.py new file mode 100644 index 00000000..4fba1fe1 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/a_mem_instance.py @@ -0,0 +1,726 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import re +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +import litellm +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionSystemMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, +) +from ...utils.cost import LiteLLMCostReport + +from .memory_note import MemoryNote +from .memory_store import MemoryStore +from .prompts import GENERATE_QUERY_PROMPT, parse_keywords_response +from ..tool_shortlisting import shortlist_tools + +try: + from ...agents.litellm_tool_calling.utils import ToolCall, ToolsActionsRegistry +except ImportError: + ToolsActionsRegistry = None + ToolCall = dict + + +class AMemAgentInstance(AgentInstance): + + def __init__( + self, + session_id: str, + model: str = "gpt-4o", + memory_model: str = "gpt-4o", + retrieve_k: int = 10, + evo_threshold: int = 100, + embedding_model: str = "all-MiniLM-L6-v2", + shuffle_mode: str = "isolated", + model_settings: Optional[ModelSettings] = None, + benchmark_id: Optional[str] = None, + enable_tool_shortlisting: bool = False, + max_selected_tools: int = 30, + ) -> None: + super().__init__(session_id) + + self.model = model + self.memory_model = memory_model + self.retrieve_k = retrieve_k + self.evo_threshold = evo_threshold + self.embedding_model = embedding_model + self.shuffle_mode = shuffle_mode + self.benchmark_id = benchmark_id + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + + if model_settings is None: + self._model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self._model_settings = model_settings + else: + self._model_settings = ModelSettings() + + self._cost = LiteLLMCostReport.initialize_empty(model_name=self.model) + self._store: Optional[MemoryStore] = None + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ChatCompletionSystemMessage, + ] + ] = [] + self._step_count: int = 0 + + self._registry: Optional[ToolsActionsRegistry] = None + self._all_actions: list[ActionType] = [] + + self._interaction_log: List[str] = [] + + # Memory tracking for this session + self._memories_added: int = 0 + self._evolutions_triggered: int = 0 + + def _log_failure( + self, component: str, error: Exception, context: Dict[str, Any] + ) -> None: + try: + log_path = self.paths.agent_dir / "amem_failures.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True) + entry = { + "timestamp": datetime.now().isoformat(), + "session_id": self.session_id, + "component": component, + "error_type": type(error).__name__, + "error_message": str(error)[:2000], + **{ + k: str(v)[:2000] if isinstance(v, str) else v + for k, v in context.items() + }, + } + with open(log_path, "a", encoding="utf-8") as fh: + fh.write(json.dumps(entry, ensure_ascii=False) + "\n") + except Exception: + pass + + def start( + self, + task: str, + context: Dict[str, Any], + actions: list[ActionType], + ) -> None: + super().start(task, context, actions) + + self._all_actions = list(self.actions) + if ToolsActionsRegistry is not None: + self._registry = ToolsActionsRegistry(self._all_actions) + + task_group = str( + context.get("task_group") + or context.get("task_id") + or context.get("task_name") + or "default" + ) + self._store = MemoryStore.get_or_create( + shuffle_mode=self.shuffle_mode, + task_group=task_group, + benchmark_id=self.benchmark_id, + embedding_model=self.embedding_model, + evo_threshold=self.evo_threshold, + ) + self._store.increment_session() + + system_content = self._build_system_prompt_with_memories() + self._add_message( + ChatCompletionSystemMessage(role="system", content=system_content) + ) + + content_parts: list[Any] = [] + ctx_str = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx_str += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx_str}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message( + ChatCompletionUserMessage(role="user", content=text_content) + ) + + self._interaction_log.append(f"Task: {self.task}") + + self.logger.info( + "A-Mem instance started store=%s session_count=%d " + "memory_count=%d benchmark=%s tools=%d", + self._store.store_id, + self._store.session_count, + self._store.memory_count, + self.benchmark_id or "(none)", + len(self._all_actions), + ) + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + self._step_count += 1 + + observation_text = self._observe(observation) + if observation_text: + self._interaction_log.append( + f"Environment: {observation_text}" + ) + + tools = self._assistant_tools() + response = self._completion( + model=self.model, + messages=self.messages, + tools=tools if tools else None, + ) + + if response is None: + self.logger.error("A-Mem: LLM returned None response") + return None + + if response.usage: + self._cost.update_cost_from_tokens( + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls" and self._registry is not None: + tool_calls = self._extract_tool_calls(message) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": tc["arguments"], + }, + } + for tc in tool_calls + ], + ) + ) + actions = self._registry.tool_calls_to_action(tool_calls) + + for tc in tool_calls: + self._interaction_log.append( + f"Agent action: {tc['name']}({tc['arguments']})" + ) + + self.logger.info( + "A-Mem step %d: tool_calls=%s", + self._step_count, + [tc["name"] for tc in tool_calls], + ) + return actions + else: + content = message.content if message.content else "" + + if not content: + self.logger.warning( + "A-Mem step %d: empty content response (finish_reason=%s), " + "treating as agent inability to continue", + self._step_count, finish_reason, + ) + return None + + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", content=content + ) + ) + + self._interaction_log.append( + f"Agent says: {content}" + ) + + self.logger.info("A-Mem step %d: message response", self._step_count) + return MessageAction(arguments=Message(content=content)) + + def close(self) -> None: + + store = self._store + if store is None: + return + + if self._interaction_log: + self._store_session_memories() + + store.record_learning( + session_id=self.session_id, + task_id=str( + self.context.get("task_id", "") if self.context else "" + ), + memories_added=self._memories_added, + evolutions_triggered=self._evolutions_triggered, + summary=( + f"steps={self._step_count} " + f"interactions={len(self._interaction_log)} " + f"memories_added={self._memories_added} " + f"evolutions={self._evolutions_triggered}" + ), + benchmark_id=self.benchmark_id or "", + ) + + try: + cp = str(self.paths.agent_dir / "memory_checkpoint.json") + store.save_checkpoint(cp) + mt = str(self.paths.agent_dir / "memories.txt") + store.save_memories_text(mt) + except Exception as exc: + self.logger.warning("A-Mem: failed to save checkpoint: %s", exc) + + self.logger.info( + "A-Mem session closed: %d memories added, %d evolutions", + self._memories_added, + self._evolutions_triggered, + ) + + def get_cost(self) -> LiteLLMCostReport: + return self._cost + + def _build_system_prompt_with_memories(self) -> str: + + parts: List[str] = [ + "You are an expert agent that completes tasks using available tools.", + "Think step-by-step before acting.", + "Use available tools to interact with the environment.", + "When you are confident in your solution, use the finish/submit tool.", + ] + + store = self._store + if store is not None and store.memory_count > 0 and self.task: + query = self._generate_query_keywords(self.task) + memory_context = self._retrieve_memory_context(query) + if memory_context: + parts.append("") + parts.append( + "Based on the context below, complete the task. " + "Use the context to inform your decisions." + ) + parts.append("") + parts.append(f"Context:\n{memory_context}") + + return "\n".join(parts) + + def _generate_query_keywords(self, question: str) -> str: + + try: + prompt = GENERATE_QUERY_PROMPT.format(question=question) + response = self._memory_llm_call(prompt) + keywords = parse_keywords_response(response) + if keywords: + self.logger.info( + "A-Mem: generated query keywords: %s", keywords + ) + return keywords + except Exception as exc: + self.logger.warning( + "A-Mem: keyword extraction failed, using raw task: %s", exc + ) + return question + + def _retrieve_memory_context(self, query: str) -> str: + + store = self._store + if store is None: + return "" + + retrieved = store.find_related_with_neighbors(query, k=self.retrieve_k) + if not retrieved: + return "" + + + model_lower = self.model.lower() if self.model else "" + needs_budget = "gemini" in model_lower + + if needs_budget: + budget = 30000 + used = 0 + lines: List[str] = [] + for mem in retrieved: + content = mem.content + if len(content) > 5000: + content = content[:5000] + "... [truncated]" + entry = ( + f"memory content: {content} " + f"memory context: {mem.context} " + f"memory keywords: {mem.keywords} " + f"memory tags: {mem.tags}" + ) + if used + len(entry) > budget and lines: + break + lines.append(entry) + used += len(entry) + self.logger.info( + "A-Mem: injected %d/%d retrieved memories (%d chars, budget=%d)", + len(lines), len(retrieved), used, budget, + ) + else: + lines = [] + for mem in retrieved: + lines.append( + f"memory content: {mem.content} " + f"memory context: {mem.context} " + f"memory keywords: {mem.keywords} " + f"memory tags: {mem.tags}" + ) + + return "\n".join(lines) + + def _store_session_memories(self) -> None: + store = self._store + if store is None: + return + + session_content = "\n".join(self._interaction_log) + + if len(session_content.strip()) < 10: + return + + try: + note = MemoryNote.create_with_analysis( + content=session_content, + llm_call=self._memory_llm_call, + ) + + evolved = store.add_memory( + note=note, + llm_call=self._memory_llm_call, + ) + + self._memories_added += 1 + if evolved: + self._evolutions_triggered += 1 + + self.logger.info( + "A-Mem: stored session memory [%s] evolved=%s (total=%d)", + note.id[:8], + evolved, + store.memory_count, + ) + except Exception as exc: + self.logger.warning( + "A-Mem: failed to store session memory: %s", exc + ) + self._log_failure( + "session_memory_storage", exc, { + "content_preview": session_content[:500], + "interaction_count": len(self._interaction_log), + }, + ) + + def _add_message(self, message: Any) -> None: + self.logger.debug( + "Adding message: role=%s", getattr(message, "role", "?") + ) + self.messages.append(message) + + def _observe(self, observation: Optional[Observation]) -> Optional[str]: + + if observation is None: + return None + + observations = observation.to_observation_list() + if observation.is_empty(): + if not any(obs.invoking_actions for obs in observations): + return None + + collected_texts: List[str] = [] + + for obs in observations: + if isinstance(obs, MessageObservation) and isinstance( + obs.result, MessagePayload + ): + self._add_message( + ChatCompletionUserMessage( + role="user", content=obs.result.message + ) + ) + collected_texts.append(obs.result.message) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + text = str(obs) + self._add_message( + ChatCompletionUserMessage( + role="user", content=text + ) + ) + collected_texts.append(text) + continue + + action_id = invoking.id + tool_call_id = invoking.id + if not ( + isinstance(tool_call_id, str) + and tool_call_id.startswith("call_") + ): + if self._registry is not None: + tool_call_id = ( + self._registry.action_id_to_tool_call_id.get( + action_id, tool_call_id + ) + ) + + value = obs.result + try: + content = json.dumps( + value, ensure_ascii=False, separators=(",", ":") + ) + except TypeError: + content = str(value) + + if tool_call_id is not None: + self._add_message( + ChatCompletionToolMessage( + role="tool", + tool_call_id=tool_call_id, + content=content, + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", + content=f"Tool result: {content}", + ) + ) + collected_texts.append( + f"Result of {invoking.name}: " + f"{self._summarize_for_memory(content)}" + ) + else: + text = str(obs) + self._add_message( + ChatCompletionUserMessage( + role="user", content=text + ) + ) + collected_texts.append(text) + + if collected_texts: + return "\n".join(collected_texts) + return None + + def _summarize_for_memory(self, content: str) -> str: + + if len(content) < 2000: + return content + + try: + data = json.loads(content) + except (json.JSONDecodeError, ValueError): + if len(content) > 10000: + return content[:10000] + f"\n... [truncated, total {len(content)} chars]" + return content + + if isinstance(data, str): + try: + data = json.loads(data) + except (json.JSONDecodeError, ValueError): + if len(data) > 10000: + return data[:10000] + f"\n... [truncated, total {len(data)} chars]" + return data + + if isinstance(data, list) and data and isinstance(data[0], dict): + first = data[0] + snippet_key = None + if "snippet" in first: + snippet_key = "snippet" + elif "content" in first and "docid" in first: + snippet_key = "content" + + if snippet_key is not None: + summaries: List[str] = [] + for item in data: + docid = item.get("docid", "?") + score = item.get("score") + snippet = item.get(snippet_key, "") + title = "" + if isinstance(snippet, str) and snippet.startswith("---"): + title_match = re.search(r"title:\s*(.+)", snippet) + if title_match: + title = title_match.group(1).strip() + score_str = f" score:{score:.3f}" if isinstance(score, (int, float)) else "" + snippet_preview = snippet[:400].replace("\n", " ") if isinstance(snippet, str) else str(snippet)[:400] + summaries.append( + f"[doc:{docid}{score_str}] {title} | {snippet_preview}" + ) + return "\n".join(summaries) + + max_item_chars = 200 + summaries_generic: List[str] = [] + for i, item in enumerate(data): + item_str = json.dumps(item, ensure_ascii=False, separators=(",", ":")) + if len(item_str) > max_item_chars: + item_str = item_str[:max_item_chars] + "..." + summaries_generic.append(item_str) + result = f"[{len(data)} items]\n" + "\n".join(summaries_generic) + return result + + if isinstance(data, dict) and len(content) > 5000: + compact = json.dumps(data, ensure_ascii=False, separators=(",", ":")) + if len(compact) > 5000: + return compact[:5000] + f"... [truncated, total {len(compact)} chars]" + return compact + + return content + + def _assistant_tools(self) -> list[dict[str, Any]]: + if self._registry is None: + return [] + tools = self._registry.openai_tools() + if not self.enable_tool_shortlisting: + return tools + + def _cost_cb(usage): + if usage: + self._cost.update_cost_from_tokens( + usage.prompt_tokens, usage.completion_tokens + ) + + return shortlist_tools( + tools=tools, + max_selected=self.max_selected_tools, + messages=self.messages, + completion_fn=self._completion, + model=self.model, + logger=self.logger, + cost_callback=_cost_cb, + ) + + @staticmethod + def _extract_tool_calls(message: Any) -> list[dict[str, str]]: + if not hasattr(message, "tool_calls") or not message.tool_calls: + return [] + tool_calls = [] + for tc in message.tool_calls: + tool_calls.append({ + "name": tc.function.name, + "arguments": tc.function.arguments, + "id": tc.id, + }) + return tool_calls + + def _completion(self, **kwargs) -> Any: + call_kwargs = self._model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + if call_kwargs.get("tools") is None: + call_kwargs.pop("tools", None) + + max_attempts = 3 + for attempt in range(max_attempts): + try: + response = litellm.completion(**call_kwargs) + choice = response["choices"][0] if response.get("choices") else None + if choice: + msg = choice.get("message") or {} + has_content = bool(msg.get("content")) + has_tools = bool(msg.get("tool_calls")) + if not has_content and not has_tools: + if attempt + 1 < max_attempts: + self.logger.warning( + "A-Mem LLM call attempt %d/%d: empty response " + "(finish_reason=%s), retrying...", + attempt + 1, max_attempts, + choice.get("finish_reason"), + ) + time.sleep(2 ** attempt) + continue + return response + except Exception as exc: + self.logger.warning( + "A-Mem LLM call attempt %d/%d failed: %s", + attempt + 1, + max_attempts, + exc, + ) + if attempt + 1 >= max_attempts: + raise + time.sleep(2 ** attempt) + return None + + def _llm_call_simple( + self, + model: str, + prompt: str, + ) -> str: + kwargs: Dict[str, Any] = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.7, + "max_tokens": 1000, + } + + max_attempts = 3 + for attempt in range(max_attempts): + try: + resp = litellm.completion(**kwargs) + if resp.usage: + self._cost.update_cost_from_tokens( + resp.usage.prompt_tokens, + resp.usage.completion_tokens, + ) + content = resp.choices[0].message.content + if content is None: + raise ValueError("LLM returned None content") + return content + except Exception as exc: + self.logger.warning( + "A-Mem simple LLM call attempt %d/%d failed: %s", + attempt + 1, + max_attempts, + exc, + ) + if attempt + 1 >= max_attempts: + self._log_failure( + "llm_call", exc, { + "model": model, + "prompt_length": len(prompt), + "prompt_preview": prompt[:500], + "attempts": max_attempts, + }, + ) + raise + time.sleep(2 ** attempt) + return "" + + def _memory_llm_call(self, prompt: str) -> str: + return self._llm_call_simple(self.memory_model, prompt) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_note.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_note.py new file mode 100644 index 00000000..eccc5c52 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_note.py @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional + +from .prompts import ( + ANALYZE_CONTENT_PROMPT, + FOCUSED_KEYWORDS_PROMPT, + heuristic_context, + heuristic_keywords, + parse_analyze_content, + validate_analysis_result, + _parse_list_items, +) + +logger = logging.getLogger("amem") + + +@dataclass +class MemoryNote: + + content: str + id: str = "" + keywords: List[str] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + context: str = "General" + links: List[int] = field(default_factory=list) + importance_score: float = 1.0 + retrieval_count: int = 0 + timestamp: str = "" + last_accessed: str = "" + evolution_history: List[Dict[str, Any]] = field(default_factory=list) + category: str = "Uncategorized" + + def __post_init__(self) -> None: + if not self.id: + self.id = str(uuid.uuid4()) + current_time = datetime.now().strftime("%Y%m%d%H%M") + if not self.timestamp: + self.timestamp = current_time + if not self.last_accessed: + self.last_accessed = current_time + # Ensure context is a string + if isinstance(self.context, list): + self.context = " ".join(self.context) + + @staticmethod + def analyze_content( + content: str, + llm_call: Callable[[str], str], + ) -> Dict[str, Any]: + prompt = ANALYZE_CONTENT_PROMPT.format(content=content) + try: + response = llm_call(prompt) + analysis = parse_analyze_content(response, content) + + # Retry focused keywords if empty + if not analysis["keywords"]: + logger.info( + "Keywords empty after initial parse -- retrying with focused prompt" + ) + retry_prompt = FOCUSED_KEYWORDS_PROMPT.format(content=content) + retry_response = llm_call(retry_prompt) + analysis["keywords"] = _parse_list_items(retry_response) + + return validate_analysis_result(analysis, content) + + except Exception as e: + logger.error("Error analyzing content: %s", e) + return { + "keywords": heuristic_keywords(content), + "context": heuristic_context(content), + "tags": heuristic_keywords(content, 3), + } + + @classmethod + def create_with_analysis( + cls, + content: str, + llm_call: Callable[[str], str], + timestamp: Optional[str] = None, + importance_score: float = 1.0, + ) -> "MemoryNote": + analysis = cls.analyze_content(content, llm_call) + return cls( + content=content, + keywords=analysis["keywords"], + context=analysis["context"], + tags=analysis["tags"], + timestamp=timestamp or "", + importance_score=importance_score, + ) + + # ------------------------------------------------------------------ + # Serialization + # ------------------------------------------------------------------ + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "content": self.content, + "keywords": self.keywords, + "tags": self.tags, + "context": self.context, + "links": self.links, + "importance_score": self.importance_score, + "retrieval_count": self.retrieval_count, + "timestamp": self.timestamp, + "last_accessed": self.last_accessed, + "evolution_history": self.evolution_history, + "category": self.category, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MemoryNote": + return cls( + id=data.get("id", ""), + content=data.get("content", ""), + keywords=data.get("keywords", []), + tags=data.get("tags", []), + context=data.get("context", "General"), + links=data.get("links", []), + importance_score=data.get("importance_score", 1.0), + retrieval_count=data.get("retrieval_count", 0), + timestamp=data.get("timestamp", ""), + last_accessed=data.get("last_accessed", ""), + evolution_history=data.get("evolution_history", []), + category=data.get("category", "Uncategorized"), + ) + + def to_retrieval_document(self) -> str: + return ( + f"content:{self.content} " + f"context:{self.context} " + f"keywords: {', '.join(self.keywords)} " + f"tags: {', '.join(self.tags)}" + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_store.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_store.py new file mode 100644 index 00000000..f67e6066 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/memory_store.py @@ -0,0 +1,417 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import logging +import threading +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .memory_note import MemoryNote +from .prompts import ( + EVOLUTION_DECISION_PROMPT, + STRENGTHEN_DETAILS_PROMPT, + UPDATE_NEIGHBORS_PROMPT, + parse_evolution_decision, + parse_strengthen_details, + parse_update_neighbors, +) +from .retriever import EmbeddingRetriever + +logger = logging.getLogger("amem") + + +@dataclass +class LearningEvent: + + session_id: str + task_id: str + step: int + memories_added: int + evolutions_triggered: int + summary: str + benchmark_id: str = "" + + +class MemoryStore: + + _instances: Dict[str, "MemoryStore"] = {} + _global_lock = threading.Lock() + + @classmethod + def get_or_create( + cls, + shuffle_mode: str = "isolated", + task_group: Optional[str] = None, + benchmark_id: Optional[str] = None, + embedding_model: str = "all-MiniLM-L6-v2", + evo_threshold: int = 100, + ) -> "MemoryStore": + if shuffle_mode == "isolated": + bm = benchmark_id or task_group or "default" + key = f"amem_isolated_{bm}" + elif shuffle_mode == "sequential": + key = "amem_sequential_global" + elif shuffle_mode == "interleaved": + key = "amem_interleaved_global" + else: + raise ValueError(f"Unknown shuffle_mode: {shuffle_mode!r}") + + with cls._global_lock: + if key not in cls._instances: + cls._instances[key] = cls( + store_id=key, + embedding_model=embedding_model, + evo_threshold=evo_threshold, + ) + return cls._instances[key] + + @classmethod + def reset_all(cls) -> None: + with cls._global_lock: + cls._instances.clear() + + @classmethod + def list_stores(cls) -> Dict[str, "MemoryStore"]: + with cls._global_lock: + return dict(cls._instances) + + def __init__( + self, + store_id: str, + embedding_model: str = "all-MiniLM-L6-v2", + evo_threshold: int = 100, + ) -> None: + self.store_id = store_id + self._lock = threading.Lock() + + self._memories: Dict[str, MemoryNote] = {} + self._retriever = EmbeddingRetriever(embedding_model) + self._embedding_model = embedding_model + self._evo_threshold = evo_threshold + self._evo_cnt: int = 0 + + self._session_count: int = 0 + self._history: List[LearningEvent] = [] + self._benchmark_counts: Dict[str, int] = {} + + + @property + def memory_count(self) -> int: + with self._lock: + return len(self._memories) + + def get_all_memories(self) -> List[MemoryNote]: + with self._lock: + return list(self._memories.values()) + + + @property + def session_count(self) -> int: + with self._lock: + return self._session_count + + def increment_session(self) -> int: + with self._lock: + self._session_count += 1 + return self._session_count + + def add_memory( + self, + note: MemoryNote, + llm_call: Callable[[str], str], + ) -> bool: + with self._lock: + evolved = self._process_and_add(note, llm_call) + return evolved + + def find_related_with_neighbors( + self, + query: str, + k: int = 10, + ) -> List[MemoryNote]: + with self._lock: + if not self._memories: + return [] + indices = self._retriever.search(query, k) + all_memories = list(self._memories.values()) + seen: set[int] = set() + results: List[MemoryNote] = [] + + for i in indices: + if i >= len(all_memories) or i in seen: + continue + seen.add(i) + note = all_memories[i] + note.retrieval_count += 1 + note.last_accessed = datetime.now().strftime("%Y%m%d%H%M") + results.append(note) + for link_idx in note.links: + if link_idx < len(all_memories) and link_idx not in seen: + seen.add(link_idx) + linked = all_memories[link_idx] + linked.retrieval_count += 1 + results.append(linked) + + return results + + def _process_and_add( + self, + note: MemoryNote, + llm_call: Callable[[str], str], + ) -> bool: + neighbor_str, indices = self._find_neighbors_for_evolution( + note.content, k=5 + ) + + evolved = False + if indices: + try: + evolved = self._run_evolution(note, neighbor_str, indices, llm_call) + except Exception as e: + logger.error( + "Evolution failed for note %s: %s -- storing without evolution", + note.id[:8], + e, + ) + + self._memories[note.id] = note + self._retriever.add_documents([note.to_retrieval_document()]) + + if evolved: + self._evo_cnt += 1 + if self._evo_cnt % self._evo_threshold == 0: + self._consolidate() + + return evolved + + def _find_neighbors_for_evolution( + self, query: str, k: int = 5 + ) -> Tuple[str, List[int]]: + if not self._memories: + return "", [] + + indices = self._retriever.search(query, k) + all_memories = list(self._memories.values()) + memory_str = "" + for i in indices: + if i >= len(all_memories): + continue + m = all_memories[i] + memory_str += ( + f"memory index:{i}" + f"\t talk start time:{m.timestamp}" + f"\t memory content: {m.content}" + f"\t memory context: {m.context}" + f"\t memory keywords: {m.keywords}" + f"\t memory tags: {m.tags}\n" + ) + return memory_str, indices + + def _run_evolution( + self, + note: MemoryNote, + neighbor_str: str, + indices: List[int], + llm_call: Callable[[str], str], + ) -> bool: + decision_prompt = EVOLUTION_DECISION_PROMPT.format( + context=note.context, + content=note.content, + keywords=note.keywords, + nearest_neighbors_memories=neighbor_str, + ) + decision_response = llm_call(decision_prompt) + decision = parse_evolution_decision(decision_response) + logger.debug("Evolution decision: %s", decision) + + if decision["decision"] == "NO_EVOLUTION": + return False + + should_strengthen = decision["decision"] in ( + "STRENGTHEN", "STRENGTHEN_AND_UPDATE" + ) + should_update = decision["decision"] in ( + "UPDATE_NEIGHBOR", "STRENGTHEN_AND_UPDATE" + ) + + if should_strengthen: + strengthen_prompt = STRENGTHEN_DETAILS_PROMPT.format( + content=note.content, + keywords=note.keywords, + nearest_neighbors_memories=neighbor_str, + ) + strengthen_response = llm_call(strengthen_prompt) + strengthen = parse_strengthen_details(strengthen_response) + logger.debug("Strengthen details: %s", strengthen) + + note.links.extend(strengthen["connections"]) + if strengthen["tags"]: + note.tags = strengthen["tags"] + + if should_update: + update_prompt = UPDATE_NEIGHBORS_PROMPT.format( + content=note.content, + context=note.context, + nearest_neighbors_memories=neighbor_str, + max_neighbor_idx=len(indices) - 1, + neighbor_count=len(indices), + ) + update_response = llm_call(update_prompt) + neighbor_updates = parse_update_neighbors( + update_response, len(indices) + ) + logger.debug("Neighbor updates: %s", neighbor_updates) + + noteslist = list(self._memories.values()) + notes_id = list(self._memories.keys()) + for i in range(min(len(indices), len(neighbor_updates))): + upd = neighbor_updates[i] + memorytmp_idx = indices[i] + if memorytmp_idx >= len(noteslist): + continue + notetmp = noteslist[memorytmp_idx] + if upd["tags"]: + notetmp.tags = upd["tags"] + if upd["context"]: + notetmp.context = upd["context"] + self._memories[notes_id[memorytmp_idx]] = notetmp + + return True + + def _consolidate(self) -> None: + logger.info( + "Consolidating memory retriever (%d memories, %d evolutions)", + len(self._memories), + self._evo_cnt, + ) + documents = [m.to_retrieval_document() for m in self._memories.values()] + self._retriever.reset(documents) + + def record_learning( + self, + session_id: str, + task_id: str, + memories_added: int, + evolutions_triggered: int, + summary: str, + benchmark_id: str = "", + ) -> None: + with self._lock: + self._history.append( + LearningEvent( + session_id=session_id, + task_id=task_id, + step=self._session_count, + memories_added=memories_added, + evolutions_triggered=evolutions_triggered, + summary=summary[:500], + benchmark_id=benchmark_id, + ) + ) + if benchmark_id: + self._benchmark_counts[benchmark_id] = ( + self._benchmark_counts.get(benchmark_id, 0) + 1 + ) + + def save_checkpoint(self, path: str) -> None: + with self._lock: + payload = { + "store_id": self.store_id, + "memory_count": len(self._memories), + "evo_cnt": self._evo_cnt, + "session_count": self._session_count, + "history_len": len(self._history), + "benchmark_counts": dict(self._benchmark_counts), + "memories": { + mid: note.to_dict() + for mid, note in self._memories.items() + }, + } + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, ensure_ascii=False) + + def load_checkpoint(self, path: str) -> None: + with open(path, "r", encoding="utf-8") as fh: + payload = json.load(fh) + with self._lock: + self._evo_cnt = payload.get("evo_cnt", 0) + self._session_count = payload.get("session_count", 0) + self._benchmark_counts = payload.get("benchmark_counts", {}) + memories_data = payload.get("memories", {}) + self._memories = { + mid: MemoryNote.from_dict(mdata) + for mid, mdata in memories_data.items() + } + if self._memories: + documents = [ + m.to_retrieval_document() for m in self._memories.values() + ] + self._retriever.reset(documents) + + def save_memories_text(self, path: str) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with self._lock: + lines = [ + f"# A-Mem Memory Store: {self.store_id}", + f"# Memories: {len(self._memories)}", + f"# Sessions: {self._session_count}", + f"# Evolutions: {self._evo_cnt}", + "", + ] + for i, note in enumerate(self._memories.values()): + lines.append(f"--- Memory {i + 1} [{note.id[:8]}] ---") + lines.append(f"Content: {note.content}") + lines.append(f"Context: {note.context}") + lines.append(f"Keywords: {', '.join(note.keywords)}") + lines.append(f"Tags: {', '.join(note.tags)}") + links_str = ( + ", ".join(str(l) for l in note.links) if note.links else "none" + ) + lines.append(f"Links: {links_str}") + lines.append( + f"Importance: {note.importance_score:.2f} " + f"Retrieved: {note.retrieval_count} times" + ) + lines.append("") + + with open(path, "w", encoding="utf-8") as fh: + fh.write("\n".join(lines)) + + def get_stats(self) -> Dict[str, Any]: + with self._lock: + total = len(self._memories) + if total == 0: + return { + "total_memories": 0, + "total_evolutions": self._evo_cnt, + "avg_links": 0.0, + "avg_keywords": 0.0, + "avg_importance": 0.0, + "most_retrieved": 0, + } + links_count = sum( + len(m.links) for m in self._memories.values() + ) + kw_count = sum( + len(m.keywords) for m in self._memories.values() + ) + imp_sum = sum( + m.importance_score for m in self._memories.values() + ) + max_retr = max( + m.retrieval_count for m in self._memories.values() + ) + return { + "total_memories": total, + "total_evolutions": self._evo_cnt, + "avg_links": links_count / total, + "avg_keywords": kw_count / total, + "avg_importance": imp_sum / total, + "most_retrieved": max_retr, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/prompts.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/prompts.py new file mode 100644 index 00000000..8b640238 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/prompts.py @@ -0,0 +1,405 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import re +import logging +from typing import Any, Callable, Dict, List, Optional + +logger = logging.getLogger("amem") + +def strip_markdown_fences(text: str) -> str: + text = text.strip() + text = re.sub(r'^```(?:json)?\s*\n?', '', text, flags=re.MULTILINE) + text = re.sub(r'\n?\s*```$', '', text, flags=re.MULTILINE) + return text.strip() + + +def parse_with_json_fallback( + response: str, + plain_text_parser: Callable, + *parser_args, +) -> Any: + try: + cleaned = strip_markdown_fences(response) + result = json.loads(cleaned) + if isinstance(result, dict): + return result + except (json.JSONDecodeError, ValueError): + pass + return plain_text_parser(response, *parser_args) + +def _parse_list_items(text: str) -> List[str]: + if not text or not text.strip(): + return [] + + lines = text.strip().splitlines() + items: List[str] = [] + + for line in lines: + line = line.strip() + if not line: + continue + line = re.sub(r'^[\-\*\u2022]\s*', '', line) + line = re.sub(r'^\d+[\.\)]\s*', '', line) + line = line.strip().strip('"').strip("'").strip() + if not line: + continue + if ',' in line: + for part in line.split(','): + part = part.strip().strip('"').strip("'").strip() + if part: + items.append(part) + else: + items.append(line) + + return items + + +def _extract_section( + text: str, + marker: str, + next_markers: Optional[List[str]] = None, +) -> str: + pattern = re.compile( + rf'^\s*{re.escape(marker)}\s*:\s*(.*)$', + re.IGNORECASE | re.MULTILINE, + ) + match = pattern.search(text) + if not match: + return "" + + start = match.end() + first_line = match.group(1).strip() + + end = len(text) + if next_markers: + for nm in next_markers: + nm_pattern = re.compile( + rf'^\s*{re.escape(nm)}\s*:', re.IGNORECASE | re.MULTILINE + ) + nm_match = nm_pattern.search(text, start) + if nm_match and nm_match.start() < end: + end = nm_match.start() + + rest = text[start:end].strip() + if first_line and rest: + return first_line + "\n" + rest + return first_line or rest + + +_STOP_WORDS = frozenset({ + 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', + 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', + 'should', 'may', 'might', 'shall', 'can', 'need', 'dare', 'ought', + 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', + 'as', 'into', 'through', 'during', 'before', 'after', 'above', + 'below', 'between', 'out', 'off', 'over', 'under', 'again', + 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', + 'how', 'all', 'both', 'each', 'few', 'more', 'most', 'other', + 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', + 'than', 'too', 'very', 'just', 'because', 'but', 'and', 'or', + 'if', 'while', 'about', 'up', 'it', 'its', 'i', 'me', 'my', + 'you', 'your', 'he', 'she', 'they', 'we', 'this', 'that', 'these', + 'those', 'what', 'which', 'who', 'whom', 'says', 'said', 'speaker', +}) + + +def heuristic_keywords(content: str, max_keywords: int = 5) -> List[str]: + words = re.findall(r'\b[a-zA-Z]{3,}\b', content) + scored = [] + seen: set[str] = set() + for w in words: + w_lower = w.lower() + if w_lower in _STOP_WORDS or w_lower in seen: + continue + seen.add(w_lower) + score = 2 if w[0].isupper() else 1 + scored.append((w_lower, score)) + scored.sort(key=lambda x: -x[1]) + return [w for w, _ in scored[:max_keywords]] + + +def heuristic_context(content: str) -> str: + match = re.match(r'(.+?[.!?])\s', content) + if match: + return match.group(1).strip() + return content[:200].strip() + + +ANALYZE_CONTENT_PROMPT = """\ +Analyze the following content and provide: +1. KEYWORDS: The most important keywords (nouns, verbs, key concepts). \ +Order from most to least important. At least three keywords. \ +Do not include speaker names or time references. +2. CONTEXT: One sentence summarizing the main topic, key points, and purpose. +3. TAGS: Broad categories/themes for classification (domain, format, type). \ +At least three tags. + +Respond using EXACTLY this format (one section per header): + +KEYWORDS: keyword1, keyword2, keyword3, ... +CONTEXT: A single sentence summarizing the content. +TAGS: tag1, tag2, tag3, ... + +Content for analysis: +{content}""" + + +EVOLUTION_DECISION_PROMPT = """\ +You are an AI memory evolution agent. Analyze the new memory note and its \ +nearest neighbors to decide if evolution is needed. + +New memory: +- Context: {context} +- Content: {content} +- Keywords: {keywords} + +Nearest neighbor memories: +{nearest_neighbors_memories} + +Based on the relationships between the new memory and its neighbors, decide: +- NO_EVOLUTION: The memory stands alone, no changes needed. +- STRENGTHEN: The new memory should be linked to some neighbors and its tags updated. +- UPDATE_NEIGHBOR: The neighbors' context/tags should be updated based on new understanding. +- STRENGTHEN_AND_UPDATE: Both strengthen and update neighbors. + +Respond using EXACTLY this format: +DECISION: +REASON: """ + + +STRENGTHEN_DETAILS_PROMPT = """\ +Given the new memory and its neighbors, provide updated connections and tags. + +New memory: +- Content: {content} +- Keywords: {keywords} + +Neighbor memories: +{nearest_neighbors_memories} + +Which neighbor indices should the new memory connect to? \ +What tags best describe this memory? + +Respond using EXACTLY this format: +CONNECTIONS: 0, 2, 3 +TAGS: tag1, tag2, tag3, ...""" + + +UPDATE_NEIGHBORS_PROMPT = """\ +Given the new memory and its neighbor memories, update each neighbor's \ +context and tags based on a holistic understanding of all these memories together. + +New memory: +- Content: {content} +- Context: {context} + +Neighbor memories: +{nearest_neighbors_memories} + +For each neighbor (indexed 0 to {max_neighbor_idx}), provide updated context \ +and tags. If no change is needed, repeat the original values. + +Respond using EXACTLY this format (one block per neighbor): + +NEIGHBOR 0: +CONTEXT: updated context sentence +TAGS: tag1, tag2, tag3 + +NEIGHBOR 1: +CONTEXT: updated context sentence +TAGS: tag1, tag2, tag3 + +(continue for all {neighbor_count} neighbors)""" + + +FOCUSED_KEYWORDS_PROMPT = """\ +List exactly 5 keywords that capture the main concepts of the following text. \ +Output only the keywords, comma-separated, nothing else. + +Text: {content}""" + + +GENERATE_QUERY_PROMPT = """\ +Given the following question, generate several keywords separated by commas. + +Question: {question} + +Keywords:""" + + +def parse_analyze_content(response: str, content: str = "") -> Dict[str, Any]: + def _section_parse(resp: str, content_text: str = "") -> Dict[str, Any]: + kw_text = _extract_section(resp, "KEYWORDS", ["CONTEXT", "TAGS"]) + ctx_text = _extract_section(resp, "CONTEXT", ["TAGS", "KEYWORDS"]) + tags_text = _extract_section(resp, "TAGS", ["KEYWORDS", "CONTEXT"]) + return { + "keywords": _parse_list_items(kw_text), + "context": ctx_text.strip() if ctx_text.strip() else "", + "tags": _parse_list_items(tags_text), + } + + result = parse_with_json_fallback(response, _section_parse, content) + return validate_analysis_result(result, content) + + +def parse_evolution_decision(response: str) -> Dict[str, str]: + def _section_parse(resp: str) -> Dict[str, str]: + decision_text = _extract_section(resp, "DECISION", ["REASON"]) + reason_text = _extract_section(resp, "REASON", ["DECISION"]) + + decision = decision_text.strip().upper().replace(" ", "_") + valid_decisions = { + "NO_EVOLUTION", "STRENGTHEN", "UPDATE_NEIGHBOR", + "STRENGTHEN_AND_UPDATE", + } + if decision not in valid_decisions: + resp_upper = resp.upper() + if "STRENGTHEN" in resp_upper and "UPDATE" in resp_upper: + decision = "STRENGTHEN_AND_UPDATE" + elif "STRENGTHEN" in resp_upper: + decision = "STRENGTHEN" + elif "UPDATE" in resp_upper: + decision = "UPDATE_NEIGHBOR" + else: + decision = "NO_EVOLUTION" + return {"decision": decision, "reason": reason_text.strip()} + + result = parse_with_json_fallback(response, _section_parse) + + if "should_evolve" in result: + should_evolve = result.get("should_evolve", False) + actions = result.get("actions", []) + if not should_evolve: + decision = "NO_EVOLUTION" + elif "strengthen" in actions and "update_neighbor" in actions: + decision = "STRENGTHEN_AND_UPDATE" + elif "strengthen" in actions: + decision = "STRENGTHEN" + elif "update_neighbor" in actions: + decision = "UPDATE_NEIGHBOR" + else: + decision = "NO_EVOLUTION" + result = {"decision": decision, "reason": ""} + + if "decision" not in result: + result = {"decision": "NO_EVOLUTION", "reason": ""} + + return result + + +def parse_strengthen_details(response: str) -> Dict[str, Any]: + def _section_parse(resp: str) -> Dict[str, Any]: + conn_text = _extract_section(resp, "CONNECTIONS", ["TAGS"]) + tags_text = _extract_section(resp, "TAGS", ["CONNECTIONS"]) + connections = [] + for item in _parse_list_items(conn_text): + try: + connections.append(int(item.strip())) + except (ValueError, TypeError): + pass + return {"connections": connections, "tags": _parse_list_items(tags_text)} + + result = parse_with_json_fallback(response, _section_parse) + + if "suggested_connections" in result and "connections" not in result: + result["connections"] = [ + int(x) + for x in result.get("suggested_connections", []) + if isinstance(x, (int, float)) + ] + if "tags_to_update" in result and "tags" not in result: + result["tags"] = result.get("tags_to_update", []) + + result.setdefault("connections", []) + result.setdefault("tags", []) + return result + + +def parse_update_neighbors( + response: str, num_neighbors: int +) -> List[Dict[str, Any]]: + def _section_parse( + resp: str, n_neighbors: int + ) -> List[Dict[str, Any]]: + neighbors = [] + for i in range(n_neighbors): + pattern = re.compile(rf'NEIGHBOR\s+{i}\s*:', re.IGNORECASE) + match = pattern.search(resp) + if not match: + neighbors.append({"context": "", "tags": []}) + continue + next_pattern = re.compile( + rf'NEIGHBOR\s+{i + 1}\s*:', re.IGNORECASE + ) + next_match = next_pattern.search(resp, match.end()) + block_end = next_match.start() if next_match else len(resp) + block = resp[match.end():block_end] + ctx = _extract_section(block, "CONTEXT", ["TAGS"]) + tags_text = _extract_section(block, "TAGS", ["CONTEXT"]) + neighbors.append({ + "context": ctx.strip(), + "tags": _parse_list_items(tags_text), + }) + return neighbors + + try: + cleaned = strip_markdown_fences(response) + data = json.loads(cleaned) + if isinstance(data, dict): + contexts = data.get("new_context_neighborhood", []) + tags_list = data.get("new_tags_neighborhood", []) + neighbors = [] + for i in range(num_neighbors): + ctx = contexts[i] if i < len(contexts) else "" + tags = tags_list[i] if i < len(tags_list) else [] + neighbors.append({"context": ctx, "tags": tags}) + return neighbors + except (json.JSONDecodeError, ValueError): + pass + + return _section_parse(response, num_neighbors) + + +def validate_analysis_result( + result: Dict[str, Any], content: str = "" +) -> Dict[str, Any]: + if not isinstance(result, dict): + result = {"keywords": [], "context": "", "tags": []} + + keywords = result.get("keywords", []) + context = result.get("context", "") + tags = result.get("tags", []) + + if isinstance(keywords, str): + keywords = _parse_list_items(keywords) + if isinstance(tags, str): + tags = _parse_list_items(tags) + if isinstance(context, list): + context = " ".join(context) + + if not keywords and content: + keywords = heuristic_keywords(content) + if not context and content: + context = heuristic_context(content) + if not tags and keywords: + tags = keywords[:3] + + result["keywords"] = keywords + result["context"] = context + result["tags"] = tags + return result + + +def parse_keywords_response(response: str) -> str: + try: + cleaned = strip_markdown_fences(response) + data = json.loads(cleaned) + if isinstance(data, dict) and "keywords" in data: + return str(data["keywords"]) + except (json.JSONDecodeError, ValueError): + pass + return response.strip() diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/retriever.py b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/retriever.py new file mode 100644 index 00000000..f1fd2efb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/a_mem/retriever.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +from typing import List, Optional + +import numpy as np +from sentence_transformers import SentenceTransformer +from sklearn.metrics.pairwise import cosine_similarity + +logger = logging.getLogger("amem") + + +class EmbeddingRetriever: + + def __init__(self, model_name: str = "all-MiniLM-L6-v2") -> None: + self.model = SentenceTransformer(model_name) + self.corpus: List[str] = [] + self.embeddings: Optional[np.ndarray] = None + + def add_documents(self, documents: List[str]) -> None: + if not documents: + return + + if not self.corpus: + self.corpus = list(documents) + self.embeddings = self.model.encode(documents) + else: + self.corpus.extend(documents) + new_embeddings = self.model.encode(documents) + if self.embeddings is None: + self.embeddings = new_embeddings + else: + self.embeddings = np.vstack([self.embeddings, new_embeddings]) + + def reset(self, documents: List[str]) -> None: + self.corpus = [] + self.embeddings = None + if documents: + self.add_documents(documents) + + def search(self, query: str, k: int = 5) -> List[int]: + if not self.corpus or self.embeddings is None: + return [] + + query_embedding = self.model.encode([query])[0] + similarities = cosine_similarity( + [query_embedding], self.embeddings + )[0] + k = min(k, len(self.corpus)) + top_k_indices = np.argsort(similarities)[-k:][::-1] + return top_k_indices.tolist() diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/__init__.py new file mode 100644 index 00000000..d9dde690 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from .ace_agent import ACEAgent +from .bulletpoint_analyzer import BulletpointAnalyzer, DEDUP_AVAILABLE + +__all__ = ["ACEAgent", "BulletpointAnalyzer", "DEDUP_AVAILABLE"] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_agent.py new file mode 100644 index 00000000..094f1b33 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_agent.py @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar, Optional + +from pydantic import ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings +from ...utils.settings import RunnerName + + +class ACEAgent(Agent): + + display_name: ClassVar[str] = "ACE Agent" + slug_name: ClassVar[str] = "ace" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + model: str = "gpt-4o" + curator_model: Optional[str] = None + + max_num_rounds: int = 3 + curator_frequency: int = 1 + playbook_token_budget: int = 80000 + + shuffle_mode: str = "isolated" + + benchmark_id: Optional[str] = None + + initial_playbook: Optional[str] = None + initial_playbook_path: Optional[str] = None + + use_json_mode: bool = True + runner: RunnerName | None = None + model_settings: ModelSettings | None = None + + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + + use_bulletpoint_analyzer: bool = False + bulletpoint_analyzer_threshold: float = 0.90 + + @classmethod + def _get_instance_class(cls): + from .ace_instance import ACEAgentInstance + return ACEAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.ace.ace_instance:ACEAgentInstance" + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + pb = self.initial_playbook + if pb is None and self.initial_playbook_path: + with open(self.initial_playbook_path, "r", encoding="utf-8") as fh: + pb = fh.read() + + return { + "session_id": session_id, + "model": self.model, + "curator_model": self.curator_model or self.model, + "max_num_rounds": self.max_num_rounds, + "curator_frequency": self.curator_frequency, + "playbook_token_budget": self.playbook_token_budget, + "shuffle_mode": self.shuffle_mode, + "initial_playbook": pb, + "use_json_mode": self.use_json_mode, + "model_settings": self.model_settings, + "benchmark_id": self.benchmark_id, + "use_bulletpoint_analyzer": self.use_bulletpoint_analyzer, + "bulletpoint_analyzer_threshold": self.bulletpoint_analyzer_threshold, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + } + + @property + def model_name(self) -> str: + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: + names = [str(self.model)] + cm = self.curator_model or self.model + if cm != self.model: + names.append(str(cm)) + return names diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_instance.py new file mode 100644 index 00000000..5850c363 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/ace_instance.py @@ -0,0 +1,775 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import re +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +import litellm +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionSystemMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, +) +from ...utils.cost import LiteLLMCostReport +from ...utils.settings import get_settings + +from .playbook_store import PlaybookStore +from .playbook_utils import ( + apply_curator_operations, + extract_json_from_text, + get_playbook_stats, + update_bullet_counts, +) +from .prompts.curator import CURATOR_PROMPT_NO_GT +from .prompts.reflector import REFLECTOR_PROMPT_NO_GT +from .bulletpoint_analyzer import BulletpointAnalyzer, DEDUP_AVAILABLE +from ..tool_shortlisting import shortlist_tools + +try: + from ...agents.litellm_tool_calling.utils import ToolsActionsRegistry +except ImportError: + ToolsActionsRegistry = None + +_BULLET_ID_RE = re.compile(r"\[([a-z]{2,5}-\d{5})\]") + +settings = get_settings() + + +class ACEAgentInstance(AgentInstance): + + def __init__( + self, + session_id: str, + model: str = "gpt-4o", + curator_model: str = "gpt-4o", + max_num_rounds: int = 3, + curator_frequency: int = 1, + playbook_token_budget: int = 80000, + shuffle_mode: str = "isolated", + initial_playbook: Optional[str] = None, + use_json_mode: bool = True, + model_settings: Optional[ModelSettings] = None, + benchmark_id: Optional[str] = None, + use_bulletpoint_analyzer: bool = False, + bulletpoint_analyzer_threshold: float = 0.90, + enable_tool_shortlisting: bool = False, + max_selected_tools: int = 30, + ) -> None: + super().__init__(session_id) + + self.model = model + self.curator_model = curator_model + self.max_num_rounds = max_num_rounds + self.curator_frequency = curator_frequency + self.playbook_token_budget = playbook_token_budget + self.shuffle_mode = shuffle_mode + self.initial_playbook = initial_playbook + self.use_json_mode = use_json_mode + self.benchmark_id = benchmark_id + self.use_bulletpoint_analyzer = use_bulletpoint_analyzer + self.bulletpoint_analyzer_threshold = bulletpoint_analyzer_threshold + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + + if model_settings is None: + self._model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self._model_settings = model_settings + else: + self._model_settings = ModelSettings() + + self._cost = LiteLLMCostReport.initialize_empty(model_name=self.model) + self._store: Optional[PlaybookStore] = None + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ChatCompletionSystemMessage, + ] + ] = [] + self._step_count: int = 0 + + self._registry: Optional[ToolsActionsRegistry] = None + self._all_actions: list[ActionType] = [] + + self._observation_log: List[Dict[str, Any]] = [] + self._action_log: List[Dict[str, Any]] = [] + + def _log_failure( + self, component: str, error: Exception, context: Dict[str, Any] + ) -> None: + try: + log_path = self.paths.agent_dir / "ace_failures.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True) + entry = { + "timestamp": datetime.now().isoformat(), + "session_id": self.session_id, + "component": component, + "error_type": type(error).__name__, + "error_message": str(error)[:2000], + **{k: str(v)[:2000] if isinstance(v, str) else v + for k, v in context.items()}, + } + with open(log_path, "a", encoding="utf-8") as fh: + fh.write(json.dumps(entry, ensure_ascii=False) + "\n") + except Exception: + pass + + def _log_bullet_usage(self, bullet_ids: List[str]) -> None: + store = self._store + if store is None: + return + try: + log_path = self.paths.agent_dir / "bullet_usage_log.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True) + + from .playbook_utils import extract_playbook_bullets + bullets_text = extract_playbook_bullets(store.playbook, bullet_ids) + + entry = { + "timestamp": datetime.now().isoformat(), + "session_id": self.session_id, + "task_id": str( + self.context.get("task_id", "") if self.context else "" + ), + "benchmark_id": self.benchmark_id or "", + "store_id": store.store_id, + "session_count": store.session_count, + "bullet_ids_used": bullet_ids, + "bullet_count": len(bullet_ids), + "bullets_detail": bullets_text, + "total_steps": self._step_count, + "total_observations": len(self._observation_log), + "total_actions": len(self._action_log), + "question_preview": ( + self.task if self.task else "" + ), + } + with open(log_path, "a", encoding="utf-8") as fh: + fh.write(json.dumps(entry, ensure_ascii=False) + "\n") + except Exception: + pass + + def start( + self, + task: str, + context: Dict[str, Any], + actions: list[ActionType], + ) -> None: + super().start(task, context, actions) + + self._all_actions = list(self.actions) + if ToolsActionsRegistry is not None: + self._registry = ToolsActionsRegistry(self._all_actions) + + task_group = str( + context.get("task_group") + or context.get("task_id") + or context.get("task_name") + or "default" + ) + self._store = PlaybookStore.get_or_create( + shuffle_mode=self.shuffle_mode, + task_group=task_group, + initial_playbook=self.initial_playbook, + benchmark_id=self.benchmark_id, + ) + self._store.increment_session() + + playbook = self._store.playbook + system_content = self._build_system_prompt(playbook) + self._add_message( + ChatCompletionSystemMessage(role="system", content=system_content) + ) + + content_parts: list[Any] = [] + ctx = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message( + ChatCompletionUserMessage(role="user", content=text_content) + ) + + self.logger.info( + "ACE v2 instance started store=%s session_count=%d " + "playbook_bullets=%d benchmark=%s tools=%d", + self._store.store_id, + self._store.session_count, + get_playbook_stats(playbook)["total_bullets"], + self.benchmark_id or "(none)", + len(self._all_actions), + ) + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + + self._step_count += 1 + self._observe(observation) + self._log_observation(observation) + + tools = self._assistant_tools() + response = self._completion( + model=self.model, + messages=self.messages, + tools=tools if tools else None, + ) + + if response is None: + self.logger.error("ACE v2: LLM returned None response") + return None + + if response.usage: + self._cost.update_cost_from_tokens( + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls" and self._registry is not None: + tool_calls = self._extract_tool_calls(message) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": tc["arguments"], + }, + } + for tc in tool_calls + ], + ) + ) + actions = self._registry.tool_calls_to_action(tool_calls) + + for tc in tool_calls: + self._action_log.append({ + "step": self._step_count, + "action": tc["name"], + "arguments": tc["arguments"], + }) + + self.logger.info("ACE v2 step %d: tool_calls=%s", self._step_count, + [tc["name"] for tc in tool_calls]) + return actions + else: + content = message.content if message.content else "" + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", content=content + ) + ) + self._action_log.append({ + "step": self._step_count, + "action": "message", + "content": content, + }) + self.logger.info("ACE v2 step %d: message response", self._step_count) + return MessageAction(arguments=Message(content=content)) + + def close(self) -> None: + store = self._store + if store is None: + return + + bullet_ids = _BULLET_ID_RE.findall( + " ".join( + str(m.get("content", "")) if isinstance(m, dict) + else str(getattr(m, "content", "")) + for m in self.messages + if (isinstance(m, dict) and m.get("role") == "assistant") + or getattr(m, "role", "") == "assistant" + ) + ) + self._log_bullet_usage(bullet_ids) + + reflection_content = "(empty)" + if self._observation_log: + try: + reflection_content = self._run_post_session_reflection() + except Exception as exc: + self.logger.warning( + "ACE v2: post-session reflection failed: %s", exc + ) + self._log_failure("reflector", exc, { + "observation_count": len(self._observation_log), + "action_count": len(self._action_log), + }) + + if store.session_count % self.curator_frequency == 0: + try: + self._run_curator(reflection_content) + except Exception as exc: + self.logger.warning( + "ACE v2: curator failed: %s", exc + ) + self._log_failure("curator", exc, { + "reflection_preview": reflection_content[:500], + }) + + store.record_learning( + session_id=self.session_id, + task_id=str(self.context.get("task_id", "") if self.context else ""), + was_correct_before=False, + was_correct_after=False, + summary=( + f"steps={self._step_count} " + f"observations={len(self._observation_log)} " + f"actions={len(self._action_log)}" + ), + benchmark_id=self.benchmark_id or "", + ) + + try: + cp = str(self.paths.agent_dir / "playbook_checkpoint.json") + store.save_checkpoint(cp) + pb = str(self.paths.agent_dir / "playbook.txt") + store.save_playbook_text(pb) + except Exception as exc: + self.logger.warning("ACE v2: failed to save checkpoint: %s", exc) + + def get_cost(self) -> LiteLLMCostReport: + return self._cost + + def _build_system_prompt(self, playbook: str) -> str: + stats = get_playbook_stats(playbook) + has_content = stats["total_bullets"] > 0 + + parts = [ + "You are an expert agent that completes tasks using available tools.", + "You have access to a curated playbook of strategies and insights " + "learned from previous tasks. Use these to make better decisions.", + "", + "## Guidelines", + "- Read the playbook carefully and apply relevant strategies", + "- Pay attention to common mistakes listed and avoid them", + "- Use available tools to interact with the environment", + "- Think step-by-step before acting", + "- When you are confident in your solution, use the finish/submit tool", + "- When a playbook bullet influences your decision, mention its ID " + "(e.g. [err-00001]) in your reasoning text", + ] + + if has_content: + parts.extend([ + "", + "## Playbook (accumulated strategies & insights)", + "Each line has a bullet ID and usage stats " + "(helpful=N means it helped N times, harmful=N means it misled N times).", + "Prefer high-helpful, low-harmful bullets.", + "", + playbook, + ]) + else: + parts.extend([ + "", + "## Playbook", + "(No strategies accumulated yet. This is the first session.)", + ]) + + return "\n".join(parts) + + def _add_message(self, message: Any) -> None: + self.logger.debug("Adding message: role=%s", getattr(message, "role", "?")) + self.messages.append(message) + + def _observe(self, observation: Optional[Observation]) -> None: + if observation is None: + return + + observations = observation.to_observation_list() + if observation.is_empty(): + if not any(obs.invoking_actions for obs in observations): + return + + for obs in observations: + if isinstance(obs, MessageObservation) and isinstance( + obs.result, MessagePayload + ): + self._add_message( + ChatCompletionUserMessage( + role="user", content=obs.result.message + ) + ) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + continue + + action_id = invoking.id + tool_call_id = invoking.id + if not ( + isinstance(tool_call_id, str) + and tool_call_id.startswith("call_") + ): + if self._registry is not None: + tool_call_id = ( + self._registry.action_id_to_tool_call_id.get( + action_id, tool_call_id + ) + ) + + value = obs.result + try: + content = json.dumps( + value, ensure_ascii=False, separators=(",", ":") + ) + except TypeError: + content = str(value) + + if tool_call_id is not None: + self._add_message( + ChatCompletionToolMessage( + role="tool", + tool_call_id=tool_call_id, + content=content, + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", + content=f"Tool result: {content}", + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + + def _log_observation(self, observation: Optional[Observation]) -> None: + if observation is None or observation.is_empty(): + return + + for obs in observation.to_observation_list(): + result = obs.result + if result is None: + continue + + entry: Dict[str, Any] = {"step": self._step_count} + if isinstance(result, str): + entry["content"] = result + elif isinstance(result, dict): + entry["content"] = json.dumps(result, ensure_ascii=False) + else: + entry["content"] = str(result) + + if obs.invoking_actions: + entry["action"] = obs.invoking_actions[0].name + + self._observation_log.append(entry) + + def _assistant_tools(self) -> list[dict[str, Any]]: + if self._registry is None: + return [] + tools = self._registry.openai_tools() + if not self.enable_tool_shortlisting: + return tools + + def _cost_cb(usage): + if usage: + self._cost.update_cost_from_tokens( + usage.prompt_tokens, usage.completion_tokens + ) + + return shortlist_tools( + tools=tools, + max_selected=self.max_selected_tools, + messages=self.messages, + completion_fn=self._completion, + model=self.model, + logger=self.logger, + cost_callback=_cost_cb, + ) + + @staticmethod + def _extract_tool_calls(message: Any) -> list[dict[str, str]]: + if not hasattr(message, "tool_calls") or not message.tool_calls: + return [] + tool_calls = [] + for tc in message.tool_calls: + tool_calls.append({ + "name": tc.function.name, + "arguments": tc.function.arguments, + "id": tc.id, + }) + return tool_calls + + def _completion(self, **kwargs) -> Any: + call_kwargs = self._model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + if call_kwargs.get("tools") is None: + call_kwargs.pop("tools", None) + + max_attempts = 3 + for attempt in range(max_attempts): + try: + response = litellm.completion(**call_kwargs) + return response + except Exception as exc: + self.logger.warning( + "ACE LLM call attempt %d/%d failed: %s", + attempt + 1, + max_attempts, + exc, + ) + if attempt + 1 >= max_attempts: + raise + time.sleep(2 ** attempt) + return None + + def _llm_call_simple( + self, + model: str, + prompt: str, + *, + json_mode: bool = False, + ) -> str: + kwargs: Dict[str, Any] = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.0, + } + if json_mode: + kwargs["response_format"] = {"type": "json_object"} + + max_attempts = 3 + for attempt in range(max_attempts): + try: + resp = litellm.completion(**kwargs) + if resp.usage: + self._cost.update_cost_from_tokens( + resp.usage.prompt_tokens, + resp.usage.completion_tokens, + ) + content = resp.choices[0].message.content + if content is None: + raise ValueError("LLM returned None content") + return content + except Exception as exc: + self.logger.warning( + "ACE simple LLM call attempt %d/%d failed: %s", + attempt + 1, + max_attempts, + exc, + ) + if attempt + 1 >= max_attempts: + self._log_failure( + "llm_call", exc, { + "model": model, + "prompt_length": len(prompt), + "prompt_preview": prompt[:500], + "attempts": max_attempts, + }, + ) + raise + time.sleep(2 ** attempt) + return "" + + def _run_post_session_reflection(self) -> str: + store = self._store + assert store is not None + + playbook = store.playbook + session_trace = self._build_session_trace() + + prompt = REFLECTOR_PROMPT_NO_GT.format( + question=self.task if self.task else "", + reasoning_trace=session_trace, + predicted_answer="(see session trace above)", + bullets_used=playbook, + ) + + raw = self._llm_call_simple( + self.model, prompt, json_mode=self.use_json_mode + ) + + bullet_tags: List[Dict] = [] + reflection_text = raw + parsed = extract_json_from_text(raw) + if parsed and isinstance(parsed, dict): + bullet_tags = parsed.get("bullet_tags", []) + reflection_text = parsed.get("reasoning", raw) + else: + self.logger.warning( + "ACE Reflector: JSON parse failed, raw length=%d", len(raw) + ) + self._log_failure( + "reflector_parse", ValueError("JSON parse failed"), { + "raw_response_preview": raw[:1000], + }, + ) + + if bullet_tags: + store.playbook = update_bullet_counts(store.playbook, bullet_tags) + + self.logger.info( + "ACE post-session reflection: %d bullet tags updated", + len(bullet_tags), + ) + + return reflection_text + + def _build_session_trace(self) -> str: + events: List[Dict[str, Any]] = [] + for entry in self._action_log: + events.append({"type": "action", **entry}) + for entry in self._observation_log: + events.append({"type": "observation", **entry}) + + events.sort(key=lambda e: (e.get("step", 0), 0 if e["type"] == "action" else 1)) + + lines: List[str] = [] + for event in events: + step = event.get("step", "?") + if event["type"] == "action": + action = event.get("action", "?") + args = event.get("arguments", event.get("content", "")) + lines.append(f"[Step {step}] Action: {action}") + if args: + lines.append(f" Args: {str(args)}") + else: + action = event.get("action", "env") + content = event.get("content", "") + lines.append(f"[Step {step}] Observation from {action}:") + lines.append(f" {content}") + + return "\n".join(lines) if lines else "(No session trace recorded)" + + def _run_curator(self, reflection_content: str) -> None: + store = self._store + assert store is not None + + playbook = store.playbook + stats = get_playbook_stats(playbook) + + question_context = self.task if self.task else "" + if self.context: + question_context += "".join( + f"\n<{k}>\n{v}\n" + for k, v in self.context.items() + ) + + prompt = CURATOR_PROMPT_NO_GT.format( + token_budget=self.playbook_token_budget, + current_step=store.session_count, + total_samples="ongoing", + playbook_stats=json.dumps(stats, indent=2), + recent_reflection=reflection_content, + current_playbook=playbook, + question_context=question_context, + ) + + raw = self._llm_call_simple( + self.curator_model, prompt, json_mode=self.use_json_mode + ) + + if raw.startswith("INCORRECT_DUE_TO_EMPTY_RESPONSE"): + self.logger.warning("ACE Curator: skipping due to empty response") + self._log_failure( + "curator_empty_response", ValueError("empty LLM response"), { + "session_count": store.session_count, + }, + ) + return + + parsed = extract_json_from_text(raw) + if parsed and isinstance(parsed, dict): + if "operations" not in parsed or not isinstance(parsed["operations"], list): + self.logger.warning("ACE Curator: missing or invalid 'operations' field") + self._log_failure( + "curator_schema", ValueError("missing 'operations' list"), { + "raw_response_preview": raw[:1000], + "parsed_keys": list(parsed.keys()), + }, + ) + return + + ops = parsed["operations"] + valid_ops = [] + for op in ops: + if not isinstance(op, dict) or "type" not in op: + continue + if op["type"] == "ADD": + if "section" in op and "content" in op: + valid_ops.append(op) + else: + valid_ops.append(op) + + if valid_ops: + new_playbook, new_id = apply_curator_operations( + playbook, valid_ops, store.next_global_id + ) + store.playbook = new_playbook + store.next_global_id = new_id + self.logger.info("ACE Curator: applied %d operations", len(valid_ops)) + + if self.use_bulletpoint_analyzer and DEDUP_AVAILABLE: + self.logger.info( + "ACE BulletpointAnalyzer: running (threshold=%.2f)", + self.bulletpoint_analyzer_threshold, + ) + analyzer = BulletpointAnalyzer( + llm_merge_fn=lambda p: self._llm_call_simple( + self.curator_model, p, json_mode=False + ), + ) + store.playbook = analyzer.analyze( + playbook=store.playbook, + threshold=self.bulletpoint_analyzer_threshold, + merge=True, + ) + else: + self.logger.warning("ACE Curator: failed to parse response") + self._log_failure( + "curator_parse", ValueError("JSON parse failed"), { + "raw_response_preview": raw[:1000], + }, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/bulletpoint_analyzer.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/bulletpoint_analyzer.py new file mode 100644 index 00000000..30636587 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/bulletpoint_analyzer.py @@ -0,0 +1,200 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import re +from typing import Any, Dict, List, Optional, Tuple + +try: + import numpy as np + from sentence_transformers import SentenceTransformer + import faiss + + DEDUP_AVAILABLE = True +except ImportError: + DEDUP_AVAILABLE = False + np = None + +from .playbook_utils import parse_playbook_line, format_playbook_line + + +class BulletpointAnalyzer: + + def __init__( + self, + llm_merge_fn: Optional[Any] = None, + embedding_model_name: str = "all-mpnet-base-v2", + ) -> None: + self.llm_merge_fn = llm_merge_fn + self.embedding_model_name = embedding_model_name + self._embedding_model: Optional[Any] = None + + def _load_embedding_model(self) -> None: + if self._embedding_model is None and DEDUP_AVAILABLE: + self._embedding_model = SentenceTransformer(self.embedding_model_name) + + @staticmethod + def _parse_playbook( + playbook: str, + ) -> Tuple[List[str], List[Dict[str, Any]], Dict[int, int]]: + lines = playbook.strip().split("\n") + bullets: List[Dict[str, Any]] = [] + bullet_line_mapping: Dict[int, int] = {} + + for line_idx, line in enumerate(lines): + parsed = parse_playbook_line(line) + if parsed: + parsed["line_number"] = line_idx + 1 + parsed["original_line"] = line + bullet_index = len(bullets) + bullet_line_mapping[bullet_index] = line_idx + bullets.append(parsed) + + return lines, bullets, bullet_line_mapping + + def _compute_embeddings(self, bullets: List[Dict[str, Any]]) -> Any: + if not DEDUP_AVAILABLE: + raise RuntimeError("Cannot compute embeddings without sentence-transformers") + self._load_embedding_model() + contents = [b["content"] for b in bullets] + embeddings = self._embedding_model.encode( + contents, convert_to_numpy=True, show_progress_bar=False + ) + faiss.normalize_L2(embeddings) + return embeddings + + @staticmethod + def _find_similar_groups( + bullets: List[Dict[str, Any]], + embeddings: Any, + threshold: float, + ) -> List[Dict[str, Any]]: + similarity_matrix = np.dot(embeddings, embeddings.T) + duplicate_groups: List[Dict[str, Any]] = [] + visited: set[int] = set() + + for i in range(len(bullets)): + if i in visited: + continue + similar_indices = [] + for j in range(i + 1, len(bullets)): + if similarity_matrix[i, j] >= threshold: + similar_indices.append(j) + if similar_indices: + group = [i] + similar_indices + duplicate_groups.append( + {"indices": group, "bullets": [bullets[idx] for idx in group]} + ) + visited.update(group) + + return duplicate_groups + + def _merge_bullets_with_llm( + self, bullets_group: List[Dict[str, Any]] + ) -> Optional[Dict[str, Any]]: + if len(bullets_group) == 1: + return bullets_group[0] + + if self.llm_merge_fn is None: + return bullets_group[0] + + bullets_text = "\n".join( + f"{i+1}. [{b['id']}] helpful={b['helpful']} harmful={b['harmful']} :: {b['content']}" + for i, b in enumerate(bullets_group) + ) + total_helpful = sum(b["helpful"] for b in bullets_group) + total_harmful = sum(b["harmful"] for b in bullets_group) + base_id = bullets_group[0]["id"] + + prompt = ( + f"You are merging similar playbook bulletpoints into a single, " + f"comprehensive entry.\n\n" + f"Given these similar bulletpoints:\n{bullets_text}\n\n" + f"Merge them into ONE bulletpoint that captures all important " + f"information while removing redundancy.\n\n" + f"Requirements:\n" + f"1. Keep the ID from the first entry: [{base_id}]\n" + f"2. Use combined counts: helpful={total_helpful} harmful={total_harmful}\n" + f"3. Combine the content to be comprehensive but concise\n" + f"4. Output ONLY in this format: [{base_id}] helpful={total_helpful} " + f"harmful={total_harmful} :: [merged content]\n\n" + f"Do NOT include any explanation, just output the merged bulletpoint." + ) + + try: + merged_content = self.llm_merge_fn(prompt).strip() + pattern = r"\[([^\]]+)\]\s+helpful=(\d+)\s+harmful=(\d+)\s+::\s+(.+)" + match = re.match(pattern, merged_content) + if match: + bullet_id, helpful, harmful, content = match.groups() + return { + "id": bullet_id, + "helpful": int(helpful), + "harmful": int(harmful), + "content": content.strip(), + "original_line": format_playbook_line( + bullet_id, int(helpful), int(harmful), content.strip() + ), + "is_merged": True, + "original_count": len(bullets_group), + } + else: + return bullets_group[0] + except Exception: + return bullets_group[0] + + def analyze( + self, + playbook: str, + threshold: float = 0.90, + merge: bool = True, + ) -> str: + if not DEDUP_AVAILABLE: + return playbook + + original_lines, bullets, bullet_line_mapping = self._parse_playbook(playbook) + + if len(bullets) == 0: + return playbook + + embeddings = self._compute_embeddings(bullets) + duplicate_groups = self._find_similar_groups(bullets, embeddings, threshold) + + if len(duplicate_groups) == 0: + return playbook + + merge_mapping: Dict[int, Dict[str, Any]] = {} + processed_indices: set[int] = set() + + if merge: + for group in duplicate_groups: + indices = group["indices"] + merged_bullet = self._merge_bullets_with_llm(group["bullets"]) + if merged_bullet: + merge_mapping[indices[0]] = merged_bullet + processed_indices.update(indices) + else: + for group in duplicate_groups: + indices = group["indices"] + processed_indices.update(indices[1:]) + + output_lines: List[str] = [] + for line_idx, original_line in enumerate(original_lines): + current_bullet_idx = None + for bi, li in bullet_line_mapping.items(): + if li == line_idx: + current_bullet_idx = bi + break + + if current_bullet_idx is not None: + if current_bullet_idx in merge_mapping: + output_lines.append(merge_mapping[current_bullet_idx]["original_line"]) + elif current_bullet_idx in processed_indices: + continue + else: + output_lines.append(original_line) + else: + output_lines.append(original_line) + + return "\n".join(output_lines) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_store.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_store.py new file mode 100644 index 00000000..61bf26ce --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_store.py @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import threading +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional + + +DEFAULT_PLAYBOOK = """\ +## STRATEGIES & INSIGHTS + +## FORMULAS & CALCULATIONS + +## CODE SNIPPETS & TEMPLATES + +## COMMON MISTAKES TO AVOID + +## PROBLEM-SOLVING HEURISTICS + +## CONTEXT CLUES & INDICATORS + +## OTHERS""" + + +@dataclass +class LearningEvent: + session_id: str + task_id: str + step: int + was_correct_before: bool + was_correct_after: bool + summary: str + benchmark_id: str = "" + + +class PlaybookStore: + + _instances: Dict[str, "PlaybookStore"] = {} + _global_lock = threading.Lock() + + @classmethod + def get_or_create( + cls, + shuffle_mode: str = "isolated", + task_group: Optional[str] = None, + initial_playbook: Optional[str] = None, + benchmark_id: Optional[str] = None, + ) -> "PlaybookStore": + if shuffle_mode == "isolated": + bm = benchmark_id or task_group or "default" + key = f"ace_isolated_{bm}" + elif shuffle_mode == "sequential": + key = "ace_sequential_global" + elif shuffle_mode == "interleaved": + key = "ace_interleaved_global" + else: + raise ValueError(f"Unknown shuffle_mode: {shuffle_mode!r}") + + with cls._global_lock: + if key not in cls._instances: + cls._instances[key] = cls( + store_id=key, + initial_playbook=initial_playbook or DEFAULT_PLAYBOOK, + ) + return cls._instances[key] + + @classmethod + def reset_all(cls) -> None: + with cls._global_lock: + cls._instances.clear() + + @classmethod + def list_stores(cls) -> Dict[str, "PlaybookStore"]: + with cls._global_lock: + return dict(cls._instances) + + def __init__(self, store_id: str, initial_playbook: str) -> None: + self.store_id = store_id + self._lock = threading.Lock() + self._playbook: str = initial_playbook + self._next_global_id: int = 1 + self._session_count: int = 0 + self._history: List[LearningEvent] = [] + self._benchmark_counts: Dict[str, int] = {} + + @property + def playbook(self) -> str: + with self._lock: + return self._playbook + + @playbook.setter + def playbook(self, value: str) -> None: + with self._lock: + self._playbook = value + + @property + def next_global_id(self) -> int: + with self._lock: + return self._next_global_id + + @next_global_id.setter + def next_global_id(self, value: int) -> None: + with self._lock: + self._next_global_id = value + + @property + def session_count(self) -> int: + with self._lock: + return self._session_count + + def increment_session(self) -> int: + with self._lock: + self._session_count += 1 + return self._session_count + + def record_learning( + self, + session_id: str, + task_id: str, + was_correct_before: bool, + was_correct_after: bool, + summary: str, + benchmark_id: str = "", + ) -> None: + with self._lock: + self._history.append( + LearningEvent( + session_id=session_id, + task_id=task_id, + step=self._session_count, + was_correct_before=was_correct_before, + was_correct_after=was_correct_after, + summary=summary[:500], + benchmark_id=benchmark_id, + ) + ) + if benchmark_id: + self._benchmark_counts[benchmark_id] = ( + self._benchmark_counts.get(benchmark_id, 0) + 1 + ) + + def save_checkpoint(self, path: str) -> None: + with self._lock: + payload = { + "store_id": self.store_id, + "playbook": self._playbook, + "next_global_id": self._next_global_id, + "session_count": self._session_count, + "history_len": len(self._history), + "benchmark_counts": dict(self._benchmark_counts), + } + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, ensure_ascii=False) + + def load_checkpoint(self, path: str) -> None: + with open(path, "r", encoding="utf-8") as fh: + payload = json.load(fh) + with self._lock: + self._playbook = payload["playbook"] + self._next_global_id = payload["next_global_id"] + self._session_count = payload.get("session_count", 0) + self._benchmark_counts = payload.get("benchmark_counts", {}) + + def save_playbook_text(self, path: str) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(self.playbook) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_utils.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_utils.py new file mode 100644 index 00000000..fa1f915b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/playbook_utils.py @@ -0,0 +1,230 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import re +from typing import Any, Dict, List, Optional, Tuple + +_SLUG_MAP = { + "strategies_and_insights": "str", + "formulas_and_calculations": "calc", + "code_snippets_and_templates": "code", + "common_mistakes_to_avoid": "err", + "problem_solving_heuristics": "prob", + "context_clues_and_indicators": "ctx", + "others": "misc", + "meta_strategies": "meta", +} + + +def get_section_slug(section_name: str) -> str: + clean = section_name.lower().strip().replace(" ", "_").replace("&", "and") + if clean in _SLUG_MAP: + return _SLUG_MAP[clean] + words = clean.split("_") + if len(words) == 1: + return words[0][:4] + return "".join(w[0] for w in words[:5]) + + +_LINE_RE = re.compile( + r"\[([^\]]+)\]\s*helpful=(\d+)\s*harmful=(\d+)\s*::\s*(.*)" +) + + +def parse_playbook_line(line: str) -> Optional[Dict[str, Any]]: + m = _LINE_RE.match(line.strip()) + if m: + return { + "id": m.group(1), + "helpful": int(m.group(2)), + "harmful": int(m.group(3)), + "content": m.group(4), + "raw_line": line, + } + return None + + +def format_playbook_line( + bullet_id: str, helpful: int, harmful: int, content: str +) -> str: + return f"[{bullet_id}] helpful={helpful} harmful={harmful} :: {content}" + +def update_bullet_counts(playbook_text: str, bullet_tags: List[Dict]) -> str: + tag_map: Dict[str, str] = {} + for tag in bullet_tags: + if not isinstance(tag, dict): + continue + bid = tag.get("id") or tag.get("bullet", "") + tval = tag.get("tag", "neutral") + if bid: + tag_map[bid] = tval + + if not tag_map: + return playbook_text + + lines = playbook_text.split("\n") + updated: List[str] = [] + for line in lines: + parsed = parse_playbook_line(line) + if parsed and parsed["id"] in tag_map: + t = tag_map[parsed["id"]] + if t == "helpful": + parsed["helpful"] += 1 + elif t == "harmful": + parsed["harmful"] += 1 + updated.append( + format_playbook_line( + parsed["id"], parsed["helpful"], parsed["harmful"], parsed["content"] + ) + ) + else: + updated.append(line) + return "\n".join(updated) + + +def apply_curator_operations( + playbook_text: str, + operations: List[Dict[str, Any]], + next_id: int, +) -> Tuple[str, int]: + lines = playbook_text.split("\n") + sections: Dict[str, int] = {} + for i, line in enumerate(lines): + if line.strip().startswith("##"): + header = line.strip()[2:].strip() + norm = header.lower().replace(" ", "_").replace("&", "and") + sections[norm] = i + + bullets_to_add: List[Tuple[str, str]] = [] + + for op in operations: + if op.get("type") != "ADD": + continue + section_raw = op.get("section", "others") + section_norm = section_raw.lower().replace(" ", "_").replace("&", "and") + if section_norm not in sections: + section_norm = "others" + + slug = get_section_slug(section_norm) + new_id = f"{slug}-{next_id:05d}" + next_id += 1 + content = op.get("content", "") + new_line = format_playbook_line(new_id, 0, 0, content) + bullets_to_add.append((section_norm, new_line)) + + final: List[str] = [] + current_section: Optional[str] = None + + for line in lines: + if line.strip().startswith("##"): + if current_section is not None: + for sec, bline in bullets_to_add: + if sec == current_section: + final.append(bline) + bullets_to_add = [ + (s, b) for s, b in bullets_to_add if s != current_section + ] + header = line.strip()[2:].strip() + current_section = header.lower().replace(" ", "_").replace("&", "and") + final.append(line) + + if current_section is not None: + for sec, bline in bullets_to_add: + if sec == current_section: + final.append(bline) + bullets_to_add = [(s, b) for s, b in bullets_to_add if s != current_section] + + for _, bline in bullets_to_add: + final.append(bline) + + return "\n".join(final), next_id + +def get_playbook_stats(playbook_text: str) -> Dict[str, Any]: + stats: Dict[str, Any] = { + "total_bullets": 0, + "high_performing": 0, + "problematic": 0, + "unused": 0, + "by_section": {}, + } + current_section = "general" + for line in playbook_text.split("\n"): + if line.strip().startswith("##"): + current_section = line.strip()[2:].strip() + continue + parsed = parse_playbook_line(line) + if parsed: + stats["total_bullets"] += 1 + h, d = parsed["helpful"], parsed["harmful"] + if h > 5 and d < 2: + stats["high_performing"] += 1 + elif d >= h and d > 0: + stats["problematic"] += 1 + elif h + d == 0: + stats["unused"] += 1 + sec = stats["by_section"].setdefault( + current_section, {"count": 0, "helpful": 0, "harmful": 0} + ) + sec["count"] += 1 + sec["helpful"] += h + sec["harmful"] += d + return stats + +def extract_playbook_bullets( + playbook_text: str, bullet_ids: List[str] +) -> str: + if not bullet_ids: + return "(No bullets used by generator)" + found: List[str] = [] + for line in playbook_text.split("\n"): + parsed = parse_playbook_line(line) + if parsed and parsed["id"] in bullet_ids: + found.append( + format_playbook_line( + parsed["id"], parsed["helpful"], parsed["harmful"], parsed["content"] + ) + ) + return "\n".join(found) if found else "(No matching bullets found)" + + +def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]: + try: + return json.loads(text.strip()) + except json.JSONDecodeError: + pass + + for m in re.finditer(r"```json\s*(.*?)\s*```", text, re.DOTALL | re.I): + try: + return json.loads(m.group(1).strip()) + except json.JSONDecodeError: + continue + + i = 0 + while i < len(text): + if text[i] == "{": + depth, start = 1, i + i += 1 + while i < len(text) and depth > 0: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + elif text[i] == '"': + i += 1 + while i < len(text) and text[i] != '"': + if text[i] == "\\": + i += 1 + i += 1 + i += 1 + if depth == 0: + try: + return json.loads(text[start:i]) + except json.JSONDecodeError: + pass + else: + i += 1 + + return None diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/__init__.py new file mode 100644 index 00000000..125af47f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/__init__.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from .reflector import REFLECTOR_PROMPT_NO_GT +from .curator import CURATOR_PROMPT_NO_GT + +__all__ = [ + "REFLECTOR_PROMPT_NO_GT", + "CURATOR_PROMPT_NO_GT", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/curator.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/curator.py new file mode 100644 index 00000000..02c217ff --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/curator.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +CURATOR_PROMPT_NO_GT = """\ +You are a master curator of knowledge. Your job is to identify what new insights \ +should be added to an existing playbook based on a reflection from a previous attempt. + +**Context:** +- The playbook you created will be used to help answering similar questions. +- The reflection is generated using environment feedback that will NOT be available \ +when the playbook is being used. + +**CRITICAL: You MUST respond with valid JSON only. Do not use markdown formatting or code blocks.** + +**Instructions:** +- Review the existing playbook and the reflection from the previous attempt +- Identify ONLY the NEW insights, strategies, or mistakes that are MISSING from the current playbook +- Avoid redundancy - if similar advice already exists, only add new content that is a perfect complement to the existing playbook +- Do NOT regenerate the entire playbook - only provide the additions needed +- Focus on quality over quantity - a focused, well-organized playbook is better than an exhaustive one +- Format your response as a PURE JSON object with specific sections +- For any operation if no new content to add, return an empty list for the operations field +- Be concise and specific - each addition should be actionable + + +**Training Context:** +- Total token budget: {token_budget} tokens +- Training progress: Sample {current_step} out of {total_samples} + +**Current Playbook Stats:** +{playbook_stats} + +**Recent Reflection:** +{recent_reflection} + +**Current Playbook:** +{current_playbook} + +**Question Context:** +{question_context} + +**Your Task:** +Output ONLY a valid JSON object with these exact fields: +- reasoning: your chain of thought / reasoning / thinking process +- operations: a list of operations to be performed on the playbook + - type: the type of operation to be performed + - section: the section to add the bullet to + - content: the new content of the bullet + +**Available Operations:** +1. ADD: Create new bullet points with fresh IDs + - section: the section to add the new bullet to + - content: the new content of the bullet. Note: no need to include the bullet_id \ +in the content like '[ctx-00263] helpful=1 harmful=0 ::', the bullet_id will be added by the system. + +**RESPONSE FORMAT - Output ONLY this JSON structure (no markdown, no code blocks):** +{{ + "reasoning": "[Your reasoning here]", + "operations": [ + {{ + "type": "ADD", + "section": "formulas_and_calculations", + "content": "[New calculation method...]" + }} + ] +}} +""" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/reflector.py b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/reflector.py new file mode 100644 index 00000000..e2b3a2e0 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/ace/prompts/reflector.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +REFLECTOR_PROMPT_NO_GT = """\ +You are an expert analyst and educator. Your job is to analyze a model's \ +reasoning process and identify potential issues or strengths based on the \ +reasoning trace alone. + +**Instructions:** +- Carefully analyze the model's reasoning trace to evaluate its approach +- The reasoning trace includes both the model's actions and environment observations in chronological order +- Identify potential conceptual errors, calculation mistakes, or misapplied strategies +- Also note what the model did well +- Provide actionable insights that could help the model perform better in future tasks +- Focus on the root cause, not just surface-level observations +- Be specific about what could be improved +- You will receive the full playbook that was available to the agent. +- Based on the reasoning trace, infer which bullets the agent likely applied or was influenced by, and tag each relevant bullet as 'helpful', 'harmful', or 'neutral'. Skip unrelated bullets. + +Your output should be a json object, which contains the following fields + - reasoning: your chain of thought / reasoning / thinking process, detailed analysis and calculations + - error_identification: what potential issues exist in the reasoning? (or "none identified" if the approach appears sound) + - root_cause_analysis: why might these issues occur? What concept may have been misunderstood? + - correct_approach: what could the model do differently or better? + - key_insight: what strategy, formula, or principle should be remembered for future tasks? + - bullet_tags: a list of json objects with bullet id and tag for each relevant playbook bullet + + +**Question:** +{question} + +**Model's Reasoning Trace:** +{reasoning_trace} + +**Model's Predicted Answer:** +{predicted_answer} + +**Full Playbook:** +{bullets_used} + +**Answer in this exact JSON format:** +{{ + "reasoning": "[Your chain of thought / reasoning / thinking process]", + "error_identification": "[What potential issues exist in the reasoning?]", + "root_cause_analysis": "[Why might these issues occur?]", + "correct_approach": "[What could the model do differently or better?]", + "key_insight": "[What strategy or principle should be remembered?]", + "bullet_tags": [ + {{"id": "calc-00001", "tag": "helpful"}}, + {{"id": "fin-00002", "tag": "harmful"}} + ] +}} +""" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/__init__.py new file mode 100644 index 00000000..17ea7313 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from .autoskill_agent import AutoSkillAgent +from .skill_store import SkillStore + +__all__ = ["AutoSkillAgent", "SkillStore"] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_agent.py new file mode 100644 index 00000000..ec0d498a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_agent.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar, Optional + +from pydantic import ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings +from ...utils.settings import RunnerName + + +class AutoSkillAgent(Agent): + + display_name: ClassVar[str] = "AutoSkill Agent" + slug_name: ClassVar[str] = "autoskill" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + + model: str = "gpt-4o" + skill_model: Optional[str] = None + + retrieve_k: int = 3 + retrieval_threshold: float = 0.4 + bm25_weight: float = 0.1 + dedupe_similarity_threshold: float = 0.4 + embedding_model: str = "all-MiniLM-L6-v2" + enable_query_rewrite: bool = True + max_context_chars: int = 6000 + + shuffle_mode: str = "isolated" + + benchmark_id: Optional[str] = None + + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + + runner: RunnerName | None = None + model_settings: ModelSettings | None = None + + @classmethod + def _get_instance_class(cls): + from .autoskill_instance import AutoSkillAgentInstance + return AutoSkillAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.autoskill.autoskill_instance:AutoSkillAgentInstance" + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "skill_model": self.skill_model or self.model, + "retrieve_k": self.retrieve_k, + "retrieval_threshold": self.retrieval_threshold, + "bm25_weight": self.bm25_weight, + "dedupe_similarity_threshold": self.dedupe_similarity_threshold, + "embedding_model": self.embedding_model, + "enable_query_rewrite": self.enable_query_rewrite, + "max_context_chars": self.max_context_chars, + "shuffle_mode": self.shuffle_mode, + "model_settings": self.model_settings, + "benchmark_id": self.benchmark_id, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + } + + @property + def model_name(self) -> str: + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: + names = [str(self.model)] + sm = self.skill_model or self.model + if sm != self.model: + names.append(str(sm)) + return names diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_instance.py new file mode 100644 index 00000000..b2d9bff2 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/autoskill_instance.py @@ -0,0 +1,636 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +import litellm +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionSystemMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, +) +from ...utils.cost import LiteLLMCostReport +from ...utils.settings import get_settings + +from .prompts import QUERY_REWRITE_PROMPT, SKILL_CONTEXT_TEMPLATE, SKILL_ENTRY_TEMPLATE +from .skill_extraction import extract_skills_from_trace +from .skill_maintenance import maintain_skill +from .skill_retrieval import compute_embedding, hybrid_search +from .skill_store import SkillStore + +try: + from ...agents.litellm_tool_calling.utils import ToolCall, ToolsActionsRegistry +except ImportError: + ToolsActionsRegistry = None + ToolCall = dict + +from ..tool_shortlisting import shortlist_tools + +settings = get_settings() + + +class AutoSkillAgentInstance(AgentInstance): + + def __init__( + self, + session_id: str, + model: str = "gpt-4o", + skill_model: str = "gpt-4o", + retrieve_k: int = 5, + retrieval_threshold: float = 0.3, + bm25_weight: float = 0.1, + dedupe_similarity_threshold: float = 0.4, + embedding_model: str = "text-embedding-3-small", + enable_query_rewrite: bool = True, + max_context_chars: int = 6000, + shuffle_mode: str = "isolated", + model_settings: Optional[ModelSettings] = None, + benchmark_id: Optional[str] = None, + enable_tool_shortlisting: bool = False, + max_selected_tools: int = 30, + ) -> None: + super().__init__(session_id) + + self.model = model + self.skill_model = skill_model + self.retrieve_k = retrieve_k + self.retrieval_threshold = retrieval_threshold + self.bm25_weight = bm25_weight + self.dedupe_similarity_threshold = dedupe_similarity_threshold + self.embedding_model = embedding_model + self.enable_query_rewrite = enable_query_rewrite + self.max_context_chars = max_context_chars + self.shuffle_mode = shuffle_mode + self.benchmark_id = benchmark_id + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + + if model_settings is None: + self._model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self._model_settings = model_settings + else: + self._model_settings = ModelSettings() + + self._cost = LiteLLMCostReport.initialize_empty(model_name=self.model) + self._store: Optional[SkillStore] = None + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ChatCompletionSystemMessage, + ] + ] = [] + self._step_count: int = 0 + + self._registry: Optional[ToolsActionsRegistry] = None + self._all_actions: list[ActionType] = [] + + self._observation_log: List[Dict[str, Any]] = [] + self._action_log: List[Dict[str, Any]] = [] + + def _log_failure( + self, component: str, error: Exception, context: Dict[str, Any] + ) -> None: + try: + log_path = self.paths.agent_dir / "autoskill_failures.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True) + entry = { + "timestamp": datetime.now().isoformat(), + "session_id": self.session_id, + "component": component, + "error_type": type(error).__name__, + "error_message": str(error)[:2000], + **{k: str(v)[:2000] if isinstance(v, str) else v + for k, v in context.items()}, + } + with open(log_path, "a", encoding="utf-8") as fh: + fh.write(json.dumps(entry, ensure_ascii=False) + "\n") + except Exception: + pass + + def start( + self, + task: str, + context: Dict[str, Any], + actions: list[ActionType], + ) -> None: + super().start(task, context, actions) + + self._all_actions = list(self.actions) + if ToolsActionsRegistry is not None: + self._registry = ToolsActionsRegistry(self._all_actions) + + task_group = str( + context.get("task_group") + or context.get("task_id") + or context.get("task_name") + or "default" + ) + self._store = SkillStore.get_or_create( + shuffle_mode=self.shuffle_mode, + task_group=task_group, + benchmark_id=self.benchmark_id, + ) + self._store.increment_session() + + skill_context = self._retrieve_skills(task, context) + + system_content = self._build_system_prompt(skill_context) + self._add_message( + ChatCompletionSystemMessage(role="system", content=system_content) + ) + + content_parts: list[Any] = [] + ctx = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message( + ChatCompletionUserMessage(role="user", content=text_content) + ) + + self.logger.info( + "AutoSkill instance started store=%s session_count=%d " + "skill_count=%d benchmark=%s tools=%d", + self._store.store_id, + self._store.session_count, + self._store.skill_count, + self.benchmark_id or "(none)", + len(self._all_actions), + ) + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + self._step_count += 1 + + self._observe(observation) + self._log_observation(observation) + + tools = self._assistant_tools() + response = self._completion( + model=self.model, + messages=self.messages, + tools=tools if tools else None, + ) + + if response is None: + self.logger.error("AutoSkill: LLM returned None response") + return None + + if response.usage: + self._cost.update_cost_from_tokens( + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls" and self._registry is not None: + tool_calls = self._extract_tool_calls(message) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": tc["arguments"], + }, + } + for tc in tool_calls + ], + ) + ) + actions = self._registry.tool_calls_to_action(tool_calls) + + for tc in tool_calls: + self._action_log.append({ + "step": self._step_count, + "action": tc["name"], + "arguments": tc["arguments"], + }) + + self.logger.info("AutoSkill step %d: tool_calls=%s", self._step_count, + [tc["name"] for tc in tool_calls]) + return actions + else: + content = message.content if message.content else "" + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", content=content + ) + ) + self._action_log.append({ + "step": self._step_count, + "action": "message", + "content": content, + }) + self.logger.info("AutoSkill step %d: message response", self._step_count) + return MessageAction(arguments=Message(content=content)) + + def close(self) -> None: + store = self._store + if store is None: + return + + action_taken = "no_extraction" + skill_name = "" + + if self._observation_log or self._action_log: + try: + session_trace = self._build_session_trace() + candidates = extract_skills_from_trace( + task=self.task if self.task else "", + benchmark_id=self.benchmark_id or "", + session_trace=session_trace, + llm_call=self._llm_call_simple, + model=self.skill_model, + logger=self.logger, + ) + + if candidates: + candidate = candidates[0] + action_taken, _ = maintain_skill( + candidate=candidate, + store=store, + llm_call=self._llm_call_simple, + model=self.skill_model, + embedding_model=self.embedding_model, + bm25_weight=self.bm25_weight, + dedupe_similarity_threshold=self.dedupe_similarity_threshold, + logger=self.logger, + ) + skill_name = candidate.name + self.logger.info( + "AutoSkill close: action=%s skill=%s", + action_taken, skill_name, + ) + + except Exception as exc: + self.logger.warning("AutoSkill: skill extraction/maintenance failed: %s", exc) + self._log_failure("skill_evolution", exc, { + "observation_count": len(self._observation_log), + "action_count": len(self._action_log), + }) + + store.record_learning( + session_id=self.session_id, + task_id=str(self.context.get("task_id", "") if self.context else ""), + benchmark_id=self.benchmark_id or "", + action=action_taken, + skill_name=skill_name, + ) + + try: + cp = str(self.paths.agent_dir / "skillstore_checkpoint.json") + store.save_checkpoint(cp) + txt = str(self.paths.agent_dir / "skillbank.txt") + store.save_skills_text(txt) + except Exception as exc: + self.logger.warning("AutoSkill: failed to save checkpoint: %s", exc) + + def get_cost(self) -> LiteLLMCostReport: + return self._cost + + def _retrieve_skills(self, task: str, context: Dict[str, Any]) -> str: + store = self._store + if store is None or store.skill_count == 0: + return "" + + query = task + if self.enable_query_rewrite and task: + try: + ctx_parts = [] + for _, v in context.items(): + if isinstance(v, str): + ctx_parts.append(v) + ctx_str = " ".join(ctx_parts) + rewritten = self._llm_call_simple( + self.skill_model, + QUERY_REWRITE_PROMPT.format(task=task, context=ctx_str), + ) + if rewritten and len(rewritten.strip()) > 5: + query = rewritten.strip() + self.logger.info("AutoSkill retrieval: query rewritten to '%s'", query[:100]) + except Exception as exc: + self.logger.debug("AutoSkill retrieval: query rewrite failed: %s", exc) + + query_embedding = None + try: + query_embedding = compute_embedding(query, model=self.embedding_model) + except Exception as exc: + self.logger.warning("AutoSkill retrieval: failed to compute query embedding: %s", exc) + + results = hybrid_search( + store=store, + query=query, + query_embedding=query_embedding, + top_k=self.retrieve_k, + threshold=self.retrieval_threshold, + bm25_weight=self.bm25_weight, + embedding_model=self.embedding_model, + ) + + if not results: + self.logger.info("AutoSkill retrieval: no skills above threshold %.2f", self.retrieval_threshold) + return "" + + self.logger.info( + "AutoSkill retrieval: %d skills retrieved (top score=%.3f)", + len(results), results[0][1], + ) + + skills_block = "" + char_budget = self.max_context_chars + for skill, _ in results: + entry_text = SKILL_ENTRY_TEMPLATE.format( + name=skill.name, + description=skill.description, + tags=", ".join(skill.tags), + triggers=", ".join(skill.triggers), + instructions=skill.instructions, + ) + if len(skills_block) + len(entry_text) > char_budget: + break + skills_block += entry_text + "\n" + + return SKILL_CONTEXT_TEMPLATE.format(skills_block=skills_block) + + def _build_system_prompt(self, skill_context: str) -> str: + parts = [ + "You are an expert agent that completes tasks using available tools.", + "Think step-by-step before acting.", + "Use available tools to interact with the environment.", + "When you are confident in your solution, use the finish/submit tool.", + ] + + if skill_context: + parts.extend(["", skill_context]) + else: + parts.extend([ + "", + "## Skills", + "(No accumulated skills yet. This is an early session.)", + ]) + + return "\n".join(parts) + + def _add_message(self, message: Any) -> None: + self.messages.append(message) + + def _observe(self, observation: Optional[Observation]) -> None: + if observation is None: + return + + observations = observation.to_observation_list() + if observation.is_empty(): + if not any(obs.invoking_actions for obs in observations): + return + + for obs in observations: + if isinstance(obs, MessageObservation) and isinstance( + obs.result, MessagePayload + ): + self._add_message( + ChatCompletionUserMessage( + role="user", content=obs.result.message + ) + ) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + continue + + action_id = invoking.id + tool_call_id = invoking.id + if not ( + isinstance(tool_call_id, str) + and tool_call_id.startswith("call_") + ): + if self._registry is not None: + tool_call_id = ( + self._registry.action_id_to_tool_call_id.get( + action_id, tool_call_id + ) + ) + + value = obs.result + try: + content = json.dumps( + value, ensure_ascii=False, separators=(",", ":") + ) + except TypeError: + content = str(value) + + if tool_call_id is not None: + self._add_message( + ChatCompletionToolMessage( + role="tool", + tool_call_id=tool_call_id, + content=content, + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", + content=f"Tool result: {content}", + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + + def _log_observation(self, observation: Optional[Observation]) -> None: + if observation is None or observation.is_empty(): + return + + for obs in observation.to_observation_list(): + result = obs.result + if result is None: + continue + + entry: Dict[str, Any] = {"step": self._step_count} + if isinstance(result, str): + entry["content"] = result + elif isinstance(result, dict): + entry["content"] = json.dumps(result, ensure_ascii=False) + else: + entry["content"] = str(result) + + if obs.invoking_actions: + entry["action"] = obs.invoking_actions[0].name + + self._observation_log.append(entry) + + def _assistant_tools(self) -> list[dict[str, Any]]: + if self._registry is None: + return [] + tools = self._registry.openai_tools() + if not self.enable_tool_shortlisting: + return tools + + def _cost_cb(usage): + if usage: + self._cost.update_cost_from_tokens( + usage.prompt_tokens, usage.completion_tokens + ) + + return shortlist_tools( + tools=tools, + max_selected=self.max_selected_tools, + messages=self.messages, + completion_fn=self._completion, + model=self.model, + logger=self.logger, + cost_callback=_cost_cb, + ) + + @staticmethod + def _extract_tool_calls(message: Any) -> list[dict[str, str]]: + if not hasattr(message, "tool_calls") or not message.tool_calls: + return [] + tool_calls = [] + for tc in message.tool_calls: + tool_calls.append({ + "name": tc.function.name, + "arguments": tc.function.arguments, + "id": tc.id, + }) + return tool_calls + + def _completion(self, **kwargs) -> Any: + call_kwargs = self._model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + if call_kwargs.get("tools") is None: + call_kwargs.pop("tools", None) + + max_attempts = 3 + for attempt in range(max_attempts): + try: + response = litellm.completion(**call_kwargs) + return response + except Exception as exc: + self.logger.warning( + "AutoSkill LLM call attempt %d/%d failed: %s", + attempt + 1, max_attempts, exc, + ) + if attempt + 1 >= max_attempts: + raise + time.sleep(2 ** attempt) + return None + + def _llm_call_simple( + self, + model: str, + prompt: str, + *, + json_mode: bool = False, + ) -> str: + kwargs: Dict[str, Any] = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.0, + } + if json_mode: + kwargs["response_format"] = {"type": "json_object"} + + max_attempts = 3 + for attempt in range(max_attempts): + try: + resp = litellm.completion(**kwargs) + if resp.usage: + self._cost.update_cost_from_tokens( + resp.usage.prompt_tokens, + resp.usage.completion_tokens, + ) + content = resp.choices[0].message.content + if content is None: + raise ValueError("LLM returned None content") + return content + except Exception as exc: + self.logger.warning( + "AutoSkill simple LLM call attempt %d/%d failed: %s", + attempt + 1, max_attempts, exc, + ) + if attempt + 1 >= max_attempts: + self._log_failure("llm_call", exc, { + "model": model, + "prompt_length": len(prompt), + "attempts": max_attempts, + }) + raise + time.sleep(2 ** attempt) + return "" + + def _build_session_trace(self) -> str: + events: List[Dict[str, Any]] = [] + for entry in self._action_log: + events.append({"type": "action", **entry}) + for entry in self._observation_log: + events.append({"type": "observation", **entry}) + + events.sort(key=lambda e: (e.get("step", 0), 0 if e["type"] == "action" else 1)) + + lines: List[str] = [] + for event in events: + step = event.get("step", "?") + if event["type"] == "action": + action = event.get("action", "?") + args = event.get("arguments", event.get("content", "")) + lines.append(f"[Step {step}] Action: {action}") + if args: + lines.append(f" Args: {str(args)}") + else: + action = event.get("action", "env") + content = event.get("content", "") + lines.append(f"[Step {step}] Observation from {action}:") + lines.append(f" {content}") + + return "\n".join(lines) if lines else "(No session trace recorded)" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/prompts.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/prompts.py new file mode 100644 index 00000000..eebe79fd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/prompts.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +QUERY_REWRITE_PROMPT = """\ +You are a retrieval query rewriter. Your job is to rewrite the current user task \ +into a concise, standalone search query for skill retrieval. + +Core rules: +- Produce exactly ONE line of output: the rewritten query. +- Resolve references ("it", "this", "the above") using the provided context. +- Keep only retrieval-relevant constraints (format, audience, quality, domain). +- Preserve the task anchor (what the task is about). +- Do NOT include generic process words without a concrete topic anchor. + +Task: {task} +Context: {context} + +Rewritten query:""" + + +SKILL_EXTRACTION_PROMPT = """\ +You are a skill extractor that turns agent interaction traces into reusable skills. + +## Extraction Principles +- Treat the task description and environment observations as primary evidence. +- Extract ONLY when there are durable, reusable constraints, policies, workflows, \ +or strategies that would help in FUTURE similar tasks. +- Do NOT extract one-shot task-specific facts or generic "be helpful" patterns. +- Capture HOW TO DO similar tasks, rather than this-instance facts. +- Remove case-specific entities (names, URLs, dates) and preserve only portable rules. +- Do NOT invent workflow steps unless explicitly demonstrated in the trace. +- If nothing reusable is found, return an empty skills list. + +## Session Information +Task: {task} + +## Session Trace (Actions & Observations) +{session_trace} + +## Output Format +Return a JSON object with this schema: +{{ + "skills": [ + {{ + "name": "", + "description": "", + "instructions": "", + "triggers": ["", "", ...], + "tags": ["", "", ...], + "confidence": + }} + ] +}} + +If nothing reusable is detected, return: {{"skills": []}} +""" + +SKILL_JUDGE_PROMPT = """\ +You are a skill set manager. Given a newly extracted skill candidate and the most \ +similar existing skill from the skill bank, decide the appropriate action. + +## Decision Procedure +1. Check if the candidate represents the same capability as the existing skill \ +(same job-to-be-done, same deliverable type, overlapping constraints). +2. Apply discard gate: reject generic, low-signal, non-portable candidates. +3. Compare on four axes: job-to-be-done, deliverable type, hard constraints/success \ +criteria, and required tools/workflow. +4. Choose "merge" ONLY when they are the same capability after removing instance details. +5. Choose "add" when the candidate is a distinct durable capability. +6. Choose "discard" when the candidate is too generic or non-reusable. + +## Candidate Skill +Name: {candidate_name} +Description: {candidate_description} +Instructions: {candidate_instructions} +Triggers: {candidate_triggers} +Tags: {candidate_tags} + +## Most Similar Existing Skill (may be empty if no skills exist) +Name: {existing_name} +Description: {existing_description} +Instructions: {existing_instructions} +Triggers: {existing_triggers} +Tags: {existing_tags} +Similarity Score: {similarity_score} + +## Output Format +Return a JSON object: +{{ + "action": "add" | "merge" | "discard", + "target_skill_id": "", + "reason": "" +}} +""" + +SKILL_MERGE_PROMPT = """\ +You are a skill merger. Combine an existing skill with a new candidate into one \ +improved skill that preserves the best of both. + +## Merge Rules +- Preserve the original capability identity (name and core goal). +- Perform semantic union rather than raw concatenation. +- Import only reusable, non-conflicting additions from the candidate. +- Avoid regressions: keep important checks from the existing skill. +- Remove case-specific entities and one-off facts. +- Do NOT invent any new standards or details not present in either skill. +- Deduplicate sections, bullets, triggers, tags. +- Keep language consistent across all fields. + +## Existing Skill +Name: {existing_name} +Description: {existing_description} +Instructions: {existing_instructions} +Triggers: {existing_triggers} +Tags: {existing_tags} + +## Candidate Skill (new evidence) +Name: {candidate_name} +Description: {candidate_description} +Instructions: {candidate_instructions} +Triggers: {candidate_triggers} +Tags: {candidate_tags} + +## Output Format +Return a JSON object with the merged skill: +{{ + "name": "", + "description": "", + "instructions": "", + "triggers": ["", ...], + "tags": ["", ...] +}} +""" + +SKILL_CONTEXT_TEMPLATE = """\ +## Retrieved Skills (from accumulated experience) +The following skills were retrieved based on relevance to the current task. \ +Use a skill ONLY when it directly matches the current intent. \ +Otherwise, ignore all retrieved skills and act normally. \ +Never explicitly mention that skills were retrieved/injected. + +{skills_block} +""" + +SKILL_ENTRY_TEMPLATE = """\ +### Skill: {name} +- **Description**: {description} +- **Tags**: {tags} +- **Triggers**: {triggers} + +**Instructions**: +{instructions} +""" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_extraction.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_extraction.py new file mode 100644 index 00000000..4478b4b4 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_extraction.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import re +import uuid +from typing import Any, Callable, Dict, List, Optional + +from .prompts import SKILL_EXTRACTION_PROMPT +from .skill_store import SkillEntry + + +def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]: + text = text.strip() + try: + return json.loads(text) + except (json.JSONDecodeError, ValueError): + pass + + match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL) + if match: + try: + return json.loads(match.group(1).strip()) + except (json.JSONDecodeError, ValueError): + pass + + start = text.find("{") + if start >= 0: + depth = 0 + for i in range(start, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + try: + return json.loads(text[start:i + 1]) + except (json.JSONDecodeError, ValueError): + break + return None + + +def _repair_json_via_llm( + raw_text: str, + llm_call: Callable[[str, str], str], + model: str, +) -> Optional[Dict[str, Any]]: + repair_prompt = ( + "The following text was supposed to be valid JSON matching the schema " + '{"skills": [{"name": str, "description": str, "instructions": str, ' + '"triggers": [str], "tags": [str], "confidence": float}]} ' + "but it is malformed. Please fix it and return ONLY valid JSON. " + 'If extraction fails, output {"skills": []}.\n\n' + f"Malformed text:\n{raw_text[:4000]}" + ) + try: + repaired = llm_call(model, repair_prompt) + if repaired: + return extract_json_from_text(repaired) + except Exception: + pass + return None + + +def extract_skills_from_trace( + task: str, + benchmark_id: str, + session_trace: str, + llm_call: Callable[[str, str], str], + model: str, + logger: Optional[Any] = None, +) -> List[SkillEntry]: + import logging + _logger = logger or logging.getLogger(__name__) + + prompt = SKILL_EXTRACTION_PROMPT.format( + task=task, + session_trace=session_trace, + ) + + _logger.info( + "AutoSkill extraction: task='%s', trace_len=%d chars", + task[:80], len(session_trace), + ) + + raw = llm_call(model, prompt) + if not raw: + _logger.info("AutoSkill extraction: LLM returned empty response") + return [] + + _logger.debug("AutoSkill extraction: raw LLM response length=%d", len(raw)) + + parsed = extract_json_from_text(raw) + if parsed is None: + _logger.info("AutoSkill extraction: levels 1-3 JSON parse failed, attempting LLM repair") + parsed = _repair_json_via_llm(raw, llm_call, model) + if not parsed or not isinstance(parsed, dict): + _logger.warning("AutoSkill extraction: all 4 JSON recovery levels failed") + return [] + + skills_data = parsed.get("skills", []) + if not isinstance(skills_data, list): + _logger.warning("AutoSkill extraction: 'skills' field is not a list") + return [] + + _logger.info("AutoSkill extraction: LLM returned %d skill candidates", len(skills_data)) + + results: List[SkillEntry] = [] + for item in skills_data: + if not isinstance(item, dict): + continue + + name = item.get("name", "").strip() + description = item.get("description", "").strip() + instructions = item.get("instructions", "").strip() + confidence = float(item.get("confidence", 0.6)) + + if not name or not description: + _logger.debug("AutoSkill extraction: skipping candidate with empty name/description") + continue + if not instructions: + instructions = description + + entry = SkillEntry( + id=str(uuid.uuid4()), + name=name, + description=description, + instructions=instructions, + triggers=item.get("triggers", [])[:8], + tags=item.get("tags", [])[:8], + confidence=confidence, + ) + _logger.info( + "AutoSkill extraction: extracted skill '%s' (confidence=%.2f)", + name, confidence, + ) + results.append(entry) + + return results diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_maintenance.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_maintenance.py new file mode 100644 index 00000000..fdc1ecd1 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_maintenance.py @@ -0,0 +1,207 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +from typing import Any, Callable, Optional, Tuple + +from .prompts import SKILL_JUDGE_PROMPT, SKILL_MERGE_PROMPT +from .skill_extraction import extract_json_from_text +from .skill_retrieval import hybrid_search, compute_embedding +from .skill_store import SkillEntry, SkillStore + + +def judge_skill( + candidate: SkillEntry, + existing: Optional[SkillEntry], + similarity_score: float, + llm_call: Callable[[str, str], str], + model: str, +) -> Tuple[str, Optional[str], str]: + prompt = SKILL_JUDGE_PROMPT.format( + candidate_name=candidate.name, + candidate_description=candidate.description, + candidate_instructions=candidate.instructions[:2000], + candidate_triggers=", ".join(candidate.triggers), + candidate_tags=", ".join(candidate.tags), + existing_name=existing.name if existing else "(none - no existing skill)", + existing_description=existing.description if existing else "", + existing_instructions=(existing.instructions[:2000] if existing else ""), + existing_triggers=", ".join(existing.triggers) if existing else "", + existing_tags=", ".join(existing.tags) if existing else "", + similarity_score=f"{similarity_score:.3f}", + ) + + raw = llm_call(model, prompt) + parsed = extract_json_from_text(raw) + + if parsed and isinstance(parsed, dict): + action = parsed.get("action", "discard").lower().strip() + target_id = parsed.get("target_skill_id") + reason = parsed.get("reason", "") + if action in ("add", "merge", "discard"): + if action == "merge" and existing: + return action, existing.id, reason + elif action == "merge" and not existing: + return "add", None, reason + " (no target for merge, adding instead)" + return action, target_id, reason + + if existing and similarity_score >= 0.82: + return "merge", existing.id, "high similarity (deterministic fallback)" + elif not existing or similarity_score <= 0.22: + return "add", None, "low similarity, distinct skill (deterministic fallback)" + elif similarity_score >= 0.50: + return "merge", existing.id, "moderate similarity (deterministic fallback)" + else: + return "add", None, "below merge threshold (deterministic fallback)" + + +def merge_skills( + existing: SkillEntry, + candidate: SkillEntry, + llm_call: Callable[[str, str], str], + model: str, +) -> SkillEntry: + prompt = SKILL_MERGE_PROMPT.format( + existing_name=existing.name, + existing_description=existing.description, + existing_instructions=existing.instructions[:3000], + existing_triggers=json.dumps(existing.triggers, ensure_ascii=False), + existing_tags=json.dumps(existing.tags, ensure_ascii=False), + candidate_name=candidate.name, + candidate_description=candidate.description, + candidate_instructions=candidate.instructions[:3000], + candidate_triggers=json.dumps(candidate.triggers, ensure_ascii=False), + candidate_tags=json.dumps(candidate.tags, ensure_ascii=False), + ) + + raw = llm_call(model, prompt) + parsed = extract_json_from_text(raw) + + merged = SkillEntry( + id=existing.id, + name=existing.name, + description=existing.description, + instructions=existing.instructions, + triggers=list(existing.triggers), + tags=list(existing.tags), + examples=list(existing.examples), + version=existing.version, + confidence=max(existing.confidence, candidate.confidence), + created_at=existing.created_at, + updated_at=existing.updated_at, + ) + + if parsed and isinstance(parsed, dict): + if parsed.get("name"): + merged.name = parsed["name"] + if parsed.get("description"): + merged.description = parsed["description"] + if parsed.get("instructions"): + merged.instructions = parsed["instructions"] + if parsed.get("triggers"): + merged.triggers = list(set(existing.triggers + parsed["triggers"]))[:10] + if parsed.get("tags"): + merged.tags = list(set(existing.tags + parsed["tags"]))[:10] + else: + merged.triggers = list(set(existing.triggers + candidate.triggers))[:10] + merged.tags = list(set(existing.tags + candidate.tags))[:10] + if candidate.instructions and candidate.instructions not in existing.instructions: + merged.instructions = ( + existing.instructions + "\n\n## Updated Constraints\n" + candidate.instructions + ) + + merged.bump_version() + return merged + + +def maintain_skill( + candidate: SkillEntry, + store: SkillStore, + llm_call: Callable[[str, str], str], + model: str, + embedding_model: str = "text-embedding-3-small", + bm25_weight: float = 0.1, + dedupe_similarity_threshold: float = 0.4, + logger: Any = None, +) -> Tuple[str, Optional[SkillEntry]]: + import logging + _logger = logger or logging.getLogger(__name__) + + # Compute candidate embedding for retrieval + candidate_text = candidate.to_search_text() + candidate_embedding = None + try: + candidate_embedding = compute_embedding(candidate_text, model=embedding_model) + except Exception as exc: + _logger.warning( + "AutoSkill maintenance: failed to compute candidate embedding for '%s': %s", + candidate.name, exc, + ) + _logger.info( + "AutoSkill maintenance: candidate='%s', embedding_dim=%d", + candidate.name, len(candidate_embedding) if candidate_embedding else 0, + ) + + results = hybrid_search( + store=store, + query=candidate_text, + query_embedding=candidate_embedding, + top_k=1, + threshold=0.0, + bm25_weight=bm25_weight, + embedding_model=embedding_model, + ) + + existing: Optional[SkillEntry] = None + similarity_score = 0.0 + if results: + existing, similarity_score = results[0] + _logger.info( + "AutoSkill maintenance: best match='%s' (score=%.3f)", + existing.name, similarity_score, + ) + else: + _logger.info("AutoSkill maintenance: no existing skills in store") + + if existing and similarity_score < dedupe_similarity_threshold: + _logger.info( + "AutoSkill maintenance: similarity %.3f < threshold %.3f, skipping merge consideration", + similarity_score, dedupe_similarity_threshold, + ) + existing = None + similarity_score = 0.0 + + action, _, reason = judge_skill( + candidate, existing, similarity_score, llm_call, model, + ) + _logger.info( + "AutoSkill maintenance: judge decision='%s', reason='%s'", + action, reason[:100], + ) + + if action == "discard": + _logger.info("AutoSkill maintenance: discarded candidate '%s'", candidate.name) + return "discard", None + elif action == "merge" and existing: + merged = merge_skills(existing, candidate, llm_call, model) + store.update_skill(merged) + if candidate_embedding: + new_emb = compute_embedding(merged.to_search_text(), model=embedding_model) + if new_emb: + store.set_embedding(merged.id, new_emb) + _logger.info( + "AutoSkill maintenance: merged into '%s' (v%s → v%s)", + merged.name, existing.version, merged.version, + ) + return "merge", merged + else: + store.add_skill(candidate) + if candidate_embedding: + store.set_embedding(candidate.id, candidate_embedding) + _logger.info( + "AutoSkill maintenance: added new skill '%s' (id=%s)", + candidate.name, candidate.id, + ) + return "add", candidate diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_retrieval.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_retrieval.py new file mode 100644 index 00000000..c4d24c41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_retrieval.py @@ -0,0 +1,159 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +import math +import re +from collections import Counter +from typing import Dict, List, Optional, Tuple + +from .skill_store import SkillEntry, SkillStore + +logger = logging.getLogger(__name__) + +_TOKEN_RE = re.compile(r"[A-Za-z0-9_]+|[\u4e00-\u9fff]|[^\W\d_]+", re.UNICODE) +_STOPWORDS = frozenset([ + "the", "a", "an", "is", "are", "was", "were", "be", "been", + "being", "have", "has", "had", "do", "does", "did", "will", + "would", "could", "should", "may", "might", "shall", "can", + "to", "of", "in", "for", "on", "with", "at", "by", "from", + "and", "or", "but", "not", "if", "then", "else", "when", + "that", "this", "it", "its", "as", "so", "no", "yes", +]) + + +def tokenize(text: str) -> List[str]: + tokens = _TOKEN_RE.findall(text.lower()) + return [t for t in tokens if t not in _STOPWORDS and len(t) > 1] + + +_st_model = None +_st_model_name = None + + +def _get_st_model(model_name: str = "all-MiniLM-L6-v2"): + global _st_model, _st_model_name + if _st_model is None or _st_model_name != model_name: + from sentence_transformers import SentenceTransformer + logger.info("Loading SentenceTransformer model: %s", model_name) + _st_model = SentenceTransformer(model_name) + _st_model_name = model_name + return _st_model + + +def compute_embedding(text: str, model: str = "all-MiniLM-L6-v2") -> List[float]: + st = _get_st_model(model) + vec = st.encode([text])[0] + return vec.tolist() + + +def bm25_score( + query_tokens: List[str], + doc_tokens: List[str], + avg_doc_len: float, + doc_count: int, + df: Dict[str, int], + k1: float = 1.5, + b: float = 0.75, +) -> float: + if not query_tokens or not doc_tokens: + return 0.0 + + doc_len = len(doc_tokens) + doc_tf = Counter(doc_tokens) + score = 0.0 + + for term in query_tokens: + if term not in doc_tf: + continue + tf = doc_tf[term] + n = df.get(term, 0) + idf = math.log((doc_count - n + 0.5) / (n + 0.5) + 1.0) + tf_norm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_len / max(avg_doc_len, 1))) + score += idf * tf_norm + + return score + + +def cosine_similarity(a: List[float], b: List[float]) -> float: + if not a or not b or len(a) != len(b): + return 0.0 + dot = sum(x * y for x, y in zip(a, b)) + norm_a = math.sqrt(sum(x * x for x in a)) + norm_b = math.sqrt(sum(x * x for x in b)) + if norm_a == 0 or norm_b == 0: + return 0.0 + return dot / (norm_a * norm_b) + + +def hybrid_search( + store: SkillStore, + query: str, + query_embedding: Optional[List[float]] = None, + top_k: int = 5, + threshold: float = 0.3, + bm25_weight: float = 0.1, + embedding_model: str = "text-embedding-3-small", +) -> List[Tuple[SkillEntry, float]]: + skills = store.list_skills() + if not skills: + return [] + + query_tokens = tokenize(query) + doc_tokens_map: Dict[str, List[str]] = {} + df: Dict[str, int] = Counter() + + for skill in skills: + tokens = tokenize(skill.to_search_text()) + doc_tokens_map[skill.id] = tokens + for t in set(tokens): + df[t] += 1 + + avg_doc_len = sum(len(t) for t in doc_tokens_map.values()) / max(len(skills), 1) + + bm25_scores: Dict[str, float] = {} + for skill in skills: + bm25_scores[skill.id] = bm25_score( + query_tokens, doc_tokens_map[skill.id], + avg_doc_len, len(skills), df, + ) + + vec_scores: Dict[str, float] = {} + if query_embedding: + embeddings = store.get_embeddings() + for skill in skills: + emb = embeddings.get(skill.id) + if emb: + vec_scores[skill.id] = cosine_similarity(query_embedding, emb) + else: + vec_scores[skill.id] = 0.0 + else: + bm25_weight = 1.0 + for skill in skills: + vec_scores[skill.id] = 0.0 + + bm25_max = max(bm25_scores.values()) if bm25_scores else 0.0 + norm_bm25: Dict[str, float] = {} + if bm25_max > 0: + norm_bm25 = {k: v / bm25_max for k, v in bm25_scores.items()} + else: + norm_bm25 = {k: 0.0 for k in bm25_scores} + + final_scores: Dict[str, float] = {} + for skill in skills: + sid = skill.id + final_scores[sid] = ( + (1 - bm25_weight) * vec_scores.get(sid, 0.0) + + bm25_weight * norm_bm25.get(sid, 0.0) + ) + + skill_map = {s.id: s for s in skills} + results = [ + (skill_map[sid], score) + for sid, score in sorted(final_scores.items(), key=lambda x: -x[1]) + if score >= threshold + ] + + return results[:top_k] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_store.py b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_store.py new file mode 100644 index 00000000..a274925e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/autoskill/skill_store.py @@ -0,0 +1,208 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import threading +import uuid +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional + + +@dataclass +class SkillEntry: + + id: str + name: str + description: str + instructions: str + triggers: List[str] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + examples: List[Dict[str, Any]] = field(default_factory=list) + version: str = "0.1.0" + confidence: float = 0.5 + created_at: str = "" + updated_at: str = "" + + def to_search_text(self) -> str: + parts = [self.name, self.description] + parts.extend(self.triggers) + parts.extend(self.tags) + parts.append(self.instructions) + return " ".join(parts) + + def bump_version(self) -> None: + parts = self.version.split(".") + if len(parts) == 3: + parts[2] = str(int(parts[2]) + 1) + self.version = ".".join(parts) + else: + self.version = "0.1.1" + self.updated_at = datetime.now().isoformat() + + +@dataclass +class LearningEvent: + + session_id: str + task_id: str + benchmark_id: str + action: str + skill_name: str + timestamp: str = "" + + +class SkillStore: + + _instances: Dict[str, "SkillStore"] = {} + _class_lock = threading.Lock() + + def __init__(self, store_id: str) -> None: + self._store_id = store_id + self._lock = threading.Lock() + self._skills: Dict[str, SkillEntry] = {} + self._embeddings: Dict[str, List[float]] = {} + self._session_count: int = 0 + self._history: List[LearningEvent] = [] + + @property + def store_id(self) -> str: + return self._store_id + + @property + def session_count(self) -> int: + with self._lock: + return self._session_count + + @property + def skill_count(self) -> int: + with self._lock: + return len(self._skills) + + @classmethod + def get_or_create( + cls, + shuffle_mode: str = "isolated", + benchmark_id: Optional[str] = None, + task_group: Optional[str] = None, + ) -> "SkillStore": + if shuffle_mode == "isolated": + key = f"autoskill_isolated_{benchmark_id or task_group or 'default'}" + elif shuffle_mode == "sequential": + key = "autoskill_sequential_global" + elif shuffle_mode == "interleaved": + key = "autoskill_interleaved_global" + else: + key = f"autoskill_{shuffle_mode}" + + with cls._class_lock: + if key not in cls._instances: + cls._instances[key] = cls(store_id=key) + return cls._instances[key] + + @classmethod + def list_stores(cls) -> Dict[str, "SkillStore"]: + with cls._class_lock: + return dict(cls._instances) + + @classmethod + def reset_all(cls) -> None: + with cls._class_lock: + cls._instances.clear() + + def increment_session(self) -> int: + with self._lock: + self._session_count += 1 + return self._session_count + + def add_skill(self, skill: SkillEntry) -> None: + with self._lock: + if not skill.id: + skill.id = str(uuid.uuid4()) + if not skill.created_at: + skill.created_at = datetime.now().isoformat() + skill.updated_at = skill.created_at + self._skills[skill.id] = skill + + def update_skill(self, skill: SkillEntry) -> None: + with self._lock: + skill.updated_at = datetime.now().isoformat() + self._skills[skill.id] = skill + + def list_skills(self) -> List[SkillEntry]: + with self._lock: + return list(self._skills.values()) + + def set_embedding(self, skill_id: str, embedding: List[float]) -> None: + with self._lock: + self._embeddings[skill_id] = embedding + + def get_embeddings(self) -> Dict[str, List[float]]: + with self._lock: + return dict(self._embeddings) + + def record_learning( + self, + session_id: str, + task_id: str, + benchmark_id: str, + action: str, + skill_name: str = "", + ) -> None: + with self._lock: + self._history.append(LearningEvent( + session_id=session_id, + task_id=task_id, + benchmark_id=benchmark_id, + action=action, + skill_name=skill_name, + timestamp=datetime.now().isoformat(), + )) + + def save_checkpoint(self, path: str) -> None: + with self._lock: + data = { + "store_id": self._store_id, + "session_count": self._session_count, + "skills": {sid: asdict(s) for sid, s in self._skills.items()}, + "embeddings": self._embeddings, + "history": [asdict(e) for e in self._history], + } + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def load_checkpoint(self, path: str) -> None: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + with self._lock: + self._session_count = data.get("session_count", 0) + self._skills = {} + for sid, sdata in data.get("skills", {}).items(): + self._skills[sid] = SkillEntry(**{ + k: v for k, v in sdata.items() + if k in SkillEntry.__dataclass_fields__ + }) + self._embeddings = data.get("embeddings", {}) + self._history = [ + LearningEvent(**{ + k: v for k, v in e.items() + if k in LearningEvent.__dataclass_fields__ + }) + for e in data.get("history", []) + ] + + def save_skills_text(self, path: str) -> None: + with self._lock: + skills = list(self._skills.values()) + lines = [f"# SkillBank: {self._store_id} ({len(skills)} skills)\n"] + for s in skills: + lines.append(f"## {s.name} (v{s.version})") + lines.append(f" {s.description}") + lines.append(f" Tags: {', '.join(s.tags)}") + lines.append(f" Triggers: {', '.join(s.triggers)}") + lines.append(f" Instructions: {s.instructions}") + lines.append("") + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/__init__.py new file mode 100644 index 00000000..1aaf12d0 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +# CLI agent bundle namespace + +from .claude.agent import ClaudeCodeAgent, ClaudeCodeAgentInstance # noqa: F401 +from .codex.agent import CodexAgent, CodexAgentInstance # noqa: F401 +from .command_runner import ExecutionBackend # noqa: F401 +from .gemini.agent import GeminiAgent, GeminiAgentInstance # noqa: F401 diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/base.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/base.py new file mode 100644 index 00000000..395d0ab8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/base.py @@ -0,0 +1,350 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import abc +import logging +import os +import tempfile +from pathlib import Path +from typing import Any + +from ...adapters.agents.mcp_agent import MCPAgentInstance +from ...core.agent import Agent +from ...core.context import context_env +from ...core.types import ModelSettings +from ...integrations.litellm import LitellmProxy +from ...integrations.litellm.health import check_model_accessible_sync +from ...integrations.litellm.trace_cost import load_trace_cost +from ...utils.cost import UpdatableCostReport +from .command_runner import ( + BaseCLIConfig, + CLIResult, + DockerRunner, + ExecutionBackend, + PodmanRunner, + ProcessRunner, +) + + +class BaseCLIWrapper(abc.ABC): + """Shared helpers for headless CLI wrappers. + + Subclasses implement build_env/build_command; run() handles the rest. + """ + + config_prefix = "cli_" + spawn_error_message = "Spawn failed" + + def __init__( + self, + env: dict[str, str] | None = None, + log_path: Path | None = None, + config_dir: Path | None = None, + logger: logging.Logger | None = None, + runner: ExecutionBackend = ExecutionBackend.AUTO, + ) -> None: + self.env = env or os.environ.copy() + self.config_dir = config_dir + self.log_path = log_path + self._last_run_context: dict[str, Any] = {} + self._logger = logger or logging.getLogger(self.__class__.__name__) + if runner == ExecutionBackend.AUTO: + from .command_runner import resolve_container_backend + + runner = resolve_container_backend() + if runner == ExecutionBackend.PROCESS: + self.runner = ProcessRunner(log_path=log_path, logger=self._logger) + elif runner == ExecutionBackend.PODMAN: + self.runner = PodmanRunner(log_path=log_path, logger=self._logger) + elif runner == ExecutionBackend.DOCKER: + self.runner = DockerRunner(log_path=log_path, logger=self._logger) + else: + raise ValueError(f"runner value: {runner} is not supported!") + + def run(self, *, prompt: str, config: BaseCLIConfig) -> CLIResult: + cfg_root = self._resolve_config_root(self.config_prefix) + self._last_run_context = { + "cfg_root": cfg_root, + "prompt": prompt, + "config": config, + } + env = self.build_env(cfg_root=cfg_root, prompt=prompt, config=config) + env.update(context_env()) + if config.env: + env = {**env, **config.env} + cmd = self.build_command(cfg_root=cfg_root, prompt=prompt, config=config) + return self.runner.run( + cmd=cmd, + env=env, + cfg_root=cfg_root, + config=config, + spawn_error_message=self.spawn_error_message, + ) + + def close(self) -> None: + self.runner.close() + + def _resolve_config_root(self, prefix: str) -> Path: + if self.config_dir is not None: + return Path(self.config_dir) + return Path(tempfile.mkdtemp(prefix=prefix)) + + def _log_warning(self, message: str) -> None: + """Best-effort logger wrapper to avoid attribute errors on exit.""" + try: + self._logger.warning(message) + except Exception: + logging.getLogger(__name__).warning(message) + + # Abstract hooks ------------------------------------------------- + + @abc.abstractmethod + def build_env(self, *, cfg_root: Path, prompt: str, config: Any) -> dict[str, str]: + ... + + @abc.abstractmethod + def build_command(self, *, cfg_root: Path, prompt: str, config: Any) -> list[str]: + ... + + +class ProxyBackedMCPAgentInstance(MCPAgentInstance, abc.ABC): + """Base class for MCP agents that launch a LiteLLM proxy and delegate to a CLI wrapper.""" + + def __init__( + self, + session_id: str, + model_id: str, + *, + max_steps: int = 150, + model_alias: str | None = None, + execution_backend: ExecutionBackend = ExecutionBackend.AUTO, + model_settings: ModelSettings | None = None, + ) -> None: + super().__init__(session_id) + self.model_id = model_id + self.max_steps = max_steps + self._proxy_log_dir = self.paths.agent_dir / "litellm_proxy" + self._trace_log_path = self._proxy_log_dir / "trace.jsonl" + self._proxy: LitellmProxy | None = None + self._cli: BaseCLIWrapper | None = None + self._model_alias = model_alias + self.execution_backend = execution_backend + if model_settings is None: + self.model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self.model_settings = model_settings + else: + raise ValueError("model_settings must be a ModelSettings instance.") + + # Check model accessibility + check_model_accessible_sync(self.model_id, logger=self.logger) + + @property + @abc.abstractmethod + def cli_display_name(self) -> str: + ... + + @abc.abstractmethod + def _build_cli(self) -> BaseCLIWrapper: + ... + + @abc.abstractmethod + def _run_cli( + self, + cli: BaseCLIWrapper, + prompt: str, + mcp_host: str, + mcp_port: int, + proxy: LitellmProxy, + ) -> Any: + ... + + def close_mcp_agent(self) -> None: + self.logger.info("Closing CLI before MCP shutdown") + if self._cli is not None: + self._cli.close() + self._cli = None + self.logger.info("Closing LiteLLM proxy before MCP shutdown") + if self._proxy is not None: + self._proxy.close() + self._proxy = None + super().close_mcp_agent() + + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> Any: + prompt = self._build_prompt() + proxy_log = self._proxy_log_dir / "litellm_proxy.log" + alias_map = self._proxy_alias_map() + + # Log model parameters for validation + params_info = [] + if self.model_settings.temperature is not None: + params_info.append(f"temperature={self.model_settings.temperature}") + if self.model_settings.max_tokens is not None: + params_info.append(f"max_tokens={self.model_settings.max_tokens}") + if self.model_settings.top_p is not None: + params_info.append(f"top_p={self.model_settings.top_p}") + params_str = f" with {', '.join(params_info)}" if params_info else "" + + self.logger.info( + "Starting LiteLLM proxy for model %s%s (log: %s)", + self.model_id, + params_str, + proxy_log, + ) + proxy_log.parent.mkdir(parents=True, exist_ok=True) + proxy = LitellmProxy( + model=self.model_id, + log_path=str(proxy_log), + usage_log_path=str(self._trace_log_path), + model_alias_map=alias_map or None, + model_settings=self.model_settings, + ) + self._proxy = proxy + try: + proxy.start() + except Exception: + self.logger.exception("LiteLLM proxy failed to start") + raise + self.logger.info("LiteLLM proxy started at %s", proxy.base_url) + + try: + cli = self._build_cli() + self._cli = cli + self.logger.info( + "Launching %s (log: %s) against MCP http://%s:%s/mcp using proxy %s", + self.cli_display_name, + cli.log_path, + mcp_host, + mcp_port, + proxy.base_url, + ) + stdout = self._run_cli(cli, prompt, mcp_host, mcp_port, proxy) + self.logger.info("%s run finished", self.cli_display_name) + return stdout + except Exception as e: + from .command_runner import CLIExecutionError + + if isinstance(e, CLIExecutionError): + if e.stderr: + self.logger.error("%s STDERR:\n%s", self.cli_display_name, e.stderr.rstrip()) + if e.stdout: + self.logger.error("%s STDOUT:\n%s", self.cli_display_name, e.stdout.rstrip()) + self.logger.exception("%s run failed: %s", self.cli_display_name, e) + raise + finally: + if self._cli is not None: + self._cli.close() + self._cli = None + proxy.close() + self._proxy = None + self._drain_server() + + def get_cost(self) -> UpdatableCostReport: + cost = load_trace_cost(self._trace_log_path, self.model_id) + report = UpdatableCostReport.initialize_empty(model_name=self.model_id) + report.add_cost(cost) + return report + + def _build_prompt(self) -> str: + prompt = "" + if self.context: + prompt += f"Context: {self.context}\n\n" + + finish_hint = "" + finish_tools = [a.name for a in self.actions if a.is_finish] + if finish_tools: + finish_hint = f" Use the designated finish tool(s): {', '.join(finish_tools)}." + + instructions = ( + "Complete this task using the available environment tools. Each tool corresponds to an action " + " you can take in the task environment.\n" + "# Important: You are on solo mode. Do not reply back or message unless its through a dedicated " + "environment tool call, every such attempt will finish the session with failure.\n" + "All your actions on with regard to the task must go through environment tool calls." + ) + + if finish_hint: + instructions += ( + f"{finish_hint} Always conclude by invoking the designated finish tool for this task environment." + ) + prompt += f"{instructions}\n" + + if self.initial_observation is not None and not self.initial_observation.is_empty(): + text = str(self.initial_observation).strip() + if text: + prompt += f"\nFirst Observation: {text}\n" + + return prompt + self.task + + def close(self) -> None: + if self._cli is not None: + self._cli.close() + self._cli = None + if self._proxy is not None: + self._proxy.close() + self._proxy = None + self._drain_server() + super().close() + + def _proxy_alias_map(self) -> dict[str, str]: + if not self._model_alias: + return {} + return {self._model_alias: self.model_id} + + def _drain_server(self) -> None: + """Best-effort stop/join of the MCP server thread to avoid teardown crashes.""" + server = self._mcp_server + if server is None: + return + try: + server.stop(raise_on_timeout=False) + except Exception as exc: + self._log_warning(f"Error while stopping MCP server: {exc}") + + def _log_warning(self, message: str) -> None: + try: + self._logger.warning(message) + except Exception: + logging.getLogger(__name__).warning(message) + + +class ProxyBackedAgent(Agent): + """Minimal agent factory helper for proxy-backed CLI agents.""" + + model: str + max_steps: int = 150 + + @classmethod + def _get_instance_class(cls): + raise NotImplementedError + + execution_backend: ExecutionBackend = ExecutionBackend.AUTO + model_settings: ModelSettings | None = None + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + # Resolve AUTO on the host side so the concrete backend (PODMAN/DOCKER) + # is serialized to the venv, where podman may not be on PATH. + backend = self.execution_backend + if backend == ExecutionBackend.AUTO: + from .command_runner import resolve_container_backend + + backend = resolve_container_backend() + return { + "session_id": session_id, + "model_id": self.model, + "max_steps": self.max_steps, + "execution_backend": backend, + "model_settings": self.model_settings, + } + + @property + def model_name(self) -> str: # type: ignore[override] + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model)] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/__init__.py new file mode 100644 index 00000000..95e9b24a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .agent import ClaudeCodeAgent, ClaudeCodeAgentInstance # noqa: F401 diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/agent.py new file mode 100644 index 00000000..220fb534 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/agent.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +from typing import Any, ClassVar + +from ....core.types import ModelSettings +from ..base import ExecutionBackend, ProxyBackedAgent, ProxyBackedMCPAgentInstance +from .cli import ClaudeCLIConfig, ClaudeCodeCLI + + +class ClaudeCodeAgentInstance(ProxyBackedMCPAgentInstance): + """Self-contained Claude Code CLI agent routed through a LiteLLM proxy.""" + + def __init__( + self, + session_id: str, + model_id: str, + max_steps: int = 150, + execution_backend: ExecutionBackend = ExecutionBackend.AUTO, + model_settings: ModelSettings | None = None, + ): + # The alias is what we ask Claude Code CLI for; the proxy maps it to the backend model. + # Must contain "sonnet-4" to get 64000 max_output_tokens in CLI + # (CLI hardcodes 8192 for names containing "3-5"). + self._claude_model_alias = "claude-sonnet-4-20250514" + super().__init__( + session_id, + model_id, + max_steps=max_steps, + model_alias=self._claude_model_alias, + execution_backend=execution_backend, + model_settings=model_settings, + ) + self._claude_log = self.paths.agent_dir / "claude_cli.log" + + @property + def cli_display_name(self) -> str: + return "Claude Code CLI" + + def _build_cli(self) -> ClaudeCodeCLI: + cfg_dir = self.paths.agent_dir / "claude_code_config" + return ClaudeCodeCLI( + env=os.environ.copy(), + log_path=self._claude_log, + config_dir=cfg_dir, + logger=self.logger, + runner=self.execution_backend, + ) + + def _run_cli( + self, + cli: ClaudeCodeCLI, + prompt: str, + mcp_host: str, + mcp_port: int, + proxy: Any, + ) -> Any: + # allowed_tools = [f"mcp__environment__{action.name}" for action in self.actions] + config = ClaudeCLIConfig( + mcp_host=mcp_host, + mcp_port=mcp_port, + provider_url=proxy.base_url, + backend_model=self.model_id, + claude_model=self._claude_model_alias, + # allowed_tools=allowed_tools, + max_turns=self.max_steps, + ) + config.env = { + "MCP_TIMEOUT": str(config.mcp_timeout_ms), + "MCP_TOOL_TIMEOUT": str(config.mcp_tool_timeout_ms), + "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32768", + } + result = cli.run(prompt=prompt, config=config) + return result.stdout + + def _stringify_empty_output(self) -> bool: + return True + + +class ClaudeCodeAgent(ProxyBackedAgent): + display_name: ClassVar[str] = "Claude Code CLI" + slug_name: ClassVar[str] = "claude_code" + execution_backend: ExecutionBackend = ExecutionBackend.AUTO + + @classmethod + def _get_instance_class(cls): + return ClaudeCodeAgentInstance + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model_id)] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/cli.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/cli.py new file mode 100644 index 00000000..225532f9 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/cli.py @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Optional + +from ..base import BaseCLIConfig, BaseCLIWrapper, ExecutionBackend + + +class ClaudeCLIConfig(BaseCLIConfig): + backend_model: str + claude_model: str + env_key: str = "ANTHROPIC_API_KEY" + auth_token_env: str = "ANTHROPIC_AUTH_TOKEN" + output_format: str = "text" + skip_permissions: bool = True + max_turns: int = 150 + mcp_only: bool = True # New: only allow MCP tools + allowed_tools: Optional[list[str]] = None + image: str = "exgentic-claude-code:dev" + image_workdir: str = "/work" + mcp_timeout_ms: int = 600_000 + mcp_tool_timeout_ms: int = 1_800_000 + + +class ClaudeCodeCLI(BaseCLIWrapper): + """Thin wrapper for running Claude Code's CLI in print mode.""" + + def __init__( + self, + env: Optional[dict[str, str]] = None, + log_path: Optional[Path] = None, + config_dir: Optional[Path] = None, + logger=None, + runner: ExecutionBackend = ExecutionBackend.PROCESS, + ) -> None: + super().__init__( + env=env, + log_path=log_path, + config_dir=config_dir, + logger=logger, + runner=runner, + ) + self._mcp_config_path: Optional[Path] = None + self.config_prefix = "claude_code_cli_" + self.spawn_error_message = "Failed to start Claude CLI" + + # Required hooks -------------------------------------------------- + + def build_env(self, *, cfg_root: Path, prompt: str, config: ClaudeCLIConfig) -> dict[str, str]: + env = self.env.copy() + env["ANTHROPIC_BASE_URL"] = config.provider_url + if config.env: + env.update(config.env) + + token = env.get(config.auth_token_env) or env.get(config.env_key) or "dummy-api-key" + env[config.env_key] = token + env[config.auth_token_env] = token + + # Isolate config/settings/state under a temp directory + env["HOME"] = str(cfg_root) + + return env + + def build_command(self, *, cfg_root: Path, prompt: str, config: ClaudeCLIConfig) -> list[str]: + if not prompt or not prompt.strip(): + raise ValueError("Prompt cannot be empty") + + mcp_cfg_path = (cfg_root / "mcp.json").absolute() + self._mcp_config_path = mcp_cfg_path + + # Rewrite localhost addresses to host gateway for container runners + mcp_host = config.mcp_host + from ..command_runner import ContainerRunner + + if isinstance(self.runner, ContainerRunner) and mcp_host in ("0.0.0.0", "127.0.0.1", "localhost"): + mcp_host = self.runner.host_gateway + + mcp_url = f"http://{mcp_host}:{config.mcp_port}/mcp" + self._write_mcp_config(mcp_cfg_path, mcp_url) + self._write_settings_config(cfg_root) + + # Verify config was written + if not mcp_cfg_path.exists(): + raise RuntimeError(f"Failed to write MCP config to {mcp_cfg_path}") + + cmd: list[str] = [ + "claude", + "-p", # print mode, single-shot + "--model", + config.claude_model, + "--mcp-config", + str(mcp_cfg_path), + "--strict-mcp-config", + "--output-format", + config.output_format, + "--debug", + "--mcp-debug", + "--no-session-persistence", + "--append-system-prompt", + "AUTONOMOUS SOLO MODE.\n" + "First, discover available MCP tools by listing tools from the environment server.\n" + "Then use those tools to complete the task.\n" + "Never ask for clarification - make reasonable assumptions.\n" + "You have NO filesystem access - work exclusively through MCP tools.", + "--dangerously-skip-permissions", + "--max-turns", + str(config.max_turns), + ] + + # if config.allowed_tools: + # cmd.extend(["--allowedTools", ",".join(config.allowed_tools)]) + + # Ensure the following argument is treated purely as the prompt, not part of a variadic flag. + cmd.append("--") + cmd.append(prompt) + return cmd + + # Internal helpers ------------------------------------------------- + + def _write_mcp_config(self, path: Path, mcp_url: str) -> None: + """Write MCP server configuration.""" + config = {"mcpServers": {"environment": {"type": "http", "url": mcp_url}}} + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + json.dump(config, fh, indent=2) + + def _write_settings_config(self, home_dir: Path) -> None: + """Write settings that completely block filesystem access.""" + settings_path = home_dir / ".claude" / "settings.json" + + settings = { + "enableAllProjectMcpServers": True, + } + + settings_path.parent.mkdir(parents=True, exist_ok=True) + with open(settings_path, "w", encoding="utf-8") as fh: + json.dump(settings, fh, indent=2) + + # Pre-create directories that the Claude Code CLI expects to write + # into. When running inside a container with ``--user``, the + # mounted volume may have restrictive ownership so the CLI cannot + # create these itself. + for subdir in ("debug", "conversations", "projects", "todos"): + (home_dir / ".claude" / subdir).mkdir(parents=True, exist_ok=True) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/setup.sh b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/setup.sh new file mode 100644 index 00000000..dc85320f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/claude/setup.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +# Determine container runtime +CONTAINER_CMD="" +if command -v podman >/dev/null 2>&1; then + CONTAINER_CMD="podman" + # Start podman machine if needed (macOS/Windows) + if podman machine list >/dev/null 2>&1; then + MACHINE_STATUS=$(podman machine list --format "{{.Running}}" 2>/dev/null | head -n 1) + if [ -z "$MACHINE_STATUS" ]; then + podman machine init && podman machine start + elif [ "$MACHINE_STATUS" != "true" ]; then + podman machine start + fi + fi +elif command -v docker >/dev/null 2>&1; then + CONTAINER_CMD="docker" +else + echo "Error: Neither Podman nor Docker found." >&2 + exit 1 +fi + +# Build Claude Code container image (inline — no external Dockerfile needed) +$CONTAINER_CMD build -t exgentic-claude-code:dev -f - . <<'DOCKERFILE' +FROM registry.access.redhat.com/ubi9/nodejs-20 +RUN npm install -g @anthropic-ai/claude-code@2.1.7 +WORKDIR /work +CMD ["claude","--help"] +DOCKERFILE + +echo "Claude Code Agent setup complete (using $CONTAINER_CMD)" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/__init__.py new file mode 100644 index 00000000..38ee1b99 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .agent import CodexAgent, CodexAgentInstance # noqa: F401 diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/agent.py new file mode 100644 index 00000000..0f9e5a98 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/agent.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +from typing import Any, ClassVar + +from ....core.types import ModelSettings +from ..base import ProxyBackedAgent, ProxyBackedMCPAgentInstance +from .cli import CodexCLI, CodexCLIConfig, ExecutionBackend + + +class CodexAgentInstance(ProxyBackedMCPAgentInstance): + """Self-contained Codex CLI agent that runs through a LiteLLM proxy.""" + + def __init__( + self, + session_id: str, + model_id: str, + max_steps: int = 150, + execution_backend: ExecutionBackend = ExecutionBackend.AUTO, + model_settings: ModelSettings | None = None, + ): + super().__init__( + session_id, + model_id, + max_steps=max_steps, + execution_backend=execution_backend, + model_settings=model_settings, + ) + self._codex_log = self.paths.agent_dir / "codex_cli.log" + + @property + def cli_display_name(self) -> str: + return "Codex CLI" + + def _build_cli(self) -> CodexCLI: + return CodexCLI( + env=os.environ.copy(), + log_path=self._codex_log, + logger=self.logger, + runner=self.execution_backend, + ) + + def _run_cli( + self, + cli: CodexCLI, + prompt: str, + mcp_host: str, + mcp_port: int, + proxy: Any, + ) -> Any: + config = CodexCLIConfig( + mcp_host=mcp_host, + mcp_port=mcp_port, + model_id=self.model_id, + provider_url=proxy.base_url, + ) + result = cli.run(prompt=prompt, config=config) + return result.stdout + + +class CodexAgent(ProxyBackedAgent): + display_name: ClassVar[str] = "Codex CLI" + slug_name: ClassVar[str] = "codex_cli" + execution_backend: ExecutionBackend = ExecutionBackend.AUTO + + @classmethod + def _get_instance_class(cls): + return CodexAgentInstance diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/cli.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/cli.py new file mode 100644 index 00000000..1d15a2e7 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/cli.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from pathlib import Path +from typing import Optional + +from ..base import BaseCLIConfig, BaseCLIWrapper, ExecutionBackend + + +class CodexCLIConfig(BaseCLIConfig): + model_id: str + env_key: str = "OPENAI_API_KEY" + profile: str = "temp_model" + provider_name: str = "Provider" + image: str = "exgentic-codex:dev" + + +class CodexCLI(BaseCLIWrapper): + """Standalone wrapper for launching Codex CLI.""" + + def __init__( + self, + env: Optional[dict[str, str]] = None, + log_path: Optional[Path] = None, + logger=None, + runner: ExecutionBackend = ExecutionBackend.PROCESS, + ) -> None: + super().__init__(env=env, log_path=log_path, config_dir=None, logger=logger, runner=runner) + self.config_prefix = "codex_cli_" + self.spawn_error_message = "Failed to start Codex CLI" + + def build_env(self, *, cfg_root: Path, prompt: str, config: CodexCLIConfig) -> dict[str, str]: + env = self.env.copy() + env["OPENAI_API_BASE"] = config.provider_url + api_key = env.get(config.env_key) or "dummy-api-key" + env[config.env_key] = api_key + return env + + def build_command(self, *, cfg_root: Path, prompt: str, config: CodexCLIConfig) -> list[str]: + # Rewrite localhost addresses to host gateway for container runners + mcp_host = config.mcp_host + provider_url = config.provider_url + from ..command_runner import ContainerRunner + + if isinstance(self.runner, ContainerRunner): + if mcp_host in ("0.0.0.0", "127.0.0.1", "localhost"): + mcp_host = self.runner.host_gateway + for local in ("://127.0.0.1:", "://localhost:"): + if local in provider_url: + provider_url = provider_url.replace(local, f"://{self.runner.host_gateway}:") + break + + mcp_url = f"http://{mcp_host}:{config.mcp_port}/mcp" + overrides = [ + f'mcp_servers.environment.url="{mcp_url}"', + f'model_providers.temp.name="{config.provider_name}"', + f'model_providers.temp.base_url="{provider_url}"', + f'model_providers.temp.env_key="{config.env_key}"', + f'profiles.{config.profile}.model_provider="temp"', + f'profiles.{config.profile}.model="{config.model_id}"', + ] + + cmd: list[str] = ["codex", "exec", "--skip-git-repo-check", "--profile", config.profile] + for override in overrides: + cmd.extend(["-c", override]) + cmd.append(prompt) + + return cmd diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/setup.sh b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/setup.sh new file mode 100644 index 00000000..80c8e0fd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/codex/setup.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +# Determine container runtime +CONTAINER_CMD="" +if command -v podman >/dev/null 2>&1; then + CONTAINER_CMD="podman" + # Start podman machine if needed (macOS/Windows) + if podman machine list >/dev/null 2>&1; then + MACHINE_STATUS=$(podman machine list --format "{{.Running}}" 2>/dev/null | head -n 1) + if [ -z "$MACHINE_STATUS" ]; then + podman machine init && podman machine start + elif [ "$MACHINE_STATUS" != "true" ]; then + podman machine start + fi + fi +elif command -v docker >/dev/null 2>&1; then + CONTAINER_CMD="docker" +else + echo "Error: Neither Podman nor Docker found." >&2 + exit 1 +fi + +# Build Codex CLI container image (inline — no external Dockerfile needed) +$CONTAINER_CMD build -t exgentic-codex:dev -f - . <<'DOCKERFILE' +FROM registry.access.redhat.com/ubi9/nodejs-20 +RUN npm install -g @openai/codex@0.93.0 +WORKDIR /work +CMD ["codex","--help"] +DOCKERFILE + +echo "Codex Agent setup complete (using $CONTAINER_CMD)" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/command_runner.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/command_runner.py new file mode 100644 index 00000000..3dd2a26d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/command_runner.py @@ -0,0 +1,471 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import os +import shlex +import shutil +import subprocess +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Optional + +from pydantic import BaseModel + + +class ExecutionBackend(str, Enum): + """Execution backend for CLI runs.""" + + PROCESS = "process" + PODMAN = "podman" + DOCKER = "docker" + AUTO = "auto" + + +def resolve_container_backend() -> ExecutionBackend: + """Auto-detect container runtime: prefer podman, fallback to docker.""" + if shutil.which("podman"): + return ExecutionBackend.PODMAN + if shutil.which("docker"): + return ExecutionBackend.DOCKER + raise RuntimeError("Neither podman nor docker found") + + +@dataclass +class CLIResult: + stdout: str + stderr: str + code: int + + +class CLIStartError(RuntimeError): + """Raised when a CLI fails to spawn.""" + + +class CLIExecutionError(RuntimeError): + """Raised when a CLI exits with a non-zero status.""" + + def __init__( + self, + message: str, + *, + code: int, + stdout: str, + stderr: str, + cmd: list[str], + ) -> None: + super().__init__(message) + self.code = code + self.stdout = stdout + self.stderr = stderr + self.cmd = cmd + + def __str__(self) -> str: + parts = [super().__str__()] + if self.stderr: + parts.append(f"STDERR:\n{self.stderr.rstrip()}") + if self.stdout: + parts.append(f"STDOUT:\n{self.stdout.rstrip()}") + return "\n".join(parts) + + +class BaseCLIConfig(BaseModel): + """Common config fields shared by CLI wrappers.""" + + mcp_host: str + mcp_port: int + provider_url: str + image: str + image_workdir: str = "/work" + env: Optional[dict[str, str]] = None + + +class ProcessRunner: + """Shared subprocess execution logic (spawn + communicate + timeout + kill). + + Concrete runners implement how cmd/env/cfg_root are transformed. + """ + + def __init__(self, log_path, logger): + super().__init__() + self.log_path = log_path + self._logger = logger + self._last_cmd: list[str] = [] + self._proc: Optional[subprocess.Popen[str]] = None + + def _write_log( + self, + stdout: str, + stderr: str, + *, + returncode: int, + config: BaseCLIConfig, + ) -> None: + if not self.log_path: + return + self.log_path.parent.mkdir(parents=True, exist_ok=True) + try: + config_json = config.model_dump_json(indent=2) + except Exception: + config_json = str(config) + with open(self.log_path, "w", encoding="utf-8") as fh: + fh.write(f"Command: {shlex.join(self._last_cmd)}\n") + fh.write("Config:\n") + fh.write(f"{config_json}\n") + fh.write(f"Exit code: {returncode}\n\n") + if stdout: + fh.write("STDOUT:\n") + fh.write(stdout) + if not stdout.endswith("\n"): + fh.write("\n") + if stderr: + if stdout: + fh.write("\n") + fh.write("STDERR:\n") + fh.write(stderr) + if not stderr.endswith("\n"): + fh.write("\n") + + def run( + self, + *, + cmd: list[str], + env: dict[str, str], + cfg_root: Path, + config: BaseCLIConfig, + spawn_error_message: str, + stdin_devnull: bool = False, + ) -> CLIResult: + self._last_cmd = cmd + stdout: str = "" + stderr: str = "" + code: int = -1 + + popen_stdin = subprocess.DEVNULL if stdin_devnull else None + timeout_s: Optional[float] = None + + try: + self._proc = subprocess.Popen( + cmd, + stdin=popen_stdin, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + except Exception as exc: + stderr = f"{spawn_error_message}: {exc}" + self._write_log(stdout, stderr, returncode=code, config=config) + raise CLIStartError(f"{spawn_error_message}: {exc}") from exc + + try: + try: + stdout, stderr = self._proc.communicate(timeout=timeout_s) + except subprocess.TimeoutExpired: + self._logger.warning("CLI timed out; terminating: %s", shlex.join(cmd)) + self._proc.terminate() + try: + stdout, stderr = self._proc.communicate(timeout=5) + except subprocess.TimeoutExpired: + self._logger.warning("CLI did not terminate; killing it: %s", shlex.join(cmd)) + self._proc.kill() + stdout, stderr = self._proc.communicate() + code = self._proc.returncode or 0 + finally: + self._write_log(stdout or "", stderr or "", returncode=code, config=config) + if code != 0: + self._logger.warning("CLI exited non-zero (%s): %s", code, shlex.join(cmd)) + if code != 0: + raise CLIExecutionError( + f"CLI exited non-zero ({code}): {shlex.join(cmd)}", + code=code, + stdout=stdout or "", + stderr=stderr or "", + cmd=cmd, + ) + return CLIResult(stdout=stdout or "", stderr=stderr or "", code=code) + + def close(self) -> None: + proc = self._proc + if self._proc and self._proc.poll() is None: + self._proc.terminate() + try: + self._proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._logger.warning("CLI process did not terminate; killing it.") + self._proc.kill() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._logger.warning("CLI process did not exit after kill.") + + +class ContainerRunner(ProcessRunner): + """Shared helpers for container-based runners.""" + + host_gateway: str = "host.docker.internal" + + def _rewrite_mcp_config_path(self, inner_cmd: list[str], workdir: str) -> list[str]: + if "--mcp-config" in inner_cmd: + i = inner_cmd.index("--mcp-config") + if i + 1 < len(inner_cmd): + inner_cmd[i + 1] = f"{workdir}/mcp.json" + return inner_cmd + + def _container_env_from( + self, + env: dict[str, str], + host_gateway: str, + config: BaseCLIConfig, + ) -> dict[str, str]: + forwarded: dict[str, str] = {} + + for k in ( + # Anthropic (Claude Code) + "ANTHROPIC_BASE_URL", + "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + # OpenAI (Codex) + "OPENAI_API_KEY", + "OPENAI_API_BASE", + # Google (Gemini) + "GEMINI_API_KEY", + "GOOGLE_GEMINI_BASE_URL", + ): + if k in env: + forwarded[k] = env[k] + for k, v in env.items(): + if k.startswith("EXGENTIC_CTX_"): + forwarded[k] = v + if config.env: + for k, v in config.env.items(): + forwarded[k] = v + + for k in ( + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + ): + if k in env: + forwarded[k] = env[k] + + if "PYTHONIOENCODING" in env: + forwarded["PYTHONIOENCODING"] = env["PYTHONIOENCODING"] + + for k, v in forwarded.items(): + if "://127.0.0.1:" in v: + forwarded[k] = v.replace("://127.0.0.1:", f"://{host_gateway}:") + if "://localhost:" in v: + forwarded[k] = v.replace("://localhost:", f"://{host_gateway}:") + + # Ensure host_gateway is excluded from proxy so container can reach + # host-side services (LiteLLM proxy, MCP server) directly. + for k in ("NO_PROXY", "no_proxy"): + existing = forwarded.get(k, "") + if host_gateway not in existing: + forwarded[k] = f"{existing},{host_gateway}" if existing else host_gateway + + return forwarded + + def _patch_mcp_json(self, *, cfg_root: Path, host_gateway: str) -> None: + mcp_path = cfg_root / "mcp.json" + if not mcp_path.exists(): + return + + try: + data = json.loads(mcp_path.read_text(encoding="utf-8")) + env_cfg = data.get("mcpServers", {}).get("environment", {}) + url = env_cfg.get("url", "") + if not isinstance(url, str) or not url: + return + + if "://127.0.0.1:" in url: + data["mcpServers"]["environment"]["url"] = url.replace("://127.0.0.1:", f"://{host_gateway}:") + mcp_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + self._logger.debug("Patched mcp.json for container host gateway: %s", mcp_path) + elif "://localhost:" in url: + data["mcpServers"]["environment"]["url"] = url.replace("://localhost:", f"://{host_gateway}:") + mcp_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + self._logger.debug("Patched mcp.json for container host gateway: %s", mcp_path) + + except Exception as exc: + self._logger.warning("Failed to patch mcp.json (%s): %s", mcp_path, exc) + + +class PodmanRunner(ContainerRunner): + """Run the inner command inside a container via Podman.""" + + host_gateway = "host.containers.internal" + + def __init__(self, log_path, logger): + super().__init__(log_path, logger) + + def run( + self, + *, + cmd: list[str], + env: dict[str, str], + cfg_root: Path, + config: BaseCLIConfig, + spawn_error_message: str, + ) -> CLIResult: + runtime = "podman" + host_gateway = "host.containers.internal" + host_cfg_root = str(cfg_root.resolve()) + + # Patch mcp.json so container uses host gateway (not 127.0.0.1/localhost) + self._patch_mcp_json(cfg_root=cfg_root, host_gateway=host_gateway) + + inner_cmd = self._rewrite_mcp_config_path(list(cmd), workdir=config.image_workdir) + + # Minimal env forwarding into container (encoded via podman -e flags) + container_env = self._container_env_from(env, host_gateway=host_gateway, config=config) + container_env["HOME"] = config.image_workdir + connection_args = self._resolve_podman_connection_args() + user_args: list[str] = [] + uid = getattr(os, "getuid", None) + gid = getattr(os, "getgid", None) + if callable(uid) and callable(gid): + try: + user_args = ["--user", f"{uid()}:{gid()}"] + except Exception: + user_args = [] + wrapped_cmd: list[str] = [ + runtime, + *connection_args, + "run", + "--rm", + *user_args, + "-v", + f"{host_cfg_root}:{config.image_workdir}:Z", + "-w", + config.image_workdir, + ] + for k, v in container_env.items(): + wrapped_cmd.extend(["-e", f"{k}={v}"]) + wrapped_cmd.append(str(config.image)) + wrapped_cmd.extend(inner_cmd) + + # Important: do NOT keep stdin open (avoids "podman run never ends") + return super().run( + cmd=wrapped_cmd, + env=env, + cfg_root=cfg_root, + config=config, + spawn_error_message=spawn_error_message, + stdin_devnull=True, + ) + + def _resolve_podman_connection_args(self) -> list[str]: + """Return extra args for the `podman` CLI to select a connection. + + Priority: + 1) PODMAN_CONNECTION env var + 2) auto-detect default connection from `podman system connection list --format json` + 3) fallback: no args (let Podman decide; works on native Linux / preconfigured env) + """ + # 1) environment override (nice for CI/users) + env_name = os.environ.get("PODMAN_CONNECTION") + if env_name: + return ["--connection", env_name] + + # 2) auto-detect default connection + try: + proc = subprocess.run( + ["podman", "system", "connection", "list", "--format", "json"], + check=False, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + self._logger.info( + "podman system connection list failed (rc=%s): %s", + proc.returncode, + (proc.stderr or "").strip(), + ) + return [] + + data = json.loads(proc.stdout or "[]") + # entries look like: {"Name": "...", "URI": "...", "Identity": "...", "Default": true, ...} + default = next((x for x in data if x.get("Default") is True), None) + if default and default.get("Name"): + return [ + "--url", + str(default["URI"]), + "--identity", + str(default["Identity"]), + ] + except Exception as exc: + self._logger.info("Failed to auto-detect Podman connection: %r", exc) + return [] + + return [] + + +class DockerRunner(ContainerRunner): + """Run the inner command inside a container via Docker.""" + + def __init__(self, log_path, logger): + super().__init__(log_path, logger) + + def run( + self, + *, + cmd: list[str], + env: dict[str, str], + cfg_root: Path, + config: BaseCLIConfig, + spawn_error_message: str, + ) -> CLIResult: + runtime = "docker" + host_gateway = "host.docker.internal" + host_cfg_root = str(cfg_root.resolve()) + + # Patch mcp.json so container uses host gateway (not 127.0.0.1/localhost) + self._patch_mcp_json(cfg_root=cfg_root, host_gateway=host_gateway) + + inner_cmd = self._rewrite_mcp_config_path(list(cmd), workdir=config.image_workdir) + + # Minimal env forwarding into container (encoded via docker -e flags) + container_env = self._container_env_from(env, host_gateway=host_gateway, config=config) + container_env["HOME"] = config.image_workdir + user_args: list[str] = [] + uid = getattr(os, "getuid", None) + gid = getattr(os, "getgid", None) + if callable(uid) and callable(gid): + try: + user_args = ["--user", f"{uid()}:{gid()}"] + except Exception: + user_args = [] + wrapped_cmd: list[str] = [ + runtime, + "run", + "--rm", + "--add-host", + f"{host_gateway}:host-gateway", + *user_args, + "-v", + f"{host_cfg_root}:{config.image_workdir}", + "-w", + config.image_workdir, + ] + for k, v in container_env.items(): + wrapped_cmd.extend(["-e", f"{k}={v}"]) + wrapped_cmd.append(str(config.image)) + wrapped_cmd.extend(inner_cmd) + + # Important: do NOT keep stdin open (avoids "docker run never ends") + return super().run( + cmd=wrapped_cmd, + env=env, + cfg_root=cfg_root, + config=config, + spawn_error_message=spawn_error_message, + stdin_devnull=True, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/__init__.py new file mode 100644 index 00000000..f14bde1d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .agent import GeminiAgent, GeminiAgentInstance # noqa: F401 diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/agent.py new file mode 100644 index 00000000..19067ccf --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/agent.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +from typing import Any, ClassVar + +from ....core.types import ModelSettings +from ..base import ExecutionBackend, ProxyBackedAgent, ProxyBackedMCPAgentInstance +from .cli import GeminiCLI, GeminiCLIConfig + + +class GeminiAgentInstance(ProxyBackedMCPAgentInstance): + """Self-contained Gemini CLI agent that runs through a LiteLLM proxy.""" + + def __init__( + self, + session_id: str, + model_id: str, + max_steps: int = 150, + execution_backend: ExecutionBackend = ExecutionBackend.AUTO, + model_settings: ModelSettings | None = None, + ): + self._gemini_model_alias = "gemini-2.5-pro" + super().__init__( + session_id, + model_id, + max_steps=max_steps, + model_alias=self._gemini_model_alias, + execution_backend=execution_backend, + model_settings=model_settings, + ) + self._gemini_log = self.paths.agent_dir / "gemini_cli.log" + + @property + def cli_display_name(self) -> str: + return "Gemini CLI" + + def _build_cli(self) -> GeminiCLI: + cfg_dir = self.paths.agent_dir / "gemini_config" + return GeminiCLI( + env=os.environ.copy(), + log_path=self._gemini_log, + config_dir=cfg_dir, + logger=self.logger, + runner=self.execution_backend, + ) + + def _run_cli( + self, + cli: GeminiCLI, + prompt: str, + mcp_host: str, + mcp_port: int, + proxy: Any, + ) -> Any: + config = GeminiCLIConfig( + mcp_host=mcp_host, + mcp_port=mcp_port, + provider_url=proxy.base_url, + backend_model=self.model_id, + gemini_model=self._gemini_model_alias, + allowed_mcp_server_names=["environment"], + allowed_tools=[action.name for action in self.actions], + ) + result = cli.run(prompt=prompt, config=config) + return result.stdout + + +class GeminiAgent(ProxyBackedAgent): + display_name: ClassVar[str] = "Gemini CLI" + slug_name: ClassVar[str] = "gemini_cli" + execution_backend: ExecutionBackend = ExecutionBackend.AUTO + + @classmethod + def _get_instance_class(cls): + return GeminiAgentInstance + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model_id)] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/cli.py b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/cli.py new file mode 100644 index 00000000..935d3fa7 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/cli.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Optional + +from ..base import BaseCLIConfig, BaseCLIWrapper, ExecutionBackend + + +class GeminiCLIConfig(BaseCLIConfig): + backend_model: str + gemini_model: str = "gemini-2.5-pro" + env_key: str = "GEMINI_API_KEY" + server_name: str = "environment" + output_format: str = "text" + approval_mode: str = "yolo" + allowed_mcp_server_names: Optional[list[str]] = None + allowed_tools: Optional[list[str]] = None + image: str = "exgentic-gemini:dev" + + +class GeminiCLI(BaseCLIWrapper): + """Lightweight wrapper for running the Gemini CLI headlessly.""" + + def __init__( + self, + env: Optional[dict[str, str]] = None, + log_path: Optional[Path] = None, + config_dir: Optional[Path] = None, + logger=None, + runner: ExecutionBackend = ExecutionBackend.PROCESS, + ) -> None: + super().__init__( + env=env, + log_path=log_path, + config_dir=config_dir, + logger=logger, + runner=runner, + ) + self._settings_path: Optional[Path] = None + self.config_prefix = "gemini_cli_" + self.spawn_error_message = "Failed to start Gemini CLI" + + # Required hooks -------------------------------------------------- + + def build_env(self, *, cfg_root: Path, prompt: str, config: GeminiCLIConfig) -> dict[str, str]: + env = self.env.copy() + env["GOOGLE_GEMINI_BASE_URL"] = config.provider_url + + api_key = env.get(config.env_key) or env.get("GEMINI_API_KEY") or "dummy-api-key" + env[config.env_key] = api_key + env["GEMINI_API_KEY"] = api_key + + env["HOME"] = str(cfg_root) + return env + + def build_command(self, *, cfg_root: Path, prompt: str, config: GeminiCLIConfig) -> list[str]: + gemini_cfg_dir = cfg_root / ".gemini" + settings_path = gemini_cfg_dir / "settings.json" + self._settings_path = settings_path + + # Rewrite localhost addresses to host gateway for container runners + mcp_host = config.mcp_host + from ..command_runner import ContainerRunner + + if isinstance(self.runner, ContainerRunner) and mcp_host in ("0.0.0.0", "127.0.0.1", "localhost"): + mcp_host = self.runner.host_gateway + + gemini_cfg_dir.mkdir(parents=True, exist_ok=True) + mcp_url = f"http://{mcp_host}:{config.mcp_port}/mcp" + self._ensure_settings(settings_path, config.server_name, mcp_url) + + cmd: list[str] = [ + "gemini", + "--model", + config.gemini_model, + "--output-format", + config.output_format, + "--approval-mode", + config.approval_mode, + ] + if config.allowed_mcp_server_names: + cmd.extend(["--allowed-mcp-server-names", *config.allowed_mcp_server_names]) + if config.allowed_tools: + cmd.extend(["--allowed-tools", *config.allowed_tools]) + cmd.append(prompt) + return cmd + + # Internal helpers ------------------------------------------------- + + def _ensure_settings(self, settings_path: Path, server_name: str, mcp_url: str) -> None: + settings: dict[str, object] = {} + if settings_path.exists(): + try: + with open(settings_path, encoding="utf-8-sig") as fh: + settings = json.load(fh) + except Exception: + settings = {} + + servers = settings.setdefault("mcpServers", {}) + if not isinstance(servers, dict): + servers = {} + settings["mcpServers"] = servers + + servers[server_name] = {"httpUrl": mcp_url, "trust": True} + + settings_path.parent.mkdir(parents=True, exist_ok=True) + with open(settings_path, "w", encoding="utf-8") as fh: + json.dump(settings, fh, indent=2) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/setup.sh b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/setup.sh new file mode 100644 index 00000000..f279d55d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/gemini/setup.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +# Determine container runtime +CONTAINER_CMD="" +if command -v podman >/dev/null 2>&1; then + CONTAINER_CMD="podman" + # Start podman machine if needed (macOS/Windows) + if podman machine list >/dev/null 2>&1; then + MACHINE_STATUS=$(podman machine list --format "{{.Running}}" 2>/dev/null | head -n 1) + if [ -z "$MACHINE_STATUS" ]; then + podman machine init && podman machine start + elif [ "$MACHINE_STATUS" != "true" ]; then + podman machine start + fi + fi +elif command -v docker >/dev/null 2>&1; then + CONTAINER_CMD="docker" +else + echo "Error: Neither Podman nor Docker found." >&2 + exit 1 +fi + +# Build Gemini CLI container image (inline — no external Dockerfile needed) +$CONTAINER_CMD build -t exgentic-gemini:dev -f - . <<'DOCKERFILE' +FROM registry.access.redhat.com/ubi9/nodejs-20 +RUN npm install -g @google/gemini-cli@0.25.0 +WORKDIR /work +CMD ["gemini","--help"] +DOCKERFILE + +echo "Gemini Agent setup complete (using $CONTAINER_CMD)" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/cli/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/agents/cli/requirements.txt new file mode 100644 index 00000000..57fa7d59 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/cli/requirements.txt @@ -0,0 +1 @@ +litellm[proxy] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/__init__.py new file mode 100644 index 00000000..e096de68 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from .harness_agent import HarnessAgent + +__all__ = ["HarnessAgent"] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/evolver.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/evolver.py new file mode 100644 index 00000000..5dea5038 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/evolver.py @@ -0,0 +1,382 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import logging +import time +from typing import Any, Dict, List + +import litellm + +from .harness_store import HarnessStore +from .prompts.evolver import EVOLVER_SYSTEM_PROMPT, build_evolution_user_message +from .retriever import compute_embedding + +logger = logging.getLogger(__name__) + +EVOLVER_TOOLS = [ + { + "type": "function", + "function": { + "name": "read_prompt", + "description": "Read the current system prompt.", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "read_memory", + "description": "Read the current long-term memory document.", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "list_skills", + "description": "List all skills with their names and descriptions.", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "read_skill", + "description": "Read the full body of a specific skill.", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "The skill name to read."}, + }, + "required": ["name"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "edit_prompt", + "description": "Replace the entire system prompt with new content.", + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The full new system prompt text."}, + }, + "required": ["body"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "edit_memory", + "description": "Replace the entire long-term memory document with new content.", + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The full new memory document text."}, + }, + "required": ["body"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "add_skill", + "description": "Add a new skill to the skill library.", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Unique skill name."}, + "description": {"type": "string", "description": "One-line description of what the skill does."}, + "body": {"type": "string", "description": "Full skill content/instructions."}, + }, + "required": ["name", "description", "body"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "edit_skill", + "description": "Modify an existing skill's description and/or body.", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "The skill name to edit."}, + "description": {"type": "string", "description": "New description (optional, omit to keep current)."}, + "body": {"type": "string", "description": "New body (optional, omit to keep current)."}, + }, + "required": ["name"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "delete_skill", + "description": "Delete a skill from the skill library.", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "The skill name to delete."}, + }, + "required": ["name"], + }, + }, + }, +] + + +# ================================================================== # +# Tool executor +# ================================================================== # + +class _EvolverToolExecutor: + """Executes evolver tools against a HarnessStore. + + Tracks changes and enforces constraints (at most 1 edit_prompt, 1 edit_memory). + """ + + def __init__(self, store: HarnessStore, embedding_model: str) -> None: + self.store = store + self.embedding_model = embedding_model + self.ops_applied: List[str] = [] + self._edit_prompt_used = False + self._edit_memory_used = False + + def execute(self, tool_name: str, args: Dict[str, Any]) -> str: + """Execute a tool call and return the result string.""" + if tool_name == "read_prompt": + return self.store.system_prompt or "(Empty)" + + elif tool_name == "read_memory": + return self.store.memory or "(Empty)" + + elif tool_name == "list_skills": + index = self.store.get_skill_index() + if not index: + return "(No skills yet)" + return json.dumps(index, ensure_ascii=False, indent=2) + + elif tool_name == "read_skill": + name = args.get("name", "") + skills = {s.name: s for s in self.store.list_skills()} + skill = skills.get(name) + if skill is None: + return f"ERROR: Skill '{name}' not found." + return f"Name: {skill.name}\nDescription: {skill.description}\n\n{skill.body}" + + elif tool_name == "edit_prompt": + if self._edit_prompt_used: + return "ERROR: edit_prompt already used this session (limit: 1 per task)." + body = args.get("body", "") + if not body: + return "ERROR: 'body' is required." + self.store.edit_prompt(body) + self._edit_prompt_used = True + self.ops_applied.append("edit_prompt") + return "OK: System prompt updated." + + elif tool_name == "edit_memory": + if self._edit_memory_used: + return "ERROR: edit_memory already used this session (limit: 1 per task)." + body = args.get("body", "") + self.store.edit_memory(body) + self._edit_memory_used = True + self.ops_applied.append("edit_memory") + return "OK: Memory updated." + + elif tool_name == "add_skill": + name = args.get("name", "") + description = args.get("description", "") + body = args.get("body", "") + if not name or not description or not body: + return "ERROR: 'name', 'description', and 'body' are all required." + # Check if skill already exists + existing = {s.name for s in self.store.list_skills()} + if name in existing: + return f"ERROR: Skill '{name}' already exists. Use edit_skill to modify it." + self.store.add_skill(name, description, body) + # Compute embedding + emb = compute_embedding(description, model=self.embedding_model) + if emb: + self.store.set_embedding(name, emb) + self.ops_applied.append(f"add_skill:{name}") + return f"OK: Skill '{name}' added." + + elif tool_name == "edit_skill": + name = args.get("name", "") + if not name: + return "ERROR: 'name' is required." + description = args.get("description") + body = args.get("body") + if description is None and body is None: + return "ERROR: At least one of 'description' or 'body' must be provided." + success = self.store.edit_skill(name, description=description, body=body) + if not success: + return f"ERROR: Skill '{name}' not found." + if description: + emb = compute_embedding(description, model=self.embedding_model) + if emb: + self.store.set_embedding(name, emb) + self.ops_applied.append(f"edit_skill:{name}") + return f"OK: Skill '{name}' updated." + + elif tool_name == "delete_skill": + name = args.get("name", "") + if not name: + return "ERROR: 'name' is required." + success = self.store.delete_skill(name) + if not success: + return f"ERROR: Skill '{name}' not found." + self.ops_applied.append(f"delete_skill:{name}") + return f"OK: Skill '{name}' deleted." + + else: + return f"ERROR: Unknown tool '{tool_name}'." + + +# ================================================================== # +# Multi-turn evolver loop +# ================================================================== # + +def _run_evolver_loop( + model: str, + system_prompt: str, + user_message: str, + executor: _EvolverToolExecutor, + max_turns: int = 20, +) -> None: + """Run multi-turn evolver with tools. + + The LLM can read harness state, then make changes via tool calls. + Loop ends when LLM stops calling tools (finish_reason != tool_calls). + """ + messages: List[Dict[str, Any]] = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ] + + for turn in range(max_turns): + # Call LLM + max_attempts = 3 + response = None + for attempt in range(max_attempts): + try: + response = litellm.completion( + model=model, + messages=messages, + tools=EVOLVER_TOOLS, + temperature=0.0, + ) + break + except Exception as exc: + logger.warning("Evolver loop turn %d attempt %d failed: %s", turn, attempt + 1, exc) + if attempt + 1 >= max_attempts: + logger.error("Evolver loop: all attempts failed at turn %d", turn) + return + time.sleep(2 ** attempt) + + if response is None: + break + + choice = response.choices[0] + message = choice.message + + # Check for tool calls + if hasattr(message, "tool_calls") and message.tool_calls: + # Add assistant message to history + assistant_msg: Dict[str, Any] = {"role": "assistant", "content": message.content or ""} + assistant_msg["tool_calls"] = [ + { + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments, + }, + } + for tc in message.tool_calls + ] + messages.append(assistant_msg) + + # Execute each tool call + for tc in message.tool_calls: + try: + args = json.loads(tc.function.arguments) if tc.function.arguments else {} + except json.JSONDecodeError: + args = {} + + result = executor.execute(tc.function.name, args) + # Log each tool call with args summary and result preview + args_summary = ", ".join(f"{k}={repr(v)[:60]}" for k, v in args.items()) + logger.info( + "Evolver turn %d: %s(%s) → %s", + turn, tc.function.name, args_summary, result[:120], + ) + messages.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": result, + }) + + continue # Next turn + else: + # LLM finished (no more tool calls) + logger.info("Evolver finished after %d turns", turn + 1) + break + + +# ================================================================== # +# Main evolver entry point +# ================================================================== # + +def run_evolver( + store: HarnessStore, + task: str, + injected_skill_names: List[str], + trajectory: str, + llm_call: Any, # kept for interface compat (unused in multi-turn impl) + evolver_model: str, + embedding_model: str = "all-MiniLM-L6-v2", +) -> tuple[int, List[str]]: + """Run the evolver: multi-turn tool-calling to read and modify harness. + + Returns (number of ops applied, list of op summaries). + """ + # Take snapshot for full rollback on catastrophic failure + full_snapshot = store.snapshot() + + try: + # Build user message + user_message = build_evolution_user_message( + task=task, + injected_skill_names=injected_skill_names, + trajectory=trajectory, + ) + + # Create tool executor + executor = _EvolverToolExecutor(store, embedding_model) + + # Run multi-turn loop + _run_evolver_loop( + model=evolver_model, + system_prompt=EVOLVER_SYSTEM_PROMPT, + user_message=user_message, + executor=executor, + max_turns=20, + ) + + ops_applied = len(executor.ops_applied) + logger.info("Harness evolver: %d ops applied: %s", ops_applied, executor.ops_applied) + return ops_applied, executor.ops_applied + + except Exception as exc: + logger.warning("Harness evolver failed, rolling back: %s", exc) + store.rollback(full_snapshot) + return 0, [] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_agent.py new file mode 100644 index 00000000..19dcc5db --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_agent.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar, Optional + +from pydantic import ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings +from ...utils.settings import RunnerName + + +class HarnessAgent(Agent): + + display_name: ClassVar[str] = "Harness Agent" + slug_name: ClassVar[str] = "harness" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + model: str = "gpt-4o" + evolver_model: Optional[str] = None + + top_k_skills: int = 3 + embedding_model: str = "all-MiniLM-L6-v2" + + shuffle_mode: str = "isolated" + + benchmark_id: Optional[str] = None + + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + + runner: RunnerName | None = None + model_settings: ModelSettings | None = None + + @classmethod + def _get_instance_class(cls): + from .harness_instance import HarnessAgentInstance + return HarnessAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.harness.harness_instance:HarnessAgentInstance" + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "evolver_model": self.evolver_model or self.model, + "top_k_skills": self.top_k_skills, + "embedding_model": self.embedding_model, + "shuffle_mode": self.shuffle_mode, + "model_settings": self.model_settings, + "benchmark_id": self.benchmark_id, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + } + + @property + def model_name(self) -> str: + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: + names = [str(self.model)] + em = self.evolver_model or self.model + if em != self.model: + names.append(str(em)) + return names diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_instance.py new file mode 100644 index 00000000..14fc1d27 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_instance.py @@ -0,0 +1,579 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +import litellm +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionSystemMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, +) +from ...utils.cost import LiteLLMCostReport +from ...utils.settings import get_settings + +from .evolver import run_evolver +from .harness_store import HarnessStore +from .prompts.inject import build_system_message +from .retriever import retrieve_skills + +try: + from ...agents.litellm_tool_calling.utils import ToolCall, ToolsActionsRegistry +except ImportError: + ToolsActionsRegistry = None + ToolCall = dict + +from ..tool_shortlisting import shortlist_tools + +settings = get_settings() + + +class HarnessAgentInstance(AgentInstance): + + def __init__( + self, + session_id: str, + model: str = "gpt-4o", + evolver_model: str = "gpt-4o", + top_k_skills: int = 3, + embedding_model: str = "all-MiniLM-L6-v2", + shuffle_mode: str = "isolated", + model_settings: Optional[ModelSettings] = None, + benchmark_id: Optional[str] = None, + enable_tool_shortlisting: bool = False, + max_selected_tools: int = 30, + ) -> None: + super().__init__(session_id) + + self.model = model + self.evolver_model = evolver_model + self.top_k_skills = top_k_skills + self.embedding_model = embedding_model + self.shuffle_mode = shuffle_mode + self.benchmark_id = benchmark_id + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + + if model_settings is None: + self._model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self._model_settings = model_settings + else: + self._model_settings = ModelSettings() + + self._cost = LiteLLMCostReport.initialize_empty(model_name=self.model) + self._store: Optional[HarnessStore] = None + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ChatCompletionSystemMessage, + ] + ] = [] + self._step_count: int = 0 + + self._registry: Optional[ToolsActionsRegistry] = None + self._all_actions: list[ActionType] = [] + + self._observation_log: List[Dict[str, Any]] = [] + self._action_log: List[Dict[str, Any]] = [] + + self._injected_skill_names: List[str] = [] + + def _log_failure( + self, component: str, error: Exception, context: Dict[str, Any] + ) -> None: + try: + log_path = self.paths.agent_dir / "harness_failures.jsonl" + log_path.parent.mkdir(parents=True, exist_ok=True) + entry = { + "timestamp": datetime.now().isoformat(), + "session_id": self.session_id, + "component": component, + "error_type": type(error).__name__, + "error_message": str(error)[:2000], + **{k: str(v)[:2000] if isinstance(v, str) else v + for k, v in context.items()}, + } + with open(log_path, "a", encoding="utf-8") as fh: + fh.write(json.dumps(entry, ensure_ascii=False) + "\n") + except Exception: + pass + + def start( + self, + task: str, + context: Dict[str, Any], + actions: list[ActionType], + ) -> None: + super().start(task, context, actions) + + self._all_actions = list(self.actions) + if ToolsActionsRegistry is not None: + self._registry = ToolsActionsRegistry(self._all_actions) + + task_group = str( + context.get("task_group") + or context.get("task_id") + or context.get("task_name") + or "default" + ) + self._store = HarnessStore.get_or_create( + shuffle_mode=self.shuffle_mode, + task_group=task_group, + benchmark_id=self.benchmark_id, + ) + self._store.increment_session() + + retrieved = [] + if self._store.skill_count > 0: + try: + retrieved = retrieve_skills( + task_text=task, + store=self._store, + top_k=self.top_k_skills, + embedding_model=self.embedding_model, + ) + except Exception as exc: + self.logger.warning("Harness: skill retrieval failed: %s", exc) + + self._injected_skill_names = [s.name for s, _ in retrieved] + + # Mark retrieved skills as used (LRU) + if self._injected_skill_names: + self._store.touch_skills(self._injected_skill_names) + + # Build system message + system_content = build_system_message( + system_prompt=self._store.system_prompt, + memory=self._store.memory, + retrieved_skills=retrieved, + ) + self._add_message( + ChatCompletionSystemMessage(role="system", content=system_content) + ) + self.logger.info( + "Harness system message built: prompt_len=%d memory_len=%d " + "skills_injected=%s total_system_len=%d", + len(self._store.system_prompt), + len(self._store.memory), + self._injected_skill_names, + len(system_content), + ) + + content_parts: list[Any] = [] + ctx = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message( + ChatCompletionUserMessage(role="user", content=text_content) + ) + + self.logger.info( + "Harness instance started store=%s session_count=%d " + "skill_count=%d skills_injected=%d benchmark=%s tools=%d", + self._store.store_id, + self._store.session_count, + self._store.skill_count, + len(self._injected_skill_names), + self.benchmark_id or "(none)", + len(self._all_actions), + ) + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + self._step_count += 1 + + self._observe(observation) + self._log_observation(observation) + + tools = self._assistant_tools() + response = self._completion( + model=self.model, + messages=self.messages, + tools=tools if tools else None, + ) + + if response is None: + self.logger.error("Harness: LLM returned None response") + return None + + if response.usage: + self._cost.update_cost_from_tokens( + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls" and self._registry is not None: + tool_calls = self._extract_tool_calls(message) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": tc["arguments"], + }, + } + for tc in tool_calls + ], + ) + ) + actions = self._registry.tool_calls_to_action(tool_calls) + + for tc in tool_calls: + self._action_log.append({ + "step": self._step_count, + "action": tc["name"], + "arguments": tc["arguments"], + }) + + self.logger.info("Harness step %d: tool_calls=%s", self._step_count, + [tc["name"] for tc in tool_calls]) + return actions + else: + content = message.content if message.content else "" + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", content=content + ) + ) + self._action_log.append({ + "step": self._step_count, + "action": "message", + "content": content, + }) + self.logger.info("Harness step %d: message response", self._step_count) + return MessageAction(arguments=Message(content=content)) + + def close(self) -> None: + store = self._store + if store is None: + return + + ops_applied = 0 + ops_summary: list[str] = [] + + self.logger.info( + "Harness close: starting evolver trajectory_steps=%d " + "actions=%d observations=%d", + self._step_count, len(self._action_log), len(self._observation_log), + ) + + if self._observation_log or self._action_log: + try: + trajectory = self._build_session_trace() + self.logger.info( + "Harness close: trajectory built length=%d chars", len(trajectory) + ) + ops_applied, ops_summary = run_evolver( + store=store, + task=self.task if self.task else "", + injected_skill_names=self._injected_skill_names, + trajectory=trajectory, + llm_call=self._llm_call_simple, + evolver_model=self.evolver_model, + embedding_model=self.embedding_model, + ) + self.logger.info( + "Harness close: evolver done ops_applied=%d ops=%s", + ops_applied, ops_summary, + ) + except Exception as exc: + self.logger.warning("Harness: evolver failed: %s", exc) + self._log_failure("evolver", exc, { + "observation_count": len(self._observation_log), + "action_count": len(self._action_log), + }) + + if ops_applied > 0: + version = store.commit_version( + session_id=self.session_id, + ops_summary=ops_summary, + ) + self.logger.info( + "Harness close: version committed v=%d skill_count=%d", + version, store.skill_count, + ) + + store.record_learning( + session_id=self.session_id, + task_id=str(self.context.get("task_id", "") if self.context else ""), + benchmark_id=self.benchmark_id or "", + ops_applied=ops_applied, + ops_summary=ops_summary, + ) + + try: + cp = str(self.paths.agent_dir / "harness_checkpoint.json") + store.save_checkpoint(cp) + txt = str(self.paths.agent_dir / "harness_state.md") + store.save_harness_text(txt) + except Exception as exc: + self.logger.warning("Harness: failed to save checkpoint: %s", exc) + + def get_cost(self) -> LiteLLMCostReport: + return self._cost + + def _add_message(self, message: Any) -> None: + self.messages.append(message) + + def _observe(self, observation: Optional[Observation]) -> None: + if observation is None: + return + + observations = observation.to_observation_list() + if observation.is_empty(): + if not any(obs.invoking_actions for obs in observations): + return + + for obs in observations: + if isinstance(obs, MessageObservation) and isinstance( + obs.result, MessagePayload + ): + self._add_message( + ChatCompletionUserMessage( + role="user", content=obs.result.message + ) + ) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + continue + + action_id = invoking.id + tool_call_id = invoking.id + if not ( + isinstance(tool_call_id, str) + and tool_call_id.startswith("call_") + ): + if self._registry is not None: + tool_call_id = ( + self._registry.action_id_to_tool_call_id.get( + action_id, tool_call_id + ) + ) + + value = obs.result + try: + content = json.dumps( + value, ensure_ascii=False, separators=(",", ":") + ) + except TypeError: + content = str(value) + + if tool_call_id is not None: + self._add_message( + ChatCompletionToolMessage( + role="tool", + tool_call_id=tool_call_id, + content=content, + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", + content=f"Tool result: {content}", + ) + ) + else: + self._add_message( + ChatCompletionUserMessage( + role="user", content=str(obs) + ) + ) + + def _log_observation(self, observation: Optional[Observation]) -> None: + if observation is None or observation.is_empty(): + return + + for obs in observation.to_observation_list(): + result = obs.result + if result is None: + continue + + entry: Dict[str, Any] = {"step": self._step_count} + if isinstance(result, str): + entry["content"] = result + elif isinstance(result, dict): + entry["content"] = json.dumps(result, ensure_ascii=False) + else: + entry["content"] = str(result) + + if obs.invoking_actions: + entry["action"] = obs.invoking_actions[0].name + + self._observation_log.append(entry) + + def _assistant_tools(self) -> list[dict[str, Any]]: + if self._registry is None: + return [] + tools = self._registry.openai_tools() + if not self.enable_tool_shortlisting: + return tools + + def _cost_cb(usage): + if usage: + self._cost.update_cost_from_tokens( + usage.prompt_tokens, usage.completion_tokens + ) + + return shortlist_tools( + tools=tools, + max_selected=self.max_selected_tools, + messages=self.messages, + completion_fn=self._completion, + model=self.model, + logger=self.logger, + cost_callback=_cost_cb, + ) + + @staticmethod + def _extract_tool_calls(message: Any) -> list[dict[str, str]]: + if not hasattr(message, "tool_calls") or not message.tool_calls: + return [] + tool_calls = [] + for tc in message.tool_calls: + tool_calls.append({ + "name": tc.function.name, + "arguments": tc.function.arguments, + "id": tc.id, + }) + return tool_calls + + def _completion(self, **kwargs) -> Any: + call_kwargs = self._model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + if call_kwargs.get("tools") is None: + call_kwargs.pop("tools", None) + + max_attempts = 3 + for attempt in range(max_attempts): + try: + response = litellm.completion(**call_kwargs) + return response + except Exception as exc: + self.logger.warning( + "Harness LLM call attempt %d/%d failed: %s", + attempt + 1, max_attempts, exc, + ) + if attempt + 1 >= max_attempts: + raise + time.sleep(2 ** attempt) + return None + + def _llm_call_simple( + self, + model: str, + prompt: str, + *, + json_mode: bool = False, + ) -> str: + kwargs: Dict[str, Any] = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.0, + } + if json_mode: + kwargs["response_format"] = {"type": "json_object"} + + max_attempts = 3 + for attempt in range(max_attempts): + try: + resp = litellm.completion(**kwargs) + if resp.usage: + self._cost.update_cost_from_tokens( + resp.usage.prompt_tokens, + resp.usage.completion_tokens, + ) + content = resp.choices[0].message.content + if content is None: + raise ValueError("LLM returned None content") + return content + except Exception as exc: + self.logger.warning( + "Harness simple LLM call attempt %d/%d failed: %s", + attempt + 1, max_attempts, exc, + ) + if attempt + 1 >= max_attempts: + self._log_failure("llm_call", exc, { + "model": model, + "prompt_length": len(prompt), + "attempts": max_attempts, + }) + raise + time.sleep(2 ** attempt) + return "" + + def _build_session_trace(self) -> str: + events: List[Dict[str, Any]] = [] + for entry in self._action_log: + events.append({"type": "action", **entry}) + for entry in self._observation_log: + events.append({"type": "observation", **entry}) + + events.sort(key=lambda e: (e.get("step", 0), 0 if e["type"] == "action" else 1)) + + lines: List[str] = [] + for event in events: + step = event.get("step", "?") + if event["type"] == "action": + action = event.get("action", "?") + args = event.get("arguments", event.get("content", "")) + lines.append(f"[Step {step}] Action: {action}") + if args: + lines.append(f" Args: {str(args)[:500]}") + else: + action = event.get("action", "env") + content = event.get("content", "") + lines.append(f"[Step {step}] Observation from {action}:") + lines.append(f" {str(content)[:500]}") + + return "\n".join(lines) if lines else "(No session trace recorded)" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_store.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_store.py new file mode 100644 index 00000000..2c3e3a21 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/harness_store.py @@ -0,0 +1,320 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import copy +import json +import threading +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional + + +DEFAULT_SYSTEM_PROMPT = """\ +You are an expert agent that completes tasks using available tools. +Think step-by-step before acting. +Use available tools to interact with the environment. +When you are confident in your solution, use the finish/submit tool.""" + +DEFAULT_MEMORY = "" + + +@dataclass +class HarnessSkill: + + name: str + description: str + body: str + last_used_session: int = 0 + created_session: int = 0 + + +@dataclass +class VersionEntry: + + version: int + session_id: str + session_count: int + ops_summary: List[str] + timestamp: str = "" + system_prompt: str = "" + memory: str = "" + skills: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class LearningEvent: + + session_id: str + task_id: str + benchmark_id: str + ops_applied: int + ops_summary: List[str] = field(default_factory=list) + timestamp: str = "" + + +class HarnessStore: + + _instances: Dict[str, "HarnessStore"] = {} + _class_lock = threading.Lock() + + def __init__(self, store_id: str) -> None: + self._store_id = store_id + self._lock = threading.Lock() + self.system_prompt: str = DEFAULT_SYSTEM_PROMPT + self.memory: str = DEFAULT_MEMORY + self.skills: Dict[str, HarnessSkill] = {} + self.skill_embeddings: Dict[str, List[float]] = {} + self._session_count: int = 0 + self._history: List[LearningEvent] = [] + self._versions: List[VersionEntry] = [] + self._next_version: int = 0 + + @property + def store_id(self) -> str: + return self._store_id + + @property + def session_count(self) -> int: + with self._lock: + return self._session_count + + @property + def skill_count(self) -> int: + with self._lock: + return len(self.skills) + + @classmethod + def get_or_create( + cls, + shuffle_mode: str = "isolated", + benchmark_id: Optional[str] = None, + task_group: Optional[str] = None, + ) -> "HarnessStore": + if shuffle_mode == "isolated": + key = f"harness_isolated_{benchmark_id or task_group or 'default'}" + elif shuffle_mode == "sequential": + key = "harness_sequential_global" + elif shuffle_mode == "interleaved": + key = "harness_interleaved_global" + else: + key = f"harness_{shuffle_mode}" + + with cls._class_lock: + if key not in cls._instances: + cls._instances[key] = cls(store_id=key) + return cls._instances[key] + + @classmethod + def list_stores(cls) -> Dict[str, "HarnessStore"]: + with cls._class_lock: + return dict(cls._instances) + + @classmethod + def reset_all(cls) -> None: + with cls._class_lock: + cls._instances.clear() + + def increment_session(self) -> int: + with self._lock: + self._session_count += 1 + return self._session_count + + def add_skill(self, name: str, description: str, body: str) -> None: + with self._lock: + self.skills[name] = HarnessSkill( + name=name, + description=description, + body=body, + last_used_session=self._session_count, + created_session=self._session_count, + ) + + def edit_skill( + self, name: str, description: Optional[str] = None, body: Optional[str] = None + ) -> bool: + with self._lock: + skill = self.skills.get(name) + if skill is None: + return False + if description is not None: + skill.description = description + if body is not None: + skill.body = body + return True + + def delete_skill(self, name: str) -> bool: + with self._lock: + if name in self.skills: + del self.skills[name] + self.skill_embeddings.pop(name, None) + return True + return False + + def touch_skills(self, names: List[str]) -> None: + with self._lock: + for name in names: + skill = self.skills.get(name) + if skill: + skill.last_used_session = self._session_count + + def get_skill_index(self) -> List[Dict[str, str]]: + with self._lock: + return [ + {"name": s.name, "description": s.description} + for s in self.skills.values() + ] + + def list_skills(self) -> List[HarnessSkill]: + with self._lock: + return list(self.skills.values()) + + def edit_prompt(self, new_prompt: str) -> None: + with self._lock: + self.system_prompt = new_prompt + + def edit_memory(self, new_memory: str) -> None: + with self._lock: + self.memory = new_memory + + def set_embedding(self, skill_name: str, embedding: List[float]) -> None: + with self._lock: + self.skill_embeddings[skill_name] = embedding + + def get_embeddings(self) -> Dict[str, List[float]]: + with self._lock: + return dict(self.skill_embeddings) + + def snapshot(self) -> Dict[str, Any]: + with self._lock: + return { + "system_prompt": self.system_prompt, + "memory": self.memory, + "skills": copy.deepcopy(self.skills), + "skill_embeddings": copy.deepcopy(self.skill_embeddings), + } + + def rollback(self, snap: Dict[str, Any]) -> None: + with self._lock: + self.system_prompt = snap["system_prompt"] + self.memory = snap["memory"] + self.skills = snap["skills"] + self.skill_embeddings = snap["skill_embeddings"] + + def commit_version(self, session_id: str, ops_summary: List[str]) -> int: + with self._lock: + version = self._next_version + self._next_version += 1 + entry = VersionEntry( + version=version, + session_id=session_id, + session_count=self._session_count, + ops_summary=ops_summary, + timestamp=datetime.now().isoformat(), + system_prompt=self.system_prompt, + memory=self.memory, + skills={name: asdict(s) for name, s in self.skills.items()}, + ) + self._versions.append(entry) + return version + + def record_learning( + self, + session_id: str, + task_id: str, + benchmark_id: str, + ops_applied: int, + ops_summary: Optional[List[str]] = None, + ) -> None: + with self._lock: + self._history.append(LearningEvent( + session_id=session_id, + task_id=task_id, + benchmark_id=benchmark_id, + ops_applied=ops_applied, + ops_summary=ops_summary or [], + timestamp=datetime.now().isoformat(), + )) + + def save_checkpoint(self, path: str) -> None: + with self._lock: + data = { + "store_id": self._store_id, + "session_count": self._session_count, + "system_prompt": self.system_prompt, + "memory": self.memory, + "skills": {name: asdict(s) for name, s in self.skills.items()}, + "skill_embeddings": self.skill_embeddings, + "history": [asdict(e) for e in self._history], + "versions": [ + { + "version": v.version, + "session_id": v.session_id, + "session_count": v.session_count, + "ops_summary": v.ops_summary, + "timestamp": v.timestamp, + "skill_names": list(v.skills.keys()), + } + for v in self._versions + ], + } + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def load_checkpoint(self, path: str) -> None: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + with self._lock: + self._session_count = data.get("session_count", 0) + self.system_prompt = data.get("system_prompt", DEFAULT_SYSTEM_PROMPT) + self.memory = data.get("memory", DEFAULT_MEMORY) + self.skills = {} + for name, sdata in data.get("skills", {}).items(): + self.skills[name] = HarnessSkill(**{ + k: v for k, v in sdata.items() + if k in HarnessSkill.__dataclass_fields__ + }) + self.skill_embeddings = data.get("skill_embeddings", {}) + self._history = [ + LearningEvent(**{ + k: v for k, v in e.items() + if k in LearningEvent.__dataclass_fields__ + }) + for e in data.get("history", []) + ] + self._versions = [] + for vdata in data.get("versions", []): + self._versions.append(VersionEntry( + version=vdata["version"], + session_id=vdata.get("session_id", ""), + session_count=vdata.get("session_count", 0), + ops_summary=vdata.get("ops_summary", []), + timestamp=vdata.get("timestamp", ""), + skills={name: {} for name in vdata.get("skill_names", [])}, + )) + self._next_version = ( + self._versions[-1].version + 1 if self._versions else 0 + ) + + def save_harness_text(self, path: str) -> None: + with self._lock: + skills = list(self.skills.values()) + lines = [ + f"# Harness State: {self._store_id}", + f"## System Prompt", + self.system_prompt, + "", + f"## Memory", + self.memory, + "", + f"## Skills ({len(skills)})", + ] + for s in skills: + lines.append(f"### {s.name}") + lines.append(f"Description: {s.description}") + lines.append(f"Last used: session {s.last_used_session}") + lines.append(s.body) + lines.append("") + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/__init__.py new file mode 100644 index 00000000..d0286a84 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. +from .inject import build_system_message +from .evolver import EVOLVER_SYSTEM_PROMPT, build_evolution_user_message + +__all__ = ["build_system_message", "EVOLVER_SYSTEM_PROMPT", "build_evolution_user_message"] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/evolver.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/evolver.py new file mode 100644 index 00000000..05a5e014 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/evolver.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +EVOLVER_SYSTEM_PROMPT = """\ +You are an evolution engine for an agent harness. Your job is to analyze a completed task session and improve the agent's harness (system prompt, long-term memory, and skill library) for future tasks. + +## Available Tools + +**Read tools** (use these first to inspect current state): +- read_prompt() — read the current system prompt +- read_memory() — read the current long-term memory document +- list_skills() — list all skills with names and descriptions +- read_skill(name) — read a specific skill's full body + +**Write tools** (use these to make changes): +- edit_prompt(body) — replace the entire system prompt +- edit_memory(body) — replace the entire memory document +- add_skill(name, description, body) — add a new skill +- edit_skill(name, description?, body?) — modify an existing skill +- delete_skill(name) — remove a skill + +## Constraints +- At most 1 edit_prompt call per session. +- At most 1 edit_memory call per session. +- No limit on skill operations. + +## Guidelines +- First READ the current harness state, then decide what changes to make. +- Skills should be generalizable (useful across tasks), not task-specific. +- Memory should capture recurring patterns, proven strategies, and environment quirks. +- System prompt changes should refine the agent's general approach. +- Do NOT duplicate information already present in the harness. +- If no changes are needed, simply stop without calling any write tools. +""" + + +def build_evolution_user_message( + task: str, + injected_skill_names: list[str], + trajectory: str, +) -> str: + parts = [ + "## This Session\n", + f"### Task\n{task}\n", + f"### Skills Injected\n{', '.join(injected_skill_names) if injected_skill_names else '(None)'}\n", + f"### Session Trajectory\n{trajectory}\n", + "\n---\n", + "Analyze the session above. Read the current harness state using the read tools, " + "then decide what changes (if any) would improve the agent's future performance. " + "Make changes using the write tools, or stop if no changes are needed.", + ] + return "\n".join(parts) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/inject.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/inject.py new file mode 100644 index 00000000..9b1e237b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/prompts/inject.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import List, Tuple + +from ..harness_store import HarnessSkill + + +def build_system_message( + system_prompt: str, + memory: str, + retrieved_skills: List[Tuple[HarnessSkill, float]], +) -> str: + parts = [system_prompt] + + if memory and memory.strip(): + parts.append("\n\n## Long-Term Memory\n") + parts.append(memory) + + if retrieved_skills: + parts.append("\n\n## Retrieved Skills\n") + for skill, _score in retrieved_skills: + parts.append(f"### Skill: {skill.name}") + parts.append(f"*{skill.description}*\n") + parts.append(skill.body) + parts.append("") + + return "\n".join(parts) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/harness/retriever.py b/labs/AgentStream/exgentic/src/exgentic/agents/harness/retriever.py new file mode 100644 index 00000000..2299a2cd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/harness/retriever.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +import math +from typing import List, Tuple + +from .harness_store import HarnessSkill, HarnessStore + +logger = logging.getLogger(__name__) + +_st_model = None +_st_model_name = None + + +def _get_st_model(model_name: str = "all-MiniLM-L6-v2"): + global _st_model, _st_model_name + if _st_model is None or _st_model_name != model_name: + from sentence_transformers import SentenceTransformer + logger.info("Loading SentenceTransformer model: %s", model_name) + _st_model = SentenceTransformer(model_name) + _st_model_name = model_name + return _st_model + + +def compute_embedding( + text: str, model: str = "all-MiniLM-L6-v2" +) -> List[float]: + try: + st = _get_st_model(model) + vec = st.encode([text])[0] + return vec.tolist() + except Exception as exc: + logger.error("Local embedding failed: %s", exc) + return [] + + +def cosine_similarity(a: List[float], b: List[float]) -> float: + if not a or not b or len(a) != len(b): + return 0.0 + dot = sum(x * y for x, y in zip(a, b)) + norm_a = math.sqrt(sum(x * x for x in a)) + norm_b = math.sqrt(sum(x * x for x in b)) + if norm_a == 0 or norm_b == 0: + return 0.0 + return dot / (norm_a * norm_b) + + +def retrieve_skills( + task_text: str, + store: HarnessStore, + top_k: int = 5, + embedding_model: str = "all-MiniLM-L6-v2", +) -> List[Tuple[HarnessSkill, float]]: + if store.skill_count == 0: + return [] + + query_embedding = compute_embedding(task_text, model=embedding_model) + if not query_embedding: + logger.warning("Failed to compute query embedding, returning no skills") + return [] + + skills = store.list_skills() + embeddings = store.get_embeddings() + for skill in skills: + if skill.name not in embeddings: + emb = compute_embedding(skill.description, model=embedding_model) + if emb: + store.set_embedding(skill.name, emb) + + embeddings = store.get_embeddings() + scored: List[Tuple[HarnessSkill, float]] = [] + for skill in skills: + emb = embeddings.get(skill.name) + if emb: + score = cosine_similarity(query_embedding, emb) + scored.append((skill, score)) + + scored.sort(key=lambda x: x[1], reverse=True) + results = scored[:top_k] + + if results: + logger.info( + "Skill retrieval: query='%s...' → retrieved %d/%d skills: %s", + task_text[:60], len(results), len(skills), + [(s.name, f"{score:.3f}") for s, score in results], + ) + else: + logger.info("Skill retrieval: no skills scored above 0") + + return results diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/instance.py new file mode 100644 index 00000000..9d44cbb1 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/instance.py @@ -0,0 +1,486 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import time +from typing import Any, Union + +import litellm +litellm.cache = None # Disable LiteLLM +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionDeveloperMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.context import get_context +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, + RetryStrategy, +) +from ...integrations.litellm.health import check_model_accessible_sync +from ...utils.cost import LiteLLMCostReport +from ...utils.settings import get_settings +from .utils import ToolCall, ToolsActionsRegistry + +settings = get_settings() + + +class NonRetryableCompletionError(ValueError): + """Raised when a completion response should not be retried.""" + + +class LiteLLMToolCallingAgentInstance(AgentInstance): + """Ultra-simple tool-calling agent. + + - If the model produces tool_calls, convert them directly to Actions without schema verification. + - If the model produces a plain assistant message, interpret it as a `message` action + (even if that tool is not advertised) and emit a corresponding Action. + """ + + def __init__( + self, + session_id: str, + model: str = "gpt-4o-mini", + max_steps: int = 150, + enable_tool_shortlisting: bool = True, + max_selected_tools: int = 30, + model_settings: ModelSettings | None = None, + allow_truncated_messages: bool = False, + ): + super().__init__(session_id) + self.model = model + self.max_steps = max_steps + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + if model_settings is None: + self.model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self.model_settings = model_settings + else: + raise ValueError("model_settings must be a ModelSettings instance.") + self._allow_truncated_messages = allow_truncated_messages + self._use_cache = settings.litellm_caching + self.logger.debug( + "LiteLLM cache %s (dir=%s)", + "enabled" if self._use_cache else "disabled", + settings.resolved_litellm_cache_dir(), + ) + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ] + ] = [] + self._step_count = 0 + self._cost_data = LiteLLMCostReport.initialize_empty(model_name=self.model) + + # Check model accessibility + check_model_accessible_sync(self.model, logger=self.logger) + + def start(self, task, context, actions): + """Receive work payload, build tool registry, and seed conversation.""" + super().start(task, context, actions) + + for a in self.actions: + if not isinstance(a, ActionType): + raise ValueError("Invalid action type provided to agent") + + self._all_actions: list[ActionType] = list(self.actions) + self._registry = ToolsActionsRegistry(self._all_actions) + + # Seed conversation with task + context + content_parts: list[Any] = [] + ctx = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message(ChatCompletionUserMessage(role="user", content=text_content)) + + def _register_cost(self, usage: litellm.Usage): + self._cost_data.update_cost_from_tokens(usage.prompt_tokens, usage.completion_tokens) + + def _add_message(self, message): + self.logger.info(f"Adding message to chat history: {message}") + self.messages.append(message) + + def _observe(self, observation: Observation | None): + if observation is None: + self.logger.info("Skipping observation: None") + return + + observations = observation.to_observation_list() + if observation.is_empty(): + # Preserve tool results even when the result payload is empty. + if not any(obs.invoking_actions for obs in observations): + self.logger.info("Skipping observation: empty with no invoking_actions") + return + + for obs in observations: + # Structured user messages: add and move on + if isinstance(obs, MessageObservation) and isinstance(obs.result, MessagePayload): + self._add_message(ChatCompletionUserMessage(role="user", content=obs.result.message)) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + # Fallback: treat as user-visible content + self._add_message(ChatCompletionUserMessage(role="user", content=str(obs))) + continue + action_id = invoking.id + tool_call_id = invoking.id + if not (isinstance(tool_call_id, str) and tool_call_id.startswith("call_")): + tool_call_id = self._registry.action_id_to_tool_call_id.get(action_id) + if tool_call_id is None: + raise RuntimeError(f"Unable to map tool call id for action {action_id}") + value = obs.result + + # Extract image_url entries for vision support + image_parts: list[dict] = [] + if isinstance(value, dict): + text_value = {} + for k, v in value.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + image_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + text_value[k] = v + if image_parts: + value = text_value # Tool result without image data + + try: + content = json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except TypeError: + content = str(value) + self._add_message(ChatCompletionToolMessage(role="tool", tool_call_id=tool_call_id, content=content)) + + # Add images as follow-up user message for vision models + if image_parts: + image_parts.insert(0, {"type": "text", "text": "Observation screenshot:"}) + self._add_message(ChatCompletionUserMessage(role="user", content=image_parts)) + else: + # Initial observation (no invoking actions) — handle vision content + value = obs.result + image_parts: list[dict] = [] + if isinstance(value, dict): + text_value = {} + for k, v in value.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + image_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + text_value[k] = v + if image_parts: + value = text_value + if image_parts: + try: + text_content = json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except TypeError: + text_content = str(value) + content_parts: list[dict] = [{"type": "text", "text": f"Initial observation: {text_content}"}] + content_parts.extend(image_parts) + self._add_message(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self._add_message(ChatCompletionUserMessage(role="user", content=str(obs))) + + def _assistant_tools(self) -> list[dict[str, Any]]: + """Returns list of available tools in openai format. + + If the number of available tools is less the max_selected_tools parameter, + returns all available tools. + + Otherwise, calls an LLM to shortlist the tools, and find the most relevant one for + the current stage in the chat, + + """ + tools = self._registry.openai_tools() + + if not self.enable_tool_shortlisting: + self.logger.info( + "Tool shortlisting disabled: returning all %d tools", + len(tools), + ) + return tools + + if len(tools) <= self.max_selected_tools: + self.logger.info( + "Tool shortlist bypassed: %d tools <= max_selected_tools", + len(tools), + ) + return tools + self.logger.info( + "Selecting tools: %d available -> top %d", + len(tools), + self.max_selected_tools, + ) + + names = [tool["function"]["name"] for tool in tools] + + names_str = "" + for tool in tools: + names_str += f"\n- {tool['function']['name']}: {tool['function']['description']}" + + history_text = self._render_history_for_shortlist() + self.logger.info("Tool shortlist history chars: %d", len(history_text)) + + dev = ChatCompletionDeveloperMessage( + role="developer", + content=( + f"Please before providing your next move list the names of the top " + f"{self.max_selected_tools} tools that are somewhat relevant for the next step, " + "ordered by relevancy (most to least). Return ONLY a JSON object with this shape: " + '{\n "tools": ["tool_name_1", "tool_name_2", ...]\n}.\n' + f"Choose from these tools only: {names_str}.\n" + f"Do not call any of those tools just return the list of the top " + f"{self.max_selected_tools} relevant tools names in the required format." + ), + ) + history = ChatCompletionUserMessage( + role="user", + content=f"Conversation so far (plain text):\n{history_text}", + ) + + try: + response = self._completion( + model=self.model, + messages=[dev, history], + caching=self._use_cache, + ) + except Exception as exc: + self.logger.warning("Tool shortlisting LLM call failed: %s", exc) + return tools[: self.max_selected_tools] + + self._register_cost(response.usage) + + text = response.choices[0].message["content"] + + if text is None: + text = str(response.choices[0].message) + + self.logger.info("Tool shortlist model response: %s", text) + + positions = [] + for name in names: + idx = text.find(name) + if idx != -1: + positions.append((idx, name)) + + if len(positions) == 0: + selected_tools = tools[: self.max_selected_tools] + self.logger.info( + "Tool shortlist fallback: %d -> %d (no matches in model response)", + len(tools), + len(selected_tools), + ) + if len(selected_tools) == 0: + self.logger.warning("Tool shortlist reduced to 0 tools") + return selected_tools + + # Sort tools by the order they appear in the model response + positions.sort(key=lambda x: x[0]) + + ordered_tools = [name for _, name in positions] + + selected_names = ordered_tools[: self.max_selected_tools] + name_to_tool = {tool["function"]["name"]: tool for tool in tools} + selected_tools = [name_to_tool[name] for name in selected_names] + self.logger.info( + "Tool shortlist from model: %d -> %d", + len(tools), + len(selected_tools), + ) + if len(selected_tools) == 0: + self.logger.warning("Tool shortlist reduced to 0 tools") + return selected_tools + + def _render_history_for_shortlist(self) -> str: + parts = [] + for message in self.messages: + msg = self._message_to_dict(message) + role = msg.get("role") or "unknown" + if role == "tool": + content = msg.get("content", "") + parts.append(f"tool: {content}") + continue + content = msg.get("content") + if content: + parts.append(f"{role}: {content}") + tool_calls = msg.get("tool_calls") or [] + for tool_call in tool_calls: + function = tool_call.get("function") or {} + name = function.get("name") or tool_call.get("name") + arguments = function.get("arguments") + parts.append(f"{role} tool_call: {name}({arguments})") + return "\n".join(parts) + + @staticmethod + def _message_to_dict(message: Any) -> dict[str, Any]: + if isinstance(message, dict): + return message + if hasattr(message, "model_dump"): + return message.model_dump() + if hasattr(message, "dict"): + return message.dict() + raise TypeError(f"Unsupported message type: {type(message).__name__}") + + def _extract_tool_calls(self, message: litellm.Message) -> list[ToolCall]: + """Extract tool calls from the message object returned from the litellm call.""" + if not message.tool_calls: + return [] + + tool_calls: list[ToolCall] = [] + + for tool_call in message.tool_calls: + tool_calls.append( + { + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + "id": tool_call.id, + } + ) + + return tool_calls + + def react(self, observation: Observation | None) -> Action | None: + self._step_count += 1 + if self._step_count > self.max_steps: + self.logger.warning("Finished: max steps reached (%d)", self.max_steps) + return None + + self._observe(observation) + + response = self._completion( + model=self.model, + messages=self.messages, + tools=self._assistant_tools(), + caching=self._use_cache, + ) + + self._register_cost(response.usage) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls": + tool_calls = self._extract_tool_calls(message) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tool_call["id"], + "type": "function", + "function": { + "name": tool_call["name"], + "arguments": tool_call["arguments"], + }, + } + for tool_call in tool_calls + ], + ) + ) + actions = self._registry.tool_calls_to_action(tool_calls) + else: + actions = MessageAction(arguments=Message(content=message.content)) + self._add_message( + ChatCompletionAssistantMessage( + role="assistant", + content=message.content, + ) + ) + + self.logger.info(f"Invoking action: {actions}") + return actions + + def _completion(self, **kwargs): + call_kwargs = self.model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + # Use 'metadata' parameter instead of 'litellm_metadata' - LiteLLM passes this to callbacks + call_kwargs.setdefault("metadata", {})["context"] = get_context() + return self._completion_with_retries(call_kwargs) + + def _completion_with_retries(self, call_kwargs: dict[str, Any]): + num_retries = self.model_settings.num_retries or 0 + max_attempts = max(1, num_retries + 1) + for attempt in range(max_attempts): + try: + response = litellm.completion(max_retries=0, **call_kwargs) + self._raise_if_invalid_completion(response) + return response + except NonRetryableCompletionError: + raise + except Exception as exc: + if attempt >= num_retries: + raise + delay = self.model_settings.retry_after + retry_strategy = self.model_settings.retry_strategy.value + if retry_strategy == RetryStrategy.EXPONENTIAL_BACKOFF.value: + delay *= 2**attempt + self.logger.warning( + "LiteLLM completion failed (attempt %d/%d): %s", + attempt + 1, + num_retries + 1, + exc, + ) + if delay > 0: + time.sleep(delay) + return None + + def _raise_if_invalid_completion(self, response: Any) -> None: + try: + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + except Exception: + return + + if finish_reason == "length" and not self._allow_truncated_messages: + self.logger.error( + "LiteLLM completion truncated (finish_reason=length). Raw response: %s", + response, + ) + raise NonRetryableCompletionError( + "LiteLLM completion truncated (finish_reason=length). " + "To allow truncated responses, configure the agent with " + "allow_truncated_messages=True, or increase max_tokens." + ) + + if finish_reason != "tool_calls": + if message is None or message.content is None: + self.logger.error( + "LiteLLM completion missing assistant content " "(finish_reason=%s). Raw response: %s", + finish_reason, + response, + ) + raise ValueError("LiteLLM completion missing assistant content.") + + def close(self) -> None: + pass + + def get_cost(self) -> LiteLLMCostReport: + return self._cost_data diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py new file mode 100644 index 00000000..c6172608 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/litellm_tool_calling_agent.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar + +from ...core.agent import Agent +from ...core.types import ModelSettings + + +class LiteLLMToolCallingAgent(Agent): + """Agent factory that always assigns the message-to-user mapping variant.""" + + display_name: ClassVar[str] = "LiteLLM Tool Calling" + slug_name: ClassVar[str] = "tool_calling" + + model: str = "watsonx/openai/gpt-oss-120b" + max_steps: int = 150 + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + model_settings: ModelSettings | None = None + allow_truncated_messages: bool = False + + @classmethod + def _get_instance_class(cls): + from .instance import LiteLLMToolCallingAgentInstance + + return LiteLLMToolCallingAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.litellm_tool_calling.instance:LiteLLMToolCallingAgentInstance" + + @property + def model_name(self) -> str: # type: ignore[override] + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model)] + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + "max_steps": self.max_steps, + "model_settings": self.model_settings, + "allow_truncated_messages": self.allow_truncated_messages, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/utils.py b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/utils.py new file mode 100644 index 00000000..03aac91a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/litellm_tool_calling/utils.py @@ -0,0 +1,135 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from typing import Any, Optional + +from typing_extensions import TypedDict + +from ...core.actions import build_action, build_unknown_action +from ...core.types import ( + Action, + ActionType, + ParallelAction, + SingleAction, + SingleObservation, +) + + +class PartialAction(SingleAction): + arguments: dict + + +class ToolCall(TypedDict): + name: str + arguments: str + id: str + + +def extract_arguments(action_type: ActionType): + return action_type.arguments + + +class ToolsActionsRegistry: + _MAX_SAFE_SCHEMA_INT = 2_147_483_647 + + @classmethod + def _clamp_schema_ints(cls, obj): + if isinstance(obj, dict): + return {k: cls._clamp_schema_ints(v) for k, v in obj.items()} + if isinstance(obj, list): + return [cls._clamp_schema_ints(v) for v in obj] + if isinstance(obj, int) and not isinstance(obj, bool): + if obj > cls._MAX_SAFE_SCHEMA_INT: + return cls._MAX_SAFE_SCHEMA_INT + if isinstance(obj, float): + if obj > cls._MAX_SAFE_SCHEMA_INT: + return float(cls._MAX_SAFE_SCHEMA_INT) + return obj + + @staticmethod + def format_observation(observation: SingleObservation) -> str: + """Serialize observation.result to JSON if possible to satisfy tool message requirements.""" + value = observation.result + try: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except TypeError: + return str(value) + + def __init__(self, actions: list[ActionType]): + self.action_types: list[ActionType] = [] + self.name_to_action: dict[str, ActionType] = {} + self.action_id_to_tool_call_id: dict[str, str] = {} + for action in actions: + self.add_action(action) + + def add_action(self, action: ActionType): + if not isinstance(action, ActionType): + raise ValueError("bad action") + self.action_types.append(action) + self.name_to_action[action.name] = action + + def openai_tools(self) -> list[dict[str, Any]]: + tools: list[dict[str, Any]] = [] + for action in self.action_types: + # Skip non-environment messaging actions; agents handle messaging flow + if action.is_message: + continue + arguments_type = extract_arguments(action) + schema = arguments_type.model_json_schema() # type: ignore[attr-defined] + # Bedrock rejects oversized integer values in tool schemas. + schema = self._clamp_schema_ints(schema) + tools.append( + { + "type": "function", + "function": { + "name": action.name, + "description": action.description, + "parameters": schema, + }, + } + ) + tools.sort( + key=lambda tool: ( + tool.get("type", ""), + tool.get("function", {}).get("name", ""), + ) + ) + return tools + + def _tool_call_to_single_action(self, tool_call: ToolCall) -> SingleAction: + name = tool_call["name"] + action_type = self.name_to_action.get(name) + + action_id = tool_call.get("id") + if action_type: + action = build_action(action_type, tool_call["arguments"], action_id=action_id) + else: + action = build_unknown_action(name, tool_call.get("arguments", {}), action_id=action_id) + + if "id" not in tool_call: + tool_call["id"] = action.id + + self.action_id_to_tool_call_id[action.id] = tool_call["id"] + + return action + + def tool_calls_to_action(self, tool_calls: list[ToolCall]) -> Optional[Action]: + actions: list[SingleAction] = [] + for tool_call in tool_calls: + actions.append(self._tool_call_to_single_action(tool_call)) + if len(actions) == 0: + return None + if len(actions) == 1: + return actions[0] + return ParallelAction(actions=actions) + + +def tool_call_to_dict(tool_call): + return { + "function": vars(tool_call.function), + "id": tool_call.id, + "type": tool_call.type, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/openai/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/openai/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/openai/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/openai/instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/openai/instance.py new file mode 100644 index 00000000..973f23ee --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/openai/instance.py @@ -0,0 +1,307 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import asyncio +import logging +from typing import Any + +import httpx +from agents import Agent as OpenAIAgent +from agents import Runner +from agents.extensions.models.litellm_model import LitellmModel +from agents.lifecycle import RunHooksBase +from agents.mcp import MCPServerStreamableHttp, MCPServerStreamableHttpParams +from agents.model_settings import ModelSettings as OpenAIModelSettings +from agents.model_settings import Reasoning +from agents.run import RunConfig +from agents.usage import Usage + +from ...adapters.agents.mcp_agent import MCPAgentInstance +from ...core.context import get_context +from ...core.types import ModelSettings, RetryStrategy +from ...integrations.litellm.health import acheck_model_accessible +from ...observers.logging import ( + attach_library_logger_to_handler, + restore_library_logger, +) +from ...utils.cost import UpdatableCostReport, litellm_tokens_cost +from ...utils.settings import get_settings +from ...utils.sync import run_sync +from .openai_mcp_agent import MCPConfig + +settings = get_settings() + + +class _UsageRunHooks(RunHooksBase[dict[str, Any], OpenAIAgent]): + def __init__(self, record_usage) -> None: + super().__init__() + self._record_usage = record_usage + + async def on_llm_end(self, context, agent, response) -> None: + self._record_usage(response.usage) + + +class RetryingLitellmModel(LitellmModel): + def __init__( + self, + model: str, + *, + num_retries: int, + retry_after: float, + retry_strategy: str | RetryStrategy, + ): + super().__init__(model=model) + if isinstance(retry_strategy, RetryStrategy): + retry_strategy = retry_strategy.value + if retry_strategy not in ("exponential_backoff_retry", "constant_retry"): + raise ValueError(f"Unsupported retry_strategy: {retry_strategy}") + if num_retries < 0: + raise ValueError("num_retries must be >= 0") + if retry_after < 0: + raise ValueError("retry_after must be >= 0") + self._num_retries = num_retries + self._retry_after = retry_after + self._retry_strategy = retry_strategy + + async def _fetch_response(self, *args, **kwargs): + # Inject context for OTEL tracing via model_settings.metadata + # The parent class extracts metadata from model_settings and passes it to litellm.acompletion() + # Note: metadata must be Dict[str, str], so we serialize Context fields individually + ctx = get_context() + + # model_settings is the 3rd positional argument (index 2) + if len(args) > 2: + model_settings = args[2] + if model_settings.metadata is None: + model_settings.metadata = {} + + # Serialize Context fields as individual string metadata entries + model_settings.metadata["exgentic_ctx_run_id"] = ctx.run_id + model_settings.metadata["exgentic_ctx_output_dir"] = ctx.output_dir + model_settings.metadata["exgentic_ctx_cache_dir"] = ctx.cache_dir + if ctx.session_id is not None: + model_settings.metadata["exgentic_ctx_session_id"] = ctx.session_id + if ctx.task_id is not None: + model_settings.metadata["exgentic_ctx_task_id"] = ctx.task_id + model_settings.metadata["exgentic_ctx_role"] = ctx.role.value + if ctx.otel_context is not None: + model_settings.metadata["exgentic_ctx_otel_trace_id"] = ctx.otel_context.trace_id + model_settings.metadata["exgentic_ctx_otel_span_id"] = ctx.otel_context.span_id + + for attempt in range(self._num_retries + 1): + try: + return await super()._fetch_response(*args, **kwargs) + except Exception as exc: + if attempt >= self._num_retries: + raise + delay = self._retry_after + if self._retry_strategy == RetryStrategy.EXPONENTIAL_BACKOFF.value: + delay *= 2**attempt + logging.getLogger(__name__).warning( + "OpenAI MCP LiteLLM call failed (attempt %d/%d): %s", + attempt + 1, + self._num_retries + 1, + exc, + ) + if delay > 0: + await asyncio.sleep(delay) + return None + + +class OpenAIMCPAgentInstance(MCPAgentInstance): + """OpenAI Agents SDK + MCP (sync entrypoint, async core).""" + + def __init__( + self, + session_id: str, + model_id: str, + max_steps: int = 150, + model_settings: ModelSettings | None = None, + mcp_config: MCPConfig | dict | None = None, + ): + super().__init__(session_id) + self.model_id = model_id + self.max_steps = max_steps + if model_settings is None: + self.model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self.model_settings = model_settings + else: + raise ValueError("model_settings must be a ModelSettings instance.") + if mcp_config is None: + self.mcp_config = MCPConfig() + elif isinstance(mcp_config, dict): + self.mcp_config = MCPConfig(**mcp_config) + else: + self.mcp_config = mcp_config + self._total_input_tokens = 0 + self._total_output_tokens = 0 + self._model_access_checked = False + + async def _check_model_access_once(self) -> None: + if self._model_access_checked or self.mcp_config.skip_health_check: + return + self.logger.info("Running LiteLLM model health check (model=%s)", self.model_id) + await acheck_model_accessible(self.model_id) + self._model_access_checked = True + + def _record_usage(self, usage: Usage | None) -> None: + if usage is None: + return + self._total_input_tokens += usage.input_tokens + self._total_output_tokens += usage.output_tokens + + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> Any: + # Run async core on the shared loop (sync API) + return run_sync(self.run_mcp_agent_async(mcp_host, mcp_port), timeout=600.0) + + async def run_mcp_agent_async(self, mcp_host: str, mcp_port: int) -> Any: + RunConfig.tracing_disabled = True + + prompt = self._build_prompt() + + file_handler = next( + (h for h in self.logger.handlers if isinstance(h, logging.FileHandler)), + None, + ) + logger_states: list[tuple] = [] + + try: + if file_handler: + logger_states += [ + attach_library_logger_to_handler("agents", file_handler), + attach_library_logger_to_handler(__name__, file_handler), + ] + logging.getLogger("agents").setLevel(logging.INFO) + await self._check_model_access_once() + + # Create custom httpx client factory with extended timeout + def httpx_client_factory(headers=None, timeout=None, auth=None): + if ( + self.mcp_config.http_timeout_seconds is None + and self.mcp_config.sse_read_timeout_seconds is None + and self.mcp_config.http_connect_timeout_seconds is None + ): + client_timeout = httpx.Timeout(None) + else: + client_timeout = httpx.Timeout( + self.mcp_config.http_timeout_seconds, + connect=self.mcp_config.http_connect_timeout_seconds, + read=self.mcp_config.sse_read_timeout_seconds, + ) + return httpx.AsyncClient( + headers=headers, + timeout=client_timeout, + auth=auth, + ) + + mcp_params: dict[str, Any] = { + "url": f"http://{mcp_host}:{mcp_port}/mcp", + "httpx_client_factory": httpx_client_factory, + "terminate_on_close": self.mcp_config.terminate_on_close, + } + if self.mcp_config.headers is not None: + mcp_params["headers"] = self.mcp_config.headers + if self.mcp_config.http_timeout_seconds is not None: + mcp_params["timeout"] = self.mcp_config.http_timeout_seconds + if self.mcp_config.sse_read_timeout_seconds is not None: + mcp_params["sse_read_timeout"] = self.mcp_config.sse_read_timeout_seconds + + async with MCPServerStreamableHttp( + params=MCPServerStreamableHttpParams(**mcp_params), + cache_tools_list=self.mcp_config.cache_tools_list, + name=self.mcp_config.name, + client_session_timeout_seconds=self.mcp_config.client_session_timeout_seconds, + use_structured_content=self.mcp_config.use_structured_content, + max_retry_attempts=self.mcp_config.max_retry_attempts, + retry_backoff_seconds_base=self.mcp_config.retry_backoff_seconds_base, + message_handler=self.mcp_config.message_handler, + ) as mcp_server: + temperature = self.model_settings.temperature + reasoning_effort = self.model_settings.reasoning_effort + openai_model_settings = OpenAIModelSettings( + temperature=temperature if temperature is not None else 1.0, + max_tokens=self.model_settings.max_tokens, + top_p=self.model_settings.top_p, + reasoning=(Reasoning(effort=reasoning_effort) if reasoning_effort is not None else None), + ) + num_retries = self.model_settings.num_retries or 0 + retry_after = self.model_settings.retry_after + retry_strategy = self.model_settings.retry_strategy.value + openai_model_settings.extra_args = { + "caching": settings.litellm_caching, + "max_retries": 0 if num_retries > 0 else 5, + } + agent = OpenAIAgent( + name="Assistant", + instructions=prompt, + model=RetryingLitellmModel( + model=self.model_id, + num_retries=num_retries, + retry_after=retry_after, + retry_strategy=retry_strategy, + ), + model_settings=openai_model_settings, + mcp_servers=[mcp_server], + ) + self.logger.info( + "Starting OpenAI MCP agent run (model=%s, task=%s, max_turns=%s)", + self.model_id, + self.task, + self.max_steps, + ) + hooks = _UsageRunHooks(self._record_usage) + try: + result = await Runner.run( + agent, + self.task, + max_turns=self.max_steps, + run_config=RunConfig(tracing_disabled=True), + hooks=hooks, + ) + except Exception: + self.logger.exception("OpenAI MCP agent run failed") + raise + if self._total_input_tokens == 0 and self._total_output_tokens == 0: + for resp in result.raw_responses: + self._record_usage(resp.usage) + self.logger.info("OpenAI MCP agent run finished: %s", result) + return result + finally: + for state in logger_states: + if state: + restore_library_logger(*state) + if file_handler: + file_handler.flush() + + def get_cost(self) -> UpdatableCostReport: + report = UpdatableCostReport.initialize_empty(model_name=self.model_id) + if self._total_input_tokens == 0 and self._total_output_tokens == 0: + return report + + cost = litellm_tokens_cost( + model_name=self.model_id, + input_tokens=self._total_input_tokens, + output_tokens=self._total_output_tokens, + ).total_cost + report.add_cost(cost) + return report + + def _build_prompt(self) -> str: + prompt = "" + if self.context: + prompt += f"Context: {self.context}\n\n" + + prompt += ( + "Complete this task using the available tools. Each tool corresponds to an action " + "you can take in the environment. Do not respond or ask clarification questions " + "unless done through a dedicated tool, and only if such tool exist. " + "Any plain message that is not a tool call will end the run in failure.\n" + ) + + if self.initial_observation is not None and not self.initial_observation.is_empty(): + text = str(self.initial_observation).strip() + if text: + prompt += f"\nFirst Observation: {text}\n" + + return prompt diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/openai/openai_mcp_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/openai/openai_mcp_agent.py new file mode 100644 index 00000000..de508467 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/openai/openai_mcp_agent.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar + +from pydantic import BaseModel, ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings + + +class MCPConfig(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + client_session_timeout_seconds: float | None = None + http_timeout_seconds: float | None = None + sse_read_timeout_seconds: float | None = None + http_connect_timeout_seconds: float | None = None + headers: dict[str, str] | None = None + terminate_on_close: bool = True + max_retry_attempts: int = -1 + retry_backoff_seconds_base: float = 1.0 + cache_tools_list: bool = False + use_structured_content: bool = False + skip_health_check: bool = False + name: str | None = None + message_handler: Any | None = None + + +class OpenAIMCPAgent(Agent): + display_name: ClassVar[str] = "OpenAI Solo" + slug_name: ClassVar[str] = "openai_solo" + + model: str + max_steps: int = 150 + model_settings: ModelSettings | None = None + mcp_config: MCPConfig | dict | None = None + + @classmethod + def _get_instance_class(cls): + from .instance import OpenAIMCPAgentInstance + + return OpenAIMCPAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.openai.instance:OpenAIMCPAgentInstance" + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + return { + "session_id": session_id, + "model_id": self.model, + "max_steps": self.max_steps, + "model_settings": self.model_settings, + "mcp_config": self.mcp_config, + } + + @property + def model_name(self) -> str: # type: ignore[override] + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model)] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/openai/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/agents/openai/requirements.txt new file mode 100644 index 00000000..6220519c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/openai/requirements.txt @@ -0,0 +1 @@ +openai-agents[litellm] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/evaluator.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/evaluator.py new file mode 100644 index 00000000..b49d1c1a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/evaluator.py @@ -0,0 +1,72 @@ +# Copyright 2026 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modifications Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +class TrajectoryEvaluator: + + def __init__(self, llm_call_fn): + self._llm_call = llm_call_fn + + def evaluate( + self, + intent: str, + think_list: list[str], + action_list: list[str], + observation_list: list[str], + final_response: str = "", + ) -> dict[str, str]: + from .prompts.eval_prompts import build_text_eval_prompt, extract_content + + action_history = "" + for idx, act in enumerate(action_list): + think = think_list[idx] if idx < len(think_list) else "" + if think: + action_history += f"{idx+1}: {think}\n {act}\n" + else: + action_history += f"{idx+1}: {act}\n" + + last_obs = observation_list[-5:] if len(observation_list) >= 5 else observation_list + combined_obs = "\n\n---\n\n".join( + f"[Page state {i+1}/{len(last_obs)}]\n{c}" + for i, c in enumerate(last_obs) + ) + + MAX_OBS_CHARS = 40000 + if len(combined_obs) > MAX_OBS_CHARS: + combined_obs = combined_obs[:MAX_OBS_CHARS] + + prompt, sys_msg = build_text_eval_prompt( + combined_obs, intent, final_response, action_history + ) + + msg_str = self._llm_call(prompt, sys_msg) + + thoughts = extract_content(msg_str, "Thoughts:") + status_raw = extract_content(msg_str, "Status:").replace('"', "").strip().lower() + + + if "success" in status_raw: + status = "success" + else: + status = "failure" + + return {"thoughts": thoughts, "status": status} diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/induce_memory.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/induce_memory.py new file mode 100644 index 00000000..3a0a94b2 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/induce_memory.py @@ -0,0 +1,70 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modifications Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +from typing import Callable + +logger = logging.getLogger(__name__) + + +def format_trajectory(think_list: list[str], action_list: list[str], observation_list: list[str] | None = None) -> str: + trajectory = [] + obs_list = observation_list or [] + for i, (t, a) in enumerate(zip(think_list, action_list)): + if t: + trajectory.append(f"\n{t}\n\n\n{a}\n") + else: + obs = obs_list[i] if i < len(obs_list) else "" + if obs: + trajectory.append(f"\n{obs}\n\n\n{a}\n") + else: + trajectory.append(f"\n{a}\n") + return "\n\n".join(trajectory) + + +def induce_memory( + query: str, + think_list: list[str], + action_list: list[str], + status: str, + eval_thoughts: str, + llm_call_fn: Callable[[str, str], str], + observation_list: list[str] | None = None, +) -> list[str]: + from .prompts.memory_instruction import FAILED_SI, SUCCESSFUL_SI + + trajectory = format_trajectory(think_list, action_list, observation_list) + trajectory = f"**Query:** {query}\n\n**Trajectory:**\n{trajectory}" + + if eval_thoughts: + status_label = "succeeded" if status == "success" else "failed" + trajectory += f"\n\nThe task {status_label} because: {eval_thoughts}" + + if status == "success": + generated_text = llm_call_fn(trajectory, SUCCESSFUL_SI) + else: + generated_text = llm_call_fn(trajectory, FAILED_SI) + + memory_items = [item.strip() for item in generated_text.split("\n\n") if item.strip()] + + logger.info( + "ReasoningBank memory induction: status=%s, generated %d items", + status, len(memory_items), + ) + + return memory_items diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/memory_management.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/memory_management.py new file mode 100644 index 00000000..1a622551 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/memory_management.py @@ -0,0 +1,112 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modifications Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import numpy as np +from sentence_transformers import SentenceTransformer + +if TYPE_CHECKING: + from .rb_store import MemoryEntry, ReasoningBankStore + +logger = logging.getLogger(__name__) + +RETRIEVAL_INSTRUCTION = ( + "Given the prior web navigation queries, your task is to analyze a current " + "query's intent and select relevant prior queries that could help resolve it." +) + +_st_model = None +_st_model_name = None + + +def _get_st_model(model_name: str = "all-MiniLM-L6-v2") -> SentenceTransformer: + global _st_model, _st_model_name + if _st_model is None or _st_model_name != model_name: + logger.info("Loading SentenceTransformer model: %s", model_name) + _st_model = SentenceTransformer(model_name) + _st_model_name = model_name + return _st_model + + +def get_detailed_instruct(task_description: str, query: str) -> str: + return f"Instruct: {task_description}\nQuery: {query}" + + +def l2_normalize(x: np.ndarray, axis: int = -1) -> np.ndarray: + norm = np.linalg.norm(x, axis=axis, keepdims=True) + norm = np.where(norm == 0, 1.0, norm) + return x / norm + + +def compute_embedding(text: str, model: str = "all-MiniLM-L6-v2") -> list[float]: + st = _get_st_model(model) + vec = st.encode([text])[0] + return vec.tolist() + + +def select_memory( + store: "ReasoningBankStore", + cur_query: str, + embedding_model: str, + top_k: int = 1, + exclude_task_id: str | None = None, +) -> list["MemoryEntry"]: + + cache_emb = store.get_embeddings_array() + if cache_emb is None or len(cache_emb) == 0: + logger.info("ReasoningBank retrieval: no cached embeddings, returning empty.") + return [] + + entries = store.get_entries() + entry_ids = store.get_entry_ids() + + instruction_query = get_detailed_instruct(RETRIEVAL_INSTRUCTION, cur_query) + instruct_vec = np.array( + compute_embedding(instruction_query, embedding_model), + dtype=np.float32, + ).reshape(1, -1) + + instruct_vec = l2_normalize(instruct_vec, axis=1) + cache_emb_norm = l2_normalize(cache_emb, axis=1) + + scores = (instruct_vec @ cache_emb_norm.T).squeeze(0) * 100.0 # (N,) + + id_score_pairs = [] + for i, (eid, score) in enumerate(zip(entry_ids, scores)): + if exclude_task_id and eid == exclude_task_id: + continue + id_score_pairs.append((i, float(score))) + + id_score_pairs.sort(key=lambda x: x[1], reverse=True) + + top_entries = [] + for idx, _score in id_score_pairs[:top_k]: + top_entries.append(entries[idx]) + + return top_entries + + +def format_memories_for_prompt(entries: list["MemoryEntry"]) -> str: + mem_items = [] + for entry in entries: + for item in entry.memory_items: + if item.strip(): + mem_items.append(item.strip()) + return "\n\n".join(mem_items) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/eval_prompts.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/eval_prompts.py new file mode 100644 index 00000000..ca4952cd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/eval_prompts.py @@ -0,0 +1,55 @@ +# Copyright 2026 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modifications Copyright (C) 2026, The AgentStream organization and its contributors. + + +def extract_content(text: str, start_tag: str) -> str: + for line in text.split("\n"): + if line.startswith(start_tag): + return line[len(start_tag):].strip() + return "" + + +def build_text_eval_prompt( + cap: str, intent: str, response: str, last_actions: str +) -> tuple[str, str]: + system_msg = """You are an expert in evaluating the performance of a task-solving agent. The agent is designed to help a human user complete a task by taking actions in an environment. Given the user's intent, the agent's action history, the environment's feedback, and the agent's response to the user, your goal is to decide whether the agent's execution is successful or not. + +*Strictness rules* +Before calling a task successful, verify all three: +- Completeness: every constraint in the intent is satisfied. +- Grounding: every value or result the agent reports is traceable to a specific observation from the environment; values that were inferred, guessed, or summarized without a visible source count as failures. +- Right target: when the task names a specific entity, confirm the agent acted on that exact entity and not an adjacent one. +When uncertain on any of these, mark failure. A false success is more harmful than a false failure, because memory induction amplifies it into future behavior. + +*IMPORTANT* +Format your response into two lines as shown below: + +Thoughts: " +Status: "success" or "failure" +""" + prompt = f"""User Intent: {intent} + +Action History: +{last_actions} + +Environment feedback (last observations): + +``` +{cap} +``` + +Agent response to the user: {response if response else "N/A"}.""" + return prompt, system_msg diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/memory_instruction.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/memory_instruction.py new file mode 100644 index 00000000..5b54a634 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/prompts/memory_instruction.py @@ -0,0 +1,70 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Modifications Copyright (C) 2026, The AgentStream organization and its contributors. + +SUCCESSFUL_SI = """ +You are an expert at analyzing agent task execution. You will be given a user query, the corresponding trajectory that represents **how an agent successfully accomplished the task**. + +## Guidelines +You need to extract and summarize useful insights in the format of memory items based on the agent's successful trajectory. +The goal of summarized memory items is to be helpful and generalizable for future similar tasks. + +## Important notes + - You must first think why the trajectory is successful, and then summarize the insights. + - You can extract *at most 3* memory items from the trajectory. + - You must not repeat similar or overlapping items. + - Prefer concrete, actionable procedures over abstract principles. Do not embed specific product names, queries, or literal string contents from the task. + +## Output Format +Your output must strictly follow the Markdown format shown below: + +``` +# Memory Item i +## Title +## Description +## Content <1-3 sentences describing the insights learned to successfully accomplishing similar tasks in the future> +``` +""" + +FAILED_SI = """ +You are an expert at analyzing agent task execution. You will be given a user query, the corresponding trajectory that represents **how an agent attempted to resolve the task but failed**. + +## Guidelines +You need to extract and summarize useful insights in the format of memory items based on the agent's failed trajectory. +The goal of summarized memory items is to be helpful and generalizable for future similar tasks. + +## Important notes + - You must first reflect and think why the trajectory failed, and then summarize what lessons you have learned or strategies to prevent the failure in the future. + - You can extract *at most 3* memory items from the trajectory. + - You must not repeat similar or overlapping items. + - Prefer concrete, actionable recovery procedures over abstract principles. Do not embed specific product names, queries, or literal string contents from the task. + +## Output Format +Your output must strictly follow the Markdown format shown below: + +``` +# Memory Item i +## Title +## Description +## Content <1-3 sentences describing the insights learned to avoid such failures and successfully accomplishing similar tasks in the future> +``` +""" + +MEMORY_INJECTION_INSTRUCTION = ( + "Below are some memory items that I accumulated from past interaction from " + "the environment that may be helpful to solve the task. You can use it when " + "you feel it's relevant. In each step, please first explicitly discuss if " + "you want to use each memory item or not, and then take action." +) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_agent.py new file mode 100644 index 00000000..ca3c1e15 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_agent.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +from typing import Any, ClassVar, Optional + +from pydantic import ConfigDict + +from ...core.agent import Agent +from ...core.types import ModelSettings +from ...utils.settings import RunnerName + + +class ReasoningBankAgent(Agent): + + display_name: ClassVar[str] = "ReasoningBank Agent" + slug_name: ClassVar[str] = "reasoning_bank" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + model: str = "gpt-4o" + memory_model: Optional[str] = None + eval_model: Optional[str] = None + embedding_model: str = "all-MiniLM-L6-v2" + + top_k_memories: int = 1 + max_memory_items: int = 3 + + shuffle_mode: str = "isolated" + + benchmark_id: Optional[str] = None + + enable_tool_shortlisting: bool = False + max_selected_tools: int = 30 + + runner: RunnerName | None = None + model_settings: ModelSettings | None = None + + @classmethod + def _get_instance_class(cls): + from .rb_instance import ReasoningBankAgentInstance + return ReasoningBankAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.reasoning_bank.rb_instance:ReasoningBankAgentInstance" + + def _get_instance_kwargs(self, session_id: str) -> dict[str, Any]: + return { + "session_id": session_id, + "model": self.model, + "memory_model": self.memory_model or self.model, + "eval_model": self.eval_model or self.model, + "embedding_model": self.embedding_model, + "top_k_memories": self.top_k_memories, + "max_memory_items": self.max_memory_items, + "shuffle_mode": self.shuffle_mode, + "model_settings": self.model_settings, + "benchmark_id": self.benchmark_id, + "enable_tool_shortlisting": self.enable_tool_shortlisting, + "max_selected_tools": self.max_selected_tools, + } + + @property + def model_name(self) -> str: + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: + names = [str(self.model)] + mm = self.memory_model or self.model + if mm != self.model: + names.append(str(mm)) + em = self.eval_model or self.model + if em != self.model and em != mm: + names.append(str(em)) + return names diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_instance.py new file mode 100644 index 00000000..523a4f4a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_instance.py @@ -0,0 +1,552 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import logging +import time +from typing import Any, Dict, Optional, Union + +import litellm +from litellm import ( + ChatCompletionAssistantMessage, + ChatCompletionSystemMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, +) + +from ...core.agent_instance import AgentInstance +from ...core.types import ( + Action, + ActionType, + Message, + MessageAction, + MessageObservation, + MessagePayload, + ModelSettings, + Observation, +) +from ...utils.cost import LiteLLMCostReport +from ...utils.settings import get_settings + +from .rb_store import MemoryEntry, ReasoningBankStore +from .memory_management import ( + compute_embedding, + format_memories_for_prompt, + select_memory, +) +from .prompts.memory_instruction import MEMORY_INJECTION_INSTRUCTION +from ..tool_shortlisting import shortlist_tools + +try: + from ...agents.litellm_tool_calling.utils import ToolCall, ToolsActionsRegistry +except ImportError: + ToolsActionsRegistry = None + ToolCall = dict + +settings = get_settings() +logger = logging.getLogger(__name__) + + +class ReasoningBankAgentInstance(AgentInstance): + + def __init__( + self, + session_id: str, + model: str = "gpt-4o", + memory_model: str = "gpt-4o", + eval_model: str = "gpt-4o", + embedding_model: str = "all-MiniLM-L6-v2", + top_k_memories: int = 1, + max_memory_items: int = 3, + shuffle_mode: str = "isolated", + model_settings: Optional[ModelSettings] = None, + benchmark_id: Optional[str] = None, + enable_tool_shortlisting: bool = False, + max_selected_tools: int = 30, + ) -> None: + super().__init__(session_id) + + self.model = model + self.memory_model = memory_model + self.eval_model = eval_model + self.embedding_model = embedding_model + self.top_k_memories = top_k_memories + self.max_memory_items = max_memory_items + self.shuffle_mode = shuffle_mode + self.benchmark_id = benchmark_id + self.enable_tool_shortlisting = enable_tool_shortlisting + self.max_selected_tools = max_selected_tools + + if model_settings is None: + self._model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self._model_settings = model_settings + else: + self._model_settings = ModelSettings() + + self._cost = LiteLLMCostReport.initialize_empty(model_name=self.model) + self._store: Optional[ReasoningBankStore] = None + + self.messages: list[ + Union[ + ChatCompletionAssistantMessage, + ChatCompletionToolMessage, + ChatCompletionUserMessage, + ChatCompletionSystemMessage, + ] + ] = [] + + self._registry: Optional[Any] = None + self._all_actions: list[ActionType] = [] + self._step_count: int = 0 + + self._think_list: list[str] = [] + self._action_list: list[str] = [] + self._observation_log: list[str] = [] + self._action_log: list[Dict[str, Any]] = [] + + self._query_embedding: Optional[list[float]] = None + self._task_query: str = "" + + def start(self, task: str, context: Dict[str, Any], actions: list[ActionType]) -> None: + super().start(task, context, actions) + + self._all_actions = list(self.actions) + if ToolsActionsRegistry is not None: + self._registry = ToolsActionsRegistry(self._all_actions) + else: + self._registry = None + + self._store = ReasoningBankStore.get_or_create( + shuffle_mode=self.shuffle_mode, + benchmark_id=self.benchmark_id, + ) + self._store.increment_session() + + self._task_query = task + if context: + context_str = "" + for k, v in context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + continue + context_str += f"\n{k}: {v}" + self._task_query = f"{task}{context_str}" + + try: + self._query_embedding = compute_embedding( + self._task_query, self.embedding_model + ) + except Exception as e: + logger.warning("Failed to compute query embedding: %s", e) + self._query_embedding = None + + retrieved_memories: list[MemoryEntry] = [] + if self._query_embedding is not None and self._store.entry_count > 0: + try: + retrieved_memories = select_memory( + store=self._store, + cur_query=self._task_query, + embedding_model=self.embedding_model, + top_k=self.top_k_memories, + ) + except Exception as e: + logger.warning("Memory retrieval failed: %s", e) + + sys_prompt = "You are an expert agent that completes tasks using available tools." + if retrieved_memories: + memory_text = format_memories_for_prompt(retrieved_memories) + if memory_text.strip(): + sys_prompt += "\n\n" + MEMORY_INJECTION_INSTRUCTION + sys_prompt += "\n\n" + memory_text + + self.messages = [ + ChatCompletionSystemMessage(role="system", content=sys_prompt), + ] + + content_parts: list[Any] = [] + ctx_str = "" + if self.context: + for k, v in self.context.items(): + if isinstance(v, dict) and v.get("type") == "image_url": + content_parts.append({"type": "image_url", "image_url": {"url": v["data"], "detail": "high"}}) + else: + ctx_str += f"\n<{k}>\n{v}\n" + + text_content = f"{self.task}\n{ctx_str}" + if content_parts: + content_parts.insert(0, {"type": "text", "text": text_content}) + self.messages.append(ChatCompletionUserMessage(role="user", content=content_parts)) + else: + self.messages.append( + ChatCompletionUserMessage(role="user", content=text_content) + ) + + logger.info( + "ReasoningBank start: retrieved %d memories for task (store has %d entries)", + len(retrieved_memories), self._store.entry_count, + ) + + def react(self, observation: Optional[Observation]) -> Optional[Action]: + self._step_count += 1 + + self._observe(observation) + + tools = self._assistant_tools() + + response = self._completion( + model=self.model, + messages=self.messages, + tools=tools if tools else None, + ) + if response is None: + return None + + if response.usage: + self._cost.update_cost_from_tokens( + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + + choice = response["choices"][0] + message = choice["message"] + finish_reason = choice.get("finish_reason") + + if finish_reason == "tool_calls" and self._registry is not None: + tool_calls = self._extract_tool_calls(message) + self.messages.append( + ChatCompletionAssistantMessage( + role="assistant", + tool_calls=[ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": tc["arguments"], + }, + } + for tc in tool_calls + ], + ) + ) + + think = message.content if message.content else "" + self._think_list.append(think) + for tc in tool_calls: + self._action_list.append(f"{tc['name']}({tc['arguments']})") + self._action_log.append({ + "step": self._step_count, + "action": tc["name"], + "arguments": tc["arguments"], + }) + + actions = self._registry.tool_calls_to_action(tool_calls) + return actions + else: + content = message.content if message.content else "" + + if not content: + logger.warning( + "ReasoningBank step %d: empty content response (finish_reason=%s), " + "treating as agent inability to continue", + self._step_count, finish_reason, + ) + return None + + self.messages.append( + ChatCompletionAssistantMessage(role="assistant", content=content) + ) + + self._think_list.append(content) + self._action_list.append(f"send_msg_to_user('{content[:200]}')") + + return MessageAction(arguments=Message(content=content)) + + def close(self) -> None: + if not self._store: + return + + if not self._action_list: + logger.info("ReasoningBank close: no actions recorded, skipping memory induction.") + self._save_session_artifacts() + return + + if self._query_embedding is None: + logger.info("ReasoningBank close: embedding unavailable, skipping memory induction.") + self._save_session_artifacts() + return + + try: + self._run_post_session_learning() + except Exception as e: + logger.warning("ReasoningBank close: memory induction failed: %s", e) + + self._save_session_artifacts() + + def _save_session_artifacts(self) -> None: + if not self._store: + return + try: + cp = str(self.paths.agent_dir / "memory_checkpoint.json") + self._store.save_checkpoint(cp) + mt = str(self.paths.agent_dir / "memories.txt") + self._store.save_memories_text(mt) + except Exception as exc: + logger.warning("ReasoningBank: failed to save session artifacts: %s", exc) + + def get_cost(self) -> LiteLLMCostReport: + return self._cost + + def _run_post_session_learning(self) -> None: + from .evaluator import TrajectoryEvaluator + from .induce_memory import induce_memory + + evaluator = TrajectoryEvaluator( + llm_call_fn=lambda prompt, sys_msg: self._llm_call_simple( + self.eval_model, prompt, system_msg=sys_msg + ) + ) + + final_response = "" + for act in reversed(self._action_list): + if "send_msg_to_user" in act: + try: + final_response = act[act.index("(") + 1:act.rindex(")")] + final_response = final_response.strip("'\"") + except (ValueError, IndexError): + pass + break + + eval_result = evaluator.evaluate( + intent=self.task or self._task_query, + think_list=self._think_list, + action_list=self._action_list, + observation_list=self._observation_log, + final_response=final_response, + ) + + status = "success" if eval_result["status"] == "success" else "fail" + eval_thoughts = eval_result.get("thoughts", "") + + logger.info( + "ReasoningBank eval: status=%s, thoughts=%s", + status, eval_thoughts[:100], + ) + + memory_items = induce_memory( + query=self.task or self._task_query, + think_list=self._think_list, + action_list=self._action_list, + status=status, + eval_thoughts=eval_thoughts, + llm_call_fn=lambda user_msg, sys_msg: self._llm_call_simple( + self.memory_model, user_msg, system_msg=sys_msg + ), + observation_list=self._observation_log, + ) + + if memory_items and self._query_embedding is not None: + entry = MemoryEntry( + task_id=self.session_id, + query=self.task or self._task_query, + think_list=self._think_list, + action_list=self._action_list, + status=status, + memory_items=memory_items, + template_id=self.context.get("template_id") if self.context else None, + ) + self._store.add_entry(entry, self._query_embedding) + logger.info( + "ReasoningBank: stored %d memory items (store now has %d entries)", + len(memory_items), self._store.entry_count, + ) + + def _completion(self, **kwargs) -> Any: + """Standard completion with tool support and retry logic.""" + call_kwargs = self._model_settings.model_dump( + exclude_none=True, + exclude={"num_retries", "retry_after", "retry_strategy"}, + ) + call_kwargs.update(kwargs) + if call_kwargs.get("tools") is None: + call_kwargs.pop("tools", None) + + max_attempts = 3 + for attempt in range(max_attempts): + try: + response = litellm.completion(**call_kwargs) + choice = response["choices"][0] if response.get("choices") else None + if choice: + msg = choice.get("message") or {} + has_content = bool(msg.get("content")) + has_tools = bool(msg.get("tool_calls")) + if not has_content and not has_tools: + if attempt + 1 < max_attempts: + logger.warning( + "ReasoningBank LLM call attempt %d/%d: empty response " + "(finish_reason=%s), retrying...", + attempt + 1, max_attempts, + choice.get("finish_reason"), + ) + time.sleep(2 ** attempt) + continue + return response + except Exception as exc: + logger.warning( + "ReasoningBank LLM call attempt %d/%d failed: %s", + attempt + 1, max_attempts, exc, + ) + if attempt + 1 >= max_attempts: + raise + time.sleep(2 ** attempt) + return None + + def _llm_call_simple( + self, + model: str, + prompt: str, + *, + system_msg: str = "", + json_mode: bool = False, + ) -> str: + messages: list = [] + if system_msg: + messages.append({"role": "system", "content": system_msg}) + messages.append({"role": "user", "content": prompt}) + + kwargs: Dict[str, Any] = { + "model": model, + "messages": messages, + "temperature": 1.0, + } + if json_mode: + kwargs["response_format"] = {"type": "json_object"} + + max_attempts = 3 + for attempt in range(max_attempts): + try: + resp = litellm.completion(**kwargs) + if resp.usage: + self._cost.update_cost_from_tokens( + resp.usage.prompt_tokens, + resp.usage.completion_tokens, + ) + return resp.choices[0].message.content or "" + except Exception as exc: + if attempt + 1 >= max_attempts: + logger.error("Simple LLM call failed: %s", exc) + return "" + time.sleep(2 ** attempt) + return "" + + def _observe(self, observation: Optional[Observation]) -> None: + if observation is None: + return + + observations = observation.to_observation_list() + if observation.is_empty(): + if not any(obs.invoking_actions for obs in observations): + return + + for obs in observations: + if isinstance(obs, MessageObservation) and isinstance( + obs.result, MessagePayload + ): + self.messages.append( + ChatCompletionUserMessage(role="user", content=obs.result.message) + ) + self._observation_log.append(obs.result.message) + continue + + if len(obs.invoking_actions) > 0: + invoking = obs.invoking_actions[0] + if invoking.name == "message": + text = str(obs) + self.messages.append( + ChatCompletionUserMessage(role="user", content=text) + ) + self._observation_log.append(text) + continue + + tool_call_id = invoking.id + if not ( + isinstance(tool_call_id, str) + and tool_call_id.startswith("call_") + ): + if self._registry is not None: + tool_call_id = ( + self._registry.action_id_to_tool_call_id.get( + tool_call_id, tool_call_id + ) + ) + + value = obs.result + try: + content = json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except TypeError: + content = str(value) + + if tool_call_id is not None: + self.messages.append( + ChatCompletionToolMessage( + role="tool", + tool_call_id=tool_call_id, + content=content, + ) + ) + else: + self.messages.append( + ChatCompletionUserMessage( + role="user", + content=f"Tool result: {content}", + ) + ) + self._observation_log.append(content) + else: + content = str(obs.result) if hasattr(obs, "result") else str(obs) + self.messages.append( + ChatCompletionUserMessage(role="user", content=content) + ) + self._observation_log.append(content) + + + def _assistant_tools(self) -> list | None: + if self._registry is None: + return None + tools = self._registry.openai_tools() + if not tools: + return None + + if not self.enable_tool_shortlisting: + return tools + + def _cost_cb(usage): + if usage: + self._cost.update_cost_from_tokens( + usage.prompt_tokens, usage.completion_tokens + ) + + return shortlist_tools( + tools=tools, + max_selected=self.max_selected_tools, + messages=self.messages, + completion_fn=self._completion, + model=self.model, + logger=logger, + cost_callback=_cost_cb, + ) + + @staticmethod + def _extract_tool_calls(message: Any) -> list[dict[str, str]]: + if not hasattr(message, "tool_calls") or not message.tool_calls: + return [] + tool_calls = [] + for tc in message.tool_calls: + tool_calls.append({ + "name": tc.function.name, + "arguments": tc.function.arguments, + "id": tc.id, + }) + return tool_calls diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_store.py b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_store.py new file mode 100644 index 00000000..0d8c4ad3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/reasoning_bank/rb_store.py @@ -0,0 +1,164 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import json +import logging +import threading +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, ClassVar + +import numpy as np + +logger = logging.getLogger(__name__) + + +@dataclass +class MemoryEntry: + + task_id: str + query: str + think_list: list[str] + action_list: list[str] + status: str + memory_items: list[str] + template_id: str | None = None + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "MemoryEntry": + return cls( + task_id=d["task_id"], + query=d["query"], + think_list=d.get("think_list", []), + action_list=d.get("action_list", []), + status=d["status"], + memory_items=d.get("memory_items", []), + template_id=d.get("template_id"), + ) + + +class ReasoningBankStore: + + _instances: ClassVar[dict[str, "ReasoningBankStore"]] = {} + _class_lock: ClassVar[threading.Lock] = threading.Lock() + + def __init__(self, store_id: str) -> None: + self._store_id = store_id + self._lock = threading.Lock() + self._entries: list[MemoryEntry] = [] + self._embeddings: list[list[float]] = [] + self._session_count: int = 0 + + @classmethod + def get_or_create( + cls, + shuffle_mode: str = "isolated", + benchmark_id: str | None = None, + ) -> "ReasoningBankStore": + if shuffle_mode == "isolated": + store_id = f"rb_isolated_{benchmark_id or 'default'}" + elif shuffle_mode == "sequential": + store_id = "rb_sequential_global" + elif shuffle_mode == "interleaved": + store_id = "rb_interleaved_global" + else: + store_id = f"rb_{shuffle_mode}_{benchmark_id or 'default'}" + + with cls._class_lock: + if store_id not in cls._instances: + cls._instances[store_id] = cls(store_id) + return cls._instances[store_id] + + @classmethod + def list_stores(cls) -> dict[str, "ReasoningBankStore"]: + with cls._class_lock: + return dict(cls._instances) + + @classmethod + def reset_all(cls) -> None: + with cls._class_lock: + cls._instances.clear() + + @property + def store_id(self) -> str: + return self._store_id + + @property + def session_count(self) -> int: + with self._lock: + return self._session_count + + @property + def entry_count(self) -> int: + with self._lock: + return len(self._entries) + + def increment_session(self) -> int: + with self._lock: + self._session_count += 1 + return self._session_count + + def add_entry(self, entry: MemoryEntry, embedding: list[float]) -> None: + with self._lock: + self._entries.append(entry) + self._embeddings.append(embedding) + + def get_entries(self) -> list[MemoryEntry]: + with self._lock: + return list(self._entries) + + def get_embeddings_array(self) -> np.ndarray | None: + with self._lock: + if not self._embeddings: + return None + return np.array(self._embeddings, dtype=np.float32) + + def get_entry_ids(self) -> list[str]: + with self._lock: + return [e.task_id for e in self._entries] + + def save_checkpoint(self, path: str) -> None: + with self._lock: + data = { + "store_id": self._store_id, + "session_count": self._session_count, + "entries": [e.to_dict() for e in self._entries], + "embeddings": self._embeddings, + } + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False) + logger.info("ReasoningBankStore[%s]: saved checkpoint (%d entries) to %s", + self._store_id, len(self._entries), path) + + def save_memories_text(self, path: str) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with self._lock: + lines = [ + f"# ReasoningBank Memory Store: {self._store_id}", + f"# Entries: {len(self._entries)}", + f"# Sessions: {self._session_count}", + "", + ] + for i, entry in enumerate(self._entries): + lines.append(f"--- Entry {i + 1} [{entry.task_id[:20]}] status={entry.status} ---") + for item in entry.memory_items: + lines.append(item.strip()) + lines.append("") + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + + def load_checkpoint(self, path: str) -> None: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + with self._lock: + self._session_count = data.get("session_count", 0) + self._entries = [MemoryEntry.from_dict(d) for d in data.get("entries", [])] + self._embeddings = data.get("embeddings", []) + logger.info("ReasoningBankStore[%s]: loaded checkpoint (%d entries) from %s", + self._store_id, len(self._entries), path) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/replay/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/replay/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/replay/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_agent.py new file mode 100644 index 00000000..ab8d5797 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_agent.py @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""ReplayAgent — replays a recorded trajectory against a benchmark. + +Usage:: + + evaluate(benchmark="gsm8k", agent="replay", agent_kwargs={"recording": "path/to/recording"}) + +A *recording* is a directory containing: + trajectory.jsonl — the recorded action/observation events + session.json — session manifest (task, context, actions schema) + +These files are produced automatically by ``exgentic evaluate`` (under +``outputs//sessions//``). +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, ClassVar + +from ...core.agent import Agent +from ...core.agent_instance import AgentInstance +from ...core.types import Action, Observation + + +class ReplayAgentInstance(AgentInstance): + """Replays recorded actions from a trajectory file.""" + + def __init__( + self, + *, + session_id: str, + trajectory_path: Path, + ) -> None: + super().__init__(session_id=session_id) + self._actions = self._load_actions(trajectory_path) + self._step = 0 + + def start(self, task, context, actions): + """Receive work payload and build the action_types lookup.""" + super().start(task, context, actions) + self._action_types = {at.name: at for at in self.actions} + + @staticmethod + def _load_actions(trajectory_path: Path) -> list[dict]: + """Extract action events from a trajectory JSONL file.""" + actions = [] + with open(trajectory_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + event = json.loads(line) + if event.get("event") == "action": + actions.append(event["action"]) + return actions + + def react(self, observation: Observation | None) -> Action | None: + if self._step >= len(self._actions): + return None # No more recorded actions — signal done + + action_data = self._actions[self._step] + self._step += 1 + + name = action_data.get("name", "") + arguments = action_data.get("arguments", {}) + + action_type = self._action_types.get(name) + if action_type is None: + from ...core.actions import build_unknown_action + + return build_unknown_action(name, arguments) + + return action_type.build_action(arguments) + + def close(self) -> None: + pass + + +class ReplayAgent(Agent): + """Agent that replays pre-recorded actions from a trajectory file.""" + + display_name: ClassVar[str] = "Replay Agent" + slug_name: ClassVar[str] = "replay" + recording: str # Path to the recording directory (or trajectory.jsonl file) + runner: str | None = "direct" # No external deps — run in host process + + @classmethod + def _get_instance_class(cls): + return ReplayAgentInstance + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + recording_path = Path(self.recording) + if recording_path.is_file(): + trajectory_path = recording_path + else: + trajectory_path = recording_path / "trajectory.jsonl" + return { + "session_id": session_id, + "trajectory_path": trajectory_path, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_benchmark.py new file mode 100644 index 00000000..c9af05f9 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_benchmark.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""ReplayBenchmark — replays a full recorded session (agent + environment). + +Pairs with ReplayAgent + ReplaySession to test the full execution loop +without needing any benchmark dependencies installed. + +Usage (from tests):: + + benchmark = ReplayBenchmark(recording_dir="path/to/recording") + agent = ReplayAgent(recording="path/to/recording") + results = evaluate(benchmark=benchmark, agent=agent) +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, ClassVar + +from ...core.benchmark import Benchmark +from ...core.evaluator import Evaluator +from ...core.types import BenchmarkResults, SessionIndex +from .replay_session import ReplaySession + + +class ReplayEvaluator(Evaluator): + """Evaluator that returns session kwargs for ReplaySession.""" + + def __init__(self, recording_dir: str) -> None: + self._recording_dir = recording_dir + + def list_tasks(self) -> list[str]: + recording = Path(self._recording_dir) + # Try to get task_id from session.json + manifest_path = recording / "session.json" + if manifest_path.exists(): + manifest = json.loads(manifest_path.read_text()) + task_id = manifest.get("task_id", "0") + return [str(task_id)] + return ["0"] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + return { + "recording_dir": self._recording_dir, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + paths = self.get_sessions_paths(sessions) + scores = [] + for p in paths: + results_path = p.benchmark_results + if results_path.exists(): + data = json.loads(results_path.read_text()) + score = data.get("score") + if score is not None: + scores.append(float(score)) + + avg_score = sum(scores) / len(scores) if scores else 0.0 + return BenchmarkResults( + benchmark_name="replay", + total_tasks=len(sessions), + score=avg_score, + ) + + +class ReplayBenchmark(Benchmark): + """Benchmark that replays recorded sessions from a directory.""" + + display_name: ClassVar[str] = "Replay Benchmark" + slug_name: ClassVar[str] = "replay" + recording_dir: str + + @classmethod + def _get_evaluator_class(cls): + return ReplayEvaluator + + @classmethod + def _get_session_class(cls): + return ReplaySession + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return {"recording_dir": self.recording_dir} + + def runner_kwargs(self) -> dict[str, Any]: + kw = super().runner_kwargs() + if self.resolve_runner() == "docker": + recording_dir = str(Path(self.recording_dir).resolve()) + kw.setdefault("volumes", {})[recording_dir] = recording_dir + return kw diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_session.py b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_session.py new file mode 100644 index 00000000..e13b38ee --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/replay/replay_session.py @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""ReplaySession — replays recorded observations from a trajectory. + +Used together with ReplayAgent to test the full execution loop +without needing any benchmark dependencies installed. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from json_schema_to_pydantic import create_model + +from ...core.session import Session +from ...core.types import Action, ActionType, Observation, SessionScore, SingleAction, SingleObservation + + +def _action_type_from_schema(entry: dict) -> ActionType: + """Reconstruct an ActionType from a session.json action entry.""" + schema = entry["arguments_schema"] + args_model = create_model(schema) + + # Create a SingleAction subclass with the right name + arguments type + action_cls = type( + f"{entry['name']}_Action", + (SingleAction,), + {"__annotations__": {"name": str, "arguments": args_model}}, + ) + + return ActionType( + name=entry["name"], + description=entry.get("description", ""), + cls=action_cls, + is_finish=entry.get("is_finish", False), + is_message=entry.get("is_message", False), + is_hidden=entry.get("is_hidden", False), + ) + + +class ReplaySession(Session): + """Session that replays recorded observations from a trajectory file. + + Does not require any benchmark dependencies — everything is + reconstructed from the recording (session.json + trajectory.jsonl). + """ + + def __init__( + self, + recording_dir: str, + *, + session_id: str | None = None, + ) -> None: + recording = Path(recording_dir) + manifest = json.loads((recording / "session.json").read_text()) + + self._task_id_val = manifest.get("task_id", "") + self._task_val = manifest.get("task", "") + self._context_val = manifest.get("context", {}) + self._action_types = [_action_type_from_schema(a) for a in manifest.get("actions", [])] + + # Load recorded observations and score from trajectory/results + self._observations: list[Any] = [] + trajectory = recording / "trajectory.jsonl" + with open(trajectory, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + event = json.loads(line) + if event.get("event") == "observation" and event.get("observation") is not None: + self._observations.append(event["observation"]) + + # Load recorded score + results_path = recording / "results.json" + if results_path.exists(): + results = json.loads(results_path.read_text()) + self._recorded_score = results.get("details", results) + else: + self._recorded_score = {"score": 0.0, "success": False, "is_finished": True} + + self._step_idx = 0 + self._done = False + + if session_id is not None: + self._session_id = session_id + + super().__init__() + + @property + def task_id(self) -> str: + return self._task_id_val + + @property + def task(self) -> str: + return self._task_val + + @property + def context(self) -> dict[str, Any]: + return self._context_val + + @property + def actions(self) -> list[ActionType]: + return self._action_types + + def _next_observation(self) -> Observation | None: + if self._step_idx < len(self._observations): + obs = self._observations[self._step_idx] + self._step_idx += 1 + return SingleObservation(result=obs.get("result") if isinstance(obs, dict) else obs) + return None + + def start(self) -> Observation | None: + return self._next_observation() + + def step(self, action: Action) -> Observation | None: + obs = self._next_observation() + if obs is None: + self._done = True + return obs + + def done(self) -> bool: + return self._done + + def score(self) -> SessionScore: + data = self._recorded_score + return SessionScore( + score=float(data.get("score", 0.0)), + success=bool(data.get("success", False)), + is_finished=data.get("is_finished", True), + session_metrics=data.get("session_metrics", {}), + session_metadata=data.get("session_metadata", {}), + ) + + def close(self) -> None: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/__init__.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/__init__.py new file mode 100644 index 00000000..882dc64e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .base_agent import SmolagentBaseAgent +from .code_agent import SmolagentCodeAgent +from .tool_calling_agent import SmolagentToolCallingAgent + +__all__ = [ + "SmolagentBaseAgent", + "SmolagentCodeAgent", + "SmolagentToolCallingAgent", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_agent.py new file mode 100644 index 00000000..1a0634ec --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_agent.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from typing import Any, ClassVar + +from ...core.agent import Agent +from ...core.types import ModelSettings + + +class SmolagentBaseAgent(Agent): + display_name: ClassVar[str] = "SmolAgents Base Agent" + slug_name: ClassVar[str] = "smolagents_base" + + model: str = "watsonx/meta-llama/llama-3-3-70b-instruct" + max_steps: int = 150 + model_settings: ModelSettings | None = None + retry_on_all_errors: bool = True + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + return { + "session_id": session_id, + "model_id": self.model, + "max_steps": self.max_steps, + "model_settings": self.model_settings, + "retry_on_all_errors": self.retry_on_all_errors, + } + + @property + def model_name(self) -> str: # type: ignore[override] + return str(self.model).split("/")[-1] + + def get_models_names(self) -> list[str]: # type: ignore[override] + return [str(self.model)] diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_instance.py new file mode 100644 index 00000000..8a1ce679 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/base_instance.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import functools +import logging +from abc import abstractmethod +from collections.abc import Callable + +from rich.console import Console +from smolagents import LiteLLMModel +from smolagents.models import is_rate_limit_error +from smolagents.monitoring import AgentLogger, LogLevel +from smolagents.tools import Tool, tool +from smolagents.utils import AgentError, Retrying + +from ...adapters.agents.code_agent import CodeAgentInstance +from ...core.context import get_context +from ...core.types import ModelSettings, RetryStrategy +from ...integrations.litellm.health import check_model_accessible_sync +from ...observers.logging import close_logger +from ...utils.cost import CostReport, LiteLLMCostReport +from ...utils.settings import get_settings + +settings = get_settings() + + +class ContextInjectingLiteLLMModel(LiteLLMModel): + """Wrapper around LiteLLMModel that injects context into litellm_metadata.""" + + def generate(self, *args, **kwargs): + """Inject context into litellm_metadata before calling the model.""" + # Use 'metadata' parameter instead of 'litellm_metadata' + # LiteLLM passes 'metadata' to callbacks in litellm_params.metadata + kwargs.setdefault("metadata", {})["context"] = get_context() + + return super().generate(*args, **kwargs) + + +class SmolagentBaseAgentInstance(CodeAgentInstance): + def __init__( + self, + session_id: str, + model_id: str, + max_steps: int = 150, + model_settings: ModelSettings | None = None, + retry_on_all_errors: bool = True, + ): + super().__init__(session_id) + self.model_id = model_id + self.max_steps = max_steps + if model_settings is None: + self.model_settings = ModelSettings() + elif isinstance(model_settings, ModelSettings): + self.model_settings = model_settings + else: + raise ValueError("model_settings must be a ModelSettings instance.") + self._retry_on_all_errors = retry_on_all_errors + self._agent = None + self._model = None + + # Check model accessibility + check_model_accessible_sync(self.model_id, logger=self.logger) + + def run_code_agent(self, functions: list[Callable]) -> None: + def _wrap_tool(fn: Callable) -> Callable: + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except RuntimeError as exc: + if "after close" in str(exc): + agent_logger = self.get_smolagent_logger() + if agent_logger is None: + agent_logger = AgentLogger( + console=Console(), + level=LogLevel.ERROR, + ) + raise AgentError("Agent interrupted (session closed).", agent_logger) from exc + raise + + return wrapper + + tools = [tool(_wrap_tool(function)) for function in functions] + return self.run_smolagent(tools=tools) + + def get_smolagent_logger(self): + smolagent_logger = None + for handler in self.logger.handlers: + if isinstance(handler, logging.FileHandler): + console = Console( + file=handler.stream, + force_terminal=False, + color_system=None, + highlight=False, + ) + smolagent_logger = AgentLogger(console=console, level=LogLevel.DEBUG) + return smolagent_logger + + def get_internal_model(self): + if self._model is None: + temperature = self.model_settings.temperature + self._model = ContextInjectingLiteLLMModel( + model_id=self.model_id, + temperature=temperature if temperature is not None else 1.0, + max_tokens=self.model_settings.max_tokens, + caching=settings.litellm_caching, + ) + num_retries = self.model_settings.num_retries or 0 + max_attempts = num_retries + 1 if num_retries > 0 else 1 + retry_strategy = self.model_settings.retry_strategy.value + exponential_base = 2.0 if retry_strategy == RetryStrategy.EXPONENTIAL_BACKOFF.value else 1.0 + log_level = logging._nameToLevel.get(settings.log_level, logging.INFO) + self._model.retryer = Retrying( + max_attempts=max_attempts, + wait_seconds=self.model_settings.retry_after, + exponential_base=exponential_base, + jitter=False, + retry_predicate=self.retry_predicate, + reraise=True, + before_sleep_logger=(self.logger, log_level), + after_logger=None, + ) + return self._model + + def retry_predicate(self, exc: BaseException) -> bool: + if self._retry_on_all_errors: + return True + return is_rate_limit_error(exc) + + @abstractmethod + def run_smolagent(self, tools: list[Tool]): + raise NotImplementedError + + def close(self): + self.logger.info("Interrupting Smolagent...") + if self._agent is not None: + self._agent.interrupt() + super().close() + self.logger.debug("Closing logger.") + close_logger(self.logger) + + def get_cost(self) -> CostReport: + if self._agent is None: + return LiteLLMCostReport.initialize_empty(model_name=self.model_id) + + token_usage = self._agent.monitor.get_total_token_counts() + + return LiteLLMCostReport.from_token_counts( + model_name=self.model_id, + input_tokens=token_usage.input_tokens, + output_tokens=token_usage.output_tokens, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_agent.py new file mode 100644 index 00000000..9466f716 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_agent.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from typing import ClassVar + +from .base_agent import SmolagentBaseAgent + + +class SmolagentCodeAgent(SmolagentBaseAgent): + display_name: ClassVar[str] = "SmolAgents Code" + slug_name: ClassVar[str] = "smolagents_code" + + @classmethod + def _get_instance_class(cls): + from .code_instance import SmolagentCodeAgentInstance + + return SmolagentCodeAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.smolagents.code_instance:SmolagentCodeAgentInstance" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_instance.py new file mode 100644 index 00000000..5d52cb8c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/code_instance.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import os + +import yaml +from smolagents import CodeAgent as SmolagentBaseCodeAgent +from smolagents.tools import Tool +from smolagents.utils import AgentError + +from .base_instance import SmolagentBaseAgentInstance + + +class SmolagentCodeAgentInstance(SmolagentBaseAgentInstance): + """Smolagent implementation.""" + + def run_smolagent(self, tools: list[Tool]): + # Load custom structured prompt templates from YAML next to this module + prompt_path = os.path.join(os.path.dirname(__file__), "structured_code_agent.yaml") + try: + with open(prompt_path, encoding="utf-8-sig") as f: + prompt_templates = yaml.safe_load(f) + except Exception: + prompt_templates = None + + self._agent = SmolagentBaseCodeAgent( + tools=tools, + model=self.get_internal_model(), + prompt_templates=prompt_templates, + use_structured_outputs_internally=True, + logger=self.get_smolagent_logger(), + ) + # Remove built-in final_answer; termination should happen by interacting with the benchmark (finish action). + self._agent.tools.pop("final_answer", None) + + prompt = f"Task: {self.task}\n\n" + if self.context: + prompt += f"Context: {self.context}\n\n" + prompt += ( + "Complete this task using the available functions. " + "Each function corresponds to an action you can take to solve the given task.\n" + "Every action should be taken only by calling one of the functions. " + "If one function fail, consider using another, at any given point one of the functions\n" + "can be a valid next step. At any point you should executing actions by writing code. " + "do not call tools with tool calling mechanism.\n\n" + "Printing or any other code will be visible only by you alone.\n\n" + # "Always provide parameter names when calling function. Do not rely on positional arguments.\n" + ) + if self.initial_observation is not None and not self.initial_observation.is_empty(): + text = str(self.initial_observation).strip() + if text: + prompt += f"\nFirst Observation: {text}\n" + try: + self._agent.run(task=prompt, max_steps=self.max_steps) + + except AgentError as e: + self.logger.info(f"AgentError: {e}") + raise diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/requirements.txt new file mode 100644 index 00000000..f7a97d41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/requirements.txt @@ -0,0 +1 @@ +smolagents>=1.13.0 diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/structured_code_agent.yaml b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/structured_code_agent.yaml new file mode 100644 index 00000000..411dc2bb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/structured_code_agent.yaml @@ -0,0 +1,257 @@ +system_prompt: |- + You are an expert assistant who can solve any task using code blobs. You will be given a task to solve as best you can. + To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code. + To solve the task, you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Code:', and 'Observation:' sequences. + + At each step, in the 'Thought:' attribute, you should first explain your reasoning towards solving the task and the tools that you want to use. + Then in the 'Code' attribute, you should write the code in simple Python. + During each intermediate step, you can use 'print()' to save whatever important information you will then need. + These print outputs will then appear in the 'Observation:' field, which will be available as input for the next step. + In the end, if a completion tool is available (e.g., finish or a submit_* tool), call it to deliver the final answer. Read that tool's description for the exact required format. You will be generating a JSON object with the following structure: + ```json + { + "thought": "...", + "code": "..." + } + ``` + + Here are a few examples using notional tools (note: "finish" in examples is a placeholder for the actual completion tool provided in your tools list; use the real tool name and follow its description): + --- + Task: "Generate an image of the oldest person in this document." + + {"thought": "I will proceed step by step and use the following tools: `document_qa` to find the oldest person in the document, then `image_generator` to generate an image according to the answer.", "code": "answer = document_qa(document=document, question=\"Who is the oldest person mentioned?\")\nprint(answer)\n"} + Observation: "The oldest person in the document is John Doe, a 55 year old lumberjack living in Newfoundland." + + {"thought": "I will now generate an image showcasing the oldest person.", "code": "image = image_generator(\"A portrait of John Doe, a 55-year-old man living in Canada.\")\nfinish(image)\n"} + --- + Task: "What is the result of the following operation: 5 + 3 + 1294.678?" + + {"thought": "I will use python code to compute the result of the operation and then return the final answer using the completion tool.", "code": "result = 5 + 3 + 1294.678\nfinish(result)\n"} + + --- + Task: + In a 1979 interview, Stanislaus Ulam discusses with Martin Sherwin about other great physicists of his time, including Oppenheimer. + What does he say was the consequence of Einstein learning too much math on his creativity, in one word? + + {"thought": "I need to find and read the 1979 interview of Stanislaus Ulam with Martin Sherwin.", "code": "pages = web_search(query=\"1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein\")\nprint(pages)\n"} + Observation: + No result found for query "1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein". + + {"thought": "The query was maybe too restrictive and did not find any results. Let's try again with a broader query.", "code": "pages = web_search(query=\"1979 interview Stanislaus Ulam\")\nprint(pages)\n"} + Observation: + Found 6 pages: + [Stanislaus Ulam 1979 interview](https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/) + + [Ulam discusses Manhattan Project](https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/) + + (truncated) + + {"thought": "I will read the first 2 pages to know more.", "code": "for url in [\"https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/\", \"https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/\"]:\n whole_page = visit_webpage(url)\n print(whole_page)\n print(\"\n\" + \"=\"*80 + \"\n\") # Print separator between pages"} + + Observation: + Manhattan Project Locations: + Los Alamos, NM + Stanislaus Ulam was a Polish-American mathematician. He worked on the Manhattan Project at Los Alamos and later helped design the hydrogen bomb. In this interview, he discusses his work at + (truncated) + + {"thought": "I now have the final answer: from the webpages visited, Stanislaus Ulam says of Einstein: \"He learned too much mathematics and sort of diminished, it seems to me personally, it seems to me his purely physics creativity.\" Let's answer in one word.", "code": "finish(\"diminished\")"} + + --- + Task: "Which city has the highest population: Guangzhou or Shanghai?" + + {"thought": "I need to get the populations for both cities and compare them: I will use the tool `web_search` to get the population of both cities.", "code": "for city in [\"Guangzhou\", \"Shanghai\"]:\n print(f\"Population {city}:\", web_search(f\"{city} population\")"} + Observation: + Population Guangzhou: ['Guangzhou has a population of 15 million inhabitants as of 2021.'] + Population Shanghai: '26 million (2019)' + + {"thought": "Now I know that Shanghai has the highest population.", "code": "finish(\"Shanghai\")"} + + --- + Task: "What is the current age of the pope, raised to the power 0.36?" + + {"thought": "I will use the tool `wikipedia_search` to get the age of the pope, and confirm that with a web search.", "code": "pope_age_wiki = wikipedia_search(query=\"current pope age\")\nprint(\"Pope age as per wikipedia:\", pope_age_wiki)\npope_age_search = web_search(query=\"current pope age\")\nprint(\"Pope age as per google search:\", pope_age_search)"} + Observation: + Pope age: "The pope Francis is currently 88 years old." + + {"thought": "I know that the pope is 88 years old. Let's compute the result using python code.", "code": "pope_current_age = 88 ** 0.36\nfinish(pope_current_age)"} + + Above example were using notional tools that might not exist for you. On top of performing computations in the Python code snippets that you create, you only have access to these tools, behaving like regular python functions: + ```python + {%- for tool in tools.values() %} + {{ tool.to_code_prompt() }} + {% endfor %} + ``` + + {%- if managed_agents and managed_agents.values() | list %} + You can also give tasks to team members. + Calling a team member works similarly to calling a tool: provide the task description as the 'task' argument. Since this team member is a real human, be as detailed and verbose as necessary in your task description. + You can also include any relevant variables or context using the 'additional_args' argument. + Here is a list of the team members that you can call: + ```python + {%- for agent in managed_agents.values() %} + def {{ agent.name }}(task: str, additional_args: dict[str, Any]) -> str: + """{{ agent.description }} + + Args: + task: Long detailed description of the task. + additional_args: Dictionary of extra inputs to pass to the managed agent, e.g. images, dataframes, or any other contextual data it may need. + """ + {% endfor %} + ``` + {%- endif %} + + {%- if custom_instructions %} + {{custom_instructions}} + {%- endif %} + + Here are the rules you should always follow to solve your task: + 1. Use only variables that you have defined! + 2. Always use the right arguments for the tools. DO NOT pass the arguments as a dict as in 'answer = wikipedia_search({'query': "What is the place where James Bond lives?"})', but use the arguments directly as in 'answer = wikipedia_search(query="What is the place where James Bond lives?")'. + 3. Take care to not chain too many sequential tool calls in the same code block, especially when the output format is unpredictable. For instance, a call to wikipedia_search has an unpredictable return format, so do not have another tool call that depends on its output in the same block: rather output results with print() to use them in the next block. + 4. Call a tool only when needed, and never re-do a tool call that you previously did with the exact same parameters. + 5. Don't name any new variable with the same name as a tool: for instance don't name a variable 'final_answer'. + 6. Never create any notional variables in our code, as having these in your logs will derail you from the true variables. + 7. You can use imports in your code, but only from the following list of modules: {{authorized_imports}} + 8. The state persists between code executions: so if in one step you've created variables or imported modules, these will all persist. + 9. Don't give up! You're in charge of solving the task, not providing directions to solve it. + + Now Begin! +planning: + initial_plan: |- + You are a world expert at analyzing a situation to derive facts, and plan accordingly towards solving a task. + Below I will present you a task. You will need to 1. build a survey of facts known or needed to solve the task, then 2. make a plan of action to solve the task. + + ## 1. Facts survey + You will build a comprehensive preparatory survey of which facts we have at our disposal and which ones we still need. + These "facts" will typically be specific names, dates, values, etc. Your answer should use the below headings: + ### 1.1. Facts given in the task + List here the specific facts given in the task that could help you (there might be nothing here). + + ### 1.2. Facts to look up + List here any facts that we may need to look up. + Also list where to find each of these, for instance a website, a file... - maybe the task contains some sources that you should reuse here. + + ### 1.3. Facts to derive + List here anything that we want to derive from the above by logical reasoning, for instance computation or simulation. + + Don't make any assumptions. For each item, provide a thorough reasoning. Do not add anything else on top of three headings above. + + ## 2. Plan + Then for the given task, develop a step-by-step high-level plan taking into account the above inputs and list of facts. + This plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer. + Do not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS. + After writing the final step of the plan, write the '' tag and stop there. + + You can leverage these tools, behaving like regular python functions: + ```python + {%- for tool in tools.values() %} + {{ tool.to_code_prompt() }} + {% endfor %} + ``` + + {%- if managed_agents and managed_agents.values() | list %} + You can also give tasks to team members. + Calling a team member works similarly to calling a tool: provide the task description as the 'task' argument. Since this team member is a real human, be as detailed and verbose as necessary in your task description. + You can also include any relevant variables or context using the 'additional_args' argument. + Here is a list of the team members that you can call: + ```python + {%- for agent in managed_agents.values() %} + def {{ agent.name }}(task: str, additional_args: dict[str, Any]) -> str: + """{{ agent.description }} + + Args: + task: Long detailed description of the task. + additional_args: Dictionary of extra inputs to pass to the managed agent, e.g. images, dataframes, or any other contextual data it may need. + """ + {% endfor %} + ``` + {%- endif %} + + --- + Now begin! Here is your task: + ``` + {{task}} + ``` + First in part 1, write the facts survey, then in part 2, write your plan. + update_plan_pre_messages: |- + You are a world expert at analyzing a situation, and plan accordingly towards solving a task. + You have been given the following task: + ``` + {{task}} + ``` + + Below you will find a history of attempts made to solve this task. + You will first have to produce a survey of known and unknown facts, then propose a step-by-step high-level plan to solve the task. + If the previous tries so far have met some success, your updated plan can build on these results. + If you are stalled, you can make a completely new plan starting from scratch. + + Find the task and history below: + update_plan_post_messages: |- + Now write your updated facts below, taking into account the above history: + ## 1. Updated facts survey + ### 1.1. Facts given in the task + ### 1.2. Facts that we have learned + ### 1.3. Facts still to look up + ### 1.4. Facts still to derive + + Then write a step-by-step high-level plan to solve the task above. + ## 2. Plan + ### 2. 1. ... + Etc. + This plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer. + Beware that you have {remaining_steps} steps remaining. + Do not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS. + After writing the final step of the plan, write the '' tag and stop there. + + You can leverage these tools, behaving like regular python functions: + ```python + {%- for tool in tools.values() %} + {{ tool.to_code_prompt() }} + {% endfor %} + ``` + + {%- if managed_agents and managed_agents.values() | list %} + You can also give tasks to team members. + Calling a team member works similarly to calling a tool: provide the task description as the 'task' argument. Since this team member is a real human, be as detailed and verbose as necessary in your task description. + You can also include any relevant variables or context using the 'additional_args' argument. + Here is a list of the team members that you can call: + ```python + {%- for agent in managed_agents.values() %} + def {{ agent.name }}(task: str, additional_args: dict[str, Any]) -> str: + """{{ agent.description }} + + Args: + task: Long detailed description of the task. + additional_args: Dictionary of extra inputs to pass to the managed agent, e.g. images, dataframes, or any other contextual data it may need. + """ + {% endfor %} + ``` + {%- endif %} + + Now write your updated facts survey below, then your new plan. +managed_agent: + task: |- + You're a helpful agent named '{{name}}'. + You have been submitted this task by your manager. + --- + Task: + {{task}} + --- + You're helping your manager solve a wider task: so make sure to not provide a one-line answer, but give as much information as possible to give them a clear understanding of the answer. + + Your final submission via the completion tool MUST contain these parts unless the tool description specifies a different format (in which case, follow the tool description exactly): + ### 1. Task outcome (short version): + ### 2. Task outcome (extremely detailed version): + ### 3. Additional context (if relevant): + + Put all these in the completion tool call, and read the tool's description for the required argument names and format. Everything that you do not pass as an argument to that tool will be lost. + And even if your task resolution is not successful, please return as much context as possible, so that your manager can act upon this feedback. + report: |- + Here is the final answer from your managed agent '{{name}}': + {{final_answer}} +final_answer: + pre_messages: |- + An agent tried to answer a user query but it got stuck and failed to do so. You are tasked with providing an answer instead. Here is the agent's memory: + post_messages: |- + Based on the above, please provide an answer to the following user task: + {{task}} diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_agent.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_agent.py new file mode 100644 index 00000000..7f3bccb7 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_agent.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from typing import ClassVar + +from .base_agent import SmolagentBaseAgent + + +class SmolagentToolCallingAgent(SmolagentBaseAgent): + display_name: ClassVar[str] = "SmolAgents Tool Calling" + slug_name: ClassVar[str] = "smolagents_tool" + + @classmethod + def _get_instance_class(cls): + from .tool_calling_instance import SmolagentToolCallingAgentInstance + + return SmolagentToolCallingAgentInstance + + @classmethod + def _get_instance_class_ref(cls) -> str: + return "exgentic.agents.smolagents.tool_calling_instance:SmolagentToolCallingAgentInstance" diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_instance.py b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_instance.py new file mode 100644 index 00000000..9426b003 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/smolagents/tool_calling_instance.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from smolagents import ToolCallingAgent +from smolagents.tools import Tool +from smolagents.utils import AgentError + +from .base_instance import SmolagentBaseAgentInstance + + +class SmolagentToolCallingAgentInstance(SmolagentBaseAgentInstance): + """Smolagent implementation.""" + + def run_smolagent(self, tools: list[Tool]): + self._agent = ToolCallingAgent( + tools=tools, + model=self.get_internal_model(), + # use_structured_outputs_internally=True, + logger=self.get_smolagent_logger(), + ) + + prompt = f"Task: {self.task}\n\n" + if self.context: + prompt += f"Context: {self.context}\n\n" + prompt += ( + "Complete this task using the available tools. " + "Each tool corresponds to an action you can take in the environment.\n" + ) + if self.initial_observation is not None and not self.initial_observation.is_empty(): + text = str(self.initial_observation).strip() + if text: + prompt += f"\nFirst Observation: {text}\n" + try: + self._agent.run(task=prompt, max_steps=self.max_steps) + except AgentError as e: + self.logger.info(f"AgentError: {e}") + raise diff --git a/labs/AgentStream/exgentic/src/exgentic/agents/tool_shortlisting.py b/labs/AgentStream/exgentic/src/exgentic/agents/tool_shortlisting.py new file mode 100644 index 00000000..25ee84de --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/agents/tool_shortlisting.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The AgentStream organization and its contributors. + +from __future__ import annotations + +import logging +from typing import Any, Callable, List + +from litellm import ( + ChatCompletionDeveloperMessage, + ChatCompletionUserMessage, +) + + +def shortlist_tools( + tools: list[dict[str, Any]], + max_selected: int, + messages: list[Any], + completion_fn: Callable[..., Any], + model: str, + logger: logging.Logger, + *, + cost_callback: Callable[[Any], None] | None = None, +) -> list[dict[str, Any]]: + if len(tools) <= max_selected: + return tools + + logger.info("Tool shortlisting: %d available -> selecting top %d", len(tools), max_selected) + + names = [tool["function"]["name"] for tool in tools] + names_str = "" + for tool in tools: + names_str += f"\n- {tool['function']['name']}: {tool['function']['description']}" + + history_text = _render_history(messages) + + dev = ChatCompletionDeveloperMessage( + role="developer", + content=( + f"Please before providing your next move list the names of the top " + f"{max_selected} tools that are somewhat relevant for the next step, " + "ordered by relevancy (most to least). Return ONLY a JSON object with this shape: " + '{\n "tools": ["tool_name_1", "tool_name_2", ...]\n}.\n' + f"Choose from these tools only: {names_str}.\n" + f"Do not call any of those tools just return the list of the top " + f"{max_selected} relevant tools names in the required format." + ), + ) + history_msg = ChatCompletionUserMessage( + role="user", + content=f"Conversation so far (plain text):\n{history_text}", + ) + + try: + response = completion_fn(model=model, messages=[dev, history_msg]) + except Exception as exc: + logger.warning("Tool shortlisting LLM call failed: %s", exc) + return tools[:max_selected] + + if cost_callback and response and response.usage: + cost_callback(response.usage) + + text = response.choices[0].message.content + if text is None: + text = str(response.choices[0].message) + + positions = [] + for name in names: + idx = text.find(name) + if idx != -1: + positions.append((idx, name)) + + if len(positions) == 0: + logger.info("Tool shortlist fallback: no matches, taking first %d", max_selected) + return tools[:max_selected] + + positions.sort(key=lambda x: x[0]) + selected_names = [name for _, name in positions][:max_selected] + name_to_tool = {tool["function"]["name"]: tool for tool in tools} + selected_tools = [name_to_tool[name] for name in selected_names] + logger.info("Tool shortlist: %d -> %d", len(tools), len(selected_tools)) + return selected_tools + + +def _render_history(messages: list[Any]) -> str: + parts: List[str] = [] + for message in messages: + msg = message if isinstance(message, dict) else dict(message) + role = msg.get("role") or "unknown" + if role == "tool": + content = msg.get("content", "") + parts.append(f"tool: {content}") + continue + content = msg.get("content") + if content: + parts.append(f"{role}: {content}") + tool_calls = msg.get("tool_calls") or [] + for tc in tool_calls: + fn = tc.get("function", {}) if isinstance(tc, dict) else {} + parts.append(f"{role} tool_call: {fn.get('name', '?')}({fn.get('arguments', '')})") + return "\n".join(parts)[-8000:] diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/__init__.py new file mode 100644 index 00000000..4fa31ba8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/__init__.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .appworld_benchmark import AppWorldBenchmark + +__all__ = [ + "AppWorldBenchmark", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_benchmark.py new file mode 100644 index 00000000..a7d5c95a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_benchmark.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""AppWorld benchmark adapter -- light benchmark class only. + +Evaluator and session classes live in ``appworld_eval.py`` and are loaded +inside the runner subprocess via ``_get_evaluator_class()`` and +``_get_session_class()``. This file must remain importable without the +``appworld`` package installed. +""" + +from __future__ import annotations + +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, ConfigDict, Field + +from ...core.benchmark import Benchmark +from ...core.types import SingleObservation + + +class AppWorldObservation(SingleObservation): + pass + + +class AppWorldBenchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "AppWorld" + slug_name: ClassVar[str] = "appworld" + available_subsets: ClassVar[list[str]] = ["train", "dev", "test_normal", "test_challenge"] + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return "exgentic.benchmarks.appworld.appworld_eval:AppWorldEvaluator" + + @classmethod + def _get_session_class(cls): + return "exgentic.benchmarks.appworld.appworld_eval:AppWorldSession" + + # Inputs + subset: Literal["train", "dev", "test_normal", "test_challenge"] = "test_challenge" + env_kwargs: dict[str, Any] = Field(default_factory=dict) + max_interactions: int = 200 + tool_name_separator: Literal[".", "__"] = "__" + SCORES_FILE_NAME: ClassVar[str] = "scores.json" + + def list_subsets(self) -> list[str]: # type: ignore[override] + return list(self.available_subsets) + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "env_kwargs": self.env_kwargs, + "max_interactions": self.max_interactions, + "tool_name_separator": self.tool_name_separator, + "use_cache": self.use_cache, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_eval.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_eval.py new file mode 100644 index 00000000..535abcd3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/appworld_eval.py @@ -0,0 +1,673 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""AppWorld evaluator and session classes. + +These classes import the ``appworld`` package at method level. They are only +ever instantiated inside the isolated runner subprocess, so the heavy +dependency is never required in the host process. + +The light ``AppWorldBenchmark`` class lives in ``appworld_benchmark.py`` and +must remain importable without the ``appworld`` package installed. +""" + +from __future__ import annotations + +import json +import logging +import shutil +from pathlib import Path +from shutil import copytree +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +from pydantic import ( + BaseModel, + create_model, +) + +from ...core.actions import ActionsHandler +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + FinishAction, + MessageAction, + Observation, + SessionIndex, + SessionScore, + SingleAction, +) +from ...utils.paths import get_run_id, get_run_paths +from ...utils.settings import get_settings +from .appworld_benchmark import AppWorldObservation + +settings = get_settings() +logger = logging.getLogger(__name__) + +APPWORLD_TOTAL_TASKS = { + "train": 90, + "dev": 57, + "test_normal": 168, + "test_challenge": 417, +} + +if TYPE_CHECKING: + from appworld.environment import AppWorld # type: ignore + + +class AppWorldSession(Session): + """Session that hosts AppWorld directly (no separate WorldProcess). + + Tools are derived from the AppWorld task API docs and mapped to ActionTypes. + Each step calls AppWorld.requester.request(app, api, **args) and wraps the + result as observations, surfacing API errors as structured payloads. + """ + + CACHE_DIR: ClassVar[str] = "./appworld_disk_cache" + TASK_OUTPUT_SUBDIR: ClassVar[str] = "task_output" + SCORES_FILE_NAME: ClassVar[str] = "scores.json" + + def __init__( + self, + session_id: str | None = None, + task_spec: dict[str, Any] | None = None, + env_kwargs: dict[str, Any] | None = None, + use_cache: bool = True, + tool_name_separator: str = ".", + max_interactions: int | None = None, + ) -> None: + if session_id is not None: + self._session_id = session_id + self._task_spec = task_spec or {} + self._env_kwargs = env_kwargs or {} + self._tool_name_separator = tool_name_separator + self._max_interactions = max_interactions + self._action_count = 0 + self._registry = ActionsHandler( + logger=self.logger, + warn_on_validation_error=False, + warn_on_unknown_action=False, + handle_validation_error=lambda action, _msg: self.apply_action(action), + handle_unknown_action=self.apply_action, + ) + self._step_count: int = 0 + self._done: bool = False + self._world_closed: bool = False + self._cached_score: SessionScore | None = None + + # Resolve task_id + task_id = self._task_spec.get("task_id") if isinstance(self._task_spec, dict) else None + if isinstance(self._task_spec, str): + task_id = self._task_spec + if not task_id: + raise ValueError("AppWorldSession requires task_spec with 'task_id' or be a task_id string") + self._task_id = str(task_id) + + # Construct AppWorld in-process (lazy import to defer side effects) + from appworld import update_root # type: ignore + from appworld.common.constants import DEFAULT_EXPERIMENT_NAME # type: ignore + from appworld.common.path_store import path_store # type: ignore + from appworld.environment import AppWorld # type: ignore + + # Point appworld at the correct data directory before loading the task. + cache = Path(settings.cache_dir).expanduser() + update_root(str(cache / "appworld")) + + # Patch appworld's SQLite connection helper to allow cross-thread usage. + # The venv runner serves via uvicorn which may dispatch requests across + # threads, but appworld's @lru_cache'd connections default to + # check_same_thread=True, causing ProgrammingError. + self._patch_appworld_sqlite() + + self._world: AppWorld = AppWorld(task_id=self._task_id, **self._env_kwargs) + self._experiment_name: str = self._world.experiment_name or DEFAULT_EXPERIMENT_NAME + self._task_output_dir: Path = ( + Path(path_store.experiment_outputs) / self._experiment_name / "tasks" / self._task_id + ) + + self.logger.info(f"Task ID: {task_spec}") + super().__init__() + + @staticmethod + def _patch_appworld_sqlite() -> None: + """Patch appworld's SQLite helpers to tolerate cross-thread access. + + appworld's ``get_direct_sqlite3_connection`` creates connections with + the default ``check_same_thread=True``, then caches them via + ``@lru_cache``. When the uvicorn runner dispatches requests on + different threads, reusing those connections raises + ``sqlite3.ProgrammingError``. We patch the function and clear the + cache so fresh connections are created with ``check_same_thread=False``. + """ + import sqlite3 as _sqlite3 + + from appworld.apps.lib.models import db as _appworld_db # type: ignore + + _original = _appworld_db.get_direct_sqlite3_connection + + if getattr(_original, "_exgentic_patched", False): + return + + def _safe_connect(db_app_path: str) -> _sqlite3.Connection: + conn = _sqlite3.connect(db_app_path, check_same_thread=False) + conn.execute("PRAGMA mmap_size = 268435456") + return conn + + _safe_connect._exgentic_patched = True # type: ignore[attr-defined] + _appworld_db.get_direct_sqlite3_connection = _safe_connect + # Clear the lru_cache so stale thread-bound connections aren't reused. + _appworld_db.get_direct_cached_sqlite3_connection.cache_clear() + + def get_config(self) -> dict[str, Any]: + return { + "task_spec": self._task_spec, + "env_kwargs": self._env_kwargs, + "tool_name_separator": self._tool_name_separator, + "max_interactions": self._max_interactions, + } + + @property + def world(self) -> AppWorld: + return self._world + + @property + def task(self) -> str: + self.logger.info(f"Task: {self.world.task.instruction}") + return "Task from supervisor:\n" + self.world.task.instruction + + @property + def context(self) -> dict[str, Any]: + if self.world.task is None: + raise ValueError("AppWorld task is not initialized") + + allowed = ", ".join(app for app in self.world.task.allowed_apps if app != "api_docs") + return { + "policy": ( + "This environment provides a set of applications," + " each exposing a predefined set of APIs that may" + " be used to perform tasks on behalf of the" + " supervisor. The applications include:" + f" {allowed}.\n" + " The available applications and their APIs are" + " fixed for the task.\n" + "\n" + "Supervisor account credentials (such as emails," + " usernames, and passwords) are available through" + " the supervisor application's APIs and are" + " accessed from there when required.\n" + "\n" + "If an application requires an access token to" + " perform authenticated operations, the access" + " token is obtained by calling that application's" + " authentication/login API using the credentials" + " retrieved from the supervisor application." + " Access tokens are not provided by the supervisor" + " application.\n" + "\n" + "References to people (e.g., friends, family," + " roommates) correspond to entries in the" + " phone_contacts application.\n" + "References to files or storage correspond to the" + " file_system application, not the local machine" + " filesystem.\n" + "\n" + "Time-based instructions (e.g., 'this month'," + " 'yesterday') are interpreted with full calendar" + " boundary ranges.\n" + "If an API returns paginated results, all pages" + " constitute the complete result.\n" + "\n" + "The environment consists only of the provided" + " applications and their documented APIs and" + " parameters. No additional endpoints, methods," + " arguments, or capabilities are assumed beyond" + " those explicitly defined.\n" + "\n" + "When task execution is finished, the designated" + " task-completion API is used to signal completion." + " If the task requires a final answer value, the" + " answer is returned through that completion API." + " If the task cannot be completed using the" + " available applications and APIs, the task may be" + " marked as failed." + ), + "supervisor": dict(self.world.task.supervisor), + # "app_descriptions": self.world.task.app_descriptions, + # "allowed_apps": self.world.task.allowed_apps, + "datetime": self.world.task.datetime.isoformat(), + } + + @property + def actions(self) -> list[ActionType]: + if not self._registry.actions: + # Build ActionTypes from AppWorld function_calling docs to leverage enriched auth parameters + from ...adapters.schemas.json_schema import make_args_model_from_json_schema + + tools_specs = self.world.task.api_docs.function_calling() + for tool in tools_specs: + function = tool["function"] + raw_name = function["name"] + separator = self._tool_name_separator + name = raw_name.replace("__", separator) + app, api = name.split(separator, 1) + if app == "api_docs": + continue + if api == "show_active_task": + continue + + args_model = make_args_model_from_json_schema(name, function["parameters"]) + + if raw_name == "supervisor__complete_task": + finish_act = create_model( + "AppWorldFinishAction", + __base__=FinishAction, + arguments=(args_model, ...), + ) + self._registry.add_action( + name="finish", + description=function["description"], + action_cls=finish_act, + handler=self.apply_action, + is_finish=True, + ) + else: + act = create_model( + f"{name}_Action", + __base__=SingleAction, + name=(Literal[name], name), + arguments=(args_model, ...), + ) + self._registry.add_action( + name=name, + description=function["description"], + action_cls=act, + handler=self.apply_action, + ) + return self._registry.actions + + @property + def task_id(self) -> str: + return str(self._task_id) + + @property + def _actions_names(self) -> set[str]: + return {a.name for a in self.actions} + + def _to_observation(self, raw: Any, invoking: list[SingleAction] | None = None) -> Observation: + return AppWorldObservation(invoking_actions=invoking or [], result=raw) + + def start(self) -> Observation | None: + self.logger.info(f"session_start id={self.session_id} task_id={self._task_id}") + # Empty initial observation; task details are provided via task/context. + return EmptyObservation() + + def _is_message_action(self, action: SingleAction) -> bool: + if isinstance(action, MessageAction) or action.name == "message": + return True + return False + + def apply_action(self, action: SingleAction): + if self._is_message_action(action): + self._step_count += 1 + return AppWorldObservation( + invoking_actions=[action], + result="Error: Sending a message is not allowed. Please use only one of the available actions.", + ) + # if action.name not in self._actions_names: + # return AppWorldObservation(invoking_actions=[action], result="Wrong name: {action.name}") + # Map benchmark-level finish to the supervisor.complete_task endpoint + effective_name = action.name + if action.name == "finish": + separator = self._tool_name_separator + effective_name = f"supervisor{separator}complete_task" + + separator = self._tool_name_separator + parts = effective_name.split(separator, 1) + if len(parts) != 2: + parts = effective_name, "" + app_name, api_name = parts + + arguments = action.arguments + if isinstance(arguments, BaseModel): + arguments = arguments.model_dump() + + self.logger.info(f"App: {app_name}, Function: {api_name}, Arguments: {arguments}") + + try: + out = self.world.requester.request(app_name, api_name, **arguments) + except Exception as e: + try: + e = json.loads(str(e).split("\n")[-1])["message"] + except json.JSONDecodeError: + pass + out = "Error: " + str(e) + finally: + self._step_count += 1 + + self.logger.info(f"Output: {out}") + + return AppWorldObservation( + invoking_actions=[action], + result=out, + ) + + @staticmethod + def _max_interactions_error(observation: Observation) -> bool: + for obs in observation.to_observation_list(): + result = obs.result + if isinstance(result, str) and "Maximum number of executions" in result: + return True + return False + + def step(self, action: Action) -> Observation | None: + if self._done: + return None + + if self._max_interactions is not None: + incoming = len(action.to_action_list()) + if self._action_count + incoming > self._max_interactions: + self.logger.warning( + "AppWorld local max_interactions reached (%s/%s); terminating session", + self._action_count, + self._max_interactions, + ) + return None + + observation = self._registry.execute(action) + if observation is None: + return None + if self._max_interactions is not None: + self._action_count += len(action.to_action_list()) + if self._max_interactions_error(observation): + self.logger.warning( + "AppWorld max_interactions reached (%s/%s); terminating session", + self.world.num_interactions, + self.world.max_interactions, + ) + return None + return observation + + def done(self) -> bool: + return self.world.task_completed() + + def score(self) -> SessionScore: + if self._cached_score is not None: + return self._cached_score + # World was already saved and closed in close(); compute the actual evaluation score now. + from appworld.apps.lib.models.db import CachedDBHandler + from appworld.evaluator import evaluate_task + + test_tracker = evaluate_task( + task_id=self._task_id, + experiment_name=self._experiment_name, + suppress_errors=True, + save_report=False, + ) + score_value = float(test_tracker.pass_percentage) / 100.0 + self.logger.info( + "Evaluation results: pass_percentage=%s pass_count=%s fail_count=%s num_tests=%s success=%s", + test_tracker.pass_percentage, + test_tracker.pass_count, + test_tracker.fail_count, + test_tracker.num_tests, + test_tracker.success, + ) + + # Check task completion before evaluate_task potentially closes DB. + try: + finished = self.world.task_completed() + except Exception: + finished = bool(self._done) + # Reset cached DB handler for this task so later aggregate evaluation can run. + CachedDBHandler.reset(self._task_id) + # Surface benchmark evaluation details for downstream analysis. + session_metrics = { + "pass_percentage": test_tracker.pass_percentage, + "pass_count": test_tracker.pass_count, + "fail_count": test_tracker.fail_count, + "num_tests": test_tracker.num_tests, + "difficulty": test_tracker.difficulty, + "success": test_tracker.success, + } + tracker_dict = test_tracker.to_dict(stats_only=False) + scores_path = self.paths.benchmark_dir / self.SCORES_FILE_NAME + scores_path.parent.mkdir(parents=True, exist_ok=True) + with open(scores_path, "w", encoding="utf-8") as f: + json.dump( + { + "task_id": self._task_id, + "session_id": self.session_id, + "test_tracker": tracker_dict, + **session_metrics, + }, + f, + ensure_ascii=False, + indent=2, + ) + session_metadata = {"test_tracker": tracker_dict} + sc = SessionScore( + score=score_value, + success=test_tracker.success, + is_finished=finished, + session_metrics=session_metrics, + session_metadata=session_metadata, + ) + # Cache here so a later close()->score() call does not re-run + # evaluate_task() (which fails with IndexError after the task DB has + # already been reset by the first evaluation). + self._cached_score = sc + return sc + + def close(self): + # Save AppWorld task state and mirror logs + self.logger.info( + "Closing AppWorld session: steps=%s done=%s world_closed=%s", + self._step_count, + self._done, + self._world_closed, + ) + if self._world_closed: + self.logger.warning("AppWorld session close called more than once.") + try: + self.world.save() + except Exception: + self.logger.exception("AppWorld world.save failed") + raise + try: + self._done = self.world.task_completed() + except Exception: + # DB may already be closed by score()/evaluate_task; safe to skip. + self.logger.debug("AppWorld task_completed check skipped in close (DB likely closed)") + logs_src = self._task_output_dir / "logs" + if logs_src.exists(): + dest = self.paths.benchmark_dir / "logs" + dest.parent.mkdir(parents=True, exist_ok=True) + try: + copytree(logs_src, dest, dirs_exist_ok=True) + except Exception: + self.logger.exception("AppWorld log copy failed") + raise + task_output_src = self._task_output_dir + if task_output_src.exists(): + task_output_dest = self.paths.benchmark_dir / self.TASK_OUTPUT_SUBDIR + try: + if task_output_dest.exists(): + shutil.rmtree(task_output_dest) + copytree(task_output_src, task_output_dest) + except Exception: + self.logger.exception("AppWorld task output copy failed") + raise + # Write a standardized results.json plus AppWorld-specific fields + try: + sc = self.score() + except Exception: + self.logger.exception("AppWorld evaluation failed") + raise + self._cached_score = sc + try: + self.save_results( + { + "score": sc.score, + "success": sc.success, + "session_id": self.session_id, + "task_id": self._task_id, + "completed": self._done, + "steps": self._step_count, + } + ) + except Exception: + self.logger.exception("AppWorld save_results failed") + raise + self.logger.info( + "Session Finished | Success: %s, steps=%s, score=%s", + self._done, + self._step_count, + sc.score, + ) + try: + self.world.close() + except Exception: + self.logger.exception("AppWorld world.close failed") + raise + self._world_closed = True + experiment_root = self._task_output_dir.parent.parent + if experiment_root.name == self._experiment_name: + try: + shutil.rmtree(experiment_root) + except Exception: + self.logger.exception("AppWorld temp experiment cleanup failed") + + +class AppWorldEvaluator(Evaluator): + """Evaluator for AppWorld -- task discovery, session config, and aggregation.""" + + def __init__( + self, + subset: str = "test_normal", + env_kwargs: dict[str, Any] | None = None, + max_interactions: int = 200, + tool_name_separator: str = "__", + use_cache: bool = True, + ) -> None: + self._subset = subset + self._env_kwargs = env_kwargs or {} + self._max_interactions = max_interactions + self._tool_name_separator = tool_name_separator + self._use_cache = use_cache + self._experiment_name: str = "" + + def _ensure_appworld_root(self) -> None: + from appworld import update_root # type: ignore + + cache = Path(settings.cache_dir).expanduser() + root = str(cache / "appworld") + update_root(root) + + def list_tasks(self) -> list[str]: + from appworld.task import load_task_ids # type: ignore + + self._ensure_appworld_root() + items: list[str] | None = load_task_ids(self._subset) + if not items: + return [] + return [str(t) for t in items] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_appworld_root() + if not self._experiment_name: + self._experiment_name = get_run_id() + task_id = index.task_id + session_id = index.session_id + experiment_name = f"{self._experiment_name}__{session_id}" + spec = {"task_id": task_id} + + return { + "session_id": session_id, + "task_spec": spec, + "env_kwargs": { + **self._env_kwargs, + "max_interactions": self._max_interactions, + "experiment_name": experiment_name, + }, + "use_cache": self._use_cache, + "tool_name_separator": self._tool_name_separator, + "max_interactions": self._max_interactions, + } + + def _stage_task_outputs( + self, + *, + task_ids: list[str], + task_to_session: dict[str, str], + temp_output_dir: Path, + ) -> None: + run_paths = get_run_paths() + for task_id in task_ids: + session_id = task_to_session.get(task_id) + if not session_id: + raise FileNotFoundError(f"Missing session mapping for AppWorld task '{task_id}'.") + session_task_output = run_paths.session(session_id).benchmark_dir / AppWorldSession.TASK_OUTPUT_SUBDIR + if not session_task_output.exists(): + raise FileNotFoundError( + f"Missing staged task output for task='{task_id}' session='{session_id}' at {session_task_output}" + ) + dest_task_dir = temp_output_dir / "tasks" / task_id + dest_task_dir.parent.mkdir(parents=True, exist_ok=True) + copytree(session_task_output, dest_task_dir) + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + from appworld.evaluator import Metric, TestTracker # type: ignore + + self._ensure_appworld_root() + + if not sessions: + return BenchmarkResults( + benchmark_name="appworld", + total_tasks=0, + score=0.0, + metrics={}, + ) + + run_paths = get_run_paths() + task_id_to_test_tracker: dict[str, TestTracker] = {} + for session in sessions: + task_id = str(session.task_id) + results_path = run_paths.session(session.session_id).results + scores_path = run_paths.session(session.session_id).benchmark_dir / AppWorldSession.SCORES_FILE_NAME + + if scores_path.exists(): + with open(scores_path, encoding="utf-8") as f: + scores_payload = json.load(f) + tracker = scores_payload.get("test_tracker") + else: + logger.warning( + "Missing AppWorld scores file for task_id=%s session_id=%s at %s; " + "falling back to session metadata in %s", + task_id, + session.session_id, + scores_path, + results_path, + ) + with open(results_path, encoding="utf-8") as f: + payload = json.load(f) + tracker = (payload.get("details") or {}).get("session_metadata", {}).get("test_tracker") + if not isinstance(tracker, dict): + raise ValueError( + "Missing test_tracker in aggregation source for " + f"task_id={task_id} session_id={session.session_id}. " + f"Checked {scores_path} and fallback {results_path}." + ) + + task_id_to_test_tracker[task_id] = TestTracker.from_dict(tracker, suppress_errors=False) + + evaluation_dict = Metric.compute_metrics(task_id_to_test_tracker, include_details=True) + report = Metric.build_report(evaluation_dict) + return BenchmarkResults( + benchmark_name="appworld", + total_tasks=len(sessions), + score=evaluation_dict["aggregate"]["task_goal_completion"] / 100, + metrics=report, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/requirements.txt new file mode 100644 index 00000000..3ec94d1c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/requirements.txt @@ -0,0 +1 @@ +appworld @ git+https://github.com/StonyBrookNLP/appworld.git@edc960129fa6889c2b381715ecd108982029f6d1 diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/setup.sh b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/setup.sh new file mode 100644 index 00000000..cf9575ba --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/appworld/setup.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v git-lfs >/dev/null 2>&1; then + echo "Error: git-lfs is required but not installed. Install it first: brew install git-lfs (macOS) or apt-get install git-lfs (Linux)" >&2 + exit 1 +fi + +APPWORLD_ROOT="." +export APPWORLD_ROOT + +TMPDIR="$(mktemp -d)" +git lfs install >/dev/null 2>&1 || true +git clone https://github.com/StonyBrookNLP/appworld.git "$TMPDIR/appworld" +cd "$TMPDIR/appworld" +git checkout edc960129fa6889c2b381715ecd108982029f6d1 +git lfs pull + +uv pip install "." + +python -m appworld.cli install + +cd - >/dev/null 2>&1 || true +rm -rf "$TMPDIR" +python -m appworld.cli download data --root "." diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/__init__.py new file mode 100644 index 00000000..7f1e6dab --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/__init__.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""BFCL benchmark adapter.""" + +from .bfcl_benchmark import BFCLBenchmark + +__all__ = ["BFCLBenchmark"] diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_benchmark.py new file mode 100644 index 00000000..f804858d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_benchmark.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""BFCL benchmark adapter — light benchmark class only. + +Evaluator, session, and helper classes live in ``bfcl_eval.py`` +and are loaded inside the runner subprocess via ``_get_evaluator_class()`` +and ``_get_session_class()``. This file must remain importable without +the ``bfcl_eval`` package installed. +""" + +from __future__ import annotations + +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, ConfigDict + +from ...core import Benchmark +from ...core.types import FinishAction + +BFCLSubset = Literal[ + "simple_python", + "simple_java", + "simple_javascript", + "multiple", + "parallel", + "parallel_multiple", + "irrelevance", + "live_simple", + "live_multiple", + "live_parallel", + "live_parallel_multiple", + "live_irrelevance", + "live_relevance", + "multi_turn_base", + "multi_turn_long_context", + "multi_turn_miss_func", + "multi_turn_miss_param", +] + + +class BFCLFinishArgs(BaseModel): + content: str = "" + + +class BFCLFinishAction(FinishAction): + name: Literal["finish"] = "finish" + arguments: BFCLFinishArgs + + +class BFCLBenchmark(Benchmark, BaseModel): + """BFCL benchmark using Gorilla assets with an Exgentic-native runtime.""" + + display_name: ClassVar[str] = "BFCL" + slug_name: ClassVar[str] = "bfcl" + available_subsets: ClassVar[list[str]] = [ + "simple_python", + "simple_java", + "simple_javascript", + "multiple", + "parallel", + "parallel_multiple", + "irrelevance", + "live_simple", + "live_multiple", + "live_parallel", + "live_parallel_multiple", + "live_irrelevance", + "live_relevance", + "multi_turn_base", + "multi_turn_long_context", + "multi_turn_miss_func", + "multi_turn_miss_param", + ] + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + @classmethod + def _get_evaluator_class(cls): + return "exgentic.benchmarks.bfcl.bfcl_eval:BFCLEvaluator" + + @classmethod + def _get_session_class(cls): + return "exgentic.benchmarks.bfcl.bfcl_eval:BFCLSession" + + subset: BFCLSubset = "simple_python" + + def list_subsets(self) -> list[str]: # type: ignore[override] + return list(self.available_subsets) + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_eval.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_eval.py new file mode 100644 index 00000000..46386610 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_eval.py @@ -0,0 +1,576 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""BFCL evaluator and session classes. + +These classes import bfcl_eval (via bfcl_shim) at runtime. They are only +ever instantiated inside the isolated runner subprocess, so the heavy +``bfcl_eval`` dependency is never required in the host process. +""" + +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path +from typing import Any, Literal + +from pydantic import BaseModel + +from ...adapters.schemas.openai import openai_tools_to_action_types +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + MultiObservation, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from .bfcl_benchmark import BFCLFinishAction +from .bfcl_shim import load_bfcl_symbols + +BFCLSubset = Literal[ + "simple_python", + "simple_java", + "simple_javascript", + "multiple", + "parallel", + "parallel_multiple", + "irrelevance", + "live_simple", + "live_multiple", + "live_parallel", + "live_parallel_multiple", + "live_irrelevance", + "live_relevance", + "multi_turn_base", + "multi_turn_long_context", + "multi_turn_miss_func", + "multi_turn_miss_param", +] + + +def _is_relevance_subset(subset: str) -> bool: + return subset in {"irrelevance", "live_irrelevance", "live_relevance"} + + +def _is_multi_turn_subset(subset: str) -> bool: + return subset.startswith("multi_turn_") + + +def _language_for_subset(subset: str, symbols: dict[str, Any]) -> Any: + language = symbols["Language"] + if subset == "simple_java": + return language.JAVA + if subset == "simple_javascript": + return language.JAVASCRIPT + return language.PYTHON + + +def _merge_observations( + *items: SingleObservation | MultiObservation | None, +) -> SingleObservation | MultiObservation | None: + observations: list[SingleObservation] = [] + for item in items: + if item is None: + continue + if isinstance(item, MultiObservation): + observations.extend(item.observations) + continue + observations.append(item) + + if not observations: + return None + if len(observations) == 1: + return observations[0] + return MultiObservation(observations=observations) + + +def _action_arguments_dict(action: SingleAction) -> dict[str, Any]: + arguments = action.arguments + if isinstance(arguments, BaseModel): + return arguments.model_dump() + if isinstance(arguments, dict): + return dict(arguments) + return {"value": arguments} + + +def _render_turn_text(turn_messages: list[dict[str, Any]]) -> str: + parts: list[str] = [] + for message in turn_messages: + if not isinstance(message, dict): + continue + content = str(message.get("content", "")).strip() + if not content: + continue + role = str(message.get("role", "")) + if role == "system": + parts.append(f"System: {content}") + else: + parts.append(content) + return "\n\n".join(parts) + + +def _write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as handle: + json.dump(payload, handle, ensure_ascii=False, indent=2, default=str) + + +class BFCLSession(Session): + """Exgentic-native BFCL session with Gorilla-backed scoring.""" + + def __init__( + self, + subset: BFCLSubset, + prompt_entry: dict[str, Any], + possible_answer_entry: dict[str, Any] | None, + session_id: str | None = None, + ) -> None: + if session_id is not None: + self._session_id = session_id + + self.subset = subset + self.prompt_entry = prompt_entry + self.possible_answer_entry = possible_answer_entry + self._task_id = str(prompt_entry["id"]) + self._conversation_turns = [list(turn) for turn in prompt_entry.get("question", [])] + self._turn_texts = [_render_turn_text(turn) for turn in self._conversation_turns] + self._task_text = self._turn_texts[0] if self._turn_texts else "" + self._action_types = self._build_action_types(prompt_entry) + + self._current_turn_index = 0 + self._completed = False + self._result_payload: dict[str, Any] | None = None + + turn_count = max(1, len(self._conversation_turns)) + self._turn_step_calls: list[list[list[str]]] = [[] for _ in range(turn_count)] + self._turn_step_actions: list[list[list[dict[str, Any]]]] = [[] for _ in range(turn_count)] + + super().__init__() + + @property + def task(self) -> str: + return self._task_text + + @property + def context(self) -> dict[str, Any]: + return {"policy": self._build_context_text()} + + @property + def actions(self) -> list[ActionType]: + return self._action_types + + @property + def task_id(self) -> str: + return self._task_id + + def start(self) -> EmptyObservation: + return EmptyObservation() + + def step(self, action: Action) -> SingleObservation | MultiObservation | None: + if self._completed: + return None + if action is None: + raise ValueError("BFCL requires an action or the dedicated finish action.") + + flat_actions = action.to_action_list() + finish_requested = False + finish_action: SingleAction | None = None + tool_actions: list[SingleAction] = [] + for item in flat_actions: + if item.name == "finish": + finish_requested = True + finish_action = item + continue + tool_actions.append(item) + + step_call_strings = [self._action_to_function_call_string(item) for item in tool_actions] + if step_call_strings: + self._turn_step_calls[self._current_turn_index].append(step_call_strings) + self._turn_step_actions[self._current_turn_index].append( + [self._serialize_action(item) for item in tool_actions] + ) + + outputs = self._build_step_output_observation(tool_actions, step_call_strings) + if finish_requested: + # Generate a tool result for the finish action so that every + # tool_call_id in the assistant message has a matching tool + # response. Without this, providers with strict message + # validation (e.g. Azure OpenAI) reject the next request. + finish_obs = SingleObservation( + result="Turn finished.", + invoking_actions=[finish_action], + ) + outputs = _merge_observations(outputs, finish_obs) if outputs else finish_obs + return self._finish_turn(outputs) + + return outputs or EmptyObservation() + + def done(self) -> bool: + return self._completed + + def score(self) -> SessionScore: + if self._result_payload is None: + self._result_payload = self._compute_result_payload() + + return SessionScore( + score=float(self._result_payload["score"]), + success=bool(self._result_payload["success"]), + is_finished=self._result_payload.get("is_finished"), + session_metrics=self._result_payload.get("session_metrics", {}), + session_metadata=self._result_payload.get("session_metadata", {}), + ) + + def close(self) -> None: + if self._result_payload is None: + self._result_payload = self._compute_result_payload() + _write_json(self.paths.benchmark_results, self._result_payload) + + def _build_action_types(self, prompt_entry: dict[str, Any]) -> list[ActionType]: + symbols = load_bfcl_symbols() + functions = self._collect_functions(prompt_entry) + openai_tools = symbols["convert_to_tool"]( + functions, + symbols["GORILLA_TO_OPENAPI"], + symbols["ModelStyle"].OPENAI_COMPLETIONS, + ) + action_types = openai_tools_to_action_types(openai_tools) + action_types.append( + ActionType( + name="finish", + description="End the current BFCL step.", + cls=BFCLFinishAction, + is_finish=True, + ) + ) + return action_types + + def _collect_functions(self, prompt_entry: dict[str, Any]) -> list[dict[str, Any]]: + functions = [deepcopy(item) for item in prompt_entry.get("function", [])] + for items in prompt_entry.get("missed_function", {}).values(): + for item in items: + functions.append(deepcopy(item)) + + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in functions: + name = str(item.get("name", "")) + if not name or name in seen: + continue + seen.add(name) + deduped.append(item) + return deduped + + def _build_context_text(self) -> str: + if len(self._conversation_turns) <= 1: + return ( + "Complete the task using one or more actions, then call the dedicated " + "finish action. Clarification questions or any type of interaction with " + "the user is not permitted. In this task, actions are recorded rather " + "than executed through a live environment. Calling finish ends the " + "execution." + ) + if _is_multi_turn_subset(self.subset): + return ( + "Complete the current step using one or more actions, then call the " + "dedicated finish action. The finish action ends the current turn only, " + "not the entire session — continue using tools in subsequent turns. " + "Clarification questions or any type of interaction with the user is " + "not permitted." + ) + return ( + "Complete the current step using one or more actions, then call the " + "dedicated finish action. Clarification questions or any type of " + "interaction with the user is not permitted. In this task, actions are " + "recorded rather than executed through a live environment." + ) + + def _build_turn_observation(self, turn_index: int) -> SingleObservation: + if turn_index >= len(self._turn_texts): + return EmptyObservation() + text = self._turn_texts[turn_index] + if not text: + return EmptyObservation() + return SingleObservation( + result=text, + invoking_actions=[], + ) + + def _finish_turn( + self, outputs: SingleObservation | MultiObservation | None + ) -> SingleObservation | MultiObservation | None: + if self._current_turn_index >= len(self._conversation_turns) - 1: + self._completed = True + return outputs + + self._current_turn_index += 1 + next_turn = self._build_turn_observation(self._current_turn_index) + return _merge_observations(outputs, next_turn) or EmptyObservation() + + def _build_step_output_observation( + self, + tool_actions: list[SingleAction], + step_call_strings: list[str], + ) -> SingleObservation | MultiObservation | None: + if not tool_actions: + return None + + if _is_multi_turn_subset(self.subset): + raw_results = self._execute_multi_turn_step(step_call_strings) + else: + raw_results = ["Action recorded." for _ in tool_actions] + + observations = [ + SingleObservation(result=result, invoking_actions=[action]) + for action, result in zip(tool_actions, raw_results, strict=False) + ] + return _merge_observations(*observations) + + def _execute_multi_turn_step(self, step_call_strings: list[str]) -> list[str]: + symbols = load_bfcl_symbols() + execution_results, _ = symbols["execute_multi_turn_func_call"]( + func_call_list=step_call_strings, + initial_config=self.prompt_entry["initial_config"], + involved_classes=self.prompt_entry["involved_classes"], + model_name=f"{symbols['proxy_model_name']}_{self.session_id}_runtime", + test_entry_id=self._task_id, + long_context=("long_context" in self.subset), + is_evaL_run=False, + ) + return execution_results + + def _action_to_function_call_string(self, action: SingleAction) -> str: + arguments = _action_arguments_dict(action) + if not arguments: + return f"{action.name}()" + rendered = ", ".join(f"{key}={value!r}" for key, value in arguments.items()) + return f"{action.name}({rendered})" + + def _serialize_action(self, action: SingleAction) -> dict[str, Any]: + return { + "id": action.id, + "name": action.name, + "arguments": _action_arguments_dict(action), + } + + def _flatten_semantic_actions(self) -> list[dict[str, Any]]: + flattened: list[dict[str, Any]] = [] + for turn in self._turn_step_actions: + for step in turn: + for action in step: + flattened.append({action["name"]: action["arguments"]}) + return flattened + + def _compute_result_payload(self) -> dict[str, Any]: + trace_payload = { + "task_id": self._task_id, + "subset": self.subset, + "turn_step_calls": self._turn_step_calls, + "turn_step_actions": self._turn_step_actions, + } + trace_path = self.paths.benchmark_dir / "bfcl_trace.json" + _write_json(trace_path, trace_payload) + + if not self._completed: + payload = { + "score": 0.0, + "success": False, + "is_finished": False, + "session_metrics": { + "completed_turns": self._current_turn_index, + }, + "session_metadata": { + "bfcl_task_id": self._task_id, + "trace_file": str(trace_path), + }, + } + _write_json(self.paths.benchmark_dir / "bfcl_score.json", payload) + return payload + + try: + if _is_multi_turn_subset(self.subset): + payload = self._score_multi_turn(trace_path) + elif _is_relevance_subset(self.subset): + payload = self._score_relevance(trace_path) + else: + payload = self._score_ast(trace_path) + except Exception as exc: + payload = { + "score": 0.0, + "success": False, + "is_finished": False, + "session_metadata": { + "bfcl_task_id": self._task_id, + "trace_file": str(trace_path), + "error": str(exc), + "error_source": "benchmark", + }, + } + + _write_json(self.paths.benchmark_dir / "bfcl_score.json", payload) + return payload + + def _score_relevance(self, trace_path: Path) -> dict[str, Any]: + tool_call_count = len(self._flatten_semantic_actions()) + success = tool_call_count == 0 if "irrelevance" in self.subset else tool_call_count > 0 + return { + "score": 1.0 if success else 0.0, + "success": success, + "is_finished": True, + "session_metrics": { + "tool_call_count": tool_call_count, + "accuracy": 1.0 if success else 0.0, + }, + "session_metadata": { + "bfcl_task_id": self._task_id, + "trace_file": str(trace_path), + }, + } + + def _score_ast(self, trace_path: Path) -> dict[str, Any]: + if self.possible_answer_entry is None: + raise ValueError(f"Missing ground truth for subset '{self.subset}'.") + + symbols = load_bfcl_symbols() + checker_result = symbols["ast_checker"]( + self.prompt_entry["function"], + self._flatten_semantic_actions(), + self.possible_answer_entry["ground_truth"], + _language_for_subset(self.subset, symbols), + self.subset, + symbols["proxy_model_name"], + ) + score = 1.0 if checker_result["valid"] else 0.0 + return { + "score": score, + "success": bool(checker_result["valid"]), + "is_finished": True, + "session_metrics": { + "accuracy": score, + "action_count": len(self._flatten_semantic_actions()), + }, + "session_metadata": { + "bfcl_task_id": self._task_id, + "trace_file": str(trace_path), + "checker_result": checker_result, + }, + } + + def _score_multi_turn(self, trace_path: Path) -> dict[str, Any]: + if self.possible_answer_entry is None: + raise ValueError(f"Missing ground truth for subset '{self.subset}'.") + + model_turns = self._turn_step_calls + ground_truth_turns = self.possible_answer_entry["ground_truth"] + if len(model_turns) != len(ground_truth_turns): + checker_result = { + "valid": False, + "error_message": ( + "Model was force-terminated before completing all turns. " + f"Observed {len(model_turns)} turns for {len(ground_truth_turns)} ground-truth turns." + ), + "error_type": "multi_turn:force_terminated", + } + else: + symbols = load_bfcl_symbols() + checker_result = symbols["multi_turn_checker"]( + model_turns, + ground_truth_turns, + self.prompt_entry, + self.subset, + f"{symbols['proxy_model_name']}_{self.session_id}_score", + ) + + score = 1.0 if checker_result["valid"] else 0.0 + return { + "score": score, + "success": bool(checker_result["valid"]), + "is_finished": True, + "session_metrics": { + "accuracy": score, + "turn_count": len(model_turns), + }, + "session_metadata": { + "bfcl_task_id": self._task_id, + "trace_file": str(trace_path), + "checker_result": checker_result, + }, + } + + +# ── Evaluator ──────────────────────────────────────────────────────── + + +class BFCLEvaluator(Evaluator): + """Evaluator for BFCL — task discovery, session kwargs, aggregation.""" + + def __init__(self, subset: str = "simple_python") -> None: + self._subset: BFCLSubset = subset # type: ignore[assignment] + self._entries: list[dict[str, Any]] | None = None + self._answers_by_id: dict[str, dict[str, Any]] | None = None + + def _ensure_loaded(self) -> None: + if self._entries is not None and self._answers_by_id is not None: + return + symbols = load_bfcl_symbols() + entries = symbols["load_dataset_entry"](self._subset) + answers = ( + {} + if _is_relevance_subset(self._subset) + else {str(entry["id"]): entry for entry in symbols["load_ground_truth_entry"](self._subset)} + ) + self._entries = entries + self._answers_by_id = answers + + def list_tasks(self) -> list[str]: + self._ensure_loaded() + assert self._entries is not None + return [str(entry["id"]) for entry in self._entries] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_loaded() + assert self._entries is not None + prompt_entry = next( + (entry for entry in self._entries if str(entry["id"]) == str(index.task_id)), + None, + ) + if prompt_entry is None: + raise KeyError(f"Unknown BFCL task id '{index.task_id}' for subset '{self._subset}'.") + answer_entry = None if _is_relevance_subset(self._subset) else self._answers_by_id.get(str(index.task_id)) + return { + "subset": self._subset, + "prompt_entry": prompt_entry, + "possible_answer_entry": answer_entry, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + payloads: list[dict[str, Any]] = [] + for paths in self.get_sessions_paths(sessions): + result_path = paths.benchmark_results + if not result_path.exists(): + raise FileNotFoundError( + f"Missing BFCL results for planned session '{paths.session_id}' at {result_path}" + ) + with open(result_path, encoding="utf-8") as handle: + payloads.append(json.load(handle)) + + total_tasks = len(payloads) + total_score = sum(float(payload.get("score", 0.0)) for payload in payloads) + successes = sum(1 for payload in payloads if payload.get("success")) + return BenchmarkResults( + benchmark_name=f"bfcl-{self._subset}", + total_tasks=total_tasks, + score=(total_score / total_tasks) if total_tasks else 0.0, + metrics={ + "subset": self._subset, + "success_count": successes, + "accuracy": (total_score / total_tasks) if total_tasks else 0.0, + }, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_shim.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_shim.py new file mode 100644 index 00000000..88fcfafc --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/bfcl_shim.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Thin import shim around Gorilla's BFCL package.""" + +from __future__ import annotations + +from functools import lru_cache +from typing import Any + +EXGENTIC_BFCL_MODEL = "exgentic-proxy-fc" + + +@lru_cache(maxsize=1) +def load_bfcl_symbols() -> dict[str, Any]: + from bfcl_eval.constants.enums import Language, ModelStyle + from bfcl_eval.constants.model_config import MODEL_CONFIG_MAPPING, ModelConfig + from bfcl_eval.constants.type_mappings import GORILLA_TO_OPENAPI + from bfcl_eval.eval_checker.ast_eval.ast_checker import ast_checker + from bfcl_eval.eval_checker.eval_runner import ( + ast_file_runner, + multi_turn_runner, + relevance_file_runner, + ) + from bfcl_eval.eval_checker.multi_turn_eval.multi_turn_checker import ( + multi_turn_checker, + multi_turn_irrelevance_checker, + ) + from bfcl_eval.eval_checker.multi_turn_eval.multi_turn_utils import ( + execute_multi_turn_func_call, + ) + from bfcl_eval.model_handler.api_inference.openai_completion import OpenAICompletionsHandler + from bfcl_eval.model_handler.base_handler import BaseHandler + from bfcl_eval.model_handler.utils import convert_to_tool + from bfcl_eval.utils import load_dataset_entry, load_ground_truth_entry + + if EXGENTIC_BFCL_MODEL not in MODEL_CONFIG_MAPPING: + MODEL_CONFIG_MAPPING[EXGENTIC_BFCL_MODEL] = ModelConfig( + model_name=EXGENTIC_BFCL_MODEL, + display_name="Exgentic Proxy (FC)", + url="https://github.com/Exgentic/exgentic", + org="Exgentic", + license="Apache 2.0", + model_handler=OpenAICompletionsHandler, + input_price=None, + output_price=None, + is_fc_model=True, + underscore_to_dot=True, + ) + + return { + "BaseHandler": BaseHandler, + "Language": Language, + "ModelStyle": ModelStyle, + "OpenAICompletionsHandler": OpenAICompletionsHandler, + "GORILLA_TO_OPENAPI": GORILLA_TO_OPENAPI, + "convert_to_tool": convert_to_tool, + "load_dataset_entry": load_dataset_entry, + "load_ground_truth_entry": load_ground_truth_entry, + "ast_checker": ast_checker, + "multi_turn_checker": multi_turn_checker, + "multi_turn_irrelevance_checker": multi_turn_irrelevance_checker, + "execute_multi_turn_func_call": execute_multi_turn_func_call, + "ast_file_runner": ast_file_runner, + "multi_turn_runner": multi_turn_runner, + "relevance_file_runner": relevance_file_runner, + "proxy_model_name": EXGENTIC_BFCL_MODEL, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/setup.sh b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/setup.sh new file mode 100644 index 00000000..3af65e29 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/bfcl/setup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_ROOT="${SCRIPT_DIR}/installation" +REPO_DIR="${INSTALL_ROOT}/gorilla" +BFCL_DIR="${REPO_DIR}/berkeley-function-call-leaderboard" +GORILLA_URL="https://github.com/ShishirPatil/gorilla.git" +GORILLA_REF="7ad0134c665944819f88bc50862108d94015968b" # pragma: allowlist secret + +pip_install() { + if command -v uv >/dev/null 2>&1; then + uv pip install "$@" + else + python -m pip install "$@" + fi +} + +mkdir -p "${INSTALL_ROOT}" + +if [ ! -d "${REPO_DIR}/.git" ]; then + rm -rf "${REPO_DIR}" + git clone "${GORILLA_URL}" "${REPO_DIR}" +fi + +git -C "${REPO_DIR}" fetch --depth 1 origin "${GORILLA_REF}" +git -C "${REPO_DIR}" checkout --force "${GORILLA_REF}" + +pip_install -e "${BFCL_DIR}" +pip_install soundfile diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_benchmark.py new file mode 100644 index 00000000..b8c4eaaf --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_benchmark.py @@ -0,0 +1,697 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import datetime +import json +import os +from collections import defaultdict +from collections.abc import Mapping +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from ...core import Benchmark, Session +from ...core.actions import ActionsHandler +from ...core.evaluator import Evaluator +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + FinishAction, + MessageAction, + Observation, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from ...environment.instance import get_manager +from ...utils.cost import CostReport, LiteLLMCostReport +from ...utils.settings import RunnerName +from .retriever import RetrieverClient, get_retriever_url, get_shared_retriever + +if TYPE_CHECKING: + from .browsecomp_eval import BrowseCompEvaluator + +# Paper-reported total for the full BrowseCompPlus dataset. +DEFAULT_TOTAL_TASKS = 830 + + +class BrowseCompPlusSearchArgs(BaseModel): + query: str + + +class BrowseCompPlusSearchAction(SingleAction): + name: Literal["search"] = "search" + arguments: BrowseCompPlusSearchArgs + + +class BrowseCompPlusGetDocumentsArgs(BaseModel): + docid: str + + +class BrowseCompPlusGetDocAction(SingleAction): + name: Literal["get_document"] = "get_document" + arguments: BrowseCompPlusGetDocumentsArgs + + +class BrowseCompPlusFinishArgs(BaseModel): + exact_answer: str = Field(description="Your succinct, final answer") + explanation: str = Field( + description=( + "Your explanation for your final answer. For this" + " explanation section only, you should cite your" + " evidence documents inline by enclosing their docids" + " in square brackets [] at the end of sentences." + " For example, [20]." + ) + ) + confidence: float = Field(description="Your confidence score between 0% and 100% for your answer") + + +class BrowseCompPlusFinishAction(FinishAction): + name: Literal["submit"] = "submit" + arguments: BrowseCompPlusFinishArgs + + +class BrowseCompPlusSession(Session): + _done: bool + evaluator: BrowseCompEvaluator + + def __init__( + self, + instance: dict[str, Any], + searcher_params: dict[str, Any], + eval_model_id: str = "openai/Azure/gpt-4.1", + max_interactions: int | None = 100, + session_id: str | None = None, + retriever_url: str | None = None, + **_kwargs: Any, + ) -> None: + if session_id is not None: + self._session_id = session_id + self._instance = instance.copy() + self._task_id = instance["task_id"] + self._done = False + self._searcher_params = searcher_params + + # Initialize search: use shared retriever service if URL provided, + # otherwise load the index locally via SearchService singleton. + if retriever_url: + self._init_retriever_client(retriever_url, searcher_params) + else: + self._init_search_tool_handler(searcher_params) + + self._registry = ActionsHandler(logger=self.logger) + self.set_action_types() + self._response = None + self.evaluator = self.get_evaluator(eval_model_id) + self.retrieved_docids = set() + self.tool_call_count = defaultdict(lambda: 0) + self.model_usage = None + self.total_actions_executed = 0 + self.max_interactions = max_interactions + self.logger.info(f"Running for query id {self.task_id}") + super().__init__() + + def _init_retriever_client(self, url: str, searcher_params: dict[str, Any]) -> None: + """Connect to a shared Retriever service by URL.""" + from .search_tool_handler import BCPSearchToolHandler + + client = RetrieverClient(url) + self.search_tool_handler = BCPSearchToolHandler( + searcher=client, + snippet_max_tokens=searcher_params["max_snippet_length"], + k=searcher_params["top_k_docs"], + include_get_document=searcher_params["include_get_document"], + full_doc_max_tokens=searcher_params["full_doc_max_tokens"], + ) + + def _init_search_tool_handler(self, searcher_params: dict[str, Any]) -> None: + """Initialize search tool handler using local singleton service.""" + from searcher.searchers import SearcherType + + from .search_service import get_search_service + from .search_tool_handler import BCPSearchToolHandler + + # Get searcher from singleton service + search_service = get_search_service() + searcher_class = SearcherType.get_searcher_class(searcher_params["searcher_type"]) + + # Extract searcher-specific args + searcher_args = { + k: v + for k, v in searcher_params.items() + if k + not in [ + "searcher_type", + "max_snippet_length", + "top_k_docs", + "include_get_document", + "full_doc_max_tokens", + ] + } + + # Pass session's logger to the service + searcher_obj = search_service.get_or_create_searcher( + searcher_type=searcher_params["searcher_type"], + searcher_class=searcher_class, + logger=self.logger, + **searcher_args, + ) + + # Create search tool handler + self.search_tool_handler = BCPSearchToolHandler( + searcher=searcher_obj, + snippet_max_tokens=searcher_params["max_snippet_length"], + k=searcher_params["top_k_docs"], + include_get_document=searcher_params["include_get_document"], + full_doc_max_tokens=searcher_params["full_doc_max_tokens"], + ) + + @property + def task(self) -> str: + get_doc_str = "and document expansion " if self.search_tool_handler.include_get_document else "" + query = self._instance["query"] + return ( + "Answer the provided question by performing search " + f"{get_doc_str}as needed, and submit your final" + " answer.\n" + f"Question: {query}\n" + "Note:\n" + "- The question has an answer discoverable through" + " proper search.\n" + "- The question requires putting together information" + " from different sources.\n" + "\n" + "Your performance is scored based on:\n" + " 1. Most importantly, the correctness of the" + " answer you assembled from different searches.\n" + " 2. Your effective use of search and your ability" + " to retrieve all relevant information for the" + " question.\n" + " 3. How efficiently you find all the relevant" + " information, using as few searches as possible.\n" + "\n" + "Important: During your work, Do NOT interact with" + " the user or send any messages at any point" + " -- messages will be ignored and are NOT considered" + " a valid final answer. The ONLY acceptable way to" + " finish is by calling 'submit' with the required" + " structured fields.\n" + "\n" + "Finish the session always by calling `submit`." + " If you fail to find the answer, submit with" + ' exact_answer: "Can\'t find the answer.".' + ) + + @property + def context(self) -> dict[str, Any]: + return {} + + @property + def actions(self) -> list[ActionType]: + return self._registry.actions + + @property + def task_id(self) -> str: + """Task identifier.""" + return str(self._instance["query_id"]) + + def _to_observation(self, raw: Any, invoking: list[SingleAction] | None = None) -> Observation: + return SingleObservation(invoking_actions=invoking or [], result=raw) + + def start(self): + return EmptyObservation() + + def record_single_action(self, action: SingleAction) -> None: + self.logger.info(f"Received *{action.name}* action with arguments: {action.arguments}") + self.tool_call_count[action.name] += 1 + self.total_actions_executed += 1 + + def step(self, action: Action) -> Optional[Observation]: + if action is None: + self._done = True + + if self.total_actions_executed >= self.max_interactions: + self.logger.info(f"Reached maximal limit of {self.total_actions_executed} allowed actions") + self._done = True + + if self._done: + return None + + observation = self._registry.execute(action) + return observation + + # Action handlers ------------------------------------------------------------ + def _handle_search(self, action: SingleAction) -> Any: + self.record_single_action(action) + result = self.get_search_result(action) + # keep retrieved docs + self.record_retrieved_docids(result) + return result + + def _handle_get_document(self, action: SingleAction) -> SingleAction | None: + self.record_single_action(action) + args_dict = self.get_arguments_dict(action.arguments) + return self.search_tool_handler.execute_tool("get_document", args_dict) + + def _handle_finish(self, action: SingleAction) -> SingleAction | None: + self.record_single_action(action) + final_response = self.get_arguments_dict(action.arguments) + self._response = json.dumps(final_response) + self._done = True + return None + + def done(self) -> bool: + return self._done + + def score(self) -> SessionScore: + results, self.model_usage = self.evaluator.evaluate_response( + agent_response=self._response, + instance=self._instance, + retrieved_docids_set=self.retrieved_docids, + tool_call_counts=self.tool_call_count, + ) + return results + + def get_cost(self) -> CostReport: + if not self.model_usage: + return LiteLLMCostReport.initialize_empty(model_name=self.evaluator.eval_model_id) + + return LiteLLMCostReport.from_token_counts( + model_name=self.evaluator.eval_model_id, + input_tokens=self.model_usage["prompt_tokens"], + output_tokens=self.model_usage["completion_tokens"], + ) + + def close(self) -> None: + super().close() + # Persist minimal results for aggregation + sc = self.score() + self.save_results(sc.model_dump()) + + def set_action_types(self): + k = self.search_tool_handler.k + n_tokens = self.search_tool_handler.snippet_max_tokens + self._registry.add_action( + name="search", + description=( + "Perform a search on a knowledge source: supply" + " a single 'query' string; the action retrieves" + f" the {k} top most relevant results, each" + f" trimmed to {n_tokens} tokens." + ), + action_cls=BrowseCompPlusSearchAction, + handler=self._handle_search, + ) + self._registry.add_action( + name="submit", + description="Submit final answer and complete", + action_cls=BrowseCompPlusFinishAction, + handler=self._handle_finish, + is_finish=True, + ) + + if self.search_tool_handler.include_get_document: + self._registry.add_action( + name="get_document", + description="Retrieve the full document using its document id", + action_cls=BrowseCompPlusGetDocAction, + handler=self._handle_get_document, + ) + # todo: agents fail in practice without this option, even with explicit instructions. should we keep it? + self._registry.add_action( + name="message", + description="Send the final answer as a message to the user", + action_cls=MessageAction, + handler=self._handle_finish, + is_hidden=True, + is_message=True, + ) + + def get_evaluator(self, eval_model_id): + from .browsecomp_eval import BrowseCompEvaluatorOpenai, BrowsecompEvaluatorQwen + + if "gpt" in eval_model_id: + return BrowseCompEvaluatorOpenai(eval_model_id=eval_model_id) + if eval_model_id == "Qwen/Qwen3-32B": + return BrowsecompEvaluatorQwen() # Currently not supported + raise ValueError(f"Invalid eval_model_id: {eval_model_id}") + + # Robustly extract the answer from pydantic model or dict + def get_arguments_dict(self, args): + if isinstance(args, BaseModel): + return args.model_dump() # Pydantic v2 + if isinstance(args, Mapping): + return dict(args) + return str(getattr(args, "value", {})) + + def record_retrieved_docids(self, result: str): + try: + result = json.loads(result) + retrieved_docids = {result.get("docid") for result in result} + self.retrieved_docids = self.retrieved_docids | retrieved_docids + except json.decoder.JSONDecodeError: + self.logger.error(f"Failed to retrieve docids: {result}") + + def get_search_result(self, act): + from .searcher_cache import SearchDiskCacheSession + + args_dict = self.get_arguments_dict(act.arguments) + searcher_params = self.get_searcher_params() + search_cache = SearchDiskCacheSession(args_dict["query"], **searcher_params) + search_cache.logger = self.logger + result = search_cache.handle_start_fetch_results() + if result: + self.logger.info(f"Retrieved docs from cache: {result}") + else: + result = self.search_tool_handler.execute_tool(act.name, args_dict) + search_cache.cache_results(result) + self.logger.info(f"Retrieved docs from searcher: {result}") + return result + + def get_searcher_params(self): + return { + "n": self.search_tool_handler.snippet_max_tokens, + "k": self.search_tool_handler.k, + "search_type": self._searcher_params.get("searcher_type", "unknown"), + "search_model": self._searcher_params.get("model_name"), + "normalize": self._searcher_params.get("normalize"), + } + + +# ── Evaluator ──────────────────────────────────────────────────────── + + +class BrowseCompPlusEvaluator(Evaluator): + """Evaluator for BrowseCompPlus — task discovery, session kwargs, aggregation.""" + + def __init__( + self, + subset: str = "main", + searcher_type: str = "faiss", + searcher_model_name: str = "Qwen/Qwen3-Embedding-8B", + max_snippet_length: int = 512, + top_k_docs: int = 5, + include_get_document: bool = True, + normalize_search: bool = True, + full_doc_max_tokens: int = 2048, + max_interactions: int | None = 100, + inference_model: str = "N/A", + retriever_url: str | None = None, + eval_model_id: str = "openai/Azure/gpt-4.1", + ) -> None: + self._subset = subset + self._searcher_type = searcher_type + self._searcher_model_name = searcher_model_name + self._max_snippet_length = max_snippet_length + self._top_k_docs = top_k_docs + self._include_get_document = include_get_document + self._normalize_search = normalize_search + self._full_doc_max_tokens = full_doc_max_tokens + self._max_interactions = max_interactions + self._inference_model = inference_model + self._retriever_url = retriever_url + self._eval_model_id = eval_model_id + self._dataset: list[dict[str, Any]] | None = None + self._task_lookup: dict[str, dict[str, Any]] | None = None + + @property + def assets_dir(self): + return get_manager().env_path("benchmarks/browsecompplus") + + def extract_dataset(self): + import pandas as pd + + data_path = self.assets_dir / "data" / "browsecomp_plus_decrypted_docids.jsonl" + if not data_path.exists(): + raise Exception(f"{data_path} does not exist. Run 'exgentic install --benchmark browsecompplus' first.") + instances = pd.read_json(path_or_buf=data_path, lines=True).to_dict(orient="records") + + def proces_instance(instance): + processed_instance = { + "query_id": instance["query_id"], + "query": instance["query"], + "gold_answer": instance["answer"], + } + for k in ["gold_docs", "evidence_docs", "negative_docs"]: + processed_instance[k] = instance[k] + return processed_instance + + instances = [proces_instance(instance) for instance in instances] + return instances + + def _ensure_dataset(self) -> None: + if self._dataset is None: + self._dataset = self.extract_dataset() + self._task_lookup = {str(item["query_id"]): item for item in self._dataset} + + def list_tasks(self) -> list[str]: + self._ensure_dataset() + return [str(item["query_id"]) for item in self._dataset] + + def _get_searcher_params(self) -> dict[str, Any]: + """Get searcher parameters to pass to session.""" + index_dir = self.assets_dir / "indexes" + if self._searcher_type == "bm25": + index_dir = index_dir / "bm25" + searcher_args = {"index_path": str(index_dir)} + else: + model_dir_name = self._searcher_model_name.lower().split("/")[-1] + index_dir = index_dir / model_dir_name + if not os.path.exists(index_dir): + available_models = os.listdir(self.assets_dir / "indexes") + raise FileNotFoundError( + f"Index dir for BrowseCompPlus benchmark {index_dir} does not exist. " + f"Please select an available embedding model out of {available_models}" + ) + index_path = index_dir / "corpus.shard*_of_4.pkl" + searcher_args = { + "index_path": str(index_path), + "model_name": self._searcher_model_name, + "normalize": self._normalize_search, + } + + return { + "searcher_type": self._searcher_type, + "max_snippet_length": self._max_snippet_length, + "top_k_docs": self._top_k_docs, + "include_get_document": self._include_get_document, + "full_doc_max_tokens": self._full_doc_max_tokens, + **searcher_args, + } + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_dataset() + task_id = index.task_id + if self._task_lookup is None or task_id not in self._task_lookup: + raise KeyError(f"Unknown BrowseCompPlus task id '{task_id}'.") + instance = {"task_id": task_id, **self._task_lookup[task_id]} + kwargs: dict[str, Any] = { + "instance": instance, + "searcher_params": self._get_searcher_params(), + "max_interactions": self._max_interactions, + "session_id": index.session_id, + "eval_model_id": self._eval_model_id, + } + if self._retriever_url: + kwargs["retriever_url"] = self._retriever_url + return kwargs + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + # Aggregate per-session scores written by sessions + scores: list[float] = [] + retrieval_recalls: list[float] = [] + confidence_list: list[float] = [] + tool_call_counts_list: list[dict[str, float]] = [] + correctness: list[float] = [] + for paths in self.get_sessions_paths(sessions): + fp = paths.benchmark_results + if not fp.exists(): + raise FileNotFoundError(f"Missing results for planned session '{paths.session_id}' at {fp}") + + with open(fp, encoding="utf-8-sig") as f: + payload = json.load(f) + if not payload: + raise ValueError(f"Empty benchmark results for session '{paths.session_id}' at {fp}") + + s = float(payload["score"]) # minimal: assume exists + scores.append(s) + metrics = payload.get("session_metrics", {}) + if not metrics: + print(f"No metrics for session '{paths.session_id}' at {fp}") + + retrieval_recall = metrics.get("Retrieval_recall", 0) + retrieval_recalls.append(float(retrieval_recall) if retrieval_recall is not None else 0) + correctness.append(payload.get("success", 0)) + + confidence = metrics.get("Confidence") + try: + confidence = float(confidence) + except Exception: + confidence = 0 + confidence_list.append(confidence) + + metadata = payload.get("session_metadata", {}) + if not metadata: + print(f"No metadata for session '{paths.session_id}' at {fp}") + tool_call_counts_list.append(metadata.get("tool_call_counts", {})) + + avg = sum(scores) / len(scores) if scores else 0.0 + avg_retrieval_recalls = sum(retrieval_recalls) / len(retrieval_recalls) if retrieval_recalls else 0.0 + tools_keys = set().union(*tool_call_counts_list) + avg_tool_use_counts = { + k: sum(d.get(k, 0) for d in tool_call_counts_list) / len(tool_call_counts_list) for k in tools_keys + } + + calibration_error = None + # calibration error only comupted for a large number of examples + if len(correctness) >= 100: + try: + from scripts_evaluation.evaluate_with_openai import calculate_calibration_error + + calibration_error = calculate_calibration_error(confidences=confidence_list, correctness=correctness) + except Exception: + print(f"Failed to calculate calibration error for session '{paths.session_id}' at {fp}") + calibration_error = 0 + metrics = { + "LLM": self._inference_model, + "Accuracy (%)": avg, + "Recall (%)": avg_retrieval_recalls, + "avg_tool_stats": avg_tool_use_counts, + "Calibration Error (%)": calibration_error, + "Retriever": self._searcher_model_name, + "Link": "change me when submitting", + "Evaluation Date": datetime.datetime.now().date().isoformat(), + } + return BenchmarkResults( + benchmark_name="BrowseCompPlus", + total_tasks=len(sessions), + score=avg, + metrics=metrics, + ) + + +# ── Benchmark config ───────────────────────────────────────────────── + + +class BrowseCompPlusBenchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "BrowseCompPlus" + slug_name: ClassVar[str] = "browsecompplus" + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return BrowseCompPlusEvaluator + + @classmethod + def _get_session_class(cls): + return BrowseCompPlusSession + + subset: Literal["main"] = "main" + + runner: RunnerName | None = None # Threadsafe; uses global default runner (venv) + + # Retriever runner — when set, the search index runs as a shared service + # instead of being loaded in each session process. Useful when sessions + # run in Docker to avoid duplicating the heavy index in RAM. + retriever_runner: RunnerName | None = None + + # Agent inference params (for logging) + inference_model: str = "N/A" + + # searcher params + searcher_type: str = "faiss" # "bm25" or "faiss" + searcher_model_name: str = "Qwen/Qwen3-Embedding-8B" # Used for faiss only + max_snippet_length: int = 512 + top_k_docs: int = 5 + include_get_document: bool = True + normalize_search: bool = True + full_doc_max_tokens: int = 2048 + max_interactions: int | None = 100 + eval_model_id: str = "openai/Azure/gpt-4.1" + + @property + def _assets_dir(self) -> str: + return str(get_manager().env_path("benchmarks/browsecompplus")) + + def _retriever_runner_kwargs(self) -> dict[str, Any]: + """Runner kwargs for the retriever container (volumes). + + Only returns Docker-specific kwargs when the retriever actually runs + in Docker; for 'service' or 'direct' these would leak into the + target class constructor and cause errors. + """ + if self.retriever_runner != "docker": + return {} + kw: dict[str, Any] = { + "env_name": f"benchmarks/{self.slug_name}", + "module_path": type(self).__module__, + } + kw["volumes"] = {self._assets_dir: self._assets_dir} + return kw + + def _get_retriever_searcher_args(self) -> dict[str, Any]: + """Searcher constructor args for the Retriever.""" + index_dir = Path(self._assets_dir) / "indexes" + if self.searcher_type == "bm25": + return {"index_path": str(index_dir / "bm25")} + model_dir = self.searcher_model_name.lower().split("/")[-1] + return { + "index_path": str(index_dir / model_dir / "corpus.shard*_of_4.pkl"), + "model_name": self.searcher_model_name, + "normalize": self.normalize_search, + } + + def _ensure_retriever(self) -> str: + """Start a shared retriever service and return its URL.""" + assert self.retriever_runner is not None + proxy = get_shared_retriever( + runner=self.retriever_runner, + runner_kwargs=self._retriever_runner_kwargs(), + searcher_type=self.searcher_type, + **self._get_retriever_searcher_args(), + ) + url = get_retriever_url(proxy) + # Rewrite URL for Docker sessions so they can reach the host. + if self.resolve_runner() == "docker": + url = url.replace("127.0.0.1", "host.docker.internal") + return url + + def runner_kwargs(self) -> dict[str, Any]: + kw = super().runner_kwargs() + kw["health_timeout"] = 120.0 + if self.resolve_runner() == "docker": + # Mount host assets (indexes, data) into the container so they + # are not re-downloaded for every image build. + volumes = kw.get("volumes", {}) + volumes[self._assets_dir] = self._assets_dir + kw["volumes"] = volumes + return kw + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + kwargs: dict[str, Any] = { + "subset": self.subset, + "searcher_type": self.searcher_type, + "searcher_model_name": self.searcher_model_name, + "max_snippet_length": self.max_snippet_length, + "top_k_docs": self.top_k_docs, + "include_get_document": self.include_get_document, + "normalize_search": self.normalize_search, + "full_doc_max_tokens": self.full_doc_max_tokens, + "max_interactions": self.max_interactions, + "inference_model": self.inference_model, + "eval_model_id": self.eval_model_id, + } + # Auto-use a shared retriever service for Docker so that session + # containers don't each load the heavy search index (OOM). + if not self.retriever_runner and self.resolve_runner() == "docker": + self.retriever_runner = "service" + if self.retriever_runner: + kwargs["retriever_url"] = self._ensure_retriever() + return kwargs diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_eval.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_eval.py new file mode 100644 index 00000000..bd67e3c5 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/browsecomp_eval.py @@ -0,0 +1,156 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +from typing import Any + +import litellm +from pydantic import BaseModel +from scripts_evaluation.evaluate_with_openai import ( + GRADER_TEMPLATE as GRADER_TEMPLATE_OPENAI, +) +from scripts_evaluation.evaluate_with_openai import ( + compute_citation_metrics, + extract_citations_from_response, + parse_judge_response, +) +from search_agent.prompts import GRADER_TEMPLATE_QWEN + +from ...core.context import try_get_context +from ...core.types import SessionScore +from ...utils.settings import get_settings + +_settings = get_settings() + + +class BrowseCompEvaluator(BaseModel): + eval_model_id: str + sampling_params: dict[str, Any] = {} + grader_template: str + + def evaluate_response( + self, + agent_response, + instance, + retrieved_docids_set=None, + tool_call_counts=None, + ) -> SessionScore: + question = instance["query"] + correct_answer = instance["gold_answer"] + positives_for_query = instance["evidence_docs"] + cited_docids = [] + confidence = None + retrieval_recall = None + extracted_final_answer = None + judge_textual_response = None + + # compute retrieval recall + if retrieved_docids_set is not None: + retrieval_recall = len(retrieved_docids_set.intersection(set(positives_for_query))) / float( + len(positives_for_query) + ) + + if not agent_response: # run was halted without a final answer + is_successful = False + score = 0 + is_complete = False + parse_error = False + judge_usage = None + + else: # call judge + is_complete = True + prompt = self.create_judge_prompt(question, agent_response, correct_answer) + judge_response = litellm.completion( + model=self.eval_model_id, + messages=[{"role": "user", "content": prompt}], + max_tokens=self.max_output_tokens, + litellm_metadata={"context": try_get_context()}, + **self.sampling_params, + ) + choice = judge_response["choices"][0] + judge_textual_response = choice.get("message").content + judge_usage = judge_response.usage.copy() + answer_metrics = parse_judge_response(judge_textual_response) + parse_error = bool(answer_metrics["parse_error"]) + + if parse_error: + is_successful = False + score = 0 + else: + is_successful = answer_metrics["correct"] + score = int(is_successful) if is_successful is not None else 0 + confidence = (answer_metrics.get("confidence", 100),) + extracted_final_answer = answer_metrics.get("extracted_final_answer") + cited_docids = extract_citations_from_response(agent_response) + + citation_metrics_positives = compute_citation_metrics(cited_docids, positives_for_query) + scores = { + "Accuracy": score, + "Retrieval_recall": retrieval_recall, + "Citation_metrics_positives": citation_metrics_positives, + "Confidence": confidence, + } + meta_data = { + "instance": instance.copy(), + "retrieved_docids": list(retrieved_docids_set) if retrieved_docids_set else [], + "response": agent_response, + "extracted_final_answer": extracted_final_answer, + "judge_model": self.eval_model_id, + "is_complete": is_complete, + "judge_parse_error": parse_error, + "tool_call_counts": tool_call_counts, + } + if parse_error: + meta_data["judge_raw_response"] = judge_textual_response + all_scores = SessionScore( + score=int(is_successful), + success=is_successful, + is_finished=is_complete, + session_metrics=scores, + session_metadata=meta_data, + ) + + return all_scores, judge_usage + + def create_judge_prompt(self, question: str, response: str, correct_answer: str) -> str: + return self.grader_template.format(question=question, response=response, correct_answer=correct_answer) + + +class BrowseCompEvaluatorOpenai(BrowseCompEvaluator): + max_output_tokens: int = 1024 + grader_template: str = GRADER_TEMPLATE_OPENAI + eval_model_id: str = "openai/Azure/gpt-4.1" + + +class BrowsecompEvaluatorQwen(BrowseCompEvaluator): + max_output_tokens: int = 4096 + temperature: int = 0.7 + top_p: float = 0.8 + top_k: int = 20 + eval_model_id: str = "Qwen/Qwen3-32B" + + def model_post_init(self, __context) -> None: + self.grader_template = GRADER_TEMPLATE_QWEN + self.sampling_params = { + "top_p": self.top_p, + "temperature": self.temperature, + "top_k": self.top_k, + } + + +if __name__ == "__main__": + e = BrowseCompEvaluatorOpenai() # eval_model_id="watsonx/openai/gpt-oss-120b") + instance = { + "gold_answer": "2015", + "query": "When did someone was born", + "evidence_docs": ["1", "4"], + } + response = { + "explanation": "as stated in docs [1] [5]", + "exact_answer": "1925", + "confidence": 0.8, + } + response = json.dumps(response) + + r = e.evaluate_response(response, instance) + print(r[0]) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/make_light_dataset.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/make_light_dataset.py new file mode 100644 index 00000000..db01af1c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/make_light_dataset.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import argparse +import json + + +def main(): + p = argparse.ArgumentParser(description="Create a light JSONL with only docids.") + p.add_argument("--input", required=True, help="Path to full JSONL") + p.add_argument("--output", required=True, help="Path to write the light JSONL") + args = p.parse_args() + + with open(args.input, encoding="utf-8") as fin, open(args.output, "w", encoding="utf-8") as fout: + for line in fin: + if line.strip(): + obj = json.loads(line) + for k in ["gold_docs", "evidence_docs", "negative_docs"]: + obj[k] = [d.get("docid") for d in obj.get(k) if d.get("docid") is not None] + + fout.write(json.dumps(obj, ensure_ascii=False) + "\n") + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/requirements.txt new file mode 100644 index 00000000..3fc1116e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/requirements.txt @@ -0,0 +1,2 @@ +# browsecomp-plus is installed by setup.sh rather than here because it pins +# conflicting versions of fastmcp, mcp, and pydantic. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/retriever.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/retriever.py new file mode 100644 index 00000000..69d4ff8e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/retriever.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Shared retriever service for BrowseCompPlus search index. + +The Retriever loads the heavy search index once and serves queries. +It can run via any runner (direct, service, docker, etc.), allowing +a single index copy to be shared across all sessions. +""" + +import argparse +import json +import threading +from typing import Any + + +class Retriever: + """Loads a search index and serves queries. + + Designed to run via ``with_runner()`` in any runner. + """ + + def __init__(self, searcher_type: str, **searcher_args: Any) -> None: + # Import torch/safetensors before the searcher module to avoid a + # native-library initialisation conflict with FAISS that causes + # segfaults on Apple Silicon (faiss_searcher.py imports faiss + # before torch at module level). + import safetensors # noqa: F401 + import torch # noqa: F401 + from searcher.searchers import SearcherType + + searcher_class = SearcherType.get_searcher_class(searcher_type) + parser = argparse.ArgumentParser() + searcher_class.parse_args(parser) + + cli: list[str] = [] + for key, value in searcher_args.items(): + flag = f"--{key.replace('_', '-')}" + if isinstance(value, bool): + if value: + cli.append(flag) + else: + cli.extend([flag, str(value)]) + + args = parser.parse_args(cli) + self._searcher = searcher_class(args) + + def search(self, query: str, k: int) -> list: + return self._searcher.search(query, k) + + def get_document(self, docid: str) -> dict | None: + return self._searcher.get_document(docid) + + +class RetrieverClient: + """Lazy HTTP client to a remote Retriever service. + + Picklable — stores only the URL. Connects on first use. + This allows it to survive serialization into Docker containers. + """ + + def __init__(self, url: str) -> None: + self._url = url + self._proxy: Any = None + + def _connect(self) -> None: + if self._proxy is None: + from ...adapters.runners.service import HTTPTransport + from ...adapters.runners.transport import ObjectProxy + + self._proxy = ObjectProxy(HTTPTransport(self._url)) + + def search(self, query: str, k: int) -> list: + self._connect() + return self._proxy.search(query, k) + + def get_document(self, docid: str) -> dict | None: + self._connect() + return self._proxy.get_document(docid) + + def close(self) -> None: + if self._proxy is not None: + try: + self._proxy.close() + except Exception: + pass + self._proxy = None + + def __getstate__(self) -> dict: + return {"url": self._url} + + def __setstate__(self, state: dict) -> None: + self._url = state["url"] + self._proxy = None + + +# ── Shared retriever cache ──────────────────────────────────────────── + +_cache_lock = threading.Lock() +_cache: dict[str, Any] = {} + + +def get_shared_retriever( + runner: str, + runner_kwargs: dict[str, Any] | None = None, + **retriever_kwargs: Any, +) -> Any: + """Get or create a shared Retriever running in the specified runner.""" + from ...adapters.runners import with_runner + + key = json.dumps(retriever_kwargs, sort_keys=True, default=str) + if key not in _cache: + with _cache_lock: + if key not in _cache: + _cache[key] = with_runner( + Retriever, + runner=runner, + **(runner_kwargs or {}), + **retriever_kwargs, + ) + return _cache[key] + + +def get_retriever_url(proxy: Any) -> str: + """Extract the HTTP URL from a retriever proxy.""" + from ...adapters.runners.service import HTTPTransport + + transport = object.__getattribute__(proxy, "_transport") + if isinstance(transport, HTTPTransport): + return transport._base_url + raise ValueError( + "Cannot extract URL from non-HTTP retriever. Use runner='service' or runner='docker' for the retriever." + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_service.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_service.py new file mode 100644 index 00000000..d5bfbbd1 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_service.py @@ -0,0 +1,173 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Thread-safe search service with semaphore for concurrency control.""" + +import argparse +import atexit +import json +import logging +import os +import threading +import time + +import psutil + + +class SearchService: + """Thread-safe singleton service for managing searcher instances.""" + + _instance = None + _instance_lock = threading.Lock() + + def __new__(cls): + if cls._instance is None: + with cls._instance_lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._searchers = {} + cls._instance._cache_lock = threading.RLock() + cls._instance._search_semaphore = threading.Semaphore(5) + cls._instance._shutdown = False + # Register cleanup to run when program exits + atexit.register(cls._instance.shutdown) + return cls._instance + + def shutdown(self): + """Clean up all searcher instances and their resources.""" + with self._cache_lock: + if self._shutdown: + return + self._shutdown = True + + logger = logging.getLogger(__name__) + logger.info("Shutting down SearchService and cleaning up models...") + + for cache_key, searcher in self._searchers.items(): + try: + # Try to close/cleanup the searcher if it has such methods + if hasattr(searcher, "close"): + searcher.close() + elif hasattr(searcher, "shutdown"): + searcher.shutdown() + + if hasattr(searcher, "searcher") and hasattr(searcher.searcher, "close"): + searcher.searcher.close() + + except Exception as e: + logger.warning(f"Error cleaning up searcher {cache_key}: {e}") + + self._searchers.clear() + logger.info("SearchService shutdown complete") + + def get_or_create_searcher( + self, + searcher_type: str, + searcher_class: type, + logger: logging.Logger | None = None, + **searcher_args, + ): + """Get or create a searcher instance with thread-safe access.""" + if logger is None: + logger = logging.getLogger(__name__) + + # Create cache key + args_str = json.dumps(searcher_args, sort_keys=True, default=str) + cache_key = f"{searcher_type}:{args_str}" + + # Check if already cached + if cache_key in self._searchers: + logger.info( + f"Reusing cached searcher: {searcher_type} (PID: {os.getpid()}, Thread: {threading.get_ident()})" + ) + return ThreadSafeSearcherWrapper(self._searchers[cache_key], self._search_semaphore) + + # Create new searcher (with lock) + with self._cache_lock: + if cache_key in self._searchers: + return ThreadSafeSearcherWrapper(self._searchers[cache_key], self._search_semaphore) + + process = psutil.Process(os.getpid()) + mem_before = process.memory_info().rss / 1024 / 1024 + thread_id = threading.get_ident() + logger.info( + f"Loading searcher model (before: {mem_before:.1f} MB, PID: {os.getpid()}, Thread: {thread_id})" + ) + + # Instantiate searcher + searcher = self._instantiate_with_overrides(searcher_class, **searcher_args) + self._searchers[cache_key] = searcher + + mem_after = process.memory_info().rss / 1024 / 1024 + logger.info( + f"Searcher model loaded: {searcher_type} " + f"(after: {mem_after:.1f} MB, delta: {mem_after - mem_before:.1f} MB)" + ) + + return ThreadSafeSearcherWrapper(searcher, self._search_semaphore) + + @staticmethod + def _instantiate_with_overrides(cls, **overrides): + """Instantiate a class that uses argparse for configuration.""" + parser = argparse.ArgumentParser() + cls.parse_args(parser) + + cli = [] + for key, value in overrides.items(): + flag = f"--{key.replace('_', '-')}" + if isinstance(value, bool): + if value: + cli.append(flag) + else: + cli.extend([flag, str(value)]) + + args = parser.parse_args(cli) + return cls(args) + + +class ThreadSafeSearcherWrapper: + """Wrapper that limits concurrent access to the searcher.""" + + def __init__(self, searcher, semaphore): + self._searcher = searcher + self._semaphore = semaphore + self._search_count = 0 + + def search(self, query: str, k: int): + """Thread-safe search with semaphore and lock for async safety.""" + # Use both semaphore and lock to ensure serialization even in async contexts + acquired = self._semaphore.acquire(blocking=True, timeout=300) + if not acquired: + raise TimeoutError("Failed to acquire semaphore for search") + try: + self._search_count += 1 + count = self._search_count + + logger = logging.getLogger(__name__) + logger.debug(f"Search #{count} starting (thread={threading.get_ident()})") + + result = self._searcher.search(query, k) + time.sleep(0.01) + logger.debug(f"Search #{count} completed") + return result + finally: + self._semaphore.release() + + def get_document(self, docid: str): + """Thread-safe document retrieval with semaphore and lock.""" + acquired = self._semaphore.acquire(blocking=True, timeout=300) + if not acquired: + raise TimeoutError("Failed to acquire semaphore for get_document") + try: + return self._searcher.get_document(docid) + finally: + self._semaphore.release() + + def __getattr__(self, name): + """Forward other attributes to the wrapped searcher.""" + return getattr(self._searcher, name) + + +def get_search_service() -> SearchService: + """Get the singleton SearchService instance.""" + return SearchService() diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_tool_handler.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_tool_handler.py new file mode 100644 index 00000000..d4ef5f27 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/search_tool_handler.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json + +from transformers import AutoTokenizer + + +class BCPSearchToolHandler: + def __init__( + self, + searcher, + snippet_max_tokens: int | None = None, + k: int = 5, + include_get_document: bool = True, + full_doc_max_tokens: int | None = None, + ): + self.searcher = searcher + self.snippet_max_tokens = snippet_max_tokens + self.k = k + self.include_get_document = include_get_document + + self.tokenizer = None + self.full_doc_max_tokens = None + if snippet_max_tokens and snippet_max_tokens > 0: + self.tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") + self.full_doc_max_tokens = full_doc_max_tokens + + def execute_tool(self, tool_name: str, arguments: dict): + if tool_name == "search": + return self._search(arguments["query"]) + if tool_name == "get_document": + return self._get_document(arguments["docid"]) + raise ValueError(f"Unknown tool: {tool_name}") + + def _search(self, query: str): + candidates = self.searcher.search(query, self.k) + + if self.snippet_max_tokens and self.snippet_max_tokens > 0 and self.tokenizer: + for cand in candidates: + text = cand["text"] + cand["snippet"] = self._truncate_text(text, self.snippet_max_tokens) + else: + for cand in candidates: + cand["snippet"] = cand["text"] + + results = [] + for cand in candidates: + if cand.get("score") is None: + results.append({"docid": cand["docid"], "snippet": cand["snippet"]}) + else: + results.append( + { + "docid": cand["docid"], + "score": cand["score"], + "snippet": cand["snippet"], + } + ) + + return json.dumps(results, indent=2) + + def _get_document(self, docid: str): + try: + result = self.searcher.get_document(docid) + except Exception: + result = None + if result is None: + return json.dumps({"error": f"Document {docid} not found"}) + + text = result.get("text") + result["text"] = self._truncate_text(text, max_len=self.full_doc_max_tokens) + return json.dumps(result, indent=2) + + def _truncate_text(self, text: str, max_len: int) -> str: + if not self.tokenizer: + raise RuntimeError("Tokenizer not initialized") + tokens = self.tokenizer.encode(text, add_special_tokens=False) + if max_len and len(tokens) > max_len: + truncated_tokens = tokens[:max_len] + return self.tokenizer.decode(truncated_tokens, skip_special_tokens=True) + return text diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/searcher_cache.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/searcher_cache.py new file mode 100644 index 00000000..86419cd3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/searcher_cache.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import hashlib +from typing import Any + +from ...utils.disk_cache import DiskCacheSessionMixin + + +class SearchDiskCacheSession(DiskCacheSessionMixin): + CACHE_DIR = "./exgentic_session_cache/browsecomp/searcher" # single DB for all runs + + def __init__( + self, + query: str, + n: int, + k: int, + search_type: str, + search_model: str, + normalize: bool = False, + use_cache: bool = True, + ): + self.query = query + self.n = n + self.k = k + self.search_type = search_type + self.search_model = search_model + self.normalize = normalize + self._init_cache_mixin(use_cache=use_cache) + + def build_cache_key_payload(self) -> dict: + return { + "search_type": self.search_type, + "search_model": self.search_model, + "normalize": self.normalize, + "n": self.n, + "k": self.k, + "q": hashlib.sha256(self.query.encode("utf-8")).hexdigest(), + } + + def build_additional_cache_metadata(self) -> dict: + return { + "search_type": self.search_type, + "search_model": self.search_model, + "normalize": self.normalize, + } + + def on_cache_hit(self, payload: dict[str, Any]) -> bool: + results = payload.get("results") + if not isinstance(results, dict): + return False + result = results.get("raw") + if result is None or not isinstance(result, str): + return False + return True + + def cache_results(self, result: str): + self.set_results_payload({"raw": result}) + self.cache_score({}) + + def handle_start_fetch_results(self): + if self.handle_cache_start(): + cache_results = self.get_results_payload() + if cache_results: + try: + return cache_results["raw"] + except Exception: + return None + return None diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/setup.sh b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/setup.sh new file mode 100644 index 00000000..3e902651 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/browsecompplus/setup.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################################################### +# 1. Detect java in PATH +############################################################################### +BM25_AVAILABLE=false +if command -v java >/dev/null 2>&1; then + JAVA_VERSION_RAW="$(java -version 2>&1 | head -n1)" || true + JAVA_MAJOR="$(echo "$JAVA_VERSION_RAW" | sed -E 's/.*"([0-9]+).*/\1/')" + if [[ "$JAVA_MAJOR" =~ ^[0-9]+$ ]] && [ "$JAVA_MAJOR" -ge 21 ]; then + BM25_AVAILABLE=true + else + echo "[WARNING] Java 21+ not detected. BM25 searcher will not be available." + fi +else + echo "[WARNING] Java not found. BM25 searcher will not be available." +fi + +BENCH_ROOT="." + +############################################################################### +# 2. Detect GPU availability +############################################################################### +GPU_AVAILABLE=false +if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then + GPU_AVAILABLE=true +fi + +############################################################################### +# 3. Install BrowseCompPlus packages +############################################################################### +GIT_SSH_URL="https://github.com/lilacheden/BrowseComp-Plus/" +GIT_REF="mac-support-and-packaging" + +if [ "$GPU_AVAILABLE" = true ]; then + uv pip install "git+${GIT_SSH_URL}@${GIT_REF}#egg=browsecomp-plus[gpu]" +else + uv pip install "git+${GIT_SSH_URL}@${GIT_REF}" +fi + +uv pip install --upgrade "mcp>=1.24" "transformers>=4.53.2,<5.0" \ + "pillow>=12.1.1" "fastmcp>=2.14.0" "fastapi-sso>=0.19.0" "openai>=2.9.0" +uv pip uninstall gradio 2>/dev/null || true + +if [ "$GPU_AVAILABLE" = true ]; then + uv pip install --no-build-isolation flash-attn +fi + +# In Docker builds, skip data/index downloads — they'll be mounted as volumes. +if [ "${EXGENTIC_DOCKER_BUILD:-}" = "1" ]; then + echo "Docker build: skipping data and index downloads (will be mounted at runtime)" + exit 0 +fi + +############################################################################### +# 4. Download + decrypt dataset +############################################################################### +DATA_DIR="${BENCH_ROOT}/data" +QUERIES_DIR="${BENCH_ROOT}/topics-qrels" +mkdir -p "${DATA_DIR}" "${QUERIES_DIR}" + +if [ -f "${DATA_DIR}/browsecomp_plus_decrypted.jsonl" ] && [ -f "${QUERIES_DIR}/queries.tsv" ]; then + echo "Dataset already exists, skipping download." +else + python -m scripts_build_index.decrypt_dataset \ + --output "${DATA_DIR}/browsecomp_plus_decrypted.jsonl" \ + --generate-tsv "${QUERIES_DIR}/queries.tsv" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIGHT_JSONL="${DATA_DIR}/browsecomp_plus_decrypted_docids.jsonl" +if [ ! -f "${LIGHT_JSONL}" ]; then + PYTHONPATH="${SCRIPT_DIR}:${PYTHONPATH:-}" python -m make_light_dataset \ + --input "${DATA_DIR}/browsecomp_plus_decrypted.jsonl" \ + --output "${LIGHT_JSONL}" +fi + +############################################################################### +# 5. Download indexes +############################################################################### +uv pip install -U hf_transfer 2>/dev/null || true + +mkdir -p "${BENCH_ROOT}/indexes" +cd "${BENCH_ROOT}/indexes" + +if [ "$BM25_AVAILABLE" = true ]; then + HF_HUB_ENABLE_HF_TRANSFER=1 hf download Tevatron/browsecomp-plus-indexes --repo-type=dataset --include="bm25/*" --local-dir . +fi + +HF_HUB_ENABLE_HF_TRANSFER=1 hf download Tevatron/browsecomp-plus-indexes --repo-type=dataset --include="qwen3-embedding-8b/*" --local-dir . + +echo "BrowseCompPlus setup complete" diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/gsm8k_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/gsm8k_benchmark.py new file mode 100644 index 00000000..868e4798 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/gsm8k_benchmark.py @@ -0,0 +1,351 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import ast +import json +import logging +import operator as op +import re +from typing import Any, ClassVar, Literal + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, +) + +from ...core.actions import ActionsHandler, extract_argument +from ...core.benchmark import Benchmark +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + FinishAction, + Observation, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from ...observers.logging import get_logger +from ...utils.paths import get_run_paths +from ...utils.settings import ExgenticSettings, RunnerName, get_settings + +GSM8K_TOTAL_TASKS = 1319 + +_run_logger: logging.Logger | None = None + + +def _get_run_logger() -> logging.Logger: + """Benchmark-level logger that writes into the run's run log.""" + global _run_logger + if _run_logger is None: + log_path = get_run_paths().tracker + _run_logger = get_logger(__name__, str(log_path)) + return _run_logger + + +def _parse_int(s: str | None) -> int | None: + if s is None: + return None + s = str(s).strip() + # allow "42\n" etc. + if re.fullmatch(r"[+-]?\d+", s): + return int(s) + return None + + +_ALLOWED_BINOPS = { + ast.Add: op.add, + ast.Sub: op.sub, + ast.Mult: op.mul, + ast.Div: op.truediv, +} +_ALLOWED_UNARYOPS = {ast.UAdd: op.pos, ast.USub: op.neg} +_ALLOWED_DESC = "numbers (ints/decimals), + - * /, parentheses, unary +/-. No names, functions, **, %, comparisons." + + +def safe_evaluate(expression: str): + expr = (expression or "").strip() + if not expr: + return f"Invalid expression: empty. Allowed: {_ALLOWED_DESC} Got: {expression!r}" + try: + tree = ast.parse(expr, mode="eval") + except SyntaxError: + return f"Invalid syntax. Allowed: {_ALLOWED_DESC} Got: {expression!r}" + + def ev(n): + if isinstance(n, ast.Expression): + return ev(n.body) + if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): + return n.value + if isinstance(n, ast.UnaryOp) and type(n.op) in _ALLOWED_UNARYOPS: + return _ALLOWED_UNARYOPS[type(n.op)](ev(n.operand)) + if isinstance(n, ast.BinOp) and type(n.op) in _ALLOWED_BINOPS: + if isinstance(n.op, ast.Div) and ev(n.right) == 0: + return f"Invalid op: division by zero. Got: {expression!r}" + return _ALLOWED_BINOPS[type(n.op)](ev(n.left), ev(n.right)) + return f"Invalid element: {type(n).__name__}. Allowed: {_ALLOWED_DESC} Got: {expression!r}" + + try: + out = ev(tree) + return ( + out + if isinstance(out, (int, float, str)) + else f"Did not evaluate to a number. Allowed: {_ALLOWED_DESC} Got: {expression!r}" + ) + except Exception as e: + return f"Error evaluating. Allowed: {_ALLOWED_DESC} Got: {expression!r}. Error: {type(e).__name__}: {e}" + + +class GSM8kCalculateExpressionArgs(BaseModel): + expression: str = Field(..., description="Arithmetic expression using + - * / and parentheses.") + + +class GSM8kCalculateExpressionAction(SingleAction): + name: Literal["calculate_expression"] = "calculate_expression" + arguments: GSM8kCalculateExpressionArgs + + +class GSM8kFinishArgs(BaseModel): + answer: str | int = Field(..., description="Final answer as a single integer (string or int).") + + @field_validator("answer", mode="before") + @classmethod + def coerce_int_to_str(cls, v: Any) -> str: + # Allow agents to pass raw integers; store as string for downstream checks. + if isinstance(v, int): + return str(v) + return v + + @field_validator("answer") + @classmethod + def must_look_like_int(cls, v: str) -> str: + v = v.strip() + if not re.fullmatch(r"[+-]?\d+", v): + raise ValueError("Answer must be a single integer.") + return v + + +class GSM8kFinishAction(FinishAction): + name: Literal["submit"] = "submit" + arguments: GSM8kFinishArgs + + +class GSM8kSession(Session): + """Session for GSM8k benchmark evaluation.""" + + _question: str + _done: bool + + def __init__( + self, + settings: ExgenticSettings, + include_calculator_tool: bool, + instance: dict[str, Any], + session_id: str | None = None, + ) -> None: + if session_id is not None: + self._session_id = session_id + self._question = instance["question"] + self._answer = instance["answer"] + self._task_id = instance["task_id"] + self._done = False + self._gold_answer = self._answer.split("####")[-1].strip() + self._final_answer = None + self._registry = ActionsHandler(logger=self.logger) + # Define Actions directly (single source of truth) + + if include_calculator_tool: + self._registry.add_action( + name="calculate_expression", + description=( + "Evaluate a mathematical expression using only" + " numbers and basic operators" + " (+, -, *, /, parentheses)." + ), + action_cls=GSM8kCalculateExpressionAction, + handler=self._handle_calculate_expression, + ) + + self._registry.add_action( + name="submit", + description="Submit final answer and complete the task.", + action_cls=GSM8kFinishAction, + handler=self._handle_finish, + is_finish=True, + ) + super().__init__() + + @property + def task(self) -> str: + return ( + "Solve the following math word problem using basic arithmetic.\n" + "You may perform intermediate calculations if helpful.\n" + "When you are finished, submit the final answer as a single integer by calling `submit`.\n" + "\n" + "Do not include units, words, or explanations in the final answer.\n" + "Your response will be graded only on whether the final integer exactly matches the correct answer.\n" + "\n" + f"Question:\n\n{self._question}" + ) + + @property + def context(self) -> dict[str, Any]: + return {} + + @property + def actions(self) -> list[ActionType]: + return self._registry.actions + + @property + def task_id(self) -> str: + return str(self._task_id) + + def _to_observation(self, raw: Any, invoking_actions: list[SingleAction] | None = None) -> Observation: + return SingleObservation(invoking_actions=invoking_actions or [], result=raw) + + def start(self) -> Observation | None: + # Empty initial observation; question is carried in the task string. + return EmptyObservation() + + def step(self, action: Action) -> Observation | None: + if action is None: + self._done = True + + if self._done: + return None + + observation = self._registry.execute(action) + + return observation + + def done(self) -> bool: + return self._done + + def score(self) -> SessionScore: + gold = _parse_int(self._gold_answer) + pred = _parse_int(self._final_answer) + score = 1.0 if (gold is not None and pred is not None and gold == pred) else 0.0 + self.logger.info(f"Gold: {self._gold_answer} Prediction: {self._final_answer} Score: {score}") + # Finished only when the benchmark finish action stores a final answer. + finished = self._final_answer is not None + success = score == 1.0 + return SessionScore(score=float(score), success=success, is_finished=finished) + + def close(self): + super().close() + # Persist minimal results for aggregation + sc = self.score() + self.save_standard_results(sc) + + # Action handlers ------------------------------------------------------------ + def _handle_calculate_expression(self, action: SingleAction) -> Any: + self.logger.info(f"Received expression: {action}") + expression = extract_argument(action.arguments, "expression", "") + result = safe_evaluate(expression) + self.logger.info(f"Calculated result: {result}") + return result + + def _handle_finish(self, action: SingleAction) -> None: + self.logger.info(f"Received final answer: {action}") + answer = extract_argument(action.arguments, "answer", None) + self._final_answer = answer + self._done = True + return + + +# ── Evaluator ──────────────────────────────────────────────────────── + + +class GSM8kEvaluator(Evaluator): + """Evaluator for GSM8k — task discovery, session kwargs, aggregation.""" + + def __init__(self, subset: str = "main", include_calculator_tool: bool = True) -> None: + self._subset = subset + self._include_calculator_tool = include_calculator_tool + self._dataset = None + + def _ensure_dataset(self) -> None: + if self._dataset is None: + from datasets import load_dataset + + self._dataset = load_dataset("gsm8k", "main")["test"] + + def list_tasks(self) -> list[str]: + return [str(i) for i in range(GSM8K_TOTAL_TASKS)] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_dataset() + idx = int(index.task_id) + if idx < 0 or idx >= len(self._dataset): + raise IndexError(f"Task id {index.task_id} out of range for GSM8k.") + instance = {"task_id": idx, **self._dataset[idx]} + return { + "settings": get_settings(), + "include_calculator_tool": self._include_calculator_tool, + "instance": instance, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + run_logger = _get_run_logger() + scores: list[float] = [] + for paths in self.get_sessions_paths(sessions): + fp = paths.benchmark_results + try: + with open(fp, encoding="utf-8-sig") as f: + payload = json.load(f) + s = float(payload["score"]) + scores.append(s) + except FileNotFoundError as err: + raise FileNotFoundError( + f"Missing benchmark result for session" f" '{paths.session_id}' at {fp}" + ) from err + except Exception: + run_logger.exception( + "Failed to load benchmark result for session %s at %s", + paths.session_id, + fp, + ) + raise + avg = sum(scores) / len(scores) if scores else 0.0 + return BenchmarkResults( + benchmark_name="gsm8k", + total_tasks=len(sessions), + score=avg, + metrics={}, + ) + + +# ── Benchmark config ───────────────────────────────────────────────── + + +class GSM8kBenchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "GSM8k" + slug_name: ClassVar[str] = "gsm8k" + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return GSM8kEvaluator + + @classmethod + def _get_session_class(cls): + return GSM8kSession + + subset: Literal["main"] = "main" + include_calculator_tool: bool = True + runner: RunnerName | None = None # Threadsafe; uses global default runner + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "include_calculator_tool": self.include_calculator_tool, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/requirements.txt new file mode 100644 index 00000000..aee11b28 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/gsm8k/requirements.txt @@ -0,0 +1 @@ +datasets diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/hle_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/hle_benchmark.py new file mode 100644 index 00000000..d128f6b3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/hle_benchmark.py @@ -0,0 +1,390 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import asyncio +import json +import math +from typing import Any, ClassVar, Literal + +import numpy as np +from pydantic import BaseModel, ConfigDict, Field + +from ...core.actions import ActionsHandler, extract_argument +from ...core.benchmark import Benchmark +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + FinishAction, + Observation, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from ...utils.cost import CostReport, LiteLLMCostReport +from ...utils.settings import RunnerName + +HLE_TOTAL_TASKS = 2500 + +JUDGE_PROMPT = """Judge whether the following [response] to [question] is correct or not based on the precise and unambiguous [correct_answer] below. + +[question]: {question} + +[response]: {response} + +Your judgement must be in the format and criteria specified below: + +extracted_final_answer: The final exact answer extracted from the [response]. Put the extracted answer as 'None' if there is no exact, final answer to extract from the response. + +[correct_answer]: {correct_answer} + +reasoning: Explain why the extracted_final_answer is correct or incorrect based on [correct_answer], focusing only on if there are meaningful differences between [correct_answer] and the extracted_final_answer. Do not comment on any background to the problem, do not attempt to solve the problem, do not argue for any answer different than [correct_answer], focus only on whether the answers match. + +correct: Answer 'yes' if extracted_final_answer matches the [correct_answer] given above, or is within a small margin of error for numerical problems. Answer 'no' otherwise, i.e. if there if there is any inconsistency, ambiguity, non-equivalency, or if the extracted answer is incorrect. + + +confidence: The extracted confidence score between 0|%| and 100|%| from [response]. Put 100 if there is no confidence score available.""" + + +class ExtractedAnswer(BaseModel): + extracted_final_answer: str + reasoning: str + correct: Literal["yes", "no"] + confidence: int + strict: Literal[True] + + +class HLEFinishArgs(BaseModel): + explanation: str = Field(..., description="Your explanation/reasoning for your answer.") + answer: str = Field(..., description="Your final answer to the question.") + confidence: int = Field(..., description="Your confidence score between 0 and 100.", ge=0, le=100) + + +class HLEFinishAction(FinishAction): + name: Literal["finish"] = "finish" + arguments: HLEFinishArgs + +class HLESession(Session): + + def __init__( + self, + task_idx: int, + judge_model: str = "o3-mini-2025-01-31", + session_id: str | None = None, + agent_timeout: int = 900, + **_kwargs: Any, + ) -> None: + if session_id is not None: + self._session_id = session_id + from datasets import load_dataset + + dataset = load_dataset("cais/hle", split="test") + row = dataset[task_idx] + + self._question = row["question"] + self._gold_answer = row["answer"] + self._image = self._encode_image(row.get("image", None)) + self._task_id = task_idx + self._judge_model = judge_model + self._agent_timeout = agent_timeout + self._start_time: float | None = None + self._done = False + self._final_answer: str | None = None + self._final_explanation: str | None = None + self._final_confidence: int | None = None + self._judge_result: dict[str, Any] | None = None + self._judge_input_tokens = 0 + self._judge_output_tokens = 0 + + self._registry = ActionsHandler(logger=self.logger) + self._registry.add_action( + name="finish", + description="Submit your final answer with explanation and confidence score.", + action_cls=HLEFinishAction, + handler=self._handle_finish, + is_finish=True, + ) + super().__init__() + + @staticmethod + def _encode_image(image) -> str | None: + if image is None: + return None + if isinstance(image, str): + return image if image else None + + import base64 + import io + + buf = io.BytesIO() + image.save(buf, format="PNG") + b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + return f"data:image/png;base64,{b64}" + + @property + def task(self) -> str: + return ( + "Answer the following question. Submit your answer by calling the 'finish' action " + "with your explanation, final answer, and confidence score (0-100).\n\n" + f"Question: {self._question}" + ) + + @property + def context(self) -> dict[str, Any]: + if self._image: + return {"image": {"type": "image_url", "data": self._image}} + return {} + + @property + def task_id(self) -> str: + return str(self._task_id) + + @property + def actions(self) -> list[ActionType]: + return self._registry.actions + + def start(self) -> Observation | None: + import time + self._start_time = time.time() + return EmptyObservation() + + def step(self, action: Action) -> Observation | None: + import time + if action is None: + self._done = True + if self._done: + return None + if self._start_time is not None: + elapsed = time.time() - self._start_time + if elapsed >= self._agent_timeout: + self._done = True + return SingleObservation( + result=f"[Agent timeout reached ({self._agent_timeout}s). Session ending.]" + ) + observation = self._registry.execute(action) + return observation + + def done(self) -> bool: + return self._done + + def _run_judge(self) -> dict[str, Any] | None: + if self._final_answer is None: + return None + + response_text = ( + f"Explanation: {self._final_explanation or ''}\n" + f"Answer: {self._final_answer}\n" + f"Confidence: {self._final_confidence or 100}%" + ) + + prompt = JUDGE_PROMPT.format( + question=self._question, + correct_answer=self._gold_answer, + response=response_text, + ) + + async def _judge_async() -> dict[str, Any] | None: + import litellm + + try: + resp = await litellm.acompletion( + model=self._judge_model, + max_tokens=4096, + messages=[{"role": "user", "content": prompt}], + response_format=ExtractedAnswer, + ) + import json as _json + + content = _json.loads(resp.choices[0].message.content) + usage = getattr(resp, "usage", None) + if usage is not None: + self._judge_input_tokens += int(getattr(usage, "prompt_tokens", 0) or 0) + self._judge_output_tokens += int(getattr(usage, "completion_tokens", 0) or 0) + return { + "correct_answer": self._gold_answer, + "model_answer": content["extracted_final_answer"], + "reasoning": content["reasoning"], + "correct": content["correct"], + "confidence": content["confidence"], + } + except Exception as e: + self.logger.warning(f"Judge failed: {e}") + return None + + return asyncio.run(_judge_async()) + + def score(self) -> SessionScore: + if self._judge_result is None: + self._judge_result = self._run_judge() + + if self._judge_result is not None: + correct = self._judge_result["correct"] == "yes" + score = 1.0 if correct else 0.0 + else: + score = 0.0 + + finished = self._final_answer is not None + return SessionScore( + score=score, + success=score == 1.0, + is_finished=finished, + session_metrics={ + "confidence": self._final_confidence, + "judge_result": self._judge_result, + }, + ) + + def close(self): + super().close() + sc = self.score() + self.save_standard_results(sc) + + def _handle_finish(self, action: SingleAction) -> None: + self._final_explanation = extract_argument(action.arguments, "explanation", None) + self._final_answer = extract_argument(action.arguments, "answer", None) + self._final_confidence = extract_argument(action.arguments, "confidence", 100) + self._done = True + return None + + def get_cost(self) -> CostReport: + if self._judge_input_tokens == 0 and self._judge_output_tokens == 0: + return LiteLLMCostReport.initialize_empty(model_name=self._judge_model) + return LiteLLMCostReport.from_token_counts( + self._judge_model, + self._judge_input_tokens, + self._judge_output_tokens, + ) + + + +def calib_err(confidence, correct, p="2", beta=100): + idxs = np.argsort(confidence) + confidence = confidence[idxs] + correct = correct[idxs] + bins = [[i * beta, (i + 1) * beta] for i in range(len(confidence) // beta)] + if not bins: + return 0.0 + bins[-1] = [bins[-1][0], len(confidence)] + + cerr = 0 + total_examples = len(confidence) + for i in range(len(bins) - 1): + bin_confidence = confidence[bins[i][0] : bins[i][1]] + bin_correct = correct[bins[i][0] : bins[i][1]] + num_examples_in_bin = len(bin_confidence) + + if num_examples_in_bin > 0: + difference = np.abs(np.nanmean(bin_confidence) - np.nanmean(bin_correct)) + if p == "2": + cerr += num_examples_in_bin / total_examples * np.square(difference) + elif p == "1": + cerr += num_examples_in_bin / total_examples * difference + elif p in ("infty", "infinity", "max"): + cerr = np.maximum(cerr, difference) + + if p == "2": + cerr = np.sqrt(cerr) + + return float(cerr) + + +class HLEEvaluator(Evaluator): + + def __init__(self, subset: str = "test", judge_model: str = "o3-mini-2025-01-31", agent_timeout: int = 900) -> None: + self._subset = subset + self._judge_model = judge_model + self._agent_timeout = agent_timeout + self._dataset = None + + def _ensure_dataset(self) -> None: + if self._dataset is None: + from datasets import load_dataset + + self._dataset = load_dataset("cais/hle", split="test") + + def list_tasks(self) -> list[str]: + self._ensure_dataset() + return [str(i) for i in range(len(self._dataset))] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_dataset() + idx = int(index.task_id) + if idx < 0 or idx >= len(self._dataset): + raise IndexError(f"Task id {index.task_id} out of range for HLE.") + return { + "task_idx": idx, + "judge_model": self._judge_model, + "agent_timeout": self._agent_timeout, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + scores: list[float] = [] + confidences: list[float] = [] + corrects: list[float] = [] + + for paths in self.get_sessions_paths(sessions): + with open(paths.benchmark_results, encoding="utf-8-sig") as f: + payload = json.load(f) + s = float(payload["score"]) + scores.append(s) + corrects.append(s) + metrics = payload.get("session_metrics", {}) + conf = metrics.get("confidence") + if conf is not None: + confidences.append(float(conf) / 100.0) + else: + confidences.append(1.0) + + n = len(scores) + accuracy = 100 * sum(scores) / n if n else 0.0 + confidence_half_width = 1.96 * math.sqrt(accuracy * (100 - accuracy) / n) if n else 0.0 + + cal_err = 0.0 + if n > 0: + cal_err = 100 * calib_err( + np.array(confidences), np.array(corrects), p="2", beta=100 + ) + + return BenchmarkResults( + benchmark_name="hle", + total_tasks=n, + score=accuracy / 100.0, + metrics={ + "accuracy_pct": round(accuracy, 2), + "confidence_interval": round(confidence_half_width, 2), + "calibration_error": round(cal_err, 2), + }, + ) + + +class HLEBenchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "HLE" + slug_name: ClassVar[str] = "hle" + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return HLEEvaluator + + @classmethod + def _get_session_class(cls): + return HLESession + + subset: Literal["test"] = "test" + judge_model: str = "o3-mini-2025-01-31" + agent_timeout: int = 900 + runner: RunnerName | None = None + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "judge_model": self.judge_model, + "agent_timeout": self.agent_timeout, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/requirements.txt new file mode 100644 index 00000000..bf051a4f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hle/requirements.txt @@ -0,0 +1,3 @@ +datasets +numpy +litellm diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/hotpotqa_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/hotpotqa_benchmark.py new file mode 100644 index 00000000..8cea899f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/hotpotqa_benchmark.py @@ -0,0 +1,373 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import asyncio +import json +import re +import stat +import string +import textwrap +import threading +from collections import Counter +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, ConfigDict + +from ...adapters.schemas.openai import ( + mcp_tools_to_openai_tools, + openai_tools_to_action_types, +) + +# Copied scoring functions from https://github.com/hotpotqa/hotpot/blob/master/hotpot_evaluate_v1.py +from ...core.actions import ActionsHandler, extract_argument +from ...core.benchmark import Benchmark +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + EmptyObservation, + FinishAction, + Observation, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from ...utils.settings import RunnerName + +HOTPOTQA_TOTAL_TASKS = 7405 + + +def normalize_answer(s): + def remove_articles(text): + return re.sub(r"\b(a|an|the)\b", " ", text) + + def white_space_fix(text): + return " ".join(text.split()) + + def remove_punc(text): + exclude = set(string.punctuation) + return "".join(ch for ch in text if ch not in exclude) + + def lower(text): + return text.lower() + + return white_space_fix(remove_articles(remove_punc(lower(s)))) + + +def f1_score(prediction, ground_truth): + normalized_prediction = normalize_answer(prediction) + normalized_ground_truth = normalize_answer(ground_truth) + + zero_metric = (0, 0, 0) + + if normalized_prediction in ["yes", "no", "noanswer"] and normalized_prediction != normalized_ground_truth: + return zero_metric + if normalized_ground_truth in ["yes", "no", "noanswer"] and normalized_prediction != normalized_ground_truth: + return zero_metric + + prediction_tokens = normalized_prediction.split() + ground_truth_tokens = normalized_ground_truth.split() + common = Counter(prediction_tokens) & Counter(ground_truth_tokens) + num_same = sum(common.values()) + if num_same == 0: + return zero_metric + precision = 1.0 * num_same / len(prediction_tokens) + recall = 1.0 * num_same / len(ground_truth_tokens) + f1 = (2 * precision * recall) / (precision + recall) + return f1, precision, recall + + +class HotpotFinishArgs(BaseModel): + answer: str + + +class HotpotFinishAction(FinishAction): + name: Literal["finish"] = "finish" + arguments: HotpotFinishArgs + + +class HotpotQASession(Session): + """Session for HotpotQA benchmark evaluation.""" + + _question: str + _done: bool + + def __init__( + self, + with_search_tools: bool, + instance: dict[str, Any], + session_id: str | None = None, + **_kwargs: Any, + ) -> None: + if session_id is not None: + self._session_id = session_id + self._question = instance["question"] + self.logger.info(f"question: {self._question}") + self._gold_answer = instance["answer"] + self._task_id = instance["task_id"] + self._done = False + self._final_answer = None + self._with_search_tools = with_search_tools + self._registry = ActionsHandler(logger=self.logger) + self._mcp_ready = threading.Event() + self._mcp_error: BaseException | None = None + + self.mcp_thread: threading.Thread | None = None + if self._with_search_tools: + self.mcp_thread = threading.Thread(target=self.run_wikipedia_server, daemon=True) + self.mcp_thread.start() + ready = self._mcp_ready.wait(timeout=60.0) + if not ready or self._mcp_error is not None: + err = self._mcp_error + raise RuntimeError(f"MCP initialization failed or timed out: {err}") from err + else: + # No search tools requested; skip MCP startup. + self._mcp_ready.set() + # Only 'finish' is provided as the completion action + self._registry.add_action( + name="finish", + description="Submit the final answer and complete the task.", + action_cls=HotpotFinishAction, + handler=self._handle_finish, + is_finish=True, + ) + super().__init__() + + def run_wikipedia_server(self): + asyncio.run(self.run_wikipedia_server_async()) + + def _mcp_client_config(self) -> dict[str, Any]: + """Return a FastMCP client config that logs wikipedia-mcp stderr to the session benchmark dir.""" + log_path = self.paths.benchmark_dir / "wikipedia_mcp.log" + log_path.parent.mkdir(parents=True, exist_ok=True) + + from ...core.context import context_env + + ctx_env = context_env() + ctx_env_json = json.dumps(ctx_env) + + # Generate a tiny Python wrapper to keep stdout for JSONRPC and send stderr to a file. + wrapper_path = self.paths.benchmark_dir / "wikipedia_mcp_wrapper.py" + wrapper_code = ( + textwrap.dedent( + f""" + #!/usr/bin/env python3 + import subprocess, sys, os, json + + log = open({str(log_path)!r}, "ab", buffering=0) + os.environ.update(json.loads({ctx_env_json!r})) + proc = subprocess.Popen( + ["wikipedia-mcp", "--transport", "stdio"], + stderr=log, + ) + proc.wait() + sys.exit(proc.returncode) + """ + ).strip() + + "\n" + ) + wrapper_path.write_text(wrapper_code, encoding="utf-8") + wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + command = str(wrapper_path) + return {"mcpServers": {"wiki": {"command": command, "args": []}}} + + async def run_wikipedia_server_async(self): + if not self._with_search_tools: + self._mcp_ready.set() + return + try: + from fastmcp import Client + + config = self._mcp_client_config() + mcp_client = Client(config) + + async with mcp_client: + tools = await mcp_client.list_tools() + + openai_tools = mcp_tools_to_openai_tools(tools) + if self._with_search_tools: + self._registry.add_actions( + openai_tools_to_action_types(openai_tools), + self._handle_mcp_action, + ) + except Exception as e: + self._mcp_error = e + self.logger.exception(f"Failed to initialize wikipedia-mcp: {e}") + raise + finally: + self._mcp_ready.set() + + @property + def task(self) -> str: + return ( + "Answer the user question. Submit the final answer as a short phrase by calling 'finish'. " + "If the question is yes/no, answer 'yes' or 'no'. Do not add explanations.\n\n" + f"Question: {self._question}" + ) + + @property + def context(self) -> dict[str, Any]: + return {} + + @property + def task_id(self) -> str: + return str(self._task_id) + + @property + def actions(self) -> list[ActionType]: + return self._registry.actions + + def _to_observation(self, raw: Any, invoking: list[SingleAction] | None = None) -> Observation: + return SingleObservation(invoking_actions=invoking or [], result=raw) + + def start(self) -> Observation | None: + # Empty initial observation; question is carried in the task string. + return EmptyObservation() + + def run_mcp_command(self, name, arguments) -> Any: + return asyncio.run(self.run_mcp_command_async(name, arguments)) + + async def run_mcp_command_async(self, name, arguments) -> Any: + from fastmcp import Client + + config = self._mcp_client_config() + mcp_client = Client(config) + + async with mcp_client: + response = await mcp_client.call_tool(name=name, arguments=arguments.model_dump()) + # print(response.structured_content) + return response.structured_content + + def step(self, action: Action) -> Observation | None: + if action is None: + self._done = True + + if self._done: + return None + + observation = self._registry.execute(action) + + return observation + + def done(self) -> bool: + return self._done + + def score(self) -> SessionScore: + # Minimal: compute F1 and always mark success + try: + f1, precision, recall = f1_score(self._gold_answer, self._final_answer) + score = float(f1) + except Exception: + score = 0.0 + self.logger.info(f"Gold: {self._gold_answer} Prediction: {self._final_answer} Score: {score}") + # Finished only when the benchmark finish action stores a final answer. + finished = self._final_answer is not None + success = score >= 1.0 - 1e-6 + return SessionScore(score=score, success=success, is_finished=finished) + + def close(self): + super().close() + # Persist minimal results for aggregation + sc = self.score() + self.save_standard_results(sc) + self.logger.debug("Closing MCP server..") + if self.mcp_thread and self.mcp_thread.is_alive(): + self.logger.debug("Waiting for MCP server to shut down.") + self.mcp_thread.join(timeout=60.0) + if self.mcp_thread.is_alive(): + self.logger.warning("MCP server thread did shutdown cleanly, continuing anyway.") + else: + self.logger.debug("MCP server shutdown cleanly.") + + # Action handlers ------------------------------------------------------------ + def _handle_finish(self, action: SingleAction) -> Any: + self.logger.info(f"Received final answer: {action}") + answer = extract_argument(action.arguments, "answer", None) + self._final_answer = answer + self._done = True + return None + + def _handle_mcp_action(self, action: SingleAction) -> Any: + result = self.run_mcp_command(action.name, action.arguments) + return result + + +# ── Evaluator ──────────────────────────────────────────────────────── + + +class HotpotQAEvaluator(Evaluator): + """Evaluator for HotpotQA — task discovery, session kwargs, aggregation.""" + + def __init__(self, subset: str = "distractor", with_search_tools: bool = True) -> None: + self._subset = subset + self._with_search_tools = with_search_tools + self._dataset = None + + def _ensure_dataset(self) -> None: + if self._dataset is None: + from datasets import load_dataset + + self._dataset = load_dataset("hotpotqa/hotpot_qa", "distractor")["validation"] + + def list_tasks(self) -> list[str]: + return [str(i) for i in range(HOTPOTQA_TOTAL_TASKS)] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_dataset() + idx = int(index.task_id) + if idx < 0 or idx >= len(self._dataset): + raise IndexError(f"Task id {index.task_id} out of range for HotpotQA.") + instance = {"task_id": idx, **self._dataset[idx]} + return { + "with_search_tools": self._with_search_tools, + "instance": instance, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + scores: list[float] = [] + for paths in self.get_sessions_paths(sessions): + with open(paths.benchmark_results, encoding="utf-8-sig") as f: + payload = json.load(f) + s = float(payload["score"]) + scores.append(s) + avg = sum(scores) / len(scores) if scores else 0.0 + return BenchmarkResults( + benchmark_name="hotpotqa", + total_tasks=len(sessions), + score=avg, + metrics={}, + ) + + +# ── Benchmark config ───────────────────────────────────────────────── + + +class HotpotQABenchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "HotpotQA" + slug_name: ClassVar[str] = "hotpotqa" + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return HotpotQAEvaluator + + @classmethod + def _get_session_class(cls): + return HotpotQASession + + subset: Literal["distractor"] = "distractor" + with_search_tools: bool = True + runner: RunnerName | None = None # Threadsafe; uses global default runner (venv) + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "with_search_tools": self.with_search_tools, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/requirements.txt new file mode 100644 index 00000000..5e9dae03 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/hotpotqa/requirements.txt @@ -0,0 +1,3 @@ +datasets +fastmcp +wikipedia-mcp diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/config.yaml b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/config.yaml new file mode 100644 index 00000000..aac16d0b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/config.yaml @@ -0,0 +1,106 @@ +# SWE-bench Benchmark Configuration + +# Session settings +session: + timeout: 1000 # Default timeout for bash commands (seconds) + environment_pull_timeout: 600 # Timeout for Docker container startup/pull (seconds) + observation_size_limit: 10000 # Max characters in observation output + max_interactions: 200 # Max actions per session (null for unlimited) + timeout_template: | + The last command {command} timed out and has been killed. + The output of the command was: + {exception_output} + Please try another command + +# Task prompt template +# Available variables: {container_repo_dir}, {problem_statement} +task_prompt: | + Resolve the given issue by editing the repository files directly on a remote machine. + + Repository directory on the remote machine: {container_repo_dir} + + ## Issue to resolve: + {problem_statement} + + ## Execution Environment & Access (STRICT): + All commands are executed on a remote machine that already contains the full + repository and all required system dependencies and prerequisites. + + The remote machine is accessible **ONLY** via the `bash` action. + All interactions with the filesystem and environment — including reading files, + editing files, and running scripts — can be performed by definition only by using `bash`. + Each command is executed in a separate shell invocation; working directory changes + and environment variables do not persist between commands. Only files written to disk persist. + All bash commands are executed with the working directory set to the root of the remote machine (/). + + ## Instructions: + You must fix the issue by directly modifying files in this repository, in place. + + **ALL file edits MUST be performed using `bash` commands on the remote machine.** + Explanations or code snippets in chat are not sufficient. + + Only changes written to files on disk and present in the working tree will be + included in the final patch. Any fix not reflected in repository files is ignored. + + Make changes only to non-test source files in a way that is general and consistent + with the existing codebase. + + ## Hard Boundaries: + - MODIFY ONLY: regular source code files in /testbed + - DO NOT MODIFY: tests or configuration files (pyproject.toml, setup.cfg, etc.) + - DO NOT use interactive editors (vi, nano, etc.) + - DO NOT suggest changes without implementing them + + ## Patch & Submission Mechanics: + Submission captures the entire working tree using: + + `git add -A && git diff --staged C0` + + This means: + - All intended fixes MUST appear in repository files + - Any file present at submission time WILL be included in the patch + - Temporary files, debug artifacts, or helper scripts MUST be removed before submission + + ## Recommended Workflow: + 1. Analyze the codebase + 2. Reproduce the issue + 3. Edit source files using `bash` + 4. Verify the fix + 5. Test edge cases + + ## Submission (FINAL STEP): + When finished, use `submit_patch` exactly once with a short summary. + Ensure the repository contains ONLY the intended final changes. + After submission, no further reading, editing, or testing is allowed. + + ## Evaluation: + Your patch will be applied and validated using a hidden test suite. + + The evaluation verifies that: + 1. The reported issue is fully resolved + 2. All previously passing functionality and tests remain unbroken + + Success requires all tests to pass. + +# Benchmark settings +benchmark: + subset: "princeton-nlp/SWE-bench_Verified" + num_tasks: 3 # Number of tasks to run (null for all) + runner: "venv" # Runner type: "direct", "process", "venv", "docker" + seed: 42 + require_submit_for_patch_evaluation: true # If false, evaluate patch even without submit_patch + +# Evaluation settings +evaluation: + harness_timeout: 1800 # Timeout for SWE-bench harness evaluation (seconds) + max_workers: 1 # Number of parallel workers for evaluation + cache_level: "instance" # Cache level for evaluation + open_file_limit: 4096 # Open file limit for evaluation + +# Logging +logging: + hook_loggers: # Logger names to hook into session + - "minisweagent" + - "minisweagent.environment" + - "LiteLLM" + log_level: "DEBUG" diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/readme.md b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/readme.md new file mode 100644 index 00000000..2d70ea5f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/readme.md @@ -0,0 +1,43 @@ +# SWE-bench + +Evaluate agents on real-world GitHub issues using the [SWE-bench](https://github.com/princeton-nlp/SWE-bench) benchmark. + +## Requirements + +- Linux x86_64 with Docker +- Python 3.11 in a virtual environment +- Run setup once: `bash src/exgentic/benchmarks/swebench/setup.sh` + +## Usage + +```python +from exgentic.core.orchestrator import run +from exgentic.benchmarks.swebench.swebench_benchmark import SWEBenchBenchmark +from exgentic.agents.litellm_tool_calling.litellm_tool_calling_agent import LiteLLMToolCallingAgent + +benchmark = SWEBenchBenchmark( + subset="princeton-nlp/SWE-bench_Lite", + num_tasks=10, +) +agent = LiteLLMToolCallingAgent(model="gpt-4o", max_steps=30) +results = run(benchmark, agent, output_dir="./outputs/swebench") +print("Avg score:", results.score) +``` + +## Output Structure + +Each session writes to `outputs//sessions//benchmark/`: + +``` +session.log - benchmark execution log +predictions.jsonl - generated patch +logs/ - SWE-bench harness evaluation logs +results.json - evaluation results (resolved, test pass rates) +``` + +## Notes + +- Each task runs in a dedicated Docker container via `minisweagent` +- Patches are evaluated using the official SWE-bench harness +- Invalid patches (missing diff markers) are rejected without evaluation +- Heavy projects (e.g., astropy) may need increased Docker RAM/CPU limits diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/requirements.txt new file mode 100644 index 00000000..a3258815 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/requirements.txt @@ -0,0 +1,2 @@ +swebench @ git+https://github.com/SWE-bench/SWE-bench.git@v4.1.0 +mini-swe-agent @ git+https://github.com/SWE-agent/mini-swe-agent.git@v1.17.0 diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_benchmark.py new file mode 100644 index 00000000..805f577c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_benchmark.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""SWE-bench benchmark adapter -- light benchmark class only. + +Evaluator, session, and helper classes live in ``swebench_eval.py`` +and are loaded inside the runner subprocess via ``_get_evaluator_class()`` +and ``_get_session_class()``. This file must remain importable without +the ``swebench`` package installed. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, ClassVar + +from pydantic import BaseModel, ConfigDict, Field + +from ...core import Benchmark +from ...core.types import FinishAction, SingleAction +from ...core.types import SessionScore as BaseSessionScore + +# ============================================================================= +# Shared action / score types (importable without heavy deps) +# ============================================================================= + + +class BashArgs(BaseModel): + command: str = Field(description="Bash command to execute") + + +class BashAction(SingleAction): + name: str = "bash" + arguments: BashArgs + + +class SubmitPatchArgs(BaseModel): + summary: str = Field(description="Brief textual summary of the fix (no diff/patch)") + + +class SubmitPatchAction(FinishAction): + name: str = "finish" + arguments: SubmitPatchArgs + + +class SessionScore(BaseSessionScore): + instance_id: str = "" + agent: dict[str, Any] = Field(default_factory=dict) + patch: dict[str, Any] = Field(default_factory=dict) + container: dict[str, Any] = Field(default_factory=dict) + evaluation: dict[str, Any] = Field(default_factory=dict) + summary: dict[str, Any] = Field(default_factory=dict) + + +# ============================================================================= +# Configuration (light -- only stdlib + yaml lazy) +# ============================================================================= + +_CONFIG: dict[str, Any] = None + + +def get_config() -> dict[str, Any]: + import yaml + + global _CONFIG + if _CONFIG is None: + path = Path(__file__).parent / "config.yaml" + _CONFIG = yaml.safe_load(path.read_text()) + return _CONFIG + + +# ============================================================================= +# Benchmark +# ============================================================================= + + +class SWEBenchBenchmark(Benchmark): + """Benchmark configuration for SWE-bench evaluation.""" + + display_name: ClassVar[str] = "SWE-bench" + slug_name: ClassVar[str] = "swebench" + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def _get_evaluator_class(cls): + return "exgentic.benchmarks.swebench.swebench_eval:SWEBenchEvaluator" + + @classmethod + def _get_session_class(cls): + return "exgentic.benchmarks.swebench.swebench_eval:SWEBenchSession" + + subset: str | None = None + require_submit_for_patch_evaluation: bool = True + docker_socket: bool = True # SWE-bench sessions create sibling Docker containers + + def model_post_init(self, __context): + cfg = get_config() + benchmark_cfg = cfg["benchmark"] + session_cfg = cfg["session"] + if self.subset is None: + self.subset = benchmark_cfg["subset"] + if self.runner is None: + self.runner = benchmark_cfg.get("runner") + if "seed" in benchmark_cfg: + self.seed = benchmark_cfg["seed"] + if ( + "require_submit_for_patch_evaluation" in benchmark_cfg + and "require_submit_for_patch_evaluation" not in self.model_fields_set + ): + self.require_submit_for_patch_evaluation = bool(benchmark_cfg["require_submit_for_patch_evaluation"]) + if self.max_interactions is None: + self.max_interactions = session_cfg.get("max_interactions") + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "require_submit_for_patch_evaluation": self.require_submit_for_patch_evaluation, + "max_interactions": self.max_interactions, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_eval.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_eval.py new file mode 100644 index 00000000..c61b4cdb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_eval.py @@ -0,0 +1,466 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""SWE-bench evaluator, session, and helper classes. + +These classes may import external dependencies (swebench, minisweagent, etc.) +at method level. They are only ever instantiated inside the isolated runner +subprocess, so the heavy dependencies are never required in the host process. + +The light ``SWEBenchBenchmark`` class lives in ``swebench_benchmark.py`` and +must remain importable without any external packages installed. +""" + +from __future__ import annotations + +import json +import logging +import subprocess +import textwrap +from pathlib import Path +from typing import Any + +from ...core.actions import ActionsHandler +from ...core.evaluator import Evaluator +from ...core.session import Session +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + Observation, + SessionIndex, + SingleObservation, +) +from ...utils.logging import hook_loggers_into_session +from ...utils.paths import get_run_id +from ...utils.settings import ExgenticSettings, get_settings +from . import swebench_evaluation, swebench_logs, swebench_metrics +from .swebench_benchmark import BashAction, SubmitPatchAction + +# ============================================================================= +# Configuration +# ============================================================================= + +_CONFIG: dict[str, Any] = None + + +def get_config() -> dict[str, Any]: + import yaml + + global _CONFIG + if _CONFIG is None: + path = Path(__file__).parent / "config.yaml" + _CONFIG = yaml.safe_load(path.read_text()) + return _CONFIG + + +# ============================================================================= +# Action Handlers +# ============================================================================= + + +def run_bash( + command: str, + env, + timeout: int | None = None, + size_limit: int | None = None, + timeout_template: str = "", +) -> dict[str, Any]: + """Execute bash command in environment.""" + try: + output = env.execute(command=command, timeout=timeout or env.config.timeout) + except subprocess.TimeoutExpired as e: + exception_output = e.output.decode("utf-8", errors="replace") if e.output else "" + msg = textwrap.dedent(timeout_template).format_map({"command": command, "exception_output": exception_output}) + output = {"output": msg, "returncode": 124} + + # Truncate large outputs + if size_limit and len(output["output"]) > size_limit: + head, tail = size_limit // 2, size_limit - size_limit // 2 + omitted = f"\n\n- OMITTED {len(output['output']) - size_limit} chars -\n\n" + output["output"] = output["output"][:head] + omitted + output["output"][-tail:] + + return output + + +def generate_patch(env, cwd: str, base_commit: str) -> str: + """Generate patch from staged changes.""" + command = f"git add -A && git diff --staged {base_commit} | cat" + output = env.execute(command=command, cwd=cwd) + return output["output"] + + +# ============================================================================= +# Session +# ============================================================================= + + +class SWEBenchSession(Session): + """Session for a single SWE-bench task.""" + + def __init__( + self, + settings: ExgenticSettings, + instance: dict[str, Any], + subset: str, + max_interactions: int | None = None, + require_submit_for_patch_evaluation: bool = True, + session_id: str | None = None, + ) -> None: + cfg = get_config() + + self._instance = instance + self._subset = subset + self._instance_id = instance["instance_id"] + if session_id is not None: + self._session_id = session_id + + self._registry = ActionsHandler( + logger=self.logger, + warn_on_validation_error=False, + warn_on_unknown_action=True, + handle_validation_error=None, + handle_unknown_action=None, + ) + + # State + self._step_count = 0 + self._done = False + self._max_interactions = max_interactions + self._require_submit_for_patch_evaluation = require_submit_for_patch_evaluation + self._action_count = 0 + self._score = None + self._final_patch: str | None = None + + # Environment (set in start()) + self.env = None + from swebench.harness.constants import DOCKER_WORKDIR + + self.container_repo_dir = DOCKER_WORKDIR + self.container_base_commit: str | None = None + + # Config + self._timeout = cfg["session"]["timeout"] + self._environment_pull_timeout = int(cfg["session"].get("environment_pull_timeout", 600)) + self._observation_size_limit = cfg["session"]["observation_size_limit"] + self._timeout_template = cfg["session"]["timeout_template"] + self._task_prompt = cfg["task_prompt"] + self._eval_config = cfg["evaluation"] + + self.logger.info( + f"INIT | Session initialized | dataset: {self._subset:<30} " + f"| instance_id: {self._instance_id:<20} | repo: {self._instance['repo']}" + ) + + hook_loggers_into_session( + self.logger, + logger_names=cfg["logging"].get("hook_loggers", []), + level=logging._nameToLevel.get(cfg["logging"]["log_level"], logging.INFO), + ) + + # Call parent to save session manifest (session.json) + super().__init__() + + # ------------------------------------------------------------------------- + # Lifecycle + # ------------------------------------------------------------------------- + + def start(self) -> Observation | None: + self.logger.info("START | Session start") + self._setup_environment() + return SingleObservation(invoking_actions=[], result=None) + + def done(self) -> bool: + return self._done + + def close(self): + self.logger.info("CLOSE | Session closing...") + super().close() + self.logger.debug("CLOSE | Cleaning agent environment") + del self.env + self.logger.info("CLOSE | Session closed") + + # ------------------------------------------------------------------------- + # Step Execution + # ------------------------------------------------------------------------- + + def _handle_bash(self, action: BashAction) -> str: + self.logger.info(f"STEP | {self._step_count:<3} | ACTION | bash | command: {action.arguments.command}") + result = run_bash( + command=action.arguments.command, + env=self.env, + timeout=self._timeout, + size_limit=self._observation_size_limit, + timeout_template=self._timeout_template, + ) + self.logger.info( + f"STEP | {self._step_count:<3} | RESULT | bash | " + f"returncode: {result['returncode']} | output_len: {len(result['output'])}" + ) + return result + + def _handle_submit_patch(self, action: SubmitPatchAction) -> str: + self.logger.info(f"STEP | {self._step_count:<3} | ACTION | submit_patch | summary: {action.arguments.summary}") + self._final_patch = generate_patch( + env=self.env, + cwd=self.container_repo_dir, + base_commit=self.container_base_commit, + ) + self._done = True + return None + + def step(self, action: Action) -> Observation | None: + if self._done: + return None + + if self._max_interactions is not None: + incoming = len(action.to_action_list()) + if self._action_count + incoming > self._max_interactions: + self.logger.warning( + "STEP | {self._step_count:<3} | Max interactions reached (%s/%s); terminating session", + self._action_count, + self._max_interactions, + ) + return None + + self._step_count += 1 + observation = self._registry.execute(action) + if observation is None: + return None + + # Post-execute: always track action count (useful for metrics) + self._action_count += len(action.to_action_list()) + + return observation + + # ------------------------------------------------------------------------- + # Scoring + # ------------------------------------------------------------------------- + + def score(self) -> swebench_logs.SessionScore: + """Compute and cache the session score.""" + if self._score is not None: + return self._score + + self.logger.info("SCORE | Computing session score") + + harness_result = None + if self._done or not self._require_submit_for_patch_evaluation: + if self._final_patch is None and not self._done: + self.logger.info("SCORE | submit_patch not called; generating patch from current workspace") + self._final_patch = self._generate_current_patch() + harness_result = self._run_harness() + else: + self.logger.info( + "SCORE | Skipping harness - submit_patch not called (require_submit_for_patch_evaluation=true)" + ) + + # Flush logs before parsing + for handler in self.logger.handlers: + handler.flush() + + # Build score + self._score = swebench_logs.build_score( + paths=self.paths, + num_actions=self._action_count, + max_interactions=self._max_interactions, + instance_id=self._instance_id, + harness_data=harness_result, + ) + + # Finished when agent called submit_patch (graceful completion) + self._score.is_finished = self._done + + results_path = self.paths.benchmark_results + swebench_logs.write_results(results_path, self._score) + self.logger.info( + "SCORE | Final | success=%s | score=%s | is_finished=%s", + self._score.success, + self._score.score, + self._score.is_finished, + ) + + return self._score + + def _run_harness(self) -> swebench_evaluation.HarnessResult | None: + """Run harness evaluation, handling errors.""" + if self._final_patch is None: + self.logger.info("SCORE | Harness evaluation skipped: no patch available for evaluation") + return None + try: + return swebench_evaluation.run_harness( + patch=self._final_patch, + instance_id=self._instance_id, + subset=self._subset, + paths=self.paths, + eval_config=self._eval_config, + logger=self.logger, + ) + except Exception as e: + self.logger.exception(f"SCORE | Harness evaluation failed: {e}") + return None + + def _generate_current_patch(self) -> str | None: + """Generate patch from current working tree for non-submit evaluation mode.""" + if self.env is None or self.container_base_commit is None: + self.logger.warning("SCORE | Cannot generate patch: environment/base commit unavailable") + return None + try: + return generate_patch( + env=self.env, + cwd=self.container_repo_dir, + base_commit=self.container_base_commit, + ) + except Exception as e: + self.logger.exception(f"SCORE | Patch generation failed: {e}") + return None + + # ------------------------------------------------------------------------- + # Properties + # ------------------------------------------------------------------------- + + @property + def actions(self) -> list[ActionType]: + if not self._registry.actions: + self._registry.add_action( + name="bash", + description="Run a bash command in the repo root and get the output", + action_cls=BashAction, + handler=self._handle_bash, + ) + self._registry.add_action( + name="finish", + description=( + "Finish the task by submitting a brief" + " summary. The system automatically computes" + " the git patch from the repository changes." + ), + action_cls=SubmitPatchAction, + handler=self._handle_submit_patch, + is_finish=True, + ) + return self._registry.actions + + @property + def task(self) -> str: + return self._task_prompt.format_map( + { + "container_repo_dir": self.container_repo_dir, + "problem_statement": self._instance["problem_statement"], + } + ) + + @property + def context(self) -> dict[str, str]: + return {} + + @property + def task_id(self) -> str: + return str(self._instance_id) + + # ------------------------------------------------------------------------- + # Environment Setup + # ------------------------------------------------------------------------- + + def _setup_environment(self) -> None: + """Initialize the Docker environment for the task.""" + self.logger.info("ENV | Setting up environment") + from ...utils.logging import capture_stdio_to_session + + with capture_stdio_to_session(self.logger): + import minisweagent + import yaml + from minisweagent.run.extra.swebench import get_sb_environment + + config_path = Path(minisweagent.__file__).parent / "config" / "extra" / "swebench.yaml" + config = yaml.safe_load(config_path.read_text()).copy() + config["environment"]["cwd"] = self.container_repo_dir + config["environment"]["pull_timeout"] = self._environment_pull_timeout + + self.env = get_sb_environment(config=config, instance=self._instance) + + result = self.env.execute(command="git rev-parse HEAD", cwd=self.container_repo_dir) + self.container_base_commit = result["output"].strip() + + if self.container_base_commit != self._instance["base_commit"]: + self.logger.error( + f"ENV | Base commit mismatch: expected {self._instance['base_commit']} " + f"| got {self.container_base_commit}" + ) + + +# ============================================================================= +# Evaluator +# ============================================================================= + + +class SWEBenchEvaluator(Evaluator): + """Evaluation logic for SWE-bench -- task discovery, session config, aggregation.""" + + def __init__( + self, + subset: str, + require_submit_for_patch_evaluation: bool = True, + max_interactions: int | None = None, + ) -> None: + self._subset = subset + self._require_submit_for_patch_evaluation = require_submit_for_patch_evaluation + self._max_interactions = max_interactions + self._dataset: Any = None + self._instances_by_id: dict[str, dict[str, Any]] = {} + + def list_tasks(self) -> list[str]: + self._ensure_dataset() + return list(self._instances_by_id.keys()) + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + self._ensure_dataset() + task_id_str = str(index.task_id) + instance = self._instances_by_id.get(task_id_str) + if instance is None: + raise KeyError(f"Unknown SWE-bench task id: {index.task_id}") + return { + "settings": get_settings(), + "instance": instance, + "subset": self._subset, + "max_interactions": self._max_interactions, + "require_submit_for_patch_evaluation": self._require_submit_for_patch_evaluation, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + scores: list[float] = [] + session_ids = [s.session_id for s in sessions] + for paths in self.get_sessions_paths(sessions): + if not paths.benchmark_results.exists(): + raise FileNotFoundError( + f"Missing results for planned session '{paths.session_id}' at {paths.benchmark_results}" + ) + with open(paths.benchmark_results, encoding="utf-8") as f: + payload = json.load(f) + scores.append(float(payload.get("score", 0.0))) + + metrics = swebench_metrics.collect_metrics(get_run_id(), session_ids) + + return BenchmarkResults( + benchmark_name="swebench", + total_tasks=len(sessions), + score=sum(scores) / len(scores) if scores else 0.0, + metrics=metrics["funnel"], + ) + + def _ensure_dataset(self) -> None: + if self._dataset is not None: + return + if self._subset is None: + raise ValueError("subset must be configured for SWE-bench.") + from datasets import load_dataset + + dataset = load_dataset(self._subset, split="test") + instances = list(dataset) + if not instances: + raise RuntimeError( + f"SWE-bench dataset '{self._subset}' returned 0 instances. Check dataset availability and HF auth." + ) + self._dataset = instances + self._instances_by_id = {str(inst["instance_id"]): inst for inst in instances} diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_evaluation.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_evaluation.py new file mode 100644 index 00000000..d2ea8977 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_evaluation.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import os +from contextlib import contextmanager +from dataclasses import dataclass +from logging import Logger +from pathlib import Path +from typing import Any + +from ...utils.logging import capture_stdio_to_session +from ...utils.paths import SessionPaths + + +@dataclass +class HarnessResult: + harness_report: dict[str, Any] + patch: str + patch_valid: bool + harness_ran: bool + error: str | None = None + + +def is_patch_valid(patch: str) -> bool: + if not patch or not patch.strip(): + return False + return any(m in patch for m in ["---", "+++", "@@", "diff --git", "*** Begin Patch"]) + + +@contextmanager +def _pushd(path): + prev = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev) + + +def run_harness( + patch: str, + instance_id: str, + subset: str, + paths: SessionPaths, + eval_config: dict[str, Any], + logger: Logger, +) -> HarnessResult: + patch = patch or "" + valid = is_patch_valid(patch) + + pred_path = paths.benchmark_dir / "predictions.jsonl" + pred_path.parent.mkdir(parents=True, exist_ok=True) + pred_path.write_text( + json.dumps( + { + "instance_id": instance_id, + "model_patch": patch, + "patch_valid": valid, + "model_name_or_path": "exgentic", + }, + ensure_ascii=False, + ) + + "\n" + ) + logger.info(f"EVAL | Writing patch | valid: {valid} | size: {len(patch)}") + + if not valid: + logger.warning("EVAL | Skipping harness - invalid patch structure") + return HarnessResult({}, patch, False, False) + + logger.info("EVAL | Running SWE-bench harness evaluation") + try: + from swebench.harness import run_evaluation + + with _pushd(paths.benchmark_dir), capture_stdio_to_session(logger): + report_path = run_evaluation.main( + dataset_name=subset, + split="test", + instance_ids=[instance_id], + predictions_path=pred_path.name, + max_workers=eval_config["max_workers"], + run_id="exgentic1", + namespace="swebench", + force_rebuild=False, + cache_level=eval_config["cache_level"], + clean=False, + open_file_limit=eval_config["open_file_limit"], + timeout=eval_config["harness_timeout"], + rewrite_reports=False, + modal=False, + ) + report = json.loads(Path(report_path).read_text()) if report_path and Path(report_path).is_file() else {} + logger.info("EVAL | Harness evaluation completed") + return HarnessResult(report, patch, True, True) + except Exception as e: + logger.exception(f"EVAL | Harness evaluation failed: {e}") + return HarnessResult({}, patch, True, True, str(e)) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_logs.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_logs.py new file mode 100644 index 00000000..07a5c61d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_logs.py @@ -0,0 +1,178 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import re +from pathlib import Path +from typing import Any + +from ...utils.paths import SessionPaths +from .swebench_benchmark import SessionScore + +FILE_OPS_RE = re.compile(r"\b(cp|mv|rm|mkdir|touch|tee|patch)\b") +BUILD_PHASES = {"base": "build_base", "env": "build_env", "instances": "build_instance"} + + +def build_score( + paths: SessionPaths, + num_actions: int, + max_interactions: int, + instance_id: str, + harness_data: Any | None = None, +) -> SessionScore: + score = SessionScore(score=0.0, success=False, instance_id=instance_id) + score.agent = _parse_agent(paths) + score.patch = _parse_patch(paths) + score.container, score.evaluation = _parse_harness_files(paths, score.patch) + if harness_data: + _apply_harness_data(score, harness_data, instance_id) + score.summary = _build_summary(score, num_actions, max_interactions) + score.score = score.summary["score"] + return score + + +def _parse_agent(paths: SessionPaths) -> dict[str, Any]: + log_path = paths.benchmark_dir / "session.log" + if not log_path.exists(): + return {"commands": None, "edit_commands": None, "call_submit": False} + content = log_path.read_text() + commands = re.findall(r"\| command: (.+?)(?=\n(?:INFO|ERROR|\[LiteLM\])|$)", content, re.DOTALL) + edit_cmds = [c for c in commands if _is_edit_cmd(c)] + return { + "commands": commands or None, + "edit_commands": edit_cmds or None, + "call_submit": bool(re.search(r"submit_patch\s*\| summary:", content)), + } + + +def _parse_patch(paths: SessionPaths) -> dict[str, Any]: + pred_path = paths.benchmark_dir / "predictions.jsonl" + _none = {"generated": None, "length": None, "structurally_valid": None} + if not pred_path.exists(): + return _none + try: + lines = pred_path.read_text().splitlines() + if not lines: + return _none + pred = json.loads(lines[0]) + patch_len = len(pred.get("model_patch", "")) + return { + "generated": patch_len > 0, + "length": patch_len, + "structurally_valid": pred.get("patch_valid", False), + } + except (json.JSONDecodeError, IndexError): + return _none + + +def _parse_harness_files(paths: SessionPaths, patch: dict) -> tuple: + container = { + "required": None, + "build_base": None, + "build_env": None, + "build_instance": None, + "started": None, + "patch_exists": None, + "applying_patch": None, + "patch_applied": None, + "removed": None, + } + evaluation = {"grading": None, "resolved": None, "test_results": {}} + + container["required"] = bool(patch.get("generated") and patch.get("structurally_valid")) + if not container["required"]: + return container, evaluation + + for path in paths.benchmark_dir.rglob("*"): + if path.name == "build_image.log" and "build_images" in str(path): + try: + phase = path.parts[-3] + if phase in BUILD_PHASES: + container[BUILD_PHASES[phase]] = path.read_text().strip().endswith("Image built successfully!") + except IndexError: + pass + + elif path.name == "run_instance.log" and "run_evaluation" in str(path): + content = path.read_text() + if re.search(r"Container .* started", content): + container["started"] = True + if re.search(r"Intermediate patch for .* written to logs", content): + container["patch_exists"] = True + if re.search(r"now applying to container", content): + container["applying_patch"] = True + if re.search(r"Patch Apply Failed", content): + container["patch_applied"] = False + if re.search(r"Grading answer for .*", content): + evaluation["grading"] = True + if re.search(r"Container .* removed\.", content): + container["removed"] = True + if container.get("applying_patch") and container.get("patch_applied") is None: + container["patch_applied"] = True + + elif path.name == "report.json": + try: + report = json.loads(path.read_text()) + if len(report) == 1: + data = next(iter(report.values())) + container["patch_exists"] = data["patch_exists"] + container["patch_applied"] = data["patch_successfully_applied"] + evaluation["resolved"] = data["resolved"] + for case, cd in data.get("tests_status", {}).items(): + s, f = len(cd.get("success", [])), len(cd.get("failure", [])) + if s + f > 0: + rate = round(100 * s / (s + f), 2) + evaluation["test_results"][case] = { + "expected": s + f, + "success": s, + "failure": f, + "rate": rate, + "display": f"expected {s + f}: success {s}, failure {f}. success rate: {rate}%", + } + except json.JSONDecodeError: + pass + + return container, evaluation + + +def _apply_harness_data(score: SessionScore, hd: Any, instance_id: str): + if hd.harness_report: + submitted = set(hd.harness_report.get("submitted_ids", [])) + if submitted != {instance_id}: + raise ValueError(f"Instance ID mismatch in harness report: expected {{'{instance_id}'}}, got {submitted}") + if hd.harness_report.get("resolved_instances", 0) > 0: + score.evaluation["resolved"] = True + score.success = hd.error is None + + +def _build_summary(score: SessionScore, num_actions: int, max_interactions: int) -> dict[str, Any]: + c, e, a, p = score.container, score.evaluation, score.agent, score.patch + f2p = e.get("test_results", {}).get("FAIL_TO_PASS", {}) + p2p = e.get("test_results", {}).get("PASS_TO_PASS", {}) + final = int(e.get("resolved") or False) + return { + "num_actions": num_actions, + "actions_limit": max_interactions, + "num_edit_commands": len(a.get("edit_commands") or []), + "agent_call_submit": 1 if a.get("call_submit") else 0, + "patch_non_empty": 1 if p.get("generated") else 0, + "container_status": -1 if not c.get("required") else (1 if c.get("started") else 0), + "fail_to_pass_rate": f2p.get("rate") if isinstance(f2p, dict) else None, + "pass_to_pass_rate": p2p.get("rate") if isinstance(p2p, dict) else None, + "score": final, + } + + +def _is_edit_cmd(cmd: str) -> bool: + c = cmd.lower() + return ( + ("python" in c and any(p in c for p in ["write_text", "write_bytes", ".write(", "replace(", "open("])) + or "git apply" in c + or "sed -i" in c + or bool(FILE_OPS_RE.search(c)) + or ">>" in c + ) + + +def write_results(results_path: Path, score: SessionScore): + results_path.parent.mkdir(parents=True, exist_ok=True) + results_path.write_text(score.model_dump_json(indent=2)) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_metrics.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_metrics.py new file mode 100644 index 00000000..3152efc8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/swebench/swebench_metrics.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json + +from ...utils.paths import RunPaths + +FUNNEL = [ + ("edit", lambda s: (s.get("num_edit_commands") or 0) > 0), + ("call_submit", lambda s: s.get("agent_call_submit") == 1), + ("non_empty_patch", lambda s: s.get("patch_non_empty") == 1), + ("container", lambda s: s.get("container_status") == 1), + ("fail_to_pass", lambda s: s.get("fail_to_pass_rate") == 100.0), + ("pass_to_pass", lambda s: s.get("pass_to_pass_rate") == 100.0), + ("score", lambda s: s.get("score") == 1), +] + + +def collect_metrics(run_id, session_ids: list[str]) -> dict: + from ...core.context import get_context + + run_paths = RunPaths(run_id=run_id, output_dir=get_context().output_dir) + summaries = [] + for sid in session_ids: + path = run_paths.session(sid).benchmark_results + if path.exists(): + summaries.append(json.loads(path.read_text()).get("summary", {})) + + return { + "funnel": {"total": len(summaries)} | {name: sum(1 for s in summaries if check(s)) for name, check in FUNNEL} + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/__init__.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/__init__.py new file mode 100644 index 00000000..4dacc76e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/__init__.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import os +from pathlib import Path + + +def _resolve_tau2_data_dir() -> str: + """Return the tau2 data directory path. + + Checks the cache directory first (populated by ``setup.sh``), then falls + back to the legacy ``installation/`` path for backwards compatibility. + """ + from ...environment.instance import get_manager + + cache_data = get_manager().env_path("benchmarks/tau2") + if cache_data.is_dir(): + return str(cache_data) + + # Legacy path (pre-setup.sh installs that cloned into the package tree) + legacy = Path(__file__).resolve().parent / "installation" / "tau2-bench" / "data" + return str(legacy) + + +def get_tau2_data_dir() -> str: + """Return the tau2 data directory, resolving lazily on first call.""" + if "TAU2_DATA_DIR" not in os.environ: + os.environ["TAU2_DATA_DIR"] = _resolve_tau2_data_dir() + return os.environ["TAU2_DATA_DIR"] diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/requirements.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/requirements.txt new file mode 100644 index 00000000..ec51ca5d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/requirements.txt @@ -0,0 +1 @@ +tau2 @ git+https://github.com/sierra-research/tau2-bench.git@v0.1.3 diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/setup.sh b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/setup.sh new file mode 100644 index 00000000..e53e4cbf --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/setup.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. +set -euo pipefail + +TAU2_REPO="https://github.com/sierra-research/tau2-bench.git" +TAU2_REF="v0.1.3" +if [ -d "tau2/domains" ]; then + echo "tau2 data already present — skipping download." + exit 0 +fi + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +echo "Cloning tau2-bench data files..." +git clone --depth 1 --branch "$TAU2_REF" --filter=blob:none --sparse "$TAU2_REPO" "$TMPDIR/tau2-bench" +cd "$TMPDIR/tau2-bench" +git sparse-checkout set data +cd - >/dev/null 2>&1 + +cp -r "$TMPDIR/tau2-bench/data/." "./" + +echo "tau2 data installed to env_dir" diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/system-deps.txt b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/system-deps.txt new file mode 100644 index 00000000..5664e303 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/system-deps.txt @@ -0,0 +1 @@ +git diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_benchmark.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_benchmark.py new file mode 100644 index 00000000..65415583 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_benchmark.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""TAU2 benchmark adapter — light benchmark class only. + +Evaluator, session, and proxy-agent classes live in ``tau2_eval.py`` +and are loaded inside the runner subprocess via ``_get_evaluator_class()`` +and ``_get_session_class()``. This file must remain importable without +the ``tau2`` package installed. +""" + +from __future__ import annotations + +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel, ConfigDict + +from ...core import Benchmark + + +class TAU2Benchmark(Benchmark, BaseModel): + display_name: ClassVar[str] = "Tau Bench 2" + slug_name: ClassVar[str] = "tau2" + available_subsets: ClassVar[list[str]] = ["mock", "retail", "airline", "telecom"] + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + @classmethod + def _get_evaluator_class(cls): + return "exgentic.benchmarks.tau2.tau2_eval:TAU2Evaluator" + + @classmethod + def _get_session_class(cls): + return "exgentic.benchmarks.tau2.tau2_eval:TAU2Session" + + subset: Literal["mock", "retail", "airline", "telecom"] = "retail" + user_simulator_model: str = "openai/Azure/gpt-4.1" + llm_temperature_user: float = 0.0 + llm_user_input_cost_per_token: float | None = None + llm_user_output_cost_per_token: float | None = None + max_steps: int = 200 + max_errors: int = 10 + num_trials: int = 1 + score_path: str | None = None + + def list_subsets(self) -> list[str]: # type: ignore[override] + return list(self.available_subsets) + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "subset": self.subset, + "user_simulator_model": self.user_simulator_model, + "llm_temperature_user": self.llm_temperature_user, + "llm_user_input_cost_per_token": self.llm_user_input_cost_per_token, + "llm_user_output_cost_per_token": self.llm_user_output_cost_per_token, + "max_steps": self.max_steps, + "max_errors": self.max_errors, + "num_trials": self.num_trials, + "seed": self.seed, + "score_path": self.score_path, + "use_cache": self.use_cache, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_eval.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_eval.py new file mode 100644 index 00000000..75a65371 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_eval.py @@ -0,0 +1,679 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""TAU2 evaluator, session, and proxy-agent classes. + +These classes import tau2 (via tau2_shim) at module level. They are only +ever instantiated inside the isolated venv subprocess, so the heavy +``tau2`` dependency is never required in the host process. +""" + +from __future__ import annotations + +import builtins +import contextvars +import json +import logging +import os +import threading +import traceback +from pathlib import Path +from shutil import move +from typing import TYPE_CHECKING, Any + +from ...adapters.actions.chat import ChatActionContext +from ...adapters.executors.proxy import PairableProxyAgent, PairableProxySession +from ...adapters.schemas.openai import openai_tools_to_action_types +from ...core.actions import ActionsHandler +from ...core.evaluator import Evaluator +from ...core.types import ( + Action, + ActionType, + BenchmarkResults, + MessageAction, + SessionIndex, + SessionScore, + SingleAction, + SingleObservation, +) +from ...integrations.litellm.config import configure_litellm +from ...integrations.litellm.health import check_model_accessible_sync +from ...observers.logging import ( + add_loguru_file_sink, + attach_library_logger_to_handler, + close_logger, + get_logger, + remove_loguru_sink, + restore_library_logger, +) +from ...utils.cost import CostReport, LiteLLMCostReport, UpdatableCostReport +from ...utils.paths import get_run_id +from ...utils.settings import get_settings +from .tau2_shim import ( + AssistantMessage, + Console, + ConsoleDisplay, + LLMAgent, + MultiToolMessage, + Results, + RunConfig, + TerminationReason, + Tool, + ToolCall, + ToolMessage, + UserMessage, + compute_metrics, + is_successful, + load_tasks, + registry, + run_domain, +) + +if TYPE_CHECKING: + pass + +# Resolve settings once per module +settings = get_settings() +logger = get_logger(__name__) + + +def current_run_id() -> str: + return get_run_id() + + +PROXY_AGENT_NAME = "proxy_agent" +TAU2_TOTAL_TASKS = { + "mock": 9, + "retail": 114, + "airline": 50, + "telecom": 114, +} + + +def _echo_action(action: SingleAction) -> SingleAction: + """Return the action unchanged so registry can normalize/validate without altering behavior.""" + return action + + +def tau_message_to_user_tool_message(message: Any) -> dict[str, Any]: + """Convert Tau2 message objects into a generic chat-style payload.""" + if isinstance(message, UserMessage): + return {"role": "user", "content": message.content} + if isinstance(message, ToolMessage): + return { + "role": "tool", + "tool_call_id": str(message.id), + "content": message.content, + } + if isinstance(message, MultiToolMessage): + return [{"role": "tool", "tool_call_id": str(m.id), "content": m.content} for m in message.tool_messages] + return {"content": str(message)} + + +def assistant_message_to_tau_message(msg_dict: dict[str, Any]) -> AssistantMessage: + """Convert a generic assistant message dict into a Tau2 AssistantMessage.""" + tool_calls: list[ToolCall] | None = None + if "tool_calls" in msg_dict: + tool_calls = [ToolCall(**p) for p in msg_dict["tool_calls"]] + return AssistantMessage(role="assistant", content=msg_dict.get("content"), tool_calls=tool_calls) + + +class TAU2Session(PairableProxySession): + """Proxy session whose score reads a single-task results file (runs remote).""" + + def __init__( + self, + run_config: RunConfig, + output_dir: str, + use_cache: bool, + session_id: str | None = None, + ): + if session_id is not None: + self._session_id = session_id + # Prepare config first so base Session.__init__ can persist it. + if isinstance(run_config, dict): + run_config = RunConfig(**run_config) + self._cfg = run_config + self.use_cache = use_cache + self._cfg.save_to = self.session_id + + self.tools: list[Tool] = [] + self.domain_policy: str = "" + self._registry: ActionsHandler | None = None + self.file_path: str | None = None + self._runner_thread: threading.Thread | None = None + self._runner_error: Exception | None = None + self._chat_ctx = ChatActionContext() + self._user_input_tokens = 0 + self._user_output_tokens = 0 + self._user_total_cost = 0.0 + + self._registry = ActionsHandler( + logger=self.logger, + warn_on_validation_error=False, + warn_on_unknown_action=False, + handle_validation_error=lambda action, _msg: SingleObservation(invoking_actions=[action], result=action), + ) + self._registry.add_action( + name="message", + description="Send a message to the user.", + action_cls=MessageAction, + handler=_echo_action, + is_message=True, + ) + + # Load TAU2 environment tools to expose actions + environment_constructor = registry.get_env_constructor(self._cfg.domain) + environment = environment_constructor() + self.domain_policy = environment.get_policy() + tools = environment.get_tools() + openai_tools = [t.openai_schema for t in tools] + action_types = openai_tools_to_action_types(openai_tools) + self._registry.add_actions(action_types, _echo_action) + + # Persist config/manifest after actions/context are initialized + super().__init__() + self.logger.debug(f"Init session PID:{os.getpid()}") + + with open(self.paths.benchmark_task, "w", encoding="utf-8") as f: + payload = self.task # pydantic RunConfig + json.dump(payload, f, ensure_ascii=False, indent=2) + + with open(self.paths.benchmark_context, "w", encoding="utf-8") as f: + payload = self.context # pydantic RunConfig + json.dump(payload, f, ensure_ascii=False, indent=2) + + from . import get_tau2_data_dir + + base = Path(get_tau2_data_dir()) / "simulations" + base.mkdir(parents=True, exist_ok=True) + self.file_path = str((base / f"{self._cfg.save_to}.json").resolve()) + self.results_file = self.paths.benchmark_results + + # Check user simulator model accessibility before starting Tau2 runner + check_model_accessible_sync(self._cfg.llm_user, logger=self.logger) + + # Start Tau2 runner + self.logger.debug("Staging for pairing") + self.stage_for_pairing() + self.logger.debug("Staged OK") + + def _runner(): + self.logger.debug(f"Runner started PID:{os.getpid()}") + agent_name = self._cfg.agent + if agent_name not in registry.get_agents(): + registry.register_agent(TAU2ProxyAgent, agent_name) + # Prepare session log path and redirect Tau2 console + prints + log_fh = open(self.paths.benchmark_dir / "tau2_session.log", "a", encoding="utf-8") + prev_console = ConsoleDisplay.console + prev_print = builtins.print + prev_input = builtins.input + tau2_logger_state = None + loguru_sink_id = None + + for handler in self.logger.handlers: + if isinstance(handler, logging.FileHandler): + ConsoleDisplay.console = Console( + file=log_fh, + force_terminal=False, + color_system=None, + highlight=False, + ) + tau2_logger_state = attach_library_logger_to_handler("tau2", handler) + # Also route Loguru logs to the Tau2 session file if Loguru is used by Tau2 + loguru_sink_id = add_loguru_file_sink(log_fh, level="DEBUG", colorize=False) + break + + def _file_print(*args, **kwargs): + if "file" not in kwargs: + kwargs["file"] = log_fh + return prev_print(*args, **kwargs) + + builtins.print = _file_print + + # Prevent interactive prompts from causing EOFError in non-interactive runs + def _no_input(*args, **kwargs): + return "" + + builtins.input = _no_input + try: + if self.file_path and Path(self.file_path).exists(): + self.logger.info( + "Removing existing TAU2 simulation file before run: %s", + self.file_path, + ) + Path(self.file_path).unlink() + self.logger.info("Starting TAU2 run domain") + run_domain(self._cfg) + self.logger.info("TAU2 run completed") + except Exception as e: + self.logger.error(f"TAU2 run FAILED with Exception: {e}") + trace_str = traceback.format_exc() + self.logger.error(trace_str) + self._runner_error = e + finally: + builtins.print = prev_print + builtins.input = prev_input + ConsoleDisplay.console = prev_console + # Restore tau2 logger handlers + if tau2_logger_state is not None: + ( + tau2_logger, + prev_tau2_handlers, + prev_tau2_propagate, + ) = tau2_logger_state + restore_library_logger(tau2_logger, prev_tau2_handlers, prev_tau2_propagate) + remove_loguru_sink(loguru_sink_id) + log_fh.flush() + log_fh.close() + # Ensure any waiting session.step() unblocks when Tau2 run completes + self.logger.debug("Sending terminal observation to unblock session.step()") + self.put_observation(None) + # If session ended before pairing with agent, release the pairing semaphore + # to avoid deadlocks + self.unstage_for_pairing() + self.logger.debug("TAU2 runner thread finishing") + + # Copy the parent's contextvars so the daemon thread inherits + # the exgentic Context (run_id, session_id, output_dir, etc.). + ctx_copy = contextvars.copy_context() + t = threading.Thread(target=ctx_copy.run, args=(_runner,), daemon=True) + t.start() + self._runner_thread = t + + def get_config(self) -> dict[str, Any]: + return self._cfg.model_dump() + + # Proxy -> Exgentic observation mapping + def update_message(self, message: Any) -> None: + if isinstance(message, UserMessage): + usage = message.usage or {} + self._user_input_tokens += usage.get("prompt_tokens", 0) + self._user_output_tokens += usage.get("completion_tokens", 0) + if message.cost is not None: + self._user_total_cost += float(message.cost) + payload = tau_message_to_user_tool_message(message) + obs = self._chat_ctx.message_to_observation(payload) + self.put_observation(obs) + + @property + def task(self) -> str: + return ( + "You are a customer service agent that helps the" + " user according to the provided below." + " Try to be helpful and always follow the policy." + ) + + @property + def context(self) -> dict[str, Any]: + return {"policy": self.domain_policy} + + @property + def actions(self) -> list[ActionType]: + return self._registry.actions + + @property + def task_id(self) -> str: + if self._cfg.task_ids: + return str(self._cfg.task_ids[0]) + return "" + + def close(self): + self.logger.debug("Closing session") + try: + super().close() # This sets self.completed = True + t = self._runner_thread + self.logger.debug(f"Thread state: alive={t.is_alive() if t else 'None'}") + if t and t.is_alive(): + self.logger.debug("Waiting for runner thread") + t.join(timeout=10.0) + if t.is_alive(): + self.logger.warning("Runner thread did not exit cleanly, continuing anyway") + self.logger.debug("Thread join completed") + if not Path(self.results_file).exists(): + if self.file_path and Path(self.file_path).exists(): + self.logger.debug("Moving results file") + move(self.file_path, self.results_file) + self.logger.debug("File move completed") + else: + self.logger.error("Results file not found") + raise FileNotFoundError( + f"TAU2 results file not found for session {self.session_id}: {self.file_path}" + ) + + self.logger.debug("Closing logging") + if not t or not t.is_alive(): + close_logger(self.logger) + finally: + # Surface runner thread error after cleanup to avoid leaking resources + if self._runner_error is not None: + raise RuntimeError( + f"TAU2 runner thread failed with error: {self._runner_error}" + ) from self._runner_error + + def get_cost(self) -> CostReport: + def _custom_token_cost(input_tokens: int, output_tokens: int) -> float | None: + args = self._cfg.llm_args_user or {} + input_rate = args.get("input_cost_per_token") + output_rate = args.get("output_cost_per_token") + if input_rate is None and output_rate is None: + return None + input_cost = input_tokens * float(input_rate or 0.0) + output_cost = output_tokens * float(output_rate or 0.0) + return input_cost + output_cost + + def _report_from_usage(input_tokens: int, output_tokens: int) -> CostReport | None: + if input_tokens == 0 and output_tokens == 0: + return None + custom_total = _custom_token_cost(input_tokens, output_tokens) + if custom_total is not None: + report = UpdatableCostReport.initialize_empty(model_name=self._cfg.llm_user) + report.add_cost(custom_total) + return report + return LiteLLMCostReport.from_token_counts( + model_name=self._cfg.llm_user, + input_tokens=input_tokens, + output_tokens=output_tokens, + ) + + def _report_from_messages(messages: list[Any]) -> CostReport | None: + total_cost = 0.0 + has_cost = False + input_tokens = 0 + output_tokens = 0 + for message in messages: + if message.role != "user": + continue + usage = message.usage or {} + input_tokens += usage.get("prompt_tokens", 0) + output_tokens += usage.get("completion_tokens", 0) + if message.cost is not None: + total_cost += float(message.cost) + has_cost = True + if has_cost and total_cost > 0: + report = UpdatableCostReport.initialize_empty(model_name=self._cfg.llm_user) + report.add_cost(total_cost) + return report + return _report_from_usage(input_tokens, output_tokens) + + def _load_results(path: str | None) -> Results | None: + if not path: + return None + results_path = Path(path) + if not results_path.exists(): + return None + try: + return Results.load(path) + except Exception: + return None + + res = _load_results(self.results_file) + if res is None: + res = _load_results(self.file_path) + if res is not None and res.simulations: + sim = res.simulations[-1] + report = _report_from_messages(sim.messages) + if report is not None: + return report + + if self._user_total_cost > 0: + report = UpdatableCostReport.initialize_empty(model_name=self._cfg.llm_user) + report.add_cost(self._user_total_cost) + return report + + report = _report_from_usage(self._user_input_tokens, self._user_output_tokens) + if report is not None: + return report + + return LiteLLMCostReport.initialize_empty(model_name=self._cfg.llm_user) + + def score(self) -> SessionScore: + # Check if the runner thread encountered an error and surface it + if self._runner_error is not None: + raise RuntimeError(f"TAU2 runner thread failed with error: {self._runner_error}") from self._runner_error + # Ensure the results file is in place. score() may be called before + # close() by the framework, so move the tau2 simulation output now. + if not Path(self.results_file).exists(): + t = self._runner_thread + if t and t.is_alive(): + t.join(timeout=30.0) + if self.file_path and Path(self.file_path).exists(): + Path(self.results_file).parent.mkdir(parents=True, exist_ok=True) + move(self.file_path, self.results_file) + res = Results.load(self.results_file) + if not res.simulations: + self.logger.error("Tau2 produced no simulations; marking session as failed.") + # Finished is false when the underlying Tau2 run produced no simulations. + return SessionScore(score=0.0, success=False, is_finished=False) + + sim = res.simulations[-1] + + self.paths.benchmark_dir.mkdir(parents=True, exist_ok=True) + with open(self.paths.benchmark_dir / "dialog.log", "w", encoding="utf-8") as f: + prev_console = ConsoleDisplay.console + ConsoleDisplay.console = Console(file=f, force_terminal=False, color_system=None) + ConsoleDisplay.display_simulation(sim) + ConsoleDisplay.console = prev_console + + self.logger.info("Computing score") + self.logger.info(f"Score: {sim.reward_info.reward}") + # Finished only when Tau2 reports an agent/user stop termination. + termination = sim.termination_reason + # Default to not finished unless Tau2 says the run ended cleanly. + graceful = False + if isinstance(termination, TerminationReason): + graceful = termination in ( + TerminationReason.AGENT_STOP, + TerminationReason.USER_STOP, + ) + elif isinstance(termination, str): + graceful = termination in ("agent_stop", "user_stop") + session_metadata: dict[str, Any] = {} + session_metrics: dict[str, Any] = {} + if sim.reward_info is not None: + session_metadata["reward_info"] = sim.reward_info.model_dump(mode="json") + session_metrics["reward"] = sim.reward_info.reward + if sim.reward_info.db_check is not None: + session_metrics["db_check_db_match"] = sim.reward_info.db_check.db_match + session_metrics["db_check_db_reward"] = sim.reward_info.db_check.db_reward + return SessionScore( + score=sim.reward_info.reward, + success=is_successful(sim.reward_info.reward), + is_finished=graceful, + session_metrics=session_metrics, + session_metadata=session_metadata, + ) + + +class TAU2ProxyAgent(LLMAgent, PairableProxyAgent[TAU2Session]): + def __init__( + self, + tools: list[Tool], + domain_policy: str, + llm: str | None = None, + llm_args: dict | None = None, + ): + sess = self.adopt_staged_session() + sess.logger.debug(f"Agent adopted PID:{os.getpid()}") + sess.tools = tools + sess.domain_policy = domain_policy + self.session = sess + configure_litellm(config=settings.to_litellm_config(), cache_only=True) + super().__init__(tools, domain_policy, llm, llm_args) + + def generate_next_message(self, message: Any, state: TAU2Session | None): + self.session.logger.info(repr(message)) + return self.handle_observation(message, state) + + def get_init_state(self, message_history: list | None = None) -> TAU2Session: # type: ignore[override] + return self.session + + # BaseProxyAgent hooks + def create_session(self, first_observation: Any) -> TAU2Session: + return self.session + + def update_session_observation(self, session: TAU2Session, observation: Any) -> None: + session.update_message(observation) + + def action_to_response(self, action: Any | None, observation: Any, session: TAU2Session): + if action is None: + message = ( + AssistantMessage(role="assistant", content="__done__", tool_calls=None), + session, + ) + return message + + actions = self._expand_actions(action, session) + + msg_dict = session._chat_ctx.actions_to_assistant_message(actions) # type: ignore[attr-defined] + message = assistant_message_to_tau_message(msg_dict) + session.logger.info(repr(message)) + return (message, session) + + @classmethod + def is_stop(cls, message: AssistantMessage) -> bool: + """Check if the message is a stop message. + + By default the agent does not stop. + """ + return message.content == "__done__" + + # Registry helpers ------------------------------------------------------- + def _expand_actions(self, action: Action, session: TAU2Session) -> list[SingleAction]: + """Normalize raw Action into a list of SingleAction via registry.""" + registry = session._registry # type: ignore[attr-defined] + expanded: list[SingleAction] = [] + for raw in action.to_action_list(): + # Handlers are no-ops; this call just normalizes/validates the action shape. + obs = registry.normalize(raw) + if obs is None: + continue + for so in obs.to_observation_list(): + res = so.result + if isinstance(res, SingleAction): + expanded.append(res) + elif so.invoking_actions: + expanded.extend(so.invoking_actions) + elif isinstance(res, Action): + expanded.append(res) # type: ignore[arg-type] + return expanded + + +class TAU2Evaluator(Evaluator): + """Evaluation logic for TAU2 -- task discovery, session kwargs, aggregation.""" + + def __init__( + self, + subset: str, + user_simulator_model: str, + llm_temperature_user: float, + llm_user_input_cost_per_token: float | None, + llm_user_output_cost_per_token: float | None, + max_steps: int, + max_errors: int, + num_trials: int, + seed: int, + score_path: str | None, + use_cache: bool, + ): + self._subset = subset + self._user_simulator_model = user_simulator_model + self._llm_temperature_user = llm_temperature_user + self._llm_user_input_cost_per_token = llm_user_input_cost_per_token + self._llm_user_output_cost_per_token = llm_user_output_cost_per_token + self._max_steps = max_steps + self._max_errors = max_errors + self._num_trials = num_trials + self._seed = seed + self._score_path = score_path + self._use_cache = use_cache + + def list_tasks(self) -> list[str]: + tasks = load_tasks(task_set_name=self._subset) + return [str(t.id) for t in tasks] + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + task_id = index.task_id + + cfg = RunConfig( + domain=self._subset, + user="user_simulator", + task_set_name=None, + task_ids=[str(task_id)], + num_tasks=1, + agent=PROXY_AGENT_NAME, + llm_agent="unknown", + llm_args_agent={}, + llm_user=self._user_simulator_model, + llm_args_user={ + "temperature": self._llm_temperature_user, + "caching": settings.litellm_caching, + }, + num_trials=self._num_trials, + max_steps=self._max_steps, + max_errors=self._max_errors, + seed=self._seed, + log_level=settings.log_level, + max_concurrency=1, + is_remote=False, + save_to=None, # Will be overridden by TauSession. + ) + if self._llm_user_input_cost_per_token is not None: + cfg.llm_args_user["input_cost_per_token"] = self._llm_user_input_cost_per_token + if self._llm_user_output_cost_per_token is not None: + cfg.llm_args_user["output_cost_per_token"] = self._llm_user_output_cost_per_token + + return { + "run_config": cfg.model_dump(), + "output_dir": settings.output_dir, + "use_cache": self._use_cache, + "session_id": index.session_id, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + """Aggregate per-session Tau2 result files and expose a final score. + + - Computes Tau2 metrics via ``compute_metrics`` for detailed reporting. + - Derives a top-level ``score`` as the mean per-session reward to provide + a single scalar suitable for tracker summaries and comparisons. + """ + files: list[Path] = [] + for paths in self.get_sessions_paths(sessions): + fp = paths.benchmark_results + if not fp.exists(): + raise FileNotFoundError(f"Missing results for planned session '{paths.session_id}' at {fp}") + files.append(fp) + + base: Results | None = None + all_sims = [] + task_map: dict[str, Any] = {} + errored_tasks = 0 + for fp in files: + r = Results.load(fp) + if base is None: + base = r + assert len(r.simulations) <= 1 # At most one simulation per file. + assert len(r.tasks) == 1 + + if len(r.simulations) == 0: + errored_tasks += 1 + continue + + all_sims.extend(r.simulations) + for t in r.tasks: + task_map[t.id] = t + + total_sessions = len(sessions) + + # Minimal path: assume at least one simulation was produced for each planned session + assert len(all_sims) > 0 + assert base is not None + combined = Results(info=base.info, tasks=list(task_map.values()), simulations=all_sims) + m = compute_metrics(combined) + + return BenchmarkResults( + benchmark_name=f"tau2-{self._subset}", + total_tasks=total_sessions, + score=m.avg_reward, + metrics=m.as_dict(), + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_shim.py b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_shim.py new file mode 100644 index 00000000..f1a5acc8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/benchmarks/tau2/tau2_shim.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tau2 import shim that centralizes logging configuration. + +This module suppresses Tau2's default console logging at import time and +provides re-exports for Tau2 symbols used by Exgentic. It also disables +propagation on the stdlib logger for Tau2 so library logs don't bubble to +the application's console. Session code can add file sinks as needed. +""" + +from __future__ import annotations + +import logging + +# 1) Quiet stdlib logging for the 'tau2' namespace +_tau2_logger = logging.getLogger("tau2") +if not _tau2_logger.handlers: + _tau2_logger.addHandler(logging.NullHandler()) +_tau2_logger.propagate = False + +# 2) Quiet Loguru's default console sink if Loguru is present +try: + from loguru import logger as _loguru + + # Remove all default sinks to avoid console output. Session code will add + # a file sink per session when needed. + try: + _loguru.remove() + except Exception: + pass +except Exception: + _loguru = None # type: ignore + +# 3) Resolve TAU2_DATA_DIR *before* importing tau2 so that +# tau2.utils.utils.DATA_DIR picks up the correct path at import time. +from . import get_tau2_data_dir # noqa: E402 + +get_tau2_data_dir() + +# 4) Re-export Tau2 modules used by Exgentic +from rich.console import Console # noqa: E402 +from tau2.agent.llm_agent import LLMAgent # noqa: E402 +from tau2.data_model.message import ( # noqa: E402 + AssistantMessage, + MultiToolMessage, + ToolCall, + ToolMessage, + UserMessage, +) +from tau2.data_model.simulation import Results, RunConfig, TerminationReason # noqa: E402 +from tau2.environment.tool import Tool # noqa: E402 +from tau2.metrics.agent_metrics import compute_metrics, is_successful # noqa: E402 +from tau2.registry import registry # noqa: E402 +from tau2.run import load_tasks, run_domain # noqa: E402 +from tau2.utils.display import ConsoleDisplay # noqa: E402 + +# Tau2's llm_utils disables LiteLLM cache by default; re-enable Exgentic cache here. +from ...integrations.litellm.config import configure_litellm # noqa: E402 +from ...utils.settings import get_settings # noqa: E402 + +configure_litellm(config=get_settings().to_litellm_config(), cache_only=True) + +__all__ = [ + "AssistantMessage", + "Console", + "ConsoleDisplay", + "LLMAgent", + "MultiToolMessage", + "Results", + "RunConfig", + "TerminationReason", + "Tool", + "ToolCall", + "ToolMessage", + "UserMessage", + "compute_metrics", + "is_successful", + "load_tasks", + "registry", + "run_domain", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/core/__init__.py b/labs/AgentStream/exgentic/src/exgentic/core/__init__.py new file mode 100644 index 00000000..68f74cf9 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/__init__.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .agent import Agent +from .agent_instance import AgentInstance +from .benchmark import Benchmark +from .evaluator import Evaluator +from .session import Session +from .types import ( + Action, + BenchmarkResults, + Integration, + Observation, + RunConfig, + RunPlan, + RunResults, + RunStatus, + SessionConfig, + SessionResults, + SessionScore, + SessionStatus, +) + +__all__ = [ + # Core interfaces + "Benchmark", + "Evaluator", + "Session", + "Agent", + "AgentInstance", + # Data models + "RunConfig", + "SessionResults", + "SessionConfig", + "RunPlan", + "SessionStatus", + "RunStatus", + "RunResults", + "Integration", + "Action", + "Observation", + # New typed models + "SessionScore", + "BenchmarkResults", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/core/actions.py b/labs/AgentStream/exgentic/src/exgentic/core/actions.py new file mode 100644 index 00000000..0bc4ad56 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/actions.py @@ -0,0 +1,340 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from collections import Counter +from logging import Logger +from typing import Any, Callable, Optional + +from pydantic import BaseModel, ValidationError + +from ..observers.logging import get_disabled_logger +from .types import ( + Action, + ActionType, + MultiObservation, + SingleAction, + SingleObservation, + ValidationReport, +) + +ActionHandler = Callable[[SingleAction], Optional[Any]] + + +def _parse_arguments_payload(arguments: Any) -> Any: + """Return arguments with best-effort JSON parsing for string inputs.""" + if isinstance(arguments, str): + try: + return json.loads(arguments) + except json.JSONDecodeError: + return arguments + return arguments + + +def _validation_report_error(report: ValidationReport) -> Optional[str]: + """Return a unified error string if the validation report marks the action invalid.""" + if not report.name_valid: + return report.error or "Invalid action name" + if not report.args_valid or not report.valid: + return report.error or "Invalid arguments" + return None + + +def build_action(action_type: ActionType, arguments: Any, *, action_id: Optional[str] = None) -> SingleAction: + """Best-effort construction of a SingleAction with validity flag and optional ID.""" + parsed_args = _parse_arguments_payload(arguments) + + data: dict[str, Any] = {"name": action_type.name, "arguments": parsed_args} + if action_id is not None: + data["id"] = action_id + + report = ValidationReport() + try: + action = action_type.cls.model_validate(data) + except ValidationError as exc: + report.valid = False + report.args_valid = False + report.error = format_validation_errors(exc) + report.details = {"errors": exc.errors()} + args_cls = action_type.arguments + if isinstance(parsed_args, dict) and isinstance(args_cls, type) and issubclass(args_cls, BaseModel): + try: + parsed_args = args_cls.model_validate(parsed_args) + data["arguments"] = parsed_args + except ValidationError: + try: + parsed_args = args_cls.model_construct(**parsed_args) + data["arguments"] = parsed_args + except Exception: + pass + action = action_type.cls.model_construct(**data) + + # Attach validation metadata on the action instance + try: + action.validation = report # type: ignore[attr-defined] + except Exception: + object.__setattr__(action, "validation", report) + return action + + +def build_unknown_action(name: str, arguments: Any = None, *, action_id: Optional[str] = None) -> SingleAction: + """Construct a best-effort unknown action marked invalid for name lookup paths.""" + parsed_args = _parse_arguments_payload(arguments) + + report = ValidationReport( + valid=False, + name_valid=False, + args_valid=True, + error="Unknown action", + details={"reason": "unknown_action"}, + ) + payload: dict[str, Any] = { + "name": name, + "arguments": parsed_args if parsed_args is not None else {}, + "validation": report, + } + if action_id is not None: + payload["id"] = action_id + # model_construct to avoid BaseModel validation on arbitrary arguments types + return SingleAction.model_construct(**payload) + + +def format_validation_errors(error: ValidationError) -> str: + """Format pydantic validation errors into a compact human-readable string.""" + result = "" + for err in error.errors(): + loc = ".".join(str(x) for x in err.get("loc", ()) if x is not Ellipsis) or "value" + msg = err.get("msg", "Invalid value") + input_value = err.get("input", None) + input_type = type(input_value).__name__ + + if err.get("type") == "missing": + result += f"The field '{loc}' is required but was not provided." + else: + result += f"Field '{loc}': {msg} " f"(received {input_value!r} of type {input_type})." + return result + + +def extract_argument(arguments: Any, field_name: str, default: Any = None) -> Any: + """Best-effort extraction for a field from BaseModel/dict-like payloads.""" + if isinstance(arguments, BaseModel): + return arguments.model_dump().get(field_name, default) + if isinstance(arguments, dict): + return arguments.get(field_name, default) + return default + + +class ActionsHandler: + """Central handler/registry for available actions and their handlers.""" + + def __init__( + self, + logger: Optional[Logger] = None, + *, + warn_on_validation_error: bool = True, + warn_on_unknown_action: bool = True, + handle_validation_error: Optional[Callable[[SingleAction, str], Optional[SingleObservation]]] = None, + handle_unknown_action: Optional[Callable[[SingleAction], Optional[SingleObservation]]] = None, + ): + if warn_on_unknown_action and handle_unknown_action is not None: + raise ValueError("Cannot both warn and custom-handle unknown actions; set warn_on_unknown_action=False") + if warn_on_validation_error and handle_validation_error is not None: + raise ValueError("Cannot both warn and custom-handle validation errors; set warn_on_validation_error=False") + self._logger = logger or get_disabled_logger() + self._actions: dict[str, ActionType] = {} + self._handlers: dict[str, ActionHandler] = {} + self._warn_on_validation_error = warn_on_validation_error + self._warn_on_unknown_action = warn_on_unknown_action + self._handle_validation_error = handle_validation_error + self._handle_unknown_action = handle_unknown_action or self._default_unknown_action + self._stats: Counter[str] = Counter() + + # Registration ---------------------------------------------------------------- + def add_action( + self, + name: str, + description: str, + action_cls: type[SingleAction], + handler: ActionHandler, + *, + is_finish: bool = False, + is_message: bool = False, + is_hidden: bool = False, + ) -> ActionType: + """Register a new action by specifying its parts; the ActionType is constructed internally.""" + action = ActionType( + name=name, + description=description, + cls=action_cls, + is_finish=is_finish, + is_message=is_message, + is_hidden=is_hidden, + ) + self.add_action_type(action, handler) + return action + + def add_action_type(self, action: ActionType, handler: ActionHandler) -> None: + """Register an already-constructed ActionType.""" + if not isinstance(action, ActionType): + raise ValueError("action must be an ActionType") + self._store_action(action, handler) + + def add_actions(self, actions: list[ActionType], handler: ActionHandler) -> None: + for action in actions: + self.add_action_type(action, handler) + + # Accessors ------------------------------------------------------------------- + @property + def actions(self) -> list[ActionType]: + all_actions = list(self._actions.values()) + return list(filter(lambda action: not action.is_hidden, all_actions)) + + def normalize( + self, + action: Optional[Action], + ) -> Optional[SingleObservation | MultiObservation]: + """Alias for execute() to emphasize validation/normalization use-cases.""" + return self.execute(action) + + # Execution ------------------------------------------------------------------- + def execute( + self, + action: Optional[Action], + ) -> Optional[SingleObservation | MultiObservation]: + """Execute user-supplied action(s) through registered handlers and return a merged observation.""" + if action is None: + return None + + observations: list[SingleObservation] = [] + + for single_action in action.to_action_list(): + outcome = self._execute_single(single_action) + if outcome is not None: + observations.append(outcome) + + if not observations: + return None + if len(observations) == 1: + return observations[0] + return MultiObservation(observations=observations) + + def _execute_single(self, action: SingleAction) -> Optional[SingleObservation]: + handler = self._handlers.get(action.name) + + if handler is None: + self._logger.error(f"Unknown action requested: {action.name}") + self._record_error("unknown_action") + return self._normalize_handler_result(self._handle_unknown_action(action), action) + + validation_error = self._validate_arguments(action) + if validation_error: + message = f"Validation Error in {action.name}: {validation_error}" + self._logger.error(message) + self._record_error("validation_error") + if self._handle_validation_error is not None: + return self._normalize_handler_result( + self._handle_validation_error(action, validation_error), + action, + ) + if not self._warn_on_validation_error: + return None + return self._normalize_handler_result(message, action) + + try: + raw_result = handler(action) + except Exception as exc: # pragma: no cover - defensive + self._logger.exception(f"Action handler failed for {action.name}: {exc}") + self._record_error("handler_exception") + return self._normalize_handler_result( + f"Action '{action.name}' failed: {exc}", + action, + ) + + observation = self._normalize_handler_result(raw_result, action) + return observation + + def _validate_arguments(self, action: SingleAction) -> Optional[str]: + arguments = action.arguments + expected_type = self._expected_arguments_type(action) + + report: Optional[ValidationReport] = action.validation + if report: + error = _validation_report_error(report) + if error: + return error + return None + + # If arguments already a BaseModel, validate round-trip + if isinstance(arguments, BaseModel): + try: + arguments.__class__.model_validate(arguments.model_dump()) + except ValidationError as e: + return format_validation_errors(e) + return None + + # If we know the expected type and got a dict, try to validate/construct + if expected_type and issubclass(expected_type, BaseModel) and isinstance(arguments, dict): + try: + expected_type.model_validate(arguments) + except ValidationError as e: + return format_validation_errors(e) + return None + + # Unknown/invalid argument shape + if not isinstance(arguments, (BaseModel, dict)): + msg = f"Invalid arguments type: {type(arguments).__name__}" + if self._logger: + self._logger.error(msg) + self._record_error("invalid_arguments_type") + return msg + return None + + @staticmethod + def _expected_arguments_type(action: SingleAction) -> Optional[type]: + try: + field = action.__class__.model_fields.get("arguments") + if field and isinstance(field.annotation, type): + return field.annotation # type: ignore[return-value] + except Exception: + return None + return None + + def _record_error(self, key: str) -> None: + self._stats[key] += 1 + + def get_errors_stats(self) -> dict[str, int]: + return dict(self._stats) + + def _default_unknown_action(self, action: SingleAction) -> SingleObservation: + if action.name == "message": + text = "Error: Sending a message is not allowed. Please use only one of the available actions." + else: + text = f"Error: Unknown action - {action.name}" + return SingleObservation(invoking_actions=[action], result=text) + + @staticmethod + def _normalize_handler_result(raw_result: Any, action: SingleAction) -> Optional[SingleObservation]: + if raw_result is None: + return None + if isinstance(raw_result, SingleObservation): + if not raw_result.invoking_actions: + raw_result.invoking_actions = [action] + return raw_result + return SingleObservation(invoking_actions=[action], result=raw_result) + + def _store_action(self, action: ActionType, handler: ActionHandler) -> None: + self._validate_action_type(action) + self._actions[action.name] = action + self._handlers[action.name] = handler + + @staticmethod + def _validate_action_type(action: ActionType) -> None: + args_type = action.arguments + if not isinstance(args_type, type) or not issubclass(args_type, BaseModel): + raise ValueError( + "Action arguments must be a Pydantic BaseModel. " + f"Action '{action.name}' has arguments type {args_type!r}. " + "Ensure the action's 'arguments' annotation resolves to a BaseModel." + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/agent.py b/labs/AgentStream/exgentic/src/exgentic/core/agent.py new file mode 100644 index 00000000..ea6f0c1a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/agent.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, ClassVar + +from pydantic import BaseModel, ConfigDict + +from ..utils.settings import RunnerName +from .runner_mixin import RunnerMixin +from .types.model_settings import ModelSettings + +if TYPE_CHECKING: + from .agent_instance import AgentInstance + + +class Agent(BaseModel, RunnerMixin, ABC): + """Agent configuration — lightweight config that lives on the host. + + Callers use ``get_instance(session_id)`` to obtain a running + ``AgentInstance`` wrapped in the configured runner, mirroring + ``Benchmark.get_evaluator()`` and ``Benchmark.get_session()``. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + display_name: ClassVar[str] + slug_name: ClassVar[str] + model_settings: ModelSettings | None = None + runner: RunnerName | None = None + docker_socket: bool = False + + @classmethod + @abstractmethod + def _get_instance_class(cls) -> type[AgentInstance]: + """Return the AgentInstance subclass for this agent. + + Subclasses implement this with a lazy import so heavy deps + (litellm, smolagents, …) are only loaded inside the runner. + """ + ... + + @classmethod + def _get_instance_class_ref(cls) -> str: + """Return a ``"module:qualname"`` string for the instance class. + + By default calls ``_get_instance_class()`` and converts to string. + Override in subclasses whose instance module has heavy third-party + imports to return the string directly without triggering the import. + """ + klass = cls._get_instance_class() + return f"{klass.__module__}:{klass.__qualname__}" + + @abstractmethod + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + """Return kwargs for creating the instance class. + + Task, context, and actions are passed separately via + ``AgentInstance.start()`` (through HTTP transport) to avoid + OS argument-list size limits. + """ + ... + + def get_instance(self, session_id: str) -> AgentInstance: + """Create an ``AgentInstance`` wrapped in the configured runner.""" + from ..adapters.runners import with_runner + + return with_runner( + self._get_instance_class_ref(), + runner=self.resolve_runner(), + **self._get_instance_kwargs(session_id=session_id), + **self.runner_kwargs(), + ) + + # Optional metadata property for dashboard/leaderboards + @property + def model_name(self) -> str: + return "unknown" + + def get_models_names(self) -> list[str]: + name = self.model_name + if not name or name == "unknown": + return [] + return [name] diff --git a/labs/AgentStream/exgentic/src/exgentic/core/agent_instance.py b/labs/AgentStream/exgentic/src/exgentic/core/agent_instance.py new file mode 100644 index 00000000..4bba6868 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/agent_instance.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import uuid +from abc import ABC, abstractmethod +from typing import Any, Optional + +from ..observers.logging import get_logger +from ..utils.cost import CostReport +from ..utils.paths import SessionPaths +from .types import Action, ActionType, Observation + + +class AgentInstance(ABC): + """Agent instance - handles decision making for one task execution.""" + + max_steps: int | None = None + + def __init__(self, session_id: str) -> None: + """Create a new agent bound to a specific session. + + The session id is the single source of truth for scoping all agent-side + logs and artifacts under `outputs//sessions//agent/`. + """ + self._session_id = session_id + + @property + def session_id(self) -> str: + return self._session_id + + @property + def agent_id(self) -> str: + """Generates a unique id for the agent.""" + if not hasattr(self, "_agent_id"): + self._agent_id = str(uuid.uuid4()).replace("-", "_") + return self._agent_id + + @property + def paths(self) -> SessionPaths: + """All filesystem paths for this session.""" + if not hasattr(self, "_paths"): + from .context import try_get_context + + ctx = try_get_context() + if ctx is not None: + self._paths = SessionPaths( + session_id=self.session_id, + run_id=ctx.run_id, + output_dir=ctx.output_dir, + ) + else: + self._paths = SessionPaths(session_id=self.session_id, run_id="default", output_dir="outputs") + return self._paths + + @property + def logger(self): + if not hasattr(self, "_logger"): + self._logger = get_logger(f"Agent_{self.agent_id}", str(self.paths.agent_log)) + return self._logger + + def get_cost(self) -> CostReport: + """Estimated monetary cost; default 0.0.""" + return CostReport.initialize_empty() + + @abstractmethod + def react(self, observation: Optional[Observation]) -> Optional[Action]: + """React to observation - agent controls decision making, None = done.""" + pass + + def start(self, task: str, context: dict[str, Any], actions: list[ActionType]): + """Receive the work payload and start the agent. + + Called via HTTP transport after the instance is constructed, so + large payloads (e.g. dozens of ActionTypes) are never serialized + as CLI arguments. + """ + self.task = task + self.context = context or {} + self.actions = actions + + @abstractmethod + def close(self) -> None: + """Cleanup agent resources - agent manages its own state.""" + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/core/benchmark.py b/labs/AgentStream/exgentic/src/exgentic/core/benchmark.py new file mode 100644 index 00000000..24390120 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/benchmark.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, ConfigDict + +from ..utils.settings import RunnerName +from .runner_mixin import RunnerMixin + +if TYPE_CHECKING: + from .evaluator import Evaluator + from .session import Session + + +class Benchmark(BaseModel, RunnerMixin, ABC): + """Benchmark configuration — lightweight config that lives on the host. + + Callers use ``get_evaluator()`` and ``get_session()`` to obtain + instances wrapped in the configured runner for container isolation. + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_by_name=True, + validate_by_alias=True, + ) + + subset: str | None = None + seed: int = 300 + runner: RunnerName | None = None + use_cache: bool = True + max_interactions: int | None = 150 + docker_socket: bool = False + + @property + def subset_name(self) -> str: + """Stable subset identifier for this benchmark run.""" + return str(self.subset) if self.subset else "unknown" + + def list_subsets(self) -> list[str]: + """Return available subset identifiers for this benchmark.""" + subset = self.subset_name + return [subset] if subset and subset != "unknown" else [] + + @classmethod + def _get_evaluator_class(cls) -> type[Evaluator]: + """Return the Evaluator subclass for this benchmark. + + Subclasses implement this with a lazy import so heavy deps + are only loaded inside the runner. + """ + raise NotImplementedError + + @classmethod + def _get_session_class(cls) -> type[Session]: + """Return the Session subclass for this benchmark. + + Subclasses implement this with a lazy import so heavy deps + are only loaded inside the runner. + """ + raise NotImplementedError + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + """Return kwargs for constructing the Evaluator. + + Subclasses override this to pass benchmark-specific config. + """ + return {} + + def get_evaluator(self) -> Evaluator: + """Create an ``Evaluator`` wrapped in the configured runner.""" + from ..adapters.runners import with_runner + + return with_runner( + self._get_evaluator_class(), + runner=self.resolve_runner(), + **self._get_evaluator_kwargs(), + **self.runner_kwargs(), + ) + + def get_session(self, **session_kwargs: Any) -> Session: + """Create a ``Session`` wrapped in the configured runner.""" + from ..adapters.runners import with_runner + + return with_runner( + self._get_session_class(), + runner=self.resolve_runner(), + **session_kwargs, + **self.runner_kwargs(), + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/context.py b/labs/AgentStream/exgentic/src/exgentic/core/context.py new file mode 100644 index 00000000..e4fd6d63 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/context.py @@ -0,0 +1,335 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import contextvars +import os +import shutil +import threading +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Iterator + +from ..utils.paths import sanitize_path_component +from ..utils.settings import get_settings + +# --------------------------------------------------------------------------- +# Role enum +# --------------------------------------------------------------------------- + + +class Role(str, Enum): + FRAMEWORK = "framework" + AGENT = "agent" + BENCHMARK = "benchmark" + + +# --------------------------------------------------------------------------- +# OTEL Context dataclass +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class OtelContext: + """OpenTelemetry span context for distributed tracing.""" + + trace_id: str + span_id: str + + +# --------------------------------------------------------------------------- +# Env-var keys used for subprocess transport +# --------------------------------------------------------------------------- + +_ENV_RUN_ID = "EXGENTIC_CTX_RUN_ID" +_ENV_OUTPUT_DIR = "EXGENTIC_CTX_OUTPUT_DIR" +_ENV_CACHE_DIR = "EXGENTIC_CTX_CACHE_DIR" +_ENV_SESSION_ID = "EXGENTIC_CTX_SESSION_ID" +_ENV_TASK_ID = "EXGENTIC_CTX_TASK_ID" +_ENV_ROLE = "EXGENTIC_CTX_ROLE" +ENV_OTEL_TRACE_ID = "EXGENTIC_CTX_OTEL_TRACE_ID" +ENV_OTEL_SPAN_ID = "EXGENTIC_CTX_OTEL_SPAN_ID" +OTEL_ENABLED_ENV = "EXGENTIC_OTEL_ENABLED" + + +# --------------------------------------------------------------------------- +# Core Context dataclass +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class Context: + run_id: str + output_dir: str + cache_dir: str + session_id: str | None = None + task_id: str | None = None + role: Role = Role.FRAMEWORK + otel_context: OtelContext | None = None + + def with_session(self, session_id: str, task_id: str | None = None) -> Context: + return Context( + run_id=self.run_id, + output_dir=self.output_dir, + cache_dir=self.cache_dir, + session_id=session_id, + task_id=task_id, + role=self.role, + otel_context=self.otel_context, + ) + + def with_role(self, role: Role) -> Context: + return Context( + run_id=self.run_id, + output_dir=self.output_dir, + cache_dir=self.cache_dir, + session_id=self.session_id, + task_id=self.task_id, + role=role, + otel_context=self.otel_context, + ) + + def with_otel_context(self, otel_context: OtelContext | None) -> Context: + """Create a new Context with updated OTEL context.""" + return Context( + run_id=self.run_id, + output_dir=self.output_dir, + cache_dir=self.cache_dir, + session_id=self.session_id, + task_id=self.task_id, + role=self.role, + otel_context=otel_context, + ) + + def to_env(self) -> dict[str, str]: + env: dict[str, str] = { + _ENV_RUN_ID: self.run_id, + _ENV_OUTPUT_DIR: self.output_dir, + _ENV_CACHE_DIR: self.cache_dir, + _ENV_ROLE: self.role.value, + } + if self.session_id is not None: + env[_ENV_SESSION_ID] = self.session_id + if self.task_id is not None: + env[_ENV_TASK_ID] = self.task_id + if self.otel_context is not None: + env[ENV_OTEL_TRACE_ID] = self.otel_context.trace_id + env[ENV_OTEL_SPAN_ID] = self.otel_context.span_id + return env + + @classmethod + def from_env(cls, env: dict[str, str] | None = None) -> Context: + src = env if env is not None else os.environ + run_id = src.get(_ENV_RUN_ID, "") + if not run_id: + raise RuntimeError(f"{_ENV_RUN_ID} not set in environment.") + run_id = sanitize_path_component(run_id) + output_dir = src.get(_ENV_OUTPUT_DIR) or get_settings().output_dir + cache_dir = src.get(_ENV_CACHE_DIR) or get_settings().cache_dir + session_id = src.get(_ENV_SESSION_ID) or None + task_id = src.get(_ENV_TASK_ID) or None + role_str = src.get(_ENV_ROLE) + try: + role = Role(role_str) if role_str else Role.FRAMEWORK + except ValueError: + role = Role.FRAMEWORK + + # Read OTEL context if present + otel_context: OtelContext | None = None + trace_id = src.get(ENV_OTEL_TRACE_ID) + span_id = src.get(ENV_OTEL_SPAN_ID) + if trace_id and span_id: + otel_context = OtelContext(trace_id=trace_id, span_id=span_id) + + return cls( + run_id=run_id, + output_dir=output_dir, + cache_dir=cache_dir, + session_id=session_id, + task_id=task_id, + role=role, + otel_context=otel_context, + ) + + +# --------------------------------------------------------------------------- +# Single ContextVar — the single source of truth +# --------------------------------------------------------------------------- + +_CONTEXT: contextvars.ContextVar[Context | None] = contextvars.ContextVar( + "exgentic_context", + default=None, +) + +# Fallback for threads that don't inherit ContextVar (uvicorn thread-pool +# workers, service runner threads). Set by init_context_from_env() and +# set_context_fallback(). +_SUBPROCESS_CONTEXT: Context | None = None + +_ENV_LOCK = threading.Lock() + + +# --------------------------------------------------------------------------- +# Accessors +# --------------------------------------------------------------------------- + + +def get_context() -> Context: + """Return the current Context. Raises RuntimeError if none is set.""" + ctx = _CONTEXT.get() + if ctx is None: + ctx = _SUBPROCESS_CONTEXT + if ctx is None: + raise RuntimeError("No context set. Use run_scope() or init_context_from_env().") + return ctx + + +def try_get_context() -> Context | None: + """Return the current Context, or None if none is set.""" + ctx = _CONTEXT.get() + return ctx if ctx is not None else _SUBPROCESS_CONTEXT + + +def context_env() -> dict[str, str]: + """Return context env vars for subprocess propagation, or empty dict.""" + ctx = try_get_context() + if ctx is None: + return {} + return ctx.to_env() + + +@contextmanager +def context_env_scope() -> Iterator[None]: + """Temporarily apply context env vars to os.environ (thread-safe).""" + env = context_env() + if not env: + yield + return + with _ENV_LOCK: + prev = {k: os.environ.get(k) for k in env} + os.environ.update(env) + try: + yield + finally: + for k, v in prev.items(): + if v is None: + os.environ.pop(k, None) + else: + os.environ[k] = v + + +def set_context(ctx: Context) -> None: + """Imperatively set the current context.""" + _CONTEXT.set(ctx) + + +def set_context_fallback(ctx: Context | None) -> None: + """Set a process-wide fallback for threads that don't inherit ContextVar.""" + global _SUBPROCESS_CONTEXT + _SUBPROCESS_CONTEXT = ctx + + +# --------------------------------------------------------------------------- +# Context managers +# --------------------------------------------------------------------------- + + +@contextmanager +def run_scope( + ctx: Context | None = None, + *, + run_id: str | None = None, + output_dir: str | None = None, + cache_dir: str | None = None, + overwrite_run: bool = False, +) -> Iterator[Context]: + """Enter a run context. + + Either pass an explicit *ctx*, or pass keyword args and the Context will + be resolved from those args / env vars / settings defaults. + """ + if ctx is None: + ctx = _resolve_context(run_id, output_dir, cache_dir, overwrite_run) + token = _CONTEXT.set(ctx) + try: + yield ctx + finally: + _CONTEXT.reset(token) + + +@contextmanager +def session_scope(session_id: str, task_id: str | None = None) -> Iterator[Context]: + """Derive a session-scoped context from the current run context.""" + parent = get_context() + ctx = parent.with_session(session_id, task_id) + token = _CONTEXT.set(ctx) + try: + yield ctx + finally: + _CONTEXT.reset(token) + + +@contextmanager +def agent_scope() -> Iterator[Context]: + """Set role=AGENT for the duration of the block, restore on exit.""" + parent = get_context() + ctx = parent.with_role(Role.AGENT) + token = _CONTEXT.set(ctx) + try: + yield ctx + finally: + _CONTEXT.reset(token) + + +@contextmanager +def benchmark_scope() -> Iterator[Context]: + """Set role=BENCHMARK for the duration of the block, restore on exit.""" + parent = get_context() + ctx = parent.with_role(Role.BENCHMARK) + token = _CONTEXT.set(ctx) + try: + yield ctx + finally: + _CONTEXT.reset(token) + + +def init_context_from_env() -> Context: + """Bootstrap ContextVar from env vars (called once in subprocess / Docker).""" + global _SUBPROCESS_CONTEXT + ctx = Context.from_env() + _CONTEXT.set(ctx) + _SUBPROCESS_CONTEXT = ctx + return ctx + + +# --------------------------------------------------------------------------- +# Internals +# --------------------------------------------------------------------------- + + +def _resolve_context( + run_id: str | None, + output_dir: str | None, + cache_dir: str | None, + overwrite_run: bool, +) -> Context: + settings = get_settings() + resolved_run_id = run_id or os.environ.get(_ENV_RUN_ID) or datetime.now().isoformat().replace(":", "--") + resolved_run_id = sanitize_path_component(resolved_run_id) + resolved_output_dir = output_dir or os.environ.get(_ENV_OUTPUT_DIR) or settings.output_dir + resolved_output_dir = str(Path(resolved_output_dir).resolve()) + resolved_cache_dir = cache_dir or os.environ.get(_ENV_CACHE_DIR) or settings.cache_dir + resolved_cache_dir = str(Path(resolved_cache_dir).resolve()) + if overwrite_run: + run_root = Path(resolved_output_dir) / resolved_run_id + if run_root.exists(): + shutil.rmtree(run_root) + return Context( + run_id=resolved_run_id, + output_dir=resolved_output_dir, + cache_dir=resolved_cache_dir, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/evaluator.py b/labs/AgentStream/exgentic/src/exgentic/core/evaluator.py new file mode 100644 index 00000000..bf633060 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/evaluator.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Evaluator ABC — benchmark evaluation logic (task discovery, session config, aggregation). + +An Evaluator is created via ``Benchmark.get_evaluator()`` for container +isolation, keeping heavy dependencies off the host. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from ..utils.paths import SessionPaths, get_run_paths +from .types import BenchmarkResults, SessionIndex + + +class Evaluator(ABC): + """Benchmark evaluation logic — task discovery, session config, aggregation. + + Runs in the same isolation level as the benchmark's runner (can be containerized). + Returns only simple serializable data across the transport boundary. + """ + + @abstractmethod + def list_tasks(self) -> list[str]: + """Return available task identifiers for this benchmark.""" + ... + + @abstractmethod + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + """Return kwargs for constructing the Session for a given task. + + The orchestrator will call:: + + benchmark.get_session(**session_kwargs) + """ + ... + + @abstractmethod + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + """Aggregate results for the specified task sessions.""" + ... + + def get_sessions_paths(self, sessions: list[SessionIndex]) -> list[SessionPaths]: + """Return ``SessionPaths`` for each session index.""" + run_paths = get_run_paths() + return [run_paths.session(s.session_id) for s in sessions] + + def close(self) -> None: + """Optional cleanup hook.""" + return diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/__init__.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/__init__.py new file mode 100644 index 00000000..ee25075a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/__init__.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .controller import Controller, CoreController, LimitController +from .observer import Observer +from .run import core_aggregate, core_evaluate, core_execute +from .session import run_session +from .termination import ( + AgentError, + AgentTerminationError, + BenchmarkError, + BenchmarkTerminationError, + InvalidActionError, + InvalidObservationError, + RunCancelError, + SessionCancelError, + SessionLimitReachedError, +) +from .tracker import Tracker + +__all__ = [ + "AgentError", + "AgentTerminationError", + "BenchmarkError", + "BenchmarkTerminationError", + "Controller", + "CoreController", + "LimitController", + "InvalidActionError", + "InvalidObservationError", + "Observer", + "RunCancelError", + "Tracker", + "SessionCancelError", + "SessionLimitReachedError", + "SessionTermination", + "run_session", + "core_aggregate", + "core_execute", + "core_evaluate", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/cleanup.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/cleanup.py new file mode 100644 index 00000000..baf9627f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/cleanup.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import asyncio +import gc + + +def close_aiohttp_sessions_silently() -> None: + """Best-effort cleanup for stray aiohttp sessions to avoid resource warnings.""" + try: + import aiohttp # type: ignore + except Exception: + return + + sessions = [obj for obj in gc.get_objects() if isinstance(obj, aiohttp.ClientSession) and not obj.closed] + if not sessions: + return + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(asyncio.gather(*(s.close() for s in sessions), return_exceptions=True)) + asyncio.set_event_loop(None) + loop.close() diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/controller.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/controller.py new file mode 100644 index 00000000..a9724ba0 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/controller.py @@ -0,0 +1,114 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import threading +from dataclasses import dataclass + +from ..types import Action, Observation +from .cleanup import close_aiohttp_sessions_silently +from .termination import ( + AgentError, + AgentTerminationError, + BenchmarkError, + BenchmarkTerminationError, + InvalidActionError, + InvalidObservationError, +) + + +class Controller: + def on_run_success(self, results, run_config) -> None: + return None + + def on_run_error(self, error) -> None: + return None + + def on_react_success(self, session, action) -> None: + return None + + def on_step_success(self, session, observation) -> None: + return None + + def on_react_error(self, session, error) -> None: + return None + + def on_step_error(self, session, error) -> None: + return None + + +class CoreController(Controller): + def on_react_success(self, session, action) -> None: + if action is None: + raise BenchmarkTerminationError() + if not isinstance(action, Action): + raise AgentError(InvalidActionError(action)) + + def on_step_success(self, session, observation) -> None: + if observation is None: + raise AgentTerminationError() + if not isinstance(observation, Observation): + raise BenchmarkError(InvalidObservationError(observation)) + + +@dataclass +class _LimitState: + steps: int = 0 + actions: int = 0 + + +class LimitController(Controller): + def __init__(self, *, max_steps: int, max_actions: int) -> None: + self._max_steps = max_steps + self._max_actions = max_actions + self._lock = threading.Lock() + self._counts: dict[str, _LimitState] = {} + + def on_react_success(self, session, action) -> None: + if not isinstance(action, Action): + return + session_id = session.session_id + action_count = len(action.to_action_list()) + with self._lock: + state = self._counts.get(session_id) + if state is None: + state = _LimitState() + self._counts[session_id] = state + state.steps += 1 + state.actions += action_count + + def on_step_success(self, session, observation) -> None: + if observation is None: + return + session_id = session.session_id + with self._lock: + state = self._counts.get(session_id) + if state is None: + return + if state.steps >= self._max_steps: + from .termination import SessionLimitReachedError + + raise SessionLimitReachedError( + reason="max_steps", + max_steps=self._max_steps, + max_actions=self._max_actions, + steps=state.steps, + actions=state.actions, + ) + if state.actions >= self._max_actions: + from .termination import SessionLimitReachedError + + raise SessionLimitReachedError( + reason="max_actions", + max_steps=self._max_steps, + max_actions=self._max_actions, + steps=state.steps, + actions=state.actions, + ) + + +class CleanupController(Controller): + def on_run_success(self, results, run_config) -> None: + close_aiohttp_sessions_silently() + + def on_run_error(self, error) -> None: + close_aiohttp_sessions_silently() diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/execution.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/execution.py new file mode 100644 index 00000000..5554c2e6 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/execution.py @@ -0,0 +1,418 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import shutil +from collections import deque +from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait +from contextvars import copy_context +from pathlib import Path + +from filelock import FileLock, Timeout + +from ...interfaces.registry import load_agent, load_benchmark +from ...observers.logging import get_disabled_logger +from ...utils.paths import get_run_paths, get_session_paths +from ..types import ( + SessionConfig, + SessionExecutionStatus, + SessionIndex, + SessionOutcomeStatus, + SessionResults, + SessionStatus, +) +from .session import run_session +from .termination import RunCancelError +from .tracker import Tracker + +_BENCHMARK_CACHE: dict[str, type] = {} +_AGENT_CACHE: dict[str, type] = {} + + +def _get_benchmark_class(slug: str): + cls = _BENCHMARK_CACHE.get(slug) + if cls is None: + cls = load_benchmark(slug) + _BENCHMARK_CACHE[slug] = cls + return cls + + +def _get_agent_class(slug: str): + cls = _AGENT_CACHE.get(slug) + if cls is None: + cls = load_agent(slug) + _AGENT_CACHE[slug] = cls + return cls + + +def _try_reuse_completed( + *, + status: SessionStatus, + session_config: SessionConfig, + sess_paths, + tracker: Tracker, + log, +) -> bool: + if status.status != SessionExecutionStatus.COMPLETED: + return False + if session_config.overwrite_sessions: + return False + results_path = sess_paths.results + if not results_path.exists(): + return False + try: + payload = json.loads(results_path.read_text(encoding="utf-8")) + results = SessionResults.model_validate(payload) + tracker.on_session_reuse(results) + log.info( + "Skipping completed session %s (task=%s)", + session_config.get_session_id(), + session_config.task_id, + ) + return True + except Exception: + log.exception( + "Failed to load results for session %s (task=%s); rerunning.", + session_config.get_session_id(), + session_config.task_id, + ) + return False + + +def _cleanup_session_dir( + *, + status: SessionStatus, + session_config: SessionConfig, + sess_paths, + log, +) -> None: + if session_config.overwrite_sessions and sess_paths.root.exists(): + shutil.rmtree(sess_paths.root) + log.info( + "Overwriting existing session %s (task=%s)", + session_config.get_session_id(), + session_config.task_id, + ) + return + if status.status == SessionExecutionStatus.INCOMPLETE and sess_paths.root.exists(): + shutil.rmtree(sess_paths.root) + log.info( + "Overwriting incomplete session %s (task=%s)", + session_config.get_session_id(), + session_config.task_id, + ) + + +def _write_session_config( + *, + session_config: SessionConfig, + sess_paths, +) -> None: + config_path = sess_paths.session_config + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w", encoding="utf-8") as f: + json.dump( + session_config.model_dump(mode="json"), + f, + ensure_ascii=False, + indent=2, + ) + + +def _status_from_paths_without_lock( + *, + session_config: SessionConfig, + sess_paths, +) -> SessionStatus: + """Derive session status from filesystem without consulting lock state.""" + results_exists = sess_paths.results.exists() + session_dir_exists = sess_paths.root.exists() + result_status = None + + if results_exists: + result_status = SessionStatus._extract_result_status(sess_paths.results) + if result_status in ( + SessionOutcomeStatus.ERROR, + SessionOutcomeStatus.CANCELLED, + SessionOutcomeStatus.LIMIT_REACHED, + ): + status = SessionExecutionStatus.INCOMPLETE + else: + status = SessionExecutionStatus.COMPLETED + elif session_dir_exists: + status = SessionExecutionStatus.INCOMPLETE + else: + status = SessionExecutionStatus.MISSING + + return SessionStatus( + task_id=str(session_config.task_id), + session_id=session_config.get_session_id(), + results_path=str(sess_paths.results), + session_dir=str(sess_paths.root), + status=status, + result_status=result_status, + ) + + +def run_session_config( + *, + session_config: SessionConfig, + tracker: Tracker, +) -> None: + bench_cls = _get_benchmark_class(session_config.benchmark) + agent_cls = _get_agent_class(session_config.agent) + benchmark = bench_cls(**(session_config.benchmark_kwargs or {})) + agent = agent_cls(**(session_config.agent_kwargs or {})) + + # Create evaluator to obtain session kwargs. + evaluator = benchmark.get_evaluator() + + session_id = session_config.get_session_id() + index = SessionIndex( + task_id=str(session_config.task_id), + session_id=session_id, + ) + + try: + session_kwargs = evaluator.get_session_kwargs(index) + # Create session via runner for isolation. + session = benchmark.get_session(**session_kwargs) + # run_session handles session.close() internally. + run_session(session_config, session, agent, tracker=tracker) + finally: + try: + evaluator.close() + except Exception: + pass + try: + benchmark.close() + finally: + agent.close() + + +def _run_task_with_lock( + *, + session_config: SessionConfig, + tracker: Tracker, + log, +) -> None: + session_id = session_config.get_session_id() + sess_paths = get_session_paths(session_id) + sess_paths.root.mkdir(parents=True, exist_ok=True) + status = SessionStatus.from_config(session_config) + lock = FileLock(str(sess_paths.lock)) + try: + lock.acquire(timeout=0) + except Timeout: + log.info( + "Skipping running session %s (task=%s)", + session_id, + session_config.task_id, + ) + return + try: + # If status was sampled while another process held the lock, refresh it + # after acquiring the lock so cleanup/reuse decisions stay accurate. + if status.status == SessionExecutionStatus.RUNNING: + status = _status_from_paths_without_lock(session_config=session_config, sess_paths=sess_paths) + if _try_reuse_completed( + status=status, + session_config=session_config, + sess_paths=sess_paths, + tracker=tracker, + log=log, + ): + return + _cleanup_session_dir( + status=status, + session_config=session_config, + sess_paths=sess_paths, + log=log, + ) + + _write_session_config(session_config=session_config, sess_paths=sess_paths) + run_session_config(session_config=session_config, tracker=tracker) + finally: + if lock.is_locked: + lock.release() + + +_TRANSIENT_ERROR_PATTERNS = ( + "must have either content or tool calls", + "AssistantMessage must have", + "object has no attribute 'session_id'", +) + +_MAX_SESSION_RETRIES = 2 + + +def _is_transient_error(exc: Exception) -> bool: + """Check if an exception is a known transient benchmark error worth retrying.""" + msg = str(exc) + return any(pat in msg for pat in _TRANSIENT_ERROR_PATTERNS) + + +def _execute_sessions_serial( + *, + session_configs: list[SessionConfig], + tracker: Tracker, + log, +) -> bool: + had_error = False + try: + for session_config in session_configs: + succeeded = False + for attempt in range(_MAX_SESSION_RETRIES + 1): + try: + _run_task_with_lock( + session_config=session_config, + tracker=tracker, + log=log, + ) + succeeded = True + break + except (KeyboardInterrupt, RunCancelError): + raise + except Exception as exc: + if attempt < _MAX_SESSION_RETRIES and _is_transient_error(exc): + log.warning( + "Transient error on task=%s (attempt %d/%d), retrying: %s", + session_config.task_id if session_config else "unknown", + attempt + 1, + _MAX_SESSION_RETRIES + 1, + exc, + ) + continue + log.exception( + "Session task failed task=%s", + session_config.task_id if session_config else "unknown", + ) + had_error = True + break + if not succeeded and not had_error: + had_error = True + except (KeyboardInterrupt, RunCancelError) as exc: + tracker.on_run_error(exc) + had_error = True + return had_error + + +def _execute_sessions_parallel( + *, + session_configs: list[SessionConfig], + tracker: Tracker, + max_workers: int, + log, +) -> bool: + had_error = False + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures: dict = {} + pending_tasks = deque(session_configs) + try: + while futures or pending_tasks: + while len(futures) < max_workers: + if not pending_tasks: + break + session_config = pending_tasks.popleft() + ctx = copy_context() + future = executor.submit( + ctx.run, + _run_task_with_lock, + session_config=session_config, + tracker=tracker, + log=log, + ) + futures[future] = session_config + + if futures: + done_set, _ = wait(futures, return_when=FIRST_COMPLETED) + for done in done_set: + session_config = futures.pop(done, None) + try: + done.result() + except RunCancelError as exc: + tracker.on_run_error(exc) + had_error = True + raise + except Exception as exc: + session_id = session_config.get_session_id() if session_config is not None else "unknown" + log.exception( + "Session task failed task=%s session=%s", + session_config.task_id if session_config else "unknown", + session_id, + ) + tracker.on_run_error(exc) + had_error = True + except (KeyboardInterrupt, RunCancelError) as exc: + tracker.on_run_error(exc) + had_error = True + for future in futures: + future.cancel() + return had_error + + +def execute_sessions( + *, + session_configs: list[SessionConfig], + tracker: Tracker, + reused_results: list[SessionResults] | None = None, + max_workers: int | None = None, + log=None, +) -> bool: + if log is None: + log = get_disabled_logger(__name__) + if reused_results: + for item in reused_results: + tracker.on_session_reuse(item) + + if max_workers and max_workers > 1: + return _execute_sessions_parallel( + session_configs=session_configs, + tracker=tracker, + max_workers=max_workers, + log=log, + ) + return _execute_sessions_serial( + session_configs=session_configs, + tracker=tracker, + log=log, + ) + + +def load_session_results( + results_path: Path, + session_id: str, + log, +) -> SessionResults | None: + try: + payload = json.loads(results_path.read_text(encoding="utf-8")) + return SessionResults.model_validate(payload) + except Exception: + log.exception( + "Failed to load session results for %s at %s", + session_id, + results_path, + ) + return None + + +def load_reused_results( + session_configs: list[SessionConfig], + log, +) -> list[SessionResults]: + run_paths = get_run_paths() + reused: list[SessionResults] = [] + for session_config in session_configs: + session_id = session_config.get_session_id() + results_path = run_paths.session(session_id).results + if not results_path.exists(): + continue + results = load_session_results(results_path, session_id, log) + if results is not None: + reused.append(results) + log.info( + "Skipping existing session %s (task=%s)", + session_id, + session_config.task_id, + ) + return reused diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/observer.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/observer.py new file mode 100644 index 00000000..7ee00845 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/observer.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from ...utils.paths import RunPaths + + +class Observer: + events = None + + def __init__(self, run_id: str | None = None) -> None: + self._run_id = run_id + self._paths: RunPaths | None = None + + @property + def paths(self) -> RunPaths: + if self._paths is None: + from ..context import get_context + + ctx = get_context() + if self._run_id is not None: + self._paths = RunPaths(run_id=self._run_id, output_dir=ctx.output_dir) + else: + self._paths = RunPaths.from_context(ctx) + self._run_id = self._paths.run_id + return self._paths + + def on_run_start(self, run_config) -> None: + return None + + def on_run_success(self, results, run_config) -> None: + return None + + def on_run_error(self, error) -> None: + return None + + def on_session_creation(self, session) -> None: + return None + + def on_session_start(self, session, agent, observation) -> None: + return None + + def on_react_success(self, session, action) -> None: + return None + + def on_step_success(self, session, observation) -> None: + return None + + def on_react_error( + self, + session, + error, + ) -> None: + return None + + def on_step_error(self, session, error) -> None: + return None + + def on_session_error(self, session, error) -> None: + return None + + def on_session_success(self, session, score, agent) -> None: + return None + + def on_session_scoring(self, session) -> None: + return None + + def on_session_reuse(self, task_result) -> None: + return None diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/run.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/run.py new file mode 100644 index 00000000..5542cca5 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/run.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from ...interfaces.registry import load_benchmark +from ...observers.logging import get_logger +from ...utils.paths import get_run_paths +from ..types import ( + RunConfig, + RunPlan, + RunResults, + RunStatus, + SessionExecutionStatus, + SessionIndex, +) +from .controller import Controller +from .execution import execute_sessions, load_reused_results +from .observer import Observer +from .tracker import Tracker + + +def _build_session_indexes(run_config: RunConfig, task_ids: list[str]): + return [run_config.to_session_config(task_id).to_index() for task_id in task_ids] + + +def _log_missing_session_results( + *, + run_config: RunConfig, + task_ids: list[str], + log, +) -> None: + if not task_ids: + return + missing: list[str] = [] + run_paths = get_run_paths() + for task_id in task_ids: + session_id = run_config.to_session_config(task_id).get_session_id() + if not run_paths.session(session_id).results.exists(): + missing.append(session_id) + if not missing: + return + count = len(missing) + total = len(task_ids) + log.warning("Missing session results for %d/%d planned sessions.", count, total) + preview = ", ".join(missing[:10]) + if count > 10: + preview = f"{preview}, ..." + log.warning("Missing session ids: %s", preview) + + +def core_run( + *, + run_config: RunConfig, + observers: list[Observer] | None = None, + controllers: list[Controller] | None = None, + execute: bool, + aggregate: bool, +) -> RunResults: + with run_config.get_context() as ctx: + if run_config.run_id is None or run_config.cache_dir is None: + updates = {} + if run_config.run_id is None: + updates["run_id"] = ctx.run_id + if run_config.cache_dir is None: + updates["cache_dir"] = ctx.cache_dir + run_config = run_config.model_copy(update=updates) + if execute: + tracker = Tracker( + observers=observers, + controllers=controllers, + max_steps=run_config.max_steps, + max_actions=run_config.max_actions, + ) + else: + tracker = Tracker(observers=observers, controllers=controllers) + run_paths = get_run_paths() + log = get_logger( + f"tracker.{run_paths.run_id}", + str(run_paths.tracker), + ) + status = RunStatus.from_config(run_config) + if run_config.task_ids is None and status.task_ids: + run_config = run_config.model_copy(update={"task_ids": status.task_ids}) + tracker.on_run_start(run_config) + if execute: + plan = RunPlan.from_config_and_status(run_config, status) + reused_results = load_reused_results(plan.reuse, log) + log.info( + "Session selection: total=%d to_run=%d skipped=%d", + len(status.task_ids), + len(plan.to_run), + len(plan.reuse), + ) + had_error = execute_sessions( + session_configs=plan.to_run, + tracker=tracker, + reused_results=reused_results, + max_workers=run_config.max_workers, + log=log, + ) + if had_error: + return tracker.results() + else: + reused_results = load_reused_results( + [run_config.to_session_config(task_id) for task_id in status.task_ids], + log, + ) + for item in reused_results: + tracker.on_session_reuse(item) + + results = None + if aggregate: + if execute: + status = RunStatus.from_config(run_config) + if status.task_ids: + _log_missing_session_results(run_config=run_config, task_ids=status.task_ids, log=log) + # Aggregate only completed sessions. + completed = [item for item in status.session_statuses if item.status == SessionExecutionStatus.COMPLETED] + if completed: + session_indexes = [SessionIndex(task_id=item.task_id, session_id=item.session_id) for item in completed] + else: + session_indexes = [] + skipped = len(status.session_statuses) - len(session_indexes) + if skipped: + log.warning( + "Skipping %d non-completed sessions during aggregation.", + skipped, + ) + if not session_indexes: + log.warning("No completed sessions available for aggregation.") + # Create evaluator for aggregation. + bench_cls = load_benchmark(run_config.benchmark) + benchmark = bench_cls(**(run_config.benchmark_kwargs or {})) + evaluator = benchmark.get_evaluator() + try: + results = evaluator.aggregate_sessions(session_indexes) + finally: + try: + evaluator.close() + except Exception: + pass + benchmark.close() + tracker.on_run_success(results, run_config) + return tracker.results() + + +def core_execute( + *, + run_config: RunConfig, + observers: list[Observer] | None = None, + controllers: list[Controller] | None = None, +) -> RunResults: + return core_run( + run_config=run_config, + observers=observers, + controllers=controllers, + execute=True, + aggregate=False, + ) + + +def core_evaluate( + *, + run_config: RunConfig, + observers: list[Observer] | None = None, + controllers: list[Controller] | None = None, +) -> RunResults: + return core_run( + run_config=run_config, + observers=observers, + controllers=controllers, + execute=True, + aggregate=True, + ) + + +def core_aggregate( + *, + run_config: RunConfig, + observers: list[Observer] | None = None, + controllers: list[Controller] | None = None, +) -> RunResults: + return core_run( + run_config=run_config, + observers=observers, + controllers=controllers, + execute=False, + aggregate=True, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/session.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/session.py new file mode 100644 index 00000000..41b7f9fe --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/session.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from ..context import agent_scope, benchmark_scope, session_scope +from ..types import SessionConfig +from .controller import Controller +from .observer import Observer +from .termination import ( + AgentError, + AgentTerminationError, + BenchmarkError, + BenchmarkTerminationError, + RunCancelError, + SessionCancelError, + SessionLimitReachedError, +) +from .tracker import Tracker + + +def _close_session_agent(session, agent_instance) -> None: + session.close() + agent_instance.close() + + +def run_session( + session_config: SessionConfig, + session, + agent, + observers: list[Observer] | None = None, + controllers: list[Controller] | None = None, + *, + tracker: Tracker | None = None, +) -> None: + """Process a single session. + + The *agent_instance* is created via ``agent.get_instance()`` for + runner isolation. + """ + if tracker is None: + tracker = Tracker(observers=observers, controllers=controllers) + + with session_scope(session.session_id, task_id=session.task_id): + agent_instance = agent.get_instance(session_id=session.session_id) + + with benchmark_scope(): + observation = session.start() + + with agent_scope(): + agent_instance.start( + task=session.task, + context=session.context, + actions=session.actions, + ) + + try: + tracker.on_session_start(session, agent_instance, observation) + while not session.done() and observation is not None: + try: + with agent_scope(): + action = agent_instance.react(observation) + except Exception as exc: + tracker.on_react_error(session, exc) + + tracker.on_react_success( + session, + action, + ) + + try: + with benchmark_scope(): + observation = session.step(action) + except Exception as exc: + tracker.on_step_error( + session, + exc, + ) + + tracker.on_step_success( + session, + observation, + ) + + if session.done(): + raise BenchmarkTerminationError() + raise AgentTerminationError() + + except KeyboardInterrupt: + tracker.on_session_error(session, RunCancelError()) + _close_session_agent(session, agent_instance) + raise + except AgentError as exc: + tracker.on_session_error(session, exc) + _close_session_agent(session, agent_instance) + except SessionLimitReachedError as exc: + with benchmark_scope(): + tracker.on_session_scoring(session) + with benchmark_scope(): + score = session.score() + score.is_finished = False + score.session_metadata = { + **(score.session_metadata or {}), + "limit_reached": True, + "limit_reason": exc.reason, + "max_steps": exc.max_steps, + "max_actions": exc.max_actions, + "steps": exc.steps, + "actions": exc.actions, + } + tracker.on_session_success(session, score, agent_instance) + _close_session_agent(session, agent_instance) + except (AgentTerminationError, BenchmarkTerminationError): + with benchmark_scope(): + tracker.on_session_scoring(session) + with benchmark_scope(): + score = session.score() + if score.is_finished is None: + score.is_finished = True + tracker.on_session_success(session, score, agent_instance) + _close_session_agent(session, agent_instance) + except BenchmarkError as exc: + tracker.on_session_error(session, exc) + agent_instance.close() + except SessionCancelError as exc: + tracker.on_session_error(session, exc) + _close_session_agent(session, agent_instance) + except RunCancelError as exc: + tracker.on_session_error(session, exc) + _close_session_agent(session, agent_instance) + raise diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/termination.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/termination.py new file mode 100644 index 00000000..6b1ea85b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/termination.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + + +class SessionTerminationError(Exception): + """Internal control-flow signal for clean session termination.""" + + +class SessionCancelError(SessionTerminationError): + pass + + +class RunCancelError(SessionTerminationError): + pass + + +class AgentTerminationError(SessionTerminationError): + pass + + +class BenchmarkTerminationError(SessionTerminationError): + pass + + +class SessionLimitReachedError(SessionTerminationError): + def __init__( + self, + *, + reason: str, + max_steps: int, + max_actions: int, + steps: int, + actions: int, + ) -> None: + super().__init__() + self.reason = reason + self.max_steps = max_steps + self.max_actions = max_actions + self.steps = steps + self.actions = actions + + def __str__(self) -> str: + return ( + f"limit_reached ({self.reason}): " + f"steps={self.steps}/{self.max_steps}, " + f"actions={self.actions}/{self.max_actions}" + ) + + +class AgentError(SessionTerminationError): + def __init__(self, error: Exception | None = None) -> None: + super().__init__() + self.error = error + + +class BenchmarkError(SessionTerminationError): + def __init__(self, error: Exception | None = None) -> None: + super().__init__() + self.error = error + + +class InvalidActionError(ValueError): + def __init__(self, action) -> None: + self.action = action + super().__init__(f"illegal action returned from agent: {action}") + + +class InvalidObservationError(ValueError): + def __init__(self, observation) -> None: + self.observation = observation + super().__init__(f"illegal observation returned from session: {observation}") diff --git a/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/tracker.py b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/tracker.py new file mode 100644 index 00000000..a320ebe5 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/orchestrator/tracker.py @@ -0,0 +1,176 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import threading +from typing import Iterable + +from ...observers.handlers.configs import ConfigsObserver +from ...observers.handlers.file_logger import FileLoggerObserver +from ...observers.handlers.logger import ConsoleLoggerObserver +from ...observers.handlers.recap import RunRecapObserver +from ...observers.handlers.results import ResultsObserver +from ...observers.handlers.warnings import WarningsObserver +from ...utils.settings import get_settings +from ..context import get_context +from .controller import CleanupController, Controller, CoreController, LimitController +from .observer import Observer +from .termination import AgentError, BenchmarkError + + +class Tracker(Observer, Controller): + def __init__( + self, + *, + observers: Iterable[Observer] | None = None, + controllers: Iterable[Controller] | None = None, + use_defaults: bool = True, + max_steps: int = 100, + max_actions: int = 100, + ) -> None: + self._run_id = get_context().run_id + self._observers: list[Observer] = [] + self._controllers: list[Controller] = [] + self._results: ResultsObserver | None = None + self.events = None + self._run_lock = threading.Lock() + self._run_state: str | None = None + if use_defaults: + self._register_observer(ResultsObserver()) + self._register_observer(ConfigsObserver()) + self._register_observer(WarningsObserver()) + self._register_observer(FileLoggerObserver()) + self._register_observer(ConsoleLoggerObserver()) + self._register_observer(RunRecapObserver(console=False)) + if get_settings().otel_enabled: + from ...observers.handlers.otel import OtelTracingObserver + + self._register_observer(OtelTracingObserver()) + self._register_controller(LimitController(max_steps=max_steps, max_actions=max_actions)) + self._register_controller(CleanupController()) + if observers: + for observer in observers: + self._register_observer(observer) + if controllers: + for controller in controllers: + self._register_controller(controller) + self._ensure_core_controller() + + @property + def run_id(self) -> str: + return self._run_id + + @property + def session_results(self): + if self._results is not None: + return self._results.session_results() + return [] + + def results(self): + if self._results is not None: + return self._results.results() + raise RuntimeError("No results observer configured to provide results.") + + def _register_observer(self, observer: Observer | None) -> None: + if observer is None: + return + if observer in self._observers: + return + if isinstance(observer, ResultsObserver): + self._results = observer + if observer.events is not None: + self.events = observer.events + self._observers.append(observer) + + def _register_controller(self, controller: Controller | None) -> None: + if controller is None: + return + if controller in self._controllers: + return + self._controllers.append(controller) + + def on_run_start(self, run_config) -> None: + for observer in self._observers: + observer.on_run_start(run_config) + + def on_run_success(self, results, run_config) -> None: + if not self._mark_run_final("success"): + return + for observer in self._observers: + observer.on_run_success(results, run_config) + for controller in self._controllers: + controller.on_run_success(results, run_config) + + def on_run_error(self, error) -> None: + if not self._mark_run_final("error"): + return + for observer in self._observers: + observer.on_run_error(error) + for controller in self._controllers: + controller.on_run_error(error) + + def on_session_creation(self, session) -> None: + for observer in self._observers: + observer.on_session_creation(session) + + def on_session_start(self, session, agent, observation) -> None: + for observer in self._observers: + observer.on_session_start(session, agent, observation) + + def on_react_success(self, session, action) -> None: + for observer in self._observers: + observer.on_react_success(session, action) + for controller in self._controllers: + controller.on_react_success(session, action) + + def on_step_success(self, session, observation) -> None: + for observer in self._observers: + observer.on_step_success(session, observation) + for controller in self._controllers: + controller.on_step_success(session, observation) + + def on_react_error(self, session, error) -> None: + for observer in self._observers: + observer.on_react_error(session, error) + for controller in self._controllers: + controller.on_react_error(session, error) + raise AgentError(error) + + def on_step_error(self, session, error) -> None: + for observer in self._observers: + observer.on_step_error(session, error) + for controller in self._controllers: + controller.on_step_error(session, error) + raise BenchmarkError(error) + + def on_session_error(self, session, error) -> None: + for observer in self._observers: + observer.on_session_error(session, error) + + def on_session_success(self, session, score, agent) -> None: + for observer in self._observers: + observer.on_session_success(session, score, agent) + + def on_session_scoring(self, session) -> None: + for observer in self._observers: + observer.on_session_scoring(session) + + def on_session_reuse(self, session_results) -> None: + for observer in self._observers: + observer.on_session_reuse(session_results) + + def _ensure_core_controller(self) -> None: + cores = [item for item in self._controllers if isinstance(item, CoreController)] + non_cores = [item for item in self._controllers if not isinstance(item, CoreController)] + if not cores: + self._controllers = [*non_cores, CoreController()] + return + self._controllers = [*non_cores, cores[-1]] + + def _mark_run_final(self, state: str) -> bool: + with self._run_lock: + if self._run_state is not None: + return False + self._run_state = state + return True diff --git a/labs/AgentStream/exgentic/src/exgentic/core/runner_mixin.py b/labs/AgentStream/exgentic/src/exgentic/core/runner_mixin.py new file mode 100644 index 00000000..fbb80c60 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/runner_mixin.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from pathlib import Path +from typing import Any + +from ..utils.settings import RunnerName, get_settings +from .context import try_get_context + + +class RunnerMixin: + """Shared runner/Docker logic for Agent and Benchmark.""" + + runner: RunnerName | None + docker_socket: bool + slug_name: str + + def resolve_runner(self) -> RunnerName: + """Resolve the runner name from ``runner`` field or settings default.""" + if self.runner is not None: + return self.runner + return get_settings().default_runner + + def runner_kwargs(self) -> dict[str, Any]: + """Return extra kwargs for ``with_runner()`` when runner is docker or venv.""" + runner = self.resolve_runner() + + kind = "agents" if self._is_agent() else "benchmarks" + + if runner == "venv": + return { + "env_name": f"{kind}/{self.slug_name}", + "module_path": type(self).__module__, + } + + if runner != "docker": + return {} + + kw: dict[str, Any] = { + "env_name": f"{kind}/{self.slug_name}", + "module_path": type(self).__module__, + } + if self.docker_socket: + kw["docker_socket"] = True + ctx = try_get_context() + output_dir = ctx.output_dir if ctx is not None else get_settings().output_dir + output_dir = str(Path(output_dir).resolve()) + kw["volumes"] = {output_dir: output_dir} + return kw + + def _is_agent(self) -> bool: + """Return True if this instance is an Agent (not a Benchmark).""" + from .agent import Agent + + return isinstance(self, Agent) + + def close(self) -> None: + """Optional cleanup hook.""" + return diff --git a/labs/AgentStream/exgentic/src/exgentic/core/session.py b/labs/AgentStream/exgentic/src/exgentic/core/session.py new file mode 100644 index 00000000..fab80472 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/session.py @@ -0,0 +1,161 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import secrets +from abc import ABC, abstractmethod +from logging import Logger +from typing import Any, Dict, List, Optional + +from ..observers.logging import get_logger +from ..utils.cost import CostReport +from ..utils.paths import SessionPaths +from .types import Action, ActionType, Observation, SessionScore + + +class Session(ABC): + """Session interface - represents one task execution in one environment.""" + + def __init__(self) -> None: + # Persist configuration once per session if provided by subclass. + self.save_config() + self.save_manifest() + + @property + def session_id(self) -> str: + """Returns a unique id for the session.""" + if not hasattr(self, "_session_id"): + self._session_id = secrets.token_hex(4) + return self._session_id + + @property + def paths(self) -> SessionPaths: + """Convenience wrapper for all filesystem paths for this session.""" + if not hasattr(self, "_paths"): + from .context import try_get_context + + ctx = try_get_context() + if ctx is not None: + self._paths = SessionPaths( + session_id=self.session_id, + run_id=ctx.run_id, + output_dir=ctx.output_dir, + ) + else: + self._paths = SessionPaths(session_id=self.session_id, run_id="default", output_dir="outputs") + return self._paths + + @property + def logger(self) -> Logger: + if not hasattr(self, "_logger"): + self._logger = get_logger(f"Session_{self.session_id}", str(self.paths.session_log)) + return self._logger + + def get_config(self) -> Dict[str, Any]: + """Return a serializable configuration dict for this session.""" + return {} + + def save_config(self) -> None: + """Persist the session configuration to the standard config path.""" + config = self.get_config() + config_path = self.paths.benchmark_config + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + def save_results(self, payload: Dict[str, Any]) -> None: + """Persist a results payload to the standard per-session results path.""" + results_path = self.paths.benchmark_results + results_path.parent.mkdir(parents=True, exist_ok=True) + with open(results_path, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + + def save_standard_results(self, score: SessionScore) -> None: + """Persist the minimal standardized results payload (score/success).""" + self.save_results({"score": score.score, "success": bool(score.success)}) + + def save_manifest(self) -> None: + """Persist task/context/actions metadata for the session.""" + actions_payload = [] + for action_type in self.actions: + action_entry = { + "name": action_type.name, + "description": action_type.description, + "is_finish": bool(action_type.is_finish), + "is_message": bool(action_type.is_message), + "is_hidden": bool(action_type.is_hidden), + } + schema = action_type.arguments.model_json_schema() # type: ignore[attr-defined] + action_entry["arguments_schema"] = schema + actions_payload.append(action_entry) + + manifest = { + "run_id": self.paths.run_id, + "session_id": self.session_id, + "task": self.task, + "context": self.context, + "actions": actions_payload, + "task_id": self.task_id, + } + path = self.paths.session_manifest + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(manifest, f, ensure_ascii=False, indent=2) + + def get_cost(self) -> CostReport: + """Estimated session cost; default 0.0.""" + return CostReport.initialize_empty() + + @property + def task_id(self) -> str: + """Task identifier.""" + return "" + + @property + @abstractmethod + def task(self) -> str: + """Task description - benchmark defines what work to do.""" + pass + + @property + @abstractmethod + def context(self) -> Dict[str, Any]: + """Task context - benchmark provides necessary information.""" + pass + + @property + @abstractmethod + def actions(self) -> List[ActionType]: + """Available actions - benchmark defines action space.""" + pass + + @abstractmethod + def start(self) -> Optional[Observation]: + """Current observation - session maintains state.""" + pass + + @abstractmethod + def step(self, action: Action) -> Optional[Observation]: + """Execute action - session controls execution, returns None when done.""" + pass + + @abstractmethod + def done(self) -> bool: + """Check completion - session knows when task is finished.""" + pass + + @abstractmethod + def score(self) -> Dict[str, Any]: + """Evaluate performance - session/benchmark controls scoring. + + Should return at least two fields: + + "success" - True iff the session execution completed without an error + "score" - The score of the session execution, should be None if an error occurred. + """ + pass + + @abstractmethod + def close(self): + """Cleanup resources - session manages its own resources.""" + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/__init__.py b/labs/AgentStream/exgentic/src/exgentic/core/types/__init__.py new file mode 100644 index 00000000..31836b13 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/__init__.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .action import ( + Action, + ActionType, + FinishAction, + Message, + MessageAction, + ParallelAction, + SequentialAction, + SingleAction, + ValidationReport, +) +from .evaluation import BaseEvaluationConfig +from .model_settings import ModelSettings, RetryStrategy +from .observation import ( + EmptyObservation, + MessageObservation, + MessagePayload, + MultiObservation, + Observation, + SingleObservation, +) +from .run import ( + BenchmarkResults, + Integration, + RunConfig, + RunPlan, + RunResults, + RunStatus, +) +from .session import ( + SessionConfig, + SessionExecutionStatus, + SessionIndex, + SessionOutcomeStatus, + SessionResults, + SessionScore, + SessionStatus, +) + +__all__ = [ + "Action", + "ActionType", + "FinishAction", + "Message", + "MessageAction", + "ParallelAction", + "SequentialAction", + "SingleAction", + "ValidationReport", + "EmptyObservation", + "MessageObservation", + "MessagePayload", + "MultiObservation", + "Observation", + "SingleObservation", + "ModelSettings", + "RetryStrategy", + "BenchmarkResults", + "Integration", + "BaseEvaluationConfig", + "RunConfig", + "RunPlan", + "RunStatus", + "RunResults", + "SessionConfig", + "SessionExecutionStatus", + "SessionOutcomeStatus", + "SessionScore", + "SessionStatus", + "SessionResults", + "SessionIndex", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/action.py b/labs/AgentStream/exgentic/src/exgentic/core/types/action.py new file mode 100644 index 00000000..f6b356dd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/action.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import inspect +import uuid +from abc import ABC, abstractmethod +from typing import Any, Literal, Optional, get_type_hints + +from pydantic import BaseModel, Field + + +class Action(BaseModel, ABC): + @abstractmethod + def to_action_list(self): + pass + + pass + + +class ValidationReport(BaseModel): + valid: bool = True + name_valid: bool = True + args_valid: bool = True + error: Optional[str] = None + details: dict[str, Any] = Field(default_factory=dict) + + +class SingleAction(Action): + name: str + arguments: BaseModel + id: str = Field(default_factory=lambda: str(uuid.uuid4()), frozen=True) + validation: ValidationReport = Field(default_factory=ValidationReport) + + def to_action_list(self): + return [self] + + +class Message(BaseModel): + content: str + + +class MessageAction(SingleAction): + name: Literal["message"] = "message" + arguments: Message + + +class FinishAction(SingleAction): + name: str + arguments: BaseModel + + +class ParallelAction(Action): + actions: list[SingleAction] + + def to_action_list(self): + return self.actions + + +class SequentialAction(Action): + actions: list[SingleAction] + + def to_action_list(self): + return self.actions + + +class ActionType(BaseModel): + name: str + description: str + cls: type[SingleAction] + # Hints for agent/adapter handling (optional) + is_message: bool = False + is_finish: bool = False + is_hidden: bool = False + + @property + def arguments(self) -> type[BaseModel]: + """Return the resolved pydantic model type for the action's arguments. + + Handles forward-referenced annotations (e.g., from `from __future__ import annotations`). + Falls back to the raw annotation if resolution fails. + """ + module = inspect.getmodule(self.cls) + globalns = vars(module) if module else {} + try: + hints = get_type_hints(self.cls, globalns=globalns, localns=globalns) + arg_t = hints.get("arguments") + if arg_t is not None: + if isinstance(arg_t, str): + resolved = globalns.get(arg_t) + if isinstance(resolved, type): + return resolved # type: ignore[return-value] + return arg_t # type: ignore[return-value] + except Exception: + pass + arg_t = self.cls.__annotations__.get("arguments") + if isinstance(arg_t, str): + resolved = globalns.get(arg_t) + if isinstance(resolved, type): + return resolved # type: ignore[return-value] + return arg_t # type: ignore[return-value] + + def build_action(self, arguments: Any, *, action_id: Optional[str] = None) -> SingleAction: + """Convenience wrapper around core.actions.build_action for this action type.""" + from ..actions import build_action # Local import to avoid circular dependency + + return build_action(self, arguments, action_id=action_id) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/evaluation.py b/labs/AgentStream/exgentic/src/exgentic/core/types/evaluation.py new file mode 100644 index 00000000..f7aeeb28 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/evaluation.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import hashlib +import json +from typing import Any, Optional + +from pydantic import BaseModel, StrictStr, model_validator + +from ...interfaces.registry import ( + apply_subset_kwargs, + load_agent, + load_benchmark, +) +from ..context import try_get_context +from .model_settings import ModelSettings + + +def _validate_kwargs(kind: str, cls: type, kwargs: dict[str, Any]) -> None: + if not isinstance(cls, type) or not issubclass(cls, BaseModel): + return + cls.model_validate(kwargs) + + +def _compute_run_id( + *, + benchmark: str, + agent: str, + benchmark_kwargs: dict[str, Any], + agent_kwargs: dict[str, Any], +) -> str: + payload = { + "benchmark": { + "slug_name": benchmark, + "params": benchmark_kwargs, + }, + "agent": { + "slug_name": agent, + "params": agent_kwargs, + }, + } + encoded = json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + default=str, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest()[:12] + + +class BaseEvaluationConfig(BaseModel): + """Shared evaluation config with canonical normalization.""" + + benchmark: StrictStr + agent: StrictStr + subset: Optional[str] = None + output_dir: str = "./outputs" + cache_dir: Optional[str] = None + run_id: Optional[str] = None + model: Optional[str] = None + benchmark_kwargs: Optional[dict[str, Any]] = None + agent_kwargs: Optional[dict[str, Any]] = None + + @model_validator(mode="before") + @classmethod + def _normalize(cls, values): + if not isinstance(values, dict): + return values + payload = dict(values) + benchmark = payload.get("benchmark") + agent = payload.get("agent") + benchmark_kwargs = payload.get("benchmark_kwargs") or {} + agent_kwargs = payload.get("agent_kwargs") or {} + if not isinstance(benchmark_kwargs, dict): + raise TypeError("benchmark_kwargs must be a dict") + if not isinstance(agent_kwargs, dict): + raise TypeError("agent_kwargs must be a dict") + + subset = payload.get("subset") + if subset is not None and benchmark: + benchmark_kwargs = apply_subset_kwargs(str(benchmark), str(subset), dict(benchmark_kwargs)) + else: + benchmark_kwargs = dict(benchmark_kwargs) + + agent_kwargs = dict(agent_kwargs) + if "model_settings" in agent_kwargs: + model_cfg = agent_kwargs["model_settings"] + if model_cfg is None: + pass + elif isinstance(model_cfg, ModelSettings): + pass + elif isinstance(model_cfg, dict): + agent_kwargs["model_settings"] = ModelSettings(**model_cfg) + else: + raise ValueError("agent.model_settings must be a ModelSettings or dict.") + + model = payload.get("model") + if model is not None: + if "model" in agent_kwargs and agent_kwargs["model"] != model: + raise ValueError("Conflicting model selection: " f"model={agent_kwargs['model']} but model={model}") + agent_kwargs["model"] = model + + if benchmark: + bench_cls = load_benchmark(str(benchmark)) + _validate_kwargs("benchmark", bench_cls, benchmark_kwargs) + if agent: + agent_cls = load_agent(str(agent)) + _validate_kwargs("agent", agent_cls, agent_kwargs) + + payload["benchmark_kwargs"] = benchmark_kwargs + payload["agent_kwargs"] = agent_kwargs + if payload.get("run_id") is None and benchmark and agent: + ctx = try_get_context() + payload["run_id"] = (ctx.run_id if ctx else None) or _compute_run_id( + benchmark=str(benchmark), + agent=str(agent), + benchmark_kwargs=benchmark_kwargs, + agent_kwargs=agent_kwargs, + ) + return payload + + def canonical_payload(self) -> dict[str, Any]: + return self.model_dump(mode="json", exclude_none=False) + + def fingerprint(self) -> str: + encoded = json.dumps( + self.canonical_payload(), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + default=str, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def get_context(self): + from ..context import run_scope + + return run_scope( + output_dir=self.output_dir, + cache_dir=self.cache_dir, + run_id=self.run_id, + ) + + @classmethod + def from_file(cls, path: str): + with open(path, encoding="utf-8") as f: + payload = json.load(f) + return cls.model_validate(payload) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/model_settings.py b/labs/AgentStream/exgentic/src/exgentic/core/types/model_settings.py new file mode 100644 index 00000000..a39b93c3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/model_settings.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, field_validator + + +class RetryStrategy(str, Enum): + EXPONENTIAL_BACKOFF = "exponential_backoff_retry" + CONSTANT = "constant_retry" + + +class ModelSettings(BaseModel): + temperature: float | None = 1.0 + top_p: float | None = None + max_tokens: int | None = None + reasoning_effort: str | None = None + num_retries: int | None = 5 + retry_after: float = 0.5 + retry_strategy: RetryStrategy = RetryStrategy.EXPONENTIAL_BACKOFF + + @field_validator("temperature") + @classmethod + def _validate_temperature(cls, value: float | None) -> float | None: + if value is not None and value < 0: + raise ValueError("temperature must be >= 0") + return value + + @field_validator("max_tokens") + @classmethod + def _validate_max_tokens(cls, value: int | None) -> int | None: + if value is not None and value < 0: + raise ValueError("max_tokens must be >= 0") + return value + + @field_validator("top_p") + @classmethod + def _validate_top_p(cls, value: float | None) -> float | None: + if value is None: + return value + if value < 0 or value > 1: + raise ValueError("top_p must be between 0 and 1") + return value + + @field_validator("num_retries") + @classmethod + def _validate_num_retries(cls, value: int | None) -> int | None: + if value is None: + return None + if value < 0: + raise ValueError("num_retries must be >= 0") + return value + + @field_validator("retry_after") + @classmethod + def _validate_retry_after(cls, value: float) -> float: + if value < 0: + raise ValueError("retry_after must be >= 0") + return value diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/observation.py b/labs/AgentStream/exgentic/src/exgentic/core/types/observation.py new file mode 100644 index 00000000..74c982bb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/observation.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from pydantic import BaseModel, Field + +from .action import SingleAction + + +class Observation(BaseModel, ABC): + """Base class to encapsulate both single and multiple observations.""" + + @abstractmethod + def to_observation_list(self): + pass + + @abstractmethod + def is_empty(self) -> bool: + """Returns True if the observation carries no meaningful content.""" + raise NotImplementedError + + +class SingleObservation(Observation): + """Observation that includes a single arbitrary result and a list of actions that invoked it. + + (i.e. the actions that generated it the observation) + + To allow the observation to be used in place of the results in CodeAgents, + we map all the magic methods of the observation to the result object. + """ + + result: Any + invoking_actions: list[SingleAction] = Field(repr=False, default_factory=list) + + def to_observation_list(self): + return [self] + + def is_empty(self) -> bool: + if self.result is None: + return True + if isinstance(self.result, str) and self.result.strip() == "": + return True + if isinstance(self.result, (list, tuple, set, dict)) and len(self.result) == 0: + return True + return False + + def __str__(self, *args, **kwargs): + return self.result.__str__(*args, **kwargs) + + def __repr__(self, *args, **kwargs): + return self.result.__repr__(*args, **kwargs) + + def __len__(self, *args, **kwargs): + return self.result.__len__(*args, **kwargs) + + def __getitem__(self, *args, **kwargs): + return self.result.__getitem__(*args, **kwargs) + + def __setitem__(self, *args, **kwargs): + return self.result.__setitem__(*args, **kwargs) + + def __delitem__(self, *args, **kwargs): + return self.result.__delitem__(*args, **kwargs) + + def __iter__(self, *args, **kwargs): + return self.result.__iter__(*args, **kwargs) + + def __contains__(self, *args, **kwargs): + return self.result.__contains__(*args, **kwargs) + + def __eq__(self, *args, **kwargs): + return self.result.__eq__(*args, **kwargs) + + def __ne__(self, *args, **kwargs): + return self.result.__ne__(*args, **kwargs) + + def __lt__(self, *args, **kwargs): + return self.result.__lt__(*args, **kwargs) + + def __le__(self, *args, **kwargs): + return self.result.__le__(*args, **kwargs) + + def __gt__(self, *args, **kwargs): + return self.result.__gt__(*args, **kwargs) + + def __ge__(self, *args, **kwargs): + return self.result.__ge__(*args, **kwargs) + + def __add__(self, *args, **kwargs): + return self.result.__add__(*args, **kwargs) + + def __sub__(self, *args, **kwargs): + return self.result.__sub__(*args, **kwargs) + + def __mul__(self, *args, **kwargs): + return self.result.__mul__(*args, **kwargs) + + def __truediv__(self, *args, **kwargs): + return self.result.__truediv__(*args, **kwargs) + + def __floordiv__(self, *args, **kwargs): + return self.result.__floordiv__(*args, **kwargs) + + def __mod__(self, *args, **kwargs): + return self.result.__mod__(*args, **kwargs) + + def __pow__(self, *args, **kwargs): + return self.result.__pow__(*args, **kwargs) + + def __and__(self, *args, **kwargs): + return self.result.__and__(*args, **kwargs) + + def __or__(self, *args, **kwargs): + return self.result.__or__(*args, **kwargs) + + def __xor__(self, *args, **kwargs): + return self.result.__xor__(*args, **kwargs) + + def __lshift__(self, *args, **kwargs): + return self.result.__lshift__(*args, **kwargs) + + def __rshift__(self, *args, **kwargs): + return self.result.__rshift__(*args, **kwargs) + + def __neg__(self, *args, **kwargs): + return self.result.__neg__(*args, **kwargs) + + def __pos__(self, *args, **kwargs): + return self.result.__pos__(*args, **kwargs) + + def __abs__(self, *args, **kwargs): + return self.result.__abs__(*args, **kwargs) + + def __invert__(self, *args, **kwargs): + return self.result.__invert__(*args, **kwargs) + + def __call__(self, *args, **kwargs): + return self.result.__call__(*args, **kwargs) + + +class MessagePayload(BaseModel): + sender: str + message: str + + +class MessageObservation(SingleObservation): + """Observation carrying a structured message payload (sender + message).""" + + result: MessagePayload + + +class EmptyObservation(SingleObservation): + """Explicit empty observation to signal 'no initial content'.""" + + result: Any = None + invoking_actions: list[SingleAction] = Field(repr=False, default_factory=list) + + def is_empty(self) -> bool: + return True + + +class MultiObservation(Observation): + """An observation which is actually a collection of multiple observations.""" + + observations: list[SingleObservation] + + def to_observation_list(self): + return self.observations + + def is_empty(self) -> bool: + return all(obs.is_empty() for obs in self.observations) diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/run.py b/labs/AgentStream/exgentic/src/exgentic/core/types/run.py new file mode 100644 index 00000000..5062782a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/run.py @@ -0,0 +1,304 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import random +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field, field_validator + +from .evaluation import BaseEvaluationConfig +from .session import ( + SessionConfig, + SessionExecutionStatus, + SessionResults, + SessionStatus, +) + + +class BenchmarkResults(BaseModel): + """Minimal benchmark-level results returned by Benchmark.aggregate_sessions().""" + + benchmark_name: str + total_tasks: int + score: float + metrics: dict[str, Any] = {} + + +class RunResults(BaseModel): + """Aggregated run results produced by the tracker.""" + + benchmark_name: str + benchmark_slug_name: str | None = None + agent_name: str + agent_slug_name: str | None = None + model_name: str | None = None + model_names: list[str] | None = None + subset_name: str | None = None + total_sessions: int + planned_sessions: Optional[int] = None + planned_session_ids: Optional[list[str]] = None + executed_session_ids: list[str] = Field(default_factory=list) + max_workers: Optional[int] = None + successful_sessions: int + # Primary benchmark-level outcome (from evaluator.aggregate_sessions()) + benchmark_score: Optional[float] = None + benchmark_results: Optional[dict[str, Any]] = None + average_score: Optional[float] = None + average_agent_cost: Optional[float] = None + total_agent_cost: Optional[float] = None + average_benchmark_cost: Optional[float] = None + total_benchmark_cost: Optional[float] = None + total_run_cost: Optional[float] = None + accumulated_agent_report: Optional[Any] = None + accumulated_benchmark_report: Optional[Any] = None + session_results: list[SessionResults] + average_steps: Optional[float] = None + average_action_count: Optional[float] = None + average_invalid_action_count: Optional[float] = None + average_invalid_action_percent: Optional[float] = None + percent_finished: Optional[float] = None + percent_successful: Optional[float] = None + percent_finished_successful: Optional[float] = None + percent_finished_unsuccessful: Optional[float] = None + percent_unfinished: Optional[float] = None + percent_error: Optional[float] = None + # Aggregation provenance + aggregation_mode: Optional[str] = None + completed_sessions: Optional[int] = None + incomplete_sessions: Optional[int] = None + missing_sessions: Optional[int] = None + running_sessions: Optional[int] = None + aggregated_session_ids: Optional[list[str]] = None + skipped_session_ids: Optional[list[str]] = None + skipped_session_reasons: Optional[dict[str, str]] = None + missing_result_files: Optional[list[str]] = None + exgentic_version: str | None = None + + +class Integration(BaseModel): + """Metadata about a benchmark or agent integration.""" + + name: str + type: Literal["benchmark", "agent"] + version: str + bundled: bool + installed: bool + entry_point: str + + +class RunStatus(BaseModel): + """Snapshot of the current run status and existing session artifacts.""" + + run_id: str + output_dir: str + run_root: str + run_dir: str + sessions_root: str + results_path: str + results_exists: bool + benchmark_results_path: str + benchmark_results_exists: bool + benchmark_name: str + benchmark_slug_name: str + agent_name: str + agent_slug_name: str + model_name: Optional[str] = None + subset_name: Optional[str] = None + task_ids: list[str] + total_tasks: int + session_statuses: list[SessionStatus] = Field(default_factory=list) + completed_sessions: int = 0 + running_sessions: int = 0 + incomplete_sessions: int = 0 + missing_sessions: int = 0 + + @classmethod + def from_config(cls, run_config: RunConfig) -> RunStatus: + session_configs = run_config.get_sessions() + return cls.from_session_configs(run_config, session_configs) + + @classmethod + def from_session_configs( + cls, + run_config: RunConfig, + session_configs: list[SessionConfig], + ) -> RunStatus: + from ...utils.paths import get_run_paths + + context_config = run_config + if session_configs: + first = session_configs[0] + updates = {} + if run_config.run_id is None and first.run_id: + updates["run_id"] = first.run_id + if run_config.cache_dir is None and first.cache_dir: + updates["cache_dir"] = first.cache_dir + if run_config.output_dir is None and first.output_dir: + updates["output_dir"] = first.output_dir + if updates: + context_config = run_config.model_copy(update=updates) + with context_config.get_context(): + run_paths = get_run_paths() + statuses = [ + SessionStatus.from_config( + session_config, + run_paths=run_paths, + ) + for session_config in session_configs + ] + task_ids = [str(item.task_id) for item in session_configs] + completed = sum(1 for item in statuses if item.status == SessionExecutionStatus.COMPLETED) + running = sum(1 for item in statuses if item.status == SessionExecutionStatus.RUNNING) + incomplete = sum(1 for item in statuses if item.status == SessionExecutionStatus.INCOMPLETE) + missing = sum(1 for item in statuses if item.status == SessionExecutionStatus.MISSING) + + return cls( + run_id=run_paths.run_id, + output_dir=str(run_paths.root.parent), + run_root=str(run_paths.root), + run_dir=str(run_paths.run_dir), + sessions_root=str(run_paths.sessions_root), + results_path=str(run_paths.results), + results_exists=run_paths.results.exists(), + benchmark_results_path=str(run_paths.benchmark_results), + benchmark_results_exists=run_paths.benchmark_results.exists(), + benchmark_name=run_config.benchmark, + benchmark_slug_name=run_config.benchmark, + agent_name=run_config.agent, + agent_slug_name=run_config.agent, + model_name=(run_config.model or (run_config.agent_kwargs or {}).get("model")), + subset_name=run_config.subset, + task_ids=task_ids, + total_tasks=len(task_ids), + session_statuses=statuses, + completed_sessions=completed, + running_sessions=running, + incomplete_sessions=incomplete, + missing_sessions=missing, + ) + + +class RunPlan(BaseModel): + """Planned execution derived from RunStatus.""" + + run_config: RunConfig + overwrite_sessions: bool + to_run: list[SessionConfig] = Field(default_factory=list) + reuse: list[SessionConfig] = Field(default_factory=list) + running: list[SessionConfig] = Field(default_factory=list) + missing: list[SessionConfig] = Field(default_factory=list) + incomplete: list[SessionConfig] = Field(default_factory=list) + + @classmethod + def from_config_and_status( + cls, + run_config: RunConfig, + status: RunStatus, + ) -> RunPlan: + overwrite_sessions = run_config.overwrite_sessions + to_run: list[SessionConfig] = [] + reuse: list[SessionConfig] = [] + running: list[SessionConfig] = [] + missing: list[SessionConfig] = [] + incomplete: list[SessionConfig] = [] + + for session_status in status.session_statuses: + session_config = run_config.to_session_config(session_status.task_id) + match session_status.status: + case SessionExecutionStatus.RUNNING: + running.append(session_config) + continue + case SessionExecutionStatus.MISSING: + missing.append(session_config) + case SessionExecutionStatus.INCOMPLETE: + incomplete.append(session_config) + case SessionExecutionStatus.COMPLETED: + if overwrite_sessions: + to_run.append(session_config) + else: + reuse.append(session_config) + continue + to_run.append(session_config) + + return cls( + run_config=run_config, + overwrite_sessions=overwrite_sessions, + to_run=to_run, + reuse=reuse, + running=running, + missing=missing, + incomplete=incomplete, + ) + + +class RunConfig(BaseEvaluationConfig): + """Configuration for a run of multiple sessions.""" + + task_ids: Optional[list[str]] = None + num_tasks: Optional[int] = None + max_workers: Optional[int] = None + max_steps: int = 100 + max_actions: int = 100 + overwrite_sessions: bool = False + + @field_validator("max_steps", "max_actions") + @classmethod + def _validate_limits(cls, value: int) -> int: + if value <= 0: + raise ValueError("limit must be > 0") + return value + + def to_session_config(self, task_id: str) -> SessionConfig: + """Derive a SessionConfig for a single task from this run config.""" + return SessionConfig.model_construct( + benchmark=self.benchmark, + agent=self.agent, + task_id=str(task_id), + subset=self.subset, + output_dir=self.output_dir, + cache_dir=self.cache_dir, + run_id=self.run_id, + model=self.model, + overwrite_sessions=self.overwrite_sessions, + benchmark_kwargs=dict(self.benchmark_kwargs or {}), + agent_kwargs=dict(self.agent_kwargs or {}), + ) + + def get_session_configs( + self, + *, + resolved_config: RunConfig | None = None, + ) -> list[SessionConfig]: + from ...interfaces.registry import load_benchmark + + resolved = resolved_config or self + task_ids = resolved.task_ids + + if task_ids is None: + bench_cls = load_benchmark(resolved.benchmark) + benchmark = bench_cls(**(resolved.benchmark_kwargs or {})) + evaluator = benchmark.get_evaluator() + try: + selected = [str(t) for t in evaluator.list_tasks()] + if resolved.num_tasks is not None: + seed = benchmark.seed + rng = random.Random(seed if seed is not None else 0) + rng.shuffle(selected) + selected = selected[: int(resolved.num_tasks)] + finally: + try: + evaluator.close() + except Exception: + pass + benchmark.close() + else: + selected = [str(t) for t in task_ids] + if resolved.num_tasks is not None: + selected = selected[: int(resolved.num_tasks)] + + return [resolved.to_session_config(task_id) for task_id in selected] + + def get_sessions(self) -> list[SessionConfig]: + return self.get_session_configs() diff --git a/labs/AgentStream/exgentic/src/exgentic/core/types/session.py b/labs/AgentStream/exgentic/src/exgentic/core/types/session.py new file mode 100644 index 00000000..d9e58987 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/core/types/session.py @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import hashlib +import json +from contextlib import contextmanager +from enum import StrEnum +from typing import Any, Optional + +from filelock import FileLock, Timeout +from pydantic import BaseModel, Field, field_validator + +from ...utils.cost import CostReport +from .evaluation import BaseEvaluationConfig + + +class SessionOutcomeStatus(StrEnum): + SUCCESS = "success" + UNSUCCESSFUL = "unsuccessful" + UNFINISHED = "unfinished" + LIMIT_REACHED = "limit_reached" + ERROR = "error" + CANCELLED = "cancelled" + UNKNOWN = "unknown" + + +class SessionExecutionStatus(StrEnum): + MISSING = "missing" + INCOMPLETE = "incomplete" + COMPLETED = "completed" + RUNNING = "running" + + +class SessionResults(BaseModel): + """Results of a single session execution.""" + + session_id: str + success: bool + score: Optional[float] = None + is_finished: Optional[bool] = None + status: SessionOutcomeStatus = SessionOutcomeStatus.UNKNOWN + steps: int + action_count: int = 0 + invalid_action_count: int = 0 + agent_cost: float + benchmark_cost: float + execution_time: float + details: dict[str, Any] = {} + cost_reports: dict[str, CostReport] = Field(default_factory=dict) + task_id: Optional[str] = None + + @field_validator("cost_reports", mode="before") + @classmethod + def accept_instances(cls, v): + if not isinstance(v, dict): + raise TypeError(f"cost_reports must be a dict[str, CostReport | dict], got {type(v)}") + + for key, val in v.items(): + if isinstance(val, CostReport) or isinstance(val, dict): + continue + + raise TypeError(f"Invalid type for cost_reports[{key}]: {type(val)}") + + return v + + +class SessionScore(BaseModel): + """Minimal per-session score returned by Session.score().""" + + score: float + success: bool + is_finished: Optional[bool] = None + session_metrics: dict[str, Any] = {} + session_metadata: dict[str, Any] = {} + + +class SessionStatus(BaseModel): + """Filesystem status for a single task session.""" + + task_id: str + session_id: str + results_path: str + session_dir: str + status: SessionExecutionStatus + result_status: Optional[SessionOutcomeStatus] = None + + @classmethod + def _is_session_locked(cls, session_paths) -> bool: + if not session_paths.lock.exists(): + return False + lock = FileLock(str(session_paths.lock)) + try: + lock.acquire(timeout=0) + except Timeout: + return True + lock.release() + return False + + @classmethod + def _extract_result_status(cls, results_path) -> Optional[SessionOutcomeStatus]: + try: + payload = json.loads(results_path.read_text(encoding="utf-8")) + except Exception: + return None + status = payload.get("status") + if status: + try: + return SessionOutcomeStatus(str(status)) + except ValueError: + return None + success = payload.get("success") + is_finished = payload.get("is_finished") + details = payload.get("details") or {} + metadata = details.get("session_metadata") or {} + error_source = metadata.get("error_source") + if error_source == "cancelled": + return SessionOutcomeStatus.CANCELLED + if metadata.get("error") or error_source in ("agent", "benchmark"): + return SessionOutcomeStatus.ERROR + if is_finished is True: + return SessionOutcomeStatus.SUCCESS if success else SessionOutcomeStatus.UNSUCCESSFUL + if is_finished is False: + return SessionOutcomeStatus.UNFINISHED + return None + + @classmethod + def from_config(cls, session_config, *, run_paths=None) -> SessionStatus: + from ...core.context import get_context + from ...utils.paths import RunPaths + + session_id = session_config.get_session_id() + if run_paths is None: + ctx = get_context() + if session_config.run_id: + run_paths = RunPaths(run_id=session_config.run_id, output_dir=ctx.output_dir) + else: + run_paths = RunPaths.from_context(ctx) + sess_paths = run_paths.session(session_id) + results_exists = sess_paths.results.exists() + session_dir_exists = sess_paths.root.exists() + is_locked = cls._is_session_locked(sess_paths) + result_status: Optional[SessionOutcomeStatus] = None + if is_locked: + status = SessionExecutionStatus.RUNNING + elif results_exists: + result_status = cls._extract_result_status(sess_paths.results) + if result_status in ( + SessionOutcomeStatus.ERROR, + SessionOutcomeStatus.CANCELLED, + ): + status = SessionExecutionStatus.INCOMPLETE + else: + status = SessionExecutionStatus.COMPLETED + elif session_dir_exists: + status = SessionExecutionStatus.INCOMPLETE + else: + status = SessionExecutionStatus.MISSING + return cls( + task_id=str(session_config.task_id), + session_id=session_id, + results_path=str(sess_paths.results), + session_dir=str(sess_paths.root), + status=status, + result_status=result_status, + ) + + +class SessionIndex(BaseModel): + """Minimal mapping between a task id and a session id.""" + + task_id: str + session_id: str + + +class SessionConfig(BaseEvaluationConfig): + """Configuration for running a single session.""" + + task_id: str + overwrite_sessions: bool = False + + def session_id_payload(self) -> dict[str, Any]: + return { + "benchmark": self.benchmark, + "benchmark_kwargs": dict(self.benchmark_kwargs or {}), + "agent": self.agent, + "agent_kwargs": dict(self.agent_kwargs or {}), + "subset": self.subset, + "task_id": str(self.task_id), + "model": self.model, + } + + def get_session_id(self) -> str: + encoded = json.dumps( + self.session_id_payload(), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + default=str, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest()[:8] + + def to_index(self) -> SessionIndex: + return SessionIndex( + task_id=str(self.task_id), + session_id=self.get_session_id(), + ) + + def get_context(self): + from ..context import run_scope, session_scope + + @contextmanager + def _ctx(): + with run_scope( + output_dir=self.output_dir, + cache_dir=self.cache_dir, + run_id=self.run_id, + ): + with session_scope(self.get_session_id(), task_id=str(self.task_id)): + yield + + return _ctx() diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/__init__.py b/labs/AgentStream/exgentic/src/exgentic/environment/__init__.py new file mode 100644 index 00000000..d28851de --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .manager import EnvironmentManager, EnvType + +__all__ = ["EnvironmentManager", "EnvType"] diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/docker.py b/labs/AgentStream/exgentic/src/exgentic/environment/docker.py new file mode 100644 index 00000000..c19364eb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/docker.py @@ -0,0 +1,405 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Docker environment: builds a Docker image with dependencies baked in.""" + +from __future__ import annotations + +import hashlib +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import tomllib + +from .helpers import find_package_file, read_lines + +_DOCKER_CLI_VERSION = "27.5.1" +_DOCKER_CLI_RUN = ( + "RUN apt-get update && apt-get install -y --no-install-recommends curl" + " && rm -rf /var/lib/apt/lists/*" + f" && ARCH=$(uname -m)" + f" && curl -fsSL https://download.docker.com/linux/static/stable/${{ARCH}}/docker-{_DOCKER_CLI_VERSION}.tgz" + " | tar xz --strip-components=1 -C /usr/local/bin docker/docker" +) + + +class DockerBackend: + """Backend that builds a Docker image with dependencies baked in. + + When *project_root* is provided the build is split into two images: + + 1. **Base image** (``exgentic-base:{hash}``) — Python + git + uv + the + project installed via a two-layer cache-friendly build. This image is + shared across all environments that use the same project root and is + only rebuilt when the project's ``pyproject.toml`` or + ``_IMAGE_VERSION`` changes. + + 2. **Bench image** (``{name}:{hash}``) — ``FROM exgentic-base`` with + benchmark-specific system deps, requirements, extra packages, an + optional Docker CLI (for sibling-container benchmarks), and the + setup script. + + When *project_root* is ``None`` the legacy single-image build is used. + """ + + _IMAGE_VERSION = "v1" + + def install( + self, + env_dir: Path, + *, + module_path: str | None = None, + **kwargs: object, + ) -> dict: + """Build a Docker image for the environment. + + Args: + env_dir: Root directory for this environment. + module_path: Dotted module path for locating package resources. + **kwargs: Accepts ``name``, ``force``, ``project_root`` (Path), + ``packages`` (list[str]), and ``docker_socket`` (bool). + + Returns: + Marker data with ``image`` tag. + """ + name: str = kwargs.get("name", env_dir.name) # type: ignore[assignment] + force: bool = kwargs.get("force", False) # type: ignore[assignment] + project_root: Path | None = kwargs.get("project_root") # type: ignore[assignment] + packages: list[str] | None = kwargs.get("packages") # type: ignore[assignment] + docker_socket: bool = bool(kwargs.get("docker_socket", False)) + + req_path = find_package_file(module_path, "requirements.txt") if module_path else None + setup_path = find_package_file(module_path, "setup.sh") if module_path else None + sysdeps_path = find_package_file(module_path, "system-deps.txt") if module_path else None + + if project_root is not None: + return self._install_with_base( + name, + project_root, + req_path=req_path, + setup_path=setup_path, + sysdeps_path=sysdeps_path, + packages=packages, + docker_socket=docker_socket, + force=force, + ) + + image_tag = self._image_tag( + name, + req_path, + setup_path, + sysdeps_path, + packages=packages, + docker_socket=docker_socket, + ) + if not force and self._image_exists(image_tag): + return {"image": image_tag} + self._build_image( + image_tag, + req_path, + setup_path, + sysdeps_path, + packages=packages, + docker_socket=docker_socket, + ) + return {"image": image_tag} + + def uninstall(self, env_dir: Path, marker_data: dict) -> None: + """Remove the Docker images referenced in the marker data. + + The bench image is always removed. The base image (if present) is + attempted too — ``docker rmi`` will silently fail if another bench + image still depends on it, so the last environment using a base tag + will clean it up automatically. + """ + for key in ("image", "base_image"): + tag = marker_data.get(key) + if tag: + subprocess.run( + ["docker", "rmi", tag], + check=False, + capture_output=True, + text=True, + ) + + # ------------------------------------------------------------------ + # Two-image path (project_root provided) + # ------------------------------------------------------------------ + + def _install_with_base( + self, + name: str, + project_root: Path, + *, + req_path: Path | None, + setup_path: Path | None, + sysdeps_path: Path | None, + packages: list[str] | None, + docker_socket: bool, + force: bool, + ) -> dict: + base_tag = self._base_image_tag(project_root) + bench_tag = self._bench_image_tag( + name, + base_tag, + req_path, + setup_path, + sysdeps_path, + packages=packages, + docker_socket=docker_socket, + ) + + if not force and self._image_exists(bench_tag): + return {"image": bench_tag} + + if force or not self._image_exists(base_tag): + self._build_base_image(base_tag, project_root) + + self._build_bench_image( + bench_tag, + base_tag, + req_path=req_path, + setup_path=setup_path, + sysdeps_path=sysdeps_path, + packages=packages, + docker_socket=docker_socket, + ) + return {"image": bench_tag, "base_image": base_tag} + + @classmethod + def _base_image_tag(cls, project_root: Path) -> str: + """Deterministic tag for the shared base image.""" + h = hashlib.sha256() + h.update(cls._IMAGE_VERSION.encode()) + h.update(b"\x00") + pyproject = project_root / "pyproject.toml" + if pyproject.is_file(): + h.update(pyproject.read_text().encode()) + h.update(b"\x00") + return f"exgentic-base:{h.hexdigest()[:12]}" + + @staticmethod + def _bench_image_tag( + name: str, + base_tag: str, + *file_paths: Path | None, + packages: list[str] | None = None, + docker_socket: bool = False, + ) -> str: + """Deterministic tag for the benchmark-specific image.""" + h = hashlib.sha256() + h.update(base_tag.encode()) + h.update(b"\x00") + for path in file_paths: + if path is not None: + h.update(path.read_text().encode()) + h.update(b"\x00") + if packages: + h.update("\n".join(sorted(packages)).encode()) + h.update(b"\x00") + if docker_socket: + h.update(b"docker-socket\x00") + safe_name = name.replace("/", "-") + return f"{safe_name}:{h.hexdigest()[:12]}" + + @staticmethod + def _build_base_image(tag: str, project_root: Path) -> None: + """Build the shared base image: Python + git + uv + project installed.""" + py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + lines = [ + f"FROM python:{py_version}-slim", + "RUN apt-get update && apt-get install -y --no-install-recommends git git-lfs" + " && rm -rf /var/lib/apt/lists/* && git lfs install", + "RUN pip install --no-cache-dir uv", + "ENV UV_SYSTEM_PYTHON=true", + "WORKDIR /app", + ] + + # Layer 1 — install dependencies (cached unless pyproject.toml changes). + copy_parts = ["COPY pyproject.toml ./"] + if (project_root / "README.md").is_file(): + copy_parts.append("COPY README.md ./") + lines.extend(copy_parts) + + pyproject_data = tomllib.loads((project_root / "pyproject.toml").read_text()) + pkg_name = pyproject_data.get("project", {}).get("name", "").replace("-", "_") + if pkg_name: + lines.append(f"RUN mkdir -p src/{pkg_name} && touch src/{pkg_name}/__init__.py") + + force_includes = ( + pyproject_data.get("tool", {}) + .get("hatch", {}) + .get("build", {}) + .get("targets", {}) + .get("wheel", {}) + .get("force-include", {}) + ) + for src_path in force_includes: + lines.append(f"RUN mkdir -p '{src_path}'") + + lines.append("RUN uv pip install --no-cache .") + + # Layer 2 — install source code only (fast, deps already cached). + lines.extend( + [ + "COPY src/ src/", + "RUN uv pip install --no-cache --no-deps .", + ] + ) + + with tempfile.NamedTemporaryFile( + mode="w", + prefix="exgentic-base-", + suffix=".Dockerfile", + delete=False, + ) as fh: + fh.write("\n".join(lines) + "\n") + dockerfile = Path(fh.name) + try: + subprocess.run( + ["docker", "build", "-f", str(dockerfile), "-t", tag, str(project_root)], + check=True, + ) + finally: + dockerfile.unlink(missing_ok=True) + + @staticmethod + def _build_bench_image( + tag: str, + base_tag: str, + *, + req_path: Path | None = None, + setup_path: Path | None = None, + sysdeps_path: Path | None = None, + packages: list[str] | None = None, + docker_socket: bool = False, + ) -> None: + """Build the benchmark-specific image on top of the base image.""" + tmp_dir = Path(tempfile.mkdtemp(prefix="exgentic-bench-")) + try: + lines = [f"FROM {base_tag}"] + + if sysdeps_path is not None: + pkgs = read_lines(sysdeps_path) + if pkgs: + lines.append( + "RUN apt-get update && apt-get install -y " + " ".join(pkgs) + " && rm -rf /var/lib/apt/lists/*" + ) + + if req_path is not None: + shutil.copy2(req_path, tmp_dir / "requirements.txt") + lines.append("COPY requirements.txt /tmp/") + lines.append("RUN GIT_LFS_SKIP_SMUDGE=1 uv pip install --no-cache -r /tmp/requirements.txt") + + if packages: + lines.append(f"RUN uv pip install --no-cache {' '.join(packages)}") + + if docker_socket: + lines.append(_DOCKER_CLI_RUN) + + if setup_path is not None: + shutil.copy2(setup_path, tmp_dir / "setup.sh") + lines.append("COPY setup.sh /tmp/") + lines.append("RUN EXGENTIC_DOCKER_BUILD=1 bash /tmp/setup.sh") + + (tmp_dir / "Dockerfile").write_text("\n".join(lines) + "\n") + subprocess.run( + ["docker", "build", "-t", tag, str(tmp_dir)], + check=True, + ) + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + # ------------------------------------------------------------------ + # Single-image path (no project_root) + # ------------------------------------------------------------------ + + @staticmethod + def _image_tag( + name: str, + *file_paths: Path | None, + packages: list[str] | None = None, + docker_socket: bool = False, + ) -> str: + """Compute a deterministic image tag from content hashes.""" + h = hashlib.sha256() + for path in file_paths: + if path is not None: + h.update(path.read_text().encode()) + h.update(b"\x00") + if packages: + h.update("\n".join(sorted(packages)).encode()) + h.update(b"\x00") + if docker_socket: + h.update(b"docker-socket\x00") + safe_name = name.replace("/", "-") + return f"{safe_name}:{h.hexdigest()[:12]}" + + @staticmethod + def _image_exists(tag: str) -> bool: + result = subprocess.run( + ["docker", "image", "inspect", tag], + check=False, + capture_output=True, + text=True, + ) + return result.returncode == 0 + + @staticmethod + def _build_image( + tag: str, + req_path: Path | None, + setup_path: Path | None, + sysdeps_path: Path | None, + *, + packages: list[str] | None = None, + docker_socket: bool = False, + ) -> None: + """Build a single image without a project root.""" + py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + tmp_dir = Path(tempfile.mkdtemp(prefix="exgentic-docker-")) + try: + lines = [f"FROM python:{py_version}-slim"] + + if sysdeps_path is not None: + pkgs = read_lines(sysdeps_path) + if pkgs: + lines.append( + "RUN apt-get update && apt-get install -y " + " ".join(pkgs) + " && rm -rf /var/lib/apt/lists/*" + ) + + lines.extend( + [ + "RUN pip install --no-cache-dir uv", + "ENV UV_SYSTEM_PYTHON=true", + ] + ) + + if req_path is not None: + shutil.copy2(req_path, tmp_dir / "requirements.txt") + lines.append("COPY requirements.txt /tmp/") + lines.append("RUN GIT_LFS_SKIP_SMUDGE=1 uv pip install --no-cache -r /tmp/requirements.txt") + + if packages: + lines.append(f"RUN uv pip install --no-cache {' '.join(packages)}") + + if docker_socket: + lines.append(_DOCKER_CLI_RUN) + + if setup_path is not None: + shutil.copy2(setup_path, tmp_dir / "setup.sh") + lines.append("COPY setup.sh /tmp/") + lines.append("RUN EXGENTIC_DOCKER_BUILD=1 bash /tmp/setup.sh") + + (tmp_dir / "Dockerfile").write_text("\n".join(lines) + "\n") + + subprocess.run( + ["docker", "build", "-t", tag, str(tmp_dir)], + check=True, + ) + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/helpers.py b/labs/AgentStream/exgentic/src/exgentic/environment/helpers.py new file mode 100644 index 00000000..9c1f2f71 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/helpers.py @@ -0,0 +1,200 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Shared helpers for environment management.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from importlib import resources +from pathlib import Path + + +def get_exgentic_install_target() -> tuple[Path | None, list[str] | None]: + """Return ``(project_root, packages)`` for installing exgentic itself. + + If running from a source checkout, returns ``(project_root, None)`` + so backends install from source. Otherwise returns + ``(None, ["exgentic==X.Y.Z"])`` matching the running version. + + When installed from a local path (e.g. ``uv pip install /path/to/repo``), + uses PEP 610 ``direct_url.json`` to locate the original source tree. + Falls back to an unpinned ``exgentic`` spec for dev versions that + don't exist on PyPI. + """ + from ..adapters.runners._utils import find_project_root + + root = find_project_root() + if (root / "pyproject.toml").exists() and (root / "src" / "exgentic").is_dir(): + return root, None + + # Try PEP 610 direct_url.json — when installed from a local path, + # pip/uv record the source URL so we can find the original source tree. + try: + import json as _json + from importlib.metadata import distribution + + direct_url_text = distribution("exgentic").read_text("direct_url.json") + if direct_url_text: + url = _json.loads(direct_url_text).get("url", "") + if url.startswith("file://"): + source_path = Path(url.removeprefix("file://")) + if (source_path / "pyproject.toml").exists() and (source_path / "src" / "exgentic").is_dir(): + return source_path, None + except Exception: + pass + + from importlib.metadata import version + + ver = version("exgentic") + # Dev versions (e.g. 0.3.3.dev32+ga685f27) don't exist on PyPI. + # Fall back to unpinned install so the latest release is used. + if ".dev" in ver or "+" in ver: + return None, ["exgentic"] + return None, [f"exgentic=={ver}"] + + +def require_uv() -> str: + """Return the path to ``uv``, raising a clear error if not found.""" + uv = shutil.which("uv") + if uv is None: + raise RuntimeError( + "Could not find 'uv' on PATH. " "Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh" + ) + return uv + + +_ENV_BLOCKLIST: frozenset[str] = frozenset( + { + "VIRTUAL_ENV", + "CONDA_DEFAULT_ENV", + "CONDA_PREFIX", + } +) +_ENV_PREFIX_BLOCKLIST: tuple[str, ...] = ("UV_", "PIP_", "VSCODE_") + + +def build_subprocess_env() -> dict: + """Build a filtered env dict for subprocess calls. + + Strips virtual-env manager vars and package-tool overrides + (``UV_*``, ``PIP_*``) that could redirect package installs or + change the Python version used by uv/pip. Preserves ``PATH``, + ``HOME``, and other vars needed for tools to run. + """ + env = { + k: v + for k, v in os.environ.items() + if k not in _ENV_BLOCKLIST and not any(k.startswith(p) for p in _ENV_PREFIX_BLOCKLIST) + } + env["GIT_LFS_SKIP_SMUDGE"] = "1" + return env + + +def install_project(uv: str, python_target: str, project_root: Path, env: dict) -> None: + """Install a Python project from *project_root* into the target Python.""" + subprocess.run( + [uv, "pip", "install", "--python", python_target, "--no-cache", str(project_root)], + check=True, + capture_output=True, + text=True, + env=env, + ) + + +def install_packages(uv: str, python_target: str, packages: list[str], env: dict) -> None: + """Install packages into the target Python environment.""" + subprocess.run( + [uv, "pip", "install", "--python", python_target, "--no-cache", *packages], + check=True, + capture_output=True, + text=True, + env=env, + ) + + +def install_requirements(uv: str, python_target: str, module_path: str, env: dict) -> None: + """Find and install requirements.txt into the target Python.""" + req_path = find_package_file(module_path, "requirements.txt") + if req_path is None: + return + lines = [ + line.strip() for line in req_path.read_text().splitlines() if line.strip() and not line.strip().startswith("#") + ] + if not lines: + return + subprocess.run( + [uv, "pip", "install", "--python", python_target, "-r", str(req_path)], + check=True, + capture_output=True, + text=True, + env=env, + ) + + +def run_setup_sh(module_path: str, env_dir: Path, *, venv_dir: Path | None = None) -> None: + """Run setup.sh in the environment directory. + + Args: + module_path: Dotted module path for locating setup.sh. + env_dir: Working directory for setup.sh execution. + venv_dir: If set, activates the venv in the subprocess. + """ + setup_path = find_package_file(module_path, "setup.sh") + if setup_path is None: + return + env = build_subprocess_env() + if venv_dir is not None: + env["VIRTUAL_ENV"] = str(venv_dir) + env["PATH"] = str(venv_dir / "bin") + os.pathsep + env.get("PATH", "") + subprocess.run(["bash", str(setup_path)], check=True, cwd=str(env_dir), env=env) + + +def find_package_file(module_path: str, filename: str) -> Path | None: + """Locate *filename* in the package directory for *module_path*.""" + parts = module_path.split(".") + for depth in range(len(parts) - 1, 1, -1): + package = ".".join(parts[:depth]) + try: + candidate = resources.files(package) / filename + except Exception: + continue + if candidate.is_file(): + return Path(str(candidate)) + return None + + +def read_lines(path: Path) -> list[str]: + """Read non-empty, non-comment lines from *path*.""" + return [line.strip() for line in path.read_text().splitlines() if line.strip() and not line.strip().startswith("#")] + + +def validate_system_deps(module_path: str) -> None: + """Check that system packages from ``system-deps.txt`` are installed.""" + sysdeps_path = find_package_file(module_path, "system-deps.txt") + if sysdeps_path is None: + return + pkgs = read_lines(sysdeps_path) + if not pkgs: + return + missing = [p for p in pkgs if shutil.which(p) is None and not _dpkg_installed(p)] + if missing: + raise RuntimeError( + f"Missing system packages required for install: {', '.join(missing)}. " + "Install them with: sudo apt-get install -y " + " ".join(missing) + ) + + +def _dpkg_installed(package: str) -> bool: + """Return *True* if *package* is installed via dpkg.""" + if shutil.which("dpkg") is None: + return False + result = subprocess.run( + ["dpkg", "-s", package], + check=False, + capture_output=True, + text=True, + ) + return result.returncode == 0 diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/instance.py b/labs/AgentStream/exgentic/src/exgentic/environment/instance.py new file mode 100644 index 00000000..dcf8be6e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/instance.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Shared EnvironmentManager instance.""" + +from __future__ import annotations + +from pathlib import Path + +from .manager import EnvironmentManager + + +def get_manager() -> EnvironmentManager: + """Return the shared EnvironmentManager instance. + + Environments live at ``~/.exgentic/`` — a fixed absolute path, + independent of the working directory or cache settings. + """ + return EnvironmentManager(base_dir=Path.home() / ".exgentic") diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/local.py b/labs/AgentStream/exgentic/src/exgentic/environment/local.py new file mode 100644 index 00000000..7a43eb9f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/local.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Local environment: installs dependencies into the current Python.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from .helpers import ( + build_subprocess_env, + install_packages, + install_project, + install_requirements, + require_uv, + run_setup_sh, + validate_system_deps, +) + + +class LocalBackend: + """Backend that installs dependencies into the current Python.""" + + def install( + self, + env_dir: Path, + *, + module_path: str | None = None, + **kwargs: object, + ) -> dict: + """Install dependencies into the current Python environment. + + Args: + env_dir: Root directory for data/markers. + module_path: Dotted module path for locating package resources. + **kwargs: Accepts ``project_root`` (Path) and ``packages`` (list). + + Returns: + Extra marker data (``python`` path). + """ + project_root: Path | None = kwargs.get("project_root") # type: ignore[assignment] + packages: list[str] | None = kwargs.get("packages") # type: ignore[assignment] + + if project_root is not None or packages or module_path is not None: + uv = require_uv() + env = build_subprocess_env() + # Ensure VIRTUAL_ENV is set so uv installs into the correct + # environment when the current Python lives inside a venv + # (e.g. when exgentic is installed as a uv tool). + if sys.prefix != sys.base_prefix: + env["VIRTUAL_ENV"] = sys.prefix + + if project_root is not None: + install_project(uv, sys.executable, project_root, env) + + if packages: + install_packages(uv, sys.executable, packages, env) + + if module_path is not None: + install_requirements(uv, sys.executable, module_path, env) + validate_system_deps(module_path) + run_setup_sh(module_path, env_dir) + + return {"python": sys.executable} + + def uninstall(self, env_dir: Path, marker_data: dict) -> None: + """No-op. Cannot remove deps from the current Python environment.""" diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/manager.py b/labs/AgentStream/exgentic/src/exgentic/environment/manager.py new file mode 100644 index 00000000..2cfe8d24 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/manager.py @@ -0,0 +1,232 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Environment manager: orchestrates install, uninstall, and queries.""" + +from __future__ import annotations + +import json +import shutil +from datetime import datetime, timezone +from enum import StrEnum +from pathlib import Path + +from .docker import DockerBackend +from .local import LocalBackend +from .protocol import EnvironmentBackend +from .venv import VenvBackend + + +class EnvType(StrEnum): + """Supported environment types.""" + + VENV = "venv" + LOCAL = "local" + DOCKER = "docker" + + +class EnvironmentManager: + """Manages isolated environments identified by name. + + Each environment lives at ``{base_dir}/{name}/`` and can have + multiple environment types (venv, local, docker) installed + simultaneously. The ``.installed`` marker file tracks which + types are present as a JSON dict keyed by :class:`EnvType`. + """ + + MARKER_FILE = ".installed" + + def __init__(self, base_dir: Path | None = None) -> None: + self.base_dir = base_dir or Path.home() / ".exgentic" + self._backends: dict[EnvType, EnvironmentBackend] = { + EnvType.VENV: VenvBackend(), + EnvType.LOCAL: LocalBackend(), + EnvType.DOCKER: DockerBackend(), + } + + # ------------------------------------------------------------------ + # Core operations + # ------------------------------------------------------------------ + + def install( + self, + name: str, + *, + env_type: EnvType = EnvType.VENV, + force: bool = False, + module_path: str | None = None, + project_root: Path | None = None, + packages: list[str] | None = None, + docker_socket: bool = False, + ) -> Path: + """Install an environment. + + Args: + name: Environment name (e.g. ``"benchmarks/tau2"``). + env_type: Type of environment to create. + force: Re-create even if already installed. + module_path: Dotted module path for locating package resources. + project_root: Root of a Python project to install into the env. + packages: Extra pip packages to install. + docker_socket: (Docker only) Install the Docker CLI binary so the + container can manage sibling containers via the mounted socket. + + Returns: + The environment directory path. + """ + env_type = EnvType(env_type) + if not force and self.is_installed(name, env_type=env_type): + return self.env_path(name) + + env_dir = self.env_path(name) + env_dir.mkdir(parents=True, exist_ok=True) + self._remove_marker_entry(name, env_type) + + backend = self._backends[env_type] + + # Build kwargs for the backend. + kwargs: dict[str, object] = {} + if project_root is not None: + kwargs["project_root"] = project_root + if packages is not None: + kwargs["packages"] = packages + if env_type is EnvType.DOCKER: + kwargs["name"] = name + kwargs["force"] = force + kwargs["docker_socket"] = docker_socket + + extra = backend.install(env_dir, module_path=module_path, **kwargs) + self._add_marker_entry(name, env_type, {"installed_at": _now_iso(), **extra}) + + return env_dir + + def uninstall(self, name: str, *, env_type: EnvType | None = None) -> None: + """Remove an installed environment. + + Args: + name: Environment name. + env_type: Specific type to remove, or *None* to remove all. + """ + if env_type is not None: + env_type = EnvType(env_type) + + env_dir = self.env_path(name) + if not env_dir.exists(): + return + + marker = self._read_marker(name) + + if env_type is None: + # Remove all env types, then the whole directory. + for et in EnvType: + if et in marker: + self._backends[et].uninstall(env_dir, marker.get(et, {})) + shutil.rmtree(env_dir) + return + + # Remove a single env type. + self._backends[env_type].uninstall(env_dir, marker.get(env_type, {})) + self._remove_marker_entry(name, env_type) + + # Clean up directory if no env types remain. + if not self._read_marker(name) and env_dir.exists(): + shutil.rmtree(env_dir) + + # ------------------------------------------------------------------ + # Queries + # ------------------------------------------------------------------ + + def is_installed(self, name: str, *, env_type: EnvType | None = None) -> bool: + """Check if an environment is installed. + + Args: + name: Environment name. + env_type: Check a specific type, or *None* for any. + """ + if env_type is not None: + env_type = EnvType(env_type) + marker = self._read_marker(name) + if env_type is None: + return bool(marker) + return env_type in marker + + def get_info(self, name: str) -> dict | None: + """Return installation info or *None* if not installed.""" + marker = self._read_marker(name) + if not marker: + return None + return {"name": name, "environments": marker} + + def list_installed(self) -> list[dict]: + """List all installed environments with details.""" + result: list[dict] = [] + if not self.base_dir.is_dir(): + return result + for child in sorted(self.base_dir.rglob(self.MARKER_FILE)): + try: + marker = json.loads(child.read_text()) + except (json.JSONDecodeError, ValueError): + continue + if isinstance(marker, dict) and marker: + name = str(child.parent.relative_to(self.base_dir)) + result.append({"name": name, "environments": marker}) + return result + + # ------------------------------------------------------------------ + # Paths & accessors + # ------------------------------------------------------------------ + + def env_path(self, name: str) -> Path: + """Return the environment directory path.""" + return self.base_dir / name + + def venv_python(self, name: str) -> str: + """Return the path to the venv Python binary.""" + return str(self.env_path(name) / "venv" / "bin" / "python") + + def docker_image(self, name: str) -> str | None: + """Return the Docker image tag, or *None* if not installed.""" + return self._read_marker(name).get(EnvType.DOCKER, {}).get("image") + + def local_python(self, name: str) -> str | None: + """Return the Python path used for local install, or *None*.""" + return self._read_marker(name).get(EnvType.LOCAL, {}).get("python") + + # ------------------------------------------------------------------ + # Marker management + # ------------------------------------------------------------------ + + def _read_marker(self, name: str) -> dict: + marker = self.env_path(name) / self.MARKER_FILE + if not marker.is_file(): + return {} + try: + data = json.loads(marker.read_text()) + return data if isinstance(data, dict) else {} + except (json.JSONDecodeError, ValueError): + return {} + + def _write_marker(self, name: str, data: dict) -> None: + env_dir = self.env_path(name) + env_dir.mkdir(parents=True, exist_ok=True) + (env_dir / self.MARKER_FILE).write_text(json.dumps(data, indent=2)) + + def _add_marker_entry(self, name: str, env_type: EnvType, info: dict) -> None: + data = self._read_marker(name) + data[env_type] = info + self._write_marker(name, data) + + def _remove_marker_entry(self, name: str, env_type: EnvType) -> None: + data = self._read_marker(name) + data.pop(env_type, None) + if data: + self._write_marker(name, data) + else: + marker = self.env_path(name) / self.MARKER_FILE + if marker.exists(): + marker.unlink() + + +def _now_iso() -> str: + """Return the current UTC time as an ISO 8601 string.""" + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/protocol.py b/labs/AgentStream/exgentic/src/exgentic/environment/protocol.py new file mode 100644 index 00000000..d1cf5f53 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/protocol.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Common protocol for environment backends.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Protocol + + +class EnvironmentBackend(Protocol): + """Uniform interface every environment backend must satisfy.""" + + def install(self, env_dir: Path, *, module_path: str | None = None, **kwargs: object) -> dict: + """Create / set up the environment. + + Args: + env_dir: Root directory for this environment. + module_path: Dotted module path for locating package resources. + **kwargs: Common options forwarded by the manager: + ``project_root`` (Path | None) - root of a Python project to install. + ``packages`` (list[str] | None) - extra pip packages to install. + Backends may also receive backend-specific options (e.g. ``name``, + ``force`` for Docker). + + Returns: + Extra marker data to persist (may be empty). + """ + ... + + def uninstall(self, env_dir: Path, marker_data: dict) -> None: + """Tear down the environment. + + Args: + env_dir: Root directory for this environment. + marker_data: Data previously stored in the marker file. + """ + ... diff --git a/labs/AgentStream/exgentic/src/exgentic/environment/venv.py b/labs/AgentStream/exgentic/src/exgentic/environment/venv.py new file mode 100644 index 00000000..fdafd39b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/environment/venv.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Venv environment: creates an isolated Python venv with dependencies.""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +from .helpers import ( + build_subprocess_env, + install_packages, + install_project, + install_requirements, + require_uv, + run_setup_sh, + validate_system_deps, +) + + +class VenvBackend: + """Backend that creates an isolated Python venv with dependencies.""" + + def install( + self, + env_dir: Path, + *, + module_path: str | None = None, + **kwargs: object, + ) -> dict: + """Create a venv-based environment. + + Args: + env_dir: Root directory for this environment. + module_path: Dotted module path for locating package resources. + **kwargs: Accepts ``project_root`` (Path) and ``packages`` (list). + + Returns: + Empty dict (no extra marker data). + """ + project_root: Path | None = kwargs.get("project_root") # type: ignore[assignment] + packages: list[str] | None = kwargs.get("packages") # type: ignore[assignment] + + venv_dir = env_dir / "venv" + if venv_dir.exists(): + shutil.rmtree(venv_dir) + + try: + uv = require_uv() + + subprocess.run( + [uv, "venv", str(venv_dir), "--python", f"{sys.version_info.major}.{sys.version_info.minor}"], + check=True, + capture_output=True, + text=True, + ) + + venv_py = str(venv_dir / "bin" / "python") + env = build_subprocess_env() + + if project_root is not None: + install_project(uv, venv_py, project_root, env) + + if packages: + install_packages(uv, venv_py, packages, env) + + if module_path is not None: + install_requirements(uv, venv_py, module_path, env) + validate_system_deps(module_path) + run_setup_sh(module_path, env_dir, venv_dir=venv_dir) + except BaseException: + if venv_dir.exists(): + shutil.rmtree(venv_dir, ignore_errors=True) + raise + + return {} + + def uninstall(self, env_dir: Path, marker_data: dict) -> None: + """Remove the venv directory.""" + venv_dir = env_dir / "venv" + if venv_dir.exists(): + shutil.rmtree(venv_dir) diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/__init__.py b/labs/AgentStream/exgentic/src/exgentic/integrations/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/__init__.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/__init__.py new file mode 100644 index 00000000..a5415540 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/__init__.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import os + +from .health import acheck_model_accessible +from .proxy import LitellmProxy +from .trace_cost import load_trace_cost +from .trace_logger import ( + DEFAULT_FILE, + FILE_ENV, + TraceLogger, + trace_logger, +) + +# When running inside the LiteLLM proxy subprocess, eagerly initialise the +# Exgentic cache so that ``litellm.cache`` is set before any request arrives. +# The parent process sets EXGENTIC_PROXY_CACHE_INIT=true when it launches the +# proxy with disk caching enabled. +if os.environ.get("EXGENTIC_PROXY_CACHE_INIT", "").lower() in ("true", "1"): + from ...utils.settings import get_settings + + get_settings() + +__all__ = [ + "LitellmProxy", + "trace_logger", + "TraceLogger", + "FILE_ENV", + "DEFAULT_FILE", + "load_trace_cost", + "acheck_model_accessible", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/__init__.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/__init__.py new file mode 100644 index 00000000..7966f185 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/__init__.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Litellm cache package.""" + +from .core import LLMCache, build_litellm_cache +from .key import ( + CacheKeyBuilder, + MessageNormalizer, + sanitize_messages_for_cache, + strip_date_time_from_text, +) +from .log import CacheLogger + +CustomCache = LLMCache + +__all__ = [ + "CacheKeyBuilder", + "CacheLogger", + "CustomCache", + "LLMCache", + "MessageNormalizer", + "build_litellm_cache", + "sanitize_messages_for_cache", + "strip_date_time_from_text", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/core.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/core.py new file mode 100644 index 00000000..2d23ee09 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/core.py @@ -0,0 +1,325 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Litellm cache implementation using deterministic keying.""" + +from __future__ import annotations + +import json +import time +from typing import Any, Optional + +from litellm.caching.caching import Cache +from litellm.main import ModelResponse + +from ....utils.settings import resolve_cache_path +from .key import ( + _NOT_HANDLED, + CacheKeyBuilder, + MessageNormalizer, + _hash_text, + _resolve_custom_param, +) +from .log import CacheLogger + +# --------------------------------------------------------------------------- +# Cache key store (sync/async boundary bookkeeping) +# --------------------------------------------------------------------------- + + +class CacheKeyStore: + """Remembers cache keys so add_cache can reuse the key from get_cache. + + Litellm's callback pipeline may lose/regenerate keys between the get and + add phases, especially in async streaming. + """ + + def __init__(self) -> None: + self._sync: dict[str, str] = {} + self._async: dict[str, str] = {} + + @staticmethod + def _valid(call_id: Any) -> bool: + return isinstance(call_id, str) + + # -- sync -- + + def remember_sync(self, call_id: Any, key: Optional[str]) -> None: + if self._valid(call_id) and key: + self._sync[call_id] = key + + def inject_sync_key(self, kwargs: dict[str, Any]) -> None: + call_id = kwargs.get("litellm_call_id") + if not self._valid(call_id) or "cache_key" in kwargs: + return + stored = self._sync.pop(call_id, None) + if stored is not None: + kwargs["cache_key"] = stored + + # -- async -- + + def remember_async(self, call_id: Any, key: Optional[str]) -> None: + if self._valid(call_id) and key: + self._async[call_id] = key + + def pop_async(self, call_id: Any) -> Optional[str]: + return self._async.pop(call_id, None) if self._valid(call_id) else None + + def clear_async(self, call_id: Any) -> None: + if self._valid(call_id): + self._async.pop(call_id, None) + + +# --------------------------------------------------------------------------- +# Cache helpers +# --------------------------------------------------------------------------- + + +def _is_empty_response(result: Any) -> bool: + """True if result is None or a ModelResponse with no content/tool_calls.""" + if result is None: + return True + if not isinstance(result, ModelResponse): + return False + msg = result.choices[0].message + return msg.content is None and not msg.tool_calls and not msg.function_call + + +def _extract_max_age(kwargs: dict[str, Any]) -> float: + cc = kwargs.get("cache", {}) + return cc.get("s-maxage") or cc.get("s-max-age") or float("inf") + + +def _classify_miss(cached_result: Any, max_age: float) -> tuple[str, Optional[int]]: + if isinstance(cached_result, dict) and "timestamp" in cached_result and max_age is not None: + age = time.time() - cached_result["timestamp"] + if age > max_age: + return "expired", int(age) + return "not_found", None + + +def _dump_raw_key(raw_key: Optional[str]) -> Optional[str]: + if raw_key is None: + return None + return json.dumps(raw_key, ensure_ascii=True, separators=(",", ":")) + + +# --------------------------------------------------------------------------- +# Cache +# --------------------------------------------------------------------------- + + +class LLMCache(Cache): + """Disk-backed LLM response cache with deterministic keys and diagnostics.""" + + def __init__(self, *, delete_time_from_messages: bool = False, **kwargs: Any) -> None: + self._normalizer = MessageNormalizer(strip_time=delete_time_from_messages) + self._key_store = CacheKeyStore() + self._log = CacheLogger( + disk_cache_dir=kwargs.get("disk_cache_dir"), + strip_time=delete_time_from_messages, + ) + super().__init__(**kwargs) + # Must be created after super().__init__ so self is a valid Cache instance. + self._key_builder = CacheKeyBuilder(self._normalizer, self) + + @property + def logger(self) -> Any: + return self._log.raw + + # -- litellm Cache interface -- + + def get_cache_key(self, **kwargs: Any) -> str: # type: ignore[override] + key, _, _, _ = self._key_builder.build(kwargs) + return key or "" + + def get_cache(self, dynamic_cache_object: Any = None, **kwargs: Any) -> Any: # type: ignore[override] + if self.should_use_cache(**kwargs) is not True: + self._log.detail("skip", reason="disabled", model=kwargs.get("model")) + return None + + key, raw_key, source, _ = self._build_key_logged(kwargs) + max_age = _extract_max_age(kwargs) + self._key_store.remember_sync(kwargs.get("litellm_call_id"), key) + + if key is None: + self._log_miss(reason="cache_key_none", source=source, kwargs=kwargs) + return None + + backend = dynamic_cache_object if dynamic_cache_object is not None else self.cache + cached_result = backend.get_cache(key, **kwargs) + return self._finalize_lookup(cached_result, key, source, raw_key, kwargs, max_age) + + async def async_get_cache(self, dynamic_cache_object: Any = None, **kwargs: Any) -> Any: # type: ignore[override] + if self.should_use_cache(**kwargs) is not True: + self._log.detail("skip", reason="disabled", model=kwargs.get("model")) + return None + + key, raw_key, source, _ = self._build_key_logged(kwargs) + max_age = _extract_max_age(kwargs) + self._key_store.remember_async(kwargs.get("litellm_call_id"), key) + + if key is None: + self._log_miss(reason="cache_key_none", source=source, kwargs=kwargs) + return None + + if dynamic_cache_object is not None: + cached_result = await dynamic_cache_object.async_get_cache(key, **kwargs) + else: + cached_result = await self.cache.async_get_cache(key, **kwargs) + return self._finalize_lookup(cached_result, key, source, raw_key, kwargs, max_age) + + def add_cache(self, result: Any, **kwargs: Any) -> None: # type: ignore[override] + self._log.detail( + "add_cache_called", + result_type=type(result).__name__, + model=kwargs.get("model"), + ) + if _is_empty_response(result): + self._log.detail("skip", reason="empty_assistant_content", model=kwargs.get("model")) + return + self._key_store.inject_sync_key(kwargs) + super().add_cache(result, **kwargs) + self._log.detail( + "add_cache_written", + result_type=type(result).__name__, + model=kwargs.get("model"), + ) + + async def async_add_cache(self, result: Any, dynamic_cache_object: Any = None, **kwargs: Any) -> None: # type: ignore[override] + call_id = kwargs.get("litellm_call_id") + self._log.detail( + "async_add_cache_called", + result_type=type(result).__name__, + model=kwargs.get("model"), + call_id=call_id, + ) + + if _is_empty_response(result): + self._key_store.clear_async(call_id) + self._log.detail("skip", reason="empty_assistant_content", model=kwargs.get("model")) + return + + stored_key = self._key_store.pop_async(call_id) + if stored_key: + self._verify_async_key_if_debug(stored_key, kwargs) + cached_data = {"timestamp": time.time(), "response": result} + if dynamic_cache_object is not None: + await dynamic_cache_object.async_set_cache(stored_key, cached_data, **kwargs) + else: + await self.cache.async_set_cache(stored_key, cached_data, **kwargs) + self._log.detail( + "async_add_cache_written", + key=stored_key, + result_type=type(result).__name__, + model=kwargs.get("model"), + ) + return + + await super().async_add_cache(result, dynamic_cache_object=dynamic_cache_object, **kwargs) + self._log.detail( + "async_add_cache_written", + result_type=type(result).__name__, + model=kwargs.get("model"), + ) + + # Override so litellm's parent class uses our normalizer. + def _get_param_value(self, param: str, kwargs: dict) -> Optional[str]: # type: ignore[override] + result = _resolve_custom_param(param, kwargs, self._normalizer) + if result is not _NOT_HANDLED: + return result + return super()._get_param_value(param, kwargs) + + # -- shared lookup helpers -- + + def _finalize_lookup( + self, + cached_result: Any, + key: str, + source: str, + raw_key: Optional[str], + kwargs: dict[str, Any], + max_age: float, + ) -> Any: + """Apply max-age logic and log the outcome. Shared by sync and async get.""" + result = self._get_cache_logic(cached_result=cached_result, max_age=max_age) + if result is not None: + self._log.hit() + self._log.detail("hit", key=key, key_source=source, model=kwargs.get("model")) + else: + reason, age_seconds = _classify_miss(cached_result, max_age) + self._log.miss() + self._log.detail( + "miss", + reason=reason, + key=key, + key_source=source, + raw_key=_dump_raw_key(raw_key), + model=kwargs.get("model"), + age=age_seconds, + max_age=None if max_age == float("inf") else max_age, + ) + return result + + def _log_miss(self, *, reason: str, source: str, kwargs: dict[str, Any]) -> None: + self._log.miss() + self._log.detail("miss", reason=reason, model=kwargs.get("model"), source=source) + + def _build_key_logged(self, kwargs: dict[str, Any]) -> tuple[Optional[str], Optional[str], str, list[str]]: + if self._log.is_debug: + from .key import _EXCLUDED_KEY_FIELDS + + self._log.detail( + "cache_key_input", + keys=sorted(kwargs.keys()), + excluded=sorted(_EXCLUDED_KEY_FIELDS), + ) + + key, raw_key, source, fields = self._key_builder.build(kwargs) + + if self._log.is_debug: + if source in ("explicit", "preset"): + self._log.detail(f"cache_key_{source}", key=key) + else: + raw_hash = _hash_text(raw_key) if raw_key else None + self._log.detail( + "cache_key_generated", + key=key, + raw_key=_dump_raw_key(raw_key), + raw_key_hash=raw_hash, + raw_key_len=len(raw_key) if raw_key else 0, + ) + self._log.material( + { + "cache_key": key, + "raw_key_hash": raw_hash, + "raw_key_len": len(raw_key) if raw_key else 0, + "included_fields": fields, + } + ) + + return key, raw_key, source, fields + + def _verify_async_key_if_debug(self, stored_key: str, kwargs: dict[str, Any]) -> None: + if not self._log.is_debug: + return + current_key, _, _, _ = self._key_builder.build(dict(kwargs)) + if current_key and current_key != stored_key: + self._log.detail("cache_key_mismatch", stored_key=stored_key, current_key=current_key) + else: + self._log.detail("cache_key_async_reuse", key=stored_key) + + +# --------------------------------------------------------------------------- +# Factory +# --------------------------------------------------------------------------- + + +def build_litellm_cache(settings: Any) -> Cache: + cache_dir = getattr(settings, "cache_dir", ".exgentic") + litellm_cache_dir = getattr(settings, "litellm_cache_dir", "~/.cache/exgentic/litellm") + return LLMCache( + type="disk", + disk_cache_dir=resolve_cache_path(cache_dir, litellm_cache_dir), + delete_time_from_messages=settings.litellm_delete_time_from_cache_key, + ) diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/key.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/key.py new file mode 100644 index 00000000..cd06ec01 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/key.py @@ -0,0 +1,289 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Cache key construction and message normalization.""" + +from __future__ import annotations + +import hashlib +import json +import re +from typing import Any, Optional + +from litellm.caching.caching import Cache, ModelParamHelper, litellm +from litellm.types.utils import all_litellm_params + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + + +def _to_stable_json(value: Any) -> str: + """Deterministic JSON string; falls back to str() for non-serializable types.""" + if not isinstance(value, (dict, list)): + return str(value) + try: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + except TypeError: + return str(value) + + +def _hash_text(value: str) -> str: + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +# --------------------------------------------------------------------------- +# 1. Message normalization +# --------------------------------------------------------------------------- + +_MONTH_PATTERN = ( + r"(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|" + r"Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)" +) + +_TODAYS_DATE_REGEX = re.compile(r"(?i)\bToday'?s date(?:\s+is|:)\s*[^\n]*") + +_DATE_TIME_REGEXES = [ + re.compile(r"\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?" r"(?:Z|[+-]\d{2}:\d{2})?\b"), + re.compile(r"\b\d{4}[-/]\d{1,2}[-/]\d{1,2}\b"), + re.compile(r"\b\d{1,2}[-/]\d{1,2}[-/]\d{2,4}\b"), + re.compile( + rf"\b{_MONTH_PATTERN}\s+\d{{1,2}}(?:st|n(?:d)|rd|th)?(?:,\s*\d{{2}}|\s+\d{{4}})?\b", + re.IGNORECASE, + ), + re.compile( + rf"\b\d{{1,2}}(?:st|n(?:d)|rd|th)?\s+{_MONTH_PATTERN}(?:,\s*\d{{2}}|\s+\d{{4}})?\b", + re.IGNORECASE, + ), + re.compile(r"\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b"), + re.compile(r"\b\d{1,2}\s?(?:[AaPp][Mm])\b"), +] + + +class MessageNormalizer: + """Produces stable, cache-friendly representations of LLM message lists. + + Strips "Today's date" lines and date/time literals (when requested), + removes non-deterministic IDs, drops None values and empty assistant messages. + """ + + def __init__(self, strip_time: bool) -> None: + self._strip_time = strip_time + + def normalize(self, messages: Any) -> Any: + normalized = self._normalize_value(messages) + if isinstance(normalized, list): + normalized = [m for m in normalized if not self._is_empty_assistant(m)] + return normalized + + def to_json(self, messages: Any) -> Optional[str]: + """Normalize then serialize. Returns None if messages is None.""" + if messages is None: + return None + normalized = self.normalize(messages) + try: + return json.dumps(normalized, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + except TypeError: + return str(normalized) + + # -- recursive value cleaning -- + + def _normalize_value(self, value: Any) -> Any: + if isinstance(value, str): + return self._clean_string(value) + if isinstance(value, list): + return [self._normalize_value(item) for item in value] + if isinstance(value, dict): + return self._clean_dict(value) + for attr in ("model_dump", "dict"): + fn = getattr(value, attr, None) + if callable(fn): + return self._normalize_value(fn()) + return value + + def _clean_string(self, text: str) -> str: + cleaned = _TODAYS_DATE_REGEX.sub("", text) + if self._strip_time: + cleaned = _strip_date_time_literals(cleaned) + return cleaned + + def _clean_dict(self, d: dict) -> dict: + is_tool_msg = d.get("role") == "tool" + result: dict[Any, Any] = {} + for key, val in d.items(): + if val is None or key == "tool_call_id": + continue + if key == "id" and is_tool_msg: + continue + if key == "tool_calls" and isinstance(val, list): + result[key] = [ + self._normalize_value( + {k: v for k, v in call.items() if k != "id"} + if isinstance(call, dict) and "id" in call + else call + ) + for call in val + ] + continue + result[key] = self._normalize_value(val) + return result + + @staticmethod + def _is_empty_assistant(msg: Any) -> bool: + return ( + isinstance(msg, dict) + and msg.get("role") == "assistant" + and isinstance(msg.get("content"), str) + and msg.get("content", "").strip() == "(no content)" + ) + + +def _strip_date_time_literals(text: str) -> str: + if not text: + return text + for regex in _DATE_TIME_REGEXES: + text = regex.sub("", text) + return re.sub(r"\s+", " ", text).strip() + + +# Backward-compatible module-level aliases. +strip_date_time_from_text = _strip_date_time_literals + + +def sanitize_messages_for_cache(messages: Any, *, strip_time: bool) -> Any: + return MessageNormalizer(strip_time).normalize(messages) + + +# --------------------------------------------------------------------------- +# 2. Cache key building +# --------------------------------------------------------------------------- + +_EXCLUDED_KEY_FIELDS = frozenset( + { + "litellm_call_id", + "litellm_trace_id", + "litellm_logging_obj", + "litellm_metadata", + "proxy_server_request", + "parent_otel_span", + "secret_fields", + "shared_session", + "use_in_pass_through", + "use_litellm_proxy", + "model_info", + "provider_specific_header", + "user", + "dynamic_cache_object", + } +) + + +def _sort_tools(tools: Any) -> Any: + """Return tools in a deterministic order for stable cache keys.""" + if not isinstance(tools, list): + return tools + + def sort_key(item: Any) -> tuple[str, str, str]: + if not isinstance(item, dict): + return ("", "", str(item)) + tool_type = str(item.get("type", "")) + func = item.get("function") + func_name = str(func.get("name", "")) if isinstance(func, dict) else "" + try: + stable = json.dumps(item, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + except TypeError: + stable = str(item) + return (tool_type, func_name, stable) + + return sorted(tools, key=sort_key) + + +_NOT_HANDLED = object() + + +def _resolve_custom_param(param: str, kwargs: dict[str, Any], normalizer: MessageNormalizer) -> Any: + """Resolve params that need custom serialization. + + Returns the string value for params we handle (messages, tools, sentinels), + or _NOT_HANDLED for params that should fall through to litellm's default. + + Shared by CacheKeyBuilder and LLMCache._get_param_value so the same + serialization logic is never duplicated. + """ + if param in ("litellm_logging_obj", "litellm_call_id"): + return param # sentinel — present but content-irrelevant + if param == "tools": + return json.dumps(_sort_tools(kwargs.get("tools", [])), sort_keys=True) + if param == "messages": + return normalizer.to_json(kwargs.get("messages")) + return _NOT_HANDLED + + +class CacheKeyBuilder: + """Builds deterministic cache keys from LLM call kwargs. + + Pure logic — no logging, no side effects. + """ + + def __init__(self, normalizer: MessageNormalizer, cache: Cache) -> None: + self._normalizer = normalizer + self._cache = cache + + def build(self, kwargs: dict[str, Any]) -> tuple[Optional[str], Optional[str], str, list[str]]: + """Return (cache_key, raw_key, source, included_fields).""" + if "cache_key" in kwargs: + return kwargs["cache_key"], None, "explicit", [] + + preset = self._cache._get_preset_cache_key_from_kwargs(**kwargs) + if preset is not None: + return preset, None, "preset", [] + + raw_key, included_fields = self._build_raw_key(kwargs) + + hashed = Cache._get_hashed_cache_key(raw_key) + # Ensure metadata is a dict — litellm's _add_namespace_to_cache_key + # does metadata.get(...) which crashes when metadata is None + # (happens on the Responses API path used by Claude Code). + safe_kwargs = kwargs + if kwargs.get("metadata") is None and "metadata" in kwargs: + safe_kwargs = {**kwargs, "metadata": {}} + hashed = self._cache._add_namespace_to_cache_key(hashed, **safe_kwargs) + self._cache._set_preset_cache_key_in_kwargs(preset_cache_key=hashed, **kwargs) + + return hashed, raw_key, "generated", included_fields + + def _build_raw_key(self, kwargs: dict[str, Any]) -> tuple[str, list[str]]: + filtered = {k: v for k, v in kwargs.items() if k not in _EXCLUDED_KEY_FIELDS} + combined_params = ModelParamHelper._get_all_llm_api_params() + litellm_params = all_litellm_params + + raw_key = "" + included: list[str] = [] + + for param in sorted(filtered): + value = self._resolve(param, filtered, combined_params, litellm_params) + if value is not None: + raw_key += f"{param}: {value}" + included.append(param) + + return raw_key, included + + def _resolve( + self, + param: str, + kwargs: dict[str, Any], + combined: set[str], + litellm_only: set[str], + ) -> Optional[str]: + if param in combined: + result = _resolve_custom_param(param, kwargs, self._normalizer) + if result is not _NOT_HANDLED: + return result + # Fallback to litellm's default handling (e.g. model name normalization). + return self._cache._get_param_value(param, kwargs) + + if param not in litellm_only and litellm.enable_caching_on_provider_specific_optional_params: + value = kwargs[param] + return _to_stable_json(value) if value is not None else None + + return None diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/log.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/log.py new file mode 100644 index 00000000..f0f5d22a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache/log.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Cache logging utilities.""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import Any, Optional + +from ....core.context import try_get_context +from ....utils.settings import get_settings + + +def _resolve_session_id() -> Optional[str]: + ctx = try_get_context() + if ctx is not None: + return ctx.session_id + return None + + +def _cache_log_path(output_dir: str, run_id: str, session_id: Optional[str], role: str, filename: str) -> Path: + base = Path(output_dir) / run_id + if session_id: + return base / "sessions" / session_id / role / "litellm" / filename + return base / "run" / "litellm" / filename + + +class CacheLogger: + """File-based cache logger scoped to run + session. + + INFO - "hit" / "miss" for every lookup + DEBUG - structured detail lines and JSON material dumps + """ + + def __init__(self, disk_cache_dir: Optional[str], strip_time: bool) -> None: + self._disk_cache_dir = disk_cache_dir + self._strip_time = strip_time + self._logger: Optional[logging.Logger] = None + self._logger_key: Optional[tuple[str, str, Optional[str]]] = None + self._init_logged = False + + @property + def raw(self) -> logging.Logger: + """The underlying stdlib logger (for external consumers).""" + return self._ensure() + + @property + def is_debug(self) -> bool: + return self._ensure().isEnabledFor(logging.DEBUG) + + def hit(self) -> None: + self._ensure().info("hit") + + def miss(self) -> None: + self._ensure().info("miss") + + def detail(self, status: str, **fields: Any) -> None: + log = self._ensure() + if not log.isEnabledFor(logging.DEBUG): + return + parts = [status] + [f"{k}={v}" for k, v in fields.items() if v is not None] + log.debug(" ".join(parts)) + + def material(self, payload: dict[str, Any]) -> None: + log = self._ensure() + if log.isEnabledFor(logging.DEBUG): + log.debug( + "cache_material %s", + json.dumps(payload, ensure_ascii=True, separators=(",", ":")), + ) + + # -- private -- + + def _ensure(self) -> logging.Logger: + ctx = try_get_context() + if ctx is None: + return self._noop_logger() + + session_id = _resolve_session_id() + role = ctx.role.value if hasattr(ctx, "role") else "framework" + key = (ctx.run_id, ctx.output_dir, session_id, role) + if self._logger is not None and self._logger_key == key: + return self._logger + + name = f"exgentic.cache.{ctx.run_id}" + (f".{session_id}" if session_id else "") + logger = logging.getLogger(name) + + if self._logger_key != key: + for h in list(logger.handlers): + logger.removeHandler(h) + h.close() + self._init_logged = False + + if not logger.handlers: + path = _cache_log_path(ctx.output_dir, ctx.run_id, session_id, role, "cache.log") + path.parent.mkdir(parents=True, exist_ok=True) + level = logging.DEBUG if get_settings().log_level == "DEBUG" else logging.INFO + handler = logging.FileHandler(path, encoding="utf-8") + handler.setLevel(level) + handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(handler) + logger.setLevel(level) + logger.propagate = False + + if not self._init_logged and logger.isEnabledFor(logging.DEBUG): + logger.debug( + "cache_init disk_cache_dir=%s delete_time_from_messages=%s", + self._disk_cache_dir, + self._strip_time, + ) + self._init_logged = True + + self._logger = logger + self._logger_key = key + return logger + + def _noop_logger(self) -> logging.Logger: + logger = logging.getLogger("exgentic.cache.noop") + if not any(isinstance(h, logging.NullHandler) for h in logger.handlers): + logger.addHandler(logging.NullHandler()) + logger.propagate = False + self._logger = logger + self._logger_key = None + return logger diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache_utils.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache_utils.py new file mode 100644 index 00000000..1b782afa --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/cache_utils.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Compatibility wrapper for litellm cache utilities.""" + +from .cache import * # noqa: F403 diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/config.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/config.py new file mode 100644 index 00000000..561366c4 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/config.py @@ -0,0 +1,114 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""LiteLLM configuration helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class LitellmSettings: + """Global LiteLLM configuration.""" + + litellm_caching: bool + litellm_delete_time_from_cache_key: bool + cache_dir: str + litellm_cache_dir: str + log_level: str + drop_params: bool = True + modify_params: bool = True + timeout: int = 180 + + +def configure_litellm( + *, + config: LitellmSettings, + cache_only: bool = False, +) -> None: + """Configure LiteLLM for Exgentic. + + Args: + config: Explicit LiteLLM configuration. + cache_only: When True, only refresh cache configuration. + """ + _configure_cache(config) + if cache_only: + return + _configure_logging(config) + _configure_callbacks() + _configure_inference(config) + + +def _configure_cache(config: LitellmSettings) -> None: + if not config.litellm_caching: + return + try: + import litellm + except ImportError: + return + from .cache_utils import build_litellm_cache + + litellm.cache = build_litellm_cache(config) + litellm.enable_cache() + + +def _configure_callbacks() -> None: + try: + import litellm + except ImportError: + return + from .trace_logger import ( + AsyncTraceLogger, + SyncTraceLogger, + async_trace_logger, + sync_trace_logger, + ) + + if not any(isinstance(cb, SyncTraceLogger) for cb in litellm.success_callback): + litellm.success_callback.append(sync_trace_logger) + if not any(isinstance(cb, AsyncTraceLogger) for cb in litellm.success_callback): + litellm.success_callback.append(async_trace_logger) + if not any(isinstance(cb, SyncTraceLogger) for cb in litellm.failure_callback): + litellm.failure_callback.append(sync_trace_logger) + if not any(isinstance(cb, AsyncTraceLogger) for cb in litellm.failure_callback): + litellm.failure_callback.append(async_trace_logger) + if not any(isinstance(cb, SyncTraceLogger) for cb in litellm._async_success_callback): + litellm._async_success_callback.append(sync_trace_logger) + if not any(isinstance(cb, AsyncTraceLogger) for cb in litellm._async_success_callback): + litellm._async_success_callback.append(async_trace_logger) + if not any(isinstance(cb, SyncTraceLogger) for cb in litellm._async_failure_callback): + litellm._async_failure_callback.append(sync_trace_logger) + if not any(isinstance(cb, AsyncTraceLogger) for cb in litellm._async_failure_callback): + litellm._async_failure_callback.append(async_trace_logger) + + +def _configure_logging(config: LitellmSettings) -> None: + try: + import logging + + import litellm + except ImportError: + return + + level = logging.DEBUG if str(config.log_level).upper() == "DEBUG" else logging.WARNING + litellm.log_level = "DEBUG" if level == logging.DEBUG else "WARNING" + litellm.suppress_debug_info = level != logging.DEBUG + litellm.set_verbose = level == logging.DEBUG + + for name in ("LiteLLM", "LiteLLM Proxy", "LiteLLM Router", "litellm"): + logger = logging.getLogger(name) + logger.setLevel(level) + logger.propagate = False + + +def _configure_inference(config: LitellmSettings) -> None: + try: + import litellm + except ImportError: + return + + litellm.drop_params = config.drop_params + litellm.modify_params = config.modify_params + litellm.timeout = config.timeout diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/health.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/health.py new file mode 100644 index 00000000..a0ccc91d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/health.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Minimal LiteLLM model accessibility check.""" + +from __future__ import annotations + +import logging + + +async def acheck_model_accessible(model: str) -> None: + """Raise if LiteLLM cannot access the configured model. + + Uses a minimal ``acompletion`` call instead of ``ahealth_check`` because + the latter pulls in ``litellm.proxy`` internals that require the optional + ``backoff`` package (only declared under ``litellm[proxy]``). + """ + import litellm + + await litellm.acompletion( + model=model, + messages=[{"role": "user", "content": "hi"}], + max_tokens=10, + ) + + +def check_model_accessible_sync( + model: str, + logger: logging.Logger, + timeout: float = 15.0, +) -> None: + """Synchronous wrapper for model health check. + + Args: + model: The model identifier to check + logger: Logger for info/error messages + timeout: Timeout in seconds for the health check + + Raises: + RuntimeError: If the model is not accessible + """ + from ...utils.sync import run_sync + + logger.info("Running LiteLLM model health check (model=%s)", model) + try: + run_sync(acheck_model_accessible(model), timeout=timeout) + logger.info("Model health check passed for %s", model) + except Exception as exc: + error_msg = getattr(exc, "message", "") or str(exc) or repr(exc) + logger.error("Model health check failed for %s: %s", model, error_msg) + raise RuntimeError(f"Model {model} is not accessible: {error_msg}") from exc diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/proxy.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/proxy.py new file mode 100644 index 00000000..954adcef --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/proxy.py @@ -0,0 +1,296 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Lightweight helper to launch a single-model LiteLLM proxy for the lifetime of an object. + +Usage (expects OPENAI_API_BASE/OPENAI_API_KEY already set for your backend): + from . import LitellmProxy + + with LitellmProxy(model="openai/gpt-4o-mini") as proxy: + os.environ["OPENAI_API_BASE"] = proxy.base_url + # run your Codex/Exgentic flow that expects an OpenAI-compatible endpoint +""" + +from __future__ import annotations + +import json +import os +import socket +import subprocess +import tempfile +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Optional + +from ...core.context import try_get_context +from ...core.types.model_settings import ModelSettings +from ...utils.paths import get_session_paths +from ...utils.settings import get_settings +from .trace_logger import ( + DEFAULT_FILE as DEFAULT_USAGE_FILE, +) + +TRACE_CALLBACK = "exgentic.integrations.litellm.trace_logger.trace_logger" +ASYNC_TRACE_CALLBACK = "exgentic.integrations.litellm.trace_logger.async_trace_logger" + + +def _get_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return int(s.getsockname()[1]) + + +class LitellmProxy: + """Launch a LiteLLM proxy serving a single model. + + The proxy process is started on creation (or __enter__) and torn down on close/__exit__. + """ + + def __init__( + self, + model: str, + *, + port: Optional[int] = None, + model_alias_map: Optional[dict[str, str]] = None, + env: Optional[dict[str, str]] = None, + log_path: Optional[str] = None, + usage_log_path: Optional[str] = None, + startup_timeout: float = 15.0, + model_settings: ModelSettings | None = None, + ) -> None: + self.model = model + self.port = port or _get_free_port() + self.model_alias_map = model_alias_map or {} + self._env_overrides = env or {} + self._log_path = log_path + self.usage_log_path = usage_log_path + self.startup_timeout = startup_timeout + self.model_settings = model_settings or ModelSettings() + self._log_file = None + self._proc: Optional[subprocess.Popen[str]] = None + self._config_file: Optional[Path] = None + + @property + def base_url(self) -> str: + return f"http://127.0.0.1:{self.port}" + + def start(self) -> None: + if self._proc and self._proc.poll() is None: + return + + env = self._build_env() + self._set_trace_log_env(env, self._resolve_usage_log_path(), self._resolve_session_root()) + self._config_file = self._write_config_file() + config_dir = str(self._config_file.parent) + env["PYTHONPATH"] = os.pathsep.join([config_dir, env.get("PYTHONPATH", "")]) + cmd = self._build_command(self._config_file) + stdout, stderr = self._build_stdio() + + self._proc = subprocess.Popen( + cmd, + stdout=stdout, + stderr=stderr, + text=True, + env=env, + ) + + # Wait briefly for the proxy to bind; fail fast if it exits. + self._wait_for_startup(stdout=stdout) + + def _build_command(self, config_path: Path) -> list[str]: + cmd = ["litellm", "--port", str(self.port)] + cmd.extend(["--config", str(config_path)]) + return cmd + + def _build_env(self) -> dict[str, str]: + env = os.environ.copy() + env.update(self._env_overrides) + + repo_root = Path(__file__).resolve().parents[4] + src_path = repo_root / "src" + extra_paths: list[str] = [str(src_path), str(repo_root)] + + settings = get_settings() + ctx = try_get_context() + if ctx is not None: + for k, v in ctx.to_env().items(): + env.setdefault(k, v) + + existing = {key.lower() for key in env} + for key, value in settings.get_env().items(): + if key.lower() in existing: + continue + env[key] = value + + # Keep proxy cache bootstrap behavior enabled by default for subprocess startup. + env["EXGENTIC_PROXY_CACHE_INIT"] = "true" + + env["PYTHONPATH"] = os.pathsep.join([*extra_paths, env.get("PYTHONPATH", "")]) + return env + + def _resolve_usage_log_path(self) -> Path: + if self.usage_log_path: + return Path(self.usage_log_path) + if self._log_path: + return Path(self._log_path).with_name("trace.jsonl") + return Path(DEFAULT_USAGE_FILE) + + def _resolve_session_root(self) -> Path | None: + ctx = try_get_context() + if ctx is not None: + session_id = ctx.session_id + if session_id is not None: + return get_session_paths(session_id).root + return None + + @staticmethod + def _set_trace_log_env(env: dict[str, str], usage_path: Path, session_root: Optional[Path]) -> None: + from .trace_logger import FILE_ENV + + env.setdefault(FILE_ENV, str(usage_path)) + + def _build_litellm_params(self) -> dict[str, object]: + litellm_params: dict[str, object] = {"model": self.model} + if self.model_settings.temperature is not None: + litellm_params["temperature"] = self.model_settings.temperature + if self.model_settings.max_tokens is not None: + litellm_params["max_tokens"] = self.model_settings.max_tokens + if self.model_settings.top_p is not None: + litellm_params["top_p"] = self.model_settings.top_p + if self.model_settings.reasoning_effort is not None: + litellm_params["reasoning_effort"] = self.model_settings.reasoning_effort + return litellm_params + + def _build_config_data(self) -> dict[str, object]: + trace_cb = TRACE_CALLBACK + async_cb = ASYNC_TRACE_CALLBACK + config_data: dict[str, object] = { + "model_list": [ + { + "model_name": self.model, + "litellm_params": self._build_litellm_params(), + } + ], + "litellm_settings": { + "success_callback": [trace_cb, async_cb], + "failure_callback": [trace_cb, async_cb], + # Force chat/completions instead of /responses for Anthropic + # message translation — many backends (Azure proxies, etc.) + # don't expose the newer Responses API endpoint. + "use_chat_completions_url_for_anthropic_messages": True, + }, + } + router_settings: dict[str, object] = {} + if self.model_alias_map: + router_settings["model_group_alias"] = self.model_alias_map + if self.model_settings.num_retries is not None: + router_settings["num_retries"] = self.model_settings.num_retries + router_settings["retry_after"] = self.model_settings.retry_after + if router_settings: + config_data["router_settings"] = router_settings + return config_data + + def _write_config_file(self) -> Path: + if self._log_path: + cfg_path = Path(self._log_path).with_name("litellm_config.json") + else: + cfg_path = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".json").name) + config_data = self._build_config_data() + with open(cfg_path, "w", encoding="utf-8") as fh: + json.dump(config_data, fh) + return cfg_path + + def _build_stdio(self): + stdout = subprocess.PIPE + stderr = subprocess.PIPE + if self._log_path: + self._log_file = open(self._log_path, "w", encoding="utf-8") + stdout = self._log_file + stderr = subprocess.STDOUT + return stdout, stderr + + def _wait_for_startup(self, *, stdout) -> None: + deadline = time.time() + self.startup_timeout + last_err: Optional[str] = None + while time.time() < deadline: + if self._proc and self._proc.poll() is not None: + # Process exited; capture any output for debugging. + out, err = ("", "") + if stdout is subprocess.PIPE: + out, err = self._proc.communicate(timeout=0.5) + if self._log_path and os.path.exists(self._log_path): + with open( + self._log_path, + encoding="utf-8-sig", + errors="replace", + newline="", + ) as lf: + last_err = lf.read() + raise RuntimeError(f"LiteLLM proxy exited early: {err or out or last_err or 'no output'}") + if _is_port_open("127.0.0.1", self.port): + if _is_proxy_ready("127.0.0.1", self.port): + return + time.sleep(0.05) + + raise RuntimeError(f"LiteLLM proxy did not open port {self.port} within timeout") + + def close(self) -> None: + proc = self._proc + if proc and proc.poll() is None: + try: + proc.terminate() + proc.wait(timeout=30) + except Exception: + # fall through to kill below + pass + if proc.poll() is None: + proc.kill() + self._proc = None + if self._log_file: + self._log_file.close() + self._log_file = None + self._config_file = None + + def __enter__(self) -> LitellmProxy: + self.start() + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self.close() + + +def _is_port_open(host: str, port: int) -> bool: + try: + with socket.create_connection((host, port), timeout=0.2): + return True + except Exception: + return False + + +def _probe_http_status(host: str, port: int, path: str) -> int | None: + url = f"http://{host}:{port}{path}" + req = urllib.request.Request(url, method="GET") + try: + with urllib.request.urlopen(req, timeout=0.5) as resp: + return getattr(resp, "status", 200) + except urllib.error.HTTPError as err: + return err.code + except Exception: + return None + + +def _is_proxy_ready(host: str, port: int) -> bool: + status = _probe_http_status(host, port, "/health/liveliness") + if status is not None and 200 <= status < 300: + return True + status = _probe_http_status(host, port, "/v1/models") + if status is None: + return False + if 200 <= status < 300: + return True + if status in (401, 403): + # Auth-required, but proxy is up. + return True + return False diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_cost.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_cost.py new file mode 100644 index 00000000..6b1fbaa4 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_cost.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from pathlib import Path + +from ...utils.cost import litellm_tokens_cost + + +def _coerce_float(value: object) -> float | None: + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + return None + return None + + +def load_trace_cost(log_path: Path | str, model_name: str) -> float: + """Sum cost from a trace JSONL log. + + Uses the explicit ``cost`` field when present; otherwise falls back to + computing cost from prompt/completion token counts. + """ + path = Path(log_path) + if not path.exists(): + return 0.0 + + total = 0.0 + try: + with path.open("r", encoding="utf-8-sig", errors="replace", newline="") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + record = json.loads(line) + except json.JSONDecodeError: + continue + + explicit = _coerce_float(record.get("cost")) + if explicit is not None: + total += explicit + continue + + prompt_tokens = record.get("prompt_tokens") or 0 + completion_tokens = record.get("completion_tokens") or 0 + computed = litellm_tokens_cost( + model_name=model_name, + input_tokens=prompt_tokens, + output_tokens=completion_tokens, + ).total_cost + total += float(computed or 0.0) + except FileNotFoundError: + return 0.0 + + return total diff --git a/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_logger.py b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_logger.py new file mode 100644 index 00000000..cc993cde --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/integrations/litellm/trace_logger.py @@ -0,0 +1,670 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Custom LiteLLM logger that writes token/cost usage and request/response trace to a JSONL file. + +Environment variables: + EXGENTIC_LLM_LOG_FILE: optional override of the output JSONL path (default: trace.jsonl in CWD). + EXGENTIC_OTEL_ENABLED: enable OpenTelemetry span creation for LLM calls. +""" + +from __future__ import annotations + +import json +import os +import warnings +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Optional + +from litellm.integrations.custom_logger import CustomLogger + +from ...utils.settings import get_settings + +# Environment variable constants +FILE_ENV = "EXGENTIC_LLM_LOG_FILE" +DEFAULT_FILE = "trace.jsonl" + + +def _otel_enabled() -> bool: + """Check if OTEL is enabled via settings.""" + return bool(get_settings().otel_enabled) + + +def _otel_record_content() -> bool: + """Check if OTEL content recording is enabled via settings.""" + return get_settings().otel_record_content + + +class TraceLogger(CustomLogger): + def __init__(self, file_path: str | None = None) -> None: + super().__init__() + self._file_path = file_path + self._tracer = None + self._otel_logger = None + self._context = None + + @staticmethod + def _ensure_context() -> None: + try: + from ...core.context import init_context_from_env, try_get_context + + if try_get_context() is not None: + return + + init_context_from_env() + except RuntimeError: + pass + + def _init_otel(self, kwargs) -> None: + import threading + + from ...utils.otel import get_session_logger, init_tracing_from_env + + ctx = self.get_context(kwargs) + if ctx is None or ctx.session_id is None or ctx.otel_context is None: + # no otel context means tracing cannot be initialized + warnings.warn( + f"No OTEL context found for TraceLogger, skipping OTEL initialization. context={ctx}", + stacklevel=2, + ) + return + + # Initialize the TracerProvider (use simple processor for subprocess) + self._tracer = init_tracing_from_env() + + base = Path(ctx.output_dir) / ctx.run_id + session_root = base / "sessions" / ctx.session_id + self._otel_logger = get_session_logger( + session_root, + f"{__name__} | pid={os.getpid()} tid={threading.get_native_id()}", + ) + + def _get_parent_context(self, kwargs) -> Any: + """Reconstruct parent span context from environment variables or ContextVar. + + For proxy subprocess: reads from environment variables + For direct callback: reads from Context ContextVar + """ + from opentelemetry import context, trace + from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags + + ctx = self.get_context(kwargs) + + trace_id_hex = ctx.otel_context.trace_id + span_id_hex = ctx.otel_context.span_id + + if not trace_id_hex or not span_id_hex: + return context.get_current() + + trace_id = int(trace_id_hex, 16) + span_id = int(span_id_hex, 16) + + self._otel_logger.log_context_read(trace_id_hex, span_id_hex) + + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=True, + trace_flags=TraceFlags(0x01), + ) + + parent_span = NonRecordingSpan(span_context) + return trace.set_span_in_context(parent_span) + + def _create_llm_span(self, kwargs, name: str, start_time: Optional[Any] = None) -> Optional[Any]: + """Create span for LLM call with CLIENT span kind. + + Args: + kwargs: Keyword arguments containing context and other parameters + name: Name of the span + start_time: Optional datetime when the span started + """ + from opentelemetry.trace import SpanKind + + parent_ctx = self._get_parent_context(kwargs) + + if start_time: + # Convert datetime to nanoseconds since epoch for OTEL + start_time_ns = int(start_time.timestamp() * 1_000_000_000) + span = self._tracer.start_span(name, context=parent_ctx, start_time=start_time_ns, kind=SpanKind.CLIENT) + else: + span = self._tracer.start_span(name, context=parent_ctx, kind=SpanKind.CLIENT) + + span_ctx = span.get_span_context() + + # Extract parent span ID from context for logging + ctx = self.get_context(kwargs) + parent_span_id = ctx.otel_context.span_id if ctx and ctx.otel_context else None + + self._otel_logger.log_span_start( + span_name=name, + span_id=format(span_ctx.span_id, "016x"), + trace_id=format(span_ctx.trace_id, "032x"), + parent_span_id=parent_span_id, + start_time=start_time, + ) + + return span + + def _set_attribute(self, span: Any, key: str, value: Any) -> None: + span.set_attribute(key, value) + span_ctx = span.get_span_context() + self._otel_logger.log_attribute_set(key, value, format(span_ctx.span_id, "016x")) + + @staticmethod + def _metadata_context(kwargs: dict[str, Any]): + from ...core.context import Context, OtelContext, Role + + # Check multiple locations where metadata might be stored + # 1. Direct litellm_metadata parameter + metadata = kwargs.get("litellm_metadata") + if not metadata: + # 2. litellm_params.litellm_metadata + metadata = kwargs.get("litellm_params", {}).get("litellm_metadata") + if not metadata: + # 3. litellm_params.metadata (for 'metadata' parameter) + metadata = kwargs.get("litellm_params", {}).get("metadata") + + if not isinstance(metadata, dict): + return None + + context = metadata.get("context") + if isinstance(context, Context): + return context + + # Reconstruct Context from serialized fields + # Check if we have the required fields + if "exgentic_ctx_run_id" not in metadata: + return None + + # Reconstruct OtelContext if present + otel_context = None + if "exgentic_ctx_otel_trace_id" in metadata and "exgentic_ctx_otel_span_id" in metadata: + otel_context = OtelContext( + trace_id=metadata["exgentic_ctx_otel_trace_id"], + span_id=metadata["exgentic_ctx_otel_span_id"], + ) + + # Reconstruct Role + role_str = metadata.get("exgentic_ctx_role", "framework") + try: + role = Role(role_str) + except ValueError: + role = Role.FRAMEWORK + + # Reconstruct Context + return Context( + run_id=metadata["exgentic_ctx_run_id"], + output_dir=metadata["exgentic_ctx_output_dir"], + cache_dir=metadata["exgentic_ctx_cache_dir"], + session_id=metadata.get("exgentic_ctx_session_id"), + task_id=metadata.get("exgentic_ctx_task_id"), + role=role, + otel_context=otel_context, + ) + + @staticmethod + def _context_log_path(ctx) -> str: + base = Path(ctx.output_dir) / ctx.run_id + if ctx.session_id: + return str(base / "sessions" / ctx.session_id / ctx.role.value / "litellm" / "trace.jsonl") + return str(base / "run" / "litellm" / "trace.jsonl") + + def get_context(self, kwargs: dict[str, Any]): + from ...core.context import Context, try_get_context + + self._ensure_context() + metadata_context = self._metadata_context(kwargs) + if isinstance(metadata_context, Context): + return metadata_context + context = kwargs.get("context") + if isinstance(context, Context): + return context + return try_get_context() + + def _resolve_log_path(self, kwargs: dict[str, Any]) -> str: + from ...core.context import Context + + if self._file_path: + return self._file_path + + self._ensure_context() + metadata_context = self._metadata_context(kwargs) + if metadata_context is not None: + return self._context_log_path(metadata_context) + + context = kwargs.get("context") + if isinstance(context, Context): + return self._context_log_path(context) + + file_path = os.environ.get(FILE_ENV) + if file_path: + return file_path + + ctx = self.get_context(kwargs) # TODO: this is redundant + if ctx is not None: + return self._context_log_path(ctx) + + return DEFAULT_FILE + + def _write_row(self, kwargs: dict[str, Any], response_obj: dict[str, Any], status: str) -> None: + file_path = self._resolve_log_path(kwargs) + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + usage = self._extract_usage(response_obj) + row = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "status": status, + "model": kwargs.get("model"), + "prompt_tokens": usage.get("prompt_tokens"), + "completion_tokens": usage.get("completion_tokens"), + "total_tokens": usage.get("total_tokens"), + "cost": kwargs.get("response_cost"), + "request": self._capture_request(kwargs), + "response": self._capture_response(response_obj), + "trace_id": kwargs.get("litellm_trace_id") or kwargs.get("litellm_call_id"), + } + + with open(file_path, "a", encoding="utf-8") as f: + f.write(json.dumps(row, ensure_ascii=False, default=str) + "\n") + + def _write_otel( + self, + kwargs: dict[str, Any], + response_obj: dict[str, Any], + status: str, + start_time=None, + end_time=None, + ) -> None: + """Write OTEL span for LLM call following GenAI inference span conventions. + + Follows OpenTelemetry GenAI semantic conventions for inference spans: + https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#inference + + Span name format: {gen_ai.operation.name} {gen_ai.request.model} + Span kind: CLIENT + """ + try: + if not _otel_enabled(): + return + + # Lazy initialize OTEL if not already done + if self._tracer is None: + self._init_otel(kwargs) + ctx = self.get_context(kwargs) + + # Lazy import GenAI semantic conventions + from opentelemetry.trace import Status, StatusCode + + optional_params = kwargs.get("optional_params", {}) or {} + litellm_params = kwargs.get("litellm_params", {}) or {} + + # ===== DETERMINE OPERATION TYPE ===== + # Required attribute: gen_ai.operation.name + operation = "chat" if kwargs.get("messages") else "text_completion" + + # ===== CREATE SPAN WITH PROPER NAME ===== + # Span name format: {gen_ai.operation.name} {gen_ai.request.model} + model = kwargs.get("model", "unknown") + span_name = f"{operation} {model}" + span = self._create_llm_span(kwargs, span_name, start_time=start_time) + + # ===== SET SPAN STATUS ===== + if status == "success": + span.set_status(Status(StatusCode.OK)) + else: + span.set_status(Status(StatusCode.ERROR)) + # Conditionally Required: error.type if operation ended in error + error_info = kwargs.get("exception") or response_obj.get("error") + if error_info: + if isinstance(error_info, dict): + error_type = error_info.get("type") or error_info.get("code") or "unknown_error" + elif isinstance(error_info, Exception): + error_type = type(error_info).__name__ + else: + error_type = str(error_info) + self._set_attribute(span, "error.type", error_type) + + # ===== REQUIRED ATTRIBUTES ===== + # gen_ai.operation.name (Required) + self._set_attribute(span, "gen_ai.operation.name", operation) + + # gen_ai.provider.name (Required) - maps LiteLLM provider to standard names + provider = litellm_params.get("custom_llm_provider", "unknown") + if provider is None: + provider = "unknown" + # Map common LiteLLM providers to standard GenAI provider names + provider_mapping = { + "openai": "openai", + "azure": "azure.ai.openai", + "anthropic": "anthropic", + "bedrock": "aws.bedrock", + "vertex_ai": "gcp.vertex_ai", + "gemini": "gcp.gemini", + "cohere": "cohere", + "groq": "groq", + "mistral": "mistral_ai", + "deepseek": "deepseek", + "perplexity": "perplexity", + "watsonx": "ibm.watsonx.ai", + "xai": "x_ai", + } + standard_provider = provider_mapping.get(provider.lower(), provider) + self._set_attribute(span, "gen_ai.provider.name", standard_provider) + + # ===== CONDITIONALLY REQUIRED ATTRIBUTES ===== + # gen_ai.request.model (Conditionally Required if available) + if model: + self._set_attribute(span, "gen_ai.request.model", model) + + # gen_ai.request.choice.count (Conditionally Required if available and !=1) + n = optional_params.get("n") + if n is not None and n != 1: + self._set_attribute(span, "gen_ai.request.choice.count", n) + + # gen_ai.request.seed (Conditionally Required if applicable and request includes seed) + seed = optional_params.get("seed") + if seed is not None: + self._set_attribute(span, "gen_ai.request.seed", seed) + + # server.port (Conditionally Required if server.address is set) + # Note: LiteLLM doesn't typically expose server details, skip for now + + # ===== RECOMMENDED REQUEST ATTRIBUTES ===== + self._set_attribute(span, "gen_ai.conversation.id", ctx.session_id) + + if optional_params.get("max_tokens") is not None: + self._set_attribute(span, "gen_ai.request.max_tokens", optional_params["max_tokens"]) + + if optional_params.get("temperature") is not None: + self._set_attribute(span, "gen_ai.request.temperature", optional_params["temperature"]) + + if optional_params.get("top_p") is not None: + self._set_attribute(span, "gen_ai.request.top_p", optional_params["top_p"]) + + if optional_params.get("top_k") is not None: + self._set_attribute(span, "gen_ai.request.top_k", float(optional_params["top_k"])) + + if optional_params.get("frequency_penalty") is not None: + self._set_attribute( + span, + "gen_ai.request.frequency_penalty", + optional_params["frequency_penalty"], + ) + + if optional_params.get("presence_penalty") is not None: + self._set_attribute( + span, + "gen_ai.request.presence_penalty", + optional_params["presence_penalty"], + ) + + # gen_ai.request.stop_sequences (Recommended) + stop = optional_params.get("stop") + if stop is not None: + if isinstance(stop, list): + self._set_attribute(span, "gen_ai.request.stop_sequences", stop) + else: + self._set_attribute(span, "gen_ai.request.stop_sequences", [stop]) + + # ===== RECOMMENDED RESPONSE ATTRIBUTES ===== + if response_obj: + # Helper function to safely get attribute from dict or object + def safe_get(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + # Get span context for logging + span_ctx = span.get_span_context() + span_id_hex = format(span_ctx.span_id, "016x") + + # gen_ai.response.id (Recommended) + response_id = safe_get(response_obj, "id") + if response_id: + self._set_attribute(span, "gen_ai.response.id", response_id) + if self._otel_logger: + self._otel_logger.log_attribute_set("gen_ai.response.id", response_id, span_id_hex) + + # gen_ai.response.model (Recommended) + response_model = safe_get(response_obj, "model") + if response_model: + self._set_attribute(span, "gen_ai.response.model", response_model) + if self._otel_logger: + self._otel_logger.log_attribute_set("gen_ai.response.model", response_model, span_id_hex) + + # gen_ai.usage.input_tokens and gen_ai.usage.output_tokens (Recommended) + usage = safe_get(response_obj, "usage") + if usage: + prompt_tokens = safe_get(usage, "prompt_tokens") + if prompt_tokens is not None: + self._set_attribute(span, "gen_ai.usage.input_tokens", prompt_tokens) + if self._otel_logger: + self._otel_logger.log_attribute_set("gen_ai.usage.input_tokens", prompt_tokens, span_id_hex) + + completion_tokens = safe_get(usage, "completion_tokens") + if completion_tokens is not None: + self._set_attribute(span, "gen_ai.usage.output_tokens", completion_tokens) + if self._otel_logger: + self._otel_logger.log_attribute_set( + "gen_ai.usage.output_tokens", + completion_tokens, + span_id_hex, + ) + + # gen_ai.response.finish_reasons (Recommended) + choices = safe_get(response_obj, "choices", []) + if choices: + finish_reasons = [] + for choice in choices: + finish_reason = safe_get(choice, "finish_reason") + if finish_reason: + finish_reasons.append(finish_reason) + if finish_reasons: + self._set_attribute(span, "gen_ai.response.finish_reasons", finish_reasons) + if self._otel_logger: + self._otel_logger.log_attribute_set( + "gen_ai.response.finish_reasons", + finish_reasons, + span_id_hex, + ) + + # ===== OPT-IN ATTRIBUTES (for content recording) ===== + # Note: These are opt-in and may contain sensitive data + # Only include if explicitly enabled via settings + if _otel_record_content(): + # gen_ai.tool.definitions (Opt-In) + tools = kwargs.get("tools") + if tools: + try: + self._set_attribute( + span, + "gen_ai.tool.definitions", + json.dumps(tools, default=str), + ) + except Exception: + pass # Skip if serialization fails + + # gen_ai.input.messages (Opt-In) - structured format + messages = kwargs.get("messages") + if messages: + # Convert to GenAI message format + try: + self._set_attribute( + span, + "gen_ai.input.messages", + json.dumps(messages, default=str), + ) + except Exception: + pass # Skip if serialization fails + + # gen_ai.output.messages (Opt-In) - structured format + if response_obj: + # Helper function already defined above in the response attributes section + def safe_get(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + choices = safe_get(response_obj, "choices", []) + if choices: + output_messages = [] + for choice in choices: + message = safe_get(choice, "message") + if message: + output_msg = { + "role": safe_get(message, "role", "assistant"), + "parts": [], + } + content = safe_get(message, "content") + if content: + output_msg["parts"].append({"type": "text", "content": content}) + # Include tool calls if present + tool_calls = safe_get(message, "tool_calls") + if tool_calls: + for tc in tool_calls: + func = safe_get(tc, "function", {}) + output_msg["parts"].append( + { + "type": "tool_call", + "id": safe_get(tc, "id"), + "name": safe_get(func, "name"), + "arguments": safe_get(func, "arguments"), + } + ) + finish_reason = safe_get(choice, "finish_reason") + if finish_reason: + output_msg["finish_reason"] = finish_reason + output_messages.append(output_msg) + + if output_messages: + try: + self._set_attribute( + span, + "gen_ai.output.messages", + json.dumps(output_messages, default=str), + ) + except Exception: + pass # Skip if serialization fails + + # ===== END SPAN ===== + if end_time: + end_time_ns = int(end_time.timestamp() * 1_000_000_000) + span.end(end_time=end_time_ns) + else: + span.end() + + span_ctx = span.get_span_context() + if self._otel_logger: + self._otel_logger.log_span_end( + span_name=span_name, + span_id=format(span_ctx.span_id, "016x"), + status=status, + end_time=end_time, + ) + except Exception: + pass + + def log_success_event(self, kwargs: dict[str, Any], response_obj: dict[str, Any], start_time, end_time): + self._write_row(kwargs, response_obj, status="success") + self._write_otel( + kwargs, + response_obj, + status="success", + start_time=start_time, + end_time=end_time, + ) + + async def async_log_success_event(self, kwargs: dict[str, Any], response_obj: dict[str, Any], start_time, end_time): + self._write_row(kwargs, response_obj, status="success") + self._write_otel( + kwargs, + response_obj, + status="success", + start_time=start_time, + end_time=end_time, + ) + + def log_failure_event(self, kwargs: dict[str, Any], response_obj: dict[str, Any], start_time, end_time): + self._write_row(kwargs, response_obj, status="failure") + self._write_otel( + kwargs, + response_obj, + status="failure", + start_time=start_time, + end_time=end_time, + ) + + async def async_log_failure_event(self, kwargs: dict[str, Any], response_obj: dict[str, Any], start_time, end_time): + self._write_row(kwargs, response_obj, status="failure") + self._write_otel( + kwargs, + response_obj, + status="failure", + start_time=start_time, + end_time=end_time, + ) + + async def async_log_stream_event(self, kwargs: dict[str, Any], response_obj: dict[str, Any], start_time, end_time): + self._write_row(kwargs, response_obj, status="stream") + self._write_otel(kwargs, response_obj, status="stream") + + # Helpers ----------------------------------------------------- + + def _extract_usage(self, response_obj: Any) -> dict[str, Any]: + usage: dict[str, Any] = {} + if isinstance(response_obj, dict): + usage = response_obj.get("usage", {}) or {} + else: + usage_attr = getattr(response_obj, "usage", None) + if isinstance(usage_attr, dict): + usage = usage_attr + elif hasattr(usage_attr, "__dict__"): + usage = usage_attr.__dict__ + return usage + + def _capture_request(self, kwargs: dict[str, Any]) -> dict[str, Any]: + safe: dict[str, Any] = {} + for key in ( + "messages", + "prompt", + "tools", + "tool_choice", + "functions", + "function_call", + "temperature", + "model", + ): + if key in kwargs: + safe[key] = kwargs[key] + return safe + + def _capture_response(self, response_obj: Any) -> dict[str, Any]: + if isinstance(response_obj, dict): + resp = dict(response_obj) + resp.pop("usage", None) + return resp + out: dict[str, Any] = {} + for attr in ("choices", "id", "object", "created", "model", "error"): + if hasattr(response_obj, attr): + out[attr] = getattr(response_obj, attr) + if not out: + out["repr"] = repr(response_obj) + return out + + +# Sync logger for direct SDK calls (e.g. tool-calling agent). +class SyncTraceLogger(TraceLogger): + def __call__(self, *args, **kwargs): + return None + + +class AsyncTraceLogger(TraceLogger): + async def __call__(self, *args, **kwargs): + return None + + +# Expose module-level instances. +trace_logger = TraceLogger() +sync_trace_logger = SyncTraceLogger() +async_trace_logger = AsyncTraceLogger() diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/__init__.py new file mode 100644 index 00000000..23d5c48c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""CLI entrypoints for Exgentic.""" diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/__init__.py new file mode 100644 index 00000000..ceb9e333 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""CLI command modules.""" diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/analyze.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/analyze.py new file mode 100644 index 00000000..55b809a8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/analyze.py @@ -0,0 +1,1223 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import rich_click as click + +plt = None +np = None +pd = None + + +def _ensure_analysis_deps() -> None: + global plt, np, pd + if plt is None or np is None or pd is None: + try: + import matplotlib.pyplot as _plt + import numpy as _np + import pandas as _pd + except ImportError as exc: + raise click.ClickException( + "Analysis commands require the optional analysis dependencies. " + "Install them with `pip install 'exgentic[analysis]'`." + ) from exc + + plt = _plt + np = _np + pd = _pd + + +BENCHMARK_MAP = { + "appworld_test_normal": "AppWorld", + "browsecompplus": "BrowseComp+", + "swebench": "SWE-bench", + "tau2_airline": "TauBench-Airline", + "tau2_retail": "TauBench-Retail", + "tau2_telecom": "TauBench-Telecom", +} + +AGENT_MAP = { + "claude_code": "claude-code", + "openai_solo": "openai-mcp", + "smolagents_code": "smolagents", + "tool_calling": "litellm-react", + "tool_calling_with_shortlisting": "litellm-shortlist", +} + +MODEL_MAP = { + "openai_aws_claude-opus-4-5": "claude-opus-4.5", + "openai_Azure_gpt-5.2-2025-12-11": "gpt-5.2", + "openai_gcp_gemini-3-pro-preview": "gemini-3-pro", +} + +AGENT_SHAPES = { + "claude-code": "*", + "litellm-react": "o", + "litellm-shortlist": "s", + "openai-mcp": "D", + "smolagents": "^", +} + +MODEL_COLORS = { + "claude-opus-4.5": "#E69F00", + "gemini-3-pro": "#56B4E9", + "gpt-5.2": "#009E73", +} + + +def _project_root() -> Path: + return Path(__file__).resolve().parents[5] + + +def _coerce_numeric(df: pd.DataFrame, column: str) -> pd.Series: + return pd.to_numeric(df[column], errors="coerce") + + +def _normalize_results_df(df: pd.DataFrame) -> pd.DataFrame: + out = df.copy() + + if "benchmark" in out.columns: + out["benchmark"] = out["benchmark"].map(BENCHMARK_MAP).fillna(out["benchmark"]) + else: + raise click.ClickException("CSV missing required column: benchmark") + + if "agent" in out.columns: + out["agent"] = out["agent"].map(AGENT_MAP).fillna(out["agent"]) + elif "agent_normalized" in out.columns: + out["agent"] = out["agent_normalized"] + else: + raise click.ClickException("CSV missing required column: agent") + + if "model" in out.columns: + out["model"] = out["model"].map(MODEL_MAP).fillna(out["model"]) + elif "model_normalized" in out.columns: + out["model"] = out["model_normalized"] + else: + raise click.ClickException("CSV missing required column: model") + + if "agent_normalized" not in out.columns: + out["agent_normalized"] = out["agent"] + if "model_normalized" not in out.columns: + out["model_normalized"] = out["model"] + + if "score" in out.columns: + out["score"] = _coerce_numeric(out, "score") + elif "benchmark_score" in out.columns: + out["score"] = _coerce_numeric(out, "benchmark_score") + else: + raise click.ClickException("CSV missing benchmark_score (average_score is not allowed).") + + if "avg_steps" not in out.columns and "average_steps" in out.columns: + out["avg_steps"] = _coerce_numeric(out, "average_steps") + + if "num_tasks" not in out.columns: + if "total_sessions" in out.columns: + out["num_tasks"] = _coerce_numeric(out, "total_sessions") + elif "planned_sessions" in out.columns: + out["num_tasks"] = _coerce_numeric(out, "planned_sessions") + + if "avg_cost" not in out.columns: + if "average_run_cost" in out.columns: + out["avg_cost"] = _coerce_numeric(out, "average_run_cost") + elif "average_agent_cost" in out.columns: + out["avg_cost"] = _coerce_numeric(out, "average_agent_cost") + elif "total_run_cost" in out.columns and "total_sessions" in out.columns: + total_cost = _coerce_numeric(out, "total_run_cost") + total_sessions = _coerce_numeric(out, "total_sessions") + out["avg_cost"] = total_cost / total_sessions.replace(0, np.nan) + + if "total_cost" not in out.columns and "total_run_cost" in out.columns: + out["total_cost"] = _coerce_numeric(out, "total_run_cost") + + if "finished_pct" not in out.columns and "percent_finished" in out.columns: + out["finished_pct"] = _coerce_numeric(out, "percent_finished") + + return out + + +def _get_benchmark_weight(benchmark: str) -> float: + if "TauBench" in benchmark: + return 1.0 / 3.0 + return 1.0 + + +def _load_normalized_frames(csv_path: Path) -> tuple[pd.DataFrame, pd.DataFrame]: + df = pd.read_csv(csv_path) + df = _normalize_results_df(df) + valid_df = df[df["score"].notna()].copy() + if valid_df.empty: + raise click.ClickException("No valid scores found in CSV.") + valid_df["benchmark_weight"] = valid_df["benchmark"].apply(_get_benchmark_weight) + return df, valid_df + + +def _compute_weighted_scores(df: pd.DataFrame) -> dict[tuple[str, str], float]: + config_scores: dict[tuple[str, str], float] = {} + for (agent, model), group in df.groupby(["agent_normalized", "model_normalized"]): + scores = [] + weights = [] + for _, row in group.iterrows(): + if pd.notna(row["score"]): + scores.append(row["score"]) + weights.append(_get_benchmark_weight(row["benchmark"])) + if scores: + config_scores[(agent, model)] = float(np.average(scores, weights=weights)) + return config_scores + + +def _compute_pareto_frontier(points: list[tuple[float, float]]) -> list[tuple[float, float]]: + pareto: list[tuple[float, float]] = [] + for i, (x1, y1) in enumerate(points): + is_pareto = True + for j, (x2, y2) in enumerate(points): + if i == j: + continue + if x2 <= x1 and y2 >= y1 and (x2 < x1 or y2 > y1): + is_pareto = False + break + if is_pareto: + pareto.append((x1, y1)) + pareto.sort(key=lambda p: p[0]) + return pareto + + +def _build_leaderboard_table(df: pd.DataFrame) -> tuple[pd.DataFrame, list[str]]: + valid_df = df[df["score"].notna()].copy() + valid_df["benchmark_weight"] = valid_df["benchmark"].apply(_get_benchmark_weight) + + pivot_scores = valid_df.pivot_table( + index=["agent_normalized", "model_normalized"], + columns="benchmark", + values="score", + aggfunc="mean", + ) + + config_metrics = [] + for (agent, model), group in valid_df.groupby(["agent_normalized", "model_normalized"]): + mean_score = np.average(group["score"], weights=group["benchmark_weight"]) + + if "avg_steps" in group.columns and group["avg_steps"].notna().any(): + mean_steps = np.average( + group[group["avg_steps"].notna()]["avg_steps"], + weights=group[group["avg_steps"].notna()]["benchmark_weight"], + ) + else: + mean_steps = np.nan + + if "avg_cost" in group.columns and group["avg_cost"].notna().any(): + mean_cost = np.average( + group[group["avg_cost"].notna()]["avg_cost"], + weights=group[group["avg_cost"].notna()]["benchmark_weight"], + ) + else: + mean_cost = np.nan + + config_metrics.append( + { + "agent": agent, + "model": model, + "mean_score": mean_score, + "mean_steps": mean_steps, + "mean_cost": mean_cost, + } + ) + + config_df = pd.DataFrame(config_metrics) + + config_points = {} + config_weighted_comparisons = {} + for _bench, group in valid_df.groupby("benchmark"): + weight = group["benchmark_weight"].iloc[0] + config_scores = {} + for _, row in group.iterrows(): + config = (row["agent_normalized"], row["model_normalized"]) + score = row["score"] + if pd.notna(score): + config_scores[config] = score + configs = list(config_scores.keys()) + for i, c1 in enumerate(configs): + for c2 in configs[i + 1 :]: + s1 = config_scores[c1] + s2 = config_scores[c2] + if c1 not in config_points: + config_points[c1] = 0.0 + config_weighted_comparisons[c1] = 0.0 + if c2 not in config_points: + config_points[c2] = 0.0 + config_weighted_comparisons[c2] = 0.0 + if s1 > s2: + config_points[c1] += 1.0 * weight + elif s1 == s2: + config_points[c1] += 0.5 * weight + config_points[c2] += 0.5 * weight + else: + config_points[c2] += 1.0 * weight + config_weighted_comparisons[c1] += weight + config_weighted_comparisons[c2] += weight + + win_rates = {} + for config in config_points: + if config_weighted_comparisons[config] > 0: + win_rates[config] = config_points[config] / config_weighted_comparisons[config] + else: + win_rates[config] = np.nan + + config_df["win_rate"] = config_df.apply(lambda row: win_rates.get((row["agent"], row["model"]), np.nan), axis=1) + + pivot_scores_reset = pivot_scores.reset_index() + full_table = config_df.merge( + pivot_scores_reset, + left_on=["agent", "model"], + right_on=["agent_normalized", "model_normalized"], + how="left", + ).sort_values("mean_score", ascending=False) + + benchmarks = list(pivot_scores.columns) + return full_table, benchmarks + + +def _generate_leaderboard(df: pd.DataFrame) -> str: + full_table, benchmarks = _build_leaderboard_table(df) + benchmark_short_names = { + "SWE-bench": "SWE", + "BrowseComp+": "Browse", + "TauBench-Airline": "Airline", + "TauBench-Retail": "Retail", + "TauBench-Telecom": "Telecom", + "AppWorld": "App", + } + bench_display = [benchmark_short_names.get(b, b) for b in benchmarks] + + agent_short = { + "litellm-react": "React", + "litellm-shortlist": "React+Short", + "smolagents": "Smol", + "openai-mcp": "OpenAI-MCP", + "claude-code": "Claude-Code", + } + model_short = { + "gpt-5.2": "GPT-5.2", + "claude-opus-4.5": "Opus-4.5", + "gemini-3-pro": "Gemini-3", + } + + latex = [] + latex.append(r"\begin{table*}[t]") + latex.append(r"\centering") + latex.append(r"\small") + latex.append(r"\caption{Agent-Model Configuration Leaderboard}") + latex.append(r"\label{tab:leaderboard}") + + num_benchmarks = len(benchmarks) + col_spec = "ll" + "c" * num_benchmarks + "cccc" + latex.append(f"\\begin{{tabular}}{{{col_spec}}}") + latex.append(r"\toprule") + + header1 = r"\textbf{Agent} & \textbf{Model}" + for bench_name in bench_display: + header1 += f" & \\textbf{{{bench_name}}}" + header1 += r" & \textbf{Mean} & \textbf{Win} & \textbf{Steps} & \textbf{Cost} \\" + latex.append(header1) + + header2 = r" & " + for _ in benchmarks: + header2 += " & " + header2 += r" & Score & Rate & (avg) & (\$) \\" + latex.append(header2) + latex.append(r"\midrule") + + for _, row in full_table.iterrows(): + agent_name = agent_short.get(row["agent"], row["agent"]) + model_name = model_short.get(row["model"], row["model"]) + row_str = f"{agent_name} & {model_name}" + for bench in benchmarks: + score = row.get(bench, np.nan) + if pd.notna(score): + row_str += f" & {score:.2f}" + else: + row_str += " & --" + if pd.notna(row["mean_score"]): + row_str += f" & {row['mean_score']:.2f}" + else: + row_str += " & --" + if pd.notna(row["win_rate"]): + row_str += f" & {row['win_rate']:.2f}" + else: + row_str += " & --" + if pd.notna(row["mean_steps"]): + row_str += f" & {row['mean_steps']:.1f}" + else: + row_str += " & --" + if pd.notna(row["mean_cost"]): + row_str += f" & {row['mean_cost']:.2f}" + else: + row_str += " & --" + row_str += r" \\" + latex.append(row_str) + + latex.append(r"\bottomrule") + latex.append(r"\end{tabular}") + latex.append(r"\end{table*}") + + return "\n".join(latex) + + +@click.group("analyse") +def analyse_cmd() -> None: + """Analyze result CSVs without intermediate files.""" + return + + +@analyse_cmd.command("leaderboard") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_leaderboard_cmd(csv_path: Path) -> None: + """Generate leaderboard table from a results CSV.""" + _ensure_analysis_deps() + df = pd.read_csv(csv_path) + df = _normalize_results_df(df) + click.echo(_generate_leaderboard(df)) + + +@analyse_cmd.command("leaderboard-paper") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.option( + "--output", + "output_path", + type=click.Path(dir_okay=False, path_type=Path), + default=None, + help="Output path for the paper-style leaderboard table.", +) +def analyse_leaderboard_paper_cmd(csv_path: Path, output_path: Path | None) -> None: + """Generate the paper-style leaderboard table and save to file.""" + _ensure_analysis_deps() + df = pd.read_csv(csv_path) + df = _normalize_results_df(df) + full_table, benchmarks = _build_leaderboard_table(df) + + bench_order = [ + "AppWorld", + "BrowseComp+", + "SWE-bench", + "TauBench-Airline", + "TauBench-Retail", + "TauBench-Telecom", + ] + bench_map = {b: b for b in benchmarks} + ordered_benchmarks = [b for b in bench_order if b in bench_map] + + agent_macro = { + "openai-mcp": r"\solo{}", + "smolagents": r"\smol{}", + "litellm-react": r"\react{}", + "litellm-shortlist": r"\short{}", + "claude-code": r"\cc{}", + } + model_macro = { + "claude-opus-4.5": r"\opus{}", + "gemini-3-pro": r"\gemini{}", + "gpt-5.2": r"\gpt{}", + } + + header = r"""\definecolor{tableheader}{RGB}{248, 249, 250} +\definecolor{rowgray}{RGB}{252, 252, 253} +\definecolor{benchmarkbg}{RGB}{245, 245, 247} +% TABLE CODE (put this where you want the table): +\begin{table}[t!] +\centering +\begin{tcolorbox}[ + colback=white, + colframe=gray!20, + boxrule=0.5pt, + arc=3pt, + outer arc=3pt, + width=\columnwidth, + top=0pt, + bottom=0pt, + left=-1pt, + right=1.5pt, + boxsep=0pt +] +\resizebox{0.8\textwidth}{!}{% +\renewcommand{\arraystretch}{1.7} +\setlength{\tabcolsep}{3pt} +\footnotesize +\begin{tabular}{@{}l l c c c !{\color{gray!20}\vrule} >{\columncolor{benchmarkbg}}c """ + r""">{\columncolor{benchmarkbg}}c >{\columncolor{benchmarkbg}}c >{\columncolor{benchmarkbg}}c """ + r""">{\columncolor{benchmarkbg}}c >{\columncolor{benchmarkbg}}c@{}} +\rowcolor{tableheader} +\textbf{\#} & \textbf{\shortstack{General Agent}} & \scriptsize\textbf{Model} & """ + r"""\shortstack{\scriptsize{Avg}\\\textbf{Success}} & \shortstack{\scriptsize{Avg}\\\textbf{Cost}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{App\\World}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{Browse\\Comp+}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{SWE\\benchV}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{Tau 2\\Airline}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{Tau 2\\Retail}} & """ + r"""\cellcolor{tableheader}\tiny\textbf{\shortstack{Tau 2\\Telecom}} \\ +""" + + def fmt_score(value: float | int | None) -> str: + if value is None or pd.isna(value): + return "--" + text = f"{value:.2f}" + if text.startswith("0"): + text = text[1:] + return text + + def fmt_cost(value: float | int | None) -> str: + if value is None or pd.isna(value): + return "--" + return f"${value:.1f}" + + rows = [] + for idx, (_, row) in enumerate(full_table.iterrows(), start=1): + agent = row["agent"] + model = row["model"] + mean_score = row["mean_score"] + mean_cost = row["mean_cost"] + agent_cell = agent_macro.get(agent, agent) + model_cell = model_macro.get(model, model) + prefix = r"\rowcolor{rowgray}" if idx % 2 == 0 else "" + cells = [ + str(idx), + agent_cell, + rf"{{\scriptsize {model_cell}}}", + fmt_score(mean_score), + fmt_cost(mean_cost), + ] + for bench in ordered_benchmarks: + score = row.get(bench, np.nan) + score_cell = f"{{\\scriptsize {fmt_score(score)}}}" if pd.notna(score) else "{\\scriptsize --}" + if prefix: + score_cell = rf"\cellcolor{{rowgray}}{score_cell}" + cells.append(score_cell) + row_line = prefix + " " + " & ".join(cells) + r" \\" + rows.append(row_line) + + footer = r""" +\end{tabular}% +} +\end{tcolorbox} +\caption{The \leaderboard{} comparing emerging general agents across standardized benchmarks. +Average Success represents the mean success rate across benchmarks; Average Cost represents the mean cost per task. +Performance is strongly influenced by backbone model choice.} +\label{tab:leaderboard} +\end{table} +""" + + latex = header + "\n".join(rows) + footer + if output_path is None: + output_path = _project_root() / "misc" / "paper" / "tables" / "leaderboard_paper.tex" + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(latex, encoding="utf-8") + click.echo(f"Saved to {output_path}") + + +@analyse_cmd.command("model-agent") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_model_agent_cmd(csv_path: Path) -> None: + """Model vs agent variance, pair means, and interaction analysis.""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + + def weighted_mean(group): + return np.average(group["score"], weights=group["benchmark_weight"]) + + def weighted_std(group): + wmean = np.average(group["score"], weights=group["benchmark_weight"]) + variance = np.average((group["score"] - wmean) ** 2, weights=group["benchmark_weight"]) + return np.sqrt(variance) + + agent_means = valid_df.groupby("agent_normalized").apply(weighted_mean) + model_means = valid_df.groupby("model_normalized").apply(weighted_mean) + valid_df["agent_mean"] = valid_df["agent_normalized"].map(agent_means) + valid_df["model_mean"] = valid_df["model_normalized"].map(model_means) + + grand_mean = np.average(valid_df["score"], weights=valid_df["benchmark_weight"]) + total_var = np.average((valid_df["score"] - grand_mean) ** 2, weights=valid_df["benchmark_weight"]) + agent_var = np.average((valid_df["agent_mean"] - grand_mean) ** 2, weights=valid_df["benchmark_weight"]) + model_var = np.average((valid_df["model_mean"] - grand_mean) ** 2, weights=valid_df["benchmark_weight"]) + + click.echo("MODEL VS AGENT (benchmark-weighted)") + click.echo(f"Total variance: {total_var:.4f}") + click.echo(f"Agent variance: {agent_var:.4f} ({100*agent_var/total_var:.1f}%)") + click.echo(f"Model variance: {model_var:.4f} ({100*model_var/total_var:.1f}%)") + + click.echo("\nBy Model (weighted):") + by_model = ( + valid_df.groupby("model_normalized") + .apply(lambda g: pd.Series({"mean": weighted_mean(g), "std": weighted_std(g), "count": len(g)})) + .round(3) + ) + click.echo(by_model.to_string()) + + click.echo("\nBy Agent (weighted):") + by_agent = ( + valid_df.groupby("agent_normalized") + .apply(lambda g: pd.Series({"mean": weighted_mean(g), "std": weighted_std(g), "count": len(g)})) + .round(3) + ) + click.echo(by_agent.to_string()) + + click.echo("\nAgent-Model pair weighted means:") + pair_means = valid_df.groupby(["agent_normalized", "model_normalized"]).apply(weighted_mean).round(3) + for (agent, model), score in pair_means.items(): + click.echo(f"{agent:<20} {model:<20} {score:>10.3f}") + + click.echo("\nInteraction analysis (cell means):") + cell_means = valid_df.groupby(["model_normalized", "agent_normalized"])["score"].mean() + df_cells = cell_means.reset_index() + grand_mean = df_cells["score"].mean() + model_effects = df_cells.groupby("model_normalized")["score"].mean() - grand_mean + agent_effects = df_cells.groupby("agent_normalized")["score"].mean() - grand_mean + + interactions = [] + for _, row in df_cells.iterrows(): + m = row["model_normalized"] + a = row["agent_normalized"] + score = row["score"] + pred = grand_mean + model_effects[m] + agent_effects[a] + interactions.append(score - pred) + + var_model = model_effects.var() + var_agent = agent_effects.var() + var_interact = np.var(interactions) + total_comp = var_model + var_agent + var_interact + click.echo(f"Model Main Effect: {100*var_model/total_comp:.1f}%") + click.echo(f"Agent Main Effect: {100*var_agent/total_comp:.1f}%") + click.echo(f"Interaction Effect:{100*var_interact/total_comp:.1f}%") + + +@analyse_cmd.command("model-win-rate") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_model_win_rate_cmd(csv_path: Path) -> None: + """Model win rate (TauBench weighted).""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + model_points = {m: 0.0 for m in valid_df["model_normalized"].unique()} + model_weighted_comparisons = {m: 0.0 for m in model_points} + + for (_agent, _bench), group in valid_df.groupby(["agent_normalized", "benchmark"]): + weight = group["benchmark_weight"].iloc[0] + model_scores = {} + for _, row in group.iterrows(): + model = row["model_normalized"] + score = row["score"] + if pd.notna(score): + model_scores[model] = score + models = list(model_scores.keys()) + for i, m1 in enumerate(models): + for m2 in models[i + 1 :]: + s1 = model_scores[m1] + s2 = model_scores[m2] + if s1 > s2: + model_points[m1] += 1.0 * weight + elif s1 == s2: + model_points[m1] += 0.5 * weight + model_points[m2] += 0.5 * weight + else: + model_points[m2] += 1.0 * weight + model_weighted_comparisons[m1] += weight + model_weighted_comparisons[m2] += weight + + click.echo(f"{'Model':<20} {'Win Rate':>10} {'Weighted Comparisons':>20}") + click.echo("-" * 55) + for model in sorted(model_points.keys()): + if model_weighted_comparisons[model] > 0: + win_rate = model_points[model] / model_weighted_comparisons[model] + click.echo(f"{model:<20} {100*win_rate:>9.1f}% {model_weighted_comparisons[model]:>20.1f}") + + +@analyse_cmd.command("config-win-rate") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_config_win_rate_cmd(csv_path: Path) -> None: + """Top configuration win rates (TauBench weighted).""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + config_points = {} + config_weighted_comparisons = {} + + for _bench, group in valid_df.groupby("benchmark"): + weight = group["benchmark_weight"].iloc[0] + config_scores = {} + for _, row in group.iterrows(): + config = (row["agent_normalized"], row["model_normalized"]) + score = row["score"] + if pd.notna(score): + config_scores[config] = score + configs = list(config_scores.keys()) + for i, c1 in enumerate(configs): + for c2 in configs[i + 1 :]: + s1 = config_scores[c1] + s2 = config_scores[c2] + if c1 not in config_points: + config_points[c1] = 0.0 + config_weighted_comparisons[c1] = 0.0 + if c2 not in config_points: + config_points[c2] = 0.0 + config_weighted_comparisons[c2] = 0.0 + if s1 > s2: + config_points[c1] += 1.0 * weight + elif s1 == s2: + config_points[c1] += 0.5 * weight + config_points[c2] += 0.5 * weight + else: + config_points[c2] += 1.0 * weight + config_weighted_comparisons[c1] += weight + config_weighted_comparisons[c2] += weight + + config_win_rates = [] + for config in config_points: + if config_weighted_comparisons[config] > 0: + win_rate = config_points[config] / config_weighted_comparisons[config] + config_win_rates.append((config, win_rate, config_weighted_comparisons[config])) + config_win_rates.sort(key=lambda x: x[1], reverse=True) + + click.echo(f"{'Agent':<20} {'Model':<20} {'Win Rate':>10} {'Weighted Comps':>15}") + click.echo("-" * 70) + for (agent, model), win_rate, comps in config_win_rates[:10]: + click.echo(f"{agent:<20} {model:<20} {100*win_rate:>9.1f}% {comps:>15.1f}") + + +@analyse_cmd.command("best-per-benchmark") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_best_per_benchmark_cmd(csv_path: Path) -> None: + """Best configuration per benchmark (top 3).""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + for bench in sorted(valid_df["benchmark"].unique()): + bench_df = valid_df[valid_df["benchmark"] == bench] + best = bench_df.loc[bench_df["score"].idxmax()] + click.echo(f"\n{bench}:") + click.echo(f" Winner: {best['agent_normalized']} + {best['model_normalized']}") + click.echo(f" Score: {best['score']:.3f}") + top3 = bench_df.nlargest(3, "score")[["agent_normalized", "model_normalized", "score"]] + click.echo(" Top 3:") + for _, row in top3.iterrows(): + click.echo(f" {row['agent_normalized']:20s} + {row['model_normalized']:20s} = {row['score']:.3f}") + + +@analyse_cmd.command("tool-shortlist") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_tool_shortlist_cmd(csv_path: Path) -> None: + """Tool shortlisting effect (AppWorld).""" + _ensure_analysis_deps() + df, _ = _load_normalized_frames(csv_path) + appworld = df[df["benchmark"] == "AppWorld"].copy() + click.echo(f"Total AppWorld rows: {len(appworld)}") + for model in ["gpt-5.2", "claude-opus-4.5", "gemini-3-pro"]: + click.echo(f"\n{model}:") + no_shortlist = appworld[ + (appworld["model_normalized"] == model) & (appworld["agent_normalized"] == "litellm-react") + ] + with_shortlist = appworld[ + (appworld["model_normalized"] == model) & (appworld["agent_normalized"] == "litellm-shortlist") + ] + if len(no_shortlist) > 0: + score_no = no_shortlist["score"].values[0] + click.echo(f" Without shortlist: {score_no:.3f}") + else: + click.echo(" Without shortlist: NO DATA") + score_no = None + if len(with_shortlist) > 0: + score_with = with_shortlist["score"].values[0] + click.echo(f" With shortlist: {score_with:.3f}") + if score_no is not None: + click.echo(f" Delta: {score_with - score_no:+.3f}") + else: + click.echo(" With shortlist: NO DATA") + + +@analyse_cmd.command("cost-efficiency") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_cost_efficiency_cmd(csv_path: Path) -> None: + """Cost-efficiency analysis (benchmark-weighted).""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + if "avg_cost" not in valid_df.columns or valid_df["avg_cost"].notna().sum() == 0: + raise click.ClickException("avg_cost is required for cost-efficiency analysis.") + cost_df = valid_df[valid_df["avg_cost"].notna()].copy() + cost_df["efficiency"] = cost_df["score"] / cost_df["avg_cost"] + + by_config = ( + cost_df.groupby(["agent_normalized", "model_normalized"]) + .apply( + lambda g: pd.Series( + { + "score": np.average(g["score"], weights=g["benchmark_weight"]), + "avg_cost": np.average(g["avg_cost"], weights=g["benchmark_weight"]), + } + ) + ) + .reset_index() + ) + by_config["efficiency"] = by_config["score"] / by_config["avg_cost"] + top_efficient = by_config.nlargest(10, "efficiency") + click.echo(f"{'Agent':<20} {'Model':<20} {'Score':>8} {'Cost':>10} {'Efficiency':>12}") + click.echo("-" * 75) + for _, row in top_efficient.iterrows(): + click.echo( + f"{row['agent_normalized']:<20} {row['model_normalized']:<20} " + f"{row['score']:>8.3f} ${row['avg_cost']:>9.2f} {row['efficiency']:>12.2f}" + ) + + +@analyse_cmd.command("component-impact") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_component_impact_cmd(csv_path: Path) -> None: + """Component impact analysis.""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + agent_components = { + "litellm-react": { + "runtime": False, + "shortlist": False, + "schema_guard": False, + "memory": False, + "planning": False, + }, + "litellm-shortlist": { + "runtime": False, + "shortlist": True, + "schema_guard": False, + "memory": False, + "planning": False, + }, + "smolagents": { + "runtime": True, + "shortlist": False, + "schema_guard": True, + "memory": False, + "planning": False, + }, + "openai-mcp": { + "runtime": False, + "shortlist": False, + "schema_guard": True, + "memory": False, + "planning": False, + }, + "claude-code": { + "runtime": True, + "shortlist": False, + "schema_guard": True, + "memory": True, + "planning": True, + }, + } + + for comp in ["runtime", "shortlist", "schema_guard", "memory", "planning"]: + valid_df[comp] = valid_df["agent_normalized"].apply(lambda a, c=comp: agent_components.get(a, {}).get(c, False)) + + click.echo(f"{'Component':<15} {'With':>8} {'Without':>8} {'Delta':>8} {'N_with':>8} {'N_without':>10}") + click.echo("-" * 70) + for comp in ["runtime", "shortlist", "schema_guard", "memory", "planning"]: + with_comp_df = valid_df[valid_df[comp]] + without_comp_df = valid_df[~valid_df[comp]] + if len(with_comp_df) > 0 and len(without_comp_df) > 0: + mean_with = np.average(with_comp_df["score"], weights=with_comp_df["benchmark_weight"]) + mean_without = np.average(without_comp_df["score"], weights=without_comp_df["benchmark_weight"]) + delta = mean_with - mean_without + click.echo( + f"{comp.replace('_', ' ').title():<15} {mean_with:>8.3f} {mean_without:>8.3f} " + f"{delta:>+8.3f} {len(with_comp_df):>8} {len(without_comp_df):>10}" + ) + + +@analyse_cmd.command("correlation") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +def analyse_correlation_cmd(csv_path: Path) -> None: + """Cross-benchmark rank correlation.""" + _ensure_analysis_deps() + _, valid_df = _load_normalized_frames(csv_path) + pivot = valid_df.pivot_table( + index=["agent_normalized", "model_normalized"], + columns="benchmark", + values="score", + ) + if pivot.shape[1] < 2: + raise click.ClickException("Need at least 2 benchmarks for correlation.") + corr = pivot.corr(method="spearman") + click.echo("Spearman Rank Correlation Matrix:") + click.echo(corr.round(2).to_string()) + click.echo("\nNotable correlations (|r| > 0.7):") + for i in range(len(corr.columns)): + for j in range(i + 1, len(corr.columns)): + val = corr.iloc[i, j] + if abs(val) > 0.7: + click.echo(f" {corr.columns[i]} vs {corr.columns[j]}: {val:.2f}") + + +@analyse_cmd.command("cost-score") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.option( + "--output", + "output_path", + type=click.Path(dir_okay=False, path_type=Path), + default=None, + help="Output PDF path for the cost-performance plot.", +) +def analyse_cost_score_cmd(csv_path: Path, output_path: Path | None) -> None: + """Generate cost vs score plot with Pareto frontier.""" + _ensure_analysis_deps() + df, _ = _load_normalized_frames(csv_path) + df["benchmark_weight"] = df["benchmark"].apply(_get_benchmark_weight) + + weighted_scores = _compute_weighted_scores(df) + if "avg_cost" not in df.columns or df["avg_cost"].notna().sum() == 0: + raise click.ClickException("avg_cost is required for cost-score plot.") + + cost_df = df[df["avg_cost"].notna()].copy() + cost_by_config = cost_df.groupby(["agent_normalized", "model_normalized"]).agg({"avg_cost": "mean"}).reset_index() + + agent_display_names = { + "claude-code": "Claude Code", + "litellm-react": "ReAct", + "litellm-shortlist": "ReAct Short", + "openai-mcp": "Solo", + "smolagents": "Smolagent", + } + model_display_names = { + "claude-opus-4.5": "Opus", + "gemini-3-pro": "Gemini", + "gpt-5.2": "GPT", + } + + plot_data = [] + for _, row in cost_by_config.iterrows(): + agent = row["agent_normalized"] + model = row["model_normalized"] + config = (agent, model) + if config in weighted_scores: + plot_data.append( + { + "agent": agent, + "model": model, + "cost": row["avg_cost"], + "score": weighted_scores[config], + "label": f"{agent_display_names.get(agent, agent)}\n{model_display_names.get(model, model)}", + } + ) + + if not plot_data: + raise click.ClickException("No plot data available.") + + plot_df = pd.DataFrame(plot_data) + + plt.rcParams.update( + { + "font.family": "serif", + "font.size": 11, + "axes.labelsize": 12, + "axes.titlesize": 14, + "legend.fontsize": 10, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + } + ) + + fig, ax = plt.subplots(figsize=(6, 4)) + for _idx, (_, row) in enumerate(plot_df.iterrows()): + shape = AGENT_SHAPES[row["agent"]] + color = MODEL_COLORS[row["model"]] + marker_size = 260 if shape == "*" else 200 + ax.scatter( + row["cost"], + row["score"], + marker=shape, + s=marker_size, + c=color, + edgecolors="black", + linewidths=0.8, + alpha=0.9, + zorder=3, + ) + + # Labels intentionally disabled. + + points = list(zip(plot_df["cost"].tolist(), plot_df["score"].tolist())) + pareto = _compute_pareto_frontier(points) + if pareto: + xs, ys = zip(*pareto) + ax.plot(xs, ys, linestyle="--", color="gray", linewidth=1.5, zorder=2) + + ax.set_xlabel("Average Cost per Task ($)", fontsize=12) + ax.set_ylabel("Success Rate", fontsize=12) + y_min = max(0, plot_df["score"].min() - 0.05) + y_max = min(1.0, plot_df["score"].max() + 0.08) + ax.set_ylim(y_min, y_max) + ax.grid(True, alpha=0.2, linestyle=":", linewidth=0.8, color="gray", zorder=1) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) + + # Legends + agent_labels = { + "claude-code": "Claude Code", + "litellm-react": "ReAct", + "litellm-shortlist": "ReAct Short", + "openai-mcp": "OpenAI Solo", + "smolagents": "Smolagent", + } + model_labels = { + "claude-opus-4.5": "Opus 4.5", + "gemini-3-pro": "Gemini 3", + "gpt-5.2": "GPT 5.2", + } + + agent_handles = [] + agent_label_list = [] + for agent in AGENT_SHAPES: + agent_handles.append( + plt.Line2D( + [0], + [0], + marker=AGENT_SHAPES[agent], + color="w", + markerfacecolor="gray", + markeredgecolor="black", + markersize=8, + linestyle="", + ) + ) + agent_label_list.append(agent_labels[agent]) + + model_handles = [] + model_label_list = [] + for model in MODEL_COLORS: + model_handles.append( + plt.Line2D( + [0], + [0], + marker="o", + color="w", + markerfacecolor=MODEL_COLORS[model], + markeredgecolor="black", + markersize=8, + linestyle="", + ) + ) + model_label_list.append(model_labels[model]) + + legend1 = ax.legend( + model_handles, + model_label_list, + loc="lower right", + bbox_to_anchor=(0.70, 0.05), + frameon=False, + fontsize=8, + title="Model", + title_fontsize=9, + ) + ax.add_artist(legend1) + + ax.legend( + agent_handles, + agent_label_list, + loc="lower right", + bbox_to_anchor=(0.95, 0.05), + frameon=False, + fontsize=8, + title="Agent", + title_fontsize=9, + ) + + if output_path is None: + output_path = _project_root() / "misc" / "paper" / "figures" / "cost_performance.pdf" + output_path.parent.mkdir(parents=True, exist_ok=True) + plt.tight_layout() + plt.savefig(output_path, dpi=300, bbox_inches="tight") + plt.savefig(output_path.with_suffix(".png"), dpi=150, bbox_inches="tight") + click.echo(f"Saved to {output_path}") + click.echo(f"Saved to {output_path.with_suffix('.png')}") + + +def _save_pdf_png(fig: Any, output_pdf: Path) -> None: + output_pdf.parent.mkdir(parents=True, exist_ok=True) + fig.tight_layout() + fig.savefig(output_pdf, dpi=300, bbox_inches="tight") + fig.savefig(output_pdf.with_suffix(".png"), dpi=150, bbox_inches="tight") + plt.close(fig) + + +@analyse_cmd.command("paper-figures") +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.option( + "--outdir", + type=click.Path(file_okay=False, path_type=Path), + default=None, + help="Output directory for generated figures.", +) +def analyse_paper_figures_cmd(csv_path: Path, outdir: Path | None) -> None: + """Generate the paper graph set from one CSV.""" + _ensure_analysis_deps() + df, valid_df = _load_normalized_frames(csv_path) + if outdir is None: + outdir = _project_root() / "misc" / "paper" / "figures" + outdir.mkdir(parents=True, exist_ok=True) + + # 1) Cost-performance (reuse existing command output default file name). + analyse_cost_score_cmd.callback(csv_path=csv_path, output_path=outdir / "cost_performance.pdf") # type: ignore[attr-defined] + + # 2) Results heatmap (config x benchmark). + heat = valid_df.pivot_table( + index=["agent_normalized", "model_normalized"], + columns="benchmark", + values="score", + aggfunc="mean", + ) + fig, ax = plt.subplots(figsize=(9, 6)) + im = ax.imshow(heat.values, aspect="auto", cmap="YlGnBu", vmin=0.0, vmax=1.0) + ax.set_xticks(range(len(heat.columns))) + ax.set_xticklabels(heat.columns, rotation=30, ha="right") + ylabels = [f"{a} | {m}" for a, m in heat.index] + ax.set_yticks(range(len(ylabels))) + ax.set_yticklabels(ylabels) + ax.set_title("Results Heatmap") + fig.colorbar(im, ax=ax, label="Score") + _save_pdf_png(fig, outdir / "results_heatmap.pdf") + click.echo(f"Saved to {outdir / 'results_heatmap.pdf'}") + click.echo(f"Saved to {(outdir / 'results_heatmap.pdf').with_suffix('.png')}") + + # 3) Benchmark correlation heatmap (Spearman). + corr = heat.corr(method="spearman") + fig, ax = plt.subplots(figsize=(7, 6)) + im = ax.imshow(corr.values, cmap="coolwarm", vmin=-1.0, vmax=1.0) + ax.set_xticks(range(len(corr.columns))) + ax.set_xticklabels(corr.columns, rotation=30, ha="right") + ax.set_yticks(range(len(corr.index))) + ax.set_yticklabels(corr.index) + ax.set_title("Benchmark Correlation (Spearman)") + fig.colorbar(im, ax=ax, label="Correlation") + _save_pdf_png(fig, outdir / "benchmark_correlation.pdf") + click.echo(f"Saved to {outdir / 'benchmark_correlation.pdf'}") + click.echo(f"Saved to {(outdir / 'benchmark_correlation.pdf').with_suffix('.png')}") + + # 4) Protocol comparison. + protocol_map = { + "litellm-react": "Tool-calling", + "litellm-shortlist": "Tool-calling", + "smolagents": "Python-functions", + "openai-mcp": "MCP", + "claude-code": "MCP", + } + by_protocol = valid_df.assign(protocol=valid_df["agent_normalized"].map(protocol_map)).dropna(subset=["protocol"]) + agg = ( + by_protocol.groupby(["benchmark", "protocol"])["score"] + .mean() + .reset_index() + .pivot(index="benchmark", columns="protocol", values="score") + ) + fig, ax = plt.subplots(figsize=(9, 5)) + agg.plot(kind="bar", ax=ax, rot=25) + ax.set_ylim(0, 1) + ax.set_ylabel("Mean Score") + ax.set_title("Protocol Comparison by Benchmark") + ax.legend(frameon=False) + _save_pdf_png(fig, outdir / "protocol_comparison.pdf") + click.echo(f"Saved to {outdir / 'protocol_comparison.pdf'}") + click.echo(f"Saved to {(outdir / 'protocol_comparison.pdf').with_suffix('.png')}") + + # 5) Component impact. + agent_components = { + "litellm-react": { + "runtime": False, + "shortlist": False, + "schema_guard": False, + "memory": False, + "planning": False, + }, + "litellm-shortlist": { + "runtime": False, + "shortlist": True, + "schema_guard": False, + "memory": False, + "planning": False, + }, + "smolagents": { + "runtime": True, + "shortlist": False, + "schema_guard": True, + "memory": False, + "planning": False, + }, + "openai-mcp": { + "runtime": False, + "shortlist": False, + "schema_guard": True, + "memory": False, + "planning": False, + }, + "claude-code": { + "runtime": True, + "shortlist": False, + "schema_guard": True, + "memory": True, + "planning": True, + }, + } + tmp = valid_df.copy() + components = ["runtime", "shortlist", "schema_guard", "memory", "planning"] + deltas = [] + for comp in components: + tmp[comp] = tmp["agent_normalized"].apply(lambda a, c=comp: agent_components.get(a, {}).get(c, False)) + with_comp = tmp[tmp[comp]]["score"] + without_comp = tmp[~tmp[comp]]["score"] + if len(with_comp) and len(without_comp): + deltas.append((comp, with_comp.mean() - without_comp.mean())) + fig, ax = plt.subplots(figsize=(8, 4.5)) + labels = [c for c, _ in deltas] + values = [v for _, v in deltas] + colors = ["#2a9d8f" if v >= 0 else "#e76f51" for v in values] + ax.barh(labels, values, color=colors) + ax.axvline(0, color="black", linewidth=1.0) + ax.set_xlabel("Score Delta (With - Without)") + ax.set_title("Component Impact") + _save_pdf_png(fig, outdir / "component_impact.pdf") + click.echo(f"Saved to {outdir / 'component_impact.pdf'}") + click.echo(f"Saved to {(outdir / 'component_impact.pdf').with_suffix('.png')}") + + # 6) Best configuration per benchmark. + winners = ( + valid_df.sort_values("score", ascending=False) + .groupby("benchmark", as_index=False) + .first() + .sort_values("score", ascending=True) + ) + fig, ax = plt.subplots(figsize=(9, 5)) + labels = [ + f"{b}\n{a} + {m}" + for b, a, m in zip( + winners["benchmark"], + winners["agent_normalized"], + winners["model_normalized"], + ) + ] + ax.barh(labels, winners["score"], color="#457b9d") + ax.set_xlim(0, 1) + ax.set_xlabel("Score") + ax.set_title("Best Per Benchmark") + _save_pdf_png(fig, outdir / "best_per_benchmark.pdf") + click.echo(f"Saved to {outdir / 'best_per_benchmark.pdf'}") + click.echo(f"Saved to {(outdir / 'best_per_benchmark.pdf').with_suffix('.png')}") + + +__all__ = [ + "analyse_cmd", + "analyse_leaderboard_cmd", + "analyse_leaderboard_paper_cmd", + "analyse_model_agent_cmd", + "analyse_model_win_rate_cmd", + "analyse_config_win_rate_cmd", + "analyse_best_per_benchmark_cmd", + "analyse_tool_shortlist_cmd", + "analyse_cost_efficiency_cmd", + "analyse_component_impact_cmd", + "analyse_correlation_cmd", + "analyse_cost_score_cmd", + "analyse_paper_figures_cmd", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/batch.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/batch.py new file mode 100644 index 00000000..0e7fc937 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/batch.py @@ -0,0 +1,1031 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import csv +import glob +import json +import os +import sys +from pathlib import Path +from typing import Any + +import rich_click as click + +from ....core.types import RunConfig, SessionConfig +from ....core.types.session import SessionExecutionStatus, SessionOutcomeStatus +from ....utils.paths import get_run_paths, get_session_paths +from ...lib.api import aggregate, evaluate, execute, status +from ..options import ( + _format_exception_for_cli, + _should_show_traceback, + apply_debug_mode, +) +from ..render import render_batch_status + + +def _load_config_file(path: str) -> dict[str, Any]: + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _load_run_like_config(path: str) -> RunConfig | SessionConfig: + """Load a RunConfig or SessionConfig from a file with full validation.""" + payload = _load_config_file(path) + try: + return RunConfig.model_validate(payload) + except Exception: + return SessionConfig.model_validate(payload) + + +def _run_config_from_session(session_config: SessionConfig) -> RunConfig: + return RunConfig( + benchmark=session_config.benchmark, + agent=session_config.agent, + subset=session_config.subset, + task_ids=[session_config.task_id], + output_dir=session_config.output_dir, + cache_dir=session_config.cache_dir, + run_id=session_config.run_id, + model=session_config.model, + benchmark_kwargs=session_config.benchmark_kwargs, + agent_kwargs=session_config.agent_kwargs, + ) + + +def _state_from_counts( + *, + total: int, + completed: int, + running: int, + incomplete: int, + missing: int, +) -> str: + if total > 0 and completed == total and running == 0 and incomplete == 0: + return "complete" + if total > 0 and completed == 0 and running == 0 and incomplete == 0 and missing == total: + return "not_started" + return "in_progress" + + +def _short_config_path(path: str) -> str: + try: + rel = os.path.relpath(path) + except Exception: + rel = path + if rel.startswith(".."): + return Path(path).name + return rel + + +def _truncate_leading(text: str, max_len: int) -> str: + if max_len <= 3 or len(text) <= max_len: + return text + return "..." + text[-(max_len - 3) :] + + +def _format_batch_error(exc: Exception) -> str: + if _should_show_traceback(): + details = _format_exception_for_cli(exc) + else: + details = str(exc) + if not details: + details = repr(exc) + return details.replace("\n", "\n ") + + +def _format_config_link(path: str, *, max_len: int = 30) -> str: + display = _short_config_path(path) + display = _truncate_leading(display, max_len) + try: + target = Path(path).resolve() + except Exception: + return display + return f"[link=file://{target}]{display}[/link]" + + +def _format_float(value: Any) -> str: + try: + num = float(value) + except Exception: + return "-" + if abs(num) >= 1000: + return f"{num:,.0f}" + if abs(num) >= 10: + return f"{num:.2f}" + return f"{num:.4g}" + + +def _csv_value(value: Any) -> str: + if value is None: + return "" + if isinstance(value, (str, int, float, bool)): + return str(value) + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + + +def _write_session_config(session_config: SessionConfig, *, overwrite: bool) -> bool: + session_id = session_config.get_session_id() + sess_paths = get_session_paths(session_id) + config_path = sess_paths.session_config + if config_path.exists() and not overwrite: + return False + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w", encoding="utf-8") as f: + json.dump( + session_config.model_dump(mode="json"), + f, + ensure_ascii=False, + indent=2, + ) + return True + + +def _format_models(value: Any) -> str: + if value is None: + return "-" + if isinstance(value, list): + names = [str(item) for item in value if item is not None] + return ", ".join(names) if names else "-" + return str(value) + + +def _compute_session_id(config: dict[str, Any]) -> str: + session_cfg = SessionConfig.model_validate(config) + return session_cfg.get_session_id() + + +def _results_path_from_config_location(config_path: str, config_obj: RunConfig | SessionConfig) -> Path: + run_id = getattr(config_obj, "run_id", None) + if not run_id: + raise click.ClickException(f"Missing run_id in config: {config_path}") + output_dir = Path(config_path).parent + return (output_dir / run_id / "results.json").resolve() + + +def _parse_patch_values(pairs: tuple[str, ...]) -> dict[str, Any]: + updates: dict[str, Any] = {} + for pair in pairs: + if "=" not in pair: + raise click.ClickException(f"Invalid --set value (expected key=value): {pair}") + key, raw = pair.split("=", 1) + key = key.strip() + if not key: + raise click.ClickException(f"Invalid --set key: {pair}") + try: + value = json.loads(raw) + except Exception: + value = raw + updates[key] = value + return updates + + +def _apply_patch(payload: dict[str, Any], updates: dict[str, Any]) -> None: + for key, value in updates.items(): + if key.startswith("agent."): + key = f"agent_kwargs.{key[len('agent.'):]}" + elif key.startswith("benchmark."): + key = f"benchmark_kwargs.{key[len('benchmark.'):]}" + if "." not in key: + payload[key] = value + continue + cursor: dict[str, Any] = payload + parts = key.split(".") + for part in parts[:-1]: + if part not in cursor or not isinstance(cursor[part], dict): + cursor[part] = {} + cursor = cursor[part] + cursor[parts[-1]] = value + + +def _update_session_ids_in_dir(session_dir: Path, new_id: str) -> None: + for json_path in session_dir.rglob("*.json"): + try: + payload = _load_config_file(str(json_path)) + except Exception: + continue + if not isinstance(payload, dict) or "session_id" not in payload: + continue + if payload.get("session_id") == new_id: + continue + payload["session_id"] = new_id + with open(json_path, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + f.write("\n") + + +def _extract_task_id_from_session(session_dir: Path) -> str | None: + results_path = session_dir / "results.json" + if not results_path.exists(): + return None + try: + payload = _load_config_file(str(results_path)) + except Exception: + return None + if not isinstance(payload, dict): + return None + task_id = payload.get("task_id") or payload.get("task_key") + return str(task_id) if task_id is not None else None + + +def _recover_session_hashes(roots: list[Path], *, do_apply: bool) -> int: + changes = 0 + for root in roots: + sessions_root = root / "sessions" + if not sessions_root.exists(): + raise click.ClickException(f"sessions dir not found: {sessions_root}") + for cfg_path in sessions_root.rglob("config.json"): + if cfg_path.parent.parent != sessions_root: + continue + session_dir = cfg_path.parent + old_id = session_dir.name + config = _load_config_file(str(cfg_path)) + new_id = _compute_session_id(config) + if new_id == old_id: + continue + changes += 1 + click.echo(f"{old_id} -> {new_id} ({session_dir})") + if not do_apply: + continue + if "session_id" in config: + config["session_id"] = new_id + with open(cfg_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + f.write("\n") + _update_session_ids_in_dir(session_dir, new_id) + session_dir.rename(session_dir.with_name(new_id)) + return changes + + +def _load_results_summary( + path: str, +) -> tuple[str, str, str, int | None, int | None, int | None, int | None]: + if not path or not Path(path).is_file(): + return "-", "-", "-", None, None, None, None + try: + payload = _load_config_file(path) + except Exception: + return "-", "-", "-", None, None, None, None + score = payload.get("benchmark_score") + if score is None: + score = payload.get("average_score") + cost = payload.get("total_run_cost") + if cost is None: + cost = payload.get("total_agent_cost") + models = payload.get("model_names") + if models is None: + models = payload.get("model_name") + ready = None + finished = None + errors = None + aggregated = None + session_results = payload.get("session_results") + aggregated_ids = payload.get("aggregated_session_ids") + if isinstance(aggregated_ids, list): + aggregated = len(aggregated_ids) + elif isinstance(payload.get("completed_sessions"), int): + aggregated = payload.get("completed_sessions") + if isinstance(session_results, list): + ready = 0 + finished = 0 + errors = 0 + for item in session_results: + if not isinstance(item, dict): + continue + status = str(item.get("status") or "").lower() + is_finished = item.get("is_finished") + if status in ("error", "cancelled"): + errors += 1 + else: + ready += 1 + if is_finished is True: + finished += 1 + return ( + _format_float(score), + _format_float(cost), + _format_models(models), + ready, + finished, + errors, + aggregated, + ) + + +def _expand_config_inputs( + config_values: tuple[str, ...], + extra_args: list[str], +) -> list[str]: + if not config_values: + raise click.ClickException("At least one --config is required.") + + raw_values = [*config_values, *extra_args] + expanded: list[str] = [] + + for value in raw_values: + value = str(value).strip() + if not value: + continue + if glob.has_magic(value): + matches = [str(Path(p)) for p in sorted(glob.glob(value, recursive=True)) if Path(p).is_file()] + if not matches: + raise click.ClickException(f"No config files matched pattern: {value}") + expanded.extend(matches) + else: + path = Path(value) + if not path.is_file(): + raise click.ClickException(f"Config file not found: {value}") + expanded.append(str(path)) + + deduped: list[str] = [] + seen: set[str] = set() + for path in expanded: + normalized = str(Path(path)) + if normalized in seen: + continue + seen.add(normalized) + deduped.append(normalized) + if not deduped: + raise click.ClickException("No config files resolved from --config values.") + return deduped + + +@click.group("batch") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +def batch_cmd(debug: bool) -> None: + """Batch operations over multiple config files.""" + apply_debug_mode(debug) + + +@batch_cmd.command( + "status", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.pass_context +def batch_status_cmd( + ctx: click.Context, + debug: bool, + config_values: tuple[str, ...], +) -> None: + """Show a status table for multiple config files.""" + apply_debug_mode(debug) + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + rows: list[dict[str, str]] = [] + + for i, config_path in enumerate(config_paths, start=1): + row: dict[str, str] = { + "#": str(i), + "config": _format_config_link(config_path, max_len=30), + "run_id": "-", + "benchmark": "-", + "agent": "-", + "subset": "-", + "models": "-", + "ready": "-", + "aggregated": "-", + "finished": "-", + "errors": "-", + "score": "-", + "cost": "-", + } + try: + cfg = _load_run_like_config(config_path) + run_status = status(cfg) + ( + score, + cost, + models, + ready, + finished, + errors, + aggregated, + ) = _load_results_summary(run_status.results_path) + if models == "-": + models = run_status.model_name or "-" + models = _truncate_leading(models, 24) + if ready is None or finished is None or errors is None: + ready = 0 + finished = 0 + errors = 0 + if aggregated is None: + aggregated = 0 + for item in run_status.session_statuses: + if item.status != SessionExecutionStatus.COMPLETED: + continue + if item.result_status in ( + SessionOutcomeStatus.ERROR, + SessionOutcomeStatus.CANCELLED, + ): + errors += 1 + else: + ready += 1 + if aggregated is not None: + aggregated += 1 + if item.result_status in ( + SessionOutcomeStatus.SUCCESS, + SessionOutcomeStatus.UNSUCCESSFUL, + ): + finished += 1 + row.update( + { + "run_id": run_status.run_id, + "benchmark": run_status.benchmark_slug_name, + "agent": run_status.agent_slug_name, + "subset": run_status.subset_name or "-", + "models": models, + "ready": f"{ready}/{run_status.total_tasks}", + "aggregated": f"{aggregated}/{run_status.total_tasks}", + "finished": f"{finished}/{run_status.total_tasks}", + "errors": f"{errors}/{run_status.total_tasks}", + "score": score, + "cost": cost, + } + ) + except Exception: + pass + rows.append(row) + + render_batch_status(rows) + + +@batch_cmd.command( + "evaluate", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.pass_context +def batch_evaluate_cmd( + ctx: click.Context, + debug: bool, + config_values: tuple[str, ...], +) -> None: + """Evaluate configs sequentially.""" + apply_debug_mode(debug) + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + failures: list[tuple[str, str]] = [] + + for config_path in config_paths: + click.echo(f"Running: {config_path}") + try: + cfg = _load_run_like_config(config_path) + evaluate(config=cfg) + except Exception as exc: + failures.append((config_path, str(exc))) + click.echo(f"Error: {config_path}\n {_format_batch_error(exc)}") + + if failures: + raise click.ClickException("Batch evaluate completed with errors in " + ", ".join(path for path, _ in failures)) + + +@batch_cmd.command( + "execute", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.pass_context +def batch_execute_cmd( + ctx: click.Context, + debug: bool, + config_values: tuple[str, ...], +) -> None: + """Execute configs sequentially (no aggregation).""" + apply_debug_mode(debug) + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + failures: list[tuple[str, str]] = [] + + for config_path in config_paths: + click.echo(f"Running: {config_path}") + try: + cfg = _load_run_like_config(config_path) + execute(config=cfg) + except Exception as exc: + failures.append((config_path, str(exc))) + click.echo(f"Error: {config_path}\n {_format_batch_error(exc)}") + + if failures: + raise click.ClickException("Batch execute completed with errors in " + ", ".join(path for path, _ in failures)) + + +@batch_cmd.command( + "prepare", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option( + "--overwrite", + is_flag=True, + help="Overwrite existing session config files.", +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.pass_context +def batch_prepare_cmd( + ctx: click.Context, + debug: bool, + overwrite: bool, + config_values: tuple[str, ...], +) -> None: + """Prepare session directories and configs without executing.""" + apply_debug_mode(debug) + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + failures: list[tuple[str, str]] = [] + + for config_path in config_paths: + click.echo(f"Preparing: {config_path}") + try: + cfg = _load_run_like_config(config_path) + if isinstance(cfg, RunConfig): + with cfg.get_context(): + session_configs = cfg.get_session_configs() + created = 0 + for sc in session_configs: + if _write_session_config(sc, overwrite=overwrite): + created += 1 + click.echo(f"Prepared {created}/{len(session_configs)} sessions.") + else: + with cfg.get_context(): + created = 1 if _write_session_config(cfg, overwrite=overwrite) else 0 + click.echo(f"Prepared {created}/1 sessions.") + except Exception as exc: + failures.append((config_path, str(exc))) + click.echo(f"Error: {config_path}\n {_format_batch_error(exc)}") + + if failures: + raise click.ClickException("Batch prepare completed with errors in " + ", ".join(path for path, _ in failures)) + + +@batch_cmd.command( + "aggregate", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.pass_context +def batch_aggregate_cmd( + ctx: click.Context, + debug: bool, + config_values: tuple[str, ...], +) -> None: + """Aggregate configs sequentially (no execution).""" + apply_debug_mode(debug) + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + failures: list[tuple[str, str]] = [] + + for config_path in config_paths: + click.echo(f"Running: {config_path}") + try: + cfg = _load_run_like_config(config_path) + aggregate(config=cfg) + except Exception as exc: + failures.append((config_path, str(exc))) + click.echo(f"Error: {config_path}\n {_format_batch_error(exc)}") + + if failures: + raise click.ClickException( + "Batch aggregate completed with errors in " + ", ".join(path for path, _ in failures) + ) + + +@batch_cmd.command( + "patch", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.option( + "--set", + "set_values", + multiple=True, + help="key=value pairs to update (repeatable). Supports dotted paths.", +) +@click.option( + "--apply", + is_flag=True, + help="Apply changes (otherwise dry-run).", +) +@click.option( + "--dry-run", + is_flag=True, + help="Preview changes without applying.", +) +@click.pass_context +def batch_patch_cmd( + ctx: click.Context, + config_values: tuple[str, ...], + set_values: tuple[str, ...], + apply: bool, + dry_run: bool, +) -> None: + """Patch run/session configs and recover session hashes.""" + if apply and dry_run: + raise click.ClickException("Use only one of --apply or --dry-run.") + if not (config_values or ctx.args): + raise click.ClickException("Provide at least one --config.") + if not set_values: + raise click.ClickException("Provide at least one --set key=value.") + do_apply = apply and not dry_run + updates = _parse_patch_values(set_values) + + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + run_roots: list[Path] = [] + for config_path in config_paths: + cfg = _load_run_like_config(config_path) + with cfg.get_context(): + run_paths = get_run_paths() + run_roots.append(Path(run_paths.root)) + payload = _load_config_file(config_path) + _apply_patch(payload, updates) + click.echo(f"Patch config: {config_path}") + if do_apply: + with open(config_path, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + f.write("\n") + + seen_roots: set[Path] = set() + for root in run_roots: + if root in seen_roots: + continue + seen_roots.add(root) + run_config_path = root / "run" / "config.json" + run_config_payload: dict[str, Any] | None = None + if run_config_path.exists(): + run_config_payload = _load_config_file(str(run_config_path)) + _apply_patch(run_config_payload, updates) + click.echo(f"Patch run config: {run_config_path}") + if do_apply: + with open(run_config_path, "w", encoding="utf-8") as f: + json.dump(run_config_payload, f, ensure_ascii=False, indent=2) + f.write("\n") + run_config_obj = RunConfig.model_validate(run_config_payload) if run_config_payload is not None else None + + sessions_root = root / "sessions" + if not sessions_root.exists(): + raise click.ClickException(f"sessions dir not found: {sessions_root}") + changes = 0 + for cfg_path in sessions_root.rglob("config.json"): + if cfg_path.parent.parent != sessions_root: + continue + session_dir = cfg_path.parent + old_id = session_dir.name + config = _load_config_file(str(cfg_path)) + _apply_patch(config, updates) + new_id = _compute_session_id(config) + if new_id != old_id: + changes += 1 + click.echo(f"{old_id} -> {new_id} ({session_dir})") + if do_apply: + with open(cfg_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + f.write("\n") + # Handle sessions without config.json by deriving from run config + results.json task id. + if run_config_obj is not None: + for session_dir in sessions_root.iterdir(): + if not session_dir.is_dir(): + continue + cfg_path = session_dir / "config.json" + if cfg_path.exists(): + continue + task_id = _extract_task_id_from_session(session_dir) + if task_id is None: + continue + old_id = session_dir.name + new_id = run_config_obj.to_session_config(task_id).get_session_id() + if new_id != old_id: + changes += 1 + target_dir = session_dir.with_name(new_id) + if target_dir.exists(): + alt_name = f"{new_id}__dup__{old_id}" + target_dir = session_dir.with_name(alt_name) + click.echo(f"{old_id} -> {new_id} (collision, renaming to {alt_name})") + else: + click.echo(f"{old_id} -> {new_id} ({session_dir})") + if do_apply: + _update_session_ids_in_dir(session_dir, new_id) + session_dir.rename(target_dir) + if do_apply: + _recover_session_hashes([root], do_apply=True) + + if not do_apply: + click.echo(f"Dry run complete. {changes} session(s) would be renamed.") + else: + click.echo(f"Done. {changes} session(s) renamed.") + + +@batch_cmd.command( + "extract", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.option( + "--output", + "output_path", + default="batch_results.csv", + show_default=True, + help="CSV output path (use '-' for stdout).", +) +@click.pass_context +def batch_extract_cmd( + ctx: click.Context, + config_values: tuple[str, ...], + output_path: str, +) -> None: + """Extract run results from multiple configs into a single CSV.""" + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + failures: list[tuple[str, str]] = [] + rows: list[dict[str, Any]] = [] + all_keys: set[str] = set() + preferred_keys = [ + "config_path", + "results_path", + "run_id", + ] + + for config_path in config_paths: + results_path = "-" + run_id = "-" + try: + cfg = _load_run_like_config(config_path) + run_id = getattr(cfg, "run_id", None) or "-" + results_path = _results_path_from_config_location(config_path, cfg) + if not results_path.is_file(): + raise click.ClickException(f"Results not found: {results_path}") + payload = _load_config_file(str(results_path)) + if not isinstance(payload, dict): + raise click.ClickException(f"Results JSON is not an object: {results_path}") + if "benchmark_score" not in payload: + raise click.ClickException(f"Missing benchmark_score in results: {results_path}") + row: dict[str, Any] = { + "config_path": config_path, + "results_path": str(results_path), + "run_id": run_id, + } + for key, value in payload.items(): + if key in row: + row[f"results_{key}"] = value + else: + row[key] = value + except Exception as exc: + failures.append((config_path, str(exc))) + row = { + "config_path": config_path, + "results_path": results_path, + "run_id": run_id, + "error": _format_batch_error(exc), + } + rows.append(row) + all_keys.update(row.keys()) + + ordered_keys = [k for k in preferred_keys if k in all_keys] + ordered_keys.extend(sorted(k for k in all_keys if k not in ordered_keys)) + + if output_path == "-": + out_stream = sys.stdout + writer = csv.DictWriter(out_stream, fieldnames=ordered_keys) + writer.writeheader() + for row in rows: + writer.writerow({k: _csv_value(row.get(k)) for k in ordered_keys}) + else: + out_file = Path(output_path) + out_file.parent.mkdir(parents=True, exist_ok=True) + with open(out_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=ordered_keys) + writer.writeheader() + for row in rows: + writer.writerow({k: _csv_value(row.get(k)) for k in ordered_keys}) + click.echo(f"Wrote {len(rows)} row(s) to {out_file}.") + + if failures: + raise click.ClickException("Batch extract completed with errors in " + ", ".join(path for path, _ in failures)) + + +# Fields to exclude when publishing to HuggingFace (internal/bulky data). +_PUBLISH_EXCLUDE_FIELDS: set[str] = { + "config_path", + "results_path", + "run_id", + "aggregated_session_ids", + "executed_session_ids", + "planned_session_ids", + "skipped_session_ids", + "skipped_session_reasons", + "missing_result_files", + "session_results", + "accumulated_agent_report", + "accumulated_benchmark_report", + "aggregation_mode", + "max_workers", + "running_sessions", + "model_names", + "agent_slug_name", + "benchmark_slug_name", + "planned_sessions", +} + + +def _collect_results_rows( + config_paths: list[str], +) -> tuple[list[dict[str, Any]], list[tuple[str, str]]]: + """Load results from config paths and return clean rows for publishing. + + Uses lightweight JSON loading to find results.json without importing + benchmark or agent modules, so it works even when those aren't installed. + """ + failures: list[tuple[str, str]] = [] + rows: list[dict[str, Any]] = [] + + for config_path in config_paths: + try: + # Read config as raw JSON to get run_id without importing modules + raw_config = _load_config_file(config_path) + run_id = raw_config.get("run_id") + if not run_id: + raise click.ClickException(f"Missing run_id in config: {config_path}") + output_dir = Path(config_path).parent + results_path = (output_dir / run_id / "results.json").resolve() + if not results_path.is_file(): + raise click.ClickException(f"Results not found: {results_path}") + payload = _load_config_file(str(results_path)) + if not isinstance(payload, dict): + raise click.ClickException(f"Results JSON is not an object: {results_path}") + if "benchmark_score" not in payload: + raise click.ClickException(f"Missing benchmark_score in results: {results_path}") + row: dict[str, Any] = {} + for key, value in payload.items(): + if key not in _PUBLISH_EXCLUDE_FIELDS: + row[key] = value + rows.append(row) + except Exception as exc: + failures.append((config_path, str(exc))) + + return rows, failures + + +@batch_cmd.command( + "publish", + context_settings={"allow_extra_args": True}, +) +@click.option( + "--config", + "config_values", + multiple=True, + help="RunConfig/SessionConfig path or glob pattern (repeatable).", +) +@click.option( + "--repo", + "repo_id", + required=True, + help="HuggingFace dataset repo ID (e.g. 'Exgentic/open-agent-leaderboard-results').", +) +@click.option( + "--private/--public", + "private", + default=True, + show_default=True, + help="Whether the dataset should be private.", +) +@click.option( + "--append/--overwrite", + "append", + default=True, + show_default=True, + help="Append to existing dataset or overwrite it.", +) +@click.pass_context +def batch_publish_cmd( + ctx: click.Context, + config_values: tuple[str, ...], + repo_id: str, + private: bool, + append: bool, +) -> None: + """Publish run results to a HuggingFace dataset.""" + try: + from datasets import Dataset, load_dataset + except ImportError as err: + raise click.ClickException( + "The 'datasets' package is required for publishing. Install it with: pip install datasets" + ) from err + + config_paths = _expand_config_inputs(config_values, list(ctx.args)) + rows, failures = _collect_results_rows(config_paths) + + if not rows: + raise click.ClickException("No valid results to publish.") + + if append: + try: + existing_ds = load_dataset(repo_id, split="train") + existing_rows = list(existing_ds) + click.echo(f"Loaded {len(existing_rows)} existing row(s) from {repo_id}.") + + # Deduplicate by (benchmark, agent, model) triple + existing_keys = set() + for r in existing_rows: + key = (r.get("benchmark"), r.get("agent"), r.get("model")) + existing_keys.add(key) + + new_rows = [] + updated = 0 + for row in rows: + key = (row.get("benchmark"), row.get("agent"), row.get("model")) + if key in existing_keys: + # Replace existing row with updated one + existing_rows = [ + r for r in existing_rows if (r.get("benchmark"), r.get("agent"), r.get("model")) != key + ] + updated += 1 + new_rows.append(row) + + all_rows = existing_rows + new_rows + click.echo(f"Publishing {len(all_rows)} row(s) " f"({len(new_rows) - updated} new, {updated} updated).") + except Exception: + click.echo("No existing dataset found, creating new one.") + all_rows = rows + else: + all_rows = rows + + ds = Dataset.from_list(all_rows) + ds.push_to_hub(repo_id, private=private) + click.echo(f"Published {len(all_rows)} row(s) to https://huggingface.co/datasets/{repo_id}") + + if failures: + click.echo(f"Warning: {len(failures)} config(s) had errors and were skipped:") + for path, err in failures: + click.echo(f" {path}: {err}") + + +__all__ = [ + "batch_cmd", + "batch_status_cmd", + "batch_evaluate_cmd", + "batch_execute_cmd", + "batch_prepare_cmd", + "batch_aggregate_cmd", + "batch_patch_cmd", + "batch_extract_cmd", + "batch_publish_cmd", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/compare.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/compare.py new file mode 100644 index 00000000..97b8a7d1 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/compare.py @@ -0,0 +1,1169 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, NamedTuple, Optional, Tuple, Union + +import rich_click as click +from rich.console import Console +from rich.progress import ( + BarColumn, + Progress, + SpinnerColumn, + TaskProgressColumn, + TextColumn, +) +from rich.table import Table + +from ....core.types import SessionResults + +stats = None +StratifiedTable = None + + +def _ensure_compare_deps() -> None: + global stats, StratifiedTable + if stats is None or StratifiedTable is None: + try: + from scipy import stats as _stats + from statsmodels.stats.contingency_tables import StratifiedTable as _StratifiedTable + except ImportError as exc: + raise click.ClickException( + "Compare commands require the optional analysis dependencies. " + "Install them with `pip install 'exgentic[analysis]'`." + ) from exc + + stats = _stats + StratifiedTable = _StratifiedTable + + +class TaskInfo(NamedTuple): + """Task information including session result, setup info, and file path.""" + + session_result: SessionResults + setup_info: str # Agent name or model name when comparing across agents/models + file_path: str # Path to the results.json file + + +class BenchmarkStats(NamedTuple): + """Statistics for a single benchmark comparison between two setups.""" + + benchmark_name: str + setup1_label: str + setup2_label: str + success_rate1: float + success_rate2: float + rate_diff: float + p_value: float + num_tasks: int + is_significant: bool + winner: str + success1: list[bool] # For Breslow-Day test + success2: list[bool] # For Breslow-Day test + breslow_day_pvalue: Optional[float] = 0 # Breslow-Day test p-value (if multiple benchmarks) + breslow_day_interpretation: Optional[str] = "" # Breslow-Day test interpretation + + +class PairwiseComparison(NamedTuple): + """Complete pairwise comparison data between two setups.""" + + setup1_label: str + setup2_label: str + per_benchmark: dict[str, BenchmarkStats] # benchmark_name -> stats + aggregate_stats: Optional[BenchmarkStats] # Overall stats across benchmarks + + +# Type alias for composite key: either a string (task_id) or tuple (task_id, setup_info) +CompositeKey = Union[str, Tuple[str, str]] + + +def _parse_benchmark_spec(benchmark_spec: str) -> tuple[str, str | None, int | None]: + """Parse benchmark specification in format 'benchmark', 'benchmark/subset', or 'benchmark/subset=limit'. + + Examples: + 'tau' -> ('tau', None, None) + 'tau/retail' -> ('tau', 'retail', None) + 'tau/retail=30' -> ('tau', 'retail', 30) + 'gsm8k=100' -> ('gsm8k', None, 100) + + Returns: + Tuple of (benchmark_name, subset_name or None, limit or None) + """ + # Check for limit parameter (=N) + limit = None + if "=" in benchmark_spec: + benchmark_spec, limit_str = benchmark_spec.rsplit("=", 1) + try: + limit = int(limit_str) + except ValueError: + raise click.ClickException( + f"Invalid limit value in benchmark spec: {limit_str}. Must be an integer." + ) from None + + # Check for subset (/subset) + if "/" in benchmark_spec: + parts = benchmark_spec.split("/", 1) + return parts[0], parts[1], limit + return benchmark_spec, None, limit + + +def _load_run_results( + output_dir: str, + agent: str | None, + model: str | None, + benchmark: str, + subset: str | None = None, + agent_kwargs: dict[str, Any] | None = None, + benchmark_kwargs: dict[str, Any] | None = None, + progress: Progress | None = None, + task_id: Any = None, +) -> dict[CompositeKey, TaskInfo] | None: + """Load run results for a specific agent/model/benchmark combination by searching config.json files. + + If agent is None, loads results from all agents and stores agent name in setup_info. + If model is None, loads results from all models and stores model name in setup_info. + + Args: + output_dir: Directory containing run outputs + agent: Agent name to filter by, or None for all agents + model: Model name to filter by, or None for all models + benchmark: Benchmark name to search for + subset: Optional benchmark subset + agent_kwargs: Optional agent configuration parameters + benchmark_kwargs: Optional benchmark configuration parameters + progress: Optional Rich Progress instance for showing progress + task_id: Optional task ID for updating progress + + Returns: + Dict mapping composite_key (task_id or (task_id, setup_info)) -> TaskInfo, or None if not found + """ + output_path = Path(output_dir) + results_by_task: dict[CompositeKey, TaskInfo] = {} + + # Collect all config files first to show progress + config_files = list(output_path.glob("**/config.json")) + + # Search through all config.json files in the output directory + for idx, config_file in enumerate(config_files): + if progress and task_id is not None: + progress.update(task_id, completed=idx, total=len(config_files)) + try: + with open(config_file, encoding="utf-8") as f: + config_data = json.load(f) + + # Handle two different config.json formats: + # 1. Direct format: {"benchmark": "...", "agent": "...", ...} + # 2. Nested format: {"benchmark": {"slug_name": "..."}, "agent": {"slug_name": "..."}, ...} + + # Extract benchmark name + config_benchmark = config_data.get("benchmark") + if isinstance(config_benchmark, dict): + config_benchmark = config_benchmark.get("slug_name") + + # Extract agent name + config_agent = config_data.get("agent") + if isinstance(config_agent, dict): + config_agent = config_agent.get("slug_name") + + # Extract model name + config_model = config_data.get("model") + if config_model is None and isinstance(config_data.get("agent"), dict): + config_model = config_data.get("agent", {}).get("model_name") + + # Extract subset name + config_subset = config_data.get("subset") + if config_subset is None and isinstance(config_data.get("benchmark"), dict): + config_subset = config_data.get("benchmark", {}).get("params", {}).get("subset") + + # Check if this config file matches our criteria + # Check benchmark + if config_benchmark != benchmark: + continue + + # Check agent if specified + if agent is not None: + if config_agent != agent: + continue + + # Check model if specified + if model is not None: + if config_model is None: + continue + # Normalize model names for comparison (handle different formats) + if model not in config_model and config_model not in model: + continue + + # Check subset if specified + if subset is not None: + if config_subset != subset: + continue + + # If we get here, this is a match + if progress and task_id is not None: + progress.update(task_id, description=f"Loading from {config_file.parent.name}...") + + # Determine the run directory based on config location + # Config can be at: outputs/config.json or outputs/{run_id}/run/config.json + if config_file.name == "config.json" and config_file.parent.name == "run": + run_dir = config_file.parent.parent + elif config_file.parent == output_path: + # Top-level config.json, need to find the actual run directory + run_id = config_data.get("run_id") + if run_id: + run_dir = output_path / run_id + else: + continue + else: + # Assume config is in the run directory + run_dir = config_file.parent + + sessions_dir = run_dir / "sessions" + + if not sessions_dir.exists(): + continue + + # Iterate through all session directories + for session_dir in sessions_dir.iterdir(): + if not session_dir.is_dir(): + continue + + session_results_file = session_dir / "results.json" + if not session_results_file.exists(): + continue + + try: + with open(session_results_file, encoding="utf-8") as f: + session_data = json.load(f) + + # Validate session result and ensure success is consistent with score + session_result = SessionResults.model_validate(session_data) + + # Check if success field is consistent with score (success should be True iff score == 1) + expected_success = session_result.score == 1.0 + if session_result.success != expected_success: + # console = Console() + # console.print( + # f"[yellow]Warning: Inconsistent success field in {session_results_file}[/yellow]\n" + # f" Session ID: {session_result.session_id}\n" + # f" Score: {session_result.score}\n" + # f" Success field: {session_result.success}\n" + # f" Expected success: {expected_success}\n" + # f" Correcting success to match score..." + # ) + # Correct the success field to match the score + session_result.success = expected_success + session_task_id = session_result.task_id + if session_task_id == "" or session_task_id is None: + raise ValueError( + f"Illegal task key '{session_task_id}'!\n" f" File: {session_results_file}\n" + ) + + # Determine setup info based on what's being compared + setup_info = "" + if agent is None and config_agent: + setup_info = config_agent + elif model is None and config_model: + setup_info = config_model + + # Create composite key: (task_id, setup_info) for unique identification + # When setup_info is empty (specific agent/model), just use task_id + composite_key = (session_task_id, setup_info) if setup_info else session_task_id + + # Check for duplicate composite keys + if composite_key in results_by_task: + existing_task_info = results_by_task[composite_key] + if existing_task_info.session_result.success != session_result.success: + raise ValueError( + f"Duplicate entry found for task '{session_task_id}' with setup '{setup_info}'!\n" + f" First occurrence:\n" + f" File: {existing_task_info.file_path}\n" + f" Session ID: {existing_task_info.session_result.session_id}\n" + f" Success: {existing_task_info.session_result.success}\n" + f" Second occurrence:\n" + f" File: {session_results_file}\n" + f" Session ID: {session_result.session_id}\n" + f" Success: {session_result.success}\n" + f" This indicates overlapping results." + ) + + task_info = TaskInfo( + session_result=session_result, + setup_info=setup_info, + file_path=str(session_results_file), + ) + results_by_task[composite_key] = task_info + # print(f"Loaded session {session_result.session_id} with composite_key {composite_key} " + # f"with score {session_result.score} from file {session_results_file}.") + + except Exception as e: + # Print error but continue loading other sessions + console = Console() + console.print(f"[yellow]Warning: Error loading {session_results_file}: {e}[/yellow]") + continue + + # When agent or model is None, continue searching for more runs + # When both are specified, return after first match + # if config_agent is not None and config_model is not None and results_by_task: + # if progress and task_id is not None: + # progress.update(task_id, completed=len(config_files), + # description=f"Loaded {len(results_by_task)} sessions") + # return results_by_task + + except Exception as e: + # Print error but continue with other config files + console = Console() + console.print(f"[yellow]Warning: Error reading {config_file}: {e}[/yellow]") + continue + + # Complete progress + if progress and task_id is not None: + progress.update( + task_id, + completed=len(config_files), + description=f"Loaded {len(results_by_task)} sessions", + ) + + # Return accumulated results (for agent=None or model=None case) or None if nothing found + return results_by_task if results_by_task else None + + +def _calculate_benchmark_stats( + benchmark_name: str, + setup1_label: str, + setup2_label: str, + setup1_results: dict[CompositeKey, TaskInfo], + setup2_results: dict[CompositeKey, TaskInfo], + limit: int | None = None, +) -> Optional[BenchmarkStats]: + """Calculate statistics for comparing two setups on a single benchmark. + + Args: + benchmark_name: Name of the benchmark + setup1_label: Label for first setup + setup2_label: Label for second setup + setup1_results: Results for first setup + setup2_results: Results for second setup + limit: Optional limit on number of tasks to compare + + Returns: + BenchmarkStats object or None if no valid comparisons + """ + # Get comparison data + benchmark_data = _compare_results(benchmark_name, setup1_results, setup2_results) + + # Extract valid comparisons (tasks present in both setups) + valid_comparisons = [(s1, s2) for _, _, _, _, s1, s2 in benchmark_data if s1 is not None and s2 is not None] + + # Apply limit if specified + if limit is not None and len(valid_comparisons) > limit: + valid_comparisons = valid_comparisons[:limit] + + if not valid_comparisons: + return None + + # Extract success lists + success1 = [s1 for s1, _ in valid_comparisons] + success2 = [s2 for _, s2 in valid_comparisons] + + # Calculate rates + success_rate1 = sum(success1) / len(success1) + success_rate2 = sum(success2) / len(success2) + num_tasks = len(valid_comparisons) + rate_diff = success_rate1 - success_rate2 + + # Compute statistical significance + ( + statistic, + p_value, + significance, + method, + n01, + n10, + ) = _compute_statistical_significance_mcnemar(success1, success2) + + is_significant = p_value < 0.05 + winner = setup1_label if rate_diff > 0 else setup2_label if rate_diff < 0 else "Tie" + + return BenchmarkStats( + benchmark_name=benchmark_name, + setup1_label=setup1_label, + setup2_label=setup2_label, + success_rate1=success_rate1, + success_rate2=success_rate2, + rate_diff=rate_diff, + p_value=p_value, + num_tasks=num_tasks, + is_significant=is_significant, + winner=winner, + success1=success1, + success2=success2, + ) + + +def _calculate_aggregate_stats( + setup1_label: str, + setup2_label: str, + per_benchmark_stats: list[BenchmarkStats], +) -> Optional[BenchmarkStats]: + """Calculate aggregate statistics across multiple benchmarks. + + Args: + setup1_label: Label for first setup + setup2_label: Label for second setup + per_benchmark_stats: List of BenchmarkStats from individual benchmarks + + Returns: + BenchmarkStats object with aggregated data or None if no data + """ + if not per_benchmark_stats: + return None + + # Aggregate success lists from all benchmarks + all_success1 = [] + all_success2 = [] + for bench_stats in per_benchmark_stats: + all_success1.extend(bench_stats.success1) + all_success2.extend(bench_stats.success2) + + if not all_success1: + return None + + # Calculate aggregate rates + success_rate1 = sum(all_success1) / len(all_success1) + success_rate2 = sum(all_success2) / len(all_success2) + num_tasks = len(all_success1) + rate_diff = success_rate1 - success_rate2 + + # Compute statistical significance + ( + statistic, + p_value, + significance, + method, + n01, + n10, + ) = _compute_statistical_significance_mcnemar(all_success1, all_success2) + + is_significant = p_value < 0.05 + winner = setup1_label if rate_diff > 0 else setup2_label if rate_diff < 0 else "Tie" + + if len(per_benchmark_stats) > 1: + bd_data = [(bench_stats.success1, bench_stats.success2) for bench_stats in per_benchmark_stats] + bd_p_value, bd_interp = _compute_breslow_day_test(bd_data) + else: + bd_p_value, bd_interp = None, None + return BenchmarkStats( + benchmark_name="Overall", + setup1_label=setup1_label, + setup2_label=setup2_label, + success_rate1=success_rate1, + success_rate2=success_rate2, + rate_diff=rate_diff, + p_value=p_value, + num_tasks=num_tasks, + is_significant=is_significant, + winner=winner, + success1=all_success1, + success2=all_success2, + breslow_day_pvalue=bd_p_value, + breslow_day_interpretation=bd_interp, + ) + + +def _compare_results( + benchmark: str, + setup1_results: dict[CompositeKey, TaskInfo] | None, + setup2_results: dict[CompositeKey, TaskInfo] | None, +) -> list[tuple[str, str, str, str, bool | None, bool | None]]: + """Compare results from two setups for a single benchmark. + + Compares results by matching composite keys directly. For cross-agent/model comparisons, + this means comparing (task_id, setup_info) pairs. + + Returns: + List of (benchmark, task_id, setup1_info, setup2_info, setup1_success, setup2_success) + """ + comparison_data = [] + + if setup1_results is None and setup2_results is None: + return comparison_data + + setup1_tasks = setup1_results or {} + setup2_tasks = setup2_results or {} + + # Get all composite keys from both setups + all_keys = set(setup1_tasks.keys()) | set(setup2_tasks.keys()) + + # Remove None or empty keys + all_keys = {k for k in all_keys if k is not None and k != ""} + + for composite_key in sorted(all_keys): + # Extract task_id from composite key + task_id = composite_key[0] if isinstance(composite_key, tuple) else composite_key + + setup1_task_info = setup1_tasks.get(composite_key) + setup2_task_info = setup2_tasks.get(composite_key) + + setup1_success = setup1_task_info.session_result.success if setup1_task_info else None + setup2_success = setup2_task_info.session_result.success if setup2_task_info else None + setup1_info = setup1_task_info.setup_info if setup1_task_info else "" + setup2_info = setup2_task_info.setup_info if setup2_task_info else "" + + comparison_data.append( + ( + benchmark, + task_id, + setup1_info, + setup2_info, + setup1_success, + setup2_success, + ) + ) + + return comparison_data + + +def _compute_statistical_significance_mcnemar( + success1: list[bool], + success2: list[bool], +) -> tuple[float, float, str, str, int, int]: + """Compute statistical significance using McNemar's test for binary outcomes. + + McNemar's test is appropriate for comparing two classifiers on the same test set. + It tests whether the disagreements between the two classifiers are systematic. + + Args: + success1: Success outcomes from setup 1 (list of booleans) + success2: Success outcomes from setup 2 (list of booleans) + + Returns: + Tuple of (statistic, p_value, interpretation, method_name, n01, n10) + """ + if len(success1) < 2 or len(success2) < 2: + return 0.0, 1.0, "insufficient data", "McNemar's test", 0, 0 + + if len(success1) != len(success2): + return 0.0, 1.0, "mismatched sample sizes", "McNemar's test", 0, 0 + + # Build contingency table + # n01: setup1 failed, setup2 succeeded + # n10: setup1 succeeded, setup2 failed + n01 = sum(1 for s1, s2 in zip(success1, success2) if not s1 and s2) + n10 = sum(1 for s1, s2 in zip(success1, success2) if s1 and not s2) + + # McNemar's test statistic + # Use continuity correction for small samples + if n01 + n10 == 0: + return 0.0, 1.0, "no disagreements", "McNemar's test", n01, n10 + + # Chi-square statistic with continuity correction + statistic = ((abs(n01 - n10) - 1) ** 2) / (n01 + n10) + + # p-value from chi-square distribution with 1 degree of freedom + # Convert to native Python float for JSON serialization + p_value = float(1 - stats.chi2.cdf(statistic, df=1)) + + # Interpret the result + if p_value < 0.001: + significance = "highly significant (p < 0.001)" + elif p_value < 0.01: + significance = "very significant (p < 0.01)" + elif p_value < 0.05: + significance = "significant (p < 0.05)" + elif p_value < 0.1: + significance = "marginally significant (p < 0.1)" + else: + significance = "not significant (p >= 0.1)" + + return float(statistic), p_value, significance, "McNemar's test", n01, n10 + + +def _compute_breslow_day_test( + contingency_tables_data: list[tuple[list[bool], list[bool]]], +) -> tuple[float, str]: + """Compute Breslow-Day test for homogeneity of odds ratios across strata (benchmarks). + + The Breslow-Day test checks whether the odds ratios are consistent across different + benchmarks. A significant result suggests that the effect of one setup vs another + varies across benchmarks. + + Args: + contingency_tables_data: List of (success1, success2) tuples for each benchmark, + where success1/success2 are lists of boolean success values + + Returns: + Tuple of (p_value, interpretation) + """ + if len(contingency_tables_data) < 2: + return 1.0, "insufficient benchmarks (need at least 2)" + + # Build 2x2xK contingency table where K is the number of benchmarks + tables = [] + for success1, success2 in contingency_tables_data: + if len(success1) != len(success2): + continue + + # Build 2x2 table for this benchmark + # Rows: Setup 1 (success=1, failure=0) + # Cols: Setup 2 (success=1, failure=0) + n11 = sum(1 for s1, s2 in zip(success1, success2) if s1 and s2) + n10 = sum(1 for s1, s2 in zip(success1, success2) if s1 and not s2) + n01 = sum(1 for s1, s2 in zip(success1, success2) if not s1 and s2) + n00 = sum(1 for s1, s2 in zip(success1, success2) if not s1 and not s2) + + # Create 2x2 table: [[n11, n10], [n01, n00]] + table = [[n11, n10], [n01, n00]] + tables.append(table) + + if len(tables) < 2: + return 1.0, "insufficient valid benchmarks" + + try: + # Create StratifiedTable and run test_equal_odds (Breslow-Day test) + st = StratifiedTable(tables) + result = st.test_equal_odds() + p_value = float(result.pvalue) + + # Interpret the result + if p_value < 0.01: + interpretation = ( + "significant heterogeneity (p < 0.01) - relative success rates varies significantly across benchmarks" + ) + elif p_value < 0.05: + interpretation = "moderate heterogeneity (p < 0.05) - relative success rate vary across benchmarks" + elif p_value < 0.1: + interpretation = "marginal heterogeneity (p < 0.1) - relative success rate somewhat vary across benchmarks" + else: + interpretation = "homogeneous (p >= 0.1) - relative success rate is consistent across benchmarks" + + return p_value, interpretation + except Exception as e: + return 1.0, f"test failed: {e!s}" + + +def _render_significance_matrix( + benchmark_stats_list: list[BenchmarkStats], + title: str = "Statistical Significance Matrix", + benchmark_name: str = "", +) -> None: + """Render a matrix showing p-value significance levels between setups. + + Matrix format: + - Rows: Setup 1 + - Columns: Setup 2 + - Cell values: Significance level + * = p < 0.05 + ** = p < 0.01 + *** = p < 0.001 + - = not significant + + Args: + benchmark_stats_list: List of BenchmarkStats objects + title: Title for the matrix + benchmark_name: Optional benchmark name to include in table title + """ + console = Console() + + # Extract all unique setups and their success rates + setup_rates: dict[str, float] = {} + for bench_stats in benchmark_stats_list: + if bench_stats.setup1_label not in setup_rates: + setup_rates[bench_stats.setup1_label] = bench_stats.success_rate1 + if bench_stats.setup2_label not in setup_rates: + setup_rates[bench_stats.setup2_label] = bench_stats.success_rate2 + + # Sort setups by success rate (descending - best first) + sorted_setups = sorted(setup_rates.keys(), key=lambda s: setup_rates[s], reverse=True) + + # Build p-value matrix and winner matrix + # Key: (setup1, setup2), Value: (p_value, winner) + comparison_data: dict[tuple[str, str], tuple[float, str]] = {} + + for bench_stats in benchmark_stats_list: + comparison_data[(bench_stats.setup1_label, bench_stats.setup2_label)] = ( + bench_stats.p_value, + bench_stats.winner if bench_stats.is_significant else "", + ) + + # Create the matrix table + console.print(f"\n[bold cyan]{title}[/bold cyan]") + console.print("Significance levels: *** p<0.001, ** p<0.01, * p<0.05, - not significant\n") + + # Include benchmark name in table title if provided + table_title = f"P-value Significance Matrix: {benchmark_name}" if benchmark_name else "P-value Significance Matrix" + table = Table(show_header=True, show_lines=True, title=table_title) + + # Add header row + table.add_column("Setup", style="cyan", no_wrap=True) + for setup in sorted_setups: + table.add_column(setup, style="white", justify="center", no_wrap=True) + + # Add data rows + for row_setup in sorted_setups: + row_data = [row_setup] + for col_setup in sorted_setups: + if row_setup == col_setup: + # Diagonal: same setup + cell = "[dim]-[/dim]" + else: + # Look up comparison data for this pair + data = comparison_data.get((row_setup, col_setup)) + + if data is None: + # Try reverse lookup (col_setup vs row_setup) + data = comparison_data.get((col_setup, row_setup)) + if data is not None: + # Reverse the winner perspective + p_value, winner = data + # Winner stays the same, but we interpret from row's perspective + else: + p_value, winner = None, "" + else: + p_value, winner = data + + if p_value is None: + cell = "[dim]N/A[/dim]" + elif p_value < 0.001: + if winner == row_setup: + cell = "[green]***[/green]" + elif winner == col_setup: + cell = "[bright_red]***[/bright_red]" + else: + cell = "***" + elif p_value < 0.01: + if winner == row_setup: + cell = "[green]**[/green]" + elif winner == col_setup: + cell = "[bright_red]**[/bright_red]" + else: + cell = "**" + elif p_value < 0.05: + if winner == row_setup: + cell = "[green]*[/green]" + elif winner == col_setup: + cell = "[bright_red]*[/bright_red]" + else: + cell = "*" + else: + cell = "[dim]-[/dim]" + + row_data.append(cell) + + table.add_row(*row_data) + + console.print(table) + console.print("\n[dim]Green: row setup is significantly better | Red: column setup is significantly better[/dim]") + + +def _render_pairwise_summary_table( + benchmark_stats_list: list[BenchmarkStats], + title: str = "Pairwise Comparison Summary", +) -> None: + """Render pairwise comparison summary table showing which setups are significantly better. + + Args: + benchmark_stats_list: List of BenchmarkStats objects + title: Title for the table + """ + console = Console() + + console.print(f"\n[bold cyan]{title}[/bold cyan]") + console.print("Shows which setups are statistically significantly better than others (p < 0.05)\n") + + # First, render the significance matrix + # Extract benchmark name from title if present (format: "Comparison Results: benchmark_name") + benchmark_name = title.split(": ", 1)[1] if ": " in title else "" + _render_significance_matrix( + benchmark_stats_list, + title="Statistical Significance Matrix", + benchmark_name=benchmark_name, + ) + + # Sort by Success Rate 1 (descending), then by Success Rate 2 (descending) + sorted_results = sorted(benchmark_stats_list, key=lambda x: (-x.success_rate1, -x.success_rate2)) + + # Include benchmark name in detailed table title if present + detailed_table_title = f"Detailed Comparison: {benchmark_name}" if benchmark_name else "Detailed Comparison" + table = Table(title=detailed_table_title, show_lines=True) + table.add_column("Setup 1", style="cyan", no_wrap=True) + table.add_column("Setup 2", style="magenta", no_wrap=True) + table.add_column("Success Rate 1", style="green", justify="right") + table.add_column("Success Rate 2", style="blue", justify="right") + table.add_column("Rate Diff", style="yellow", justify="right") + table.add_column("# Tasks", style="white", justify="right") + table.add_column("p-value", style="white", justify="right") + table.add_column("Significant?", style="white", justify="center") + table.add_column("Winner", style="green", no_wrap=True) + table.add_column("Breslow-Day", style="white", justify="center") + + for bench_stats in sorted_results: + rate1_str = f"{bench_stats.success_rate1:.1%}" + rate2_str = f"{bench_stats.success_rate2:.1%}" + + diff_str = f"{bench_stats.rate_diff:+.1%}" + if bench_stats.rate_diff > 0: + diff_str = f"[green]{diff_str}[/green]" + elif bench_stats.rate_diff < 0: + diff_str = f"[bright_red]{diff_str}[/bright_red]" + + p_value_str = f"{bench_stats.p_value:.4f}" + + if bench_stats.is_significant: + sig_str = "[green]✓ Yes[/green]" + winner_str = f"[green]{bench_stats.winner}[/green]" + else: + sig_str = "[yellow]○ No[/yellow]" + winner_str = "[dim]No difference[/dim]" + + row_data = [ + bench_stats.setup1_label, + bench_stats.setup2_label, + rate1_str, + rate2_str, + diff_str, + str(bench_stats.num_tasks), + p_value_str, + sig_str, + winner_str, + ] + + if bench_stats.breslow_day_interpretation is not None and bench_stats.breslow_day_pvalue is not None: + if bench_stats.breslow_day_pvalue < 0.05: + bd_str = f"[yellow]p={bench_stats.breslow_day_pvalue:.3f}[/yellow]" + else: + bd_str = f"[green]p={bench_stats.breslow_day_pvalue:.3f}[/green]" + bd_str += f"\n{bench_stats.breslow_day_interpretation}" + row_data.append(bd_str) + + table.add_row(*row_data) + + console.print(table) + + +@click.command("compare") +@click.option("--agent1", help="First agent slug name (optional if comparing across agents)") +@click.option("--agent2", help="Second agent slug name (optional if comparing across agents)") +@click.option("--agent3", help="Third agent slug name (optional)") +@click.option("--agent4", help="Fourth agent slug name (optional)") +@click.option("--agent5", help="Fifth agent slug name (optional)") +@click.option("--model1", help="Model for first agent (optional)") +@click.option("--model2", help="Model for second agent (optional)") +@click.option("--model3", help="Model for third agent (optional)") +@click.option("--model4", help="Model for fourth agent (optional)") +@click.option("--model5", help="Model for fifth agent (optional)") +@click.option( + "--benchmark", + "benchmarks", + multiple=True, + required=True, + help="Benchmark(s) to compare in format 'benchmark', 'benchmark/subset', or 'benchmark/subset=limit' " + "(e.g., tau2/airline, gsm8k, tau2/retail=30 to limit to 30 instances)", +) +@click.option( + "--output-dir", + default="./outputs", + show_default=True, + help="Output directory to search for results", +) +@click.option( + "--format", + "output_format", + type=click.Choice(["text", "json"]), + default="text", + help="Output format", +) +def compare_cmd( + agent1: str | None, + agent2: str | None, + agent3: str | None, + agent4: str | None, + agent5: str | None, + model1: str | None, + model2: str | None, + model3: str | None, + model4: str | None, + model5: str | None, + benchmarks: tuple[str, ...], + output_dir: str, + output_format: str, +) -> None: + r"""Compare performance of multiple agent/model combinations across benchmarks. + + This command searches the output directory to locate results for each agent/model/benchmark combination. + + Benchmarks can be specified with optional subsets using the format 'benchmark/subset'. + + When comparing 2 setups: performs direct comparison. + When comparing 3+ setups: performs pairwise comparisons and shows significance matrix. + + Examples: + # Compare two agents and model configuration on a specific benchmark + exgentic compare --agent1 tool_calling --model1 openai/gcp/gemini-3-pro-preview \\ + --agent2 claude_code --model1 openai/aws/claude-opus-4-5 + + # Compare multiple agents (pairwise) + exgentic compare --agent1 tool_calling --agent2 claude_code --agent3 openai_solo \\ + --benchmark gsm8k + + # Compare multiple models + exgentic compare --model1 openai/aws/claude-opus-4-5 --model2 openai/gcp/gemini-3-pro-preview \\ + --benchmark tau2/airline + + # Compare up to 5 different setups + exgentic compare --agent1 tool_calling --model1 openai/aws/claude-opus-4-5t \\ + --agent2 claude_code --model2 openai/gcp/gemini-3-pro-preview \\ + --agent3 openai_sologemini --model3 openai/gcp/gemini-3-pro-preview \\ + --benchmark gsm8k --benchmark tau2/airline + """ + _ensure_compare_deps() + + if not benchmarks: + raise click.ClickException("At least one --benchmark is required.") + + # Collect all agent/model pairs + agents = [agent1, agent2, agent3, agent4, agent5] + models = [model1, model2, model3, model4, model5] + + # Filter out None values and create setup list + setups = [] + for i, (agent, model) in enumerate(zip(agents, models), 1): + if agent is not None or model is not None: + setups.append((i, agent, model)) + + if len(setups) < 2: + raise click.ClickException("At least 2 agent/model setups are required for comparison.") + + if len(setups) > 5: + raise click.ClickException("Maximum 5 agent/model setups are supported.") + + # Validate input consistency: must be one of three patterns + # 1. All agent/model pairs defined (both agent and model for each setup) + # 2. Only agents defined (all setups have agent, no models) + # 3. Only models defined (all setups have model, no agents) + + has_agents = [agent is not None for _, agent, _ in setups] + has_models = [model is not None for _, _, model in setups] + + all_have_agents = all(has_agents) + all_have_models = all(has_models) + none_have_agents = not any(has_agents) + none_have_models = not any(has_models) + + # Valid patterns: + # 1. All have both agent and model + # 2. All have agent, none have model + # 3. None have agent, all have model + + if not ( + (all_have_agents and all_have_models) + or (all_have_agents and none_have_models) + or (none_have_agents and all_have_models) + ): + raise click.ClickException( + "Invalid input combination. Must use one of these patterns:\n" + " 1. All setups with both agent and model (e.g., --agent1 X --model1 Y --agent2 A --model2 B)\n" + " 2. All setups with only agents (e.g., --agent1 X --agent2 Y --agent3 Z)\n" + " 3. All setups with only models (e.g., --model1 X --model2 Y --model3 Z)" + ) + + # Create labels for each setup + setup_labels = [] + for idx, agent, model in setups: + label = (f"{agent}" if agent else "") + (" and " if agent and model else "") + (f"{model}" if model else "") + if not label: + label = f"Setup {idx}" + setup_labels.append(label) + + # Load results for all setups across all benchmarks with progress bar + all_setup_results = [] + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=Console(), + ) as progress: + for setup_idx, (_idx, agent, model) in enumerate(setups): + setup_label = setup_labels[setup_idx] + setup_results = {} + + for benchmark_spec in benchmarks: + benchmark_name, subset, limit = _parse_benchmark_spec(benchmark_spec) + display_name = benchmark_spec + + # Create progress task for this load operation + task_id = progress.add_task(f"Loading {setup_label} on {display_name}...", total=100) + + results = _load_run_results( + output_dir, + agent, + model, + benchmark_name, + subset, + progress=progress, + task_id=task_id, + ) + + # Remove the task after completion + progress.remove_task(task_id) + + if results is None: + eval_cmd = "exgentic evaluate" + if agent: + eval_cmd += f" --agent {agent}" + if model: + eval_cmd += f" --model {model}" + eval_cmd += f" --benchmark {benchmark_name}" + if subset: + eval_cmd += f" --subset {subset}" + + raise click.ClickException( + f"No results found for {setup_label} on '{benchmark_spec}'. " f"Run '{eval_cmd}' first." + ) + + setup_results[display_name] = results + + all_setup_results.append(setup_results) + + # Calculate all pairwise comparisons using new calculation functions + all_pairwise_comparisons: list[PairwiseComparison] = [] + + # Compare all pairs (i, j) where i < j + for i in range(len(setups)): + for j in range(i + 1, len(setups)): + setup1_label = setup_labels[i] + setup2_label = setup_labels[j] + + # Calculate statistics for each benchmark + per_benchmark_stats: dict[str, BenchmarkStats] = {} + + for benchmark_spec in benchmarks: + benchmark_name, subset, limit = _parse_benchmark_spec(benchmark_spec) + display_name = benchmark_spec + + setup1_results = all_setup_results[i][display_name] + setup2_results = all_setup_results[j][display_name] + + # Calculate stats for this benchmark with limit + bench_stats = _calculate_benchmark_stats( + display_name, + setup1_label, + setup2_label, + setup1_results, + setup2_results, + limit=limit, + ) + + if bench_stats: + per_benchmark_stats[display_name] = bench_stats + + # Calculate aggregate statistics across all benchmarks + aggregate_stats = _calculate_aggregate_stats(setup1_label, setup2_label, list(per_benchmark_stats.values())) + + # Store pairwise comparison + all_pairwise_comparisons.append( + PairwiseComparison( + setup1_label=setup1_label, + setup2_label=setup2_label, + per_benchmark=per_benchmark_stats, + aggregate_stats=aggregate_stats, + ) + ) + + # Render results + if output_format == "text": + # Render per-benchmark tables + for benchmark_spec in benchmarks: + # Collect stats for this benchmark from all pairwise comparisons + benchmark_stats_for_display = [] + for comparison in all_pairwise_comparisons: + if benchmark_spec in comparison.per_benchmark: + benchmark_stats_for_display.append(comparison.per_benchmark[benchmark_spec]) + if benchmark_stats_for_display: + _render_pairwise_summary_table( + benchmark_stats_for_display, + title=f"Comparison Results: {benchmark_spec}", + ) + + # Render overall table with Breslow-Day test (if multiple benchmarks) + if len(benchmarks) > 1: + # Render each pairwise comparison with its own Breslow-Day result + aggregate_stats_list = [] + for comparison in all_pairwise_comparisons: + if comparison.aggregate_stats: + aggregate_stats_list.append(comparison.aggregate_stats) + + _render_pairwise_summary_table( + aggregate_stats_list, + title="Overall Comparison (All Benchmarks)", + ) + + else: + # JSON output + output = { + "setups": setup_labels, + "per_benchmark": {}, + "overall": {"pairwise_comparisons": []}, + } + + # Add per-benchmark results from all pairwise comparisons + for benchmark_spec in benchmarks: + benchmark_results = [] + for comparison in all_pairwise_comparisons: + if benchmark_spec in comparison.per_benchmark: + stats = comparison.per_benchmark[benchmark_spec] + benchmark_results.append( + { + "setup1": stats.setup1_label, + "setup2": stats.setup2_label, + "success_rate1": float(stats.success_rate1), + "success_rate2": float(stats.success_rate2), + "rate_difference": float(stats.rate_diff), + "p_value": float(stats.p_value), + "num_tasks": int(stats.num_tasks), + "is_significant": bool(stats.is_significant), + "winner": stats.winner, + } + ) + if benchmark_results: + output["per_benchmark"][benchmark_spec] = benchmark_results + + # Add overall results with Breslow-Day + for comparison in all_pairwise_comparisons: + if comparison.aggregate_stats: + stats = comparison.aggregate_stats + # Get Breslow-Day result from comparison object + if ( + comparison.aggregate_stats.breslow_day_pvalue is not None + and comparison.aggregate_stats.breslow_day_interpretation is not None + ): + breslow_day = { + "p_value": float(comparison.aggregate_stats.breslow_day_pvalue), + "interpretation": comparison.aggregate_stats.breslow_day_interpretation, + } + else: + breslow_day = None + + output["overall"]["pairwise_comparisons"].append( + { + "setup1": stats.setup1_label, + "setup2": stats.setup2_label, + "success_rate1": float(stats.success_rate1), + "success_rate2": float(stats.success_rate2), + "rate_difference": float(stats.rate_diff), + "p_value": float(stats.p_value), + "num_tasks": int(stats.num_tasks), + "is_significant": bool(stats.is_significant), + "winner": stats.winner, + "breslow_day": breslow_day, + } + ) + + print(json.dumps(output, indent=2)) + + +__all__ = ["compare_cmd"] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/dashboard.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/dashboard.py new file mode 100644 index 00000000..ad2a0206 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/dashboard.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import rich_click as click + +from ..options import apply_debug_mode + + +@click.command("dashboard") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +def dashboard_cmd(debug: bool) -> None: + """Start the experiments graphical dashboard.""" + apply_debug_mode(debug) + from ...dashboard.app import main as dashboard_main + + dashboard_main() + + +__all__ = ["dashboard_cmd"] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/evaluate.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/evaluate.py new file mode 100644 index 00000000..5ebb59bd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/evaluate.py @@ -0,0 +1,429 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +import rich_click as click + +from ....core.types import RunConfig, SessionConfig +from ....utils.settings import get_settings +from ...lib.api import ( + aggregate, + evaluate, + execute, +) +from ..options import add_run_options, has_run_options, run_with + + +def _is_isolated_runner(set_values: tuple[str, ...]) -> bool: + """Check if the runner is docker or venv (via --set or global settings).""" + isolated = {"docker", "venv"} + for item in set_values: + if "=" not in item: + continue + key, val = item.split("=", 1) + if key in ("benchmark.runner", "agent.runner", "settings.default_runner") and val.strip("\"'") in isolated: + return True + return get_settings().default_runner in isolated + + +def _get_runner_from_set(set_values: tuple[str, ...]) -> str | None: + """Extract the runner name from --set values, if specified.""" + for item in set_values: + if "=" not in item: + continue + key, val = item.split("=", 1) + if key in ("benchmark.runner", "agent.runner", "settings.default_runner"): + return val.strip("\"'") + return None + + +def _get_registry_entry(slug: str, kind: str): + """Look up a RegistryEntry for the given slug and kind ('benchmark' or 'agent').""" + from ...registry import AGENTS, BENCHMARKS + + registry = BENCHMARKS if kind == "benchmark" else AGENTS + entry = registry.get(slug) + if entry is None: + raise click.ClickException(f"Unknown {kind} slug '{slug}'") + return entry + + +def _needs_setup(name: str, install_type: str) -> bool: + """Check if a benchmark/agent has a setup.sh or requirements.txt.""" + from ...lib.api import needs_setup + + return needs_setup(name, install_type) + + +def _ensure_installed( + benchmark: str, + agent: str, + set_values: tuple[str, ...], +) -> None: + """Ensure benchmark/agent dependencies and data are installed. + + Env type is determined by the runner from --set flags or settings: + - docker -> DOCKER + - venv (default) -> VENV + - anything else (direct, thread, etc.) -> LOCAL + + For isolated runners (docker/venv), setup runs automatically without prompting. + For other runners, the user is prompted to confirm. + """ + from ....environment import EnvType + from ....environment.instance import get_manager + + mgr = get_manager() + runner = _get_runner_from_set(set_values) or get_settings().default_runner + if runner == "docker": + env_type = EnvType.DOCKER + elif runner == "venv": + env_type = EnvType.VENV + else: + env_type = EnvType.LOCAL + + to_install: list[tuple[str, str, str]] = [] + bench_name = f"benchmarks/{benchmark}" + agent_name = f"agents/{agent}" + + if not mgr.is_installed(bench_name, env_type=env_type) and _needs_setup(benchmark, "benchmark"): + to_install.append(("benchmark", benchmark, bench_name)) + if not mgr.is_installed(agent_name, env_type=env_type) and _needs_setup(agent, "agent"): + to_install.append(("agent", agent, agent_name)) + + if not to_install: + return + + if not _is_isolated_runner(set_values): + names = ", ".join(f"{t} '{n}'" for t, n, _ in to_install) + if not click.confirm(f"{names} not set up. Install now?", default=True): + raise click.Abort() + + for install_type, slug, name in to_install: + entry = _get_registry_entry(slug, install_type) + kwargs: dict = {"env_type": env_type, "module_path": entry.module} + if env_type in (EnvType.VENV, EnvType.DOCKER): + from ....environment.helpers import get_exgentic_install_target + + project_root, packages = get_exgentic_install_target() + if project_root is not None: + kwargs["project_root"] = project_root + if packages: + kwargs["packages"] = packages + mgr.install(name, **kwargs) + + +def _load_config_file(path: str) -> dict: + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _run_config_from_session(session_config: SessionConfig) -> RunConfig: + return RunConfig( + benchmark=session_config.benchmark, + agent=session_config.agent, + subset=session_config.subset, + task_ids=[session_config.task_id], + output_dir=session_config.output_dir, + cache_dir=session_config.cache_dir, + run_id=session_config.run_id, + model=session_config.model, + benchmark_kwargs=session_config.benchmark_kwargs, + agent_kwargs=session_config.agent_kwargs, + ) + + +@click.group("evaluate", invoke_without_command=True) +@add_run_options(required=False) +@click.option("--config", "config_path", help="RunConfig JSON file") +@click.pass_context +def evaluate_cmd( + ctx: click.Context, + benchmark: str | None, + agent: str | None, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, + config_path: str | None, +) -> None: + """Run sessions and aggregate results.""" + if config_path: + if ctx.invoked_subcommand is not None: + raise click.ClickException("--config cannot be used with subcommands.") + if has_run_options( + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + ): + raise click.ClickException("Do not pass run options together with --config.") + config = RunConfig.model_validate(_load_config_file(config_path)) + evaluate(config) + return + if ctx.invoked_subcommand is not None: + if has_run_options( + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + ): + raise click.ClickException( + "Pass options after the subcommand, e.g. 'exgentic evaluate execute --benchmark ...'." + ) + return + if not benchmark or not agent: + raise click.ClickException("--benchmark and --agent are required.") + + _ensure_installed(benchmark, agent, set_values) + + run_with( + evaluate, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + ) + + +@evaluate_cmd.command("execute") +@add_run_options +def evaluate_execute_cmd( + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, +) -> None: + """Run sessions only.""" + _ensure_installed(benchmark, agent, set_values) + run_with( + execute, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + ) + + +@evaluate_cmd.command("aggregate") +@add_run_options +def evaluate_aggregate_cmd( + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, +) -> None: + """Aggregate results from completed sessions.""" + run_with( + aggregate, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + ) + + +@evaluate_cmd.command("session") +@add_run_options(required=False) +@click.option( + "--config", + "session_config_path", + help="SessionConfig JSON file for a single session.", +) +def evaluate_session_cmd( + benchmark: str | None, + agent: str | None, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, + session_config_path: str | None, +) -> None: + """Run a single session.""" + if session_config_path: + if has_run_options( + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + ): + raise click.ClickException("Do not pass run options together with --config.") + session_config = SessionConfig.model_validate(_load_config_file(session_config_path)) + run_config = _run_config_from_session(session_config) + execute(run_config) + return + if not benchmark or not agent: + raise click.ClickException("--benchmark and --agent are required.") + if num_tasks is not None: + raise click.ClickException("Use --task instead of --num-tasks for sessions.") + if len(tasks) != 1: + raise click.ClickException("Exactly one --task is required for sessions.") + _ensure_installed(benchmark, agent, set_values) + run_with( + execute, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=None, + model=model, + debug=debug, + overwrite=overwrite, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + ) + + +__all__ = [ + "evaluate_aggregate_cmd", + "evaluate_cmd", + "evaluate_execute_cmd", + "evaluate_session_cmd", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/listing.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/listing.py new file mode 100644 index 00000000..d0d042ed --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/listing.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import rich_click as click + +from ...lib.api import list_agents, list_benchmarks, list_subsets, list_tasks +from ..options import apply_debug_mode +from ..render import render_list, render_named_list + + +@click.group("list") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +def list_cmd(debug: bool) -> None: + """List available resources.""" + apply_debug_mode(debug) + + +@list_cmd.command("benchmarks") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def list_benchmarks_cmd(debug: bool, output_format: str) -> None: + """List available benchmarks.""" + apply_debug_mode(debug) + items = list_benchmarks() + render_named_list(items, output_format, title="Benchmarks") + + +@list_cmd.command("agents") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def list_agents_cmd(debug: bool, output_format: str) -> None: + """List available agents.""" + apply_debug_mode(debug) + items = list_agents() + render_named_list(items, output_format, title="Agents") + + +@list_cmd.command("subsets") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option("--benchmark", required=True, help="Benchmark slug_name") +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def list_subsets_cmd(debug: bool, benchmark: str, output_format: str) -> None: + """List available subsets for a benchmark.""" + apply_debug_mode(debug) + try: + subsets = list_subsets(benchmark) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + render_list(subsets, output_format, title=f"Subsets ({benchmark})") + + +@list_cmd.command("tasks") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option("--benchmark", required=True, help="Benchmark slug_name") +@click.option("--subset", help="Benchmark subset name") +@click.option("--limit", type=int, help="Limit output to first N tasks") +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def list_tasks_cmd( + debug: bool, + benchmark: str, + subset: str | None, + limit: int | None, + output_format: str, +) -> None: + """List task ids for a benchmark.""" + apply_debug_mode(debug) + try: + tasks = list_tasks(benchmark=benchmark, subset=subset) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + if limit is not None: + tasks = tasks[: int(limit)] + label = f"Tasks ({benchmark})" if subset is None else f"Tasks ({benchmark}:{subset})" + render_list(tasks, output_format, title=label) + + +__all__ = [ + "list_cmd", + "list_benchmarks_cmd", + "list_agents_cmd", + "list_subsets_cmd", + "list_tasks_cmd", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/run_info.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/run_info.py new file mode 100644 index 00000000..2928f161 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/run_info.py @@ -0,0 +1,212 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +import rich_click as click + +from ....core.types import RunConfig, SessionConfig +from ...lib.api import preview, results, status +from ..options import add_run_options, has_run_options, run_query +from ..render import render_run_plan, render_run_results, render_run_status + + +@click.command("status") +@add_run_options(required=False, include_overwrite=False, include_max_workers=False) +@click.option( + "--config", + "config_path", + help="RunConfig or SessionConfig JSON file", +) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def status_cmd( + benchmark: str | None, + agent: str | None, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + config_path: str | None, + output_format: str, +) -> None: + """Show current run status.""" + if config_path: + if has_run_options( + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + overwrite=False, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=None, + ): + raise click.ClickException("Do not pass run options together with --config.") + payload = _load_config_file(config_path) + try: + config = RunConfig.model_validate(payload) + except Exception: + session_config = SessionConfig.model_validate(payload) + config = _run_config_from_session(session_config) + render_run_status(status(config), output_format) + return + if not benchmark or not agent: + raise click.ClickException("--benchmark and --agent are required.") + run_query( + run_func=status, + render_func=render_run_status, + output_format=output_format, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + ) + + +def _load_config_file(path: str) -> dict: + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _run_config_from_session(session_config: SessionConfig) -> RunConfig: + return RunConfig( + benchmark=session_config.benchmark, + agent=session_config.agent, + subset=session_config.subset, + task_ids=[session_config.task_id], + output_dir=session_config.output_dir, + cache_dir=session_config.cache_dir, + run_id=session_config.run_id, + model=session_config.model, + benchmark_kwargs=session_config.benchmark_kwargs, + agent_kwargs=session_config.agent_kwargs, + ) + + +@click.command("preview") +@add_run_options(include_overwrite=False, include_max_workers=False) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def preview_cmd( + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + output_format: str, +) -> None: + """Show the planned execution for a run.""" + run_query( + run_func=preview, + render_func=render_run_plan, + output_format=output_format, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + ) + + +@click.command("results") +@add_run_options(include_overwrite=False, include_max_workers=False) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text") +def results_cmd( + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + max_steps: int | None, + max_actions: int | None, + model: str | None, + debug: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + output_format: str, +) -> None: + """Load the saved run results.""" + run_query( + run_func=results, + render_func=render_run_results, + output_format=output_format, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + max_steps=max_steps, + max_actions=max_actions, + model=model, + debug=debug, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + ) + + +__all__ = ["status_cmd", "preview_cmd", "results_cmd"] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/serve.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/serve.py new file mode 100644 index 00000000..2399893f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/serve.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import logging + +import rich_click as click + +from ..options import apply_debug_mode + +logger = logging.getLogger(__name__) + + +@click.command("serve") +@click.option("--host", default="0.0.0.0", help="Host to bind to") +@click.option("--port", type=int, default=8080, help="Port to listen on") +@click.option("--cls", required=True, help="Class to serve (module.path:ClassName)") +@click.option("--kwargs", "kwargs_json", default=None, help="JSON constructor kwargs") +@click.option("--kwargs-b64", "kwargs_b64", default=None, help="Base64-encoded cloudpickle kwargs") +@click.option("--debug", is_flag=True, hidden=True) +def serve_cmd(host: str, port: int, cls: str, kwargs_json: str | None, kwargs_b64: str | None, debug: bool) -> None: + """Serve a class instance over HTTP.""" + apply_debug_mode(debug) + + import importlib + import json + import os + + from ....core.context import init_context_from_env + + try: + init_context_from_env() + except RuntimeError as exc: + logger.warning("Context init failed: %s", exc) + ctx_vars = {k: v for k, v in os.environ.items() if k.startswith("EXGENTIC_CTX")} + logger.debug("Context env vars: %s", ctx_vars) + + from ....adapters.runners.service import serve + + # Import the target module FIRST so that package __init__.py files + # (which may set environment variables like TAU2_DATA_DIR) run before + # cloudpickle deserialization triggers transitive library imports. + module_path, attr_name = cls.rsplit(":", 1) + mod = importlib.import_module(module_path) + klass = getattr(mod, attr_name) + + # Deserialize kwargs: JSON (preferred) or cloudpickle fallback. + if kwargs_b64 is not None: + import base64 + + import cloudpickle as cp + + kw = cp.loads(base64.b64decode(kwargs_b64)) + elif kwargs_json is not None: + kw = json.loads(kwargs_json) + else: + kw = {} + + obj = klass(**kw) + serve(obj, host=host, port=port) diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/setup.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/setup.py new file mode 100644 index 00000000..44ce50fd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/commands/setup.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import rich_click as click + +from ..options import apply_debug_mode + + +def _get_registry_entry(slug: str, kind: str): + """Look up a RegistryEntry for the given slug and kind ('benchmark' or 'agent').""" + from ...registry import AGENTS, BENCHMARKS + + registry = BENCHMARKS if kind == "benchmark" else AGENTS + entry = registry.get(slug) + if entry is None: + raise click.ClickException(f"Unknown {kind} slug '{slug}'") + return entry + + +@click.command("install") +@click.option("--benchmark", "benchmark", default=None, help="Benchmark slug name to install.") +@click.option("--agent", "agent", default=None, help="Agent slug name to install.") +@click.option("--force", is_flag=True, help="Force reinstall even if already installed.") +@click.option("--docker", is_flag=True, help="Build a Docker image for the environment.") +@click.option("--local", is_flag=True, help="Install into the current Python (no isolation).") +def install_cmd(benchmark: str | None, agent: str | None, force: bool, docker: bool, local: bool) -> None: + """Install a benchmark or agent environment. + + By default, creates an isolated Python venv with all dependencies. + Use --docker to build a Docker image, or --local to install into + the current Python (no isolation). + """ + from ....environment import EnvType + from ....environment.instance import get_manager + + if benchmark is not None and agent is not None: + raise click.UsageError("Specify either --benchmark or --agent, not both.") + if benchmark is None and agent is None: + raise click.UsageError("Specify either --benchmark or --agent.") + + if docker and local: + raise click.UsageError("Specify either --docker or --local, not both.") + + if docker: + env_type = EnvType.DOCKER + elif local: + env_type = EnvType.LOCAL + else: + env_type = EnvType.VENV + + mgr = get_manager() + + try: + if benchmark is not None: + entry = _get_registry_entry(benchmark, "benchmark") + name = f"benchmarks/{benchmark}" + else: + entry = _get_registry_entry(agent, "agent") + name = f"agents/{agent}" + + kwargs: dict = {"env_type": env_type, "module_path": entry.module, "force": force} + if env_type in (EnvType.VENV, EnvType.DOCKER): + from ....environment.helpers import get_exgentic_install_target + + project_root, packages = get_exgentic_install_target() + if project_root is not None: + kwargs["project_root"] = project_root + if packages: + kwargs["packages"] = packages + mgr.install(name, **kwargs) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + + +@click.command("uninstall") +@click.option("--benchmark", "benchmark", default=None, help="Benchmark slug name to uninstall.") +@click.option("--agent", "agent", default=None, help="Agent slug name to uninstall.") +@click.option("--docker", is_flag=True, help="Uninstall Docker environment only.") +@click.option("--local", is_flag=True, help="Uninstall local environment only.") +def uninstall_cmd(benchmark: str | None, agent: str | None, docker: bool, local: bool) -> None: + """Uninstall a benchmark or agent environment. + + Without flags, removes all environment types for the given name. + """ + from ....environment import EnvType + from ....environment.instance import get_manager + + if benchmark is not None and agent is not None: + raise click.UsageError("Specify either --benchmark or --agent, not both.") + if benchmark is None and agent is None: + raise click.UsageError("Specify either --benchmark or --agent.") + + if docker and local: + raise click.UsageError("Specify either --docker or --local, not both.") + + if docker: + env_type = EnvType.DOCKER + elif local: + env_type = EnvType.LOCAL + else: + env_type = None + + mgr = get_manager() + + try: + if benchmark is not None: + name = f"benchmarks/{benchmark}" + else: + name = f"agents/{agent}" + + if env_type is not None: + mgr.uninstall(name, env_type=env_type) + else: + mgr.uninstall(name) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + + +@click.command("setup") +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +@click.option("--benchmark", "benchmark", default=None, help="Benchmark slug name to set up.") +@click.option("--agent", "agent", default=None, help="Agent slug name to set up.") +@click.option( + "--force", + is_flag=True, + help="Force reinstall even if already installed.", +) +def setup_cmd(debug: bool, benchmark: str | None, agent: str | None, force: bool) -> None: + """[Deprecated] Use 'exgentic install' instead.""" + apply_debug_mode(debug) + click.echo( + "WARNING: 'exgentic setup' is deprecated. Use 'exgentic install' instead.", + err=True, + ) + + # Delegate to install logic + from ....environment import EnvType + from ....environment.instance import get_manager + + if benchmark is not None and agent is not None: + raise click.UsageError("Specify either --benchmark or --agent, not both.") + if benchmark is None and agent is None: + raise click.UsageError("Specify either --benchmark or --agent.") + + mgr = get_manager() + + try: + if benchmark is not None: + entry = _get_registry_entry(benchmark, "benchmark") + name = f"benchmarks/{benchmark}" + else: + entry = _get_registry_entry(agent, "agent") + name = f"agents/{agent}" + + mgr.install(name, env_type=EnvType.LOCAL, module_path=entry.module, force=force) + except Exception as exc: + raise click.ClickException(str(exc)) from exc + + +__all__ = ["install_cmd", "setup_cmd", "uninstall_cmd"] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/main.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/main.py new file mode 100644 index 00000000..6e827315 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/main.py @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import rich_click as click + +from ... import __version__ +from .commands.analyze import analyse_cmd +from .commands.batch import batch_cmd +from .commands.compare import compare_cmd +from .commands.dashboard import dashboard_cmd +from .commands.evaluate import evaluate_cmd +from .commands.listing import list_cmd +from .commands.run_info import preview_cmd, results_cmd, status_cmd +from .commands.serve import serve_cmd +from .commands.setup import install_cmd, setup_cmd, uninstall_cmd +from .options import apply_debug_mode +from .render import print_banner, should_print_banner + +click.rich_click.text_markup = False +click.rich_click.show_arguments = True +click.rich_click.options_table_column_types = [ + "required", + "opt_short", + "opt_long", + "metavar", + "help", +] +click.rich_click.show_envvar = True +click.rich_click.COMMAND_GROUPS = { + "exgentic": [ + { + "name": "Run", + "commands": [ + "evaluate", + "batch", + "status", + "preview", + "results", + ], + }, + { + "name": "Analyze", + "commands": ["compare", "analyse"], + }, + { + "name": "Discover", + "commands": ["list", "install", "uninstall", "setup"], + }, + { + "name": "Explore", + "commands": ["dashboard"], + }, + { + "name": "Infrastructure", + "commands": ["serve"], + }, + ] +} + + +def _version_callback(ctx: click.Context, param: click.Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + click.echo(f"exgentic {__version__}") + ctx.exit() + + +@click.group() +@click.option( + "--version", + "-V", + is_flag=True, + callback=_version_callback, + expose_value=False, + is_eager=True, + help="Show version and exit.", +) +@click.option( + "--debug", + is_flag=True, + help="Enable debug mode (sets settings.debug=true and log level to DEBUG)", +) +def cli(debug: bool) -> None: + """Exgentic CLI.""" + apply_debug_mode(debug) + + +cli.add_command(evaluate_cmd) +cli.add_command(batch_cmd) +cli.add_command(analyse_cmd) +cli.add_command(status_cmd) +cli.add_command(preview_cmd) +cli.add_command(results_cmd) +cli.add_command(compare_cmd) +cli.add_command(list_cmd) +cli.add_command(dashboard_cmd) +cli.add_command(setup_cmd) +cli.add_command(install_cmd) +cli.add_command(uninstall_cmd) +cli.add_command(serve_cmd) + + +def main() -> None: + if should_print_banner(): + print_banner() + cli() + + +__all__ = [ + "batch_cmd", + "cli", + "compare_cmd", + "dashboard_cmd", + "evaluate_cmd", + "install_cmd", + "list_cmd", + "main", + "preview_cmd", + "results_cmd", + "serve_cmd", + "setup_cmd", + "status_cmd", + "uninstall_cmd", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/options.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/options.py new file mode 100644 index 00000000..aa64f265 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/options.py @@ -0,0 +1,474 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import traceback +from typing import Any + +import rich_click as click + +from ...core.types import ModelSettings, RunConfig +from ...utils.settings import ExgenticSettings, get_settings + +DEFAULT_OUTPUT_DIR = "./outputs" + + +def _api(): + from ..lib import api + + return api + + +def apply_debug_mode(debug: bool) -> None: + if not debug: + return + settings = get_settings() + settings.debug = True + settings.log_level = "DEBUG" + + +def _should_show_traceback() -> bool: + return bool(get_settings().debug) + + +def _format_exception_for_cli(exc: Exception) -> str: + parts = [str(exc)] + remote_tb = getattr(exc, "__remote_traceback__", None) + if remote_tb: + parts.append(f"Remote traceback:\n{str(remote_tb).rstrip()}") + local_tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + if local_tb.strip(): + parts.append(f"Local traceback:\n{local_tb.rstrip()}") + stdout = getattr(exc, "stdout", None) + if stdout: + parts.append(f"stdout:\n{str(stdout).rstrip()}") + stderr = getattr(exc, "stderr", None) + if stderr: + parts.append(f"stderr:\n{str(stderr).rstrip()}") + return "\n\n".join(parts) + + +def _load_json_arg(raw: str | None) -> dict[str, Any]: + if not raw: + return {} + if raw.startswith("@"): + path = raw[1:] + with open(path, encoding="utf-8") as f: + return json.load(f) + return json.loads(raw) + + +def _parse_kv_list(values: tuple[str, ...]) -> dict[str, Any]: + payload: dict[str, Any] = {} + for item in values: + if "=" not in item: + raise click.ClickException(f"Expected key=value, got '{item}'") + key, raw = item.split("=", 1) + try: + value = json.loads(raw) + except Exception: + value = raw + payload[key] = value + return payload + + +def _set_nested(target: dict[str, Any], path: list[str], value: Any) -> None: + if not path or any(not part for part in path): + raise click.ClickException("Invalid --set key (empty path).") + cur = target + for part in path[:-1]: + if part in cur and not isinstance(cur[part], dict): + raise click.ClickException(f"Conflicting --set path at '{part}': not a mapping.") + cur = cur.setdefault(part, {}) + leaf = path[-1] + if leaf in cur and cur[leaf] != value: + raise click.ClickException(f"Conflicting --set value for '{'.'.join(path)}'.") + cur[leaf] = value + + +def _parse_set_list(values: tuple[str, ...]) -> list[tuple[str, list[str], Any]]: + items: list[tuple[str, list[str], Any]] = [] + for item in values: + if "=" not in item: + raise click.ClickException(f"Expected key=value, got '{item}'") + key, raw = item.split("=", 1) + try: + value = json.loads(raw) + except Exception: + value = raw + if key.startswith("benchmark."): + path = key.split(".")[1:] + items.append(("benchmark", path, value)) + elif key.startswith("agent.model."): + path = ["model_settings", *key.split(".")[2:]] + items.append(("agent", path, value)) + elif key.startswith("agent."): + path = key.split(".")[1:] + items.append(("agent", path, value)) + elif key.startswith("settings."): + path = key.split(".")[1:] + items.append(("settings", path, value)) + else: + raise click.ClickException( + "Invalid --set key. Use benchmark.=..., agent.=..., " "or settings.=..." + ) + return items + + +def _validate_set_keys_for_benchmark(benchmark: str, items: list[tuple[str, list[str], Any]]) -> None: + try: + info = _api().get_benchmark_info(benchmark) + except ImportError: + # Benchmark has uninstalled deps (e.g. running with runner=docker + # before setup on the host). Skip --set validation; the container + # will catch real errors at runtime. + return + except Exception as exc: + raise click.ClickException(str(exc)) from exc + forbidden = {"num_tasks", "subset"} + subset_arg = info.get("subset_arg") + if subset_arg: + forbidden.add(subset_arg) + allowed = set(info.get("kwargs") or []) + allow_any = "**kwargs" in allowed + for group, path, _ in items: + if group != "benchmark" or not path: + continue + if path[0] in forbidden: + raise click.ClickException(f"Use --subset/--num-tasks instead of --set benchmark.{path[0]}.") + if not allow_any and path[0] not in allowed: + raise click.ClickException( + f"Unknown benchmark override '{path[0]}'. " f"Available: {', '.join(sorted(allowed))}" + ) + + +def _validate_set_keys_for_agent(agent: str, items: list[tuple[str, list[str], Any]]) -> None: + try: + info = _api().get_agent_info(agent) + except ImportError: + return + except Exception as exc: + raise click.ClickException(str(exc)) from exc + allowed = set(info.get("kwargs") or []) + allow_any = "**kwargs" in allowed + model_fields = set(ModelSettings.model_fields.keys()) + for group, path, _ in items: + if group != "agent" or not path: + continue + if path[0] == "model_settings": + if len(path) < 2 or path[1] not in model_fields: + raise click.ClickException( + f"Unknown agent model override '{'.'.join(path)}'. " f"Available: {', '.join(sorted(model_fields))}" + ) + continue + if not allow_any and path[0] not in allowed: + raise click.ClickException( + f"Unknown agent override '{path[0]}'. " f"Available: {', '.join(sorted(allowed))}" + ) + + +def _validate_set_keys_for_settings( + items: list[tuple[str, list[str], Any]], +) -> None: + allowed = set(ExgenticSettings.model_fields.keys()) + for group, path, _ in items: + if group != "settings" or not path: + continue + if len(path) != 1 or path[0] not in allowed: + raise click.ClickException( + f"Unknown settings override '{'.'.join(path)}'. " f"Available: {', '.join(sorted(allowed))}" + ) + + +def _apply_settings_overrides(items: list[tuple[str, list[str], Any]]) -> None: + settings = get_settings() + for group, path, value in items: + if group != "settings" or not path: + continue + setattr(settings, path[0], value) + + +def _apply_options(target, specs): + for args, kwargs in reversed(specs): + target = click.option(*args, **kwargs)(target) + return target + + +def _run_option_specs( + *, + required: bool, + include_overwrite: bool, + include_max_workers: bool, +): + specs = [ + (("--benchmark",), {"required": required, "help": "Benchmark slug_name"}), + (("--agent",), {"required": required, "help": "Agent slug_name"}), + (("--agent-json",), {"help": "Agent kwargs JSON (or @file)"}), + (("--agent-arg",), {"multiple": True, "help": "Agent kwarg key=value"}), + ( + ("--set", "set_values"), + { + "multiple": True, + "help": "Set benchmark.*, agent.*, or settings.* values", + }, + ), + (("--subset",), {"help": "Benchmark subset name"}), + (("--task", "tasks"), {"multiple": True, "help": "Task to run (repeatable)"}), + (("--num-tasks",), {"type": int, "help": "Number of tasks to run"}), + (("--max-steps",), {"type": int, "help": "Max steps per session"}), + (("--max-actions",), {"type": int, "help": "Max actions per session"}), + (("--model",), {"help": "Agent model (must be supported by the agent)"}), + ( + ("--debug",), + { + "is_flag": True, + "help": "Enable debug mode (sets settings.debug=true and log level to DEBUG)", + }, + ), + ( + ("--log-level",), + { + "type": click.Choice( + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + case_sensitive=False, + ), + "help": "Override EXGENTIC_LOG_LEVEL for this run", + }, + ), + (("--output-dir",), {"default": DEFAULT_OUTPUT_DIR, "show_default": True}), + ( + ("--cache-dir",), + {"help": "Base cache directory (overrides EXGENTIC_CACHE_DIR)"}, + ), + (("--run-id",), {"help": "Run id (overrides context env var)"}), + ] + if include_overwrite: + specs.append( + ( + ("--overwrite",), + { + "is_flag": True, + "help": "Overwrite existing sessions instead of skipping them", + }, + ) + ) + if include_max_workers: + specs.append( + ( + ("--max-workers",), + { + "type": int, + "help": "Parallel workers (>=2 runs sessions in parallel)", + }, + ) + ) + return specs + + +def add_run_options( + func=None, + *, + required: bool = True, + include_overwrite: bool = True, + include_max_workers: bool = True, +): + def decorator(target): + specs = _run_option_specs( + required=required, + include_overwrite=include_overwrite, + include_max_workers=include_max_workers, + ) + return _apply_options(target, specs) + + if func is None: + return decorator + return decorator(func) + + +def has_run_options( + *, + benchmark: str | None, + agent: str | None, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str | None, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, + max_steps: int | None, + max_actions: int | None, +) -> bool: + if benchmark or agent or agent_json or subset or num_tasks or model or log_level: + return True + if agent_arg or set_values or tasks: + return True + if overwrite or run_id or max_workers is not None: + return True + if max_steps is not None or max_actions is not None: + return True + if cache_dir: + return True + if output_dir and output_dir != DEFAULT_OUTPUT_DIR: + return True + return False + + +def build_run_config( + *, + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + model: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, + max_steps: int | None, + max_actions: int | None, + overwrite: bool, +) -> RunConfig: + bench_kwargs: dict[str, Any] = {} + agent_kwargs = _load_json_arg(agent_json) + agent_kwargs.update(_parse_kv_list(agent_arg)) + set_items = _parse_set_list(set_values) + _validate_set_keys_for_settings(set_items) + _apply_settings_overrides(set_items) + _validate_set_keys_for_benchmark(benchmark, set_items) + _validate_set_keys_for_agent(agent, set_items) + for group, path, value in set_items: + if group == "benchmark": + _set_nested(bench_kwargs, path, value) + elif group == "agent": + _set_nested(agent_kwargs, path, value) + return RunConfig( + benchmark=benchmark, + agent=agent, + subset=subset, + task_ids=list(tasks) if tasks else None, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=(RunConfig.model_fields["max_steps"].default if max_steps is None else max_steps), + max_actions=(RunConfig.model_fields["max_actions"].default if max_actions is None else max_actions), + overwrite_sessions=overwrite, + benchmark_kwargs=bench_kwargs, + agent_kwargs=agent_kwargs, + ) + + +def run_with( + run_func, + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + model: str | None, + debug: bool, + overwrite: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_workers: int | None, + max_steps: int | None, + max_actions: int | None, +) -> Any: + """Run a benchmark with an agent by slug name.""" + if log_level: + settings = get_settings() + level = log_level.upper() + settings.log_level = level + settings.debug = level == "DEBUG" + apply_debug_mode(debug) + try: + config = build_run_config( + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + model=model, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite=overwrite, + ) + return run_func(config) + except Exception as exc: + if _should_show_traceback(): + raise click.ClickException(_format_exception_for_cli(exc)) from exc + raise click.ClickException(str(exc)) from exc + + +def run_query( + *, + run_func, + render_func, + output_format: str, + benchmark: str, + agent: str, + agent_json: str | None, + agent_arg: tuple[str, ...], + set_values: tuple[str, ...], + subset: str | None, + tasks: tuple[str, ...], + num_tasks: int | None, + model: str | None, + debug: bool, + log_level: str | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + max_steps: int | None, + max_actions: int | None, +) -> None: + result = run_with( + run_func, + benchmark=benchmark, + agent=agent, + agent_json=agent_json, + agent_arg=agent_arg, + set_values=set_values, + subset=subset, + tasks=tasks, + num_tasks=num_tasks, + model=model, + debug=debug, + overwrite=False, + log_level=log_level, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + max_workers=None, + max_steps=max_steps, + max_actions=max_actions, + ) + render_func(result, output_format) diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/render.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/render.py new file mode 100644 index 00000000..96bad505 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/render.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import sys +from typing import Any + +from rich import box +from rich.align import Align +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +CONSOLE = Console() + + +def print_json(data: Any) -> None: + CONSOLE.print_json(json.dumps(data, ensure_ascii=False, indent=2)) + + +def render_list(items: list[str], output_format: str, *, title: str = "Items") -> None: + if output_format == "json": + print_json(items) + return + table = Table(title=title, box=box.SIMPLE, show_header=False) + table.add_column("Item") + if not items: + table.add_row("[dim]none[/dim]") + else: + for item in items: + table.add_row(str(item)) + CONSOLE.print(table) + + +def render_named_list( + items: list[dict[str, Any]], + output_format: str, + *, + fields: tuple[str, str] = ("slug_name", "display_name"), + title: str = "Items", +) -> None: + if output_format == "json": + print_json(items) + return + + # Check if items have installation info + has_install_info = items and "installed" in items[0] + + table = Table(title=title, box=box.SIMPLE, show_header=True, header_style="bold magenta") + table.add_column("Slug") + table.add_column("Name") + if has_install_info: + table.add_column("Installed", justify="center") + table.add_column("Installed At") + + if not items: + if has_install_info: + table.add_row("[dim]none[/dim]", "[dim]none[/dim]", "[dim]-[/dim]", "[dim]-[/dim]") + else: + table.add_row("[dim]none[/dim]", "[dim]none[/dim]") + else: + for item in items: + slug = str(item[fields[0]]) + name = str(item[fields[1]]) + if has_install_info: + installed = item.get("installed", False) + installed_at = item.get("installed_at", "") + status = "[green]✓[/green]" if installed else "[dim]-[/dim]" + # Format the timestamp to be more readable + if installed_at: + try: + from datetime import datetime + + dt = datetime.fromisoformat(installed_at.replace("Z", "+00:00")) + installed_at = dt.strftime("%Y-%m-%d %H:%M UTC") + except Exception: + pass + table.add_row(slug, name, status, installed_at if installed_at else "[dim]-[/dim]") + else: + table.add_row(slug, name) + CONSOLE.print(table) + + +def render_model(obj: Any, output_format: str) -> None: + if output_format == "json": + print_json(obj.model_dump()) + return + CONSOLE.print(str(obj)) + + +def render_run_status(status: Any, output_format: str) -> None: + if output_format == "json": + render_model(status, output_format) + return + meta = Table.grid(padding=(0, 2)) + meta.add_column(style="bold cyan") + meta.add_column() + meta.add_row("Run", str(status.run_id)) + meta.add_row("Results", "yes" if status.results_exists else "no") + meta.add_row("Benchmark Results", "yes" if status.benchmark_results_exists else "no") + CONSOLE.print(Panel(meta, title="Run Status", border_style="cyan")) + + counts = Table( + title="Sessions", + box=box.SIMPLE, + show_header=True, + header_style="bold magenta", + ) + counts.add_column("Total", justify="right") + counts.add_column("Completed", justify="right") + counts.add_column("Running", justify="right") + counts.add_column("Incomplete", justify="right") + counts.add_column("Missing", justify="right") + counts.add_row( + str(status.total_tasks), + str(status.completed_sessions), + str(status.running_sessions), + str(status.incomplete_sessions), + str(status.missing_sessions), + ) + CONSOLE.print(counts) + + +def render_batch_status(rows: list[dict[str, str]]) -> None: + total_configs = len(rows) + done_configs = sum(1 for row in rows if row.get("ready") == row.get("finished")) + total_sessions = 0 + total_ready = 0 + for row in rows: + ready = row.get("ready", "-") + if isinstance(ready, str) and "/" in ready: + try: + done_str, total_str = ready.split("/", 1) + total_sessions += int(total_str) + total_ready += int(done_str) + except Exception: + continue + + summary = Table.grid(padding=(0, 2)) + summary.add_column(style="bold cyan") + summary.add_column() + summary.add_row("Configs", f"{total_configs} total") + summary.add_row("Fully Done", f"{done_configs} total") + summary.add_row("Sessions", f"{total_ready}/{total_sessions}") + CONSOLE.print(Panel(summary, title="Batch Summary", border_style="cyan")) + + table = Table( + title="Batch Status", + box=box.SIMPLE, + show_header=True, + header_style="bold magenta", + ) + table.add_column("#", justify="right", no_wrap=True) + table.add_column("Config", overflow="crop", max_width=30) + table.add_column("Run", no_wrap=True, min_width=8) + table.add_column("Benchmark", no_wrap=True) + table.add_column("Agent", no_wrap=True) + table.add_column("Subset", no_wrap=True) + table.add_column("Models", overflow="crop", max_width=24) + table.add_column("Ready", justify="right") + table.add_column("Aggregated", justify="right") + table.add_column("Finished", justify="right") + table.add_column("Errors", justify="right") + table.add_column("Score", justify="right") + table.add_column("Cost", justify="right") + + for row in rows: + table.add_row( + row["#"], + row["config"], + row["run_id"], + row["benchmark"], + row["agent"], + row.get("subset", "-"), + row["models"], + row["ready"], + row["aggregated"], + row["finished"], + row["errors"], + row["score"], + row["cost"], + ) + + CONSOLE.print(table) + + +def render_run_plan(plan: Any, output_format: str) -> None: + if output_format == "json": + render_model(plan, output_format) + return + meta = Table.grid(padding=(0, 2)) + meta.add_column(style="bold cyan") + meta.add_column() + meta.add_row("Run", str(plan.run_config.run_id)) + meta.add_row("Overwrite", "yes" if plan.overwrite_sessions else "no") + CONSOLE.print(Panel(meta, title="Run Plan", border_style="cyan")) + + counts = Table( + title="Sessions", + box=box.SIMPLE, + show_header=True, + header_style="bold magenta", + ) + counts.add_column("Total", justify="right") + counts.add_column("To Run", justify="right") + counts.add_column("Reuse", justify="right") + counts.add_column("Running", justify="right") + counts.add_column("Missing", justify="right") + counts.add_column("Incomplete", justify="right") + total_sessions = len(plan.to_run) + len(plan.reuse) + len(plan.running) + len(plan.missing) + len(plan.incomplete) + counts.add_row( + str(total_sessions), + str(len(plan.to_run)), + str(len(plan.reuse)), + str(len(plan.running)), + str(len(plan.missing)), + str(len(plan.incomplete)), + ) + CONSOLE.print(counts) + + +def render_run_results(results: Any, output_format: str) -> None: + if output_format == "json": + render_model(results, output_format) + return + meta = Table.grid(padding=(0, 2)) + meta.add_column(style="bold cyan") + meta.add_column() + meta.add_row("Benchmark", str(results.benchmark_name)) + meta.add_row("Agent", str(results.agent_name)) + CONSOLE.print(Panel(meta, title="Run Results", border_style="cyan")) + + summary = Table( + title="Summary", + box=box.SIMPLE, + show_header=True, + header_style="bold magenta", + ) + summary.add_column("Sessions", justify="right") + summary.add_column("Successes", justify="right") + summary.add_column("Final Score", justify="right") + summary.add_column("Avg Score", justify="right") + summary.add_row( + str(results.total_sessions), + str(results.successful_sessions), + str(results.benchmark_score), + str(results.average_score), + ) + CONSOLE.print(summary) + + +def should_print_banner() -> bool: + args = sys.argv[1:] + if not args: + return True + return any(arg in ("-h", "--help") for arg in args) + + +def print_banner() -> None: + title = Align.center("[bold magenta]EXGENTIC[/bold magenta]\n" "[dim]General Agent Evaluation[/dim]") + CONSOLE.print(Panel(title, border_style="magenta", padding=(1, 8))) diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/run.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/run.py new file mode 100644 index 00000000..0b9c4e27 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/cli/run.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from .main import main + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/app.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/app.py new file mode 100644 index 00000000..88890f8d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/app.py @@ -0,0 +1,243 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from pathlib import Path + +from nicegui import ui + +from .views import ( + RunState, + build_history_tab, + build_leaderboard_tab, + build_run_tab, + refresh_ui, +) + +ASSETS_DIR = Path(__file__).resolve().parents[3] / "assets" + + +def create_ui() -> None: + state = RunState() + + ui.page_title("Exgentic Dashboard") + ui.add_head_html( + """ + + + + + """ + ) + dark = ui.dark_mode() + dark.disable() + + with ui.row().classes("w-full items-center justify-between"): + logo_path = ASSETS_DIR.parent.parent / "misc" / "assets" / "exgentic_banner_black_no_background.png" + if logo_path.is_file(): + ui.image(logo_path).classes("app-logo").props("fit=contain") + with ui.tabs() as main_tabs: + run_tab = ui.tab("Run") + leaderboard_tab = ui.tab("Leaderboard") + history_tab = ui.tab("History") + + with ui.tab_panels(main_tabs, value=run_tab).classes("w-full"): + with ui.tab_panel(run_tab): + run_views = build_run_tab(state) + with ui.tab_panel(leaderboard_tab): + build_leaderboard_tab(state) + with ui.tab_panel(history_tab): + build_history_tab(state) + + ui.timer(0.2, lambda: refresh_ui(state, run_views)) + + +def main() -> None: + ui.run(root=create_ui, reload=False, dark=False) + + +if __name__ == "__main__": + main() diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/__init__.py new file mode 100644 index 00000000..7734a5cd --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/__init__.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from .runtime import ( + build_history_tab, + build_leaderboard_tab, + build_run_tab, + refresh_ui, +) +from .state import RunState, RunViews + +__all__ = [ + "RunState", + "RunViews", + "build_run_tab", + "build_leaderboard_tab", + "build_history_tab", + "refresh_ui", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/data.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/data.py new file mode 100644 index 00000000..8494274c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/data.py @@ -0,0 +1,529 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, Optional + +from ....utils.paths import RunPaths +from ....utils.settings import get_settings +from ...lib.api import list_agents, list_benchmarks +from .state import RunContext +from .status import _status_from_outcome + + +def get_display_mappings() -> tuple[dict[str, str], dict[str, str]]: + bench_label_to_key: dict[str, str] = {} + for item in list_benchmarks(): + bench_label_to_key[str(item["display_name"])] = item["slug_name"] + + agent_label_to_key: dict[str, str] = {} + for item in list_agents(): + agent_label_to_key[str(item["display_name"])] = item["slug_name"] + + return bench_label_to_key, agent_label_to_key + + +def _build_overview_secondary_metrics( + sessions: dict, + results: Optional[dict], +) -> list[tuple[str, Any]]: + total_sessions = None + successful_sessions = None + percent_successful = None + percent_finished = None + percent_finished_unsuccessful = None + percent_unfinished = None + percent_error = None + avg_score = None + avg_steps = None + avg_agent_cost = None + avg_benchmark_cost = None + total_run_cost = None + if isinstance(results, dict): + total_sessions = results.get("total_sessions") + successful_sessions = results.get("successful_sessions") + percent_successful = results.get("percent_successful") + percent_finished = results.get("percent_finished") + percent_finished_unsuccessful = results.get("percent_finished_unsuccessful") + percent_unfinished = results.get("percent_unfinished") + percent_error = results.get("percent_error") + avg_score = results.get("average_score") + avg_steps = results.get("average_steps") + avg_agent_cost = results.get("average_agent_cost") + avg_benchmark_cost = results.get("average_benchmark_cost") + total_run_cost = results.get("total_run_cost") + + if total_sessions is None: + total_sessions = len(sessions) + if successful_sessions is None: + successful_sessions = sum(1 for s in sessions.values() if s.get("status") == "success") + + if avg_steps is None: + steps = [s.get("steps") for s in sessions.values() if s.get("steps") is not None] + if steps: + avg_steps = sum(float(x) for x in steps) / len(steps) + if avg_score is None: + scores = [s.get("score") for s in sessions.values() if s.get("score") is not None] + if scores: + avg_score = sum(float(x) for x in scores) / len(scores) + if avg_agent_cost is None: + costs = [s.get("agent_cost") for s in sessions.values() if s.get("agent_cost") is not None] + if costs: + avg_agent_cost = sum(float(x) for x in costs) / len(costs) + if avg_benchmark_cost is None: + costs = [s.get("benchmark_cost") for s in sessions.values() if s.get("benchmark_cost") is not None] + if costs: + avg_benchmark_cost = sum(float(x) for x in costs) / len(costs) + if total_run_cost is None: + total_run_cost = None + had_cost = False + for s in sessions.values(): + agent_cost = s.get("agent_cost") + benchmark_cost = s.get("benchmark_cost") + if agent_cost is not None: + total_run_cost = (total_run_cost or 0.0) + float(agent_cost) + had_cost = True + if benchmark_cost is not None: + total_run_cost = (total_run_cost or 0.0) + float(benchmark_cost) + had_cost = True + if not had_cost: + total_run_cost = None + + completed_sessions = [s for s in sessions.values() if s.get("status") != "running"] + completed_total = len(completed_sessions) + + success_rate = percent_successful + if success_rate is None and completed_total > 0: + success_rate = sum(1 for s in completed_sessions if s.get("status") == "success") / completed_total + + finished_rate = percent_finished + if finished_rate is None and completed_total > 0: + finished_rate = ( + sum(1 for s in completed_sessions if s.get("status") in ("success", "unsuccessful")) / completed_total + ) + + finished_unsuccessful_rate = percent_finished_unsuccessful + if finished_unsuccessful_rate is None and completed_total > 0: + finished_unsuccessful_rate = ( + sum(1 for s in completed_sessions if s.get("status") == "unsuccessful") / completed_total + ) + + unfinished_rate = percent_unfinished + if unfinished_rate is None and completed_total > 0: + unfinished_rate = sum(1 for s in completed_sessions if s.get("status") == "unfinished") / completed_total + + error_rate = percent_error + if error_rate is None and completed_total > 0: + error_rate = ( + sum( + 1 + for s in completed_sessions + if s.get("status") in ("error", "agent error", "benchmark error", "cancelled") + ) + / completed_total + ) + + return [ + ("Avg Score", avg_score), + ("Avg Steps", avg_steps), + ("Avg Agent Cost", avg_agent_cost), + ("Avg Benchmark Cost", avg_benchmark_cost), + ("Total Run Cost", total_run_cost), + ] + + +def load_leaderboard_data(output_dir: str) -> list[dict]: + rows = [] + try: + for run_id in os.listdir(output_dir): + results_path = RunPaths(run_id=run_id, output_dir=output_dir).results + if not os.path.isfile(results_path): + continue + try: + with open(results_path, encoding="utf-8-sig") as f: + r = json.load(f) + rows.append( + { + "Agent": r.get("agent_name", "unknown"), + "Model": r.get("model_name", "unknown"), + "Benchmark": r.get("benchmark_name", "unknown"), + "Subset": r.get("subset_name", "unknown"), + "Num Tasks": r.get("total_sessions", 0), + "Final Score": r.get("average_score"), + "Total Run Cost": r.get("total_run_cost"), + "Avg Agent Cost": r.get("average_agent_cost"), + "run_id": run_id, + } + ) + except (OSError, json.JSONDecodeError): + continue + except FileNotFoundError: + pass + return rows + + +def load_run_results(results_path: str) -> Optional[dict]: + try: + with open(results_path, encoding="utf-8-sig") as f: + return json.load(f) + except (OSError, json.JSONDecodeError): + return None + + +def load_run_config(run_id: str, *, output_dir: Optional[str] = None) -> Optional[dict]: + resolved_output = output_dir or get_settings().output_dir + run_paths = RunPaths(run_id=run_id, output_dir=resolved_output) + config_path = run_paths.config + config_data = load_session_file(str(config_path), "json") + if config_data is not None: + return config_data + legacy_path = run_paths.root / "config.json" + if legacy_path == config_path: + return None + return load_session_file(str(legacy_path), "json") + + +def _short_model_name(name: Any) -> str: + text = str(name) + if "/" in text: + return text.split("/")[-1] + return text + + +def _resolve_run_meta( + results: Optional[dict], + config_data: Optional[dict], + *, + fallback_benchmark: Optional[str] = None, + fallback_agent: Optional[str] = None, +) -> dict[str, Any]: + benchmark = None + agent = None + models: list[str] = [] + + if isinstance(results, dict): + benchmark = results.get("benchmark_name") or benchmark + agent = results.get("agent_name") or agent + model_names = results.get("model_names") or [] + if isinstance(model_names, list): + models = [_short_model_name(m) for m in model_names if m] + if not models and results.get("model_name"): + models = [_short_model_name(results.get("model_name"))] + + if isinstance(config_data, dict): + bench_cfg = config_data.get("benchmark") or {} + agent_cfg = config_data.get("agent") or {} + if isinstance(bench_cfg, str): + if not benchmark: + benchmark = bench_cfg + bench_cfg = {} + if isinstance(agent_cfg, str): + if not agent: + agent = agent_cfg + agent_cfg = {} + if not benchmark: + benchmark = bench_cfg.get("display_name") or bench_cfg.get("slug_name") or bench_cfg.get("class") + if not agent: + agent = agent_cfg.get("display_name") or agent_cfg.get("slug_name") or agent_cfg.get("class") + if not models: + model_names = agent_cfg.get("model_names") + if isinstance(model_names, list): + models = [_short_model_name(m) for m in model_names if m] + if not models and agent_cfg.get("model_name"): + models = [_short_model_name(agent_cfg.get("model_name"))] + + if not benchmark: + benchmark = fallback_benchmark + if not agent: + agent = fallback_agent + + if models: + models = list(dict.fromkeys(models)) + + return { + "benchmark": benchmark or "-", + "agent": agent or "-", + "models": models, + } + + +def _resolve_planned_sessions( + results: Optional[dict], + config_data: Optional[dict], + fallback: Optional[int], +) -> Optional[int]: + if isinstance(results, dict) and results.get("planned_sessions") is not None: + return results.get("planned_sessions") + if isinstance(config_data, dict): + if config_data.get("planned_sessions") is not None: + return config_data.get("planned_sessions") + if config_data.get("num_tasks") is not None: + return config_data.get("num_tasks") + + bench_cfg = config_data.get("benchmark") + if isinstance(bench_cfg, dict) and bench_cfg.get("planned_sessions") is not None: + return bench_cfg.get("planned_sessions") + + run_cfg = config_data.get("run") + if isinstance(run_cfg, dict): + if run_cfg.get("planned_sessions") is not None: + return run_cfg.get("planned_sessions") + if run_cfg.get("num_tasks") is not None: + return run_cfg.get("num_tasks") + return fallback + + +def _resolve_total_workers( + results: Optional[dict], + config_data: Optional[dict], + fallback: Optional[int], +) -> Optional[int]: + if isinstance(results, dict) and results.get("max_workers") is not None: + return results.get("max_workers") + if isinstance(config_data, dict): + if config_data.get("max_workers") is not None: + return config_data.get("max_workers") + + run_cfg = config_data.get("run") + if isinstance(run_cfg, dict) and run_cfg.get("max_workers") is not None: + return run_cfg.get("max_workers") + return fallback + + +def _load_run_context( + run_id: Optional[str], + *, + fallback_benchmark: Optional[str] = None, + fallback_agent: Optional[str] = None, + planned_fallback: Optional[int] = None, + workers_fallback: Optional[int] = None, + output_dir: Optional[str] = None, +) -> RunContext: + results = None + config_data = None + if run_id: + resolved_output = output_dir or get_settings().output_dir + results = load_run_results(str(RunPaths(run_id=run_id, output_dir=resolved_output).results)) + config_data = load_run_config(run_id, output_dir=resolved_output) + run_meta = _resolve_run_meta( + results, + config_data, + fallback_benchmark=fallback_benchmark, + fallback_agent=fallback_agent, + ) + planned_sessions = _resolve_planned_sessions(results, config_data, planned_fallback) + total_workers = _resolve_total_workers(results, config_data, workers_fallback) + return RunContext( + results=results, + config=config_data, + run_meta=run_meta, + planned_sessions=planned_sessions, + total_workers=total_workers, + ) + + +def _resolve_tab_label( + value: Any, + tab_by_name: dict[str, Any], + default: str, +) -> str: + if isinstance(value, str) and value in tab_by_name: + return value + for name, tab in tab_by_name.items(): + if value is tab: + return name + return default + + +def _open_session_from_row( + state, + scope: str, + row: dict, + refresh, +) -> None: + session_id = row.get("session") + if not session_id: + return + if scope == "history": + state.selected_history_session = session_id + else: + state.selected_session = session_id + state.active_tabs[scope] = "Sessions" + tabs = state.tabs_controls.get(scope) + tab_by_name = state.tabs_by_scope.get(scope) or {} + target = tab_by_name.get("Sessions") + if tabs is not None and target is not None: + tabs.value = target + refresh() + + +def _build_session_rows(sessions: dict) -> list[dict]: + return [ + { + "session": sid, + "status": data.get("status", ""), + "steps": data.get("steps", 0), + "score": data.get("score"), + } + for sid, data in sessions.items() + ] + + +def load_session_file(file_path: str, file_type: str) -> Optional[Any]: + if not os.path.isfile(file_path): + return None + try: + if file_type == "log": + with open(file_path, encoding="utf-8-sig", errors="replace", newline="") as f: + lines = f.readlines()[-200:] + return "".join(lines) if lines else "(empty)" + with open(file_path, encoding="utf-8-sig") as f: + return json.load(f) + except (OSError, json.JSONDecodeError): + return None + + +def _list_text_files(root: Path) -> list[Path]: + if not root.is_dir(): + return [] + files = [] + for name in sorted(os.listdir(root)): + path = root / name + if not path.is_file(): + continue + if path.suffix.lower() in {".txt", ".log", ".json"}: + files.append(path) + return files + + +def _load_text_file(path: Path) -> Optional[str]: + if not path.is_file(): + return None + suffix = path.suffix.lower() + if suffix == ".json": + data = load_session_file(str(path), "json") + if data is None: + return None + return json.dumps(data, ensure_ascii=False, indent=2) + if suffix in {".txt", ".log"}: + return load_session_file(str(path), "log") + return None + + +def _load_trajectory_events(path: Path) -> list[dict]: + if not path.is_file(): + return [] + events: list[dict] = [] + try: + with open(path, encoding="utf-8-sig") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + continue + except OSError: + return [] + return events + + +def _build_history_sessions( + run_id: str, + results: Optional[dict], + *, + output_dir: Optional[str] = None, +) -> dict[str, dict]: + sessions: dict[str, dict] = {} + session_results = results.get("session_results") if isinstance(results, dict) else None + if isinstance(session_results, list): + for item in session_results: + if not isinstance(item, dict): + continue + session_id = item.get("session_id") + if not session_id: + continue + success = item.get("success", False) + is_finished = item.get("is_finished") + details = item.get("details") or {} + session_metadata = details.get("session_metadata") or {} + error_source = session_metadata.get("error_source") + error_message = session_metadata.get("error") + sessions[session_id] = { + "status": _status_from_outcome(success, is_finished, error_source), + "success": bool(success), + "steps": item.get("steps", 0), + "score": item.get("score"), + "execution_time": item.get("execution_time"), + "agent_cost": item.get("agent_cost"), + "benchmark_cost": item.get("benchmark_cost"), + "is_finished": is_finished, + "error_source": error_source, + "error": error_message, + } + if sessions: + return sessions + + resolved_output = output_dir or get_settings().output_dir + run_paths = RunPaths(run_id=run_id, output_dir=resolved_output) + sessions_root = run_paths.sessions_root + if not os.path.isdir(sessions_root): + return sessions + for name in sorted(os.listdir(sessions_root)): + sess_paths = run_paths.session(name) + sess_results = sess_paths.results + sess_summary = sess_paths.summary + target_path = sess_results if sess_results.is_file() else sess_summary + if not target_path.is_file(): + continue + data = load_session_file(str(target_path), "json") + if not isinstance(data, dict): + continue + success = data.get("success", False) + is_finished = data.get("is_finished") + details = data.get("details") or {} + session_metadata = details.get("session_metadata") or {} + error_source = session_metadata.get("error_source") + error_message = session_metadata.get("error") + sessions[name] = { + "status": _status_from_outcome(success, is_finished, error_source), + "success": bool(success), + "steps": data.get("steps", 0), + "score": data.get("score"), + "execution_time": data.get("execution_time"), + "agent_cost": data.get("agent_cost"), + "benchmark_cost": data.get("benchmark_cost"), + "is_finished": is_finished, + "error_source": error_source, + "error": error_message, + } + return sessions + + +def _load_history_turns( + run_id: str, + session_id: str, + *, + output_dir: Optional[str] = None, +) -> list[dict]: + resolved_output = output_dir or get_settings().output_dir + sess_paths = RunPaths(run_id=run_id, output_dir=resolved_output).session(session_id) + events = _load_trajectory_events(sess_paths.trajectory) + turns: list[dict] = [] + for event in events: + kind = event.get("event") or event.get("type") or "event" + if kind == "action": + payload = event.get("action") + elif kind == "observation": + payload = event.get("observation") + elif kind == "error": + payload = event.get("error") or event + else: + payload = event + turns.append({"type": kind, "step": event.get("step"), "content": payload}) + return turns[-200:] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/formatting.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/formatting.py new file mode 100644 index 00000000..8065bf4a --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/formatting.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import math +from typing import Any + + +def _format_value(value: Any) -> str: + if value is None: + return "-" + if isinstance(value, float): + if math.isnan(value): + return "NaN" + abs_val = abs(value) + if abs_val >= 100 or value.is_integer(): + return f"{value:.2f}".rstrip("0").rstrip(".") + if abs_val >= 1: + return f"{value:.3f}".rstrip("0").rstrip(".") + return f"{value:.6f}".rstrip("0").rstrip(".") + return str(value) + + +def _format_payload(value: Any, limit: int = 4000) -> str: + try: + payload = json.dumps(value, ensure_ascii=False, indent=2, default=str) + except Exception: + payload = str(value) + if len(payload) > limit: + return payload[:limit] + "..." + return payload + + +def _format_error_message(value: Any, limit: int = 4000) -> str: + if isinstance(value, str): + if len(value) > limit: + return value[:limit] + "..." + return value + return _format_payload(value, limit=limit) + + +def _normalize_action_payload(payload: Any) -> dict: + if payload is None: + return {"name": None, "arguments": None} + if isinstance(payload, dict): + action_type = payload.get("type") + if action_type in {"parallel", "sequential"}: + actions = payload.get("actions") or [] + cleaned_actions = [] + if isinstance(actions, list): + for item in actions: + if not isinstance(item, dict): + cleaned_actions.append({"name": str(item), "arguments": None}) + continue + cleaned_actions.append( + { + "name": item.get("name"), + "arguments": item.get("arguments"), + } + ) + return {"mode": action_type, "actions": cleaned_actions} + return { + "name": payload.get("name"), + "arguments": payload.get("arguments"), + } + return {"raw": payload} + + +def _normalize_observation_item(item: Any) -> Any: + if item is None: + return None + if isinstance(item, dict): + if "result" in item: + result = item.get("result") + if isinstance(result, dict): + if "sender" in result and "message" in result: + return { + "sender": result.get("sender"), + "message": result.get("message"), + } + return result + if "sender" in item and "message" in item: + return {"sender": item.get("sender"), "message": item.get("message")} + return item + return item + + +def _normalize_observation_payload(payload: Any) -> Any: + if payload is None: + return None + if isinstance(payload, dict): + if isinstance(payload.get("observations"), list): + return [_normalize_observation_item(item) for item in payload["observations"]] + return _normalize_observation_item(payload) + if isinstance(payload, list): + return [_normalize_observation_item(item) for item in payload] + return payload diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/forms.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/forms.py new file mode 100644 index 00000000..b42a8f27 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/forms.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import types +from typing import Any, Literal, Union, get_args, get_origin + +from nicegui import ui + +try: + from pydantic.fields import PydanticUndefined +except Exception: # pragma: no cover + PydanticUndefined = None + + +def _is_optional(ann: Any) -> tuple[bool, Any]: + origin = get_origin(ann) + if origin in (Union, types.UnionType): + args = list(get_args(ann)) + if len(args) == 2 and type(None) in args: + base = args[0] if args[1] is type(None) else args[1] + return True, base + return False, ann + + +def _build_pydantic_form(model_cls: type[Any], disabled: bool) -> dict[str, Any]: + controls: dict[str, Any] = {} + fields = model_cls.model_fields + schema = model_cls.model_json_schema() + schema_props: dict[str, Any] = schema.get("properties", {}) + + with ui.column().classes("w-full form-stack"): + for name, form_field in fields.items(): + if name.startswith("_"): + continue + ann = form_field.annotation + default = form_field.default + if PydanticUndefined is not None and default is PydanticUndefined: + default = None + is_opt, base = _is_optional(ann) + origin = get_origin(base) + + options = None + prop = schema_props.get(name) or {} + if isinstance(prop, dict): + if "enum" in prop: + options = list(prop["enum"]) + elif "anyOf" in prop: + any_of = prop["anyOf"] + if isinstance(any_of, list): + for sub in any_of: + if isinstance(sub, dict) and "enum" in sub: + options = list(sub["enum"]) + break + elif "const" in prop: + options = [prop["const"]] + if origin is Literal and options is None: + options = list(get_args(base)) + + if options: + shown = ["", *options] if is_opt and default is None else options + value = default if default in shown else (shown[0] if shown else None) + control = ui.select(shown, value=value, label=name).props("dense") + control.enabled = not disabled + controls[name] = ("select", control, is_opt) + elif base in (int, float): + value = default if default is not None else 0 + control = ui.number(label=name, value=value) + control.enabled = not disabled + controls[name] = ("number", control, is_opt, base) + elif base is bool: + value = bool(default) if default is not None else False + control = ui.checkbox(name, value=value) + control.enabled = not disabled + controls[name] = ("checkbox", control, is_opt) + elif (get_origin(base) is dict) or (base is dict): + init = json.dumps(default or {}, ensure_ascii=False, indent=2) + control = ui.textarea(label=name, value=init).props("rows=4") + control.enabled = not disabled + controls[name] = ("json", control, is_opt, "dict") + elif (get_origin(base) is list) or (base is list): + init = json.dumps(default or [], ensure_ascii=False, indent=2) + control = ui.textarea(label=name, value=init).props("rows=4") + control.enabled = not disabled + controls[name] = ("json", control, is_opt, "list") + else: + value = "" if default is None else str(default) + control = ui.input(label=name, value=value) + control.enabled = not disabled + controls[name] = ("text", control, is_opt) + + return controls + + +def _build_agent_form(agent_cls: type[Any], disabled: bool) -> dict[str, Any]: + return _build_pydantic_form(agent_cls, disabled) + + +def _collect_values(controls: dict[str, Any]) -> dict[str, Any]: + values: dict[str, Any] = {} + for name, data in controls.items(): + kind = data[0] + control = data[1] + is_opt = data[2] if len(data) > 2 else False + raw = control.value + if kind == "select": + if is_opt and (raw is None or raw == ""): + values[name] = None + else: + values[name] = raw + elif kind == "number": + base = data[3] + if raw is None and is_opt: + values[name] = None + else: + values[name] = base(raw) if raw is not None else 0 + elif kind == "checkbox": + values[name] = bool(raw) + elif kind == "json": + if raw is None or raw == "": + values[name] = {} if data[3] == "dict" else [] + else: + try: + values[name] = json.loads(raw) + except Exception: + values[name] = {} if data[3] == "dict" else [] + else: + if is_opt and (raw is None or raw == ""): + values[name] = None + else: + values[name] = raw + return values diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/panels.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/panels.py new file mode 100644 index 00000000..f8841927 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/panels.py @@ -0,0 +1,505 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Optional + +from nicegui import ui + +from ....utils.paths import RunPaths +from ....utils.settings import get_settings +from .data import ( + _build_session_rows, + _list_text_files, + _load_text_file, + load_run_config, + load_session_file, +) +from .formatting import ( + _format_error_message, + _format_payload, + _format_value, + _normalize_action_payload, + _normalize_observation_payload, +) +from .state import SESSION_COLUMNS +from .status import _render_status_pie, _status_counts_from_sessions + + +def _metric_help_text(label: str) -> Optional[str]: + return { + "Benchmark": "Benchmark used for this run.", + "Agent": "Agent used for this run.", + "Models": "LLMs used in this run.", + "Model": "Primary model used in this run.", + "Subset": "Benchmark subset for this run.", + "Running": "Currently running sessions / worker capacity.", + "Completed": "Finished sessions / planned sessions.", + "Total Sessions": "Total sessions recorded for this run.", + "Successful": "Number of successful sessions.", + "Benchmark Score": "Aggregate benchmark score for the run.", + "Avg Score": "Average score across completed sessions.", + "Avg Steps": "Average steps per session.", + "Avg Agent Cost": "Average agent cost per session.", + "Avg Benchmark Cost": "Average benchmark cost per session.", + "Total Agent Cost": "Total agent cost across sessions.", + "Total Benchmark Cost": "Total benchmark cost across sessions.", + "Total Run Cost": "Total agent + benchmark cost.", + "Finished": "Share of sessions marked finished.", + "Success Rate": "Share of successful sessions.", + "Finished Rate": "Share of sessions marked finished.", + "Success %": "Share of successful sessions.", + "Finished %": "Share of sessions marked finished.", + "Status": ( + "Session status (running/success/unsuccessful/unfinished/" "agent error/benchmark error/cancelled/error)." + ), + "Steps": "Number of actions taken in the session.", + "Score": "Session score from the benchmark.", + "Exec Time (s)": "Elapsed time for the session.", + "Agent Cost": "Cost attributed to the agent.", + "Benchmark Cost": "Cost attributed to the benchmark.", + }.get(label) + + +def _metric_info(help_text: str) -> None: + ui.icon("info").classes("metric-info").tooltip(help_text) + + +def _metric_card(label: str, value: Any) -> None: + with ui.element("div").classes("metric-card"): + help_text = _metric_help_text(label) + if help_text: + _metric_info(help_text) + ui.label(label).classes("metric-label") + ui.label(_format_value(value)).classes("metric-value") + + +def _metric_card_small(label: str, value: Any) -> None: + display_value = value if isinstance(value, str) else _format_value(value) + with ui.element("div").classes("metric-card metric-card-sm"): + help_text = _metric_help_text(label) + if help_text: + _metric_info(help_text) + ui.label(label).classes("metric-label") + ui.label(display_value).classes("metric-value") + + +def _metric_card_large(label: str, value: Any) -> None: + display_value = value if isinstance(value, str) else _format_value(value) + with ui.element("div").classes("metric-card metric-card-lg"): + help_text = _metric_help_text(label) + if help_text: + _metric_info(help_text) + ui.label(label).classes("metric-label") + ui.label(display_value).classes("metric-value") + + +def _render_run_log(run_id: Optional[str], *, output_dir: Optional[str] = None) -> None: + if not run_id: + ui.label("No run log found.") + return + resolved_output = output_dir or get_settings().output_dir + run_paths = RunPaths(run_id=run_id, output_dir=resolved_output) + log_content = load_session_file(str(run_paths.tracker), "log") + if log_content is None: + legacy_path = run_paths.root / "tracker.log" + if legacy_path != run_paths.tracker: + log_content = load_session_file(str(legacy_path), "log") + if log_content is None: + ui.label("No run log found.") + return + ui.code(log_content, language="text").classes("w-full") + + +def _render_action_entry(payload: Any) -> None: + data = _normalize_action_payload(payload) + if "raw" in data: + ui.code(_format_payload(data["raw"]), language="json").classes("w-full") + return + if "actions" in data: + mode = str(data.get("mode", "parallel")).title() + actions = data.get("actions") or [] + ui.label(f"{mode} actions ({len(actions)})").classes("metric") + if not actions: + ui.label("No actions provided.") + return + with ui.column().classes("w-full gap-3"): + for idx, item in enumerate(actions, start=1): + name = item.get("name") or f"Action {idx}" + args = item.get("arguments") + with ui.element("div").classes("trajectory-box"): + ui.label(name).classes("trajectory-title") + if args is None: + ui.label("No arguments.") + else: + ui.code(_format_payload(args), language="json").classes("w-full") + return + name = data.get("name") or "Action" + args = data.get("arguments") + if data.get("name") is None and data.get("arguments") is None: + ui.label("Empty action.") + return + with ui.element("div").classes("trajectory-box"): + ui.label(name).classes("trajectory-title") + if args is None: + ui.label("No arguments.") + else: + ui.code(_format_payload(args), language="json").classes("w-full") + + +def _render_observation_entry(payload: Any) -> None: + data = _normalize_observation_payload(payload) + if data is None: + ui.label("Empty observation.") + return + if isinstance(data, list): + ui.label(f"Observations ({len(data)})").classes("metric") + if not data: + ui.label("No observations provided.") + return + with ui.column().classes("w-full gap-3"): + for item in data: + if item is None: + ui.label("Empty observation.") + continue + if isinstance(item, dict) and "sender" in item and "message" in item: + sender = item.get("sender") or "Sender" + message = item.get("message") or "" + with ui.element("div").classes("trajectory-box"): + ui.label(str(sender)).classes("trajectory-title") + ui.code(str(message), language="text").classes("w-full") + continue + with ui.element("div").classes("trajectory-box"): + ui.code(_format_payload(item), language="json").classes("w-full") + return + if isinstance(data, dict) and "sender" in data and "message" in data: + sender = data.get("sender") or "Sender" + message = data.get("message") or "" + with ui.element("div").classes("trajectory-box"): + ui.label(str(sender)).classes("trajectory-title") + ui.code(str(message), language="text").classes("w-full") + return + ui.code(_format_payload(data), language="json").classes("w-full") + + +def _render_trajectory_entry(kind: str, payload: Any) -> None: + if kind == "action": + _render_action_entry(payload) + return + if kind == "observation": + _render_observation_entry(payload) + return + if kind == "error": + ui.code(_format_payload(payload), language="json").classes("w-full") + return + ui.code(_format_payload(payload), language="json").classes("w-full") + + +def _render_overview_panel( + sessions: dict, + *, + run_active: bool, + run_id: Optional[str], + results: Optional[dict] = None, + planned_sessions: int | None = None, + total_workers: int | None = None, + run_meta: Optional[dict[str, Any]] = None, + on_open_session=None, +) -> None: + if run_meta: + models = run_meta.get("models") or [] + models_text = ", ".join(models) if models else "-" + with ui.element("div").classes("w-full metric-grid"): + _metric_card_large("Benchmark", run_meta.get("benchmark")) + _metric_card_large("Agent", run_meta.get("agent")) + _metric_card_large("Models", models_text) + if not sessions: + if run_active: + ui.label("Waiting for sessions...").classes("text-sm muted") + else: + ui.label("No sessions found.") + return + total = len(sessions) + running = sum(1 for s in sessions.values() if s.get("status") == "running") + done = total - running + if planned_sessions is None: + planned_sessions = total + running_total = total_workers if total_workers is not None else "-" + benchmark_score = results.get("benchmark_score") if isinstance(results, dict) else None + with ui.element("div").classes("w-full metric-grid"): + _metric_card("Running", f"{running}/{running_total}") + _metric_card("Completed", f"{done}/{planned_sessions}") + _metric_card("Benchmark Score", _format_value(benchmark_score)) + + from .data import _build_overview_secondary_metrics + + secondary_metrics = _build_overview_secondary_metrics(sessions, results) + with ui.element("div").classes("w-full metric-grid"): + for label, value in secondary_metrics: + _metric_card_small(label, value) + + with ui.card().classes("w-full card p-4"): + ui.label("Status Breakdown").classes("section-title") + status_counts = _status_counts_from_sessions(sessions) + _render_status_pie(status_counts) + + with ui.card().classes("w-full card p-4"): + rows = _build_session_rows(sessions) + selection = "single" if on_open_session else None + + def _handle_select(e) -> None: + selection_rows = getattr(e, "selection", None) + if not selection_rows: + return + row = selection_rows[-1] + if isinstance(row, dict): + on_open_session(row) + + ( + ui.table( + columns=SESSION_COLUMNS, + rows=rows, + row_key="session", + selection=selection, + on_select=_handle_select if on_open_session else None, + ) + .classes("w-full") + .props("flat") + ) + + +def _render_tabs( + labels: list[str], + renderers: dict[str, Any], + *, + default: Optional[str] = None, +) -> None: + default_label = default or labels[0] + with ui.tabs() as tabs: + tab_by_name = {label: ui.tab(label) for label in labels} + + with ui.tab_panels(tabs, value=tab_by_name[default_label]).classes("w-full"): + for label in labels: + with ui.tab_panel(tab_by_name[label]): + renderers[label]() + + +def _render_session_results_overview(meta: dict) -> None: + with ui.element("div").classes("w-full metric-grid"): + _metric_card("Status", meta.get("status", "-")) + _metric_card("Steps", meta.get("steps", 0)) + _metric_card("Score", meta.get("score", "-")) + error_msg = meta.get("error") + if error_msg: + ui.label("Error Details").classes("section-title") + ui.code(_format_error_message(error_msg), language="text").classes("w-full").style("white-space: pre-wrap;") + + with ui.element("div").classes("w-full metric-grid"): + _metric_card_small("Exec Time (s)", meta.get("execution_time")) + _metric_card_small("Agent Cost", meta.get("agent_cost")) + _metric_card_small("Benchmark Cost", meta.get("benchmark_cost")) + + +def _render_session_trajectory(turns_list: list[dict]) -> None: + if not turns_list: + ui.label("No trajectory data available yet.") + return + with ui.timeline(side="right"): + for item in turns_list: + kind = str(item.get("type", "event")) + step_no = item.get("step", "?") + content = item.get("content") + title = kind.title() + subtitle = f"Step {step_no}" if step_no != "?" else None + icon = None + if kind == "action": + icon = "bolt" + elif kind == "observation": + icon = "visibility" + elif kind == "error": + icon = "error" + with ui.timeline_entry(title=title, subtitle=subtitle, icon=icon): + _render_trajectory_entry(kind, content) + + +def _render_session_logs( + run_id: Optional[str], + agent_files: list[Path], + benchmark_files: list[Path], +) -> None: + if not run_id: + ui.label("No logs found.") + return + + def _render_agent_logs() -> None: + _render_log_files(agent_files) + + def _render_benchmark_logs() -> None: + _render_log_files(benchmark_files) + + _render_tabs( + ["Agent", "Benchmark"], + {"Agent": _render_agent_logs, "Benchmark": _render_benchmark_logs}, + ) + + +def _render_session_config( + run_config_content: Optional[dict], + benchmark_config_content: Optional[dict], +) -> None: + if run_config_content is None and benchmark_config_content is None: + ui.label("No config found.") + return + if run_config_content is not None: + with ui.expansion("Run Config", value=False): + ui.code(_format_payload(run_config_content), language="json").classes("w-full") + if benchmark_config_content is not None: + with ui.expansion("Benchmark Config", value=False): + ui.code(_format_payload(benchmark_config_content), language="json").classes("w-full") + + +def _render_session_benchmark_results(results_content: Optional[dict]) -> None: + if results_content is None: + ui.label("No results found.") + else: + ui.code(_format_payload(results_content), language="json").classes("w-full") + + +def _render_session_tabs( + *, + meta: dict, + session_data: Optional[dict], + turns_list: list[dict], + run_id: Optional[str], + agent_files: list[Path], + benchmark_files: list[Path], + run_config_content: Optional[dict], + benchmark_config_content: Optional[dict], + results_content: Optional[dict], +) -> None: + def _render_results_tab() -> None: + _render_session_results_overview(meta) + + def _render_task_tab() -> None: + _render_task_details(session_data) + + def _render_trajectory_tab() -> None: + _render_session_trajectory(turns_list) + + def _render_logs_tab() -> None: + _render_session_logs(run_id, agent_files, benchmark_files) + + def _render_config_tab() -> None: + _render_session_config(run_config_content, benchmark_config_content) + + def _render_benchmark_results_tab() -> None: + _render_session_benchmark_results(results_content) + + _render_tabs( + ["Results", "Task", "Trajectory", "Logs", "Config", "Benchmark Results"], + { + "Results": _render_results_tab, + "Task": _render_task_tab, + "Trajectory": _render_trajectory_tab, + "Logs": _render_logs_tab, + "Config": _render_config_tab, + "Benchmark Results": _render_benchmark_results_tab, + }, + default="Results", + ) + + +def _render_dive_panel( + sessions: dict, + turns: dict, + *, + run_id: Optional[str], + selected_session: Optional[str], + on_session_change, + output_dir: Optional[str] = None, +) -> None: + session_ids = sorted(sessions.keys()) + if not session_ids: + ui.label("No sessions to inspect yet.") + return + + active_session = selected_session if selected_session in session_ids else session_ids[0] + + with ui.card().classes("w-full card p-4"): + ui.select(session_ids, value=active_session, label="Select session").props("dense").on_value_change( + on_session_change + ) + meta = sessions.get(active_session, {}) + turns_list = turns.get(active_session, []) + + agent_files: list[Path] = [] + benchmark_files: list[Path] = [] + session_data = None + run_config_content = None + benchmark_config_content = None + results_content = None + if run_id: + resolved_output = output_dir or get_settings().output_dir + sess_paths = RunPaths(run_id=run_id, output_dir=resolved_output).session(active_session) + base = sess_paths.benchmark_dir + agent_files = _list_text_files(sess_paths.agent_dir) + benchmark_files = _list_text_files(base) + session_data = load_session_file(str(sess_paths.session_manifest), "json") + run_config_content = load_run_config(run_id, output_dir=resolved_output) + benchmark_config_content = load_session_file(str(base / "config.json"), "json") + results_content = load_session_file(str(sess_paths.benchmark_results), "json") + + with ui.card().classes("w-full card p-4"): + _render_session_tabs( + meta=meta, + session_data=session_data, + turns_list=turns_list, + run_id=run_id, + agent_files=agent_files, + benchmark_files=benchmark_files, + run_config_content=run_config_content, + benchmark_config_content=benchmark_config_content, + results_content=results_content, + ) + + +def _render_log_files(files: list[Path]) -> None: + if not files: + ui.label("No logs found.") + return + for path in files: + with ui.expansion(path.name, value=False): + content = _load_text_file(path) + if content is None: + ui.label("Unable to read file.") + else: + language = "json" if path.suffix.lower() == ".json" else "text" + ui.code(content, language=language).classes("w-full") + + +def _render_task_details(session_data: Optional[dict]) -> None: + if not session_data: + ui.label("No session data found.") + return + ordered_keys = ["task", "context", "actions"] + seen = set() + for key in ordered_keys: + if key in session_data: + seen.add(key) + with ui.expansion(str(key), value=False): + value = session_data.get(key) + if isinstance(value, str): + ui.code(value, language="text").classes("w-full") + else: + ui.code(_format_payload(value), language="json").classes("w-full") + for key in sorted(session_data.keys()): + if key in seen: + continue + with ui.expansion(str(key), value=False): + value = session_data.get(key) + if isinstance(value, str): + ui.code(value, language="text").classes("w-full") + else: + ui.code(_format_payload(value), language="json").classes("w-full") diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/runtime.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/runtime.py new file mode 100644 index 00000000..9514c490 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/runtime.py @@ -0,0 +1,895 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import logging +import os +import threading +from typing import Any + +from nicegui import ui + +from ....core.context import run_scope +from ....core.types import RunConfig +from ....observers.handlers.dashboard_events import DashboardEventsObserver +from ....utils.paths import RunPaths +from ....utils.settings import get_settings +from ...lib.api import ( + evaluate, + get_agent_info, + get_benchmark_info, + load_agent_class, + load_benchmark_class, +) +from .data import ( + _build_history_sessions, + _load_history_turns, + _load_run_context, + _open_session_from_row, + _resolve_tab_label, + get_display_mappings, + load_leaderboard_data, +) +from .forms import _build_agent_form, _build_pydantic_form, _collect_values +from .panels import ( + _render_dive_panel, + _render_overview_panel, + _render_run_log, +) +from .state import LEADERBOARD_COLUMNS, RunState, RunViews +from .status import _status_from_outcome + +_LOG = logging.getLogger("exgentic.interfaces.dashboard") +if not _LOG.handlers: + _handler = logging.StreamHandler() + _handler.setFormatter(logging.Formatter("Dashboard | %(message)s")) + _LOG.addHandler(_handler) +_LOG.setLevel(logging.INFO) +_LOG.propagate = False + + +def _init_tabs( + state: RunState, + scope: str, + labels: list[str], +) -> tuple[Any, dict[str, Any], Any]: + if scope not in state.active_tabs: + state.active_tabs[scope] = labels[0] + + with ui.tabs() as tabs: + tab_by_name = {label: ui.tab(label) for label in labels} + + state.tabs_controls[scope] = tabs + state.tabs_by_scope[scope] = tab_by_name + + def _on_tab_change(e) -> None: + value = getattr(e, "value", None) + state.active_tabs[scope] = _resolve_tab_label(value, tab_by_name, labels[0]) + + tabs.on_value_change(_on_tab_change) + + active_label = state.active_tabs.get(scope, labels[0]) + active_tab = tab_by_name.get(active_label, tab_by_name[labels[0]]) + return tabs, tab_by_name, active_tab + + +def process_new_events(state: RunState) -> bool: + tracker = state.tracker + if tracker is None: + return False + + events_to_process = [] + event_count = 0 + max_events = 40 + + while not tracker.events.empty() and event_count < max_events: + try: + evt = tracker.events.get_nowait() + events_to_process.append(evt) + event_count += 1 + except Exception: + break + + if not events_to_process: + return False + + sessions = state.sessions + turns = state.turns + + for evt in events_to_process: + state.events.append(evt) + et = evt.get("type") + + if et == "session_started": + sid = evt.get("session_id") + if sid: + sessions[sid] = { + "status": "running", + "steps": 0, + "success": None, + "is_finished": None, + "error_source": None, + } + + elif et == "step": + sid = evt.get("session_id") + if sid and sid in sessions: + sessions[sid]["steps"] = sessions[sid].get("steps", 0) + 1 + if "execution_time" in evt: + sessions[sid]["execution_time"] = evt.get("execution_time") + if "agent_cost" in evt: + sessions[sid]["agent_cost"] = evt.get("agent_cost") + if "benchmark_cost" in evt: + sessions[sid]["benchmark_cost"] = evt.get("benchmark_cost") + step_no = evt.get("n") + act_obj = evt.get("action_obj") + if act_obj is not None: + turns.setdefault(sid, []).append({"type": "action", "step": step_no, "content": act_obj}) + turns[sid] = turns[sid][-200:] + + elif et == "observation": + sid = evt.get("session_id") + if sid and sid in sessions: + step_no = evt.get("step") + obs_obj = evt.get("observation") + if "execution_time" in evt: + sessions[sid]["execution_time"] = evt.get("execution_time") + if "agent_cost" in evt: + sessions[sid]["agent_cost"] = evt.get("agent_cost") + if "benchmark_cost" in evt: + sessions[sid]["benchmark_cost"] = evt.get("benchmark_cost") + if obs_obj is not None: + turns.setdefault(sid, []).append({"type": "observation", "step": step_no, "content": obs_obj}) + turns[sid] = turns[sid][-200:] + + elif et == "session_finished": + sid = evt.get("session_id") + if sid and sid in sessions: + success = evt.get("success", False) + is_finished = evt.get("is_finished") + error_source = evt.get("error_source") + sessions[sid]["status"] = _status_from_outcome(success, is_finished, error_source) + sessions[sid]["success"] = success + sessions[sid]["score"] = evt.get("score") + if "steps" in evt: + sessions[sid]["steps"] = evt.get("steps") + if "execution_time" in evt: + sessions[sid]["execution_time"] = evt.get("execution_time") + if "agent_cost" in evt: + sessions[sid]["agent_cost"] = evt.get("agent_cost") + if "benchmark_cost" in evt: + sessions[sid]["benchmark_cost"] = evt.get("benchmark_cost") + if "is_finished" in evt: + sessions[sid]["is_finished"] = is_finished + if "error_source" in evt: + sessions[sid]["error_source"] = error_source + if "details" in evt: + details = evt.get("details") or {} + sessions[sid]["error"] = details.get("error") + if not success: + details = evt.get("details") or {} + turns.setdefault(sid, []).append( + { + "type": "error", + "step": sessions[sid].get("steps", 0), + "content": details or "Session ended with an error.", + } + ) + + elif et == "saved": + ctx_mgr = state.run_context_manager + if ctx_mgr is not None: + try: + ctx_mgr.__exit__(None, None, None) + except Exception: + pass + state.run_context_manager = None + state.run_active = False + state.refresh_needed = True + _LOG.info("run saved; dashboard state updated") + + elif et == "run_meta": + run_id = evt.get("run_id") + if run_id: + state.run_id = run_id + _LOG.info("run_meta received: %s", run_id) + + return True + + +@ui.refreshable +def bench_form_panel(state: RunState) -> None: + bench_key = state.bench_key + if bench_key is None: + ui.label("No benchmark selected") + return + try: + bench_cls = load_benchmark_class(bench_key) + except Exception as exc: + ui.label(f"Failed to load benchmark '{bench_key}': {exc}") + return + state.bench_controls = _build_pydantic_form(bench_cls, disabled=state.run_active) + + +@ui.refreshable +def agent_form_panel(state: RunState) -> None: + agent_key = state.agent_key + if agent_key is None: + ui.label("No agent selected") + return + try: + agent_cls = load_agent_class(agent_key) + except Exception as exc: + ui.label(f"Failed to load agent '{agent_key}': {exc}") + return + state.agent_controls = _build_agent_form(agent_cls, disabled=state.run_active) + + +@ui.refreshable +def run_log_panel(state: RunState) -> None: + _render_run_log(state.run_id) + + +@ui.refreshable +def overview_panel(state: RunState) -> None: + fallback_benchmark = None + if state.bench_key: + try: + fallback_benchmark = get_benchmark_info(state.bench_key)["display_name"] + except Exception: + fallback_benchmark = None + fallback_agent = None + if state.agent_key: + try: + fallback_agent = get_agent_info(state.agent_key)["display_name"] + except Exception: + fallback_agent = None + context = _load_run_context( + state.run_id, + fallback_benchmark=fallback_benchmark, + fallback_agent=fallback_agent, + planned_fallback=state.planned_sessions, + workers_fallback=state.max_workers if state.run_active else None, + ) + + def _open_session(row: dict) -> None: + _open_session_from_row(state, "run", row, dive_panel.refresh) + + _render_overview_panel( + state.sessions, + run_active=state.run_active, + run_id=state.run_id, + results=context.results, + planned_sessions=context.planned_sessions, + total_workers=context.total_workers, + run_meta=context.run_meta, + on_open_session=_open_session, + ) + + +@ui.refreshable +def dive_panel(state: RunState) -> None: + sessions = sorted(state.sessions.keys()) + if not sessions: + ui.label("No sessions to inspect yet.") + return + + if state.selected_session not in sessions: + state.selected_session = sessions[0] + + def on_session_change(e) -> None: + state.selected_session = e.value + dive_panel.refresh() + + _render_dive_panel( + state.sessions, + state.turns, + run_id=state.run_id, + selected_session=state.selected_session, + on_session_change=on_session_change, + ) + + +@ui.refreshable +def leaderboard_panel(state: RunState) -> None: + settings = get_settings() + rows = load_leaderboard_data(settings.output_dir) + rows = sorted(rows, key=lambda r: r.get("run_id", ""), reverse=True) + if not rows: + ui.label("No runs found yet.") + return + + def _norm(value: object) -> str: + if value is None: + return "unknown" + text = str(value).strip() + return text or "unknown" + + agents = sorted({_norm(row.get("Agent")) for row in rows}) + models = sorted({_norm(row.get("Model")) for row in rows}) + benchmarks = sorted({_norm(row.get("Benchmark")) for row in rows}) + subsets = sorted({_norm(row.get("Subset")) for row in rows}) + + if not state.selected_agents: + state.selected_agents = agents + if not state.selected_models: + state.selected_models = models + if not state.selected_benchmarks: + state.selected_benchmarks = benchmarks + if not state.selected_subsets: + state.selected_subsets = subsets + + def on_agents_change(e) -> None: + state.selected_agents = e.value or [] + leaderboard_panel.refresh() + + def on_models_change(e) -> None: + state.selected_models = e.value or [] + leaderboard_panel.refresh() + + def on_benchmarks_change(e) -> None: + state.selected_benchmarks = e.value or [] + leaderboard_panel.refresh() + + def on_subsets_change(e) -> None: + state.selected_subsets = e.value or [] + leaderboard_panel.refresh() + + def on_min_tasks_change(e) -> None: + try: + state.min_tasks = int(e.value) + except Exception: + state.min_tasks = 0 + leaderboard_panel.refresh() + + with ui.card().classes("w-full card p-4"): + with ui.row().classes("w-full"): + ui.select( + agents, + value=state.selected_agents, + label="Agent", + multiple=True, + ).props("dense").on_value_change(on_agents_change) + ui.select( + models, + value=state.selected_models, + label="Model", + multiple=True, + ).props("dense").on_value_change(on_models_change) + ui.select( + benchmarks, + value=state.selected_benchmarks, + label="Benchmark", + multiple=True, + ).props("dense").on_value_change(on_benchmarks_change) + ui.select( + subsets, + value=state.selected_subsets, + label="Subset", + multiple=True, + ).props("dense").on_value_change(on_subsets_change) + ui.number( + label="Min Tasks", + value=state.min_tasks, + min=0, + step=1, + ).props("dense").on_value_change(on_min_tasks_change) + + def _include(row: dict) -> bool: + if row.get("Agent", "unknown") not in state.selected_agents: + return False + if row.get("Model", "unknown") not in state.selected_models: + return False + if row.get("Benchmark", "unknown") not in state.selected_benchmarks: + return False + if row.get("Subset", "unknown") not in state.selected_subsets: + return False + try: + return int(row.get("Num Tasks", 0)) >= state.min_tasks + except Exception: + return False + + filtered = [row for row in rows if _include(row)] + with ui.card().classes("w-full card p-4"): + ui.table(columns=LEADERBOARD_COLUMNS, rows=filtered, row_key="run_id").classes("w-full").props("flat") + + +@ui.refreshable +def history_panel(state: RunState) -> None: + settings = get_settings() + + def _normalize_history_root(value: str) -> str: + raw = (value or "").strip() + if not raw: + return settings.output_dir + return os.path.abspath(os.path.expanduser(raw)) + + history_root = _normalize_history_root(state.history_root) + runs = [] + if os.path.isdir(history_root): + for name in os.listdir(history_root): + results_path = RunPaths(run_id=name, output_dir=history_root).results + if os.path.isfile(results_path): + runs.append(name) + runs = sorted(runs, reverse=True) + + if state.selected_history_run not in runs: + state.selected_history_run = runs[0] if runs else None + state.selected_history_session = None + state.active_tabs["history"] = "Overview" + + def on_history_change(e) -> None: + state.selected_history_run = e.value + state.selected_history_session = None + state.active_tabs["history"] = "Overview" + history_panel.refresh() + + def on_history_root_change(e) -> None: + state.history_root = e.value or "" + state.selected_history_run = None + state.selected_history_session = None + state.active_tabs["history"] = "Overview" + history_panel.refresh() + + def open_history_browser() -> None: + browser_state = {"path": history_root} + + dialog = ui.dialog() + + def refresh_entries(container) -> None: + container.clear() + current = browser_state["path"] + with container: + if not os.path.isdir(current): + ui.label("Directory not found.").classes("text-negative") + return + parent = os.path.dirname(current.rstrip(os.sep)) + entries = [name for name in sorted(os.listdir(current)) if os.path.isdir(os.path.join(current, name))] + if parent and parent != current: + entries = ["..", *entries] + if not entries: + ui.label("(empty)") + return + for name in entries: + ui.button( + name, + on_click=lambda n=name: on_entry_click(n, container), + ).props("flat dense").classes("justify-start w-full") + + def on_entry_click(name: str, container) -> None: + current = browser_state["path"] + if name == "..": + parent = os.path.dirname(current.rstrip(os.sep)) + if parent and parent != current: + browser_state["path"] = parent + else: + browser_state["path"] = os.path.join(current, name) + path_input.value = browser_state["path"] + refresh_entries(container) + + def on_select() -> None: + if not os.path.isdir(browser_state["path"]): + ui.notify("Directory not found.") + return + state.history_root = browser_state["path"] + state.selected_history_run = None + state.selected_history_session = None + state.active_tabs["history"] = "Overview" + history_panel.refresh() + dialog.close() + + with dialog, ui.card().classes("w-[520px] max-w-full"): + ui.label("Select run directory") + with ui.row().classes("w-full items-center gap-2"): + path_input = ( + ui.input( + label="Directory", + value=browser_state["path"], + ) + .props("dense") + .style("flex: 1;") + ) + + entries_box = ui.column().classes("w-full gap-2").style("max-height: 320px; overflow-y: auto;") + refresh_entries(entries_box) + + def on_path_change(e) -> None: + browser_state["path"] = e.value or "" + refresh_entries(entries_box) + + path_input.on_value_change(on_path_change) + + with ui.row().classes("w-full justify-end gap-2"): + ui.button("Cancel", on_click=dialog.close).props("flat dense") + ui.button("Select", on_click=on_select).props("dense").style( + "background:#111111 !important; color:#ffffff !important;" + ) + + dialog.open() + + def on_history_session_change(e) -> None: + state.selected_history_session = e.value + state.active_tabs["history"] = "Sessions" + history_panel.refresh() + + with ui.card().classes("w-full card p-4"): + with ui.row().classes("w-full items-center gap-3"): + ui.input( + label="Run directory", + value=state.history_root or settings.output_dir, + placeholder=settings.output_dir, + ).props("dense").style("min-width: 360px;").on_value_change(on_history_root_change) + ui.button(icon="folder_open").props("dense flat").on_click(open_history_browser) + ui.select(runs, value=state.selected_history_run, label="Select run").props("dense").style( + "min-width: 280px;" + ).on_value_change(on_history_change) + if not os.path.isdir(history_root): + ui.label(f"Directory not found: {history_root}") + if not state.selected_history_run: + ui.label("No runs found.") + return + + run_id = state.selected_history_run + context = _load_run_context(run_id, output_dir=history_root) + sessions = _build_history_sessions(run_id, context.results, output_dir=history_root) + + session_ids = sorted(sessions.keys()) + if state.selected_history_session not in session_ids: + state.selected_history_session = session_ids[0] if session_ids else None + + turns: dict[str, list[dict]] = {} + if state.selected_history_session: + turns[state.selected_history_session] = _load_history_turns( + run_id, + state.selected_history_session, + output_dir=history_root, + ) + + with ui.card().classes("w-full card p-4"): + tabs, tab_by_name, active_tab = _init_tabs(state, "history", ["Overview", "Sessions", "Log"]) + overview_tab = tab_by_name["Overview"] + sessions_tab = tab_by_name["Sessions"] + log_tab = tab_by_name["Log"] + + with ui.tab_panels(tabs, value=active_tab).classes("w-full"): + with ui.tab_panel(overview_tab): + + def _open_history_session(row: dict) -> None: + _open_session_from_row(state, "history", row, history_panel.refresh) + + _render_overview_panel( + sessions, + run_active=False, + run_id=run_id, + results=context.results, + planned_sessions=context.planned_sessions, + total_workers=context.total_workers, + run_meta=context.run_meta, + on_open_session=_open_history_session, + ) + with ui.tab_panel(sessions_tab): + _render_dive_panel( + sessions, + turns, + run_id=run_id, + selected_session=state.selected_history_session, + on_session_change=on_history_session_change, + output_dir=history_root, + ) + with ui.tab_panel(log_tab): + _render_run_log(run_id, output_dir=history_root) + + +def _set_enabled(control: Any, enabled: bool) -> None: + if hasattr(control, "enabled"): + control.enabled = enabled + return + if enabled and hasattr(control, "enable"): + control.enable() + elif not enabled and hasattr(control, "disable"): + control.disable() + + +def _set_controls_enabled(controls: dict[str, Any], enabled: bool) -> None: + for data in controls.values(): + control = data[1] + _set_enabled(control, enabled) + + +def _set_visible(control: Any, visible: bool) -> None: + if hasattr(control, "visible"): + control.visible = visible + return + if visible and hasattr(control, "show"): + control.show() + elif not visible and hasattr(control, "hide"): + control.hide() + + +def build_run_tab(state: RunState) -> RunViews: + bench_label_to_key, agent_label_to_key = get_display_mappings() + bench_labels = list(bench_label_to_key.keys()) + agent_labels = list(agent_label_to_key.keys()) + + if state.bench_key is None and bench_labels: + state.bench_key = bench_label_to_key[bench_labels[0]] + if state.agent_key is None and agent_labels: + state.agent_key = agent_label_to_key[agent_labels[0]] + + with ui.column().classes("w-full items-center"): + with ui.column().classes("w-full max-w-6xl gap-4"): + with ui.element("div").classes("w-full split-grid"): + with ui.card().classes("w-full card p-4"): + with ui.row().classes("w-full items-center justify-between gap-3"): + ui.label("Agent") + agent_select = ( + ui.select( + agent_labels, + value=agent_labels[0] if agent_labels else None, + label="", + ) + .props("dense") + .style("min-width: 220px;") + ) + with ui.expansion("", value=False).classes("w-full"): + agent_form_panel(state) + with ui.card().classes("w-full card p-4"): + with ui.row().classes("w-full items-center justify-between gap-3"): + ui.label("Benchmark") + bench_select = ( + ui.select( + bench_labels, + value=bench_labels[0] if bench_labels else None, + label="", + ) + .props("dense") + .style("min-width: 220px;") + ) + with ui.expansion("", value=False).classes("w-full"): + bench_form_panel(state) + + with ui.card().classes("w-full card p-4"): + with ui.row().classes("w-full gap-4 items-end"): + num_tasks_input = ui.number( + label="Num Tasks", + value=state.num_tasks or 5, + min=0, + step=1, + ).props("dense") + max_workers_input = ui.number(label="Parallel Workers", value=state.max_workers, min=1).props( + "dense" + ) + start_button = ( + ui.button("Start Run") + .classes("ml-auto start-run-btn") + .style("background:#39ff14 !important; color:#0b0f10 !important;") + ) + + with ui.card().classes("w-full card p-4") as run_panel_box: + run_tabs, tab_by_name, active_tab = _init_tabs(state, "run", ["Overview", "Sessions", "Log"]) + overview_tab = tab_by_name["Overview"] + sessions_tab = tab_by_name["Sessions"] + log_tab = tab_by_name["Log"] + + with ui.tab_panels(run_tabs, value=active_tab).classes("w-full"): + with ui.tab_panel(overview_tab) as overview_panel_el: + overview_panel(state) + with ui.tab_panel(sessions_tab) as sessions_panel_el: + dive_panel(state) + with ui.tab_panel(log_tab) as log_panel_el: + run_log_panel(state) + + def on_bench_change(e) -> None: + label = e.value + state.bench_key = bench_label_to_key.get(label) + state.bench_controls = {} + bench_form_panel.refresh() + + def on_agent_change(e) -> None: + label = e.value + state.agent_key = agent_label_to_key.get(label) + agent_form_panel.refresh() + + def on_workers_change(e) -> None: + try: + state.max_workers = int(e.value) + except Exception: + state.max_workers = 1 + + def on_num_tasks_change(e) -> None: + try: + value = int(e.value) + except Exception: + state.num_tasks = None + return + state.num_tasks = value if value > 0 else None + + bench_select.on_value_change(on_bench_change) + agent_select.on_value_change(on_agent_change) + num_tasks_input.on_value_change(on_num_tasks_change) + max_workers_input.on_value_change(on_workers_change) + + show_sessions = state.run_active or bool(state.sessions) + _set_visible(run_panel_box, show_sessions) + _set_visible(overview_tab, show_sessions) + _set_visible(sessions_tab, show_sessions) + _set_visible(log_tab, show_sessions) + _set_visible(overview_panel_el, show_sessions) + _set_visible(sessions_panel_el, show_sessions) + _set_visible(log_panel_el, show_sessions) + + def start_run() -> None: + if state.run_active: + return + if state.bench_key is None or state.agent_key is None: + ui.notify("Please select a benchmark and agent.") + return + + num_tasks_value = None + try: + raw = num_tasks_input.value + if raw is not None: + parsed = int(raw) + if parsed > 0: + num_tasks_value = parsed + except Exception: + num_tasks_value = None + state.num_tasks = num_tasks_value + + benchmark = None + agent = None + try: + bench_cls = load_benchmark_class(state.bench_key) + agent_cls = load_agent_class(state.agent_key) + bench_values = _collect_values(state.bench_controls) + agent_values = _collect_values(state.agent_controls) + benchmark = bench_cls(**bench_values) + agent = agent_cls(**agent_values) + state.planned_sessions = num_tasks_value if num_tasks_value else None + except Exception as exc: + ui.notify(f"Config error: {exc}") + _LOG.info("config error: %s", exc) + return + finally: + if benchmark is not None: + try: + benchmark.close() + except Exception: + _LOG.info("benchmark preview close failed") + if agent is not None: + try: + agent.close() + except Exception: + _LOG.info("agent preview close failed") + + settings = get_settings() + output_dir = settings.output_dir + + state.events = [] + state.sessions = {} + state.turns = {} + state.refresh_needed = True + + ctx_mgr = run_scope(output_dir=output_dir) + ctx = ctx_mgr.__enter__() + dashboard_observer = DashboardEventsObserver() + + state.tracker = dashboard_observer + state.run_id = ctx.run_id + state.run_context_manager = ctx_mgr + state.run_active = True + os.environ["EXGENTIC_MAX_WORKERS"] = str(state.max_workers) + mode = "parallel" if state.max_workers > 1 else "sequential" + _LOG.info( + "starting run bench=%s agent=%s mode=%s workers=%s run_id=%s", + state.bench_key, + state.agent_key, + mode, + state.max_workers, + state.run_id, + ) + + def worker() -> None: + try: + _LOG.info("worker started") + config = RunConfig( + benchmark=state.bench_key, + agent=state.agent_key, + benchmark_kwargs=bench_values, + agent_kwargs=agent_values, + output_dir=output_dir, + max_workers=state.max_workers if state.max_workers > 1 else None, + run_id=state.run_id, + num_tasks=num_tasks_value, + ) + evaluate( + config, + observers=[dashboard_observer], + ) + _LOG.info("worker finished") + except Exception: + _LOG.exception("worker crashed") + + thread = threading.Thread(target=worker, daemon=True) + thread.start() + state.thread = thread + + start_button.on("click", lambda _: start_run()) + + return RunViews( + start_button=start_button, + bench_select=bench_select, + agent_select=agent_select, + num_tasks_input=num_tasks_input, + max_workers_input=max_workers_input, + bench_form=bench_form_panel, + agent_form=agent_form_panel, + overview_panel=overview_panel, + sessions_panel=dive_panel, + run_log_panel=run_log_panel, + leaderboard_panel=leaderboard_panel, + history_panel=history_panel, + run_panel_box=run_panel_box, + overview_tab=overview_tab, + sessions_tab=sessions_tab, + log_tab=log_tab, + overview_panel_el=overview_panel_el, + sessions_panel_el=sessions_panel_el, + log_panel_el=log_panel_el, + ) + + +def build_leaderboard_tab(state: RunState) -> None: + leaderboard_panel(state) + + +def build_history_tab(state: RunState) -> None: + history_panel(state) + + +def refresh_ui(state: RunState, views: RunViews) -> None: + changed = process_new_events(state) + + if state.run_active and state.thread and not state.thread.is_alive(): + state.run_active = False + if state.run_context_manager is not None: + try: + state.run_context_manager.__exit__(None, None, None) + except Exception: + pass + state.run_context_manager = None + state.refresh_needed = True + + _set_enabled(views.start_button, not state.run_active) + _set_enabled(views.bench_select, not state.run_active) + _set_enabled(views.agent_select, not state.run_active) + _set_enabled(views.num_tasks_input, not state.run_active) + _set_enabled(views.max_workers_input, not state.run_active) + _set_controls_enabled(state.bench_controls, not state.run_active) + _set_controls_enabled(state.agent_controls, not state.run_active) + + show_sessions = state.run_active or bool(state.sessions) + _set_visible(views.run_panel_box, show_sessions) + _set_visible(views.overview_tab, show_sessions) + _set_visible(views.sessions_tab, show_sessions) + _set_visible(views.log_tab, show_sessions) + _set_visible(views.overview_panel_el, show_sessions) + _set_visible(views.sessions_panel_el, show_sessions) + _set_visible(views.log_panel_el, show_sessions) + + if state.last_run_active != state.run_active: + views.bench_form.refresh() + views.agent_form.refresh() + state.last_run_active = state.run_active + + if changed: + views.overview_panel.refresh() + views.sessions_panel.refresh() + views.run_log_panel.refresh() + + if state.refresh_needed: + views.leaderboard_panel.refresh() + views.history_panel.refresh() + views.run_log_panel.refresh() + state.refresh_needed = False diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/state.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/state.py new file mode 100644 index 00000000..14683956 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/state.py @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import threading +from dataclasses import dataclass, field +from typing import Any, Optional + +from ....observers.handlers.dashboard_events import DashboardEventsObserver + + +@dataclass +class RunState: + events: list[dict] = field(default_factory=list) + sessions: dict[str, dict] = field(default_factory=dict) + turns: dict[str, list] = field(default_factory=dict) + bench_controls: dict[str, Any] = field(default_factory=dict) + agent_controls: dict[str, Any] = field(default_factory=dict) + tracker: DashboardEventsObserver | None = None + thread: threading.Thread | None = None + run_id: str | None = None + run_context_manager: Any = None + run_active: bool = False + refresh_needed: bool = True + last_run_active: bool = False + bench_key: str | None = None + agent_key: str | None = None + num_tasks: int | None = None + max_workers: int = 3 + selected_session: str | None = None + selected_history_run: str | None = None + selected_history_session: str | None = None + history_root: str = "" + selected_agents: list[str] = field(default_factory=list) + selected_models: list[str] = field(default_factory=list) + selected_benchmarks: list[str] = field(default_factory=list) + selected_subsets: list[str] = field(default_factory=list) + min_tasks: int = 0 + planned_sessions: int | None = None + active_tabs: dict[str, str] = field(default_factory=dict) + tabs_controls: dict[str, Any] = field(default_factory=dict) + tabs_by_scope: dict[str, dict[str, Any]] = field(default_factory=dict) + + +@dataclass +class RunContext: + results: Optional[dict] + config: Optional[dict] + run_meta: dict[str, Any] + planned_sessions: Optional[int] + total_workers: Optional[int] + + +@dataclass +class RunViews: + start_button: Any + bench_select: Any + agent_select: Any + num_tasks_input: Any + max_workers_input: Any + bench_form: Any + agent_form: Any + overview_panel: Any + sessions_panel: Any + run_log_panel: Any + leaderboard_panel: Any + history_panel: Any + run_panel_box: Any + overview_tab: Any + sessions_tab: Any + log_tab: Any + overview_panel_el: Any + sessions_panel_el: Any + log_panel_el: Any + + +SESSION_COLUMNS = [ + {"name": "session", "label": "Session", "field": "session"}, + {"name": "status", "label": "Status", "field": "status"}, + {"name": "steps", "label": "Steps", "field": "steps"}, + {"name": "score", "label": "Score", "field": "score"}, +] + +LEADERBOARD_COLUMNS = [ + {"name": "agent", "label": "Agent", "field": "Agent"}, + {"name": "model", "label": "Model", "field": "Model"}, + {"name": "benchmark", "label": "Benchmark", "field": "Benchmark"}, + {"name": "subset", "label": "Subset", "field": "Subset"}, + {"name": "tasks", "label": "Num Tasks", "field": "Num Tasks"}, + {"name": "score", "label": "Final Score", "field": "Final Score"}, + {"name": "run_cost", "label": "Total Run Cost", "field": "Total Run Cost"}, + {"name": "avg_agent_cost", "label": "Avg Agent Cost", "field": "Avg Agent Cost"}, +] + +TASK_RESULT_COLUMNS = [ + {"name": "session_id", "label": "Session", "field": "session_id"}, + {"name": "task_id", "label": "Task Id", "field": "task_id"}, + {"name": "success", "label": "Success", "field": "success"}, + {"name": "is_finished", "label": "Finished", "field": "is_finished"}, + {"name": "score", "label": "Score", "field": "score"}, + {"name": "steps", "label": "Steps", "field": "steps"}, + {"name": "agent_cost", "label": "Agent Cost", "field": "agent_cost"}, + {"name": "benchmark_cost", "label": "Benchmark Cost", "field": "benchmark_cost"}, + {"name": "execution_time", "label": "Exec Time", "field": "execution_time"}, +] + +ACTION_COLUMNS = [ + {"name": "name", "label": "Action", "field": "name"}, + {"name": "description", "label": "Description", "field": "description"}, + {"name": "is_message", "label": "Message", "field": "is_message"}, + {"name": "is_finish", "label": "Finish", "field": "is_finish"}, +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/status.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/status.py new file mode 100644 index 00000000..4b8ce072 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/dashboard/views/status.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any + +from nicegui import ui + +_STATUS_ORDER = [ + "success", + "unsuccessful", + "unfinished", + "agent error", + "benchmark error", + "cancelled", + "error", + "running", +] + +_STATUS_COLORS = { + "success": "#22c55e", + "unsuccessful": "#f59e0b", + "unfinished": "#facc15", + "agent error": "#ef4444", + "benchmark error": "#f97316", + "cancelled": "#94a3b8", + "error": "#dc2626", + "running": "#38bdf8", +} + + +def _status_from_outcome(success: Any, is_finished: Any, error_source: Any = None) -> str: + if success is True: + return "success" + if is_finished is True: + return "unsuccessful" + if is_finished is False: + return "unfinished" + if error_source == "agent": + return "agent error" + if error_source == "benchmark": + return "benchmark error" + if error_source == "cancelled": + return "cancelled" + return "error" + + +def _status_counts_from_sessions(sessions: dict) -> dict[str, int]: + counts: dict[str, int] = {} + for data in sessions.values(): + status = data.get("status") or "error" + counts[status] = counts.get(status, 0) + 1 + return counts + + +def _render_status_pie(status_counts: dict[str, int]) -> None: + if not status_counts: + ui.label("No session data available.") + return + data = [] + colors = [] + for status in _STATUS_ORDER: + count = status_counts.get(status, 0) + if count: + data.append({"value": count, "name": status}) + colors.append(_STATUS_COLORS.get(status, "#94a3b8")) + if not data: + ui.label("No session data available.") + return + ui.echart( + { + "tooltip": {"trigger": "item"}, + "legend": {"orient": "vertical", "left": "left"}, + "color": colors, + "series": [ + { + "name": "Sessions", + "type": "pie", + "radius": ["35%", "70%"], + "center": ["60%", "55%"], + "label": {"formatter": "{b}: {c} ({d}%)"}, + "data": data, + } + ], + } + ).classes("w-full").style("height: 260px;") diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/__init__.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/__init__.py new file mode 100644 index 00000000..7de8d437 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Public API helpers for interfaces.""" diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/api.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/api.py new file mode 100644 index 00000000..e1147a91 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/lib/api.py @@ -0,0 +1,613 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import inspect +import json +from typing import Any + +from ...core.agent import Agent +from ...core.benchmark import Benchmark +from ...core.orchestrator.run import ( + core_aggregate, + core_evaluate, + core_execute, +) +from ...core.types import RunConfig, RunPlan, RunResults, RunStatus, SessionConfig +from ..registry import ( + apply_subset_kwargs, + get_agent_entries, + get_benchmark_entries, + get_benchmark_subsets, + load_agent, + load_benchmark, +) + + +def list_benchmarks() -> list[dict[str, Any]]: + from ...environment.instance import get_manager + + mgr = get_manager() + entries = get_benchmark_entries() + result = [] + for slug_name, entry in entries.items(): + name = f"benchmarks/{slug_name}" + info = mgr.get_info(name) + installed = info is not None + installed_at = None + if info: + envs = info["environments"] + timestamps = [e["installed_at"] for e in envs.values() if "installed_at" in e] + installed_at = min(timestamps) if timestamps else None + result.append( + { + "slug_name": slug_name, + "display_name": entry.display_name, + "installed": installed, + "installed_at": installed_at, + } + ) + return result + + +def list_agents() -> list[dict[str, Any]]: + from ...environment.instance import get_manager + + mgr = get_manager() + entries = get_agent_entries() + result = [] + for slug_name, entry in entries.items(): + name = f"agents/{slug_name}" + info = mgr.get_info(name) + installed = info is not None + installed_at = None + if info: + envs = info["environments"] + timestamps = [e["installed_at"] for e in envs.values() if "installed_at" in e] + installed_at = min(timestamps) if timestamps else None + result.append( + { + "slug_name": slug_name, + "display_name": entry.display_name, + "installed": installed, + "installed_at": installed_at, + } + ) + return result + + +def load_benchmark_class(benchmark: str) -> type[Benchmark]: + entries = get_benchmark_entries() + if benchmark not in entries: + raise ValueError(f"Unknown benchmark slug '{benchmark}'. Available: {', '.join(sorted(entries.keys()))}") + return load_benchmark(benchmark) + + +def load_agent_class(agent: str) -> type[Agent]: + entries = get_agent_entries() + if agent not in entries: + raise ValueError(f"Unknown agent slug '{agent}'. Available: {', '.join(sorted(entries.keys()))}") + return load_agent(agent) + + +def _run_config_from_session(session_config: SessionConfig) -> RunConfig: + return RunConfig( + benchmark=session_config.benchmark, + agent=session_config.agent, + subset=session_config.subset, + task_ids=[session_config.task_id], + output_dir=session_config.output_dir, + cache_dir=session_config.cache_dir, + run_id=session_config.run_id, + model=session_config.model, + benchmark_kwargs=session_config.benchmark_kwargs, + agent_kwargs=session_config.agent_kwargs, + overwrite_sessions=session_config.overwrite_sessions, + ) + + +def _normalize_run_config( + config: RunConfig | SessionConfig | None, + *, + benchmark: str | Benchmark | None, + agent: str | Agent | None, + subset: str | None, + task_ids: list[str] | None, + num_tasks: int | None, + output_dir: str, + cache_dir: str | None, + run_id: str | None, + model: str | None, + max_workers: int | None, + max_steps: int, + max_actions: int, + overwrite_sessions: bool, + benchmark_kwargs: dict[str, Any] | None, + agent_kwargs: dict[str, Any] | None, +) -> RunConfig: + if config is not None: + if ( + any( + value is not None + for value in ( + benchmark, + agent, + subset, + task_ids, + num_tasks, + cache_dir, + run_id, + model, + max_workers, + benchmark_kwargs, + agent_kwargs, + ) + ) + or overwrite_sessions + or output_dir != "./outputs" + or max_steps != 100 + or max_actions != 100 + ): + raise ValueError("Do not pass run parameters together with config.") + if isinstance(config, SessionConfig): + return _run_config_from_session(config) + return config + if benchmark is None or agent is None: + raise ValueError("benchmark and agent are required.") + + bench_slug: str + bench_kwargs: dict[str, Any] + if isinstance(benchmark, Benchmark): + if benchmark_kwargs is not None or subset is not None: + raise ValueError("Do not pass benchmark args with a benchmark instance.") + bench_slug = benchmark.slug_name + bench_kwargs = benchmark.model_dump() + subset = getattr(benchmark, "subset", None) + else: + bench_slug = benchmark + bench_kwargs = dict(benchmark_kwargs or {}) + + agent_slug: str + agent_cfg: dict[str, Any] + if isinstance(agent, Agent): + if agent_kwargs is not None or model is not None: + raise ValueError("Do not pass agent args with an agent instance.") + agent_slug = agent.slug_name + agent_cfg = agent.model_dump() + else: + agent_slug = agent + agent_cfg = dict(agent_kwargs or {}) + + return RunConfig( + benchmark=bench_slug, + agent=agent_slug, + subset=subset, + task_ids=task_ids, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite_sessions=overwrite_sessions, + benchmark_kwargs=bench_kwargs, + agent_kwargs=agent_cfg, + ) + + +def evaluate( + config: RunConfig | SessionConfig | None = None, + *, + benchmark: str | Benchmark | None = None, + agent: str | Agent | None = None, + subset: str | None = None, + task_ids: list[str] | None = None, + num_tasks: int | None = None, + output_dir: str = "./outputs", + cache_dir: str | None = None, + run_id: str | None = None, + model: str | None = None, + max_workers: int | None = None, + max_steps: int = 100, + max_actions: int = 100, + overwrite_sessions: bool = False, + benchmark_kwargs: dict[str, Any] | None = None, + agent_kwargs: dict[str, Any] | None = None, + observers: list[Any] | None = None, + controllers: list[Any] | None = None, +) -> RunResults: + """Evaluate sessions and aggregate results. + + Accepts either a RunConfig/SessionConfig or benchmark/agent identifiers. + + Args: + config: RunConfig or SessionConfig. When provided, no other run args + may be passed. + benchmark: Benchmark slug or Benchmark instance. + agent: Agent slug or Agent instance. + subset: Benchmark subset name. + task_ids: Explicit task ids to run. + num_tasks: Number of tasks to run. + output_dir: Output root directory. + cache_dir: Cache directory. + run_id: Run id override. + model: Agent model override. + max_workers: Parallel workers. + max_steps: Max steps per session. + max_actions: Max actions per session. + overwrite_sessions: Overwrite existing session artifacts. + benchmark_kwargs: Benchmark kwargs (when benchmark is a slug). + agent_kwargs: Agent kwargs (when agent is a slug). + observers: Optional observers. + controllers: Optional controllers. + + Returns: + RunResults: Aggregated run results. + + Raises: + ValueError: If config is combined with other run args or if instance + args are mixed with kwargs. + """ + run_config = _normalize_run_config( + config, + benchmark=benchmark, + agent=agent, + subset=subset, + task_ids=task_ids, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite_sessions=overwrite_sessions, + benchmark_kwargs=benchmark_kwargs, + agent_kwargs=agent_kwargs, + ) + return core_evaluate( + run_config=run_config, + observers=observers, + controllers=controllers, + ) + + +def execute( + config: RunConfig | SessionConfig | None = None, + *, + benchmark: str | Benchmark | None = None, + agent: str | Agent | None = None, + subset: str | None = None, + task_ids: list[str] | None = None, + num_tasks: int | None = None, + output_dir: str = "./outputs", + cache_dir: str | None = None, + run_id: str | None = None, + model: str | None = None, + max_workers: int | None = None, + max_steps: int = 100, + max_actions: int = 100, + overwrite_sessions: bool = False, + benchmark_kwargs: dict[str, Any] | None = None, + agent_kwargs: dict[str, Any] | None = None, + observers: list[Any] | None = None, + controllers: list[Any] | None = None, +) -> RunResults: + """Run sessions without aggregation. + + Accepts either a RunConfig/SessionConfig or benchmark/agent identifiers. + + Args: + config: RunConfig or SessionConfig. When provided, no other run args + may be passed. + benchmark: Benchmark slug or Benchmark instance. + agent: Agent slug or Agent instance. + subset: Benchmark subset name. + task_ids: Explicit task ids to run. + num_tasks: Number of tasks to run. + output_dir: Output root directory. + cache_dir: Cache directory. + run_id: Run id override. + model: Agent model override. + max_workers: Parallel workers. + max_steps: Max steps per session. + max_actions: Max actions per session. + overwrite_sessions: Overwrite existing session artifacts. + benchmark_kwargs: Benchmark kwargs (when benchmark is a slug). + agent_kwargs: Agent kwargs (when agent is a slug). + observers: Optional observers. + controllers: Optional controllers. + + Returns: + RunResults: Run results without aggregation. + + Raises: + ValueError: If config is combined with other run args or if instance + args are mixed with kwargs. + """ + run_config = _normalize_run_config( + config, + benchmark=benchmark, + agent=agent, + subset=subset, + task_ids=task_ids, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite_sessions=overwrite_sessions, + benchmark_kwargs=benchmark_kwargs, + agent_kwargs=agent_kwargs, + ) + return core_execute( + run_config=run_config, + observers=observers, + controllers=controllers, + ) + + +def aggregate( + config: RunConfig | SessionConfig | None = None, + *, + benchmark: str | Benchmark | None = None, + agent: str | Agent | None = None, + subset: str | None = None, + task_ids: list[str] | None = None, + num_tasks: int | None = None, + output_dir: str = "./outputs", + cache_dir: str | None = None, + run_id: str | None = None, + model: str | None = None, + max_workers: int | None = None, + max_steps: int = 100, + max_actions: int = 100, + overwrite_sessions: bool = False, + benchmark_kwargs: dict[str, Any] | None = None, + agent_kwargs: dict[str, Any] | None = None, + observers: list[Any] | None = None, + controllers: list[Any] | None = None, +) -> RunResults: + """Aggregate results from completed sessions. + + Accepts either a RunConfig/SessionConfig or benchmark/agent identifiers. + + Args: + config: RunConfig or SessionConfig. When provided, no other run args + may be passed. + benchmark: Benchmark slug or Benchmark instance. + agent: Agent slug or Agent instance. + subset: Benchmark subset name. + task_ids: Explicit task ids to run. + num_tasks: Number of tasks to run. + output_dir: Output root directory. + cache_dir: Cache directory. + run_id: Run id override. + model: Agent model override. + max_workers: Parallel workers. + max_steps: Max steps per session. + max_actions: Max actions per session. + overwrite_sessions: Overwrite existing session artifacts. + benchmark_kwargs: Benchmark kwargs (when benchmark is a slug). + agent_kwargs: Agent kwargs (when agent is a slug). + observers: Optional observers. + controllers: Optional controllers. + + Returns: + RunResults: Aggregated run results. + + Raises: + ValueError: If config is combined with other run args or if instance + args are mixed with kwargs. + """ + run_config = _normalize_run_config( + config, + benchmark=benchmark, + agent=agent, + subset=subset, + task_ids=task_ids, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite_sessions=overwrite_sessions, + benchmark_kwargs=benchmark_kwargs, + agent_kwargs=agent_kwargs, + ) + return core_aggregate( + run_config=run_config, + observers=observers, + controllers=controllers, + ) + + +def status( + config: RunConfig | SessionConfig | None = None, + *, + benchmark: str | Benchmark | None = None, + agent: str | Agent | None = None, + subset: str | None = None, + task_ids: list[str] | None = None, + num_tasks: int | None = None, + output_dir: str = "./outputs", + cache_dir: str | None = None, + run_id: str | None = None, + model: str | None = None, + max_workers: int | None = None, + max_steps: int = 100, + max_actions: int = 100, + overwrite_sessions: bool = False, + benchmark_kwargs: dict[str, Any] | None = None, + agent_kwargs: dict[str, Any] | None = None, +) -> RunStatus: + run_config = _normalize_run_config( + config, + benchmark=benchmark, + agent=agent, + subset=subset, + task_ids=task_ids, + num_tasks=num_tasks, + output_dir=output_dir, + cache_dir=cache_dir, + run_id=run_id, + model=model, + max_workers=max_workers, + max_steps=max_steps, + max_actions=max_actions, + overwrite_sessions=overwrite_sessions, + benchmark_kwargs=benchmark_kwargs, + agent_kwargs=agent_kwargs, + ) + return RunStatus.from_config(run_config) + + +def preview(config: RunConfig) -> RunPlan: + status = RunStatus.from_config(config) + return RunPlan.from_config_and_status( + config, + status, + ) + + +def results(config: RunConfig) -> RunResults: + from ...core.context import get_context + from ...utils.paths import RunPaths + + with config.get_context(): + results_path = RunPaths.from_context(get_context()).results + if not results_path.exists(): + raise ValueError(f"Run results not found at {results_path}.") + payload = json.loads(results_path.read_text(encoding="utf-8")) + return RunResults.model_validate(payload) + + +def get_benchmark_info(benchmark: str) -> dict[str, Any]: + entries = get_benchmark_entries() + entry = entries.get(benchmark) + if entry is None: + raise ValueError(f"Unknown benchmark slug '{benchmark}'. Available: {', '.join(sorted(entries.keys()))}") + bench_cls = load_benchmark_class(benchmark) + return { + "slug_name": entry.slug_name, + "display_name": entry.display_name, + "subsets": list(entry.subsets), + "subset_arg": entry.subset_arg, + "task_ids_arg": entry.task_ids_arg, + "task_id_type": entry.task_id_type, + "kwargs": _describe_init_args(bench_cls), + } + + +def get_agent_info(agent: str) -> dict[str, Any]: + entries = get_agent_entries() + entry = entries.get(agent) + if entry is None: + raise ValueError(f"Unknown agent slug '{agent}'. Available: {', '.join(sorted(entries.keys()))}") + agent_cls = load_agent_class(agent) + return { + "slug_name": entry.slug_name, + "display_name": entry.display_name, + "kwargs": _describe_init_args(agent_cls), + } + + +def list_subsets(benchmark: str) -> list[str]: + benchmark_entries = get_benchmark_entries() + if benchmark not in benchmark_entries: + raise ValueError( + f"Unknown benchmark slug '{benchmark}'. Available: {', '.join(sorted(benchmark_entries.keys()))}" + ) + return get_benchmark_subsets(benchmark) + + +def list_tasks( + *, + benchmark: str, + subset: str | None = None, + benchmark_kwargs: dict[str, Any] | None = None, +) -> list[str]: + benchmark_entries = get_benchmark_entries() + if benchmark not in benchmark_entries: + raise ValueError( + f"Unknown benchmark slug '{benchmark}'. Available: {', '.join(sorted(benchmark_entries.keys()))}" + ) + bench_kwargs = dict(benchmark_kwargs or {}) + if subset is not None: + bench_kwargs = apply_subset_kwargs(benchmark, subset, bench_kwargs) + bench_cls = load_benchmark_class(benchmark) + benchmark_obj: Benchmark = bench_cls(**bench_kwargs) + evaluator = benchmark_obj.get_evaluator() + try: + try: + return evaluator.list_tasks() + except NotImplementedError as exc: + raise ValueError(str(exc)) from exc + finally: + try: + evaluator.close() + except Exception: + pass + benchmark_obj.close() + + +def needs_setup(name: str, kind: str) -> bool: + """Return True if a benchmark/agent has a setup.sh or requirements.txt.""" + from ...environment.helpers import find_package_file + + entries = get_benchmark_entries() if kind == "benchmark" else get_agent_entries() + entry = entries.get(name) + if entry is None: + return False + return ( + find_package_file(entry.module, "setup.sh") is not None + or find_package_file(entry.module, "requirements.txt") is not None + ) + + +def _describe_init_args(cls: type) -> list[str]: + model_fields = getattr(cls, "model_fields", None) + if model_fields: + names = set(model_fields.keys()) + for field in model_fields.values(): + alias = getattr(field, "alias", None) + if alias and alias not in names: + names.add(alias) + return sorted(names) + try: + sig = inspect.signature(cls.__init__) + except (TypeError, ValueError): + return [] + args = [] + for name, param in sig.parameters.items(): + if name == "self": + continue + if param.kind == param.VAR_KEYWORD: + args.append("**kwargs") + continue + args.append(name) + return args + + +__all__ = [ + "aggregate", + "evaluate", + "execute", + "list_agents", + "list_benchmarks", + "list_subsets", + "list_tasks", + "preview", + "results", + "status", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/interfaces/registry.py b/labs/AgentStream/exgentic/src/exgentic/interfaces/registry.py new file mode 100644 index 00000000..089f3c31 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/interfaces/registry.py @@ -0,0 +1,352 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import importlib +import importlib.util +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel + +if TYPE_CHECKING: + from ..core.agent import Agent + from ..core.benchmark import Benchmark + + +@dataclass(frozen=True) +class RegistryEntry: + slug_name: str + display_name: str + module: str + attr: str + kind: str + subsets: tuple[str, ...] = () + subset_arg: str | None = None + task_ids_arg: str | None = None + task_id_type: str | None = None + + def is_available(self) -> bool: + return importlib.util.find_spec(self.module) is not None + + def load(self) -> type: + try: + module = importlib.import_module(self.module) + except Exception as exc: + raise ImportError(f"Failed to import {self.kind} '{self.slug_name}' from {self.module}: {exc}") from exc + try: + return getattr(module, self.attr) + except AttributeError as exc: + raise ImportError(f"Missing {self.kind} class '{self.attr}' in {self.module}") from exc + + +BENCHMARKS: dict[str, RegistryEntry] = { + "bfcl": RegistryEntry( + slug_name="bfcl", + display_name="BFCL", + module="exgentic.benchmarks.bfcl.bfcl_benchmark", + attr="BFCLBenchmark", + kind="benchmark", + subsets=( + "simple_python", + "simple_java", + "simple_javascript", + "multiple", + "parallel", + "parallel_multiple", + "irrelevance", + "live_simple", + "live_multiple", + "live_parallel", + "live_parallel_multiple", + "live_irrelevance", + "live_relevance", + "multi_turn_base", + "multi_turn_long_context", + "multi_turn_miss_func", + "multi_turn_miss_param", + ), + subset_arg="subset", + task_id_type="str", + ), + "tau2": RegistryEntry( + slug_name="tau2", + display_name="Tau Bench 2", + module="exgentic.benchmarks.tau2.tau2_benchmark", + attr="TAU2Benchmark", + kind="benchmark", + subsets=("mock", "retail", "airline", "telecom"), + subset_arg="subset", + task_id_type="int", + ), + "appworld": RegistryEntry( + slug_name="appworld", + display_name="AppWorld", + module="exgentic.benchmarks.appworld.appworld_benchmark", + attr="AppWorldBenchmark", + kind="benchmark", + subsets=("train", "dev", "test_normal", "test_challenge"), + subset_arg="subset", + task_id_type="str", + ), + "gsm8k": RegistryEntry( + slug_name="gsm8k", + display_name="GSM8k", + module="exgentic.benchmarks.gsm8k.gsm8k_benchmark", + attr="GSM8kBenchmark", + kind="benchmark", + subsets=("main",), + subset_arg="subset", + task_id_type="int", + ), + "hle": RegistryEntry( + slug_name="hle", + display_name="HLE", + module="exgentic.benchmarks.hle.hle_benchmark", + attr="HLEBenchmark", + kind="benchmark", + subsets=("test",), + subset_arg="subset", + task_id_type="int", + ), + "hotpotqa": RegistryEntry( + slug_name="hotpotqa", + display_name="HotpotQA", + module="exgentic.benchmarks.hotpotqa.hotpotqa_benchmark", + attr="HotpotQABenchmark", + kind="benchmark", + subsets=("distractor",), + subset_arg="subset", + task_id_type="int", + ), + "browsecompplus": RegistryEntry( + slug_name="browsecompplus", + display_name="BrowseCompPlus", + module="exgentic.benchmarks.browsecompplus.browsecomp_benchmark", + attr="BrowseCompPlusBenchmark", + kind="benchmark", + subsets=("main",), + subset_arg="subset", + task_id_type="int", + ), + "swebench": RegistryEntry( + slug_name="swebench", + display_name="SWE-bench", + module="exgentic.benchmarks.swebench.swebench_benchmark", + attr="SWEBenchBenchmark", + kind="benchmark", + subsets=(), + subset_arg="subset", + task_id_type="str", + ), +} + +AGENTS: dict[str, RegistryEntry] = { + "tool_calling": RegistryEntry( + slug_name="tool_calling", + display_name="LiteLLM Tool Calling", + module="exgentic.agents.litellm_tool_calling.litellm_tool_calling_agent", + attr="LiteLLMToolCallingAgent", + kind="agent", + ), + "smolagents_tool": RegistryEntry( + slug_name="smolagents_tool", + display_name="SmolAgents Tool Calling", + module="exgentic.agents.smolagents.tool_calling_agent", + attr="SmolagentToolCallingAgent", + kind="agent", + ), + "smolagents_code": RegistryEntry( + slug_name="smolagents_code", + display_name="SmolAgents Code", + module="exgentic.agents.smolagents.code_agent", + attr="SmolagentCodeAgent", + kind="agent", + ), + "openai_solo": RegistryEntry( + slug_name="openai_solo", + display_name="OpenAI Solo", + module="exgentic.agents.openai.openai_mcp_agent", + attr="OpenAIMCPAgent", + kind="agent", + ), + "claude_code": RegistryEntry( + slug_name="claude_code", + display_name="Claude Code CLI", + module="exgentic.agents.cli.claude.agent", + attr="ClaudeCodeAgent", + kind="agent", + ), + "codex_cli": RegistryEntry( + slug_name="codex_cli", + display_name="Codex CLI", + module="exgentic.agents.cli.codex.agent", + attr="CodexAgent", + kind="agent", + ), + "gemini_cli": RegistryEntry( + slug_name="gemini_cli", + display_name="Gemini CLI", + module="exgentic.agents.cli.gemini.agent", + attr="GeminiAgent", + kind="agent", + ), + "ace": RegistryEntry( + slug_name="ace", + display_name="ACE Agent", + module="exgentic.agents.ace.ace_agent", + attr="ACEAgent", + kind="agent", + ), + "a_mem": RegistryEntry( + slug_name="a_mem", + display_name="A-Mem Agent", + module="exgentic.agents.a_mem.a_mem_agent", + attr="AMemAgent", + kind="agent", + ), + "reasoning_bank": RegistryEntry( + slug_name="reasoning_bank", + display_name="ReasoningBank Agent", + module="exgentic.agents.reasoning_bank.rb_agent", + attr="ReasoningBankAgent", + kind="agent", + ), + "autoskill": RegistryEntry( + slug_name="autoskill", + display_name="AutoSkill Agent", + module="exgentic.agents.autoskill.autoskill_agent", + attr="AutoSkillAgent", + kind="agent", + ), + "harness": RegistryEntry( + slug_name="harness", + display_name="Harness Agent", + module="exgentic.agents.harness.harness_agent", + attr="HarnessAgent", + kind="agent", + ), +} + + +def get_benchmark_entries() -> dict[str, RegistryEntry]: + return dict(BENCHMARKS) + + +def get_agent_entries() -> dict[str, RegistryEntry]: + return dict(AGENTS) + + +def get_benchmark_subsets(slug_name: str) -> list[str]: + entry = BENCHMARKS.get(slug_name) + if entry is None: + raise KeyError(f"Unknown benchmark slug '{slug_name}'") + return list(entry.subsets) + + +def get_benchmark_subset_arg(slug_name: str) -> str | None: + entry = BENCHMARKS.get(slug_name) + if entry is None: + raise KeyError(f"Unknown benchmark slug '{slug_name}'") + return entry.subset_arg + + +def apply_subset_kwargs(slug_name: str, subset: str | None, kwargs: dict[str, Any]) -> dict[str, Any]: + if subset is None: + return kwargs + subsets = get_benchmark_subsets(slug_name) + if subsets and subset not in subsets: + raise ValueError(f"Unknown subset '{subset}' for '{slug_name}'. Available: {', '.join(subsets)}") + subset_arg = get_benchmark_subset_arg(slug_name) + if subset_arg: + if subset_arg in kwargs and kwargs[subset_arg] != subset: + raise ValueError(f"Conflicting subset selection: {subset_arg}={kwargs[subset_arg]} but subset={subset}") + merged = dict(kwargs) + merged[subset_arg] = subset + return merged + if subsets and subset != subsets[0]: + raise ValueError( + f"Benchmark '{slug_name}' does not support subset selection; default subset is '{subsets[0]}'." + ) + return kwargs + + +def apply_task_kwargs(slug_name: str, tasks: list[str] | None, kwargs: dict[str, Any]) -> dict[str, Any]: + if not tasks: + return kwargs + entry = BENCHMARKS.get(slug_name) + if entry is None: + raise KeyError(f"Unknown benchmark slug '{slug_name}'") + if not entry.task_ids_arg: + raise ValueError(f"Benchmark '{slug_name}' does not support task filtering.") + if entry.task_id_type == "int": + try: + coerced = [int(v) for v in tasks] + except Exception as exc: + raise ValueError(f"Invalid task for '{slug_name}': {tasks}. Expected integers.") from exc + else: + coerced = [str(v) for v in tasks] + if entry.task_ids_arg in kwargs and kwargs[entry.task_ids_arg] != coerced: + raise ValueError( + f"Conflicting task selection: {entry.task_ids_arg}={kwargs[entry.task_ids_arg]} but tasks={coerced}" + ) + merged = dict(kwargs) + merged[entry.task_ids_arg] = coerced + return merged + + +def load_benchmark(slug_name: str) -> type[Benchmark]: + entry = BENCHMARKS.get(slug_name) + if entry is None: + raise KeyError(f"Unknown benchmark slug '{slug_name}'") + cls = entry.load() + _validate_entry(entry, cls) + return cls # type: ignore[return-value] + + +def load_agent(slug_name: str) -> type[Agent]: + entry = AGENTS.get(slug_name) + if entry is None: + raise KeyError(f"Unknown agent slug '{slug_name}'") + cls = entry.load() + _validate_entry(entry, cls) + return cls # type: ignore[return-value] + + +def _validate_entry(entry: RegistryEntry, cls: type) -> None: + try: + slug = cls.slug_name + except AttributeError as exc: + raise ValueError(f"{entry.kind} class '{entry.attr}' is missing slug_name") from exc + if str(slug) != entry.slug_name: + raise ValueError( + f"{entry.kind} slug mismatch: registry '{entry.slug_name}' " + f"!= class '{slug}' for {entry.module}.{entry.attr}" + ) + try: + display = cls.display_name + except AttributeError as exc: + raise ValueError(f"{entry.kind} class '{entry.attr}' is missing display_name") from exc + if str(display) != entry.display_name: + raise ValueError( + f"{entry.kind} display_name mismatch: registry '{entry.display_name}' " + f"!= class '{display}' for {entry.module}.{entry.attr}" + ) + if not issubclass(cls, BaseModel): + raise TypeError(f"{entry.kind} class '{entry.attr}' must be a Pydantic BaseModel.") + + +__all__ = [ + "AGENTS", + "BENCHMARKS", + "RegistryEntry", + "apply_subset_kwargs", + "apply_task_kwargs", + "get_agent_entries", + "get_benchmark_entries", + "get_benchmark_subset_arg", + "get_benchmark_subsets", + "load_agent", + "load_benchmark", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/__init__.py b/labs/AgentStream/exgentic/src/exgentic/observers/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/__init__.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/__init__.py new file mode 100644 index 00000000..ff5bac8c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Observer handlers for filesystem, console, and dashboard outputs.""" diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/configs.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/configs.py new file mode 100644 index 00000000..d35a212e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/configs.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import threading +from typing import Any, Optional + +from ...core.orchestrator.observer import Observer + + +class ConfigsObserver(Observer): + def __init__(self, run_id: str | None = None) -> None: + super().__init__(run_id) + self._lock = threading.Lock() + self._run_config: Optional[Any] = None + + def on_run_start(self, run_config) -> None: + with self._lock: + self._run_config = run_config + self._write_config(run_config) + + def on_run_success(self, results, run_config) -> None: + with self._lock: + self._run_config = run_config + self._write_config(run_config) + + def on_run_error(self, error) -> None: + with self._lock: + run_config = self._run_config + if run_config is None: + return + self._write_config(run_config) + + def _write_config(self, run_config) -> None: + rp = self.paths + config_path = rp.config + try: + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w", encoding="utf-8") as f: + json.dump( + run_config.model_dump(mode="json"), + f, + ensure_ascii=False, + indent=2, + ) + except OSError: + # Read-only runs should still be able to aggregate without persisting. + return diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/dashboard_events.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/dashboard_events.py new file mode 100644 index 00000000..c4ea8e0e --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/dashboard_events.py @@ -0,0 +1,360 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import threading +import time +from collections import deque +from queue import Queue +from typing import Any + +from pydantic import BaseModel + +from ...core.agent import Agent +from ...core.context import get_context +from ...core.orchestrator.observer import Observer +from ...core.orchestrator.termination import ( + AgentError, + BenchmarkError, + RunCancelError, + SessionCancelError, +) +from ...core.types import BenchmarkResults, SessionScore +from ...utils.paths import RunPaths + + +class DashboardEventsObserver(Observer): + """Emit dashboard-friendly run events to a queue.""" + + def __init__(self) -> None: + self.events: Queue[dict[str, Any]] = Queue(maxsize=10000) + self._thread_session: dict[int, str] = {} + self._session_steps: dict[str, int] = {} + self._session_started_at: dict[str, float] = {} + self._session_agents: dict[str, Agent] = {} + self._events_lock = threading.Lock() + self._pending_events: deque = deque() + self._last_batch_time = time.time() + self._batch_interval = 0.02 + self._pending_run_meta = True + + def _emit(self, etype: str, **payload: Any) -> None: + evt = {"type": etype, "ts": time.time(), **payload} + + current_time = time.time() + + if etype in ("session_started", "session_finished", "run_meta", "saved"): + try: + self.events.put_nowait(evt) + except Exception: + try: + self.events.get_nowait() + self.events.put_nowait(evt) + except Exception: + pass + return + + with self._events_lock: + if len(self._pending_events) >= 100: + self._pending_events.popleft() + self._pending_events.append(evt) + + if current_time - self._last_batch_time >= self._batch_interval or len(self._pending_events) >= 10: + self._flush_batch() + self._last_batch_time = current_time + + def _flush_batch(self) -> None: + while self._pending_events: + try: + evt = self._pending_events.popleft() + self.events.put_nowait(evt) + except Exception: + try: + self.events.get_nowait() + self.events.put_nowait(evt) + except Exception: + break + + def on_run_start(self, run_config) -> None: + self._emit_run_meta() + + def on_session_start(self, session, agent: Agent, observation) -> None: + sid = session.session_id + tid = threading.get_ident() + with self._events_lock: + if tid is not None: + self._thread_session[int(tid)] = sid + self._session_steps[sid] = 0 + self._session_started_at[sid] = time.time() + self._session_agents[sid] = agent + self._emit("session_started", session_id=sid) + + def on_react_success(self, session, action) -> None: + step_n = None + sid = None + tid = threading.get_ident() + if tid is not None: + with self._events_lock: + sid = self._thread_session.get(int(tid)) + if sid: + step_n = self._session_steps.get(sid, 0) + 1 + self._session_steps[sid] = step_n + + def _action_summary(a: Any) -> str: + try: + from ...core.types import ParallelAction, SingleAction + + if isinstance(a, SingleAction): + return f"{a.name}"[:100] + if isinstance(a, ParallelAction): + return f"parallel[{len(a.actions)} actions]" + except Exception: + pass + return str(a)[:100] + + def _action_obj(a: Any): + try: + from ...core.types import ParallelAction, SingleAction + + if isinstance(a, SingleAction): + args = a.arguments.model_dump() + if isinstance(args, dict) and len(str(args)) > 500: + args = {k: (v if len(str(v)) < 50 else f"{str(v)[:50]}...") for k, v in list(args.items())[:5]} + return {"type": "single", "name": a.name, "arguments": args} + if isinstance(a, ParallelAction): + items = [] + for i, x in enumerate(a.actions[:5]): + try: + args = x.arguments.model_dump() + if isinstance(args, dict) and len(str(args)) > 200: + args = { + k: (v if len(str(v)) < 30 else f"{str(v)[:30]}...") + for k, v in list(args.items())[:3] + } + items.append({"name": x.name, "arguments": args}) + except Exception: + items.append(str(x)[:100]) + if i >= 4: + break + return {"type": "parallel", "actions": items} + except Exception: + return None + return None + + if sid: + agent_cost, benchmark_cost = self._get_cost_snapshot(sid, session) + execution_time = self._get_execution_time(sid) + self._emit( + "step", + event="action", + session_id=sid, + n=step_n, + action=_action_summary(action), + action_obj=_action_obj(action), + execution_time=execution_time, + agent_cost=agent_cost, + benchmark_cost=benchmark_cost, + ) + + def on_step_success(self, session, observation) -> None: + sid = None + tid = threading.get_ident() + if tid is not None: + with self._events_lock: + sid = self._thread_session.get(int(tid)) + + def _safe_json(obj: Any) -> str: + try: + if isinstance(obj, BaseModel): + data = obj.model_dump() + if isinstance(data, dict) and len(str(data)) > 1000: + truncated = { + k: (v if len(str(v)) < 100 else f"{str(v)[:100]}...") for k, v in list(data.items())[:10] + } + return json.dumps(truncated, ensure_ascii=False) + return json.dumps(data, ensure_ascii=False) + obj_str = str(obj) + if len(obj_str) > 1000: + obj_str = obj_str[:1000] + "..." + return json.dumps(obj_str, default=str, ensure_ascii=False) + except Exception: + return str(obj)[:500] + + def _obs_obj(o: Any): + try: + if isinstance(o, BaseModel): + data = o.model_dump() + if isinstance(data, dict) and len(str(data)) > 1000: + truncated = { + k: (v if len(str(v)) < 100 else f"{str(v)[:100]}...") for k, v in list(data.items())[:10] + } + return truncated + return data + safe_json = _safe_json(o) + return json.loads(safe_json) + except Exception: + return None + + if sid: + step_n = self._session_steps.get(sid, 0) + agent_cost, benchmark_cost = self._get_cost_snapshot(sid, session) + execution_time = self._get_execution_time(sid) + self._emit( + "observation", + event="observation", + session_id=sid, + step=step_n, + observation=_obs_obj(observation), + initial=False, + execution_time=execution_time, + agent_cost=agent_cost, + benchmark_cost=benchmark_cost, + ) + + def on_session_success(self, session, score: SessionScore, agent: Agent) -> None: + sid = session.session_id + self._flush_pending() + self._cleanup_thread_session() + success = bool(score.success) + value = score.score + details = score.model_dump() + with self._events_lock: + steps = self._session_steps.pop(sid, 0) + started_at = self._session_started_at.pop(sid, None) + self._session_agents.pop(sid, None) + execution_time = time.time() - started_at if started_at is not None else None + agent_cost = 0.0 + benchmark_cost = 0.0 + if agent is not None: + try: + report = agent.get_cost() + agent_cost = float(report.total_cost) + except Exception: + agent_cost = 0.0 + try: + report = session.get_cost() + benchmark_cost = float(report.total_cost) + except Exception: + benchmark_cost = 0.0 + self._emit( + "session_finished", + session_id=sid, + success=success, + score=value, + details=details, + steps=steps, + execution_time=execution_time, + agent_cost=agent_cost, + benchmark_cost=benchmark_cost, + is_finished=score.is_finished, + ) + + def on_session_error(self, session, error) -> None: + sid = session.session_id + self._flush_pending() + self._cleanup_thread_session() + error_source = None + if isinstance(error, AgentError): + error_source = "agent" + elif isinstance(error, BenchmarkError): + error_source = "benchmark" + elif isinstance(error, (SessionCancelError, RunCancelError, KeyboardInterrupt)): + error_source = "cancelled" + root_error = error.error if isinstance(error, (AgentError, BenchmarkError)) else None + error_message = str(root_error) if root_error else str(error) + details = {"error": error_message} + if error_source is not None: + details["error_source"] = error_source + with self._events_lock: + steps = self._session_steps.pop(sid, 0) + started_at = self._session_started_at.pop(sid, None) + self._session_agents.pop(sid, None) + execution_time = time.time() - started_at if started_at is not None else None + agent_cost = 0.0 + benchmark_cost = 0.0 + try: + report = session.get_cost() + benchmark_cost = float(report.total_cost) + except Exception: + benchmark_cost = 0.0 + self._emit( + "session_finished", + session_id=sid, + success=False, + score=None, + details=details, + steps=steps, + execution_time=execution_time, + agent_cost=agent_cost, + benchmark_cost=benchmark_cost, + is_finished=None, + error_source=error_source, + ) + + def _get_execution_time(self, session_id: str) -> float | None: + with self._events_lock: + started_at = self._session_started_at.get(session_id) + if started_at is None: + return None + return time.time() - started_at + + def _get_cost_snapshot(self, session_id: str, session) -> tuple[float, float]: + with self._events_lock: + agent = self._session_agents.get(session_id) + agent_cost = 0.0 + benchmark_cost = 0.0 + if agent is not None: + try: + report = agent.get_cost() + agent_cost = float(report.total_cost) + except Exception: + agent_cost = 0.0 + try: + report = session.get_cost() + benchmark_cost = float(report.total_cost) + except Exception: + benchmark_cost = 0.0 + return agent_cost, benchmark_cost + + def on_run_success(self, results: BenchmarkResults, run_config) -> None: + payload = results.model_dump() + self._emit("benchmark_recorded", results=payload) + self._emit_saved_from_context() + + def on_run_error(self, error) -> None: + self._emit_saved_from_context() + + def emit_saved(self, path: str) -> None: + self._flush_pending() + self._emit("saved", path=path) + + def _flush_pending(self) -> None: + with self._events_lock: + if self._pending_events: + self._flush_batch() + + def _cleanup_thread_session(self) -> None: + tid = threading.get_ident() + if tid is not None: + with self._events_lock: + self._thread_session.pop(int(tid), None) + + def _emit_run_meta(self) -> None: + if not self._pending_run_meta: + return + try: + run_id = get_context().run_id + except RuntimeError: + return + self._emit("run_meta", run_id=run_id) + self._pending_run_meta = False + + def _emit_saved_from_context(self) -> None: + try: + ctx = get_context() + except RuntimeError: + return + results_path = RunPaths.from_context(ctx).results + self.emit_saved(str(results_path)) diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/file_logger.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/file_logger.py new file mode 100644 index 00000000..e74ecfff --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/file_logger.py @@ -0,0 +1,337 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import threading +import time +from typing import Dict + +from ...core.orchestrator.observer import Observer +from ...core.orchestrator.termination import ( + AgentError, + BenchmarkError, + InvalidActionError, + InvalidObservationError, + RunCancelError, + SessionCancelError, +) +from ...core.types import Action, Observation, SessionResults, SessionScore +from ...interfaces.registry import get_agent_entries, get_benchmark_entries +from ..logging import get_logger +from .session_ledger import SessionLedger + + +class FileLoggerObserver(Observer): + def __init__( + self, + run_id: str | None = None, + *, + console: bool = False, + logger=None, + ) -> None: + super().__init__(run_id) + self._logger = logger + self._console = console + self._lock = threading.Lock() + self._ledger = SessionLedger() + self._session_reasons: Dict[str, str] = {} + + def _ensure_logger(self) -> None: + if self._logger is not None: + return + rp = self.paths + log_path = rp.tracker + self._logger = get_logger( + f"tracker.{self._run_id}", + str(log_path), + console=self._console, + propagate=False, + ) + + def on_run_start(self, run_config) -> None: + self._ensure_logger() + run_id = self._run_id + bench_entry = get_benchmark_entries().get(run_config.benchmark) + agent_entry = get_agent_entries().get(run_config.agent) + bench_name = bench_entry.display_name if bench_entry is not None else run_config.benchmark + agent_name = agent_entry.display_name if agent_entry is not None else run_config.agent + model_value = run_config.model or (run_config.agent_kwargs or {}).get("model") + model_names = [str(model_value)] if model_value else None + models_text = ", ".join(model_names) if model_names else "" + if run_id is not None: + self._logger.info("==== Exgentic Run %s ====", run_id) + if models_text: + self._logger.info( + "Agent: %s (%s) | Benchmark: %s", + agent_name, + models_text, + bench_name, + ) + else: + self._logger.info( + "Agent: %s | Benchmark: %s", + agent_name, + bench_name, + ) + + def on_run_success(self, results, run_config) -> None: + self._log_save() + + def on_run_error(self, error) -> None: + self._log_error("run", None, error) + self._log_save() + + def on_session_start(self, session, agent, observation) -> None: + self._ensure_logger() + session_id = session.session_id + session_number = self._ledger.register(session_id) + self._logger.info("▶️ Starting Session %s (logs: %s)", session_number, session.paths.root) + if isinstance(observation, Observation): + self._logger.info("⏺️ Recorded Start Session %s", session_number) + + def on_react_success(self, session, action) -> None: + self._ensure_logger() + if action is None: + self._set_reason(session, "ended by agent (agent returned None action)") + return + if not isinstance(action, Action): + self._set_reason( + session, + f"terminated by illegal action returned from agent: {action}", + ) + self._log_error("react", session, InvalidActionError(action)) + return + step_n, session_number = self._step(session) + self._logger.info("⏩ Recorded Step %s Session %s", step_n, session_number) + + def on_step_success(self, session, observation) -> None: + self._ensure_logger() + if observation is not None and not isinstance(observation, Observation): + self._set_reason( + session, + "terminated by illegal observation returned from session: " f"{observation}", + ) + self._log_error("step", session, InvalidObservationError(observation)) + return + if observation is None: + self._set_reason(session, "ended by benchmark") + + def on_react_error(self, session, error) -> None: + self._ensure_logger() + if isinstance(error, InvalidActionError): + self._set_reason( + session, + f"terminated by illegal action returned from agent: {error.action}", + ) + else: + self._set_reason(session, "terminated by agent exception") + + def on_step_error(self, session, error) -> None: + self._ensure_logger() + if isinstance(error, InvalidObservationError): + self._set_reason( + session, + "terminated by illegal observation returned from session: " f"{error.observation}", + ) + else: + self._set_reason(session, "terminated by session exception") + + def on_session_error(self, session, error) -> None: + self._ensure_logger() + error_source = None + if isinstance(error, AgentError): + error_source = "agent" + elif isinstance(error, BenchmarkError): + error_source = "benchmark" + if isinstance(error, (SessionCancelError, RunCancelError, KeyboardInterrupt)): + self._set_reason(session, "cancelled by user", overwrite=True) + error_source = "cancelled" + else: + if error_source == "agent": + reason = "terminated by agent exception" + elif error_source == "benchmark": + reason = "terminated by benchmark exception" + else: + reason = "terminated by unexpected exception (see console)" + self._set_reason(session, reason, overwrite=True) + session_id = session.session_id if session else None + detail = self._format_error_detail(error) + if error_source == "agent": + self._logger.error( + "Agent error in session %s: %s", + session_id or "-", + detail, + ) + elif error_source == "benchmark": + self._logger.error( + "Benchmark error in session %s: %s", + session_id or "-", + detail, + ) + else: + self._logger.error( + "Session error in session %s: %s", + session_id or "-", + detail, + ) + score = SessionScore(score=0, success=False, is_finished=None) + self._log_session(session, score) + + def on_session_success(self, session, score, agent) -> None: + self._ensure_logger() + self._log_session(session, score) + + def on_session_scoring(self, session) -> None: + self._ensure_logger() + session_id = session.session_id + session_number = self._ledger.get_number(session_id) + self._logger.info("⏳ Scoring Session %s (logs: %s)", session_number, session.paths.root) + + def _log_save(self) -> None: + self._ensure_logger() + rp = self.paths + self._logger.info("💾 Saving results to %s", rp.root) + + def _step(self, session) -> tuple[int, int]: + session_id = session.session_id + step_n = self._ledger.increment_steps(session_id) + session_number = self._ledger.get_number(session_id) + return step_n, session_number + + def _log_session(self, session, score: SessionScore) -> None: + session_id = session.session_id + session_number = self._ledger.get_number(session_id) + stats = self._ledger.pop_state(session_id) + execution_time = time.time() - stats.started_at if stats is not None else 0.0 + steps = stats.steps if stats is not None else 0 + reason = self._pop_reason(session) + self._logger.info("⏹️ Session %s %s.", session_number, reason) + + success = bool(score.success) + value = score.score + is_finished = score.is_finished + score_text = f"{value}" + success_emoji = self._success_emoji(success, value, is_finished) + status = self._status_label(success, is_finished, reason) + task_id = session.task_id + task_id_str = f" | task_id: {task_id}" if task_id else "" + self._logger.info( + "%s Completed Session %s | status: %s | score: %s | steps: %s | time: %.1fs%s\n" "logs: %s", + success_emoji, + session_number, + status, + score_text, + steps, + execution_time, + task_id_str, + session.paths.root, + ) + + def _log_error(self, where, session, error) -> None: + self._ensure_logger() + session_id = session.session_id if session else None + if session_id: + self._logger.error( + "error (%s) session=%s: %s", + where, + session_id, + error, + ) + else: + self._logger.error("error (%s): %s", where, error) + + def on_session_reuse(self, session_results: SessionResults) -> None: + self._ensure_logger() + session_id = session_results.session_id + session_number = self._ledger.mark_reuse(session_id) + + reason = "reused existing session" + success = bool(session_results.success) + value = session_results.score + is_finished = session_results.is_finished + score_text = f"{value}" + success_emoji = self._success_emoji(success, value, is_finished) + status = self._status_label(success, is_finished, reason) + task_id = session_results.task_id + task_id_str = f" | task_id: {task_id}" if task_id else "" + execution_time = float(session_results.execution_time or 0.0) + steps = int(session_results.steps or 0) + sess_paths = self.paths.session(session_id) + self._logger.info( + "⏭️ Reused Session %s from existing results.", + session_number, + ) + self._logger.info( + "%s Completed Session %s | status: %s | score: %s | steps: %s | time: %.1fs%s\n" "logs: %s", + success_emoji, + session_number, + status, + score_text, + steps, + execution_time, + task_id_str, + sess_paths.root, + ) + + @staticmethod + def _format_error_detail(error: Exception | None) -> str: + detail = error + if isinstance(error, (AgentError, BenchmarkError)): + detail = error.error + if detail is None: + return "unknown error" + text = str(detail) + if not text: + return type(detail).__name__ + if isinstance(detail, Exception): + return f"{type(detail).__name__}: {text}" + return text + + def _set_reason(self, session, reason: str, *, overwrite: bool = False) -> None: + session_id = session.session_id if session else None + if session_id is None: + return + with self._lock: + if session_id in self._session_reasons and not overwrite: + return + self._session_reasons[session_id] = reason + + def _pop_reason(self, session) -> str: + session_id = session.session_id if session else None + if session_id is None: + return "ended" + with self._lock: + return self._session_reasons.pop(session_id, "ended") + + @staticmethod + def _success_emoji(success: bool, value: float | None, is_finished: bool | None) -> str: + if success: + if value is not None and value == 1.0: + return "✅" + return "☑️ " + if is_finished is True: + return "☑️ " + if is_finished is False: + return "⚠️" + return "❌" + + @staticmethod + def _status_label(success: bool, is_finished: bool | None, reason: str) -> str: + if success: + return "success" + if is_finished is True: + return "unsuccessful" + if is_finished is False: + return "unfinished" + reason_lower = reason.lower() if reason else "" + if "illegal action" in reason_lower or "agent" in reason_lower: + return "agent error" + if ( + "benchmark" in reason_lower + or "session exception" in reason_lower + or "illegal observation" in reason_lower + or "observation returned from session" in reason_lower + ): + return "benchmark error" + if "cancelled" in reason_lower: + return "cancelled" + return "error" diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/logger.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/logger.py new file mode 100644 index 00000000..471b316d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/logger.py @@ -0,0 +1,508 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import logging +import threading +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict + +from rich.columns import Columns +from rich.console import Console +from rich.panel import Panel +from rich.progress import ( + BarColumn, + Progress, + ProgressColumn, + SpinnerColumn, + TextColumn, +) +from rich.table import Table +from rich.text import Text + +from ...core.context import get_context +from ...core.orchestrator.observer import Observer +from ...core.orchestrator.termination import RunCancelError, SessionCancelError +from ...core.types import Action, ModelSettings, SessionOutcomeStatus +from ...utils.paths import RunPaths, get_run_paths, get_session_paths +from ...utils.settings import get_settings +from .recap import RunRecapMixin +from .session_ledger import SessionLedger + + +class _DurationColumn(ProgressColumn): + def render(self, task) -> Text: + duration = task.fields.get("duration") + if isinstance(duration, (int, float)): + elapsed = duration + else: + elapsed = task.finished_time if task.finished else task.elapsed + if elapsed is None: + return Text("-:--:--", style="progress.elapsed") + delta = timedelta(seconds=max(0, int(elapsed))) + return Text(str(delta), style="progress.elapsed") + + +class _CountColumn(ProgressColumn): + def render(self, task) -> Text: + unit = task.fields.get("unit") or "" + hide_total = bool(task.fields.get("hide_total")) + total = task.total + completed = int(task.completed or 0) + if hide_total or unit == "steps": + text = f"{completed} {unit}".strip() + elif total is None: + text = f"{completed} {unit}".strip() + else: + text = f"{completed}/{int(total)} {unit}".strip() + return Text(text, style="progress.remaining") + + +class ConsoleLoggerObserver(Observer, RunRecapMixin): + _MAX_REUSE_DURATION_SECONDS = 7 * 24 * 60 * 60 + _MAX_VISIBLE_SESSIONS = 10 + + def __init__(self, console: Console | None = None) -> None: + self._console = console or Console() + self._lock = threading.Lock() + self._ledger = SessionLedger() + self._start_time: datetime | None = None + self._progress: Progress | None = None + self._run_task_id: int | None = None + self._session_tasks: Dict[str, tuple[int, int | None]] = {} + self._completed_session_tasks: list[int] = [] + self._run_config = None + + def on_run_start(self, run_config) -> None: + if not self._enabled(logging.INFO): + return + self._run_config = run_config + self._start_time = datetime.now() + run_ctx = get_context() + run_id = run_ctx.run_id + + # Display OTEL configuration if enabled + settings = get_settings() + if settings.otel_enabled: + import os + + endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "not set") + protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + service_name = os.getenv("OTEL_SERVICE_NAME", "exgentic") + record_content = "yes" if settings.otel_record_content else "no" + + otel_lines = [ + "[bold cyan]📊 OpenTelemetry Tracing ENABLED[/bold cyan]", + f"[bold]Service Name:[/bold] {service_name}", + f"[bold]Collector:[/bold] {endpoint}", + f"[bold]Protocol:[/bold] {protocol}", + f"[bold]Record Content:[/bold] {record_content}", + ] + otel_body = "\n".join(otel_lines) + self._print(Panel(otel_body, border_style="cyan", padding=(1, 2), title="OpenTelemetry")) + + lines = [f"[bold]Run:[/bold] [cyan]{run_id}[/cyan]"] + overrides = {} + if run_config.max_steps != 100: + overrides["max_steps"] = str(run_config.max_steps) + if run_config.max_actions != 100: + overrides["max_actions"] = str(run_config.max_actions) + if run_config.max_workers is not None: + overrides["max_workers"] = str(run_config.max_workers) + if overrides: + for key in sorted(overrides): + lines.append(f"[bold]{key}:[/bold] {overrides[key]}") + body = "\n".join(lines) + "\n" + title = Text("EXGENTIC", style="bold magenta") + self._print(Panel(body, border_style="magenta", padding=(1, 2), title=title)) + config_panels = self._build_config_panels() + if config_panels: + self._print(config_panels) + self._start_progress(run_config) + + def on_session_start(self, session, agent, observation) -> None: + if not self._enabled(logging.INFO): + return + session_id = session.session_id + session_number = self._ledger.register(session_id) + self._start_session_progress(session_id, session_number, agent, session) + + def on_react_success(self, session, action) -> None: + if not isinstance(action, Action): + return + if not self._enabled(logging.INFO): + return + session_id = session.session_id + self._ledger.increment_steps(session_id) + self._advance_session_progress(session_id) + + def on_session_success(self, session, score, agent) -> None: + if not self._enabled(logging.INFO): + return + session_id = session.session_id + session_number = self._ledger.get_number(session_id) + limit_reached = False + try: + limit_reached = bool(score.session_metadata.get("limit_reached")) + except Exception: + limit_reached = False + status = None + if limit_reached and not (score.is_finished is True and bool(score.success)): + status = SessionOutcomeStatus.LIMIT_REACHED + outcome = self._format_outcome( + status=status, + success=bool(score.success), + is_finished=score.is_finished, + ) + if outcome == "success": + link = self._format_path_link(session.paths.root) + desc = f"Session {session_number} ✔ success ({link})" + color = "green" + else: + link = self._format_path_link(session.paths.root) + desc = f"Session {session_number} ⏹ {outcome} ({link})" + color = "yellow" + self._update_session_progress_description(session_id, f"[{color}]{desc}[/{color}]") + self._stop_session_progress(session_id) + self._advance_run_progress() + + def on_session_scoring(self, session) -> None: + if not self._enabled(logging.INFO): + return + session_id = session.session_id + session_number = self._ledger.get_number(session_id) + link = self._format_path_link(session.paths.root) + desc = f"Session {session_number} ⏳ scoring ({link})" + self._update_session_progress_description(session_id, f"[yellow]{desc}[/yellow]") + if self._progress is None: + self._print(Text.from_markup(f"[yellow]{desc}[/yellow]")) + else: + self._progress.refresh() + + def on_session_error(self, session, error) -> None: + session_id = session.session_id if session else None + session_root = None + if session is not None: + session_root = session.paths.root + elif session_id is not None: + try: + ctx = get_context() + session_root = RunPaths.from_context(ctx).session(session_id).root + except RuntimeError: + pass + session_number = self._ledger.get_number(session_id) + if not self._enabled(logging.INFO): + return + if isinstance(error, (SessionCancelError, RunCancelError, KeyboardInterrupt)): + desc = f"[yellow]Session {session_number} ⏹ cancelled" + else: + desc = f"[red]Session {session_number} ✖ error" + if session_root is not None: + link = self._format_path_link(session_root) + desc = f"{desc} ({link})" + if desc.startswith("[red]"): + desc = f"{desc}[/red]" + else: + desc = f"{desc}[/yellow]" + self._update_session_progress_description(session_id, desc) + self._stop_session_progress(session_id) + self._advance_run_progress() + + def on_session_reuse(self, session_results) -> None: + if not self._enabled(logging.INFO): + return + session_id = session_results.session_id + session_number = self._ledger.mark_reuse(session_id) + steps = session_results.steps + status = session_results.status + execution_time = session_results.execution_time + if ( + not isinstance(execution_time, (int, float)) + or execution_time < 0 + or execution_time > self._MAX_REUSE_DURATION_SECONDS + ): + execution_time = None + outcome = self._format_outcome( + status=status, + success=session_results.success, + is_finished=session_results.is_finished, + ) + detail_parts = ["↺ reused", outcome] + detail_text = " ".join(detail_parts) + session_root = get_session_paths(session_id).root + link = self._format_path_link(session_root) + desc = f"[yellow]Session {session_number} {detail_text} " f"({link})[/yellow]" + total = steps if isinstance(steps, int) and steps > 0 else 1 + self._add_completed_session_task( + desc, + total=total, + duration=execution_time if isinstance(execution_time, (int, float)) else None, + ) + self._advance_run_progress() + + def on_run_success(self, results, run_config) -> None: + self._stop_progress() + self._print_recap() + + def on_run_error(self, error) -> None: + self._stop_progress() + self._print_recap() + + def _print_recap(self) -> None: + if not self._enabled(logging.INFO): + return + data = self._load_recap_data(get_run_paths().results, self._start_time) + if data is None: + return + table = Table(show_header=False, box=None, pad_edge=False) + table.add_row( + "[bold]Sessions[/bold]", + f"{data.total_sessions} (successes: {data.successful_sessions})", + ) + if data.success_rate is not None: + table.add_row( + "[bold]Success %[/bold]", + f"{data.success_rate:.2%}", + ) + if data.finished_sessions is not None: + table.add_row( + "[bold]Finished[/bold]", + f"{data.finished_sessions}", + ) + table.add_row("[bold]Avg steps[/bold]", f"{data.average_steps}") + self._print(Panel(table, border_style="magenta", title="Recap")) + + cost_table = Table(show_header=False, box=None, pad_edge=False) + cost_table.add_row( + "[bold]Run[/bold]", + f"{self._format_money(data.run_cost)}", + ) + cost_table.add_row( + "[bold]Avg agent[/bold]", + f"{self._format_money(data.avg_agent_cost)}", + ) + self._print(Panel(cost_table, border_style="magenta", title="Costs")) + + results_table = Table(show_header=False, box=None, pad_edge=False) + results_table.add_row("[bold]Results[/bold]", f"{data.results_path}") + self._print(Panel(results_table, border_style="magenta", title="Results")) + + def _enabled(self, level: int) -> bool: + configured = logging._nameToLevel.get(get_settings().log_level.upper(), logging.INFO) + return level >= configured + + def _format_value(self, value) -> str: + if isinstance(value, (dict, list, tuple)): + try: + return json.dumps(value, ensure_ascii=True) + except TypeError: + return str(value) + return str(value) + + def _build_config_panels(self): + if self._run_config is None: + return None + bench_overrides = dict(self._run_config.benchmark_kwargs or {}) + agent_overrides = dict(self._run_config.agent_kwargs or {}) + model_value = self._run_config.model or agent_overrides.get("model") or "unknown" + model_settings = agent_overrides.pop("model_settings", None) + agent_overrides = { + "model": str(model_value), + **agent_overrides, + } + if model_settings: + if hasattr(model_settings, "model_dump"): + model_settings = model_settings.model_dump(exclude_none=True) + if isinstance(model_settings, dict): + default_settings = ModelSettings().model_dump(exclude_none=True) + for key, value in model_settings.items(): + if value is None: + continue + if default_settings.get(key) == value: + continue + agent_overrides[f"model.{key}"] = value + + bench_name = self._run_config.benchmark + agent_name = self._run_config.agent + bench_panel = self._build_config_panel(f"Benchmark: {bench_name}", bench_overrides, border_style="cyan") + agent_panel = self._build_config_panel(f"Agent: {agent_name}", agent_overrides, border_style="green") + console_width = self._console.width + gap = 2 + panel_width = max(20, (console_width - gap) // 2) + bench_panel.width = panel_width + agent_panel.width = panel_width + return Columns([bench_panel, agent_panel], equal=True, expand=True) + + def _build_config_panel( + self, + title: str, + overrides: Dict[str, str], + *, + border_style: str = "magenta", + ) -> Panel: + table = Table(show_header=False, box=None, pad_edge=False) + if overrides: + for key in sorted(overrides): + table.add_row(f"[bold]{key}[/bold]", self._format_value(overrides[key])) + else: + table.add_row("[dim]no overrides[/dim]", "") + return Panel(table, border_style=border_style, title=title) + + def _format_money(self, value: float | None) -> str: + if value is None: + return "-" + return f"${value:.1f}" + + def _format_score(self, value) -> str: + if value is None: + return "-" + if isinstance(value, (int, float)): + return f"{value:.2f}" + return str(value) + + def _format_outcome( + self, + *, + status=None, + success: bool | None = None, + is_finished: bool | None = None, + ) -> str: + if status is not None: + return str(status) + if is_finished is False: + return "unfinished" + if is_finished is True: + return "success" if success else "unsuccessful" + return "unknown" + + @staticmethod + def _format_path_link(path, *, max_len: int = 80) -> str: + text = str(path) + if len(text) > max_len and max_len > 3: + text = "..." + text[-(max_len - 3) :] + try: + target = str(Path(path).resolve()) + except Exception: + return text + return f"[link={target}]{text}[/link]" + + def _print(self, renderable) -> None: + with self._lock: + if self._progress is not None: + self._progress.console.print(renderable) + else: + self._console.print(renderable) + + def _start_progress(self, run_config) -> None: + total = None + if run_config is not None: + if run_config.task_ids: + total = len(run_config.task_ids) + if run_config.num_tasks is not None: + total = min(total, int(run_config.num_tasks)) + elif run_config.num_tasks is not None: + total = int(run_config.num_tasks) + self._progress = Progress( + SpinnerColumn(), + TextColumn("[bold]{task.description}[/bold]"), + BarColumn(bar_width=None), + _CountColumn(), + _DurationColumn(), + console=self._console, + transient=False, + ) + self._progress.start() + self._run_task_id = self._progress.add_task("Run", total=total if total else None, unit="sessions") + + def _start_session_progress(self, session_id: str, session_number: int, agent, session) -> None: + if self._progress is None: + return + total = agent.max_steps + task_id = self._progress.add_task( + f"Session {session_number} ({self._format_path_link(session.paths.root)})", + total=total if total else None, + unit="steps", + hide_total=True, + ) + self._session_tasks[session_id] = (task_id, total) + + def _update_session_progress_description(self, session_id: str, description: str) -> None: + if self._progress is None: + return + entry = self._session_tasks.get(session_id) + if entry is None: + return + task_id, _ = entry + self._progress.update(task_id, description=description) + + def _add_completed_session_task( + self, + description: str, + total: int, + duration: float | None = None, + ) -> None: + if self._progress is None: + return + task_id = self._progress.add_task( + description, + total=total, + unit="steps", + duration=duration, + ) + self._progress.update(task_id, completed=total) + self._progress.stop_task(task_id) + self._track_completed_session_task(task_id) + + def _advance_session_progress(self, session_id: str) -> None: + if self._progress is None: + return + entry = self._session_tasks.get(session_id) + if entry is None: + return + task_id, _ = entry + self._progress.update(task_id, advance=1) + + def _stop_session_progress(self, session_id: str) -> None: + if self._progress is None: + return + entry = self._session_tasks.pop(session_id, None) + if entry is None: + return + task_id, total = entry + steps = self._ledger.get_steps(session_id) + if steps > 0: + self._progress.update(task_id, total=steps, completed=steps) + else: + self._progress.update(task_id, completed=0) + self._progress.stop_task(task_id) + self._track_completed_session_task(task_id) + + def _track_completed_session_task(self, task_id: int) -> None: + if self._progress is None: + return + self._completed_session_tasks.append(task_id) + excess = len(self._completed_session_tasks) - self._MAX_VISIBLE_SESSIONS + if excess <= 0: + return + for _ in range(excess): + old_id = self._completed_session_tasks.pop(0) + if old_id == self._run_task_id: + continue + try: + self._progress.remove_task(old_id) + except Exception: + continue + + def _advance_run_progress(self) -> None: + if self._progress is None or self._run_task_id is None: + return + self._progress.update(self._run_task_id, advance=1) + + def _stop_progress(self) -> None: + if self._progress is None: + return + self._progress.stop() + self._progress = None + self._run_task_id = None + self._session_tasks = {} diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/otel.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/otel.py new file mode 100644 index 00000000..4f6c717f --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/otel.py @@ -0,0 +1,451 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import os +import threading +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional, cast + +from opentelemetry import context, trace +from opentelemetry.sdk.trace import Span +from opentelemetry.trace import SpanKind, Tracer +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util.types import AttributeValue + +from ...core.context import OtelContext, get_context, set_context +from ...core.orchestrator.observer import Observer +from ...interfaces.registry import get_agent_entries, get_benchmark_entries +from ...utils.otel import ( + flush_traces, + get_session_logger, + init_tracing_from_env, + to_otel_attribute_value, +) +from ...utils.settings import get_settings + +tracer = init_tracing_from_env() + + +class SessionSpanManager: + """Manages isolated span hierarchy for a single session.""" + + def __init__(self, session_id: str, session_root: Path, tracer: Tracer = tracer): + self.session_id = session_id + self._tracer = tracer + self._span_stack: list[Span] = [] + self._heritable_attributes: Dict[str, AttributeValue] = {} + + # Initialize session logger + self._logger = get_session_logger( + session_root, + f"{__name__} | pid={os.getpid()} tid={threading.get_native_id()}", + ) + self._logger.info(f"SessionSpanManager initialized for session {self.session_id}") + + def start_span(self, name: str, **kwargs) -> Span: + """Start a new span as a child of the current span. + + Args: + name: Name of the span + **kwargs: Additional arguments passed to tracer.start_span + """ + parent_span = self._span_stack[-1] if self._span_stack else None + ctx = trace.set_span_in_context(parent_span) if parent_span else context.get_current() + + span = cast(Span, self._tracer.start_span(name, context=ctx, **kwargs)) + self._span_stack.append(span) + + # Apply heritable attributes to new span + for k, v in self._heritable_attributes.items(): + span.set_attribute(k, v) + + # Log span start + span_ctx = span.get_span_context() + parent_span_id = format(parent_span.get_span_context().span_id, "016x") if parent_span else None + start_time = datetime.fromtimestamp(span.start_time / 1_000_000_000) + + self._logger.log_span_start( + span_name=name, + span_id=format(span_ctx.span_id, "016x"), + trace_id=format(span_ctx.trace_id, "032x"), + parent_span_id=parent_span_id, + is_root=(len(self._span_stack) == 1), + depth=len(self._span_stack), + start_time=start_time, + ) + return span + + def end_current_span(self) -> None: + """End the current span and set parent as current.""" + if not self._span_stack: + self._logger.warning("Attempted to end span with empty stack") + return + + span = self._span_stack.pop() + span_ctx = span.get_span_context() + span_name = getattr(span, "_name", "unknown") # Try to get span name + + span.end() + end_time = datetime.fromtimestamp(span.end_time / 1_000_000_000) + + self._logger.log_span_end( + span_name=span_name, + span_id=format(span_ctx.span_id, "016x"), + is_root=(len(self._span_stack) == 0), + depth=len(self._span_stack), + end_time=end_time, + ) + + @property + def current_span(self) -> Optional[Span]: + return self._span_stack[-1] if self._span_stack else None + + def get_otel_context(self) -> Optional[OtelContext]: + """Export current span context for subprocess transport. + + Returns: + OtelContext with trace_id and span_id as hex strings, or None if no current span. + """ + if not self.current_span: + return None + + span_ctx = self.current_span.get_span_context() + return OtelContext( + trace_id=format(span_ctx.trace_id, "032x"), + span_id=format(span_ctx.span_id, "016x"), + ) + + def update_tracing_context(self) -> None: + """Update the global Context with current OTEL span context.""" + otel_context = self.get_otel_context() + ctx = get_context() + new_ctx = ctx.with_otel_context(otel_context) + set_context(new_ctx) + self._logger.log_context_update( + otel_context.trace_id if otel_context is not None else None, + otel_context.span_id if otel_context is not None else None, + operation="write", + ) + + def set_attribute(self, key: str, value: AttributeValue) -> None: + if self.current_span is None: + raise (AttributeError("No current span")) + self.current_span.set_attribute(key, value) + span_ctx = self.current_span.get_span_context() + self._logger.log_attribute_set(key, value, format(span_ctx.span_id, "016x")) + + def set_attributes(self, attributes: Optional[Dict[str, AttributeValue]] = None, **kwargs) -> None: + if attributes is None: + attributes = {} + attributes.update(kwargs) + for k, v in attributes.items(): + self.set_attribute(k, v) + + def set_heritable_attribute(self, key: str, value: AttributeValue) -> None: + self._heritable_attributes[key] = value + if self.current_span: + self.set_attribute(key, value) + + def set_heritable_attributes(self, attributes: Optional[Dict[str, AttributeValue]] = None, **kwargs) -> None: + if attributes: + self._heritable_attributes.update(attributes) + self._heritable_attributes.update(kwargs) + if self.current_span: + self.set_attributes(attributes, **kwargs) + + def record_exception(self, exc: Exception, set_error_status: bool = True) -> None: + if self.current_span: + self.current_span.record_exception(exc) + if set_error_status: + self.current_span.set_status(Status(StatusCode.ERROR)) + self._logger.log_exception(exc) + + def update_current_span_name(self, new_name: str) -> None: + if self.current_span: + old_name = getattr(self.current_span, "_name", "unknown") + self.current_span.update_name(new_name) + span_ctx = self.current_span.get_span_context() + self._logger.log_span_rename(old_name, new_name, format(span_ctx.span_id, "016x")) + + +# OBSERVER +class OtelTracingObserver(Observer): + """OpenTelemetry tracing observer for sessions. + + This observer creates a complete trace for each session, with spans for: + - Session (root span) + - Steps (child spans) + - Actions and observations (nested child spans) + + Each session gets its own SessionSpanContext instance, ensuring complete + isolation between concurrent sessions + """ + + def __init__(self): + super().__init__() + self._run_attributes: Dict[str, AttributeValue] = {} + self._span_managers: Dict[str, SessionSpanManager] = {} + self._session_step_counters: Dict[str, int] = {} + self._session_agents: Dict[str, Any] = {} # Store agent instances by session_id + self._session_actions: Dict[str, list] = {} # Store session actions for tool definitions + + def _get_span_manager(self, session_id: str) -> SessionSpanManager: + return self._span_managers[session_id] + + def _get_action_description(self, session, action_name: str) -> Optional[str]: + """Look up action description from session.actions by name.""" + for action_type in session.actions: + if action_type.name == action_name: + return action_type.description + return None + + def _get_tool_definitions(self, session_id: str) -> str: + """Generate gen_ai.tool.definitions JSON from session actions.""" + actions = self._session_actions.get(session_id, []) + tool_definitions = [] + + for action_type in actions: + tool_def = { + "type": "function", + "function": { + "name": action_type.name, + "description": action_type.description, + }, + } + + # Add parameters schema if available + try: + schema = action_type.arguments.model_json_schema() + # Convert to OpenAI function calling format + tool_def["function"]["parameters"] = { + "type": "object", + "properties": schema.get("properties", {}), + "required": schema.get("required", []), + } + except Exception: + pass + + tool_definitions.append(tool_def) + + try: + return json.dumps(tool_definitions) + except Exception: + return "[]" + + def on_run_start(self, run_config) -> None: + bench_entry = get_benchmark_entries().get(run_config.benchmark) + agent_entry = get_agent_entries().get(run_config.agent) + + # Extract model name from run_config + model_name = run_config.model or (run_config.agent_kwargs or {}).get("model") + + from ...utils.paths import get_run_paths + + self._run_attributes = { + "exgentic.benchmark.slug_name": bench_entry.slug_name if bench_entry is not None else run_config.benchmark, + "exgentic.benchmark.subset": run_config.subset, + "exgentic.benchmark.agent.name": agent_entry.slug_name if agent_entry is not None else run_config.agent, + "exgentic.agent.slug": run_config.agent, + "exgentic.run.id": get_run_paths().run_id, + } + + # Store model name as heritable attribute + if model_name: + self._run_attributes["gen_ai.request.model"] = model_name + + def on_session_creation(self, session) -> None: + span_manager = SessionSpanManager(session.session_id, self.paths.session(session.session_id).root) + self._span_managers[session.session_id] = span_manager + self._session_step_counters[session.session_id] = 0 + self._session_actions[session.session_id] = session.actions # Store actions for tool definitions + + # Start root session span + bench_name = self._run_attributes.get("exgentic.benchmark.slug_name", "unknown_benchmark") + subset = self._run_attributes.get("exgentic.benchmark.subset", "subset") + span_manager.start_span(f"{bench_name} {subset} session") + span_manager.update_tracing_context() # pass otel context to trace_logger + + span_manager.set_heritable_attributes(self._run_attributes) + + # Set session-level attributes + # gen_ai.conversation.id is the primary correlation attribute (heritable) + span_manager.set_heritable_attribute( + "gen_ai.conversation.id", + session.session_id, + ) + # Also keep exgentic.session.id for backwards compatibility + span_manager.set_heritable_attribute( + "exgentic.session.id", + session.session_id, + ) + span_manager.set_attribute("exgentic.session.task_id", session.task_id) + + # Only record task content if otel_record_content is enabled + if get_settings().otel_record_content: + span_manager.set_attribute( + "exgentic.session.task", + session.task, + ) + + for action in session.actions: + span_manager.set_attribute(f"exgentic.session.action.{action.name}.name", action.name) + span_manager.set_attribute(f"exgentic.session.action.{action.name}.description", action.description) + span_manager.set_attribute(f"exgentic.session.action.{action.name}.is_message", action.is_message) + span_manager.set_attribute(f"exgentic.session.action.{action.name}.is_finish", action.is_finish) + for k, v in session.context.items(): + otel_value = to_otel_attribute_value(v) + if otel_value is not None: + span_manager.set_attribute(f"exgentic.context.{k}", otel_value) + + def on_session_start(self, session, agent, observation) -> None: + self._session_agents[session.session_id] = agent # Store agent instance + span_manager = self._span_managers[session.session_id] + + span_manager.set_attribute("exgentic.session.agent.id", agent.agent_id) + agent_path_otel = to_otel_attribute_value(agent.paths.agent_dir) + if agent_path_otel is not None: + span_manager.set_attribute("exgentic.session.agent.path", agent_path_otel) + + # Record initial observation as execute_tool span + span_manager.start_span("execute_tool initial_observation", kind=SpanKind.CLIENT) + span_manager.current_span.set_attribute("gen_ai.operation.name", "execute_tool") + span_manager.current_span.set_attribute("gen_ai.tool.name", "initial_observation") + span_manager.current_span.set_attribute("gen_ai.tool.description", "Initial observation from benchmark") + + self._record_observation(session.session_id, observation) + span_manager.end_current_span() # end initial observation span + + # Increment step counter (no invoke_agent span created) + self._session_step_counters[session.session_id] += 1 + + def _record_observation(self, session_id: str, observation) -> None: + """Record observation details on the current span.""" + span_manager = self._get_span_manager(session_id) + observation_list = observation.to_observation_list() if observation is not None else [] + + # Only record observation content if otel_record_content is enabled + if get_settings().otel_record_content: + observation_otel = to_otel_attribute_value(observation_list) + if observation_otel is not None: + span_manager.current_span.set_attribute("gen_ai.tool.result", observation_otel) + + def on_react_success(self, session, action) -> None: + span_manager = self._get_span_manager(session.session_id) + + # Create execute_tool span with semantic conventions + action_list = action.to_action_list() if action else [] + tool_name = action_list[0].name if action_list and action_list[0] else "unknown" + span_manager.start_span(f"execute_tool {tool_name}", kind=SpanKind.CLIENT) + + # Set required semantic convention attributes + span_manager.current_span.set_attribute("gen_ai.operation.name", "execute_tool") + span_manager.current_span.set_attribute("gen_ai.tool.name", tool_name) + + # Set recommended attributes + if action_list: + first_action = action_list[0] + span_manager.current_span.set_attribute("gen_ai.tool.id", first_action.id) + + # Get tool description from session.actions + tool_desc = self._get_action_description(session, tool_name) + if tool_desc: + span_manager.current_span.set_attribute("gen_ai.tool.description", tool_desc) + + # Set tool parameters as JSON + if get_settings().otel_record_content: + try: + params_json = first_action.arguments.model_dump_json() + span_manager.current_span.set_attribute("gen_ai.tool.parameters", params_json) + except Exception: + pass + + def on_react_error(self, session, error) -> None: + return None + + def on_step_success(self, session, observation) -> None: + span_manager = self._get_span_manager(session.session_id) + self._record_observation(session.session_id, observation) + span_manager.end_current_span() # end execute_tool span + + self._session_step_counters[session.session_id] += 1 + + def on_step_error(self, session, error) -> None: + span_manager = self._get_span_manager(session.session_id) + span_manager.record_exception(error) + + def on_session_success(self, session, score, agent) -> None: + span_manager = self._get_span_manager(session.session_id) + + # Certain session conditions may lead to a trailing execute_tool span + if len(span_manager._span_stack) == 2: + span_manager.end_current_span() # end execute_tool span + + # Add final session attributes (with exgentic. prefix) + span_manager.set_attribute("exgentic.score.success", score.success) + span_manager.set_attribute("exgentic.score", score.score) + span_manager.set_attribute("exgentic.score.is_finished", score.is_finished) + span_manager.set_attribute("exgentic.session.steps", self._session_step_counters[session.session_id]) + + # Convert cost objects to JSON strings for OTEL compatibility + try: + agent_cost = agent.get_cost() + span_manager.set_attribute("exgentic.agent.agent_cost", json.dumps(agent_cost, default=str)) + except Exception: + pass + + try: + session_cost = session.get_cost() + span_manager.set_attribute("exgentic.session.cost", json.dumps(session_cost, default=str)) + except Exception: + pass + + span_manager.set_attribute("exgentic.session.task_id", session.task_id) + + # Close session span + span_manager.end_current_span() + + # Flush traces to ensure they are exported + flush_traces() + + # Clean up + del self._span_managers[session.session_id] + del self._session_step_counters[session.session_id] + del self._session_agents[session.session_id] + del self._session_actions[session.session_id] + + def on_session_error(self, session, error) -> None: + span_manager = self._get_span_manager(session.session_id) + + # Certain session conditions may lead to a trailing execute_tool span + if len(span_manager._span_stack) == 2: + span_manager.end_current_span() # end execute_tool span + + # Record error on session span + span_manager.record_exception(error) + + # Convert cost object to JSON string for OTEL compatibility + try: + session_cost = session.get_cost() + span_manager.set_attribute("exgentic.session.cost", json.dumps(session_cost, default=str)) + except Exception: + pass + + span_manager.set_attribute("exgentic.session.task_id", session.task_id) + + span_manager.end_current_span() # Close session span + + # Flush traces to ensure they are exported + flush_traces() + + # Clean up + del self._span_managers[session.session_id] + del self._session_step_counters[session.session_id] + if session.session_id in self._session_agents: + del self._session_agents[session.session_id] + if session.session_id in self._session_actions: + del self._session_actions[session.session_id] + + +# Made with Bob diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/recap.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/recap.py new file mode 100644 index 00000000..f5d9b7ba --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/recap.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +from dataclasses import dataclass +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any + +from ...core.orchestrator.observer import Observer +from ..logging import get_logger + + +@dataclass(frozen=True) +class RunRecapData: + total_sessions: int + successful_sessions: int + success_rate: float | None + finished_sessions: int | None + benchmark_score: Any + average_score: Any + run_cost: Any + avg_agent_cost: Any + total_agent_cost: Any + total_benchmark_cost: Any + average_steps: Any + execution_time: timedelta + results_path: Path + + +class RunRecapMixin: + def _load_recap_data(self, results_path: Path, start_time: datetime | None) -> RunRecapData | None: + if not results_path.exists(): + return None + + try: + with open(results_path, encoding="utf-8") as f: + results = json.load(f) + except Exception: + return None + + total = results.get("total_sessions", 0) + succ = results.get("successful_sessions", 0) + success_rate = results.get("percent_successful") + final = results.get("benchmark_score") + avg = results.get("average_score") + run_cost = results.get("total_run_cost") + avg_agent_cost = results.get("average_agent_cost") + total_agent_cost = results.get("total_agent_cost") + total_benchmark_cost = results.get("total_benchmark_cost") + avg_steps = results.get("average_steps") + + session_results = results.get("session_results") or [] + if session_results: + total = len(session_results) + succ = sum(1 for r in session_results if r.get("success")) + if total: + success_rate = succ / total + finished = sum(1 for r in session_results if r.get("is_finished")) if session_results else None + + started = start_time or datetime.now() + execution_time = datetime.now() - started + return RunRecapData( + total_sessions=total, + successful_sessions=succ, + success_rate=success_rate, + finished_sessions=finished, + benchmark_score=final, + average_score=avg, + run_cost=run_cost, + avg_agent_cost=avg_agent_cost, + total_agent_cost=total_agent_cost, + total_benchmark_cost=total_benchmark_cost, + average_steps=avg_steps, + execution_time=execution_time, + results_path=results_path, + ) + + def _format_money(self, value: Any) -> str: + return f"${value:.4f}" if isinstance(value, (int, float)) else "-" + + +class RunRecapObserver(Observer, RunRecapMixin): + def __init__( + self, + run_id: str | None = None, + *, + console: bool = False, + logger=None, + ) -> None: + super().__init__(run_id) + self._logger = logger + self._console = console + self._start_time: datetime | None = None + + def _ensure_logger(self) -> None: + if self._logger is not None: + return + rp = self.paths + log_path = rp.tracker + self._logger = get_logger( + f"tracker.recap.{self._run_id}", + str(log_path), + console=self._console, + propagate=False, + ) + + def on_run_start(self, run_config) -> None: + self._ensure_logger() + self._start_time = datetime.now() + + def on_run_success(self, results, run_config) -> None: + self._log_recap() + + def on_run_error(self, error) -> None: + self._log_recap() + + def _log_recap(self) -> None: + self._ensure_logger() + results_path = self.paths.results + data = self._load_recap_data(results_path, self._start_time) + if data is None: + return + + finished_str = f" | Finished: {data.finished_sessions}" if data.finished_sessions is not None else "" + success_rate_str = f" | Success%: {data.success_rate:.2%}" if data.success_rate is not None else "" + + final_str = f"{data.benchmark_score}" if data.benchmark_score is not None else "-" + avg_str = f"{data.average_score}" if data.average_score is not None else "-" + run_cost_str = self._format_money(data.run_cost) + avg_agent_cost_str = self._format_money(data.avg_agent_cost) + total_agent_cost_str = self._format_money(data.total_agent_cost) + total_benchmark_cost_str = self._format_money(data.total_benchmark_cost) + + recap = ( + f"📊 Sessions: {data.total_sessions} | Successes: {data.successful_sessions}" + f"{success_rate_str}{finished_str}\n" + f"🏁 Scores: Final={final_str} | Avg={avg_str}\n" + f"💰 Costs: Run={run_cost_str} | Avg Agent={avg_agent_cost_str} | " + f"Agent Total={total_agent_cost_str} | Benchmark Total={total_benchmark_cost_str}\n" + f"🐾 Average number of steps={data.average_steps}\n" + f"🕐 Total execution time={data.execution_time}\n" + f"📄 Results: {data.results_path}" + ) + self._logger.info(recap) diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/results.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/results.py new file mode 100644 index 00000000..4a69a0a3 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/results.py @@ -0,0 +1,537 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import json +import os +import threading +import time +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from ... import __version__ as exgentic_version +from ...core.orchestrator.observer import Observer +from ...core.orchestrator.termination import ( + AgentError, + BenchmarkError, + InvalidActionError, + InvalidObservationError, + RunCancelError, + SessionCancelError, +) +from ...core.types import ( + Action, + BenchmarkResults, + Observation, + RunResults, + RunStatus, + SessionExecutionStatus, + SessionOutcomeStatus, + SessionResults, + SessionScore, +) +from ...interfaces.registry import get_agent_entries, get_benchmark_entries +from ...utils.cost import CostReport, accumulate_reports +from .session_ledger import SessionLedger + + +@dataclass +class _SessionData: + action_count: int = 0 + invalid_action_count: int = 0 + agent: Any | None = None + reason: Optional[str] = None + + +class ResultsObserver(Observer): + def __init__(self, run_id: str | None = None) -> None: + super().__init__(run_id) + self._lock = threading.Lock() + self._ledger = SessionLedger() + self._sessions: Dict[str, _SessionData] = {} + self._session_results: list[SessionResults] = [] + self._results: RunResults | None = None + self._run_config: Optional[Any] = None + self._final_results: Optional[Any] = None + + def on_run_start(self, run_config) -> None: + with self._lock: + self._run_config = run_config + if self._run_id is None: + self._run_id = run_config.run_id + + def on_session_start(self, session, agent, observation) -> None: + session_id = session.session_id + self._ledger.register(session_id) + with self._lock: + self._sessions[session_id] = _SessionData(agent=agent) + if isinstance(observation, Observation): + self._record_observation(session, observation, step=0, initial=True) + + def on_react_success(self, session, action) -> None: + if action is None: + self._set_reason(session, "ended by benchmark (agent returned None action)") + return + if not isinstance(action, Action): + self._set_reason( + session, + f"terminated by illegal action returned from agent: {action}", + ) + return + self._record_action(session, action) + + def on_step_success(self, session, observation) -> None: + if observation is not None and not isinstance(observation, Observation): + self._set_reason( + session, + "terminated by illegal observation returned from session: " f"{observation}", + ) + return + self._record_observation(session, observation) + if observation is None: + self._set_reason(session, "ended by agent (session returned None observation)") + + def on_react_error(self, session, error) -> None: + if isinstance(error, InvalidActionError): + self._set_reason( + session, + f"terminated by illegal action returned from agent: {error.action}", + ) + else: + self._set_reason(session, "terminated by agent exception") + + def on_step_error(self, session, error) -> None: + if isinstance(error, InvalidObservationError): + self._set_reason( + session, + "terminated by illegal observation returned from session: " f"{error.observation}", + ) + else: + self._set_reason(session, "terminated by session exception") + + def on_session_error(self, session, error) -> None: + error_source = None + if isinstance(error, AgentError): + error_source = "agent" + elif isinstance(error, BenchmarkError): + error_source = "benchmark" + if isinstance(error, (SessionCancelError, RunCancelError, KeyboardInterrupt)): + self._set_reason(session, "cancelled by user") + error_source = "cancelled" + else: + self._set_reason( + session, + "terminated by unexpected exception (see console)", + ) + root_error = error.error if isinstance(error, (AgentError, BenchmarkError)) else None + error_message = str(root_error) if root_error else str(error) + session_metadata = {"error": error_message} + if error_source is not None: + session_metadata["error_source"] = error_source + score = SessionScore( + score=0, + success=False, + is_finished=None, + session_metadata=session_metadata, + ) + self._record_session(session, score) + + def on_session_success(self, session, score, agent) -> None: + self._record_session(session, score, agent=agent) + + def on_run_success(self, results, run_config) -> None: + with self._lock: + self._final_results = results + self._run_config = run_config + self._results = self._write_run_results() + + def on_run_error(self, error) -> None: + self._results = self._write_run_results() + + def results(self) -> RunResults: + if self._results is None: + raise RuntimeError("Run results have not been computed yet.") + return self._results + + def session_results(self) -> list[SessionResults]: + return list(self._session_results) + + def on_session_reuse(self, session_results: SessionResults) -> None: + with self._lock: + self._session_results.append(session_results) + + def _record_action(self, session, action: Action) -> int: + session_id = session.session_id + single_actions = action.to_action_list() + total_actions = len(single_actions) + invalid_actions = 0 + for single_action in single_actions: + report = single_action.validation + if not report.valid or not report.name_valid or not report.args_valid: + invalid_actions += 1 + step_n = self._ledger.increment_steps(session_id) + session_number = self._ledger.get_number(session_id) + with self._lock: + data = self._sessions.get(session_id) + if data is None: + data = _SessionData() + self._sessions[session_id] = data + data.action_count += total_actions + data.invalid_action_count += invalid_actions + agent_cost, benchmark_cost = self._get_cost_snapshot(session_id, session) + traj_path = session.paths.trajectory + traj_path.parent.mkdir(parents=True, exist_ok=True) + event = { + "event": "action", + "run_id": self._run_id, + "session_id": session_id, + "session_number": session_number, + "task_id": session.task_id, + "step": step_n, + "action": json.loads(action.model_dump_json()), + "initial": False, + "agent_cost": agent_cost, + "benchmark_cost": benchmark_cost, + } + with open(traj_path, "a", encoding="utf-8") as f: + json.dump(event, f, ensure_ascii=False) + f.write("\n") + return step_n + + def _record_observation( + self, + session, + observation: Optional[Observation], + *, + step: Optional[int] = None, + initial: bool = False, + ) -> None: + session_id = session.session_id + session_number = self._ledger.get_number(session_id) + step_n = self._ledger.get_steps(session_id) if step is None else step + agent_cost, benchmark_cost = self._get_cost_snapshot(session_id, session) + traj_path = session.paths.trajectory + traj_path.parent.mkdir(parents=True, exist_ok=True) + payload = json.loads(observation.model_dump_json()) if observation is not None else None + event = { + "event": "observation", + "run_id": self._run_id, + "session_id": session_id, + "session_number": session_number, + "task_id": session.task_id, + "step": step_n, + "observation": payload, + "initial": initial, + "agent_cost": agent_cost, + "benchmark_cost": benchmark_cost, + } + with open(traj_path, "a", encoding="utf-8") as f: + json.dump(event, f, ensure_ascii=False) + f.write("\n") + + def _record_session(self, session, score: SessionScore, *, agent=None) -> None: + session_id = session.session_id + with self._lock: + data = self._sessions.get(session_id) + state = self._ledger.pop_state(session_id) + execution_time = time.time() - state.started_at if state is not None else 0.0 + steps = state.steps if state is not None else 0 + action_count = data.action_count if data is not None else 0 + invalid_action_count = data.invalid_action_count if data is not None else 0 + success = bool(score.success) + value = score.score + is_finished = score.is_finished + agent_cost_report = agent.get_cost() if agent is not None else CostReport.initialize_empty() + benchmark_cost_report = session.get_cost() + status = self._resolve_session_status(score) + tr = SessionResults( + session_id=session_id, + success=success, + score=value, + is_finished=is_finished, + status=status, + steps=steps, + action_count=action_count, + invalid_action_count=invalid_action_count, + agent_cost=agent_cost_report.total_cost, + benchmark_cost=benchmark_cost_report.total_cost, + execution_time=execution_time, + details=score.model_dump(), + cost_reports={ + "agent": agent_cost_report, + "benchmark": benchmark_cost_report, + }, + task_id=session.task_id, + ) + self._pop_reason(session) + with self._lock: + self._session_results.append(tr) + if session_id in self._sessions: + del self._sessions[session_id] + sess_paths = self.paths.session(session_id) + sess_paths.results.parent.mkdir(parents=True, exist_ok=True) + with open(sess_paths.results, "w", encoding="utf-8") as f: + json.dump(tr.model_dump(), f, ensure_ascii=False, indent=2, default=str) + error_message = score.session_metadata.get("error") + if error_message: + error_source = score.session_metadata.get("error_source") + error_path = sess_paths.error_log + error_path.parent.mkdir(parents=True, exist_ok=True) + with open(error_path, "w", encoding="utf-8") as f: + if error_source: + f.write(f"source: {error_source}\n") + f.write(str(error_message)) + + def _resolve_session_status(self, score: SessionScore) -> SessionOutcomeStatus: + error_source = score.session_metadata.get("error_source") + if error_source == "cancelled": + return SessionOutcomeStatus.CANCELLED + if score.session_metadata.get("limit_reached"): + if score.is_finished is True and score.success: + return SessionOutcomeStatus.SUCCESS + return SessionOutcomeStatus.LIMIT_REACHED + if error_source in ("agent", "benchmark"): + return SessionOutcomeStatus.ERROR + if score.session_metadata.get("error"): + return SessionOutcomeStatus.ERROR + if score.is_finished is True: + return SessionOutcomeStatus.SUCCESS if score.success else SessionOutcomeStatus.UNSUCCESSFUL + if score.is_finished is False: + return SessionOutcomeStatus.UNFINISHED + return SessionOutcomeStatus.ERROR if not score.success else SessionOutcomeStatus.UNKNOWN + + def _write_run_results(self) -> RunResults: + rp = self.paths + with self._lock: + results_snapshot = list(self._session_results) + run_config = self._run_config + bench_results_obj = self._final_results if isinstance(self._final_results, BenchmarkResults) else None + if run_config is None: + raise RuntimeError("Run config not recorded in results observer.") + + # Derive current session status snapshot for provenance. + try: + status = RunStatus.from_config(run_config) + except Exception: + status = None + + completed_sessions = None + incomplete_sessions = None + missing_sessions = None + running_sessions = None + aggregated_session_ids = None + skipped_session_ids = None + skipped_session_reasons = None + missing_result_files = None + + if status is not None: + completed = [s for s in status.session_statuses if s.status == SessionExecutionStatus.COMPLETED] + incomplete = [s for s in status.session_statuses if s.status == SessionExecutionStatus.INCOMPLETE] + missing = [s for s in status.session_statuses if s.status == SessionExecutionStatus.MISSING] + running = [s for s in status.session_statuses if s.status == SessionExecutionStatus.RUNNING] + completed_sessions = len(completed) + incomplete_sessions = len(incomplete) + missing_sessions = len(missing) + running_sessions = len(running) + aggregated_session_ids = [s.session_id for s in completed] + skipped = incomplete + missing + running + skipped_session_ids = [s.session_id for s in skipped] + skipped_session_reasons = {s.session_id: str(s.status) for s in skipped} + missing_result_files = [s.results_path for s in missing] + + total_sessions = len(results_snapshot) + executed_session_ids = [r.session_id for r in results_snapshot] + planned_sessions = None + planned_session_ids = None + if run_config.task_ids: + planned_task_ids = list(run_config.task_ids) + if run_config.num_tasks is not None: + planned_task_ids = planned_task_ids[: int(run_config.num_tasks)] + planned_sessions = len(planned_task_ids) + planned_session_ids = [ + run_config.to_session_config(task_id).get_session_id() for task_id in planned_task_ids + ] + elif run_config.num_tasks is not None: + planned_sessions = int(run_config.num_tasks) + if planned_sessions is None: + planned_sessions = total_sessions + successful_sessions = sum(1 for r in results_snapshot if r.success) + percent_successful = successful_sessions / total_sessions if total_sessions else None + scores = [r.score for r in results_snapshot if r.score is not None] + average_score = (sum(scores) / len(scores)) if scores else None + + finished_successful = sum(1 for r in results_snapshot if r.is_finished is True and bool(r.success)) + finished_unsuccessful = sum(1 for r in results_snapshot if r.is_finished is True and not bool(r.success)) + unfinished = sum(1 for r in results_snapshot if r.is_finished is False) + errored = sum(1 for r in results_snapshot if r.is_finished is None) + percent_finished_successful = finished_successful / total_sessions if total_sessions else None + percent_finished_unsuccessful = finished_unsuccessful / total_sessions if total_sessions else None + percent_unfinished = unfinished / total_sessions if total_sessions else None + percent_error = errored / total_sessions if total_sessions else None + percent_finished = (finished_successful + finished_unsuccessful) / total_sessions if total_sessions else None + + total_agent_cost = sum(r.agent_cost for r in results_snapshot) if total_sessions else 0.0 + total_benchmark_cost = sum(r.benchmark_cost for r in results_snapshot) if total_sessions else 0.0 + total_run_cost = total_agent_cost + total_benchmark_cost + if results_snapshot: + agent_reports = [r.cost_reports["agent"] for r in results_snapshot] + benchmark_reports = [r.cost_reports["benchmark"] for r in results_snapshot] + try: + accumulated_agent_report = accumulate_reports(agent_reports) + except ValueError: + accumulated_agent_report = CostReport.initialize_empty() + for report in agent_reports: + accumulated_agent_report.accumulate_from(report) + try: + accumulated_benchmark_report = accumulate_reports(benchmark_reports) + except ValueError: + accumulated_benchmark_report = CostReport.initialize_empty() + for report in benchmark_reports: + accumulated_benchmark_report.accumulate_from(report) + else: + accumulated_agent_report = CostReport.initialize_empty() + accumulated_benchmark_report = CostReport.initialize_empty() + average_agent_cost = (total_agent_cost / total_sessions) if total_sessions else None + average_benchmark_cost = (total_benchmark_cost / total_sessions) if total_sessions else None + + steps = [tr.steps for tr in results_snapshot] + avg_steps = sum(steps) / len(steps) if steps else None + action_counts = [tr.action_count for tr in results_snapshot] + avg_action_count = sum(action_counts) / len(action_counts) if action_counts else None + invalid_action_counts = [tr.invalid_action_count for tr in results_snapshot] + avg_invalid_action_count = ( + sum(invalid_action_counts) / len(invalid_action_counts) if invalid_action_counts else None + ) + total_action_count = sum(action_counts) if action_counts else 0 + total_invalid_action_count = sum(invalid_action_counts) if invalid_action_counts else 0 + avg_invalid_action_percent = ( + (total_invalid_action_count / total_action_count * 100) if total_action_count else None + ) + + bench_score: Optional[float] = bench_results_obj.score if bench_results_obj is not None else None + + model_name = run_config.model or (run_config.agent_kwargs or {}).get("model") + model_names = [str(model_name)] if model_name else None + + max_workers = run_config.max_workers + if max_workers is None: + max_workers_env = os.environ.get("EXGENTIC_MAX_WORKERS") + if max_workers_env: + try: + max_workers = int(max_workers_env) + except ValueError: + max_workers = None + + bench_entry = get_benchmark_entries().get(run_config.benchmark) + agent_entry = get_agent_entries().get(run_config.agent) + bench_name = bench_entry.display_name if bench_entry is not None else run_config.benchmark + agent_name = agent_entry.display_name if agent_entry is not None else run_config.agent + + results_obj = RunResults( + benchmark_name=str(bench_name), + benchmark_slug_name=str(run_config.benchmark), + agent_name=str(agent_name), + agent_slug_name=str(run_config.agent), + model_name=str(model_name) if model_name is not None else None, + model_names=model_names, + subset_name=str(run_config.subset) if run_config.subset is not None else None, + total_sessions=total_sessions, + planned_sessions=planned_sessions, + planned_session_ids=planned_session_ids, + executed_session_ids=executed_session_ids, + max_workers=max_workers, + successful_sessions=successful_sessions, + benchmark_score=bench_score, + benchmark_results=(bench_results_obj.model_dump() if bench_results_obj is not None else None), + average_score=average_score, + average_agent_cost=average_agent_cost, + total_agent_cost=total_agent_cost, + average_benchmark_cost=average_benchmark_cost, + total_benchmark_cost=total_benchmark_cost, + total_run_cost=total_run_cost, + session_results=results_snapshot, + accumulated_agent_report=accumulated_agent_report, + accumulated_benchmark_report=accumulated_benchmark_report, + average_steps=avg_steps, + average_action_count=avg_action_count, + average_invalid_action_count=avg_invalid_action_count, + average_invalid_action_percent=avg_invalid_action_percent, + percent_finished=percent_finished, + percent_successful=percent_successful, + percent_finished_successful=percent_finished_successful, + percent_finished_unsuccessful=percent_finished_unsuccessful, + percent_unfinished=percent_unfinished, + percent_error=percent_error, + aggregation_mode="completed_only", + completed_sessions=completed_sessions, + incomplete_sessions=incomplete_sessions, + missing_sessions=missing_sessions, + running_sessions=running_sessions, + aggregated_session_ids=aggregated_session_ids, + skipped_session_ids=skipped_session_ids, + skipped_session_reasons=skipped_session_reasons, + missing_result_files=missing_result_files, + exgentic_version=exgentic_version, + ) + + try: + rp.results.parent.mkdir(parents=True, exist_ok=True) + with open(rp.results, "w", encoding="utf-8") as f: + json.dump(results_obj.model_dump(), f, ensure_ascii=False, indent=2, default=str) + if bench_results_obj is not None: + rp.benchmark_results.parent.mkdir(parents=True, exist_ok=True) + with open(rp.benchmark_results, "w", encoding="utf-8") as f: + json.dump( + bench_results_obj.model_dump(), + f, + ensure_ascii=False, + indent=2, + default=str, + ) + except OSError: + # Allow aggregation in read-only output directories. + pass + return results_obj + + def _set_reason(self, session, reason: str, *, overwrite: bool = False) -> None: + session_id = session.session_id if session else None + if session_id is None: + return + with self._lock: + data = self._sessions.get(session_id) + if data is None: + return + if data.reason is not None and not overwrite: + return + data.reason = reason + + def _pop_reason(self, session) -> str: + session_id = session.session_id if session else None + if session_id is None: + return "ended" + with self._lock: + data = self._sessions.get(session_id) + if data is None or data.reason is None: + return "ended" + reason = data.reason + data.reason = None + return reason + + def _get_cost_snapshot(self, session_id: str, session) -> tuple[float, float]: + with self._lock: + data = self._sessions.get(session_id) + agent = data.agent if data is not None else None + agent_cost_report = CostReport.initialize_empty() + benchmark_cost_report = CostReport.initialize_empty() + if agent is not None: + try: + agent_cost_report = agent.get_cost() + except Exception: + agent_cost_report = CostReport.initialize_empty() + try: + benchmark_cost_report = session.get_cost() + except Exception: + benchmark_cost_report = CostReport.initialize_empty() + return agent_cost_report.total_cost, benchmark_cost_report.total_cost + + +FileSystemObserver = ResultsObserver diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/session_ledger.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/session_ledger.py new file mode 100644 index 00000000..59d5e06c --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/session_ledger.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import threading +import time +from dataclasses import dataclass + + +@dataclass +class SessionState: + started_at: float + steps: int = 0 + + +class SessionLedger: + def __init__(self) -> None: + self._lock = threading.Lock() + self._counter = 0 + self._numbers: dict[str, int] = {} + self._states: dict[str, SessionState] = {} + + def register(self, session_id: str) -> int: + with self._lock: + number = self._numbers.get(session_id) + if number is None: + self._counter += 1 + number = self._counter + self._numbers[session_id] = number + self._states[session_id] = SessionState(started_at=time.time()) + return number + + def mark_reuse(self, session_id: str) -> int: + with self._lock: + number = self._numbers.get(session_id) + if number is None: + self._counter += 1 + number = self._counter + self._numbers[session_id] = number + return number + + def increment_steps(self, session_id: str, count: int = 1) -> int: + with self._lock: + state = self._states.get(session_id) + if state is None: + if session_id not in self._numbers: + self._counter += 1 + self._numbers[session_id] = self._counter + state = SessionState(started_at=time.time()) + self._states[session_id] = state + state.steps += count + return state.steps + + def get_steps(self, session_id: str) -> int: + with self._lock: + state = self._states.get(session_id) + return state.steps if state is not None else 0 + + def get_number(self, session_id: str) -> int: + with self._lock: + return self._numbers.get(session_id, 0) + + def pop_state(self, session_id: str) -> SessionState | None: + with self._lock: + return self._states.pop(session_id, None) diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/handlers/warnings.py b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/warnings.py new file mode 100644 index 00000000..4ac96021 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/handlers/warnings.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from ...core.context import get_context +from ...core.orchestrator.observer import Observer +from ..logging import configure_warnings_logging + + +class WarningsObserver(Observer): + def __init__(self, *, replace_existing_file_handlers: bool = True) -> None: + self._replace = replace_existing_file_handlers + self._configured = False + + def on_run_start(self, run_config) -> None: + if self._configured: + return + ctx = get_context() + try: + configure_warnings_logging( + ctx.output_dir, + ctx.run_id, + replace_existing_file_handlers=self._replace, + ) + except OSError: + # Ignore failures in read-only runs. + return + self._configured = True diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/logging/__init__.py b/labs/AgentStream/exgentic/src/exgentic/observers/logging/__init__.py new file mode 100644 index 00000000..800d38a6 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/logging/__init__.py @@ -0,0 +1,358 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Basic logging setup for Exgentic components.""" +import logging +import os +import sys +from pathlib import Path +from typing import Callable, Dict, List, Tuple + +from uvicorn.logging import DefaultFormatter + +from ...core.context import try_get_context +from ...core.context import try_get_context as _try_get_context_for_run_id +from ...utils.settings import get_settings + + +def _build_console_handler(*, log_level: int, formatter: logging.Formatter) -> logging.Handler: + try: + from rich.logging import RichHandler + except Exception: + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(log_level) + handler.setFormatter(formatter) + return handler + + console_formatter = logging.Formatter("%(message)s") + handler = RichHandler( + show_time=False, + show_level=True, + show_path=False, + rich_tracebacks=True, + markup=False, + ) + handler.setLevel(log_level) + handler.setFormatter(console_formatter) + return handler + + +def get_logger( + name: str, + log_file_path: str | None = None, + *, + console: bool | None = None, + propagate: bool | None = None, +) -> logging.Logger: + """Get a logger with standard Exgentic configuration. + + - If `log_file_path` is provided: attach a file handler. By default, do not propagate + to avoid console duplication; set `propagate=True` if you want bubbling. + - If `log_file_path` is not provided: no console by default unless `console=True` or + `EXGENTIC_CONSOLE_LOG` is truthy. Optional default file sink via context run dir. + - Console handlers use Rich when available, falling back to a standard stream handler. + + Callers decide explicitly if they want console output by passing `console=True`. + """ + logger = logging.getLogger(name) + + if logger.handlers: + return logger + + settings = get_settings() + log_level = logging._nameToLevel.get(settings.log_level.upper(), logging.INFO) + logger.setLevel(log_level) + + formatter = logging.Formatter("%(levelname)s | %(message)s") + + if log_file_path: + # File-targeted logger + log_file = Path(log_file_path) + try: + log_file.parent.mkdir(parents=True, exist_ok=True) + fh = logging.FileHandler(log_file, encoding="utf-8") + except OSError: + # Fall back to console logging when file logging isn't writable. + ch = _build_console_handler(log_level=logging.INFO, formatter=formatter) + logger.addHandler(ch) + if propagate is None: + propagate = False + logger.propagate = propagate + return logger + fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + logger.addHandler(fh) + + # Optional console mirroring if explicitly requested + if bool(console): + ch = _build_console_handler(log_level=logging.INFO, formatter=formatter) + logger.addHandler(ch) + + # Default: avoid bubbling to parent to prevent duplicates + if propagate is None: + propagate = False + logger.propagate = propagate + else: + # Non-file logger + enable_console_env = os.environ.get("EXGENTIC_CONSOLE_LOG", "").lower() in ( + "1", + "true", + "yes", + ) + if bool(console) or enable_console_env: + ch = _build_console_handler(log_level=logging.INFO, formatter=formatter) + logger.addHandler(ch) + + # Optional default file sink (single consolidated log) if requested + ctx = try_get_context() + if ctx is not None: + lf = Path(ctx.output_dir) / "exgentic.log" # global log, not run-scoped + try: + lf.parent.mkdir(parents=True, exist_ok=True) + fh = logging.FileHandler(lf, encoding="utf-8") + except OSError: + fh = None + if fh is not None: + fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + logger.addHandler(fh) + + # Default propagation for non-file logger: True unless explicitly set + if propagate is not None: + logger.propagate = propagate + + return logger + + +def get_disabled_logger(name: str | None = None) -> logging.Logger: + """Return a logger that discards all messages (NullHandler, no propagation).""" + lname = name or f"{__name__}.noop" + logger = logging.getLogger(lname) + if not any(isinstance(h, logging.NullHandler) for h in logger.handlers): + logger.addHandler(logging.NullHandler()) + logger.propagate = False + logger.setLevel(logging.CRITICAL) + return logger + + +def close_logger(log: logging.Logger) -> None: + """Close and detach all file handlers from the given logger.""" + handlers_to_close = list(log.handlers) + + for handler in handlers_to_close: + if isinstance(handler, logging.FileHandler): + handler.close() + log.removeHandler(handler) + + +def configure_warnings_logging( + run_dir_base: str | None = None, + run_id: str | None = None, + *, + replace_existing_file_handlers: bool = True, +) -> str: + """Set up logging for Python warnings to a `warnings.log` file. + + - Computes `//run/warnings.log` using provided args or env/settings. + - Enables `logging.captureWarnings(True)`. + - Optionally replaces existing FileHandlers on the `py.warnings` logger. + + Returns the path to the warnings log file as a string. + """ + from ...utils.paths import RunPaths + + ctx = try_get_context() + if ctx is not None: + base = run_dir_base or ctx.output_dir + else: + settings = get_settings() + base = run_dir_base or settings.output_dir + if run_id is None: + ctx = _try_get_context_for_run_id() + rid = ctx.run_id if ctx is not None else "default" + else: + rid = run_id + warnings_path = RunPaths(run_id=rid, output_dir=base).warnings + warnings_path.parent.mkdir(parents=True, exist_ok=True) + + logging.captureWarnings(True) + wlogger = logging.getLogger("py.warnings") + + # Remove conflicting handlers per requested policy + if replace_existing_file_handlers: + for h in list(wlogger.handlers): + if isinstance(h, logging.FileHandler): + wlogger.removeHandler(h) + try: + h.close() + except Exception: + pass + else: + for h in list(wlogger.handlers): + try: + if isinstance(h, logging.FileHandler) and h.baseFilename == str(warnings_path): + wlogger.removeHandler(h) + try: + h.close() + except Exception: + pass + except Exception: + pass + + wfh = logging.FileHandler(str(warnings_path), encoding="utf-8") + wfh.setLevel(logging.WARNING) + wfh.setFormatter(logging.Formatter("%(levelname)s | %(message)s")) + wlogger.addHandler(wfh) + wlogger.setLevel(logging.WARNING) + wlogger.propagate = False + return str(warnings_path) + + +def configure_uvicorn_file_logging(log_path: Path, *, thread_id: int) -> Callable[[], None]: + handler = logging.FileHandler(log_path, encoding="utf-8") + handler.setLevel(logging.INFO) + handler.setFormatter( + DefaultFormatter( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + handler.addFilter(lambda record: record.thread == thread_id) + + uvicorn_loggers = [ + logging.getLogger("uvicorn"), + logging.getLogger("uvicorn.error"), + logging.getLogger("uvicorn.access"), + ] + prev_logger_state: Dict[logging.Logger, tuple[int, bool, list[logging.Handler]]] = {} + for lg in uvicorn_loggers: + removed_handlers = list(lg.handlers) + for h in removed_handlers: + lg.removeHandler(h) + prev_logger_state[lg] = (lg.level, lg.propagate, removed_handlers) + lg.addHandler(handler) + lg.setLevel(logging.INFO) + lg.propagate = False + + def _cleanup() -> None: + for lg in uvicorn_loggers: + lg.removeHandler(handler) + prev_state = prev_logger_state.get(lg) + if prev_state is not None: + lg.setLevel(prev_state[0]) + lg.propagate = prev_state[1] + for h in prev_state[2]: + if h not in lg.handlers: + lg.addHandler(h) + handler.close() + + return _cleanup + + +def configure_library_file_logging( + log_path: Path, *, logger_names: list[str], thread_id: int | None = None +) -> Callable[[], None]: + handler = logging.FileHandler(log_path, encoding="utf-8") + handler.setLevel(logging.INFO) + handler.setFormatter( + logging.Formatter( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + if thread_id is not None: + handler.addFilter(lambda record: record.thread == thread_id) + + prefixes = tuple(logger_names) + names = set(logger_names) + for name in logging.root.manager.loggerDict: + if name.startswith(prefixes): + names.add(name) + + prev_logger_state: Dict[logging.Logger, tuple[int, bool, list[logging.Handler]]] = {} + for name in names: + lg = logging.getLogger(name) + removed_handlers = list(lg.handlers) + for h in removed_handlers: + lg.removeHandler(h) + prev_logger_state[lg] = (lg.level, lg.propagate, removed_handlers) + lg.addHandler(handler) + lg.setLevel(logging.INFO) + lg.propagate = False + + def _cleanup() -> None: + for lg, (level, propagate, removed) in prev_logger_state.items(): + lg.removeHandler(handler) + lg.setLevel(level) + lg.propagate = propagate + for h in removed: + if h not in lg.handlers: + lg.addHandler(h) + handler.close() + + return _cleanup + + +def attach_library_logger_to_handler( + library_logger_name: str, + handler: logging.Handler, + *, + level: int = logging.DEBUG, + propagate: bool = False, +) -> Tuple[logging.Logger, List[logging.Handler], bool]: + """Attach a library logger to the given handler. + + Returns a tuple of (logger, previous_handlers, previous_propagate) so callers + can restore the original configuration later. + """ + logger = logging.getLogger(library_logger_name) + prev_handlers = list(logger.handlers) + prev_propagate = logger.propagate + + handler.setLevel(level) + logger.handlers = [handler] + logger.propagate = propagate + + return logger, prev_handlers, prev_propagate + + +def restore_library_logger( + logger: logging.Logger, + handlers: List[logging.Handler], + propagate: bool, +) -> None: + """Restore a library logger's handlers and propagation flag.""" + logger.handlers = handlers + logger.propagate = propagate + + +def add_loguru_file_sink(file_obj, level: str = "DEBUG", colorize: bool = False): + """Add a Loguru sink for the given file-like object. + + Returns the sink id, or None if Loguru is not available. + """ + try: + from loguru import logger as _loguru # type: ignore[import-not-found] + except Exception: + return None + + try: + return _loguru.add(file_obj, level=level, colorize=colorize) + except Exception: + return None + + +def remove_loguru_sink(sink_id) -> None: + """Remove a previously registered Loguru sink, ignoring errors.""" + if sink_id is None: + return + + try: + from loguru import logger as _loguru # type: ignore[import-not-found] + except Exception: + return + + try: + _loguru.remove(sink_id) + except Exception: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/observers/tracing/__init__.py b/labs/AgentStream/exgentic/src/exgentic/observers/tracing/__init__.py new file mode 100644 index 00000000..ed18cc54 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/observers/tracing/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +__all__ = [] diff --git a/labs/AgentStream/exgentic/src/exgentic/testing/__init__.py b/labs/AgentStream/exgentic/src/exgentic/testing/__init__.py new file mode 100644 index 00000000..d9d5fd04 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/testing/__init__.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Lightweight test fixtures for integration testing. + +These classes are intentionally kept inside the installed package so they +are available inside Docker containers (where ``tests.*`` is not installed). +They are *not* registered in the benchmark/agent registry by default. +""" + +from .agent import ( + BAD_ACTION_TYPE, + FINISH_ACTION_TYPE, + GOOD_ACTION_TYPE, + BadAction, + EmptyArgs, + FinishAction, + GoodAction, + TestAgent, + TestAgentInstance, +) +from .benchmark import TestBenchmark, TestEvaluator, TestSession +from .calculator import Calculator, CalculatorError +from .docker_session import DockerSession + +__all__ = [ + "BAD_ACTION_TYPE", + "BadAction", + "Calculator", + "CalculatorError", + "DockerSession", + "EmptyArgs", + "FINISH_ACTION_TYPE", + "FinishAction", + "GOOD_ACTION_TYPE", + "GoodAction", + "TestAgent", + "TestAgentInstance", + "TestBenchmark", + "TestEvaluator", + "TestSession", +] diff --git a/labs/AgentStream/exgentic/src/exgentic/testing/agent.py b/labs/AgentStream/exgentic/src/exgentic/testing/agent.py new file mode 100644 index 00000000..f9dd2ca9 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/testing/agent.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import hashlib +import random +from typing import Any, ClassVar, Literal + +from pydantic import BaseModel + +from ..core.agent import Agent +from ..core.agent_instance import AgentInstance +from ..core.types import Action, ActionType, SingleAction, SingleObservation + + +class EmptyArgs(BaseModel): + pass + + +class GoodAction(SingleAction): + name: Literal["good"] = "good" + arguments: EmptyArgs + + +class BadAction(SingleAction): + name: Literal["bad"] = "bad" + arguments: EmptyArgs + + +class FinishAction(SingleAction): + name: Literal["finish"] = "finish" + arguments: EmptyArgs + + +GOOD_ACTION_TYPE = ActionType( + name="good", + description="Good action", + cls=GoodAction, +) +BAD_ACTION_TYPE = ActionType( + name="bad", + description="Bad action", + cls=BadAction, +) +FINISH_ACTION_TYPE = ActionType( + name="finish", + description="Finish action", + cls=FinishAction, + is_finish=True, +) + + +class TestAgentInstance(AgentInstance): + __test__ = False + + def __init__( + self, + *, + session_id: str, + seed: int, + policy: str, + finish_after: int, + max_steps: int | None, + ) -> None: + super().__init__(session_id=session_id) + self._seed = seed + self._policy = policy + self._finish_after = finish_after + self._rng = self._build_rng(session_id, seed) + self._step = 0 + self.max_steps = max_steps + + @staticmethod + def _build_rng(session_id: str, seed: int) -> random.Random: + payload = f"{session_id}:{seed}".encode() + digest = hashlib.sha256(payload).digest() + value = int.from_bytes(digest[:4], "big") + return random.Random(value) + + def react(self, observation: SingleObservation | None) -> Action | None: + self._step += 1 + if self._policy == "return_none": + return None + if self._policy == "raise_error": + raise RuntimeError("agent failure") + if self._policy == "invalid_action": + return "not-an-action" # type: ignore[return-value] + if self._policy == "finish_immediately": + return FinishAction(arguments=EmptyArgs()) + if self._policy == "good_only": + return GoodAction(arguments=EmptyArgs()) + if self._policy == "bad_only": + return BadAction(arguments=EmptyArgs()) + if self._policy == "good_then_finish": + if self._step >= self._finish_after: + return FinishAction(arguments=EmptyArgs()) + return GoodAction(arguments=EmptyArgs()) + # random policy + choice = self._rng.choice([GOOD_ACTION_TYPE, BAD_ACTION_TYPE, FINISH_ACTION_TYPE]) + if choice is GOOD_ACTION_TYPE: + return GoodAction(arguments=EmptyArgs()) + if choice is BAD_ACTION_TYPE: + return BadAction(arguments=EmptyArgs()) + return FinishAction(arguments=EmptyArgs()) + + def close(self) -> None: + return None + + +class TestAgent(Agent): + __test__ = False + display_name: ClassVar[str] = "Test Agent" + slug_name: ClassVar[str] = "test_agent" + runner: str | None = "direct" # No external deps — run in host process + + @classmethod + def _get_instance_class(cls): + return TestAgentInstance + + seed: int = 0 + policy: Literal[ + "random", + "good_only", + "bad_only", + "good_then_finish", + "finish_immediately", + "return_none", + "invalid_action", + "raise_error", + ] = "random" + finish_after: int = 2 + max_steps: int | None = None + + def _get_instance_kwargs( + self, + session_id: str, + ) -> dict[str, Any]: + return { + "session_id": session_id, + "seed": self.seed, + "policy": self.policy, + "finish_after": self.finish_after, + "max_steps": self.max_steps, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/testing/benchmark.py b/labs/AgentStream/exgentic/src/exgentic/testing/benchmark.py new file mode 100644 index 00000000..4d9dd22d --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/testing/benchmark.py @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from typing import Any, ClassVar + +from ..core.benchmark import Benchmark +from ..core.evaluator import Evaluator +from ..core.session import Session +from ..core.types import ( + Action, + ActionType, + BenchmarkResults, + SessionIndex, + SessionScore, + SingleObservation, +) +from .agent import ( + BAD_ACTION_TYPE, + FINISH_ACTION_TYPE, + GOOD_ACTION_TYPE, + BadAction, + FinishAction, + GoodAction, +) + + +class TestSession(Session): + __test__ = False + + def __init__( + self, + *, + task_id: str, + session_id: str, + stop_on_step: bool, + invalid_observation: bool, + ) -> None: + self._session_id = session_id + self._task_id = task_id + self._stop_on_step = stop_on_step + self._invalid_observation = invalid_observation + self._done = False + self._good = 0 + self._bad = 0 + self._steps = 0 + super().__init__() + + @property + def task_id(self) -> str: + return self._task_id + + @property + def task(self) -> str: + return f"Task {self._task_id}" + + @property + def context(self) -> dict[str, Any]: + return {"task_id": self._task_id} + + @property + def actions(self) -> list[ActionType]: + return [GOOD_ACTION_TYPE, BAD_ACTION_TYPE, FINISH_ACTION_TYPE] + + def start(self) -> SingleObservation: + return SingleObservation(result="start") + + def step(self, action: Action) -> SingleObservation | None: + self._steps += 1 + if self._invalid_observation: + return "invalid-observation" # type: ignore[return-value] + if isinstance(action, GoodAction): + self._good += 1 + elif isinstance(action, BadAction): + self._bad += 1 + elif isinstance(action, FinishAction): + self._done = True + return SingleObservation(result="finish") + if self._stop_on_step: + return None + return SingleObservation(result="step") + + def done(self) -> bool: + return self._done + + def score(self) -> SessionScore: + total = self._good + self._bad + score = float(self._good / total) if total > 0 else 0.0 + success = bool(self._done and self._bad == 0 and total > 0) + result = SessionScore( + score=score, + success=success, + is_finished=self._done, + session_metrics={"good": self._good, "bad": self._bad, "total": total}, + session_metadata={"steps": self._steps}, + ) + self.save_standard_results(result) + return result + + def get_config(self) -> dict[str, Any]: + return { + "task_id": self._task_id, + "stop_on_step": self._stop_on_step, + } + + def close(self) -> None: + return None + + +class TestEvaluator(Evaluator): + __test__ = False + + def __init__( + self, + *, + tasks: list[str] | None = None, + stop_on_step: bool = False, + invalid_observation: bool = False, + ) -> None: + self._tasks = tasks or ["task-1", "task-2", "task-3"] + self._stop_on_step = stop_on_step + self._invalid_observation = invalid_observation + + def list_tasks(self) -> list[str]: + return list(self._tasks) + + def get_session_kwargs(self, index: SessionIndex) -> dict[str, Any]: + return { + "task_id": str(index.task_id), + "session_id": index.session_id, + "stop_on_step": self._stop_on_step, + "invalid_observation": self._invalid_observation, + } + + def aggregate_sessions(self, sessions: list[SessionIndex]) -> BenchmarkResults: + scores: list[float] = [] + for paths in self.get_sessions_paths(sessions): + if not paths.results.exists(): + continue + payload = json.loads(paths.results.read_text(encoding="utf-8")) + try: + score = float(payload["score"]) + except Exception: + continue + scores.append(score) + avg = sum(scores) / len(scores) if scores else 0.0 + return BenchmarkResults( + benchmark_name="test_benchmark", + total_tasks=len(sessions), + score=avg, + metrics={}, + ) + + +class TestBenchmark(Benchmark): + __test__ = False + display_name: ClassVar[str] = "Test Benchmark" + slug_name: ClassVar[str] = "test_benchmark" + runner: str | None = "direct" # Run in-process; override via benchmark_kwargs for runner tests + tasks: list[str] = ["task-1", "task-2", "task-3"] # noqa: RUF012 + + @classmethod + def _get_evaluator_class(cls): + return TestEvaluator + + @classmethod + def _get_session_class(cls): + return TestSession + + stop_on_step: bool = False + invalid_observation: bool = False + + def _get_evaluator_kwargs(self) -> dict[str, Any]: + return { + "tasks": self.tasks, + "stop_on_step": self.stop_on_step, + "invalid_observation": self.invalid_observation, + } diff --git a/labs/AgentStream/exgentic/src/exgentic/testing/calculator.py b/labs/AgentStream/exgentic/src/exgentic/testing/calculator.py new file mode 100644 index 00000000..4a8c1b3b --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/testing/calculator.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Dummy Calculator class for runner/transport tests. + +Lives inside the installed package so it is importable inside Docker +containers (where ``tests.*`` is not installed). +""" + +from __future__ import annotations + +import os +import threading + + +class CalculatorError(Exception): + """Custom exception for testing cross-transport error propagation.""" + + def __init__(self, message: str, code: int = 0) -> None: + super().__init__(message) + self.code = code + + +class Calculator: + """Dummy target for transport tests.""" + + def __init__(self, value: int = 0) -> None: + self.value = value + + def add(self, a: int, b: int) -> int: + return a + b + + def accumulate(self, n: int) -> int: + self.value += n + return self.value + + def divide(self, a: int, b: int) -> float: + return a / b + + def fail_custom(self) -> None: + raise CalculatorError("something went wrong", code=42) + + def thread_id(self) -> int: + return threading.get_ident() + + def pid(self) -> int: + return os.getpid() + + def echo(self, obj: object) -> object: + return obj diff --git a/labs/AgentStream/exgentic/src/exgentic/testing/docker_session.py b/labs/AgentStream/exgentic/src/exgentic/testing/docker_session.py new file mode 100644 index 00000000..ce17b442 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/testing/docker_session.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Minimal session-like object for Docker e2e tests. + +Not a real Session subclass — avoids pulling in Session.__init__ +which writes files. We only need the method/property interface that +the ObjectProxy will forward over HTTP. +""" + +from __future__ import annotations + +import os + + +class DockerSession: + """Minimal session-like object for Docker e2e tests.""" + + def __init__(self, task_id: str, output_dir: str | None = None) -> None: + self._task_id = task_id + self._done = False + self._good = 0 + self._steps = 0 + self._output_dir = output_dir + + @property + def task_id(self) -> str: + return self._task_id + + @property + def task(self) -> str: + return f"Task {self._task_id}" + + @property + def context(self) -> dict: + return {"task_id": self._task_id} + + def start(self) -> dict: + return {"result": "start"} + + def step(self, action_name: str) -> dict: + self._steps += 1 + if action_name == "good": + self._good += 1 + return {"result": "step"} + if action_name == "finish": + self._done = True + return {"result": "finish"} + return {"result": "step"} + + def done(self) -> bool: + return self._done + + def score(self) -> dict: + total = self._good + return {"score": 1.0 if total > 0 else 0.0, "success": self._done and total > 0} + + def write_output(self, filename: str, content: str) -> str: + """Write a file to the output dir. Used to verify volume mounts.""" + out = self._output_dir or os.environ.get("EXGENTIC_OUTPUT_DIR", "/tmp") + path = os.path.join(out, filename) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write(content) + return path + + def close(self) -> None: + pass diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/__init__.py b/labs/AgentStream/exgentic/src/exgentic/utils/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/cost.py b/labs/AgentStream/exgentic/src/exgentic/utils/cost.py new file mode 100644 index 00000000..2b01ead8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/cost.py @@ -0,0 +1,222 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from typing import Any, Optional, Self, Sequence, TypeVar + +from pydantic import BaseModel, computed_field + +name_map = {"claude-3-5-haiku": "claude-3-5-haiku-20241022"} + + +class TokensCost(BaseModel): + input_cost: float + output_cost: float + total_cost: float + + +def _cost_per_token(*, model: str, prompt_tokens: int, completion_tokens: int): + from litellm.cost_calculator import cost_per_token + + return cost_per_token(model=model, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens) + + +def litellm_cost_per_token(model_name: str): + return _cost_per_token(model=model_name, prompt_tokens=1, completion_tokens=1) + + +def litellm_tokens_cost(input_tokens: int, output_tokens: int, model_name: str) -> TokensCost: + for src, dst in name_map.items(): + model_name = model_name.replace(src, dst) + + parts = model_name.lower().split("/") + for i in range(len(parts)): + model = "/".join(parts[-i:]) + try: + input_cost, output_cost = _cost_per_token( + model=model, prompt_tokens=input_tokens, completion_tokens=output_tokens + ) + return TokensCost( + input_cost=input_cost, + output_cost=output_cost, + total_cost=input_cost + output_cost, + ) + except Exception: + input_cost, output_cost, total_cost = None, None, None + continue + return TokensCost( + input_cost=input_cost or 0.0, + output_cost=output_cost or 0.0, + total_cost=total_cost or 0.0, + ) + + +class CostReport(BaseModel): + _total_cost: float = 0 + model_name: str = "" + + @classmethod + def initialize_empty(cls, model_name: str = "") -> Self: + """Create an empty cost report with zero costs.""" + return cls(model_name=model_name) + + @computed_field + @property + def total_cost(self) -> float: + return self._total_cost + + def accumulate_from(self, other: Self) -> None: + # Accumulate by total_cost only + self._total_cost += float(other.total_cost) + + +class UpdatableCostReport(CostReport): + """A simple accumulator that supports adding arbitrary cost amounts.""" + + def add_cost(self, new_cost: float) -> None: + self._total_cost += new_cost + + +class LLMCostReport(CostReport): + """Represents a cost report for LLM usage. + + Usage: + # Explicit definition + report = CostReport(model_name="gpt-4", input_tokens=100, output_tokens=50, input_cost=0.02, output_cost=0.03) + + # Empty report + empty_report = CostReport.initialize_empty("gpt-4") + """ + + input_tokens: int + output_tokens: int + input_cost: float + output_cost: float + + @classmethod + def initialize_empty(cls, model_name: str = "") -> Self: + """Create an empty cost report with zero tokens and costs.""" + return cls( + model_name=model_name, + input_tokens=0, + output_tokens=0, + input_cost=0, + output_cost=0, + ) + + @computed_field + @property + def total_cost(self) -> float: + return self.input_cost + self.output_cost + + @computed_field + @property + def total_tokens(self) -> float: + return self.input_tokens + self.output_tokens + + def update_cost(self, input_tokens, output_tokens, input_cost, output_cost) -> None: + """Update the report with additional tokens and costs.""" + self.input_tokens += input_tokens + self.output_tokens += output_tokens + self.input_cost += input_cost + self.output_cost += output_cost + + def accumulate_from(self, other: Self) -> None: + """Accumulate costs and tokens from other.""" + self.input_tokens += int(other.input_tokens) + self.output_tokens += int(other.output_tokens) + self.input_cost += float(other.input_cost) + self.output_cost += float(other.output_cost) + + +class LiteLLMCostReport(LLMCostReport): + """Specialized cost report that calculates costs using LiteLLM pricing. + + Additional Features: + - Auto-calculates cost if not provided. + - Provides helper methods to update cost from token counts. + + Usage: + report = LiteLLMCostReport.from_token_counts("gpt-4", 100, 50) + report.update_cost_from_tokens(20, 10) + """ + + output_cost: Optional[float] = None + input_cost: Optional[float] = None + + def model_post_init(self, __context: Any) -> None: + if self.output_cost is None or self.input_cost is None: + cost_data = LiteLLMCostReport.get_litellm_tokens_cost( + self.input_tokens, self.output_tokens, model_name=self.model_name + ) + self.output_cost = cost_data.output_cost + self.input_cost = cost_data.input_cost + + @classmethod + def from_token_counts(cls, model_name, input_tokens, output_tokens) -> "LiteLLMCostReport": + """Create a report from token counts, auto-calculating costs.""" + cost_data = cls.get_litellm_tokens_cost(input_tokens, output_tokens, model_name=model_name) + return cls( + model_name=model_name, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_cost=cost_data.input_cost, + output_cost=cost_data.output_cost, + ) + + def update_cost_from_tokens(self, new_input_tokens, new_output_tokens): + """Update cost based on new token counts.""" + new_cost_data = LiteLLMCostReport.get_litellm_tokens_cost(new_input_tokens, new_output_tokens, self.model_name) + self.update_cost( + new_input_tokens, + new_output_tokens, + new_cost_data.input_cost, + new_cost_data.output_cost, + ) + + @classmethod + def get_litellm_tokens_cost(cls, input_tokens, output_tokens, model_name) -> TokensCost: + """Fetch cost data from LiteLLM pricing API.""" + if input_tokens == 0 and output_tokens == 0: + return TokensCost(input_cost=0, output_cost=0, total_cost=0) + return litellm_tokens_cost(input_tokens, output_tokens, model_name=model_name) + + +T = TypeVar("T", bound=CostReport) + + +def accumulate_reports(reports: Sequence[T]) -> T: + """Accumulate a sequence of same-typed cost reports into a single report of the same type. + + Behavior: + - Enforces that all items are of the same concrete type. + - Preserves `model_name` if identical across items; else uses 'mixed'. + - Uses the report's own `accumulate_from` implementation to merge. + + Args: + reports: non-empty sequence of reports, all of the same type. + + Returns: + A new report of the same type, containing the accumulated data. + + Raises: + ValueError: if the list is empty or contains mixed types. + """ + if not reports: + raise ValueError("The reports list cannot be empty.") + + first = reports[0] + first_type = type(first) + + if not all(type(r) is first_type for r in reports): + raise ValueError("All reports must be of the same concrete type.") + + # Preserve model_name if consistent; otherwise use 'mixed' + first_model_name = first.model_name + same_model_name = all(r.model_name == first_model_name for r in reports) + acc_report = first_type.initialize_empty(model_name=first_model_name if same_model_name else "mixed") + + # Fold via the type's own accumulation logic + for r in reports: + acc_report.accumulate_from(r) + + return acc_report diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/disk_cache.py b/labs/AgentStream/exgentic/src/exgentic/utils/disk_cache.py new file mode 100644 index 00000000..07754d52 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/disk_cache.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import threading +from pathlib import Path +from typing import Any, ClassVar, Optional + +from diskcache import Cache, JSONDisk + + +class DiskCacheSessionMixin: + """Mixin that provides disk-backed caching for benchmark sessions. + + Subclasses should set `CACHE_DIR` to control the cache location and + implement the abstract hooks: + - build_cache_key_payload() -> Dict[str, Any] + - on_cache_hit(payload) -> bool + - prepare_cache_payload(score) -> Dict[str, Any] + """ + + CACHE_DIR: ClassVar[str] = "./exgentic_session_cache" + _cache_local: ClassVar[threading.local] = threading.local() + logger: Any | None = None + use_cache: bool = True + _cache_hit: bool = False + _cache_payload: Optional[dict[str, Any]] = None + _cached_score: Optional[dict[str, Any]] = None + _results_payload: Optional[dict[str, Any]] = None + + def _init_cache_mixin(self, use_cache: bool = True) -> None: + self.use_cache = use_cache + self._cache_hit = False + self._cache_payload: Optional[dict[str, Any]] = None + self._cached_score: Optional[dict[str, Any]] = None + self._results_payload: Optional[dict[str, Any]] = None + + # --- Logging helpers ------------------------------------------------- + def _log_debug(self, message: str) -> None: + if self.logger: + self.logger.debug(message) + + def _log_info(self, message: str) -> None: + if self.logger: + self.logger.info(message) + + def _log_warning(self, message: str) -> None: + if self.logger: + self.logger.warning(message) + + # --- Cache internals ------------------------------------------------- + @classmethod + def _cache(cls) -> Cache: + # Use thread-local storage so each thread gets its own Cache instance. + # diskcache supports concurrent access from separate Cache objects but + # a single Cache (wrapping one SQLite connection) cannot be shared + # across threads. + attr = f"_cache_{cls.__name__}" + cache = getattr(cls._cache_local, attr, None) + if cache is None: + Path(cls.CACHE_DIR).mkdir(parents=True, exist_ok=True) + cache = Cache(cls.CACHE_DIR, disk=JSONDisk) + setattr(cls._cache_local, attr, cache) + return cache + + def build_cache_key_payload(self) -> dict[str, Any]: + raise NotImplementedError + + def build_cache_key(self) -> Optional[str]: + try: + payload = self.build_cache_key_payload() + return json.dumps(payload, sort_keys=True, default=str) + except TypeError as exc: + self._log_warning(f"Failed to serialize cache key payload: {exc}") + return None + + def on_cache_hit(self, payload: dict[str, Any]) -> bool: + """Hook invoked when cache payload is loaded. Return False to ignore.""" + return True + + def prepare_cache_payload(self, score: dict[str, Any]) -> dict[str, Any]: + """Hook used to create payload to store in cache.""" + payload: dict[str, Any] = {"score": score} + results_payload = self.get_results_payload() + if results_payload is not None: + payload["results"] = results_payload + metadata = self.build_additional_cache_metadata() + if metadata: + payload["metadata"] = metadata + return payload + + def build_additional_cache_metadata(self) -> dict[str, Any]: + """Hook for subclasses to include extra metadata in the cache entry.""" + return {} + + # --- Public helpers -------------------------------------------------- + @property + def cache_hit(self) -> bool: + return self._cache_hit + + @property + def cache_payload(self) -> Optional[dict[str, Any]]: + return self._cache_payload + + @property + def cached_score(self) -> Optional[dict[str, Any]]: + return self._cached_score + + def set_results_payload(self, payload: Optional[dict[str, Any]]) -> None: + self._results_payload = payload + + def get_results_payload(self) -> Optional[dict[str, Any]]: + return self._results_payload + + def handle_cache_start(self) -> bool: + return self.maybe_load_from_cache() + + def maybe_load_from_cache(self) -> bool: + if not self.use_cache: + return False + cache_key = self.build_cache_key() + if not cache_key: + return False + payload = self._cache().get(cache_key) + if payload is None: + self._log_debug("No cached entry for session.") + return False + if not self.on_cache_hit(payload): + self._log_warning("Cache payload rejected by session hook.") + return False + self._set_cached_score(payload.get("score")) + self.set_results_payload(payload.get("results")) + self._cache_hit = True + self._cache_payload = payload + self._log_info(f"Reusing session results found in disk cache at {self.CACHE_DIR}") + return True + + def write_cache_entry(self, payload: dict[str, Any]) -> None: + if not self.use_cache: + return + cache_key = self.build_cache_key() + if not cache_key: + return + try: + self._cache()[cache_key] = payload + self._cache_payload = payload + self._log_info(f"Saving session results to cache at {self.CACHE_DIR}") + except Exception as exc: # pragma: no cover - defensive + self._log_warning(f"Failed to write cache entry: {exc}") + + def cache_score(self, score: dict[str, Any]) -> None: + self._set_cached_score(score) + payload = self.prepare_cache_payload(score) + self.write_cache_entry(payload) + + def restore_cache_payload(self) -> None: + payload = self.cache_payload + if payload: + self.on_cache_restore(payload) + + def on_cache_restore(self, payload: dict[str, Any]) -> None: + """Optional hook to restore artifacts from cache.""" + return + + def _set_cached_score(self, score: Optional[dict[str, Any]]) -> None: + self._cached_score = score diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/logging.py b/labs/AgentStream/exgentic/src/exgentic/utils/logging.py new file mode 100644 index 00000000..4b3b7fd8 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/logging.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +# exgentic/benchmarks/swebench/util.py +import logging +import sys +from contextlib import contextmanager + + +class SessionLogHandler(logging.Handler): + """Forward logs from other loggers into an Exgentic session logger, with source label.""" + + def __init__(self, session_logger: logging.Logger): + super().__init__() + self.session_logger = session_logger + + def emit(self, record: logging.LogRecord) -> None: + msg = self.format(record) + source = record.name or "logger" + self.session_logger.log(record.levelno, f"[{source}] {msg}") + + +def hook_loggers_into_session( + session_logger: logging.Logger, + logger_names: list[str], + level: int = logging.INFO, +) -> None: + """Redirect logs from the given logger names into session_logger. + + Each line will be prefixed with [logger_name]. + """ + handler = SessionLogHandler(session_logger) + # You can add a formatter if you want timestamps inside msg + # handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + + for name in logger_names: + lg = logging.getLogger(name) + lg.handlers.clear() # prevent duplicate stdout noise + lg.addHandler(handler) + lg.setLevel(level) + lg.propagate = False + + +class StreamToLogger: + """File-like object that sends writes to session_logger with a given source label.""" + + def __init__(self, session_logger: logging.Logger, source: str, level: int): + self.logger = session_logger + self.source = source + self.level = level + self._buf = "" + + def write(self, message: str) -> None: + if not message: + return + self._buf += message + while "\n" in self._buf: + line, self._buf = self._buf.split("\n", 1) + line = line.rstrip("\r") + if line.strip(): + self.logger.log(self.level, f"[{self.source}] {line}") + + def flush(self) -> None: + if self._buf.strip(): + self.logger.log(self.level, f"[{self.source}] {self._buf.strip()}") + self._buf = "" + + +@contextmanager +def capture_stdio_to_session( + session_logger: logging.Logger, + stdout_level: int = logging.INFO, + stderr_level: int = logging.WARNING, +): + """Redirect sys.stdout and sys.stderr into session_logger. + + Lines from stdout are prefixed with [stdout], from stderr with [stderr]. + """ + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout = StreamToLogger(session_logger, "stdout", stdout_level) + sys.stderr = StreamToLogger(session_logger, "stderr", stderr_level) + try: + yield + finally: + sys.stdout.flush() + sys.stderr.flush() + sys.stdout, sys.stderr = old_stdout, old_stderr diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/otel.py b/labs/AgentStream/exgentic/src/exgentic/utils/otel.py new file mode 100644 index 00000000..f0cb6834 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/otel.py @@ -0,0 +1,532 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Utility functions for OpenTelemetry initialization and logging. + +This module provides OTEL setup functions and structured logging for OTEL operations. +""" + +import base64 +import json +import os +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path, PurePath +from typing import Any, Dict, Mapping, Optional, Sequence, Union + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter as GrpcOTLPSpanExporter, +) +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter as HttpOTLPSpanExporter, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor +from opentelemetry.sdk.trace.id_generator import IdGenerator +from opentelemetry.trace import Tracer +from opentelemetry.util.types import AttributeValue + +OTEL_SPAN_ATTRIBUTE_NAMESPACE = "exgentic" + + +class UrandomIdGenerator(IdGenerator): + """ID generator using os.urandom for unique random IDs.""" + + def generate_span_id(self) -> int: + return int.from_bytes(os.urandom(8), "big") + + def generate_trace_id(self) -> int: + return int.from_bytes(os.urandom(16), "big") + + +def init_tracing_from_env( + service_name: Optional[str] = None, + use_urandom_ids: bool = True, + use_simple_processor: bool = True, +) -> Tracer: + """Initialize OpenTelemetry tracing from environment variables. + + This function is idempotent - if a TracerProvider is already set, it will not + reinitialize and will just return a tracer. + + Args: + service_name: Optional service name override. Defaults to OTEL_SERVICE_NAME env var or "exgentic". + use_urandom_ids: Whether to use UrandomIdGenerator for span/trace IDs. Default True. + use_simple_processor: If True, use SimpleSpanProcessor (immediate export) instead of BatchSpanProcessor. + Useful for subprocesses that may exit before batch export completes. + + Returns: + A Tracer instance. + + Env vars honored by the SDK / exporters include (non-exhaustive): + - OTEL_SERVICE_NAME + - OTEL_SERVICE_VERSION + - OTEL_RESOURCE_ATTRIBUTES + - OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG + - OTEL_EXPORTER_OTLP_{ENDPOINT,HEADERS,PROTOCOL} + - OTEL_EXPORTER_OTLP_TRACES_{ENDPOINT,HEADERS,PROTOCOL,COMPRESSION,TIMEOUT} + """ + # Check if tracer provider is already set + current_provider = trace.get_tracer_provider() + # ProxyTracerProvider means no real provider is set + if type(current_provider).__name__ != "ProxyTracerProvider": + # Already initialized, just return a tracer + return trace.get_tracer(__name__) + + # Initialize the tracer provider + resource = Resource.create( + { + "service.name": service_name or os.getenv("OTEL_SERVICE_NAME", "exgentic"), + "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), + "service.namespace": os.getenv("OTEL_SERVICE_NAMESPACE", "exgentic"), + "deployment.environment.name": os.getenv("DEPLOYMENT_ENVIRONMENT", "dev"), + } + ) + + id_generator = UrandomIdGenerator() if use_urandom_ids else None + provider = TracerProvider(resource=resource, id_generator=id_generator) + trace.set_tracer_provider(provider) + + protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf").strip().lower() + exporter = GrpcOTLPSpanExporter() if protocol == "grpc" else HttpOTLPSpanExporter() + + # Use SimpleSpanProcessor for immediate export (useful for subprocesses) + # or BatchSpanProcessor for better performance (default) + if use_simple_processor: + provider.add_span_processor(SimpleSpanProcessor(exporter)) + else: + provider.add_span_processor(BatchSpanProcessor(exporter)) + + return trace.get_tracer(__name__) + + +def check_otel_collector_health(timeout: int = 5) -> tuple[bool, Optional[str]]: + """Check if the OTEL collector endpoint is reachable and protocol matches. + + Verifies: + 1. Endpoint is reachable (socket connection) + 2. Protocol (HTTP/gRPC) matches what the server supports + + Args: + timeout: Connection timeout in seconds (default: 5) + + Returns: + Tuple of (is_healthy, error_message) + - (True, None) if collector is reachable and protocol matches + - (False, error_message) if collector is not reachable or protocol mismatch + """ + import socket + from urllib.parse import urlparse + + # Get endpoint and protocol from environment + protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf").strip().lower() + + # Try traces-specific endpoint first, fall back to general endpoint + endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + + if not endpoint: + return False, "OTEL_EXPORTER_OTLP_ENDPOINT not specified" + + host = "Unknown" + port = 0 + try: + parsed = urlparse(endpoint) + host = parsed.hostname + port = parsed.port + + if not host or not port: + return False, f"Invalid endpoint URL: {endpoint}" + + # Step 1: Check if endpoint is reachable via socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + + if result != 0: + return False, f"Cannot connect to OTEL collector at {host}:{port}" + + # Step 2: Verify protocol matches by attempting a minimal request + if protocol.startswith("http"): + # For HTTP protocol, try a simple HTTP request to verify it's an HTTP server + try: + import http.client + + # Determine if we should use HTTPS + use_https = parsed.scheme == "https" + conn_class = http.client.HTTPSConnection if use_https else http.client.HTTPConnection + + conn = conn_class(host, port, timeout=timeout) + # Try to access the OTLP traces endpoint + conn.request("POST", "/v1/traces", headers={"Content-Type": "application/x-protobuf"}) + response = conn.getresponse() + conn.close() + + # We expect either 200 (OK), 400 (bad request - empty body), or 405 (method not allowed) + # What we DON'T want is connection refused or protocol errors + if response.status in (200, 400, 405, 415): # 415 = Unsupported Media Type + return True, None + return False, f"HTTP endpoint responded with unexpected status: {response.status}" + + except http.client.HTTPException as e: + return False, f"HTTP protocol error: {e!s}. Server {endpoint} may not support HTTP protocol." + except Exception as e: + # If we can connect via socket but HTTP fails, likely a protocol mismatch + return False, f"Protocol mismatch: configured as HTTP but server {endpoint} may be gRPC. Error: {e!s}" + + elif protocol == "grpc": + # For gRPC, attempt a basic protocol check + # We'll try to send a minimal gRPC frame to verify it's a gRPC server + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + + # Send HTTP/2 connection preface followed by SETTINGS frame + # This is what a real gRPC client sends + preface = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + # HTTP/2 SETTINGS frame: length=0, type=4, flags=0, stream_id=0 + settings_frame = b"\x00\x00\x00\x04\x00\x00\x00\x00\x00" + sock.sendall(preface + settings_frame) + + # Try to receive a response + # A gRPC/HTTP2 server MUST respond with a SETTINGS frame + sock.settimeout(2) # Short timeout for response + response = sock.recv(1024) + sock.close() + + # Validate we got a proper HTTP/2 response + # HTTP/2 frames start with 3-byte length, 1-byte type, 1-byte flags, 4-byte stream ID + if len(response) >= 9: + # Check if we got a SETTINGS frame (type=4) or other valid HTTP/2 frame + frame_type = response[3] + if frame_type in (0x04, 0x00, 0x01): # SETTINGS, DATA, or HEADERS frame + return True, None + + # If we got a response but it's not HTTP/2, it's likely HTTP/1.1 + if response: + if response.startswith(b"HTTP/1"): + return ( + False, + f"Server at {endpoint} is HTTP/1.1, not gRPC. Use 'http/protobuf' protocol instead.", + ) + return ( + False, + f"Server at {endpoint} responded but not with valid HTTP/2 frames. May not be a gRPC server.", + ) + + return False, f"gRPC endpoint at {endpoint} did not respond to HTTP/2 preface" + + except socket.timeout: + return ( + False, + f"Timeout waiting for gRPC response from {endpoint}. Server may not support gRPC protocol.", + ) + except Exception as e: + return False, f"gRPC protocol check failed: {e!s}. Server {endpoint} may not support gRPC protocol." + + else: + return False, f"Unknown protocol: {protocol}. Expected 'http/protobuf' or 'grpc'" + + except socket.gaierror: + return False, f"Cannot resolve hostname: {host}" + except socket.timeout: + return False, f"Connection timeout to {host}:{port}" + except Exception as e: + return False, f"Error checking OTEL collector: {e!s}" + + +def flush_traces(timeout_millis: int = 30000) -> bool: + """Flush all pending spans to the configured exporter. + + This ensures that all spans are exported before the process exits or + when you want to guarantee delivery at a specific point (e.g., end of session). + + Args: + timeout_millis: Maximum time to wait for flush to complete, in milliseconds. + Default is 30000 (30 seconds). + + Returns: + True if flush succeeded, False otherwise. + """ + try: + provider = trace.get_tracer_provider() + # Check if we have a real TracerProvider (not ProxyTracerProvider) + if type(provider).__name__ == "ProxyTracerProvider": + return True # No real provider, nothing to flush + return provider.force_flush(timeout_millis) + except Exception: + return False + + +_Primitive = (str, bool, int, float) + + +def _to_primitive_number(x: Any) -> Optional[Union[int, float]]: + """Convert numeric-ish types to plain Python int/float.""" + if isinstance(x, bool): + # bool is a subclass of int; do not coerce. + return None + if isinstance(x, (int, float)): + return x + if isinstance(x, Decimal): + # Prefer float for AttributeValue; fall back to string elsewhere if needed. + return float(x) + return None + + +def _canonical_attr_type(x: Any) -> Optional[type]: + """Return which primitive type x maps to, or None if not primitive.""" + if isinstance(x, bool): + return bool + if isinstance(x, int) and not isinstance(x, bool): + return int + if isinstance(x, float): + return float + if isinstance(x, str): + return str + return None + + +def _json_default(o: Any) -> Any: + """Safe fallback for json.dumps(default=...).""" + # Pydantic v2 + if hasattr(o, "model_dump"): + try: + return o.model_dump() + except Exception: + pass + # Pydantic v1 + if hasattr(o, "dict"): + try: + return o.dict() + except Exception: + pass + # dataclasses + try: + from dataclasses import asdict, is_dataclass + + if is_dataclass(o): + return asdict(o) + except Exception: + pass + # Datetime/Date + if isinstance(o, (datetime, date)): + return o.isoformat() + # Paths + if isinstance(o, (PurePath,)): + return str(o) + # Bytes-like → base64 wrapper so it round-trips + if isinstance(o, (bytes, bytearray, memoryview)): + return {"__bytes_b64__": base64.b64encode(bytes(o)).decode("ascii")} + # Fallback: repr as string + return str(o) + + +def _to_homogeneous_sequence(seq: Sequence[Any]) -> Optional[Sequence[Union[str, bool, int, float]]]: + """Try to coerce a sequence into a homogeneous list of primitives allowed by AttributeValue. + + Returns list on success, None on failure. + """ + arr = list(seq) + + primed = [] + for v in arr: + if v is None: + # None in arrays is not portably supported; bail to JSON outside. [2](https://opentelemetry.io/docs/specs/otel/common/) + return None + if isinstance(v, _Primitive): + primed.append(v) + continue + # Try numeric coercion (Decimal) + num = _to_primitive_number(v) + if num is not None: + primed.append(num) + continue + # Datetimes/paths/bytes -> string + if isinstance(v, (datetime, date, PurePath, bytes, bytearray, memoryview)): + primed.append(str(_json_default(v))) + continue + # Not representable as primitive + return None + + # Check homogeneity (bool must not mix with ints) + types = {_canonical_attr_type(x) for x in primed} + if None in types: + return None + if len(types) == 1: + return primed + # Allow implicit upcast to float when mixing int/float + if types == {int, float}: + return [float(x) for x in primed] + # Mixed types like str+int or bool+int are not allowed for attribute arrays. [2](https://opentelemetry.io/docs/specs/otel/common/) + return None + + +def to_otel_attribute_value(value: Any, *, prefer_json: bool = True) -> Optional[AttributeValue]: + """Convert an arbitrary value into an OpenTelemetry-Python AttributeValue for spans. + + Returns: + - A valid AttributeValue (str|bool|int|float|homogeneous Sequence thereof) on success. + - None if the attribute should be skipped (e.g., value is None). + """ + # 1) None: skip (undefined/strongly discouraged). [3](https://opentelemetry-python.readthedocs.io/en/latest/api/trace.span.html) + if value is None: + return None + + # 2) Accepted primitives + if isinstance(value, _Primitive): + return bool(value) if isinstance(value, bool) else value # type: ignore[return-value] + + # 3) Numeric-like -> int/float + num = _to_primitive_number(value) + if num is not None: + return num # type: ignore[return-value] + + # 4) datetime/date -> ISO string; Path -> str; bytes -> base64 string + if isinstance(value, (datetime, date)): + return value.isoformat() + if isinstance(value, (PurePath,)): + return str(value) + if isinstance(value, (bytes, bytearray, memoryview)): + return base64.b64encode(bytes(value)).decode("ascii") + + # 5) Sequences -> attempt homogeneous primitive array; else JSON + from collections.abc import Sequence as _Seq + + if isinstance(value, _Seq) and not isinstance(value, (str, bytes, bytearray, memoryview)): + coerced = _to_homogeneous_sequence(value) + if coerced is not None: + return coerced # type: ignore[return-value] + if prefer_json: + try: + return json.dumps(value, default=_json_default, ensure_ascii=False, sort_keys=True) + except Exception: + return str(value) + + # 6) Mappings/objects -> JSON string + if isinstance(value, Mapping) or hasattr(value, "__dict__") or hasattr(value, "model_dump"): + try: + return json.dumps(value, default=_json_default, ensure_ascii=False, sort_keys=True) + except Exception: + return str(value) + + # 7) Fallback: string + return str(value) + + +def get_session_logger(session_root: Path, name: str) -> "OtelLogger": + """Get a session-specific OTEL logger. + + Creates a logger that writes to /otel.log with standardized + formatting for OTEL operations across different processes. + + Args: + session_root: Path to the session output directory (not run directory) + name: Name of the logger (e.g., "session_span_manager", "otel_callback") + + Returns: + OtelLogger instance configured for the session + """ + # Import here to avoid circular dependency at module level + from ..observers.logging import get_logger + + log_path = session_root / "otel.log" + # Use session_id to make logger unique per session + # This ensures each session gets its own file handler for otel.log + logger = get_logger(f"{name}.{session_root.name}", str(log_path), console=False) + + return OtelLogger(logger, scope=name) + + +class OtelLogger: + """Structured logger for OpenTelemetry operations. + + Provides standardized logging methods for common OTEL operations like + span creation, attribute setting, context file I/O, etc. + """ + + def __init__(self, logger, scope: str): + self._logger = logger + self._scope = scope + + def _format_message(self, message: str) -> str: + return f"[{self._scope}] {message}" + + def debug(self, message: str) -> None: + self._logger.debug(self._format_message(message)) + + def info(self, message: str) -> None: + self._logger.info(self._format_message(message)) + + def warning(self, message: str) -> None: + self._logger.warning(self._format_message(message)) + + def error(self, message: str) -> None: + self._logger.error(self._format_message(message)) + + # Standardized OTEL operation logging methods + + def log_tracer_init(self, tracer_params: Dict[str, Any]) -> None: + params_str = ", ".join(f"{k}={v}" for k, v in tracer_params.items()) + self.info(f"TRACER_INIT | {params_str}") + + def log_span_start( + self, + span_name: str, + span_id: str, + trace_id: str, + parent_span_id: Optional[str] = None, + is_root: bool = False, + depth: Optional[int] = None, + start_time: Optional[datetime] = None, + ) -> None: + root_marker = " [ROOT]" if is_root else "" + parent_info = f" parent={parent_span_id}" if parent_span_id else "" + depth_info = f" depth={depth}" if depth is not None else "" + timestamp = start_time.strftime("%Y-%m-%d %H:%M:%S.%f") + time_info = f" start_time={timestamp}" if start_time else "" + self.info( + f"SPAN_START{root_marker} | name='{span_name}' id={span_id} trace={trace_id}" + f"{parent_info}{depth_info}{time_info}" + ) + + def log_span_rename(self, old_name: str, new_name: str, span_id: str) -> None: + self.info(f"SPAN_RENAME | '{old_name}' -> '{new_name}' id={span_id}") + + def log_span_end( + self, + span_name: str, + span_id: str, + is_root: bool = False, + status: Optional[str] = None, + depth: Optional[int] = None, + end_time: Optional[datetime] = None, + ) -> None: + root_marker = " [ROOT]" if is_root else "" + status_info = f" status={status}" if status else "" + depth_info = f" depth={depth}" if depth is not None else "" + time_info = f" end_time={end_time.strftime('%Y-%m-%d %H:%M:%S.%f')}" if end_time else "" + self.info(f"SPAN_END{root_marker} | name='{span_name}' id={span_id}{status_info}{depth_info}{time_info}") + + def log_attribute_set(self, key: str, value: Any, span_id: Optional[str] = None) -> None: + span_info = f" span={span_id}" if span_id else "" + # Truncate long values + value_str = str(value) + if len(value_str) > 100: + value_str = value_str[:97] + "..." + self.debug(f"ATTR_SET | {key}={value_str}{span_info}") + + def log_exception(self, exc: Exception, context: Optional[str] = None) -> None: + context_str = f" context={context}" if context else "" + self.error(f"EXCEPTION | {type(exc).__name__}: {exc}{context_str}") + + def log_context_update(self, trace_id: Optional[str], span_id: Optional[str], operation: str = "update") -> None: + """Log OTEL context update operation.""" + self.debug(f"CONTEXT_{operation.upper()} | trace={trace_id} span={span_id}") + + def log_context_read(self, trace_id: str, span_id: str) -> None: + """Log OTEL context read operation.""" + self.debug(f"CONTEXT_READ | trace={trace_id} span={span_id}") diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/paths.py b/labs/AgentStream/exgentic/src/exgentic/utils/paths.py new file mode 100644 index 00000000..e0c771fb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/paths.py @@ -0,0 +1,192 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +_WINDOWS_FORBIDDEN = set('<>:"/\\|?*') + + +def sanitize_path_component(value: str) -> str: + cleaned = "".join("_" if ch in _WINDOWS_FORBIDDEN else ch for ch in value) + cleaned = cleaned.rstrip(" .") + return cleaned or "run" + + +@dataclass(frozen=True) +class SessionPaths: + """All filesystem paths for a single session. + + Stores resolved values — no lazy lookups, no context dependency. + """ + + session_id: str + run_id: str + output_dir: Path + + def __post_init__(self) -> None: + if not isinstance(self.output_dir, Path): + object.__setattr__(self, "output_dir", Path(self.output_dir)) + + @classmethod + def from_context(cls, ctx) -> SessionPaths: + if ctx.session_id is None: + raise ValueError("Context has no session_id") + return cls(session_id=ctx.session_id, run_id=ctx.run_id, output_dir=ctx.output_dir) + + @property + def root(self) -> Path: + return self.output_dir / self.run_id / "sessions" / self.session_id + + @property + def results(self) -> Path: + return self.root / "results.json" + + @property + def trajectory(self) -> Path: + return self.root / "trajectory.jsonl" + + @property + def benchmark_dir(self) -> Path: + return self.root / "benchmark" + + @property + def agent_dir(self) -> Path: + return self.root / "agent" + + @property + def benchmark_results(self) -> Path: + return self.benchmark_dir / "results.json" + + @property + def benchmark_config(self) -> Path: + return self.benchmark_dir / "config.json" + + @property + def benchmark_task(self) -> Path: + return self.benchmark_dir / "task.json" + + @property + def benchmark_context(self) -> Path: + return self.benchmark_dir / "context.json" + + @property + def session_manifest(self) -> Path: + return self.root / "session.json" + + @property + def session_config(self) -> Path: + return self.root / "config.json" + + @property + def session_log(self) -> Path: + return self.benchmark_dir / "session.log" + + @property + def agent_log(self) -> Path: + return self.agent_dir / "agent.log" + + @property + def error_log(self) -> Path: + return self.root / "error.log" + + @property + def summary(self) -> Path: + return self.root / "summary.json" + + @property + def otel_log(self) -> Path: + return self.root / "otel.log" + + @property + def lock(self) -> Path: + return self.root / "session.lock" + + +@dataclass(frozen=True) +class RunPaths: + """All filesystem paths for a single run. + + Stores resolved values — no lazy lookups, no context dependency. + """ + + run_id: str + output_dir: Path + + def __post_init__(self) -> None: + if not isinstance(self.output_dir, Path): + object.__setattr__(self, "output_dir", Path(self.output_dir)) + + @classmethod + def from_context(cls, ctx) -> RunPaths: + return cls(run_id=ctx.run_id, output_dir=ctx.output_dir) + + @property + def root(self) -> Path: + return self.output_dir / self.run_id + + @property + def sessions_root(self) -> Path: + return self.root / "sessions" + + @property + def run_dir(self) -> Path: + return self.root / "run" + + @property + def results(self) -> Path: + return self.root / "results.json" + + @property + def benchmark_results(self) -> Path: + return self.root / "benchmark_results.json" + + @property + def tracker(self) -> Path: + return self.run_dir / "run.log" + + @property + def warnings(self) -> Path: + return self.run_dir / "warnings.log" + + @property + def config(self) -> Path: + return self.run_dir / "config.json" + + def session(self, session_id: str) -> SessionPaths: + return SessionPaths(session_id=session_id, run_id=self.run_id, output_dir=self.output_dir) + + +# --------------------------------------------------------------------------- +# Convenience accessors — thin wrappers over get_context() +# --------------------------------------------------------------------------- + + +def get_run_id() -> str: + """Return the current run ID from context.""" + from ..core.context import get_context + + return get_context().run_id + + +def get_run_paths() -> RunPaths: + """Return RunPaths for the current context.""" + from ..core.context import get_context + + return RunPaths.from_context(get_context()) + + +def get_session_paths(session_id: str | None = None) -> SessionPaths: + """Return SessionPaths for the given (or current) session. + + If *session_id* is ``None``, uses the session ID from the current context. + """ + from ..core.context import get_context + + ctx = get_context() + sid = session_id or ctx.session_id + if sid is None: + raise ValueError("No session_id provided and none in context") + return RunPaths.from_context(ctx).session(sid) diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/settings.py b/labs/AgentStream/exgentic/src/exgentic/utils/settings.py new file mode 100644 index 00000000..cb818439 --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/settings.py @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +from dotenv import load_dotenv +from pydantic_settings import BaseSettings, SettingsConfigDict + +DOTENV_PATH = Path(os.environ.get("EXGENTIC_DOTENV_PATH", ".env")) + +load_dotenv(DOTENV_PATH) + +if TYPE_CHECKING: + from ..integrations.litellm.config import LitellmSettings + +RunnerName = Literal["direct", "thread", "process", "service", "docker", "venv"] +LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + +def resolve_cache_path(cache_dir: str, cache_path: str) -> str: + base = Path(cache_dir).expanduser() + target = Path(cache_path).expanduser() + if target.is_absolute(): + return str(target) + return str(base / target) + + +class ExgenticSettings(BaseSettings): + """Global settings for Exgentic.""" + + default_runner: RunnerName = "venv" + output_dir: str = "./outputs" + # Logging level (env: EXGENTIC_LOG_LEVEL) + log_level: LogLevel = "INFO" + debug: bool = False + litellm_log_level: LogLevel = "WARNING" + litellm_delete_time_from_cache_key: bool = False + litellm_caching: bool = True + cache_dir: str = ".exgentic" + litellm_cache_dir: str = "~/.cache/exgentic/litellm" + dotenv_path: str = str(DOTENV_PATH) + otel_enabled: bool = False + otel_record_content: bool = False + + _litellm_cache_configured: bool = False + + def resolved_litellm_cache_dir(self) -> str: + return resolve_cache_path(self.cache_dir, self.litellm_cache_dir) + + def get_env(self) -> dict[str, str]: + """Return env vars for all settings fields (EXGENTIC_*).""" + env: dict[str, str] = {} + prefix = self.model_config.get("env_prefix") or "" + for name, _field in type(self).model_fields.items(): + if name.startswith("_"): + continue + value = getattr(self, name, None) + if value is None: + continue + key = f"{prefix}{name}".upper() + if isinstance(value, bool): + env[key] = "true" if value else "false" + else: + env[key] = str(value) + return env + + def get_overrides(self) -> dict[str, Any]: + """Return settings values that differ from class defaults.""" + overrides: dict[str, Any] = {} + for name, field in type(self).model_fields.items(): + if name.startswith("_"): + continue + default = field.default + current = getattr(self, name, None) + if current != default: + overrides[name] = current + return overrides + + def model_post_init(self, __context) -> None: + if self.debug and self.log_level != "DEBUG": + object.__setattr__(self, "log_level", "DEBUG") + elif self.log_level == "DEBUG" and not self.debug: + object.__setattr__(self, "debug", True) + self.configure_litellm() + object.__setattr__(self, "_litellm_cache_configured", True) + + def configure_litellm(self, *, cache_only: bool = False) -> None: + from ..integrations.litellm.config import configure_litellm + + configure_litellm(config=self.to_litellm_config(), cache_only=cache_only) + + def to_litellm_config(self) -> LitellmSettings: + from ..integrations.litellm.config import LitellmSettings + + return LitellmSettings( + litellm_caching=self.litellm_caching, + litellm_delete_time_from_cache_key=self.litellm_delete_time_from_cache_key, + cache_dir=self.cache_dir, + litellm_cache_dir=self.litellm_cache_dir, + log_level=self.litellm_log_level, + ) + + def __setattr__(self, name: str, value: Any) -> None: + prev_value = self.__dict__.get(name, None) + super().__setattr__(name, value) + if name == "debug" and value != prev_value and bool(value): + if self.log_level != "DEBUG": + object.__setattr__(self, "log_level", "DEBUG") + if name == "log_level" and value != prev_value: + is_debug = str(value).upper() == "DEBUG" + if self.__dict__.get("debug") != is_debug: + object.__setattr__(self, "debug", is_debug) + if ( + name + in { + "litellm_delete_time_from_cache_key", + "litellm_caching", + "cache_dir", + "litellm_cache_dir", + } + and self._litellm_cache_configured + and value != prev_value + ): + self.configure_litellm(cache_only=True) + object.__setattr__(self, "_litellm_cache_configured", True) + if name == "litellm_log_level" and value != prev_value: + self.configure_litellm(cache_only=False) + + model_config = SettingsConfigDict( + env_prefix="EXGENTIC_", + case_sensitive=False, + ) + + +@lru_cache(maxsize=1) +def get_settings() -> ExgenticSettings: + return ExgenticSettings() # type: ignore[arg-type] diff --git a/labs/AgentStream/exgentic/src/exgentic/utils/sync.py b/labs/AgentStream/exgentic/src/exgentic/utils/sync.py new file mode 100644 index 00000000..7dca3fcb --- /dev/null +++ b/labs/AgentStream/exgentic/src/exgentic/utils/sync.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import asyncio +import threading +from typing import Any, Coroutine, Optional + +_loop: Optional[asyncio.AbstractEventLoop] = None +_thread: Optional[threading.Thread] = None +_ready = threading.Event() +_lock = threading.Lock() + + +def _loop_thread_main() -> None: + global _loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + _loop = loop + _ready.set() + loop.run_forever() + + +def run_sync(coro: Coroutine[Any, Any, Any], timeout: float | None = None) -> Any: + """Run an async coroutine from sync code using ONE long-lived event loop. + + Safe from any thread in this process. + + Do not call from an async context. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + raise RuntimeError("run_sync() called from a running event loop; use `await` instead") + + global _loop, _thread + with _lock: + if _loop is None or not _loop.is_running(): + _ready.clear() + _thread = threading.Thread(target=_loop_thread_main, name="exgentic-async-loop", daemon=True) + _thread.start() + + if not _ready.wait(timeout=5.0) or _loop is None: + raise RuntimeError("Failed to start shared asyncio loop") + + fut = asyncio.run_coroutine_threadsafe(coro, _loop) + return fut.result(timeout=timeout) diff --git a/labs/AgentStream/exgentic/tests/__init__.py b/labs/AgentStream/exgentic/tests/__init__.py new file mode 100644 index 00000000..a445052e --- /dev/null +++ b/labs/AgentStream/exgentic/tests/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Test package marker.""" diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/__init__.py b/labs/AgentStream/exgentic/tests/adapters/runners/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/conftest.py b/labs/AgentStream/exgentic/tests/adapters/runners/conftest.py new file mode 100644 index 00000000..b70dd0f3 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/conftest.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Shared fixtures for runner/transport tests. + +The ``Calculator`` class and the parametrized ``calc`` fixture are used +across all transport test modules so that every transport is verified +against the exact same behavioural contract. +""" + +from __future__ import annotations + +import pytest +from exgentic.adapters.runners import with_runner +from exgentic.testing.calculator import Calculator, CalculatorError + +# Re-export so existing test imports keep working. +__all__ = ["Calculator", "CalculatorError"] + +# Runners available for the current milestone. +_AVAILABLE_RUNNERS = ["direct", "thread", "process", "service"] + + +@pytest.fixture(params=_AVAILABLE_RUNNERS) +def runner_name(request): + return request.param + + +@pytest.fixture +def calc(runner_name): + proxy = with_runner(Calculator, runner=runner_name, value=10) + yield proxy + try: + proxy.close() + except Exception: + pass diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_docker.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_docker.py new file mode 100644 index 00000000..e5741d07 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_docker.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Docker runner tests (skipped when Docker is unavailable).""" + +from __future__ import annotations + +import shutil +import subprocess + +import pytest +from exgentic.adapters.runners import with_runner + +from .conftest import Calculator + +# Skip entire module if docker is not available. +_docker_available = shutil.which("docker") is not None +if _docker_available: + try: + subprocess.run(["docker", "info"], check=True, capture_output=True, timeout=5) + except Exception: + _docker_available = False + +pytestmark = [ + pytest.mark.skipif(not _docker_available, reason="Docker not available"), +] + + +@pytest.fixture(scope="module") +def docker_calc(): + """Shared docker-backed Calculator (building is slow).""" + proxy = with_runner( + Calculator, + runner="docker", + env_name="tests/calculator", + module_path="exgentic.testing.calculator", + value=10, + ) + yield proxy + try: + proxy.close() + except Exception: + pass + + +def test_call_method(docker_calc): + assert docker_calc.add(2, 3) == 5 + + +def test_accumulate(docker_calc): + assert docker_calc.accumulate(5) == 15 + + +def test_get_attribute(docker_calc): + assert docker_calc.value == 15 + + +def test_set_attribute(docker_calc): + docker_calc.value = 42 + assert docker_calc.value == 42 + + +def test_error_propagation(docker_calc): + with pytest.raises(ZeroDivisionError): + docker_calc.divide(1, 0) + + +def test_echo(docker_calc): + assert docker_calc.echo({"key": [1, 2, 3]}) == {"key": [1, 2, 3]} diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_e2e_session.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_e2e_session.py new file mode 100644 index 00000000..5571be5c --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_e2e_session.py @@ -0,0 +1,289 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""End-to-end tests — run a full session lifecycle through every transport. + +Uses the test fixtures (TestSession, TestAgent) to verify that the complete +benchmark→session→agent loop works over each runner/transport layer. +""" + +from __future__ import annotations + +import platform +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest +from exgentic.adapters.runners import with_runner +from exgentic.testing import ( + BadAction, + DockerSession, + EmptyArgs, + FinishAction, + GoodAction, + TestAgent, + TestSession, +) + +# Detect Docker availability for conditional tests. +_docker_available = shutil.which("docker") is not None +if _docker_available: + try: + subprocess.run(["docker", "info"], check=True, capture_output=True, timeout=5) + except Exception: + _docker_available = False + +_RUNNERS = ["direct", "thread", "process", "service"] + + +@pytest.fixture(params=_RUNNERS) +def runner_name(request): + return request.param + + +@pytest.fixture +def session_proxy(runner_name, tmp_path, monkeypatch): + """Create a TestSession wrapped in the specified runner.""" + monkeypatch.setenv("EXGENTIC_OUTPUT_DIR", str(tmp_path)) + proxy = with_runner( + TestSession, + runner=runner_name, + task_id="task-1", + session_id="sess-e2e-001", + stop_on_step=False, + invalid_observation=False, + ) + yield proxy + try: + proxy.close() + except Exception: + pass + + +# ── basic lifecycle ────────────────────────────────────────────────── + + +class TestSessionLifecycle: + """Full start → step → done → score lifecycle across transports.""" + + def test_start_returns_observation(self, session_proxy): + obs = session_proxy.start() + assert obs.result == "start" + + def test_step_good_action(self, session_proxy): + session_proxy.start() + obs = session_proxy.step(GoodAction(arguments=EmptyArgs())) + assert obs.result == "step" + + def test_done_false_before_finish(self, session_proxy): + session_proxy.start() + session_proxy.step(GoodAction(arguments=EmptyArgs())) + assert session_proxy.done() is False + + def test_finish_action_marks_done(self, session_proxy): + session_proxy.start() + session_proxy.step(GoodAction(arguments=EmptyArgs())) + obs = session_proxy.step(FinishAction(arguments=EmptyArgs())) + assert obs.result == "finish" + assert session_proxy.done() is True + + def test_score_after_good_and_finish(self, session_proxy): + session_proxy.start() + session_proxy.step(GoodAction(arguments=EmptyArgs())) + session_proxy.step(FinishAction(arguments=EmptyArgs())) + result = session_proxy.score() + assert result.score == 1.0 + assert result.success is True + + def test_score_no_actions(self, session_proxy): + session_proxy.start() + session_proxy.step(FinishAction(arguments=EmptyArgs())) + result = session_proxy.score() + assert result.score == 0.0 + assert result.success is False + + +# ── property access over transports ────────────────────────────────── + + +class TestPropertyAccess: + """Verify that property reads work transparently across transports.""" + + def test_task_property(self, session_proxy): + assert session_proxy.task == "Task task-1" + + def test_task_id_property(self, session_proxy): + assert session_proxy.task_id == "task-1" + + def test_context_property(self, session_proxy): + ctx = session_proxy.context + assert ctx == {"task_id": "task-1"} + + def test_actions_property(self, session_proxy): + actions = session_proxy.actions + assert len(actions) == 3 + names = {a.name for a in actions} + assert names == {"good", "bad", "finish"} + + +# ── agent integration ──────────────────────────────────────────────── + + +class TestAgentWithRunnerSession: + """Run a TestAgent against a session through each transport.""" + + def test_good_then_finish_policy(self, session_proxy): + agent = TestAgent(policy="good_then_finish", finish_after=3) + instance = agent._get_instance_class()( + **agent._get_instance_kwargs(session_id="sess-e2e-001"), + ) + instance.start( + task=session_proxy.task, + context=session_proxy.context, + actions=session_proxy.actions, + ) + + obs = session_proxy.start() + steps = 0 + while not session_proxy.done() and steps < 10: + action = instance.react(obs) + if action is None: + break + obs = session_proxy.step(action) + steps += 1 + + assert session_proxy.done() is True + result = session_proxy.score() + assert result.success is True + assert result.score == 1.0 + + def test_finish_immediately_policy(self, session_proxy): + agent = TestAgent(policy="finish_immediately") + instance = agent._get_instance_class()( + **agent._get_instance_kwargs(session_id="sess-e2e-001"), + ) + instance.start( + task=session_proxy.task, + context=session_proxy.context, + actions=session_proxy.actions, + ) + + obs = session_proxy.start() + action = instance.react(obs) + session_proxy.step(action) + + assert session_proxy.done() is True + result = session_proxy.score() + assert result.score == 0.0 + + +# ── stateful consistency ───────────────────────────────────────────── + + +class TestStatefulConsistency: + """Multiple steps keep consistent state across transports.""" + + def test_multiple_good_actions(self, session_proxy): + session_proxy.start() + for _ in range(5): + obs = session_proxy.step(GoodAction(arguments=EmptyArgs())) + assert obs.result == "step" + session_proxy.step(FinishAction(arguments=EmptyArgs())) + result = session_proxy.score() + assert result.score == 1.0 + assert result.session_metrics["good"] == 5 + assert result.session_metrics["total"] == 5 + + def test_mixed_actions(self, session_proxy): + session_proxy.start() + session_proxy.step(GoodAction(arguments=EmptyArgs())) + session_proxy.step(BadAction(arguments=EmptyArgs())) + session_proxy.step(GoodAction(arguments=EmptyArgs())) + session_proxy.step(FinishAction(arguments=EmptyArgs())) + result = session_proxy.score() + assert result.score == pytest.approx(2 / 3) + assert result.success is False # had bad actions + + +# ── Docker transport (skipped when Docker unavailable) ─────────────── +# +# Uses DockerSession from exgentic.testing — a minimal session-like +# object that is part of the installed package and therefore importable +# inside the Docker container. + + +@pytest.mark.skipif(not _docker_available, reason="Docker not available") +class TestDockerSessionE2E: + """Full session lifecycle over the Docker transport. + + Uses a class-scoped fixture so the image is built only once. + """ + + @pytest.fixture(scope="class") + def docker_session(self): + # Rancher Desktop / Docker Desktop on macOS only share /Users/ by + # default via reverse-sshfs. pytest's tmp_path lives under + # /var/folders/ which is NOT shared, so volume mounts silently fail. + # Use a temp dir under $HOME to ensure Docker can mount it. + if platform.system() == "Darwin": + out = Path(tempfile.mkdtemp(prefix=".exgentic_test_", dir=Path.home())) + else: + out = Path(tempfile.mkdtemp(prefix="exgentic_test_")) + import os + + old_output_dir = os.environ.get("EXGENTIC_OUTPUT_DIR") + os.environ["EXGENTIC_OUTPUT_DIR"] = str(out) + proxy = with_runner( + DockerSession, + runner="docker", + env_name="test/docker-e2e", + module_path="", + task_id="task-1", + output_dir=str(out), + volumes={str(out): str(out)}, + ) + yield proxy, out + try: + proxy.close() + except Exception: + pass + if old_output_dir is None: + os.environ.pop("EXGENTIC_OUTPUT_DIR", None) + else: + os.environ["EXGENTIC_OUTPUT_DIR"] = old_output_dir + shutil.rmtree(out, ignore_errors=True) + + def test_start(self, docker_session): + proxy, _ = docker_session + obs = proxy.start() + assert obs["result"] == "start" + + def test_step_and_finish(self, docker_session): + proxy, _ = docker_session + obs = proxy.step("good") + assert obs["result"] == "step" + obs = proxy.step("finish") + assert obs["result"] == "finish" + assert proxy.done() is True + + def test_score(self, docker_session): + proxy, _ = docker_session + result = proxy.score() + assert result["score"] == 1.0 + assert result["success"] is True + + def test_properties(self, docker_session): + proxy, _ = docker_session + assert proxy.task_id == "task-1" + assert proxy.task == "Task task-1" + assert proxy.context == {"task_id": "task-1"} + + def test_volume_mount_output_visible_on_host(self, docker_session): + """Verify that files written inside the container are visible on the host.""" + proxy, out = docker_session + proxy.write_output("test_result.txt", "hello from docker") + result_file = out / "test_result.txt" + assert result_file.exists(), "Output file written in container not visible on host" + assert result_file.read_text() == "hello from docker" diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_process.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_process.py new file mode 100644 index 00000000..40e621cb --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_process.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Process-specific tests that verify actual process isolation.""" + +from __future__ import annotations + +import gc +import os + +import pytest +from exgentic.adapters.runners import with_runner + +from .conftest import Calculator + + +def _make_process_calc(**kwargs): + try: + return with_runner(Calculator, runner="process", **kwargs) + except PermissionError: + pytest.skip("multiprocessing semaphores not available") + + +def test_runs_in_different_pid(): + calc = _make_process_calc(value=0) + try: + assert calc.pid() != os.getpid() + finally: + calc.close() + + +def test_crash_isolation(): + """Errors in the remote process don't crash the proxy.""" + calc = _make_process_calc(value=0) + try: + with pytest.raises(ZeroDivisionError): + calc.divide(1, 0) + assert calc.add(1, 2) == 3 + finally: + calc.close() + # Force cleanup of multiprocessing resources to avoid interference + # with subsequent thread tests (CPython 3.11 bug workaround). + gc.collect() diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_thread.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_thread.py new file mode 100644 index 00000000..757929ea --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_thread.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Thread-specific tests that verify actual thread isolation.""" + +from __future__ import annotations + +import threading + +from exgentic.adapters.runners import with_runner + +from .conftest import Calculator + + +def test_runs_in_different_thread(): + calc = with_runner(Calculator, runner="thread", value=0) + try: + remote_tid = calc.thread_id() + local_tid = threading.get_ident() + assert remote_tid != local_tid + finally: + calc.close() + + +def test_runs_in_same_pid(): + import os + + calc = with_runner(Calculator, runner="thread", value=0) + try: + assert calc.pid() == os.getpid() + finally: + calc.close() + + +def test_close_joins_thread(): + calc = with_runner(Calculator, runner="thread", value=0) + transport = object.__getattribute__(calc, "_transport") + thread = transport._thread + assert thread is not None and thread.is_alive() + calc.close() + assert not thread.is_alive() diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_transport.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_transport.py new file mode 100644 index 00000000..52c26c7c --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_transport.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Behavioural contract tests — run against every available transport. + +When a new transport is added, append it to ``_AVAILABLE_RUNNERS`` +in ``conftest.py`` and all tests here will automatically cover it. +""" + +from __future__ import annotations + +import pytest + +# ── method calls ───────────────────────────────────────────────────── + + +def test_call_method(calc): + assert calc.add(2, 3) == 5 + + +def test_call_method_kwargs(calc): + assert calc.add(a=2, b=3) == 5 + + +def test_call_method_mixed_args(calc): + assert calc.add(2, b=3) == 5 + + +# ── stateful operations ───────────────────────────────────────────── + + +def test_accumulate(calc): + assert calc.accumulate(5) == 15 # started at 10 + assert calc.accumulate(5) == 20 + + +# ── attribute access ───────────────────────────────────────────────── + + +def test_get_attribute(calc): + assert calc.value == 10 + + +def test_set_attribute(calc): + calc.value = 42 + assert calc.value == 42 + + +# ── error propagation ──────────────────────────────────────────────── + + +def test_builtin_error(calc): + with pytest.raises(ZeroDivisionError): + calc.divide(1, 0) + + +def test_attribute_error(calc): + with pytest.raises(AttributeError): + _ = calc.nonexistent_attribute + + +def test_custom_exception(calc): + """Custom (non-builtin) exceptions preserve type and attributes.""" + from .conftest import CalculatorError + + with pytest.raises(CalculatorError) as exc_info: + calc.fail_custom() + assert "something went wrong" in str(exc_info.value) + assert exc_info.value.code == 42 + + +def test_remote_traceback(calc): + """Non-direct transports attach __remote_traceback__.""" + try: + calc.divide(1, 0) + except ZeroDivisionError as exc: + if hasattr(exc, "__remote_traceback__"): + assert isinstance(exc.__remote_traceback__, str) + + +# ── echo / serialization ──────────────────────────────────────────── + + +def test_echo_int(calc): + assert calc.echo(42) == 42 + + +def test_echo_string(calc): + assert calc.echo("hello") == "hello" + + +def test_echo_list(calc): + assert calc.echo([1, 2, 3]) == [1, 2, 3] + + +def test_echo_dict(calc): + assert calc.echo({"a": 1}) == {"a": 1} + + +def test_echo_none(calc): + assert calc.echo(None) is None diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_utils.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_utils.py new file mode 100644 index 00000000..6f59a288 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_utils.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for :mod:`exgentic.adapters.runners._utils`.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from exgentic.adapters.runners._utils import find_project_root + + +def test_find_project_root_returns_repo_root(): + """When a pyproject.toml exists in a parent, return that directory.""" + root = find_project_root() + assert (root / "pyproject.toml").exists() + + +def test_find_project_root_falls_back_to_dot_exgentic(tmp_path: Path): + """When no pyproject.toml is found, fall back to ~/.exgentic/.""" + fake_home = tmp_path / "home" + fake_home.mkdir() + + # Create a fake __file__ path with no pyproject.toml in any parent + fake_file = tmp_path / "lib" / "pkg" / "mod.py" + fake_file.parent.mkdir(parents=True) + fake_file.touch() + + with ( + patch( + "exgentic.adapters.runners._utils.Path.__file__", + create=True, + ), + patch( + "exgentic.adapters.runners._utils.Path.home", + return_value=fake_home, + ), + ): + # Patch __file__ at the module level so Path(__file__) resolves + # to a location without pyproject.toml in any ancestor. + import exgentic.adapters.runners._utils as mod + + original_file = mod.__file__ + try: + mod.__file__ = str(fake_file) + result = find_project_root() + finally: + mod.__file__ = original_file + + expected = fake_home / ".exgentic" + assert result == expected + assert expected.is_dir() + + +def test_find_project_root_fallback_is_idempotent(tmp_path: Path): + """Calling find_project_root twice with fallback doesn't error.""" + fake_home = tmp_path / "home" + fake_home.mkdir() + + fake_file = tmp_path / "lib" / "mod.py" + fake_file.parent.mkdir(parents=True) + fake_file.touch() + + with patch( + "exgentic.adapters.runners._utils.Path.home", + return_value=fake_home, + ): + import exgentic.adapters.runners._utils as mod + + original_file = mod.__file__ + try: + mod.__file__ = str(fake_file) + result1 = find_project_root() + result2 = find_project_root() + finally: + mod.__file__ = original_file + + assert result1 == result2 + assert result1 == fake_home / ".exgentic" diff --git a/labs/AgentStream/exgentic/tests/adapters/runners/test_venv.py b/labs/AgentStream/exgentic/tests/adapters/runners/test_venv.py new file mode 100644 index 00000000..dea4b84f --- /dev/null +++ b/labs/AgentStream/exgentic/tests/adapters/runners/test_venv.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Venv runner tests (skipped when uv is unavailable).""" + +from __future__ import annotations + +import os +import shutil + +import pytest +from exgentic.adapters.runners import with_runner + +from .conftest import Calculator + +# Skip entire module if uv is not available. +_uv_available = shutil.which("uv") is not None + +pytestmark = [ + pytest.mark.skipif(not _uv_available, reason="uv not available"), +] + + +@pytest.fixture(scope="module") +def venv_calc(): + """Shared venv-backed Calculator (venv creation is slow).""" + proxy = with_runner( + Calculator, + runner="venv", + env_name="tests/calculator", + module_path="exgentic.testing.calculator", + value=10, + ) + yield proxy + try: + proxy.close() + except Exception: + pass + + +def test_call_method(venv_calc): + assert venv_calc.add(2, 3) == 5 + + +def test_accumulate(venv_calc): + assert venv_calc.accumulate(5) == 15 + + +def test_get_attribute(venv_calc): + assert venv_calc.value == 15 + + +def test_set_attribute(venv_calc): + venv_calc.value = 42 + assert venv_calc.value == 42 + + +def test_error_propagation(venv_calc): + with pytest.raises(ZeroDivisionError): + venv_calc.divide(1, 0) + + +def test_echo(venv_calc): + assert venv_calc.echo({"key": [1, 2, 3]}) == {"key": [1, 2, 3]} + + +def test_different_pid(venv_calc): + """Venv runner should run in a separate process.""" + assert venv_calc.pid() != os.getpid() diff --git a/labs/AgentStream/exgentic/tests/agents/cli/test_claude_cli_config.py b/labs/AgentStream/exgentic/tests/agents/cli/test_claude_cli_config.py new file mode 100644 index 00000000..a787b608 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/agents/cli/test_claude_cli_config.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for Claude Code CLI configuration writing. + +Verifies that the Claude CLI wrapper pre-creates directories that +the Claude Code CLI expects to write into, preventing EACCES errors +when running inside containers with --user flags. +""" + +from __future__ import annotations + +from exgentic.agents.cli.claude.cli import ClaudeCodeCLI, ExecutionBackend + + +def test_settings_config_creates_required_subdirs(tmp_path): + """_write_settings_config must pre-create subdirectories for the container.""" + cli = ClaudeCodeCLI(runner=ExecutionBackend.PROCESS) + cli._write_settings_config(tmp_path) + + claude_dir = tmp_path / ".claude" + assert claude_dir.is_dir() + assert (claude_dir / "settings.json").exists() + + # These directories must be pre-created so the container + # process doesn't need mkdir permissions on .claude/ + for subdir in ("debug", "conversations", "projects", "todos"): + assert (claude_dir / subdir).is_dir(), ( + f".claude/{subdir} must be pre-created to avoid " f"EACCES errors in container environments" + ) diff --git a/labs/AgentStream/exgentic/tests/agents/cli/test_cli_context_env.py b/labs/AgentStream/exgentic/tests/agents/cli/test_cli_context_env.py new file mode 100644 index 00000000..b79e427a --- /dev/null +++ b/labs/AgentStream/exgentic/tests/agents/cli/test_cli_context_env.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from pathlib import Path + +from exgentic.agents.cli.base import ( + BaseCLIConfig, + BaseCLIWrapper, + CLIResult, + ExecutionBackend, +) +from exgentic.core.context import Context, set_context + + +class _DummyRunner: + def __init__(self): + self.env = None + + def run(self, *, cmd, env, cfg_root, config, spawn_error_message, stdin_devnull=False): + self.env = env + return CLIResult(stdout="", stderr="", code=0) + + def close(self) -> None: + return None + + +class _DummyCLI(BaseCLIWrapper): + def build_env(self, *, cfg_root: Path, prompt: str, config: BaseCLIConfig): + return {} + + def build_command(self, *, cfg_root: Path, prompt: str, config: BaseCLIConfig): + return ["echo", "ok"] + + +def test_cli_includes_context_env(): + ctx = Context(run_id="run-cli", output_dir="/tmp/out", cache_dir="/tmp/cache") + set_context(ctx) + + runner = _DummyRunner() + cli = _DummyCLI(runner=ExecutionBackend.PROCESS) + cli.runner = runner + cli.run( + prompt="hi", + config=BaseCLIConfig( + mcp_host="127.0.0.1", + mcp_port=1234, + provider_url="http://example.com", + image="img", + ), + ) + + assert runner.env["EXGENTIC_CTX_RUN_ID"] == "run-cli" diff --git a/labs/AgentStream/exgentic/tests/agents/cli/test_cli_error_surfacing.py b/labs/AgentStream/exgentic/tests/agents/cli/test_cli_error_surfacing.py new file mode 100644 index 00000000..3cd32a0f --- /dev/null +++ b/labs/AgentStream/exgentic/tests/agents/cli/test_cli_error_surfacing.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for CLI error surfacing. + +Verifies that when a CLI agent fails, the stderr/stdout from the process +are included in the error message so they propagate through the +coordinator and session orchestrator to the user. +""" + +from __future__ import annotations + +from exgentic.agents.cli.command_runner import CLIExecutionError + + +def test_cli_execution_error_includes_stderr(): + """CLIExecutionError.__str__() must include stderr output.""" + err = CLIExecutionError( + "CLI exited non-zero (1): some-cmd", + code=1, + stdout="", + stderr="Error: permission denied, mkdir '/work/.claude/debug'", + cmd=["some-cmd"], + ) + msg = str(err) + assert "CLI exited non-zero (1)" in msg + assert "STDERR:" in msg + assert "permission denied" in msg + + +def test_cli_execution_error_includes_stdout(): + """CLIExecutionError.__str__() must include stdout when present.""" + err = CLIExecutionError( + "CLI exited non-zero (2): my-cli", + code=2, + stdout="some useful debug output", + stderr="fatal error occurred", + cmd=["my-cli"], + ) + msg = str(err) + assert "STDERR:" in msg + assert "fatal error occurred" in msg + assert "STDOUT:" in msg + assert "some useful debug output" in msg + + +def test_cli_execution_error_omits_empty_streams(): + """CLIExecutionError.__str__() omits STDERR/STDOUT sections when empty.""" + err = CLIExecutionError( + "CLI exited non-zero (1): cmd", + code=1, + stdout="", + stderr="", + cmd=["cmd"], + ) + msg = str(err) + assert "STDERR:" not in msg + assert "STDOUT:" not in msg + assert msg == "CLI exited non-zero (1): cmd" diff --git a/labs/AgentStream/exgentic/tests/agents/test_tool_calling_utils.py b/labs/AgentStream/exgentic/tests/agents/test_tool_calling_utils.py new file mode 100644 index 00000000..f9e40537 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/agents/test_tool_calling_utils.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from exgentic.agents.litellm_tool_calling.utils import ToolsActionsRegistry +from exgentic.core.actions import ActionsHandler +from exgentic.core.types import SingleAction +from pydantic import BaseModel + + +class EmptyArgs(BaseModel): + pass + + +class DummyAction(SingleAction): + arguments: EmptyArgs + + +def test_unknown_tool_call_name_yields_unknown_action_observation(): + registry = ToolsActionsRegistry(actions=[]) + tool_calls = [{"name": "not_a_tool", "arguments": "{}", "id": "call-1"}] + + action = registry.tool_calls_to_action(tool_calls) + + assert action is not None + assert action.validation.name_valid is False + assert action.validation.error == "Unknown action" + + handler = ActionsHandler() + observation = handler.execute(action) + + assert "Unknown action" in str(observation.result) diff --git a/labs/AgentStream/exgentic/tests/api/__init__.py b/labs/AgentStream/exgentic/tests/api/__init__.py new file mode 100644 index 00000000..4720acd9 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""API test package.""" diff --git a/labs/AgentStream/exgentic/tests/api/conftest.py b/labs/AgentStream/exgentic/tests/api/conftest.py new file mode 100644 index 00000000..797faa61 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/conftest.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Iterator + +import pytest +from exgentic.interfaces import registry +from exgentic.interfaces.registry import RegistryEntry + + +@pytest.fixture(autouse=True) +def register_test_components() -> Iterator[None]: + original_benchmarks = dict(registry.BENCHMARKS) + original_agents = dict(registry.AGENTS) + registry.BENCHMARKS["test_benchmark"] = RegistryEntry( + slug_name="test_benchmark", + display_name="Test Benchmark", + module="exgentic.testing.benchmark", + attr="TestBenchmark", + kind="benchmark", + ) + registry.AGENTS["test_agent"] = RegistryEntry( + slug_name="test_agent", + display_name="Test Agent", + module="exgentic.testing.agent", + attr="TestAgent", + kind="agent", + ) + try: + yield + finally: + registry.BENCHMARKS.clear() + registry.BENCHMARKS.update(original_benchmarks) + registry.AGENTS.clear() + registry.AGENTS.update(original_agents) diff --git a/labs/AgentStream/exgentic/tests/api/fixtures/__init__.py b/labs/AgentStream/exgentic/tests/api/fixtures/__init__.py new file mode 100644 index 00000000..1090f572 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/fixtures/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""API test fixtures.""" diff --git a/labs/AgentStream/exgentic/tests/api/fixtures/test_agent.py b/labs/AgentStream/exgentic/tests/api/fixtures/test_agent.py new file mode 100644 index 00000000..9fadd5aa --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/fixtures/test_agent.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +# Re-export from the installed package so existing tests keep working. +from exgentic.testing.agent import ( + BAD_ACTION_TYPE, + FINISH_ACTION_TYPE, + GOOD_ACTION_TYPE, + BadAction, + EmptyArgs, + FinishAction, + GoodAction, + TestAgent, + TestAgentInstance, +) + +__all__ = [ + "BAD_ACTION_TYPE", + "BadAction", + "EmptyArgs", + "FINISH_ACTION_TYPE", + "FinishAction", + "GOOD_ACTION_TYPE", + "GoodAction", + "TestAgent", + "TestAgentInstance", +] diff --git a/labs/AgentStream/exgentic/tests/api/fixtures/test_benchmark.py b/labs/AgentStream/exgentic/tests/api/fixtures/test_benchmark.py new file mode 100644 index 00000000..91513d62 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/fixtures/test_benchmark.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +# Re-export from the installed package so existing tests keep working. +from exgentic.testing.benchmark import ( + TestBenchmark, + TestEvaluator, + TestSession, +) + +__all__ = [ + "TestBenchmark", + "TestEvaluator", + "TestSession", +] diff --git a/labs/AgentStream/exgentic/tests/api/test_agent_package_integrity.py b/labs/AgentStream/exgentic/tests/api/test_agent_package_integrity.py new file mode 100644 index 00000000..1798b11b --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_agent_package_integrity.py @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for agent/benchmark package integrity. + +These tests catch two classes of bugs that are easy to introduce when +splitting agent/benchmark modules into separate config and instance files: + +1. Missing ``__init__.py`` — without it, ``importlib.resources.files()`` + cannot discover ``requirements.txt`` / ``setup.sh``, so the CLI's + ``needs_setup()`` returns False and dependencies are never installed. + +2. Host-side import of heavy deps — if ``_get_instance_class_ref()`` falls + back to ``_get_instance_class()`` (which does a lazy import), it will + pull heavy third-party packages (litellm, smolagents, openai-agents) + into the host process. Agents with heavy deps must override + ``_get_instance_class_ref()`` to return a string directly. +""" + +from __future__ import annotations + +import importlib +from pathlib import Path + +from exgentic.interfaces.registry import get_agent_entries, get_benchmark_entries, load_agent + + +def test_all_agent_packages_have_init(): + """Every registered agent's package directory must contain __init__.py. + + Without ``__init__.py``, ``importlib.resources.files()`` cannot + locate ``requirements.txt``, breaking auto-setup. + """ + entries = get_agent_entries() + for slug, entry in entries.items(): + # entry.module is like "exgentic.agents.openai.openai_mcp_agent" + parts = entry.module.split(".") + # Check every package directory from the agent package up + for depth in range(3, len(parts)): + package = ".".join(parts[:depth]) + try: + mod = importlib.import_module(package) + except ImportError: + continue + mod_file = getattr(mod, "__file__", None) + if mod_file is None: + # Namespace package — missing __init__.py + raise AssertionError( + f"Agent '{slug}': package '{package}' is a namespace " + f"package (no __init__.py). This breaks " + f"importlib.resources.files() and prevents " + f"requirements.txt discovery." + ) + + +def test_all_benchmark_packages_have_init(): + """Every registered benchmark's package directory must contain __init__.py.""" + entries = get_benchmark_entries() + for slug, entry in entries.items(): + parts = entry.module.split(".") + for depth in range(3, len(parts)): + package = ".".join(parts[:depth]) + try: + mod = importlib.import_module(package) + except ImportError: + continue + mod_file = getattr(mod, "__file__", None) + if mod_file is None: + raise AssertionError( + f"Benchmark '{slug}': package '{package}' is a namespace " + f"package (no __init__.py). This breaks " + f"importlib.resources.files() and prevents " + f"requirements.txt discovery." + ) + + +def test_agent_instance_class_ref_is_valid_string(): + """Every agent's _get_instance_class_ref() must return a 'module:class' string. + + This verifies the ref is well-formed; it does NOT import the module + (which would defeat the purpose of the string ref). + """ + entries = get_agent_entries() + for slug, _entry in entries.items(): + agent_cls = load_agent(slug) + ref = agent_cls._get_instance_class_ref() + assert isinstance(ref, str), ( + f"Agent '{slug}': _get_instance_class_ref() returned " f"{type(ref).__name__}, expected str" + ) + assert ":" in ref, ( + f"Agent '{slug}': _get_instance_class_ref() returned '{ref}', " f"expected 'module:qualname' format" + ) + module_path, qualname = ref.rsplit(":", 1) + assert module_path, f"Agent '{slug}': empty module path in ref '{ref}'" + assert qualname, f"Agent '{slug}': empty qualname in ref '{ref}'" + + +def test_agent_instance_class_ref_module_file_exists(): + """The module referenced by _get_instance_class_ref() must exist on disk. + + This catches typos in string refs without importing the module. + """ + entries = get_agent_entries() + for slug, entry in entries.items(): + agent_cls = load_agent(slug) + ref = agent_cls._get_instance_class_ref() + module_path, _ = ref.rsplit(":", 1) + # Convert module path to file path + parts = module_path.split(".") + # Find the source root by looking at the agent module's file + agent_mod = importlib.import_module(entry.module) + agent_file = Path(agent_mod.__file__) + # Walk up from the agent file to find the source root + src_root = agent_file + module_parts = entry.module.split(".") + for _ in module_parts: + src_root = src_root.parent + # Now resolve the ref module path + expected_file = src_root / Path(*parts[:-1]) / f"{parts[-1]}.py" + assert expected_file.exists(), ( + f"Agent '{slug}': _get_instance_class_ref() points to " + f"'{module_path}' but {expected_file} does not exist." + ) + + +def test_with_runner_accepts_string_cls(): + """with_runner() must accept a 'module:class' string for the direct runner.""" + from exgentic.adapters.runners import with_runner + + ref = "exgentic.testing.agent:TestAgentInstance" + proxy = with_runner( + ref, + runner="direct", + session_id="test-string-ref", + seed=42, + policy="good_then_finish", + finish_after=2, + max_steps=10, + ) + # Should successfully create the instance + assert proxy is not None + proxy.close() diff --git a/labs/AgentStream/exgentic/tests/api/test_api_errors.py b/labs/AgentStream/exgentic/tests/api/test_api_errors.py new file mode 100644 index 00000000..e44e6589 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_errors.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic import execute +from exgentic.core.types import RunConfig, SessionOutcomeStatus + + +def _base_config(tmp_path, *, run_id: str) -> RunConfig: + return RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + ) + + +def test_invalid_action_from_agent(tmp_path): + config = _base_config(tmp_path, run_id="run-invalid-action").model_copy( + update={ + "agent_kwargs": {"policy": "invalid_action"}, + } + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.ERROR + + +def test_invalid_observation_from_benchmark(tmp_path): + config = _base_config(tmp_path, run_id="run-invalid-observation").model_copy( + update={ + "agent_kwargs": {"policy": "good_only"}, + "benchmark_kwargs": {"tasks": ["task-1"], "invalid_observation": True}, + } + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.ERROR + + +def test_agent_exception_marks_error(tmp_path): + config = _base_config(tmp_path, run_id="run-agent-error").model_copy( + update={ + "agent_kwargs": {"policy": "raise_error"}, + } + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.ERROR diff --git a/labs/AgentStream/exgentic/tests/api/test_api_files.py b/labs/AgentStream/exgentic/tests/api/test_api_files.py new file mode 100644 index 00000000..275651e3 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_files.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +from exgentic import execute +from exgentic.core.types import RunConfig + + +def test_session_files_written(tmp_path): + output_dir = tmp_path / "outputs" + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(output_dir), + cache_dir=str(tmp_path / "cache"), + run_id="run-files", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + results = execute(config) + session_id = results.session_results[0].session_id + session_root = output_dir / "run-files" / "sessions" / session_id + assert (session_root / "config.json").exists() + assert (session_root / "results.json").exists() + assert (session_root / "session.json").exists() + assert (session_root / "benchmark" / "config.json").exists() + trajectory = session_root / "trajectory.jsonl" + assert trajectory.exists() + first_event = json.loads(trajectory.read_text(encoding="utf-8").splitlines()[0]) + assert first_event["run_id"] == "run-files" diff --git a/labs/AgentStream/exgentic/tests/api/test_api_instances.py b/labs/AgentStream/exgentic/tests/api/test_api_instances.py new file mode 100644 index 00000000..a89a77e8 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_instances.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import pytest +from exgentic import execute +from exgentic.core.types import RunConfig + +from .fixtures.test_agent import TestAgent +from .fixtures.test_benchmark import TestBenchmark + + +def test_execute_with_instances(tmp_path): + benchmark = TestBenchmark(tasks=["task-1"]) + agent = TestAgent(policy="good_then_finish", finish_after=2) + results = execute( + benchmark=benchmark, + agent=agent, + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-instance", + ) + assert results.total_sessions == 1 + + +def test_instance_kwargs_rejected(tmp_path): + benchmark = TestBenchmark(tasks=["task-1"]) + agent = TestAgent(policy="good_then_finish", finish_after=2) + with pytest.raises(ValueError): + execute( + benchmark=benchmark, + agent=agent, + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-instance-bad", + benchmark_kwargs={"tasks": ["task-1"]}, + ) + with pytest.raises(ValueError): + execute( + benchmark=benchmark, + agent=agent, + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-instance-bad-2", + agent_kwargs={"policy": "good_only"}, + ) + + +def test_config_and_args_rejected(tmp_path): + run_config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-config-ok", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + with pytest.raises(ValueError): + execute( + run_config, + benchmark="test_benchmark", + agent="test_agent", + ) diff --git a/labs/AgentStream/exgentic/tests/api/test_api_limits.py b/labs/AgentStream/exgentic/tests/api/test_api_limits.py new file mode 100644 index 00000000..80975808 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_limits.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic import execute +from exgentic.core.types import RunConfig, SessionOutcomeStatus + + +def _base_config(tmp_path, *, run_id: str) -> RunConfig: + return RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + ) + + +def test_action_level_stop(tmp_path): + config = _base_config(tmp_path, run_id="run-action-stop").model_copy( + update={"agent_kwargs": {"policy": "return_none"}} + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.UNFINISHED + + +def test_step_level_stop(tmp_path): + config = _base_config(tmp_path, run_id="run-step-stop").model_copy( + update={ + "benchmark_kwargs": {"tasks": ["task-1"], "stop_on_step": True}, + "agent_kwargs": {"policy": "good_only"}, + } + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.UNFINISHED + + +def test_limit_reached(tmp_path): + config = _base_config(tmp_path, run_id="run-limit").model_copy( + update={ + "agent_kwargs": {"policy": "good_only"}, + "max_steps": 1, + "max_actions": 1, + } + ) + results = execute(config) + assert results.session_results[0].status == SessionOutcomeStatus.LIMIT_REACHED diff --git a/labs/AgentStream/exgentic/tests/api/test_api_missing_results.py b/labs/AgentStream/exgentic/tests/api/test_api_missing_results.py new file mode 100644 index 00000000..9bede4a9 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_missing_results.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic import aggregate, execute +from exgentic.core.types import RunConfig + + +def test_aggregate_logs_missing_results_and_records_ids(tmp_path): + output_dir = tmp_path / "outputs" + run_id = "run-missing-results" + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(output_dir), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + task_ids=["task-1", "task-2"], + agent_kwargs={"policy": "good_then_finish", "finish_after": 1}, + ) + execute(config) + + missing_session_id = config.to_session_config("task-1").get_session_id() + kept_session_id = config.to_session_config("task-2").get_session_id() + missing_results = output_dir / run_id / "sessions" / missing_session_id / "results.json" + assert missing_results.exists() + missing_results.unlink() + + results = aggregate(config) + + log_path = output_dir / run_id / "run" / "run.log" + log_text = log_path.read_text(encoding="utf-8") + assert "Missing session results for 1/2 planned sessions." in log_text + assert "Missing session ids:" in log_text + + assert results.planned_sessions == 2 + assert results.total_sessions == 1 + assert set(results.planned_session_ids or []) == { + missing_session_id, + kept_session_id, + } + assert results.executed_session_ids == [kept_session_id] diff --git a/labs/AgentStream/exgentic/tests/api/test_api_random.py b/labs/AgentStream/exgentic/tests/api/test_api_random.py new file mode 100644 index 00000000..9d8af819 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_random.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic import execute +from exgentic.core.types import RunConfig + + +def _run(tmp_path, run_id: str): + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "random", "seed": 123}, + max_steps=5, + max_actions=5, + ) + return execute(config) + + +def test_random_policy_deterministic(tmp_path): + first = _run(tmp_path, "run-random-1") + second = _run(tmp_path, "run-random-2") + assert first.session_results[0].score == second.session_results[0].score + assert first.session_results[0].status == second.session_results[0].status diff --git a/labs/AgentStream/exgentic/tests/api/test_api_reuse.py b/labs/AgentStream/exgentic/tests/api/test_api_reuse.py new file mode 100644 index 00000000..2c526bb4 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_reuse.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import time + +from exgentic import execute +from exgentic.core.types import RunConfig + + +def test_reuse_skips_completed_sessions(tmp_path): + output_dir = tmp_path / "outputs" + run_id = "run-reuse" + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(output_dir), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + execute(config) + log_path = output_dir / run_id / "run" / "run.log" + assert log_path.exists() + session_id = config.to_session_config("task-1").get_session_id() + results_path = output_dir / run_id / "sessions" / session_id / "results.json" + bench_results_path = output_dir / run_id / "sessions" / session_id / "benchmark" / "results.json" + assert results_path.exists() + assert bench_results_path.exists() + before_results_mtime = results_path.stat().st_mtime_ns + before_bench_mtime = bench_results_path.stat().st_mtime_ns + + time.sleep(0.01) + execute(config) + after_results_mtime = results_path.stat().st_mtime_ns + after_bench_mtime = bench_results_path.stat().st_mtime_ns + + assert after_results_mtime == before_results_mtime + assert after_bench_mtime == before_bench_mtime diff --git a/labs/AgentStream/exgentic/tests/api/test_api_run_config.py b/labs/AgentStream/exgentic/tests/api/test_api_run_config.py new file mode 100644 index 00000000..1963dd10 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_run_config.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +from exgentic import ( + aggregate, + evaluate, + execute, + list_agents, + list_benchmarks, + list_subsets, + list_tasks, + preview, + results, + status, +) +from exgentic.core.types import RunConfig, SessionOutcomeStatus + + +def _run_config(tmp_path, *, run_id: str, policy: str = "good_then_finish") -> RunConfig: + return RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id=run_id, + num_tasks=1, + max_steps=5, + max_actions=5, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": policy, "finish_after": 2}, + ) + + +def test_evaluate_run_config_writes_files(tmp_path): + config = _run_config(tmp_path, run_id="run-eval") + run_results = evaluate(config) + assert run_results.total_sessions == 1 + assert run_results.successful_sessions == 1 + assert run_results.benchmark_score == 1.0 + session = run_results.session_results[0] + assert session.score == 1.0 + assert session.status == SessionOutcomeStatus.SUCCESS + + session_id = config.to_session_config("task-1").get_session_id() + run_root = tmp_path / "outputs" / "run-eval" + assert (run_root / "run" / "config.json").exists() + assert (run_root / "results.json").exists() + assert (run_root / "benchmark_results.json").exists() + assert (run_root / "sessions" / session_id / "config.json").exists() + assert (run_root / "sessions" / session_id / "results.json").exists() + assert (run_root / "sessions" / session_id / "benchmark" / "results.json").exists() + + payload = json.loads((run_root / "results.json").read_text(encoding="utf-8")) + assert payload["total_sessions"] == 1 + + +def test_execute_then_aggregate(tmp_path): + config = _run_config(tmp_path, run_id="run-exec") + exec_results = execute(config) + assert exec_results.benchmark_score is None + agg_results = aggregate(config) + assert agg_results.benchmark_score == 1.0 + + +def test_preview_status_results(tmp_path): + config = _run_config(tmp_path, run_id="run-preview") + plan = preview(config) + assert len(plan.to_run) == 1 + run_status = status(config) + assert run_status.total_tasks == 1 + evaluate(config) + loaded = results(config) + assert loaded.total_sessions == 1 + + +def test_listing_apis(): + benchmarks = list_benchmarks() + agents = list_agents() + assert any(item["slug_name"] == "test_benchmark" for item in benchmarks) + assert any(item["slug_name"] == "test_agent" for item in agents) + assert list_subsets("test_benchmark") == [] + tasks = list_tasks(benchmark="test_benchmark") + assert "task-1" in tasks diff --git a/labs/AgentStream/exgentic/tests/api/test_api_runners.py b/labs/AgentStream/exgentic/tests/api/test_api_runners.py new file mode 100644 index 00000000..92857a4b --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_runners.py @@ -0,0 +1,165 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""End-to-end evaluate flow through every runner. + +Exercises the full evaluate() pipeline — evaluator creation, session execution, +scoring, close(), and aggregation — through each transport layer. + +This catches issues like: +- ContextVar propagation across uvicorn thread-pool workers (service runner) +- Path resolution mismatches between host and runner +- close() forwarding through proxied objects +- Result file persistence across runner boundaries +- Volume mount and env var consistency (docker runner) +""" + +from __future__ import annotations + +import json +import platform +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest +from exgentic import evaluate, execute +from exgentic.core.types import RunConfig + +_RUNNERS = ["direct", "thread", "process", "service"] + +# Detect Docker availability for conditional tests. +_docker_available = shutil.which("docker") is not None +if _docker_available: + try: + subprocess.run(["docker", "info"], check=True, capture_output=True, timeout=5) + except Exception: + _docker_available = False + +if _docker_available and sys.version_info[:2] == (3, 12): + _RUNNERS.append("docker") + + +@pytest.fixture(params=_RUNNERS) +def runner(request): + return request.param + + +@pytest.fixture(scope="module") +def _docker_tmpdir(): + """Module-scoped temp dir for Docker tests under $HOME. + + Rancher Desktop / Docker Desktop on macOS only share ``/Users/`` by + default. pytest's ``tmp_path`` lives under ``/var/folders/`` which + is NOT shared, so Docker volume mounts silently fail. + + Using a module-scoped parent dir avoids cleaning up temp dirs between + tests, which would break stale logging FileHandlers held by the + evaluate framework. + """ + if platform.system() != "Darwin": + yield None + return + d = Path(tempfile.mkdtemp(prefix=".exgentic_test_", dir=Path.home())) + yield d + shutil.rmtree(d, ignore_errors=True) + + +@pytest.fixture +def run_tmp(runner, tmp_path, _docker_tmpdir): + """Temp path that works with Docker volume mounts on macOS.""" + if runner != "docker" or _docker_tmpdir is None: + return tmp_path + d = Path(tempfile.mkdtemp(dir=_docker_tmpdir)) + return d + + +def _run_config(tmp_path, runner: str, *, num_tasks: int = 2, policy: str = "good_then_finish") -> RunConfig: + tasks = [f"task-{i}" for i in range(1, num_tasks + 1)] + return RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id=f"run-{runner}", + num_tasks=num_tasks, + max_steps=10, + max_actions=10, + benchmark_kwargs={"tasks": tasks, "runner": runner}, + agent_kwargs={"policy": policy, "finish_after": 2}, + ) + + +def test_evaluate_full_flow(run_tmp, runner): + """Full evaluate (sessions + aggregation) through each runner.""" + config = _run_config(run_tmp, runner) + results = evaluate(config) + + assert results.total_sessions == 2 + assert results.successful_sessions == 2 + assert results.benchmark_score == 1.0 + + for session in results.session_results: + assert session.score == 1.0 + assert session.success is True + + +def test_result_files_written(run_tmp, runner): + """Verify all expected output files are created through each runner.""" + config = _run_config(run_tmp, runner, num_tasks=1) + evaluate(config) + + run_dir = run_tmp / "outputs" / f"run-{runner}" + session_id = config.to_session_config("task-1").get_session_id() + session_dir = run_dir / "sessions" / session_id + + # Run-level files + assert (run_dir / "results.json").exists() + assert (run_dir / "benchmark_results.json").exists() + + # Session-level files + assert (session_dir / "results.json").exists() + assert (session_dir / "benchmark" / "results.json").exists() + + # Verify content is valid JSON with expected fields + payload = json.loads((run_dir / "results.json").read_text()) + assert payload["total_sessions"] == 1 + assert payload["benchmark_score"] == 1.0 + + +def test_execute_then_aggregate(run_tmp, runner): + """Verify execute-only + aggregate-only works through each runner.""" + from exgentic import aggregate + + config = _run_config(run_tmp, runner, num_tasks=1) + + exec_results = execute(config) + assert exec_results.benchmark_score is None + assert exec_results.total_sessions == 1 + + agg_results = aggregate(config) + assert agg_results.benchmark_score == 1.0 + + +def test_unsuccessful_session(run_tmp, runner): + """Agent that finishes immediately scores 0 through each runner.""" + config = _run_config(run_tmp, runner, num_tasks=1, policy="finish_immediately") + results = evaluate(config) + + assert results.total_sessions == 1 + assert results.successful_sessions == 0 + assert results.session_results[0].score == 0.0 + assert results.session_results[0].success is False + + +def test_parallel_workers(run_tmp, runner): + """Evaluate with max_workers>1 to test thread-safety of pickling.""" + config = _run_config(run_tmp, runner, num_tasks=4) + config = config.model_copy(update={"max_workers": 2}) + results = evaluate(config) + + assert results.total_sessions == 4 + assert results.successful_sessions == 4 + assert results.benchmark_score == 1.0 diff --git a/labs/AgentStream/exgentic/tests/api/test_api_session_config.py b/labs/AgentStream/exgentic/tests/api/test_api_session_config.py new file mode 100644 index 00000000..1fc32945 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_api_session_config.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic import execute, status +from exgentic.core.types import RunConfig, SessionOutcomeStatus + + +def test_execute_session_config(tmp_path): + run_config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-session", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + session_config = run_config.to_session_config("task-1") + results = execute(session_config) + assert results.total_sessions == 1 + assert results.session_results[0].status == SessionOutcomeStatus.SUCCESS + + +def test_status_session_config(tmp_path): + run_config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(tmp_path / "outputs"), + cache_dir=str(tmp_path / "cache"), + run_id="run-status-session", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + session_config = run_config.to_session_config("task-1") + run_status = status(session_config) + assert run_status.total_tasks == 1 + assert run_status.task_ids == ["task-1"] diff --git a/labs/AgentStream/exgentic/tests/api/test_cli_batch.py b/labs/AgentStream/exgentic/tests/api/test_cli_batch.py new file mode 100644 index 00000000..b40039e7 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_cli_batch.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import csv +import json + +from click.testing import CliRunner +from exgentic.core.types import RunConfig +from exgentic.interfaces.cli.main import cli + + +def _write_config(path, *, run_id: str) -> str: + cfg = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(path.parent / "outputs"), + cache_dir=str(path.parent / "cache"), + run_id=run_id, + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + path.write_text(cfg.model_dump_json(indent=2), encoding="utf-8") + return str(path) + + +def test_batch_status_multiple_configs(tmp_path): + runner = CliRunner() + c1 = _write_config(tmp_path / "a.json", run_id="run-a") + c2 = _write_config(tmp_path / "b.json", run_id="run-b") + + result = runner.invoke( + cli, + [ + "batch", + "status", + "--config", + c1, + "--config", + c2, + ], + ) + + assert result.exit_code == 0, result.output + assert "Batch Status" in result.output + assert "run-a" in result.output + assert "run-b" in result.output + + +def test_batch_status_config_glob_pattern(tmp_path): + runner = CliRunner() + p1 = tmp_path / "outputs" / "r1" / "config.json" + p2 = tmp_path / "outputs" / "r2" / "config.json" + p1.parent.mkdir(parents=True, exist_ok=True) + p2.parent.mkdir(parents=True, exist_ok=True) + _write_config(p1, run_id="run-g1") + _write_config(p2, run_id="run-g2") + + result = runner.invoke( + cli, + [ + "batch", + "status", + "--config", + str(tmp_path / "outputs" / "**" / "config.json"), + ], + ) + + assert result.exit_code == 0, result.output + assert "run-g1" in result.output + assert "run-g2" in result.output + + +def test_batch_status_shell_expanded_values_after_single_config(tmp_path): + runner = CliRunner() + c1 = _write_config(tmp_path / "s1.json", run_id="run-s1") + c2 = _write_config(tmp_path / "s2.json", run_id="run-s2") + + # Simulates shell expansion where one --config token becomes multiple values. + result = runner.invoke( + cli, + [ + "batch", + "status", + "--config", + c1, + c2, + ], + ) + + assert result.exit_code == 0, result.output + assert "run-s1" in result.output + assert "run-s2" in result.output + + +def test_batch_extract_writes_csv(tmp_path): + runner = CliRunner() + config_path = _write_config(tmp_path / "extract.json", run_id="run-extract") + + results_path = tmp_path / "run-extract" / "results.json" + results_path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "benchmark_name": "Test Benchmark", + "agent_name": "Test Agent", + "benchmark_score": 1.0, + "total_sessions": 1, + } + results_path.write_text(json.dumps(payload), encoding="utf-8") + + output_csv = tmp_path / "results.csv" + result = runner.invoke( + cli, + [ + "batch", + "extract", + "--config", + config_path, + "--output", + str(output_csv), + ], + ) + + assert result.exit_code == 0, result.output + with open(output_csv, encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + rows = list(reader) + + assert len(rows) == 1 + assert rows[0]["benchmark_name"] == "Test Benchmark" + assert rows[0]["benchmark_score"] == "1.0" diff --git a/labs/AgentStream/exgentic/tests/api/test_cli_commands.py b/labs/AgentStream/exgentic/tests/api/test_cli_commands.py new file mode 100644 index 00000000..61b1acec --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_cli_commands.py @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from click.testing import CliRunner +from exgentic.core.types import RunConfig +from exgentic.interfaces.cli.main import cli + + +def test_cli_execute_concurrent(tmp_path): + runner = CliRunner() + output_dir = tmp_path / "outputs" + result = runner.invoke( + cli, + [ + "evaluate", + "execute", + "--benchmark", + "test_benchmark", + "--agent", + "test_agent", + "--task", + "task-1", + "--task", + "task-2", + "--set", + 'agent.policy="good_then_finish"', + "--set", + "agent.finish_after=2", + "--max-workers", + "2", + "--run-id", + "run-cli-concurrent", + "--output-dir", + str(output_dir), + ], + ) + assert result.exit_code == 0, result.output + sessions_root = output_dir / "run-cli-concurrent" / "sessions" + session_dirs = [p for p in sessions_root.iterdir() if (p / "config.json").exists()] + assert len(session_dirs) == 2 + + +def test_cli_execute_session_config(tmp_path): + runner = CliRunner() + output_dir = tmp_path / "outputs" + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(output_dir), + cache_dir=str(tmp_path / "cache"), + run_id="run-cli-session", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + session_config = config.to_session_config("task-1") + config_path = tmp_path / "session_config.json" + config_path.write_text(session_config.model_dump_json(indent=2), encoding="utf-8") + + result = runner.invoke( + cli, + [ + "evaluate", + "session", + "--config", + str(config_path), + ], + ) + assert result.exit_code == 0, result.output + sessions_root = output_dir / "run-cli-session" / "sessions" + session_dirs = [p for p in sessions_root.iterdir() if (p / "config.json").exists()] + assert len(session_dirs) == 1 + + +def test_cli_status_with_config(tmp_path): + runner = CliRunner() + output_dir = tmp_path / "outputs" + config = RunConfig( + benchmark="test_benchmark", + agent="test_agent", + output_dir=str(output_dir), + cache_dir=str(tmp_path / "cache"), + run_id="run-cli-status", + num_tasks=1, + benchmark_kwargs={"tasks": ["task-1"]}, + agent_kwargs={"policy": "good_then_finish", "finish_after": 2}, + ) + config_path = tmp_path / "run_config.json" + config_path.write_text(config.model_dump_json(indent=2), encoding="utf-8") + + result = runner.invoke( + cli, + [ + "status", + "--config", + str(config_path), + ], + ) + assert result.exit_code == 0, result.output + assert "run-cli-status" in result.output diff --git a/labs/AgentStream/exgentic/tests/api/test_cli_compare.py b/labs/AgentStream/exgentic/tests/api/test_cli_compare.py new file mode 100644 index 00000000..c5c14167 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_cli_compare.py @@ -0,0 +1,1638 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import re +from pathlib import Path + +from click.testing import CliRunner +from exgentic.core.types import SessionResults +from exgentic.core.types.session import SessionOutcomeStatus +from exgentic.interfaces.cli.main import cli + + +def create_mock_results( + output_dir: Path, + agent: str, + model: str, + benchmark: str, + subset: str | None, + tasks: list[tuple[str, float]], + run_id: str | None = None, +) -> None: + """Create mock results structure for testing compare command. + + Args: + output_dir: Base output directory + agent: Agent slug name + model: Model name + benchmark: Benchmark slug name + subset: Optional subset name + tasks: List of (task_id, score) tuples + run_id: Optional run ID (generated if not provided) + """ + if run_id is None: + run_id = f"test-{agent}-{model}-{benchmark}" + if subset: + run_id += f"-{subset}" + + run_dir = output_dir / run_id + run_dir.mkdir(parents=True, exist_ok=True) + + # Create config.json in run/config.json format + config_dir = run_dir / "run" + config_dir.mkdir(parents=True, exist_ok=True) + + config_data = { + "benchmark": {"slug_name": benchmark}, + "agent": {"slug_name": agent, "model_name": model}, + "model": model, + "run_id": run_id, + } + + if subset: + config_data["benchmark"]["params"] = {"subset": subset} + config_data["subset"] = subset + + config_file = config_dir / "config.json" + config_file.write_text(json.dumps(config_data, indent=2), encoding="utf-8") + + # Create sessions directory with results + sessions_dir = run_dir / "sessions" + sessions_dir.mkdir(parents=True, exist_ok=True) + + for task_id, score in tasks: + session_id = f"session-{task_id}" + session_dir = sessions_dir / session_id + session_dir.mkdir(parents=True, exist_ok=True) + + # Create results.json + session_result = SessionResults( + session_id=session_id, + task_id=task_id, + success=score >= 0.99, + score=score, + is_finished=True, + status=SessionOutcomeStatus.SUCCESS, + steps=5, + action_count=5, + invalid_action_count=0, + agent_cost=0.01, + benchmark_cost=0.0, + execution_time=10.0, + ) + + results_file = session_dir / "results.json" + results_file.write_text(session_result.model_dump_json(indent=2), encoding="utf-8") + + +def test_compare_two_agents_same_benchmark(tmp_path): + """Test comparing two different agents using the same model on a single benchmark. + + Verifies: + - Comparison table is displayed with task results + - Basic comparison functionality works + - Task-level results are shown correctly + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create mock results for agent1 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0), ("task-2", 0.8), ("task-3", 0.6)], + ) + + # Create mock results for agent2 + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 0.9), ("task-2", 0.85), ("task-3", 0.7)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + assert "Comparison Results" in result.output + # New format shows summary table with setup names and statistics + assert "agent1 and model1" in result.output + assert "agent2 and model1" in result.output + assert "Statistical Significance Matrix" in result.output + + +def test_compare_two_models_same_agent(tmp_path): + """Test comparing two different models using the same agent. + + Verifies: + - Model comparison works correctly + - Model-specific filtering is applied + - Results are properly differentiated by model + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create mock results for model1 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0), ("task-2", 0.9)], + ) + + # Create mock results for model2 + create_mock_results( + output_dir, + agent="agent1", + model="model2", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 0.8), ("task-2", 0.7)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent1", + "--model1", + "model1", + "--model2", + "model2", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + assert "Comparison Results" in result.output + # Verify model-specific filtering is applied - both models should appear + assert "model1" in result.output + assert "model2" in result.output + # New format shows summary table + assert "Statistical Significance Matrix" in result.output + + +def test_compare_with_subset(tmp_path): + """Test comparing results with benchmark subsets (e.g., benchmark/subset). + + Verifies: + - Subset filtering works correctly + - Subset parameter is properly parsed and applied + - Results are filtered to the specified subset + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create mock results with subset + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset="subset1", + tasks=[("task-1", 1.0), ("task-2", 0.8)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset="subset1", + tasks=[("task-1", 0.9), ("task-2", 0.85)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark/subset1", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + # Verify subset parameter is properly parsed and applied + assert "test_benchmark/subset1" in result.output + # New format shows summary table + assert "Statistical Significance Matrix" in result.output + + +def test_compare_json_output(tmp_path): + """Test JSON output format (--format json). + + Verifies: + - JSON structure includes setups, per_benchmark, and overall + - Proper JSON serialization of all data + - Summary statistics are included in JSON output + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0), ("task-2", 0.8)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 0.9), ("task-2", 0.85)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, result.output + + # Parse JSON output - new structure + output_data = json.loads(result.output) + assert "setups" in output_data + assert "per_benchmark" in output_data + assert "overall" in output_data + assert len(output_data["setups"]) == 2 + assert "test_benchmark" in output_data["per_benchmark"] + assert len(output_data["overall"]["pairwise_comparisons"]) == 1 + + +def test_compare_multiple_benchmarks(tmp_path): + """Test comparing across multiple benchmarks simultaneously. + + Verifies: + - Multiple benchmarks can be compared in one command + - Overall statistics are computed with equal weight per benchmark + - "Overall" section appears in output + - Per-benchmark and overall summaries are both shown + """ + import warnings + + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results for benchmark1 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[("task-1", 1.0), ("task-2", 0.8)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[("task-1", 0.9), ("task-2", 0.85)], + ) + + # Create results for benchmark2 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[("task-1", 0.95), ("task-2", 0.75)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[("task-1", 0.85), ("task-2", 0.8)], + ) + + # Suppress statsmodels warnings for small sample sizes + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning, module="statsmodels") + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "benchmark1", + "--benchmark", + "benchmark2", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + assert "benchmark1" in result.output + assert "benchmark2" in result.output + assert "Overall" in result.output + + +def test_compare_pairwise_three_agents(tmp_path): + """Test pairwise comparison mode with 3 agents. + + Verifies: + - Pairwise comparison summary table is displayed + - All pair combinations are computed (3 pairs for 3 agents) + - Statistical significance matrix is shown + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results for three agents + for benchmark_num in [1, 2]: + for agent_num in [1, 2, 3]: + score_offset = (agent_num - 1) * 0.1 + create_mock_results( + output_dir, + agent=f"agent{agent_num}", + model="model1", + benchmark=f"test_benchmark{benchmark_num}", + subset=None, + tasks=[ + ("task-1", 1.0 - score_offset), + ("task-2", 0.9 - score_offset), + ], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--agent3", + "agent3", + "--model1", + "model1", + "--model2", + "model1", + "--model3", + "model1", + "--benchmark", + "test_benchmark1", + "--benchmark", + "test_benchmark2", + "--output-dir", + str(output_dir), + ], + ) + # print(result.output) + assert result.exit_code == 0, result.output + assert "Comparison Results" in result.output + # Verify all pair combinations are computed (3 pairs for 3 agents) + # Should see comparisons: agent1 vs agent2, agent1 vs agent3, agent2 vs agent3 + assert "agent1 and model1" in result.output + assert "agent2 and model1" in result.output + assert "agent3 and model1" in result.output + assert "Detailed Comparison: test_benchmark1" in result.output + assert "Detailed Comparison: test_benchmark2" in result.output + assert "Detailed Comparison: test_benchmark2" in result.output + assert re.search( + r"Detailed Comparison.*" r"agent1 and model1.*agent2 and model1.*" r"agent1 and model1.*agent2 and model1.*", + result.output, + flags=re.DOTALL, + ) + assert "Statistical Significance Matrix" in result.output + + +def test_compare_missing_results(tmp_path): + """Test error handling when results are missing for one setup. + + Verifies: + - Appropriate error message is displayed + - Suggests the correct 'exgentic evaluate' command to run + - Exits with non-zero code + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Only create results for agent1, not agent2 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code != 0 + assert "No results found" in result.output + # Verify suggests the correct 'exgentic evaluate' command to run + assert "exgentic evaluate" in result.output or "evaluate" in result.output.lower() + + +def test_compare_mismatched_tasks(tmp_path): + """Test comparing when agents have different task sets. + + Verifies: + - N/A is shown for missing tasks + - Comparison continues with available tasks + - No errors are raised for mismatched task sets + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Agent1 has tasks 1 and 2 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0), ("task-2", 0.8)], + ) + + # Agent2 has tasks 2 and 3 + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-2", 0.85), ("task-3", 0.7)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + # New format only compares overlapping tasks, shows # Tasks = 1 + assert "Statistical Significance Matrix" in result.output + # Only task-2 is compared (overlaps between both agents) + assert "1" in result.output # Should show 1 task compared + + +def test_compare_invalid_input_patterns(tmp_path): + """Test validation of input patterns (must be consistent). + + Verifies: + - Error is raised for invalid combinations + - All setups must follow same pattern (all with agent+model, all with agent only, or all with model only) + - Appropriate error message explains valid patterns + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Invalid: agent1 has model, agent2 doesn't + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code != 0 + assert "Invalid input combination" in result.output + # Verify appropriate error message explains valid patterns + assert "pattern" in result.output.lower() or "consistent" in result.output.lower() + + +def test_compare_requires_two_setups(tmp_path): + """Test that at least 2 setups are required for comparison. + + Verifies: + - Error is raised when only one setup is provided + - Appropriate error message is shown + - Minimum comparison requirement is enforced + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--model1", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code != 0 + assert "At least 2 agent/model setups are required" in result.output + + +def test_compare_statistical_significance_significant(tmp_path): + """Test that statistical significance is detected when there's a clear difference. + + Verifies: + - Statistical significance is detected for clear differences + - McNemar's test is used + - p-value is below significance threshold (< 0.05) + - Significance status is correctly reported + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results with clear significant difference + # Agent1 succeeds on all tasks (score >= 0.99) + # Agent2 fails on all tasks (score < 0.99) + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), + ("task-2", 1.0), + ("task-3", 1.0), + ("task-4", 1.0), + ("task-5", 1.0), + ("task-6", 1.0), + ("task-7", 1.0), + ("task-8", 1.0), + ("task-9", 1.0), + ("task-10", 1.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 0.5), + ("task-2", 0.5), + ("task-3", 0.5), + ("task-4", 0.5), + ("task-5", 0.5), + ("task-6", 0.5), + ("task-7", 0.5), + ("task-8", 0.5), + ("task-9", 0.5), + ("task-10", 0.5), + ], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, result.output + + # Parse JSON output for precise checks - new structure + output_data = json.loads(result.output) + + # Check per-benchmark results + benchmark_comparison = output_data["per_benchmark"]["test_benchmark"][0] + assert benchmark_comparison["num_tasks"] == 10 + assert benchmark_comparison["success_rate1"] == 1.0 + assert benchmark_comparison["success_rate2"] == 0.0 + assert benchmark_comparison["rate_difference"] == 1.0 + assert benchmark_comparison["is_significant"] is True + assert benchmark_comparison["p_value"] < 0.05 + + # Check overall results + overall_comparison = output_data["overall"]["pairwise_comparisons"][0] + assert overall_comparison["num_tasks"] == 10 + assert overall_comparison["is_significant"] is True + assert overall_comparison["p_value"] < 0.05 + + +def test_compare_statistical_significance_not_significant(tmp_path): + """Test that non-significant results are correctly identified. + + Verifies: + - Non-significant results are correctly identified + - is_significant flag is False + - p-value is above significance threshold + - Appropriate message indicates non-significance + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results with no significant difference + # Both agents have identical performance (all succeed) + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), + ("task-2", 1.0), + ("task-3", 1.0), + ("task-4", 1.0), + ("task-5", 1.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), + ("task-2", 1.0), + ("task-3", 1.0), + ("task-4", 1.0), + ("task-5", 1.0), + ], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, result.output + + # Parse JSON output - new structure + output_data = json.loads(result.output) + + # Check per-benchmark results + benchmark_comparison = output_data["per_benchmark"]["test_benchmark"][0] + assert benchmark_comparison["num_tasks"] == 5 + assert benchmark_comparison["success_rate1"] == 1.0 + assert benchmark_comparison["success_rate2"] == 1.0 + assert benchmark_comparison["rate_difference"] == 0.0 + assert benchmark_comparison["is_significant"] is False + assert benchmark_comparison["p_value"] >= 0.1 + + +def test_compare_statistical_significance_marginal(tmp_path): + """Test marginal significance detection (small but detectable difference). + + Verifies: + - Marginal differences are detected + - Average scores are computed correctly + - Statistical test handles partial disagreements + - p-value reflects the marginal difference + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results with marginal difference + # Agent1 succeeds on 7/10 tasks, Agent2 succeeds on 3/10 tasks + # This creates 4 disagreements where agent1 wins + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), # success (both succeed) + ("task-2", 1.0), # success (both succeed) + ("task-3", 1.0), # success (both succeed) + ("task-4", 1.0), # success (agent1 wins) + ("task-5", 1.0), # success (agent1 wins) + ("task-6", 1.0), # success (agent1 wins) + ("task-7", 1.0), # success (agent1 wins) + ("task-8", 0.5), # fail (both fail) + ("task-9", 0.5), # fail (both fail) + ("task-10", 0.5), # fail (both fail) + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), # success (both succeed) + ("task-2", 1.0), # success (both succeed) + ("task-3", 1.0), # success (both succeed) + ("task-4", 0.5), # fail (agent1 wins) + ("task-5", 0.5), # fail (agent1 wins) + ("task-6", 0.5), # fail (agent1 wins) + ("task-7", 0.5), # fail (agent1 wins) + ("task-8", 0.5), # fail (both fail) + ("task-9", 0.5), # fail (both fail) + ("task-10", 0.5), # fail (both fail) + ], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, result.output + + # Parse JSON output - new structure + output_data = json.loads(result.output) + + # Check per-benchmark results + benchmark_comparison = output_data["per_benchmark"]["test_benchmark"][0] + assert benchmark_comparison["num_tasks"] == 10 + + # Verify success rates are computed correctly + # Agent1: 7 successes (score >= 0.99) out of 10 = 0.7 + # Agent2: 3 successes (score >= 0.99) out of 10 = 0.3 + assert abs(benchmark_comparison["success_rate1"] - 0.7) < 0.01 + assert abs(benchmark_comparison["success_rate2"] - 0.3) < 0.01 + assert abs(benchmark_comparison["rate_difference"] - 0.4) < 0.01 + + # Verify p-value reflects the marginal difference + assert benchmark_comparison["p_value"] < 0.5 + + +def test_compare_statistical_significance_text_output(tmp_path): + """Test that statistical significance is displayed correctly in text output. + + Verifies: + - "Statistical Significance" section appears + - Test method name is shown (McNemar's test) + - p-value is displayed + - Task count is shown + - Average scores are displayed for both setups + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results with significant difference + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 1.0), + ("task-2", 1.0), + ("task-3", 1.0), + ("task-4", 1.0), + ("task-5", 1.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[ + ("task-1", 0.5), + ("task-2", 0.5), + ("task-3", 0.5), + ("task-4", 0.5), + ("task-5", 0.5), + ], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--model1", + "model1", + "--model2", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + # New format shows summary table + assert "Statistical Significance Matrix" in result.output + assert "Comparison Results" in result.output + # Verify task count is shown (in the # Tasks column) + assert "5" in result.output + + +def test_compare_five_agents_max(tmp_path): + """Test comparing the maximum of 5 agents/models. + + Verifies: + - All pairwise combinations are computed (10 pairs for 5 agents) + - Pairwise summary table is generated correctly + - System handles maximum allowed setups + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results for 5 agents + for agent_num in range(1, 6): + create_mock_results( + output_dir, + agent=f"agent{agent_num}", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0 - agent_num * 0.1)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--agent3", + "agent3", + "--agent4", + "agent4", + "--agent5", + "agent5", + "--model1", + "model1", + "--model2", + "model1", + "--model3", + "model1", + "--model4", + "model1", + "--model5", + "model1", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + assert "Statistical Significance Matrix" in result.output + # Verify all 5 agents appear in output (system handles maximum allowed setups) + for agent_num in range(1, 6): + assert f"agent{agent_num}" in result.output + + +def test_compare_only_agents_no_models(tmp_path): + """Test comparing agents without specifying models. + + Verifies: + - Comparison works across any models when only agents are specified + - Flexible input pattern is supported + - Results are aggregated correctly regardless of model + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results for two agents (any model) + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 0.9)], + ) + + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, result.output + # Verify comparison works across any models (both agents should appear) + assert "agent1" in result.output + assert "agent2" in result.output + # New format shows summary table + assert "Statistical Significance Matrix" in result.output + + +def test_compare_only_models_no_agents(tmp_path): + """Test comparing models without specifying agents. + + Verifies: + - Comparison works across any agents when only models are specified + - Model-only comparison mode is supported + - Results are aggregated correctly regardless of agent + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + + # Create results for two models (any agent) + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0)], + ) + + create_mock_results( + output_dir, + agent="agent1", + model="model2", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 0)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model2", + benchmark="test_benchmark", + subset=None, + tasks=[("task-1", 1.0)], + ) + result = runner.invoke( + cli, + [ + "compare", + "--model1", + "model1", + "--model2", + "model2", + "--benchmark", + "test_benchmark", + "--output-dir", + str(output_dir), + ], + ) + assert result.exit_code == 0, result.output + # Verify comparison works across any agents (both models should appear) + assert "model1" in result.output + assert "model2" in result.output + assert "50." in result.output or "50…" in result.output # May be truncated as "50.…" in table + # New format shows summary table + assert "Statistical Significance Matrix" in result.output + + +def test_breslow_day_homogeneous_high_pvalue(tmp_path): + """Test Breslow-Day test with homogeneous data (high p-value). + + Creates data where the effect is consistent across benchmarks: + - Both benchmarks show similar odds ratios + - Should result in high p-value (p > 0.05) indicating homogeneity + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + output_dir.mkdir() + + # Create consistent effect across two benchmarks + # Contingency table: [[4, 3], [1, 2]] -> OR = 2.67 (same for both benchmarks) + # Benchmark 1: Agent1 wins 7/10, Agent2 wins 5/10 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[ + # Both succeed (4 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + # Agent1 only (3 tasks) + ("task5", 1.0), + ("task6", 1.0), + ("task7", 1.0), + # Agent2 only (1 task) + ("task8", 0.0), + # Both fail (2 tasks) + ("task9", 0.0), + ("task10", 0.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[ + # Both succeed (4 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + # Agent1 only (3 tasks) - Agent2 fails + ("task5", 0.0), + ("task6", 0.0), + ("task7", 0.0), + # Agent2 only (1 task) + ("task8", 1.0), + # Both fail (2 tasks) + ("task9", 0.0), + ("task10", 0.0), + ], + ) + + # Benchmark 2: Same pattern - Agent1 wins 7/10, Agent2 wins 5/10 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[ + # Both succeed (4 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + # Agent1 only (3 tasks) + ("task5", 1.0), + ("task6", 1.0), + ("task7", 1.0), + # Agent2 only (1 task) + ("task8", 0.0), + # Both fail (2 tasks) + ("task9", 0.0), + ("task10", 0.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[ + # Both succeed (4 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + # Agent1 only (3 tasks) - Agent2 fails + ("task5", 0.0), + ("task6", 0.0), + ("task7", 0.0), + # Agent2 only (1 task) + ("task8", 1.0), + # Both fail (2 tasks) + ("task9", 0.0), + ("task10", 0.0), + ], + ) + + # Run compare command with JSON output + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--benchmark", + "benchmark1", + "--benchmark", + "benchmark2", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + # Parse JSON output - new structure + output_data = json.loads(result.output) + + # Check that overall statistics include Breslow-Day test in pairwise comparisons + assert "overall" in output_data + assert "pairwise_comparisons" in output_data["overall"] + assert len(output_data["overall"]["pairwise_comparisons"]) > 0 + + # Get Breslow-Day test from first pairwise comparison + breslow_day = output_data["overall"]["pairwise_comparisons"][0]["breslow_day"] + assert breslow_day is not None + assert "p_value" in breslow_day + assert "interpretation" in breslow_day + + # High p-value indicates homogeneity (consistent effect across benchmarks) + assert ( + breslow_day["p_value"] > 0.05 + ), f"Expected high p-value (>0.05) for homogeneous data, got {breslow_day['p_value']}" + assert ( + "homogeneous" in breslow_day["interpretation"].lower() or "consistent" in breslow_day["interpretation"].lower() + ), f"Expected 'homogeneous' or 'consistent' in interpretation, got: {breslow_day['interpretation']}" + + +def test_breslow_day_heterogeneous_low_pvalue(tmp_path): + """Test Breslow-Day test with heterogeneous data (low p-value). + + Creates data where the effect varies across benchmarks: + - Benchmark 1: Agent1 much better than Agent2 + - Benchmark 2: Agent2 much better than Agent1 + - Should result in low p-value (p < 0.05) indicating heterogeneity + + This creates contingency tables with different odds ratios: + - Benchmark 1: OR = 1.5 (Agent1 better) + - Benchmark 2: OR = 0.0185 (Agent2 better) + """ + runner = CliRunner() + output_dir = tmp_path / "outputs" + output_dir.mkdir() + + # Benchmark 1: Agent1 better (21/27 vs 8/27) + # Contingency table: [[18, 6], [2, 1]] -> OR = 1.5 + # Both succeed: 18, Agent1 only: 3, Agent2 only: 2, Both fail: 1 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[ + # Both succeed (18 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + ("task5", 1.0), + ("task6", 1.0), + ("task7", 1.0), + ("task8", 1.0), + ("task9", 1.0), + ("task10", 1.0), + ("task11", 1.0), + ("task12", 1.0), + ("task13", 1.0), + ("task14", 1.0), + ("task15", 1.0), + ("task16", 1.0), + ("task17", 1.0), + ("task18", 1.0), + # Agent1 only (6 tasks) + ("task19", 1.0), + ("task20", 1.0), + ("task21", 1.0), + ("task22", 1.0), + ("task23", 1.0), + ("task24", 1.0), + # Both fail (1 task) + ("task27", 0.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[ + # Both succeed (18 tasks) + ("task1", 1.0), + ("task2", 1.0), + ("task3", 1.0), + ("task4", 1.0), + ("task5", 1.0), + ("task6", 1.0), + ("task7", 1.0), + ("task8", 1.0), + ("task9", 1.0), + ("task10", 1.0), + ("task11", 1.0), + ("task12", 1.0), + ("task13", 1.0), + ("task14", 1.0), + ("task15", 1.0), + ("task16", 1.0), + ("task17", 1.0), + ("task18", 1.0), + # Agent1 only (6 tasks) - Agent2 fails + ("task19", 0.0), + ("task20", 0.0), + ("task21", 0.0), + ("task22", 0.0), + ("task23", 0.0), + ("task24", 0.0), + # Agent2 only (2 tasks) + ("task25", 1.0), + ("task26", 1.0), + # Both fail (1 task) + ("task27", 0.0), + ], + ) + + # Benchmark 2: Agent2 better (8/27 vs 21/27) - opposite pattern + # Contingency table: [[2, 18], [6, 1]] -> OR = 0.0185 + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[ + # Both succeed (2 tasks) + ("task1", 1.0), + ("task2", 1.0), + # Agent2 only (18 tasks) - Agent1 fails + ("task3", 0.0), + ("task4", 0.0), + ("task5", 0.0), + ("task6", 0.0), + ("task7", 0.0), + ("task8", 0.0), + ("task9", 0.0), + ("task10", 0.0), + ("task11", 0.0), + ("task12", 0.0), + ("task13", 0.0), + ("task14", 0.0), + ("task15", 0.0), + ("task16", 0.0), + ("task17", 0.0), + ("task18", 0.0), + ("task19", 0.0), + ("task20", 0.0), + # Agent1 only (6 tasks) + ("task21", 1.0), + ("task22", 1.0), + ("task23", 1.0), + ("task24", 1.0), + ("task25", 1.0), + ("task26", 1.0), + # Both fail (1 task) + ("task27", 0.0), + ], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[ + # Both succeed (2 tasks) + ("task1", 1.0), + ("task2", 1.0), + # Agent2 only (18 tasks) + ("task3", 1.0), + ("task4", 1.0), + ("task5", 1.0), + ("task6", 1.0), + ("task7", 1.0), + ("task8", 1.0), + ("task9", 1.0), + ("task10", 1.0), + ("task11", 1.0), + ("task12", 1.0), + ("task13", 1.0), + ("task14", 1.0), + ("task15", 1.0), + ("task16", 1.0), + ("task17", 1.0), + ("task18", 1.0), + ("task19", 1.0), + ("task20", 1.0), + # Agent1 only (6 tasks) - Agent2 fails + ("task21", 0.0), + ("task22", 0.0), + ("task23", 0.0), + ("task24", 0.0), + ("task25", 0.0), + ("task26", 0.0), + # Both fail (1 task) + ("task27", 0.0), + ], + ) + + # Run compare command with JSON output + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--benchmark", + "benchmark1", + "--benchmark", + "benchmark2", + "--output-dir", + str(output_dir), + "--format", + "json", + ], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + # Parse JSON output - new structure + output_data = json.loads(result.output) + + # Check that overall statistics include Breslow-Day test in pairwise comparisons + assert "overall" in output_data + assert "pairwise_comparisons" in output_data["overall"] + assert len(output_data["overall"]["pairwise_comparisons"]) > 0 + + # Get Breslow-Day test from first pairwise comparison + breslow_day = output_data["overall"]["pairwise_comparisons"][0]["breslow_day"] + assert breslow_day is not None + assert "p_value" in breslow_day + assert "interpretation" in breslow_day + + # Low p-value indicates heterogeneity (effect varies across benchmarks) + assert ( + breslow_day["p_value"] < 0.05 + ), f"Expected low p-value (<0.05) for heterogeneous data, got {breslow_day['p_value']}" + assert ( + "heterogeneity" in breslow_day["interpretation"].lower() or "varies" in breslow_day["interpretation"].lower() + ), f"Expected 'heterogeneity' or 'varies' in interpretation, got: {breslow_day['interpretation']}" + + +def test_breslow_day_text_output(tmp_path): + """Test that Breslow-Day test results appear in text output.""" + import warnings + + runner = CliRunner() + output_dir = tmp_path / "outputs" + output_dir.mkdir() + + # Create data for two benchmarks + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[("task1", 1.0), ("task2", 1.0), ("task3", 0.0)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark1", + subset=None, + tasks=[("task1", 1.0), ("task2", 0.0), ("task3", 0.0)], + ) + + create_mock_results( + output_dir, + agent="agent1", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[("task1", 1.0), ("task2", 1.0), ("task3", 0.0)], + ) + + create_mock_results( + output_dir, + agent="agent2", + model="model1", + benchmark="benchmark2", + subset=None, + tasks=[("task1", 1.0), ("task2", 0.0), ("task3", 0.0)], + ) + + # Run compare command with text output (default) + # Suppress statsmodels warnings for small sample sizes + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning, module="statsmodels") + result = runner.invoke( + cli, + [ + "compare", + "--agent1", + "agent1", + "--agent2", + "agent2", + "--benchmark", + "benchmark1", + "--benchmark", + "benchmark2", + "--output-dir", + str(output_dir), + ], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + # Check that text output includes Breslow-Day test information + # New format shows it in the overall table section + assert ( + "Breslow-Day" in result.output or "breslow" in result.output.lower() or "Bre…" in result.output + ), "Expected Breslow-Day test information in text output" + assert ( + "P-value" in result.output or "p-value" in result.output or "p=" in result.output + ), "Expected p-value in text output" + assert ( + "Interpretation" in result.output + or "homogeneous" in result.output.lower() + or "consistent" in result.output.lower() + or "hom…" in result.output + or "con…" in result.output + ), "Expected interpretation in text output" diff --git a/labs/AgentStream/exgentic/tests/api/test_cli_version.py b/labs/AgentStream/exgentic/tests/api/test_cli_version.py new file mode 100644 index 00000000..7045ce7e --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_cli_version.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from click.testing import CliRunner +from exgentic import __version__ +from exgentic.interfaces.cli.main import cli + + +def test_cli_version_long_flag(): + """Test that --version flag displays version and exits.""" + runner = CliRunner() + result = runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert f"exgentic {__version__}" in result.output + + +def test_cli_version_short_flag(): + """Test that -V flag displays version and exits.""" + runner = CliRunner() + result = runner.invoke(cli, ["-V"]) + assert result.exit_code == 0 + assert f"exgentic {__version__}" in result.output diff --git a/labs/AgentStream/exgentic/tests/api/test_package_exports.py b/labs/AgentStream/exgentic/tests/api/test_package_exports.py new file mode 100644 index 00000000..3d7e924a --- /dev/null +++ b/labs/AgentStream/exgentic/tests/api/test_package_exports.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import exgentic + +from tests.api.fixtures.test_agent import TestAgent +from tests.api.fixtures.test_benchmark import TestBenchmark + + +def test_top_level_registry_class_imports_via_from_import(): + scope: dict[str, object] = {} + exec("from exgentic import TestAgent, TestBenchmark", {}, scope) + assert scope["TestAgent"] is TestAgent + assert scope["TestBenchmark"] is TestBenchmark + + +def test_top_level_registry_class_imports_via_attribute_access(): + assert exgentic.TestAgent is TestAgent + assert exgentic.TestBenchmark is TestBenchmark + + +def test_unknown_top_level_export_raises_attribute_error(): + try: + _ = exgentic.NotARealExport + except AttributeError as exc: + assert "NotARealExport" in str(exc) + else: + raise AssertionError("Expected AttributeError for unknown top-level export.") diff --git a/labs/AgentStream/exgentic/tests/benchmarks/__init__.py b/labs/AgentStream/exgentic/tests/benchmarks/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/recording.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/recording.json new file mode 100644 index 00000000..020f2515 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/recording.json @@ -0,0 +1,6 @@ +{ + "benchmark": "appworld", + "task_id": "f3f60f0_3", + "subset": "test_normal", + "expected_score": 1.0 +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/results.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/results.json new file mode 100644 index 00000000..fd836d88 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/results.json @@ -0,0 +1,79 @@ +{ + "session_id": "9e993ba4", + "success": true, + "score": 1.0, + "is_finished": true, + "status": "success", + "steps": 31, + "action_count": 31, + "invalid_action_count": 7, + "agent_cost": 4.379010000000001, + "benchmark_cost": 0.0, + "execution_time": 92.7417664527893, + "details": { + "score": 1.0, + "success": true, + "is_finished": true, + "session_metrics": { + "pass_percentage": 100.0, + "pass_count": 8, + "fail_count": 0, + "num_tests": 8, + "difficulty": 1, + "success": true + }, + "session_metadata": { + "test_tracker": { + "success": true, + "difficulty": 1, + "num_tests": 8, + "passes": [ + { + "requirement": "assert answers match.", + "label": "no_op_fail" + }, + { + "requirement": "assert model changes match spotify.Song, spotify.SongLike, spotify.Album, spotify.AlbumLike.", + "label": "no_op_fail" + }, + { + "requirement": "assert all newly liked songs are in my library via models.changed_records", + "label": "no_op_fail" + }, + { + "requirement": "assure none of the updated or removed song likes are from outside of song library.", + "label": "no_op_pass" + }, + { + "requirement": "assure everything in main_user's song library is liked now.", + "label": "no_op_fail" + }, + { + "requirement": "assert all newly liked albums are in my library via models.changed_records", + "label": "no_op_fail" + }, + { + "requirement": "assure none of the updated or removed album likes are from outside of album library.", + "label": "no_op_pass" + }, + { + "requirement": "assure everything in main_user's album library is liked now.", + "label": "no_op_fail" + } + ], + "failures": [] + } + } + }, + "cost_reports": { + "agent": { + "model_name": "openai/aws/claude-opus-4-5", + "total_cost": 4.379010000000001 + }, + "benchmark": { + "model_name": "", + "total_cost": 0 + } + }, + "task_id": "f3f60f0_3" +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/session.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/session.json new file mode 100644 index 00000000..a1eb8440 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/session.json @@ -0,0 +1,17009 @@ +{ + "run_id": "0c890a5dde8c", + "session_id": "9e993ba4", + "task": "Task from supervisor:\nLike all the songs and albums in my Spotify song and album library, respectively, that I have not liked yet.", + "context": { + "policy": "This environment provides a set of applications, each exposing a predefined set of APIs that may be used to perform tasks on behalf of the supervisor. The applications include: supervisor, amazon, phone, file_system, spotify, venmo, gmail, splitwise, simple_note, todoist.\n The available applications and their APIs are fixed for the task.\n\nSupervisor account credentials (such as emails, usernames, and passwords) are available through the supervisor application’s APIs and are accessed from there when required.\n\nIf an application requires an access token to perform authenticated operations, the access token is obtained by calling that application's authentication/login API using the credentials retrieved from the supervisor application. Access tokens are not provided by the supervisor application.\n\nReferences to people (e.g., friends, family, roommates) correspond to entries in the phone_contacts application.\nReferences to files or storage correspond to the file_system application, not the local machine filesystem.\n\nTime-based instructions (e.g., 'this month', 'yesterday') are interpreted with full calendar boundary ranges.\nIf an API returns paginated results, all pages constitute the complete result.\n\nThe environment consists only of the provided applications and their documented APIs and parameters. No additional endpoints, methods, arguments, or capabilities are assumed beyond those explicitly defined.\n\nWhen task execution is finished, the designated task-completion API is used to signal completion. If the task requires a final answer value, the answer is returned through that completion API. If the task cannot be completed using the available applications and APIs, the task may be marked as failed.", + "supervisor": { + "first_name": "Caitlin", + "last_name": "Burch", + "email": "caiburc@gmail.com", + "phone_number": "5896230193" + }, + "datetime": "2023-05-18T12:00:00" + }, + "actions": [ + { + "name": "finish", + "description": "Mark the currently active task as complete with the given answer.", + "is_finish": true, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "answer": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + }, + { + "type": "string" + } + ], + "default": null, + "description": "The answer to the task instruction. Pass it if and only if the task requests an answer, e.g., 'How many songs are in the Spotify queue?'. The answer must be concise, such as a number, a yes/no, or a specific entity, name, etc, e.g., '10' in this case. If the task is not a question, e.g., 'Start my Spotify music player.', the answer must be left to the default value, i.e., None.", + "title": "Answer" + }, + "status": { + "default": "success", + "description": "Mark the task complete.", + "enum": [ + "success", + "fail" + ], + "title": "Status", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "supervisor__show_profile", + "description": "Show your supervisor's profile information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "supervisor__show_addresses", + "description": "Show your supervisor's addresses.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "supervisor__show_payment_cards", + "description": "Show your supervisor's payment_cards.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "supervisor__show_account_passwords", + "description": "Show your supervisor's app account passwords.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product", + "description": "Show product information based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "The ID of the product.", + "title": "Product Id", + "type": "integer" + } + }, + "required": [ + "product_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_recommended_products", + "description": "Show products recommended for you.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_browsing_history", + "description": "Show products in your browsing history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__clear_browsing_history", + "description": "Clear your browsing history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_product_to_browsing_history", + "description": "Add a product to your browsing history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to add to browsing history.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__remove_product_from_browsing_history", + "description": "Remove a product from your browsing history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to remove from browsing history.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_browsing_history_tracking", + "description": "Update browsing history tracking preference.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "track_browsing_history": { + "description": "Whether to track browsing history.", + "title": "Track Browsing History", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "track_browsing_history", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_last_product_purchase", + "description": "Show your last purchase information of a product with the given ID or its size and color variations.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "The ID of the product.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_rating_distribution", + "description": "Show the rating distribution of a product.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "The ID of the product.", + "title": "Product Id", + "type": "integer" + } + }, + "required": [ + "product_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__search_sellers", + "description": "Search for sellers with a query.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_seller", + "description": "Show a detailed information about the seller.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "seller_id": { + "description": "The ID of the seller.", + "title": "Seller Id", + "type": "integer" + } + }, + "required": [ + "seller_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__search_product_types", + "description": "Search product types present in the database.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 9223372036854775807, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_feature_choices", + "description": "Show the choices of colors, relative sizes and sellers aggregated over all products of the given product type. Because it's an aggregation, the choices may not be available for all products. If product type is not passed, it will return the choices for all products in the database.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_type": { + "default": null, + "description": "The product type to obtain feature choices for.", + "title": "Product Type", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__search_products", + "description": "Search for products with a query and various filtering criteria.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "product_type": { + "default": null, + "description": "The type of product to search for.", + "title": "Product Type", + "type": "string" + }, + "color": { + "default": null, + "description": "The color of product to search for.", + "title": "Color", + "type": "string" + }, + "relative_size": { + "default": null, + "description": "The relative size of product to search for.", + "enum": [ + "extra-small", + "small", + "medium", + "large", + "extra-large" + ], + "title": "Relative Size", + "type": "string" + }, + "min_price": { + "default": 0.0, + "description": "The minimum price for search results.", + "minimum": 0.0, + "title": "Min Price", + "type": "number" + }, + "max_price": { + "default": 9.223372036854776e+18, + "description": "The maximum price for search results.", + "minimum": 0.0, + "title": "Max Price", + "type": "number" + }, + "min_product_rating": { + "default": 0.0, + "description": "The minimum product rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Min Product Rating", + "type": "number" + }, + "max_product_rating": { + "default": 5.0, + "description": "The maximum product rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Max Product Rating", + "type": "number" + }, + "min_seller_rating": { + "default": 0.0, + "description": "The minimum seller rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Min Seller Rating", + "type": "number" + }, + "max_seller_rating": { + "default": 5.0, + "description": "The maximum seller rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Max Seller Rating", + "type": "number" + }, + "seller_id": { + "default": null, + "description": "ID of the seller to search for.", + "title": "Seller Id", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the products by prefixed with +/- to reflect ascending/descending. Valid attributes: rating, price and delivery_days. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_cart", + "description": "show your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__clear_cart", + "description": "Clear your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_product_to_cart", + "description": "Add product by id and quantities to your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "default": 1, + "description": "The quantity of the product to be added to the cart.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "clear_cart_first": { + "default": false, + "description": "If true, the cart will be cleared before adding the product to the cart.", + "title": "Clear Cart First", + "type": "boolean" + }, + "product_id": { + "description": "ID of the product to be added to the cart.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_product_quantity_in_cart", + "description": "Update product quantity in the user cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "description": "The new quantity of the product to be updated in the cart.", + "minimum": 0, + "title": "Quantity", + "type": "integer" + }, + "product_id": { + "description": "ID of the product to be updated in the cart.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "quantity", + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_product_from_cart", + "description": "Remove a product from your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to be deleted from the cart.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__apply_promo_code_to_cart", + "description": "Apply a promo code to your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "promo_code": { + "description": "The promo code to be applied to the cart.", + "title": "Promo Code", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "promo_code", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__remove_promo_code_from_cart", + "description": "Remove a promo code from your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_wish_list", + "description": "Get list of products in your wishlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__clear_wish_list", + "description": "Clear wish list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_product_to_wish_list", + "description": "Add product by id and quantities to your wish list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "default": 1, + "description": "The quantity of the product to be added to the wish list.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "clear_wish_list_first": { + "default": false, + "description": "If true, the wish list will be cleared before adding the product to the wish list.", + "title": "Clear Wish List First", + "type": "boolean" + }, + "product_id": { + "description": "ID of the product to be added to the wish list.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_product_from_wish_list", + "description": "Remove product from the user wish list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to be deleted from the wish list.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_product_quantity_in_wish_list", + "description": "Update product quantity in the user wish_list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "description": "The new quantity of the product to be updated in the wish list.", + "minimum": 0, + "title": "Quantity", + "type": "integer" + }, + "product_id": { + "description": "ID of the product being updated in the wish list.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "quantity", + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__move_product_from_cart_to_wish_list", + "description": "Move product from the cart to the wish list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "default": 1, + "description": "Quantity of the product to move.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "product_id": { + "description": "ID of the product to be move", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__move_product_from_wish_list_to_cart", + "description": "Move product from the wish list to the cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "default": 1, + "description": "Quantity of product to move to cart.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "product_id": { + "description": "ID of the product to move to cart.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_gift_wrapping_to_product", + "description": "Add gift wrapping to a product in your cart. If the product is already set to be gift wrapped, its quantity will be updated.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "quantity": { + "default": 1, + "description": "Quantity of the product to be gift wrapped.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "product_id": { + "description": "ID of the product in your cart to be gift wrapped.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__remove_gift_wrapping_from_product", + "description": "Remove gift wrapping from a product in your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product in your cart to be gift wrapped.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_orders", + "description": "Show or search your past orders", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the orders by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__place_order", + "description": "Place an order for all the items in your cart.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to use for this order.", + "title": "Payment Card Id", + "type": "integer" + }, + "address_id": { + "description": "ID of the address used for shipping this order.", + "title": "Address Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "address_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_purchases", + "description": "Show products you have purchased in the past.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_order", + "description": "Get details of a past order.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "order_id": { + "description": "ID of the order to be shown.", + "title": "Order Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "order_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__download_order_receipt", + "description": "Download the receipt of a past order.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "order_id": { + "description": "ID of the order to download the receipt for.", + "title": "Order Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "order_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_payment_cards", + "description": "Get a list of your payment_cards.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_payment_card", + "description": "Add a new payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "owner_name": { + "description": "Full name of the owner of the payment card.", + "minLength": 1, + "title": "Owner Name", + "type": "string" + }, + "card_number": { + "description": "16-digit card number.", + "exclusiveMaximum": 10000000000000000, + "minimum": 1000000000000000, + "title": "Card Number", + "type": "integer" + }, + "expiry_year": { + "description": "Expiration year of the payment card.", + "title": "Expiry Year", + "type": "integer" + }, + "expiry_month": { + "description": "Expiration month of the payment card.", + "maximum": 12, + "minimum": 1, + "title": "Expiry Month", + "type": "integer" + }, + "cvv_number": { + "description": "A 3-digit CVV number of the payment card.", + "exclusiveMaximum": 1000, + "minimum": 100, + "title": "Cvv Number", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "owner_name", + "card_number", + "expiry_year", + "expiry_month", + "cvv_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_payment_card", + "description": "Get details of a payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be shown.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_payment_card", + "description": "Update payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card to update.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "payment_card_id": { + "description": "ID of the payment card to update.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_payment_card", + "description": "Delete payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be deleted.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_addresses", + "description": "Get a list of your addresses.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__add_address", + "description": "Add a new address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "description": "Name of the address, for example 'Home' or 'Work'.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "street_address": { + "description": "Street address line.", + "minLength": 1, + "title": "Street Address", + "type": "string" + }, + "city": { + "description": "Name of the city.", + "minLength": 1, + "title": "City", + "type": "string" + }, + "state": { + "description": "Name of the state.", + "minLength": 1, + "title": "State", + "type": "string" + }, + "country": { + "description": "Name of the country.", + "minLength": 1, + "title": "Country", + "type": "string" + }, + "zip_code": { + "description": "5-digit zip code of the address.", + "exclusiveMaximum": 100000, + "minimum": 10000, + "title": "Zip Code", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "name", + "street_address", + "city", + "state", + "country", + "zip_code", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_address", + "description": "Update address information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "default": null, + "description": "New name for the address", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "street_address": { + "default": null, + "description": "New street address line.", + "minLength": 1, + "title": "Street Address", + "type": "string" + }, + "city": { + "default": null, + "description": "New city name for the address", + "minLength": 1, + "title": "City", + "type": "string" + }, + "state": { + "default": null, + "description": "New state name for the address", + "minLength": 1, + "title": "State", + "type": "string" + }, + "country": { + "default": null, + "description": "New country name for the address", + "minLength": 1, + "title": "Country", + "type": "string" + }, + "zip_code": { + "default": null, + "description": "New ZIP code for the address", + "maximum": 99999, + "minimum": 10000, + "title": "Zip Code", + "type": "integer" + }, + "address_id": { + "description": "ID of the address to update", + "title": "Address Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "address_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_address", + "description": "Delete address information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "address_id": { + "description": "ID of the address to be deleted.", + "title": "Address Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "address_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_reviews", + "description": "Search or show a list of product reviews.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to show reviews for.", + "title": "Product Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user to filter reviews by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "min_rating": { + "default": 1, + "description": "The minimum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Min Rating", + "type": "integer" + }, + "max_rating": { + "default": 5, + "description": "The maximum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Max Rating", + "type": "integer" + }, + "is_verified": { + "default": null, + "description": "Filter reviews by whether they from a verified purchaser or not.", + "title": "Is Verified", + "type": "boolean" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the product reviews by prefixed with +/- to reflect ascending/descending. Valid attributes: rating and created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "required": [ + "product_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__write_product_review", + "description": "Write a product review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "description": "Product rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": "", + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": "", + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "product_id": { + "description": "ID of the product to review.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "rating", + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_product_review", + "description": "Update a product review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "default": null, + "description": "Product rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": null, + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": null, + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "review_id": { + "description": "ID of the product review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_product_review", + "description": "Delete a product review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "ID of the product review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_questions", + "description": "Search or show a list of product questions.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "product_id": { + "description": "ID of the product to show questions for.", + "title": "Product Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user who posted the question", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the questions by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "required": [ + "product_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__write_product_question", + "description": "Post a question about a product.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "question": { + "description": "Question to be written for the product.", + "minLength": 1, + "title": "Question", + "type": "string" + }, + "product_id": { + "description": "ID of the product to ask a question about.", + "title": "Product Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "question", + "product_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_product_question_answers", + "description": "Search or show a list of answers to a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "question_id": { + "description": "ID of the product question.", + "title": "Question Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user who posted the answer", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "is_verified": { + "default": null, + "description": "Filter answers by whether they from a verified purchaser or not.", + "title": "Is Verified", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the question answers by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "required": [ + "question_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__write_product_question_answer", + "description": "Write a answer to a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "answer": { + "description": "Answer to the question.", + "minLength": 1, + "title": "Answer", + "type": "string" + }, + "question_id": { + "description": "ID of the product question this is an answer to.", + "title": "Question Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "answer", + "question_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_product_question", + "description": "Update a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "question": { + "default": null, + "description": "The updated question.", + "minLength": 1, + "title": "Question", + "type": "string" + }, + "question_id": { + "description": "ID of the product question to update.", + "title": "Question Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "question_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_product_question", + "description": "Delete a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "question_id": { + "description": "ID of the question to delete.", + "title": "Question Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "question_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__update_product_question_answer", + "description": "Update answer to a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "answer": { + "default": null, + "description": "The updated answer for the question.", + "minLength": 1, + "title": "Answer", + "type": "string" + }, + "question_answer_id": { + "description": "ID of the question answer to update.", + "title": "Question Answer Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "question_answer_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__delete_product_question_answer", + "description": "Delete a answer to a product question.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "question_answer_id": { + "description": "ID of the question answer to delete.", + "title": "Question Answer Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "question_answer_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_returns", + "description": "Get a list of your product returns.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "order_id": { + "default": null, + "description": "ID of the order to filter returns by.", + "title": "Order Id", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": "-initiated_at", + "description": "The attribute to sort the product returns by prefixed with +/- to reflect ascending/descending. Valid attributes: quantity, initiated_at and returned_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__initiate_return", + "description": "Initiate a product return.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "order_id": { + "description": "ID of the order to be returned.", + "title": "Order Id", + "type": "integer" + }, + "product_id": { + "description": "ID of the product to be returned.", + "title": "Product Id", + "type": "integer" + }, + "deliverer_id": { + "description": "ID of the deliverer assigned to the return.", + "title": "Deliverer Id", + "type": "integer" + }, + "quantity": { + "description": "Quantity of the product to be returned.", + "minimum": 1, + "title": "Quantity", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "order_id", + "product_id", + "deliverer_id", + "quantity", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_return", + "description": "Show product return status.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "return_id": { + "description": "ID of the product return.", + "title": "Return Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "return_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_return_deliverers", + "description": "Get a list of product return deliverers.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_prime_plans", + "description": "Show information about prime plans available. Delivery fee is waived for prime members.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__subscribe_prime", + "description": "Subscribe to prime membership.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to use for buy prime subscription.", + "title": "Payment Card Id", + "type": "integer" + }, + "duration": { + "description": "Duration of the prime subscription.", + "enum": [ + "monthly", + "yearly" + ], + "title": "Duration", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "duration", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__show_prime_subscriptions", + "description": "Show your prime subscription history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "amazon__download_prime_subscription_receipt", + "description": "Download the receipt for a prime subscription.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "prime_subscription_id": { + "description": "ID of the prime subscription to download the receipt for.", + "title": "Prime Subscription Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from amazon app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "prime_subscription_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "phone_number": { + "description": "Your 10-digit phone number.", + "maxLength": 10, + "minLength": 10, + "title": "Phone Number", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "phone_number", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account phone_number.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__send_password_reset_code", + "description": "Send password reset code to your phone number.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "phone_number": { + "description": "Your phone number.", + "title": "Phone Number", + "type": "string" + } + }, + "required": [ + "phone_number" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "phone_number": { + "description": "Your phone number.", + "title": "Phone Number", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your phone number.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "phone_number", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "phone_number": { + "default": null, + "description": "Phone number of the person you want to see the profile information of.", + "title": "Phone Number", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_contact_relationships", + "description": "Get a list of all relationships available in your contact book.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__search_contacts", + "description": "Search your contact book for relatives' information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query for the contacts list.", + "title": "Query", + "type": "string" + }, + "relationship": { + "default": null, + "description": "Relationship with the person in the contacts list to filter by.", + "title": "Relationship", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__add_contact", + "description": "Add a new contact.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "First name of the contact.", + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Last name of the contact.", + "title": "Last Name", + "type": "string" + }, + "phone_number": { + "default": null, + "description": "Phone number of the contact.", + "title": "Phone Number", + "type": "string" + }, + "email": { + "default": null, + "description": "Email of the contact.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "relationships": { + "default": null, + "description": "Relationship with the contact.", + "items": { + "type": "string" + }, + "title": "Relationships", + "type": "array" + }, + "birthday": { + "default": null, + "description": "Birthday of the contact in YYYY-MM-DD format.", + "title": "Birthday", + "type": "string" + }, + "home_address": { + "default": null, + "description": "Home address of the contact.", + "title": "Home Address", + "type": "string" + }, + "work_address": { + "default": null, + "description": "Work address of the contact.", + "title": "Work Address", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__update_contact", + "description": "Update contact information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Updated first name of the contact.", + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Updated last name of the contact.", + "title": "Last Name", + "type": "string" + }, + "phone_number": { + "default": null, + "description": "Updated phone number of the contact.", + "title": "Phone Number", + "type": "string" + }, + "email": { + "default": null, + "description": "Updated email of the contact.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "relationships": { + "default": null, + "description": "Updated relationship with the contact.", + "items": { + "type": "string" + }, + "title": "Relationships", + "type": "array" + }, + "birthday": { + "default": null, + "description": "Updated birthday of the contact in YYYY-MM-DD format.", + "title": "Birthday", + "type": "string" + }, + "home_address": { + "default": null, + "description": "Updated home address of the contact.", + "title": "Home Address", + "type": "string" + }, + "work_address": { + "default": null, + "description": "Updated work address of the contact.", + "title": "Work Address", + "type": "string" + }, + "contact_id": { + "description": "ID of the contact to update.", + "title": "Contact Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "contact_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__delete_contact", + "description": "Delete contact information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "contact_id": { + "description": "ID of the contact to be deleted.", + "title": "Contact Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "contact_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_text_message_window", + "description": "Show text messages with a contact around a given date and time.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "phone_number": { + "description": "The phone number of the contact to show messages with.", + "title": "Phone Number", + "type": "string" + }, + "min_datetime": { + "default": "1500-01-01|00:00:00", + "description": "The minimum datetime to show messages on or after in YYYY-MM-DD|HH:MM:SS format.", + "title": "Min Datetime", + "type": "string" + }, + "max_datetime": { + "default": "3000-01-01|00:00:00", + "description": "The maximum datetime to show messages on or before in YYYY-MM-DD|HH:MM:SS format.", + "title": "Max Datetime", + "type": "string" + }, + "pagination_order": { + "default": "descending", + "description": "If set to ascending, as page_index increases, the results will have newer messages. If set to descending, as page_index increases, the results will have older messages. The messages within each page will always be oldest to newest.", + "enum": [ + "ascending", + "descending" + ], + "title": "Pagination Order", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "phone_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__search_text_messages", + "description": "Show or search your text messages.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "phone_number": { + "default": null, + "description": "The phone number of the contact to show messages with.", + "title": "Phone Number", + "type": "string" + }, + "only_latest_per_contact": { + "default": false, + "description": "If set to true, only the latest message from each contact will be shown.", + "title": "Only Latest Per Contact", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the messages by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_text_message", + "description": "Show text message details.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "text_message_id": { + "description": "ID of the text message to show.", + "title": "Text Message Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "text_message_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__delete_text_message", + "description": "Delete a text message.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "text_message_id": { + "description": "ID of the text message to be deleted.", + "title": "Text Message Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "text_message_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__send_text_message", + "description": "Send a text message on the given phone number.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "message": { + "description": "The content of the text message.", + "minLength": 1, + "title": "Message", + "type": "string" + }, + "phone_number": { + "description": "The phone number of the contact to send the message to.", + "title": "Phone Number", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "message", + "phone_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_alarms", + "description": "Get a list of alarms.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__create_alarm", + "description": "Create a new alarm.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "time": { + "description": "The time of the alarm in HH:MM format.", + "title": "Time", + "type": "string" + }, + "repeat_days": { + "default": null, + "description": "Days on which the alarm repeats.", + "items": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "title": "Repeat Days", + "type": "array" + }, + "label": { + "default": null, + "description": "The label for the alarm.", + "title": "Label", + "type": "string" + }, + "enabled": { + "default": true, + "description": "Whether the alarm is enabled or not.", + "title": "Enabled", + "type": "boolean" + }, + "snooze_minutes": { + "default": 15, + "description": "The duration of snooze in minutes. Use 0 for no snooze.", + "minimum": 0, + "title": "Snooze Minutes", + "type": "integer" + }, + "vibration": { + "default": true, + "description": "Whether the alarm should vibrate or not.", + "title": "Vibration", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "time", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_alarm", + "description": "Show alarm details.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "alarm_id": { + "description": "ID of the alarm to show.", + "title": "Alarm Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "alarm_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__update_alarm", + "description": "Update an alarm's settings.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "time": { + "default": null, + "description": "The updated time of the alarm in HH:MM format.", + "title": "Time", + "type": "string" + }, + "repeat_days": { + "default": null, + "description": "The updated days on which the alarm should repeat.", + "items": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "title": "Repeat Days", + "type": "array" + }, + "label": { + "default": null, + "description": "The updated label for the alarm.", + "title": "Label", + "type": "string" + }, + "enabled": { + "default": null, + "description": "Whether the alarm is enabled or not.", + "title": "Enabled", + "type": "boolean" + }, + "snooze_minutes": { + "default": null, + "description": "The updated duration of snooze in minutes. Use 0 for no snooze.", + "minimum": 0, + "title": "Snooze Minutes", + "type": "integer" + }, + "vibration": { + "default": null, + "description": "Whether the alarm should vibrate or not.", + "title": "Vibration", + "type": "boolean" + }, + "alarm_id": { + "description": "ID of the alarm to be updated.", + "title": "Alarm Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "alarm_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__delete_alarm", + "description": "Delete an alarm.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "alarm_id": { + "description": "ID of the alarm to delete.", + "title": "Alarm Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "alarm_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_voice_message_window", + "description": "Show voice messages with a contact around a given date and time.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "phone_number": { + "description": "The phone number of the contact to show messages with.", + "title": "Phone Number", + "type": "string" + }, + "min_datetime": { + "default": "1500-01-01|00:00:00", + "description": "The minimum datetime to show messages on or after in YYYY-MM-DD|HH:MM:SS format.", + "title": "Min Datetime", + "type": "string" + }, + "max_datetime": { + "default": "3000-01-01|00:00:00", + "description": "The maximum datetime to show messages on or before in YYYY-MM-DD|HH:MM:SS format.", + "title": "Max Datetime", + "type": "string" + }, + "pagination_order": { + "default": "descending", + "description": "If set to ascending, as page_index increases, the results will have newer messages. If set to descending, as page_index increases, the results will have older messages. The messages within each page will always be oldest to newest.", + "enum": [ + "ascending", + "descending" + ], + "title": "Pagination Order", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "phone_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__search_voice_messages", + "description": "Show or search text voice_messages between the user and a contact.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "phone_number": { + "default": null, + "description": "The phone number of the contact to show voice_messages with.", + "title": "Phone Number", + "type": "string" + }, + "only_latest_per_contact": { + "default": false, + "description": "If set to true, only the latest message from each contact will be shown.", + "title": "Only Latest Per Contact", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the voice messages by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__show_voice_message", + "description": "Show voice message details.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "voice_message_id": { + "description": "ID of the voice message to show.", + "title": "Voice Message Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "voice_message_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__delete_voice_message", + "description": "Delete a voice message.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "voice_message_id": { + "description": "The ID of the voice message to delete.", + "title": "Voice Message Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "voice_message_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__send_voice_message", + "description": "Send a voice message on the given phone number.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "message": { + "description": "The message text of the voice_message.", + "minLength": 1, + "title": "Message", + "type": "string" + }, + "phone_number": { + "description": "The phone number of the contact to send the voice message to.", + "title": "Phone Number", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from phone app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "message", + "phone_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "phone__get_current_date_and_time", + "description": "Show current date and time.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__create_directory", + "description": "Create a directory if it does not exist, optionally recursively.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "directory_path": { + "description": "Path of the directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Directory Path", + "type": "string" + }, + "recursive": { + "default": false, + "description": "If True, it will create all parent directories recursively if they don't exist.", + "title": "Recursive", + "type": "boolean" + }, + "allow_if_exists": { + "default": true, + "description": "If True, it will not raise an error if the directory already exists.", + "title": "Allow If Exists", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__delete_directory", + "description": "Delete a directory with its sub-directories and files.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "directory_path": { + "description": "Path of the directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Directory Path", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__show_directory", + "description": "Show a list of files and/or sub-directories, optionally recursively, in a directory.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "directory_path": { + "default": "/", + "description": "Path of the directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "minLength": 1, + "title": "Directory Path", + "type": "string" + }, + "substring": { + "default": null, + "description": "If passed, only files and sub-directories containing the given substring (ignoring case) will be shown.", + "title": "Substring", + "type": "string" + }, + "entry_type": { + "default": "all", + "description": "Whether to show all files and sub-directories, only files, or only sub-directories.", + "enum": [ + "all", + "files", + "directories" + ], + "title": "Entry Type", + "type": "string" + }, + "recursive": { + "default": true, + "description": "Whether to show files recursively.", + "title": "Recursive", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__directory_exists", + "description": "Check if a directory exists.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "directory_path": { + "description": "Path of the directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "minLength": 1, + "title": "Directory Path", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__show_file", + "description": "Show a file's content and other details, if it exists.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_path": { + "description": "Path of the file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "File Path", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__create_file", + "description": "Create a new file with the given content.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_path": { + "description": "Path of the file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "File Path", + "type": "string" + }, + "content": { + "default": "", + "description": "The content of the file.", + "title": "Content", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__update_file", + "description": "Update a file's content.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_path": { + "description": "Path of the file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "File Path", + "type": "string" + }, + "content": { + "description": "The updated content of the file.", + "title": "Content", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "file_path", + "content", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__delete_file", + "description": "Delete a file.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_path": { + "description": "Path of the file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "File Path", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__file_exists", + "description": "Check if a file exists.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_path": { + "description": "Path of the file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "minLength": 1, + "title": "File Path", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__copy_file", + "description": "Copy a file to another location.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "source_file_path": { + "description": "Path of the source file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Source File Path", + "type": "string" + }, + "destination_file_path": { + "description": "Path of the destination file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Destination File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "retain_dates": { + "default": false, + "description": "Whether the copied file should retain the original file's created and updated dates.", + "title": "Retain Dates", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "source_file_path", + "destination_file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__move_file", + "description": "Move a file to another location.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "source_file_path": { + "description": "Path of the source file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Source File Path", + "type": "string" + }, + "destination_file_path": { + "description": "Path of the destination file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Destination File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "retain_dates": { + "default": false, + "description": "Whether the moved file should retain the original file's created and updated dates.", + "title": "Retain Dates", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "source_file_path", + "destination_file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__copy_directory", + "description": "Copy a directory to another location.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "source_directory_path": { + "description": "Path of the source directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Source Directory Path", + "type": "string" + }, + "destination_directory_path": { + "description": "Path of the destination directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Destination Directory Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the directory if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "retain_dates": { + "default": false, + "description": "Whether the files in the copied directory should retain the created and updated dates of the files in the original directory.", + "title": "Retain Dates", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "source_directory_path", + "destination_directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__move_directory", + "description": "Move a directory to another location.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "source_directory_path": { + "description": "Path of the source directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Source Directory Path", + "type": "string" + }, + "destination_directory_path": { + "description": "Path of the destination directory. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Destination Directory Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the directory if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "retain_dates": { + "default": false, + "description": "Whether the files in the moved directory should retain the created and updated dates of the files in the original directory.", + "title": "Retain Dates", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "source_directory_path", + "destination_directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__compress_directory", + "description": "Compress a directory.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "directory_path": { + "description": "Path of the directory to compress. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'.", + "title": "Directory Path", + "type": "string" + }, + "compressed_file_path": { + "default": null, + "description": "Path of the compressed file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If passed, must have an extension: zip or tar. If not passed, it'll be stored as a zip file with the base name of directory_path in directory_path's parent directory.", + "title": "Compressed File Path", + "type": "string" + }, + "delete_directory": { + "default": false, + "description": "Whether to delete the directory after compression.", + "title": "Delete Directory", + "type": "boolean" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the compressed file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "directory_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "file_system__decompress_file", + "description": "Decompress a compressed file.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "compressed_file_path": { + "description": "Path of the compressed file. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. Must have an extension: zip or tar.", + "title": "Compressed File Path", + "type": "string" + }, + "decompressed_directory_path": { + "default": null, + "description": "Path of the directory to save decompressed files. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it'll be saved in the same directory as the compressed file.", + "title": "Decompressed Directory Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the decompressed directory if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "retain_dates": { + "default": true, + "description": "Whether the decompressed files should retain the created and updated dates of the files in the compressed file.", + "title": "Retain Dates", + "type": "boolean" + }, + "delete_compressed_file": { + "default": false, + "description": "Whether to delete the compressed file after decompression.", + "title": "Delete Compressed File", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from file_system app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "compressed_file_path", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__search_users", + "description": "Search users by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_genres", + "description": "Show the list of all music genres.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__search_songs", + "description": "Search for songs with a query.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "artist_id": { + "default": null, + "description": "The artist id to filter the search results by.", + "title": "Artist Id", + "type": "integer" + }, + "album_id": { + "default": null, + "description": "The album id to filter the search results by.", + "title": "Album Id", + "type": "integer" + }, + "genre": { + "default": null, + "description": "The genre to filter the albums by.", + "title": "Genre", + "type": "string" + }, + "min_release_date": { + "default": "1500-01-01", + "description": "The minimum release date for search results in YYYY-MM-DD format.", + "title": "Min Release Date", + "type": "string" + }, + "max_release_date": { + "default": "3000-01-01", + "description": "The maximum release date for search results in YYYY-MM-DD format.", + "title": "Max Release Date", + "type": "string" + }, + "min_duration": { + "default": 0, + "description": "The minimum duration in seconds for search results.", + "minimum": 0, + "title": "Min Duration", + "type": "integer" + }, + "max_duration": { + "default": 9223372036854775807, + "description": "The maximum duration in seconds for search results.", + "minimum": 0, + "title": "Max Duration", + "type": "integer" + }, + "min_rating": { + "default": 0.0, + "description": "The minimum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Min Rating", + "type": "number" + }, + "max_rating": { + "default": 5.0, + "description": "The maximum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Max Rating", + "type": "number" + }, + "min_like_count": { + "default": 0, + "description": "The minimum like count for search results.", + "minimum": 0, + "title": "Min Like Count", + "type": "integer" + }, + "max_like_count": { + "default": 9223372036854775807, + "description": "The maximum like count for search results.", + "minimum": 0, + "title": "Max Like Count", + "type": "integer" + }, + "min_play_count": { + "default": 0, + "description": "The minimum play count for search results.", + "minimum": 0, + "title": "Min Play Count", + "type": "integer" + }, + "max_play_count": { + "default": 9223372036854775807, + "description": "The maximum play count for search results.", + "minimum": 0, + "title": "Max Play Count", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the songs by prefixed with +/- to reflect ascending/descending. Valid attributes: rating, like_count and play_count. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song", + "description": "Get details of a specific song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to retrieve.", + "title": "Song Id", + "type": "integer" + } + }, + "required": [ + "song_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song_privates", + "description": "Show information about the song that is private to the user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to retrieve.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__like_song", + "description": "Like a song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to like.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__unlike_song", + "description": "Unlike a song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to unlike.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_liked_songs", + "description": "Get a list of songs you have liked.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": "-liked_at", + "description": "The attribute to sort the liked songs by prefixed with +/- to reflect ascending/descending. Valid attributes: liked_at, play_count and title.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__search_albums", + "description": "Search for albums with a query.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "min_rating": { + "default": 0.0, + "description": "The minimum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Min Rating", + "type": "number" + }, + "max_rating": { + "default": 5.0, + "description": "The maximum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Max Rating", + "type": "number" + }, + "min_release_date": { + "default": "1500-01-01", + "description": "The minimum release date for search results in YYYY-MM-DD format.", + "title": "Min Release Date", + "type": "string" + }, + "max_release_date": { + "default": "3000-01-01", + "description": "The maximum release date for search results in YYYY-MM-DD format.", + "title": "Max Release Date", + "type": "string" + }, + "min_like_count": { + "default": 0, + "description": "The minimum like count for search results.", + "minimum": 0, + "title": "Min Like Count", + "type": "integer" + }, + "max_like_count": { + "default": 9223372036854775807, + "description": "The maximum like count for search results.", + "minimum": 0, + "title": "Max Like Count", + "type": "integer" + }, + "genre": { + "default": null, + "description": "The genre to filter the albums by.", + "title": "Genre", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the albums by prefixed with +/- to reflect ascending/descending. Valid attributes: rating and release_date. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_album", + "description": "Get details of a specific album.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to retrieve.", + "title": "Album Id", + "type": "integer" + } + }, + "required": [ + "album_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_album_privates", + "description": "Show information about the album that is private to the user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to retrieve.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__like_album", + "description": "Like a album.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to like.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__unlike_album", + "description": "Unlike a album.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to unlike.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_liked_albums", + "description": "Get a list of albums you have liked.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": "-liked_at", + "description": "The attribute to sort the liked albums by prefixed with +/- to reflect ascending/descending. Valid attributes: liked_at and title.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_playlist_library", + "description": "Search or show a list of playlists in your playlist library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "is_public": { + "default": null, + "description": "Whether to show public playlists or private playlists.", + "title": "Is Public", + "type": "boolean" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the library playlists by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and title. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__search_playlists", + "description": "Search for playlists with a query. It will search over all public playlists and your own private playlists. If the access token is not provided, it will only search public playlists.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "min_like_count": { + "default": 0, + "description": "The minimum like count for search results.", + "minimum": 0, + "title": "Min Like Count", + "type": "integer" + }, + "max_like_count": { + "default": 9223372036854775807, + "description": "The maximum like count for search results.", + "minimum": 0, + "title": "Max Like Count", + "type": "integer" + }, + "min_rating": { + "default": 0.0, + "description": "The minimum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Min Rating", + "type": "number" + }, + "max_rating": { + "default": 5.0, + "description": "The maximum rating for search results.", + "maximum": 5.0, + "minimum": 0.0, + "title": "Max Rating", + "type": "number" + }, + "owner_email": { + "default": null, + "description": "If passed, will filter results to only the ones owned by this user.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Owner Email", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the playlists by prefixed with +/- to reflect ascending/descending. Valid attributes: like_count, rating and created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -like_count.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "default": null, + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__create_playlist", + "description": "Create a new playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "description": "The title of the playlist.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "is_public": { + "default": false, + "description": "Whether the playlist is public or not.", + "title": "Is Public", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "title", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_playlist", + "description": "Get detailed information about a specific playlist. You can view your own playlists or others' playlists if they are public.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to retrieve.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "default": null, + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_playlist", + "description": "Update a playlist title or privacy.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "default": null, + "description": "The updated title of the playlist.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "is_public": { + "default": null, + "description": "Whether the playlist is public or not.", + "title": "Is Public", + "type": "boolean" + }, + "playlist_id": { + "description": "The playlist id to update.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_playlist", + "description": "Delete a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to delete.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_playlist_privates", + "description": "Show information about the playlist that is private to the user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to retrieve.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__like_playlist", + "description": "Like a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to like.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__unlike_playlist", + "description": "Unlike a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to unlike.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_liked_playlists", + "description": "Get a list of playlists you have liked.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": "-liked_at", + "description": "The attribute to sort the liked playlists by prefixed with +/- to reflect ascending/descending. Valid attributes: liked_at and title.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__search_artists", + "description": "Search for artists with a query.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "genre": { + "default": null, + "description": "The genre to filter the artists by.", + "title": "Genre", + "type": "string" + }, + "min_follower_count": { + "default": 0, + "description": "The minimum number of followers for search results.", + "minimum": 0, + "title": "Min Follower Count", + "type": "integer" + }, + "max_follower_count": { + "default": 9223372036854775807, + "description": "The maximum number of followers for search results.", + "minimum": 0, + "title": "Max Follower Count", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the artists by prefixed with +/- to reflect ascending/descending. Valid attributes: follower_count. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_artist", + "description": "Get details of a specific artist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "artist_id": { + "description": "The artist id to retrieve.", + "title": "Artist Id", + "type": "integer" + } + }, + "required": [ + "artist_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_artist_following", + "description": "Show if the user is following the artist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "artist_id": { + "description": "The artist id to retrieve.", + "title": "Artist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "artist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song_library", + "description": "Search or show a list of songs in your song library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the library songs by prefixed with +/- to reflect ascending/descending. Valid attributes: added_at and title. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -added_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__add_song_to_library", + "description": "Add a song to your song library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to add.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__remove_song_from_library", + "description": "Remove a song from your song library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to remove.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_album_library", + "description": "Search or show a list of albums in your album library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the library albums by prefixed with +/- to reflect ascending/descending. Valid attributes: added_at and title. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -added_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__add_album_to_library", + "description": "Add an album to your album library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to add.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__remove_album_from_library", + "description": "Remove an album from your album library.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to remove.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__add_song_to_playlist", + "description": "Add a song to a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to add the song to.", + "title": "Playlist Id", + "type": "integer" + }, + "song_id": { + "description": "The song id to add.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__remove_song_from_playlist", + "description": "Remove a song from a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to remove the song from.", + "title": "Playlist Id", + "type": "integer" + }, + "song_id": { + "description": "The song id to remove.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id", + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_downloaded_songs", + "description": "Search or show a list of your downloaded songs.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "min_downloaded_at": { + "default": "1500-01-01", + "description": "Filter songs by minimum downloaded_at date in YYYY-MM-DD format.", + "title": "Min Downloaded At", + "type": "string" + }, + "max_downloaded_at": { + "default": "3000-01-01", + "description": "Filter songs by maximum downloaded_at date in YYYY-MM-DD format.", + "title": "Max Downloaded At", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the downloaded songs by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and downloaded_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -downloaded_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__download_song", + "description": "Download a song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to download.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__remove_downloaded_song", + "description": "Remove a song from downloads.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to remove from downloads.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_following_artists", + "description": "Search or show a list of artists you are following.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the following artists by prefixed with +/- to reflect ascending/descending. Valid attributes: followed_at and name. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -followed_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__follow_artist", + "description": "Follow an artist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "artist_id": { + "description": "The artist id to follow.", + "title": "Artist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "artist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__unfollow_artist", + "description": "Unfollow an artist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "artist_id": { + "description": "The artist id to unfollow.", + "title": "Artist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "artist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song_reviews", + "description": "Search or show a list of reviews for a song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "description": "The song id to retrieve reviews for.", + "title": "Song Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user to filter reviews by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "min_rating": { + "default": 1, + "description": "The minimum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Min Rating", + "type": "integer" + }, + "max_rating": { + "default": 5, + "description": "The maximum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Max Rating", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the song reviews by prefixed with +/- to reflect ascending/descending. Valid attributes: rating and created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "required": [ + "song_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__review_song", + "description": "Rate or review a song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "description": "Song rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": "", + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": "", + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "song_id": { + "description": "ID of the song to review.", + "title": "Song Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "rating", + "song_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_song_review", + "description": "Update a song review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "default": null, + "description": "Song rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": null, + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": null, + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "review_id": { + "description": "ID of the song review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_song_review", + "description": "Delete a song review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "ID of the song review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song_review", + "description": "Show a song review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "The song review id to retrieve.", + "title": "Review Id", + "type": "integer" + } + }, + "required": [ + "review_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_album_reviews", + "description": "Search or show a list of reviews for an album.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "album_id": { + "description": "The album id to retrieve reviews for.", + "title": "Album Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user to filter reviews by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "min_rating": { + "default": 1, + "description": "The minimum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Min Rating", + "type": "integer" + }, + "max_rating": { + "default": 5, + "description": "The maximum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Max Rating", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the album reviews by prefixed with +/- to reflect ascending/descending. Valid attributes: rating and created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + } + }, + "required": [ + "album_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__review_album", + "description": "Rate or review an album.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "description": "Album rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": "", + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": "", + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "album_id": { + "description": "ID of the album to review.", + "title": "Album Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "rating", + "album_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_album_review", + "description": "Update an album review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "default": null, + "description": "Album rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": null, + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": null, + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "review_id": { + "description": "ID of the album review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_album_review", + "description": "Delete an album review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "ID of the album review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_album_review", + "description": "Show an album review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "The album review id to retrieve.", + "title": "Review Id", + "type": "integer" + } + }, + "required": [ + "review_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_playlist_reviews", + "description": "Search or show a list of reviews for your playlist or others' public playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "playlist_id": { + "description": "The playlist id to retrieve reviews for.", + "title": "Playlist Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email of the user to filter reviews by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "min_rating": { + "default": 1, + "description": "The minimum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Min Rating", + "type": "integer" + }, + "max_rating": { + "default": 5, + "description": "The maximum rating for a review.", + "maximum": 5, + "minimum": 1, + "title": "Max Rating", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the playlist reviews by prefixed with +/- to reflect ascending/descending. Valid attributes: rating and created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "default": null, + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "playlist_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__review_playlist", + "description": "Rate or review a playlist.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "description": "Playlist rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": "", + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": "", + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "playlist_id": { + "description": "ID of the playlist to review.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "rating", + "playlist_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_playlist_review", + "description": "Update a playlist review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "rating": { + "default": null, + "description": "Playlist rating for this review.", + "maximum": 5, + "minimum": 1, + "title": "Rating", + "type": "integer" + }, + "title": { + "default": null, + "description": "Title of the review.", + "title": "Title", + "type": "string" + }, + "text": { + "default": null, + "description": "Text content of the review.", + "title": "Text", + "type": "string" + }, + "review_id": { + "description": "ID of the playlist review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_playlist_review", + "description": "Delete a playlist review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "ID of the playlist review.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_playlist_review", + "description": "Show a playlist review.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "review_id": { + "description": "The song review id to retrieve.", + "title": "Review Id", + "type": "integer" + }, + "access_token": { + "default": null, + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "review_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_payment_cards", + "description": "Get a list of users payment cards.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__add_payment_card", + "description": "Add a new payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "owner_name": { + "description": "Full name of the owner of the payment card.", + "minLength": 1, + "title": "Owner Name", + "type": "string" + }, + "card_number": { + "description": "16-digit card number.", + "exclusiveMaximum": 10000000000000000, + "minimum": 1000000000000000, + "title": "Card Number", + "type": "integer" + }, + "expiry_year": { + "description": "Expiration year of the payment card.", + "title": "Expiry Year", + "type": "integer" + }, + "expiry_month": { + "description": "Expiration month of the payment card.", + "maximum": 12, + "minimum": 1, + "title": "Expiry Month", + "type": "integer" + }, + "cvv_number": { + "description": "A 3-digit CVV number of the payment card.", + "exclusiveMaximum": 1000, + "minimum": 100, + "title": "Cvv Number", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "owner_name", + "card_number", + "expiry_year", + "expiry_month", + "cvv_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_payment_card", + "description": "Get details of a payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be shown.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__update_payment_card", + "description": "Update payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card to update.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "payment_card_id": { + "description": "ID of the payment card to update.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__delete_payment_card", + "description": "Delete payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be deleted.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_current_song", + "description": "Show details of the current song on the queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__play_music", + "description": "Play music based on various criteria. You can pass, at most, any one of queue_position, song_id, album_id or playlist_id. If one of song_id, album_id or playlist_id is passed, that song, album or playlist will be added to the queue and played. Otherwise, the queue will remain unchanged. If queue_position is passed, the song at that position in the queue will be played. If none is passed, the current song in the queue will be played.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "default": null, + "description": "ID of the song to play.", + "title": "Song Id", + "type": "integer" + }, + "album_id": { + "default": null, + "description": "ID of the album to play.", + "title": "Album Id", + "type": "integer" + }, + "playlist_id": { + "default": null, + "description": "ID of the playlist to play.", + "title": "Playlist Id", + "type": "integer" + }, + "queue_position": { + "default": null, + "description": "Position of the song in the queue to play.", + "minimum": 0, + "title": "Queue Position", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__pause_music", + "description": "Pause the currently playing song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__previous_song", + "description": "Go to the previous song in the song queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__next_song", + "description": "Go to the next song in the song queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__move_song_in_queue", + "description": "Move a song in the queue to a new position.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "current_position": { + "description": "The current position of the song in the queue.", + "minimum": 0, + "title": "Current Position", + "type": "integer" + }, + "new_position": { + "description": "The new position of the song in the queue.", + "minimum": 0, + "title": "New Position", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "current_position", + "new_position", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__seek_song", + "description": "Seek the current song to the given number of seconds.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "seek_seconds": { + "description": "The number of seconds to seek.", + "minimum": 0, + "title": "Seek Seconds", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "seek_seconds", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__loop_song", + "description": "Set whether to loop the current song.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "loop": { + "description": "Whether to loop the current song.", + "title": "Loop", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "loop", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__shuffle_song_queue", + "description": "Shuffle songs in the music player queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_song_queue", + "description": "Get the music player song queue. Songs are played in the order of the queue in a cycle.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__add_to_queue", + "description": "Add a song, album or playlist to the music player song queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "song_id": { + "default": null, + "description": "ID of the song to add to queue.", + "title": "Song Id", + "type": "integer" + }, + "album_id": { + "default": null, + "description": "ID of the album to add to queue.", + "title": "Album Id", + "type": "integer" + }, + "playlist_id": { + "default": null, + "description": "ID of the playlist to add to queue.", + "title": "Playlist Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__clear_song_queue", + "description": "Clear the music player song queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__remove_song_from_queue", + "description": "Remove a song at the given position from the music player song queue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "position": { + "description": "The 0-indexed position of the song in the queue.", + "title": "Position", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "position", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_volume", + "description": "Get the volume level of the music player.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__set_volume", + "description": "Set the volume level of the music player.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "volume": { + "description": "Volume level to set.", + "maximum": 10, + "minimum": 0, + "title": "Volume", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "volume", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_recommendations", + "description": "Get personalized song recommendations for the user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_premium_plans", + "description": "Show information about premium plans available.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__subscribe_premium", + "description": "Subscribe to premium membership.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to use for buy premium subscription.", + "title": "Payment Card Id", + "type": "integer" + }, + "duration": { + "description": "Duration of the premium subscription.", + "enum": [ + "monthly", + "yearly" + ], + "title": "Duration", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "duration", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__show_premium_subscriptions", + "description": "Show your premium subscription history.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "spotify__download_premium_subscription_receipt", + "description": "Download the receipt for a premium subscription.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "premium_subscription_id": { + "description": "ID of the premium subscription to download the receipt for.", + "title": "Premium Subscription Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from spotify app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "premium_subscription_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_profile", + "description": "Show public profile information of a user, including your friendship status with them.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__search_users", + "description": "Search Venmo users by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__search_friends", + "description": "Search your or others' friends by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search friends by name or email address.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "Email address of the user whose friends you want to see. If not passed, your friends will be returned.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__add_friend", + "description": "Add a friend to your friend list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_email": { + "description": "Email address of the friend to add.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "user_email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__remove_friend", + "description": "Remove a friend from your friend list.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_email": { + "description": "Email address of the friend to remove.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "user_email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__add_to_venmo_balance", + "description": "Add money to your Venmo balance.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "amount": { + "description": "Amount to add to Venmo balance.", + "exclusiveMinimum": 0.0, + "title": "Amount", + "type": "number" + }, + "payment_card_id": { + "description": "ID of the payment card to use for adding balance.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "amount", + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_venmo_balance", + "description": "Show your Venmo balance.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__withdraw_from_venmo_balance", + "description": "Withdraw money from your Venmo balance.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "amount": { + "description": "Amount to withdraw from Venmo balance.", + "exclusiveMinimum": 0.0, + "title": "Amount", + "type": "number" + }, + "payment_card_id": { + "description": "ID of the payment card to credit the withdrawn amount to.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "amount", + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_bank_transfer_history", + "description": "Show histroy of money transfer from Venmo to payment card and vice versa.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transfer_type": { + "default": null, + "description": "Filter bank transfers by type. Will skip filtering if not passed.", + "enum": [ + "card_to_venmo", + "venmo_to_card" + ], + "title": "Transfer Type", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__download_bank_transfer_receipt", + "description": "Download the receipt of money transfer from Venmo to payment card or vice versa.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "bank_transfer_id": { + "description": "ID of the bank transfer to download the receipt for.", + "title": "Bank Transfer Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "bank_transfer_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_transaction", + "description": "Show transaction details.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transaction_id": { + "description": "ID of the transaction to retrieve.", + "title": "Transaction Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__update_transaction", + "description": "Update transaction information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "description": { + "default": null, + "description": "Updated description of or note about the transaction.", + "title": "Description", + "type": "string" + }, + "private": { + "default": null, + "description": "Updated privacy of the transaction.", + "title": "Private", + "type": "boolean" + }, + "transaction_id": { + "description": "ID of the transaction to update.", + "title": "Transaction Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__create_transaction", + "description": "Send money to a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "receiver_email": { + "description": "Email address of the receiver.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Receiver Email", + "type": "string" + }, + "amount": { + "description": "Amount of the transaction.", + "exclusiveMinimum": 0.0, + "title": "Amount", + "type": "number" + }, + "description": { + "default": "", + "description": "Description of or note about the transaction.", + "title": "Description", + "type": "string" + }, + "payment_card_id": { + "default": null, + "description": "ID of the payment card to use for the transaction. If not passed, Venmo balance will be used.", + "title": "Payment Card Id", + "type": "integer" + }, + "private": { + "default": false, + "description": "Whether the transaction is private or not.", + "title": "Private", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "receiver_email", + "amount", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_transactions", + "description": "Search or show a list of your transactions.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "user_email": { + "default": null, + "description": "If passed, only transactions between you and user with this email address will be shown.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Minimum created_at datetime to filter transactions in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Maximum created_at datetime to filter transactions in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "min_like_count": { + "default": 0, + "description": "Minimum like count to filter transactions.", + "title": "Min Like Count", + "type": "integer" + }, + "max_like_count": { + "default": 9223372036854775807, + "description": "Maximum like count to filter transactions.", + "title": "Max Like Count", + "type": "integer" + }, + "min_amount": { + "default": 0.01, + "description": "Minimum amount to filter transactions.", + "exclusiveMinimum": 0.0, + "title": "Min Amount", + "type": "number" + }, + "max_amount": { + "default": 9.223372036854776e+18, + "description": "Maximum amount to filter transactions.", + "exclusiveMinimum": 0.0, + "title": "Max Amount", + "type": "number" + }, + "private": { + "default": null, + "description": "Filter transactions by privacy.", + "title": "Private", + "type": "boolean" + }, + "direction": { + "default": null, + "description": "Filter transactions by direction of the transaction (sent or received). Will skip filtering if not passed.", + "enum": [ + "sent", + "received" + ], + "title": "Direction", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the transactions by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at, like_count and amount. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__download_transaction_receipt", + "description": "Download the receipt of a transaction (money sent from one user to another).", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transaction_id": { + "description": "ID of the transaction to download the receipt for.", + "title": "Transaction Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__like_transaction", + "description": "Like a transaction.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transaction_id": { + "description": "ID of the transaction to like.", + "title": "Transaction Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__unlike_transaction", + "description": "Unlike a transaction.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transaction_id": { + "description": "ID of the transaction to unlike.", + "title": "Transaction Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_transaction_comments", + "description": "Get a list of transaction comments.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "transaction_id": { + "description": "ID of the transaction to retrieve comments for.", + "title": "Transaction Id", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__create_transaction_comment", + "description": "Create a new transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "Comment on the transaction.", + "title": "Comment", + "type": "string" + }, + "transaction_id": { + "description": "ID of the transaction to comment on.", + "title": "Transaction Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "transaction_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_transaction_comment", + "description": "Show detailed information about a transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "ID of the transaction comment to show details of.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__update_transaction_comment", + "description": "Update a transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "Updated comment on the transaction.", + "title": "Comment", + "type": "string" + }, + "comment_id": { + "description": "ID of the transaction comment to update.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_transaction_comment", + "description": "Delete a transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "ID of the transaction comment to be deleted.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__like_transaction_comment", + "description": "Like a transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "ID of the transaction comment to like.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__unlike_transaction_comment", + "description": "Unlike a previously liked transaction comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "ID of the transaction comment to like.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_payment_card", + "description": "Get details of a payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be shown.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__update_payment_card", + "description": "Update payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card to update.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "payment_card_id": { + "description": "ID of the payment card to update.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_payment_card", + "description": "Delete payment card information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "description": "ID of the payment card to be deleted.", + "title": "Payment Card Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_card_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_payment_cards", + "description": "Get a list of users payment cards.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__add_payment_card", + "description": "Add a new payment card.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "card_name": { + "description": "Name of the payment card.", + "minLength": 1, + "title": "Card Name", + "type": "string" + }, + "owner_name": { + "description": "Full name of the owner of the payment card.", + "minLength": 1, + "title": "Owner Name", + "type": "string" + }, + "card_number": { + "description": "16-digit card number.", + "exclusiveMaximum": 10000000000000000, + "minimum": 1000000000000000, + "title": "Card Number", + "type": "integer" + }, + "expiry_year": { + "description": "Expiration year of the payment card.", + "title": "Expiry Year", + "type": "integer" + }, + "expiry_month": { + "description": "Expiration month of the payment card.", + "maximum": 12, + "minimum": 1, + "title": "Expiry Month", + "type": "integer" + }, + "cvv_number": { + "description": "A 3-digit CVV number of the payment card.", + "exclusiveMaximum": 1000, + "minimum": 100, + "title": "Cvv Number", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "card_name", + "owner_name", + "card_number", + "expiry_year", + "expiry_month", + "cvv_number", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_received_payment_requests", + "description": "Search or show a list of payment requests you have received from others.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "status": { + "default": null, + "description": "Filter payment requests by status. Will skip filtering if not passed.", + "enum": [ + "pending", + "approved", + "denied" + ], + "title": "Status", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_sent_payment_requests", + "description": "Search or show a list of payment requests you have sent to others.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "status": { + "default": null, + "description": "Filter payment requests by status. Will skip filtering if not passed.", + "enum": [ + "pending", + "approved", + "denied" + ], + "title": "Status", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__create_payment_request", + "description": "Send a payment request.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_email": { + "description": "Email address of the receiver user.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "amount": { + "description": "Amount of the payment request.", + "exclusiveMinimum": 0.0, + "title": "Amount", + "type": "number" + }, + "description": { + "default": "", + "description": "Description of or note about the payment request.", + "title": "Description", + "type": "string" + }, + "private": { + "default": false, + "description": "The privacy of the transaction on approval of the payment request.", + "title": "Private", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "user_email", + "amount", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__update_payment_request", + "description": "Update payment request information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "amount": { + "default": null, + "description": "Updated amount of the payment request.", + "exclusiveMinimum": 0.0, + "title": "Amount", + "type": "number" + }, + "description": { + "default": null, + "description": "Updated description of or note about the payment request.", + "title": "Description", + "type": "string" + }, + "private": { + "default": null, + "description": "Updated privacy of the transaction on payment request approval.", + "title": "Private", + "type": "boolean" + }, + "payment_request_id": { + "description": "ID of the payment request to update.", + "title": "Payment Request Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_request_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_payment_request", + "description": "Delete a payment request.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_request_id": { + "description": "ID of the payment request to delete.", + "title": "Payment Request Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_request_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__approve_payment_request", + "description": "Approve a payment request.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_card_id": { + "default": null, + "description": "ID of the payment card to use for approving the payment request. If not passed, Venmo balance will be used.", + "title": "Payment Card Id", + "type": "integer" + }, + "payment_request_id": { + "description": "ID of the payment request to approve.", + "title": "Payment Request Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_request_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__deny_payment_request", + "description": "Deny a payment request.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_request_id": { + "description": "ID of the payment request to deny.", + "title": "Payment Request Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_request_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__remind_payment_request", + "description": "Send a reminder to a user via notification about this payment request.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_request_id": { + "description": "ID of the payment request to remind about.", + "title": "Payment Request Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_request_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_social_feed", + "description": "Show your social feed (transactions of your friends).", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_notifications", + "description": "Get a list of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "read": { + "default": null, + "description": "Filter notifications by read status. Will skip filtering if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_notifications", + "description": "Delete all of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__mark_notifications", + "description": "Mark all notifications as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Read status of the notification.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__show_notifications_count", + "description": "Get the count of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "default": null, + "description": "Count notifications by read status. Will count all if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__delete_notification", + "description": "Delete a notification.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "notification_id": { + "description": "ID of the notification to delete.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "venmo__mark_notification", + "description": "Mark a notification as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Mark the notification as read or unread.", + "title": "Read", + "type": "boolean" + }, + "notification_id": { + "description": "ID of the notification to mark.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from venmo app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__set_status", + "description": "Set your availability status.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "status": { + "description": "Your availability status.", + "enum": [ + "active", + "do_not_disturb", + "away" + ], + "title": "Status", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "status", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__search_users", + "description": "Search Gmail users by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__search_labels", + "description": "Search email thread labels by name.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 9223372036854775807, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_inbox_threads", + "description": "Show or search email threads you have received. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "starred": { + "default": null, + "description": "Filter emails by starred status.", + "title": "Starred", + "type": "boolean" + }, + "archived": { + "default": false, + "description": "Filter emails by archived status.", + "title": "Archived", + "type": "boolean" + }, + "spam": { + "default": false, + "description": "Filter emails by spam status.", + "title": "Spam", + "type": "boolean" + }, + "snoozed": { + "default": false, + "description": "Filter emails by snoozed status.", + "title": "Snoozed", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by whether they have an attachment or not.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_outbox_threads", + "description": "Show or search email threads you have sent. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "starred": { + "default": null, + "description": "Filter emails by starred status.", + "title": "Starred", + "type": "boolean" + }, + "archived": { + "default": false, + "description": "Filter emails by archived status.", + "title": "Archived", + "type": "boolean" + }, + "spam": { + "default": false, + "description": "Filter emails by spam status.", + "title": "Spam", + "type": "boolean" + }, + "snoozed": { + "default": false, + "description": "Filter emails by snoozed status.", + "title": "Snoozed", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_snoozed_threads", + "description": "Show or search email threads you have snoozed. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "starred": { + "default": null, + "description": "Filter emails by starred status.", + "title": "Starred", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_starred_threads", + "description": "Show or search email threads you have starred. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "archived": { + "default": null, + "description": "Filter emails by archived status.", + "title": "Archived", + "type": "boolean" + }, + "spam": { + "default": null, + "description": "Filter emails by spam status.", + "title": "Spam", + "type": "boolean" + }, + "snoozed": { + "default": null, + "description": "Filter emails by snoozed status.", + "title": "Snoozed", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_archived_threads", + "description": "Show or search email threads you have archived. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "starred": { + "default": null, + "description": "Filter emails by starred status.", + "title": "Starred", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_spam_threads", + "description": "Show or search email threads that have been marked as spam. This will not show detailed information about emails within the thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "label": { + "default": null, + "description": "The label to filter the emails by.", + "title": "Label", + "type": "string" + }, + "starred": { + "default": null, + "description": "Filter emails by starred status.", + "title": "Starred", + "type": "boolean" + }, + "read": { + "default": null, + "description": "Filter emails by read status.", + "title": "Read", + "type": "boolean" + }, + "attachment": { + "default": null, + "description": "Filter emails by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "from_email": { + "default": null, + "description": "Filter emails by sender email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "From Email", + "type": "string" + }, + "to_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "To Email", + "type": "string" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the email threads by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_category_sizes", + "description": "Show the number of email threads in each category of inbox, outbox, archived, spam, and the number of unscheduled and scheduled email drafts.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "default": null, + "description": "Count only read or unread email threads. If None, count both read and unread threads. It is not applicable for drafts.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_thread", + "description": "Show detailed information about a given email thread, including emails and drafts within it.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to show.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__delete_thread", + "description": "Delete an email thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to delete.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_email", + "description": "Show detailed information about a given email.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_id": { + "description": "The ID of the email to show.", + "title": "Email Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__label_thread", + "description": "Label an email thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "label": { + "description": "The label to assign to the email thread.", + "title": "Label", + "type": "string" + }, + "email_thread_id": { + "description": "The ID of the email thread to label.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "label", + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__unlabel_thread", + "description": "Remove label from an email thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to unlabel.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_read", + "description": "Mark an email thread as read.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to mark as read.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_unread", + "description": "Mark an email thread as unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to mark as unread.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_archived", + "description": "Mark an email thread as archived. This will also remove the spam and snooze status if any.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to archive.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_unarchived", + "description": "Mark an email thread as unarchived.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to unarchive.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_spam", + "description": "Mark an email thread as spam. This will also remove the archived and snooze status if any.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to mark as spam.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_not_spam", + "description": "Mark an email thread as not spam.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to mark as not spam.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_starred", + "description": "Mark an email thread as starred.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to star.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__mark_thread_unstarred", + "description": "Mark an email thread as unstarred.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to unstar.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__delete_email_in_thread", + "description": "Delete an email in a thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to delete an email from.", + "title": "Email Thread Id", + "type": "integer" + }, + "email_id": { + "description": "The ID of the email to delete.", + "title": "Email Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "email_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__snooze_thread", + "description": "Snooze an email thread until a given date and time in the future. It will reappear in unread state in your inbox and/or outbox at that time. It will also be removed from archived or spam categories if it was in either of those.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "snooze_until": { + "description": "The date and time to snooze the email thread until. The date and time should be in YYYY-MM-DD|HH:MM:SS format", + "title": "Snooze Until", + "type": "string" + }, + "email_thread_id": { + "description": "The ID of the email thread to snooze.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "snooze_until", + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__unsnooze_thread", + "description": "Unsnooze an email thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_thread_id": { + "description": "The ID of the email thread to unsnooze.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__send_email", + "description": "Send a new email to one or more recipients.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_addresses": { + "description": "List of recipient email addresses.", + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "subject": { + "description": "The subject of the email.", + "title": "Subject", + "type": "string" + }, + "body": { + "description": "The body of the email.", + "title": "Body", + "type": "string" + }, + "attachment_file_paths": { + "default": null, + "description": "List of absolute file paths (starting with /) from the file_system app to attach to the email.", + "items": { + "type": "string" + }, + "title": "Attachment File Paths", + "type": "array" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed for attachments.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_addresses", + "subject", + "body", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__reply_to_email", + "description": "Reply to an existing email in a thread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_addresses": { + "default": null, + "description": "List of recipient email addresses. If passed, it'll reply to all recipients in the list, otherwise, to the sender.", + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "attachment_file_paths": { + "default": null, + "description": "List of absolute file paths (starting with /) from the file_system app to attach to the email.", + "items": { + "type": "string" + }, + "title": "Attachment File Paths", + "type": "array" + }, + "body": { + "description": "The body of the reply email.", + "title": "Body", + "type": "string" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed for attachments.", + "title": "File System Access Token", + "type": "string" + }, + "email_thread_id": { + "description": "The ID of the email thread to reply to.", + "title": "Email Thread Id", + "type": "integer" + }, + "email_id": { + "description": "The ID of the email to reply to.", + "title": "Email Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "body", + "email_thread_id", + "email_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__forward_email_from_thread", + "description": "Forward an email from an email thread to one or more recipients.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_addresses": { + "description": "List of recipient email addresses.", + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "draft_not_send": { + "default": false, + "description": "If true, the email will be saved as a draft instead of being sent. This way it can be edited before sending.", + "title": "Draft Not Send", + "type": "boolean" + }, + "email_thread_id": { + "description": "The ID of the email thread to forward.", + "title": "Email Thread Id", + "type": "integer" + }, + "email_id": { + "description": "The ID of the email to forward.", + "title": "Email Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_addresses", + "email_thread_id", + "email_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__forward_email_thread", + "description": "Forward entire email thread to one or more recipients.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_addresses": { + "description": "List of recipient email addresses.", + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "draft_not_send": { + "default": false, + "description": "If true, the email will be saved as a draft instead of being sent. This way it can be edited before sending.", + "title": "Draft Not Send", + "type": "boolean" + }, + "email_thread_id": { + "description": "The ID of the email thread to forward.", + "title": "Email Thread Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email_addresses", + "email_thread_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__create_draft", + "description": "Create a new draft.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "recipient_email_addresses": { + "description": "List of recipient email addresses.", + "items": { + "type": "string" + }, + "title": "Recipient Email Addresses", + "type": "array" + }, + "subject": { + "default": null, + "description": "The subject of the draft. Must be None if it's a reply to an email.", + "title": "Subject", + "type": "string" + }, + "body": { + "description": "The body of the draft.", + "title": "Body", + "type": "string" + }, + "belongs_to_email_thread_id": { + "default": null, + "description": "The ID of the email thread that the draft should belong to. Must be passed if it's a reply to or forward of an email.", + "title": "Belongs To Email Thread Id", + "type": "integer" + }, + "response_to_email_id": { + "default": null, + "description": "The ID of the email in the thread that the draft should responds to.", + "title": "Response To Email Id", + "type": "integer" + }, + "attachment_file_paths": { + "default": null, + "description": "List of absolute file paths (starting with /) from the file_system app to attach to the draft.", + "items": { + "type": "string" + }, + "title": "Attachment File Paths", + "type": "array" + }, + "scheduled_send_at": { + "default": null, + "description": "If set, the draft will be sent at the specified future time in YYYY-MM-DD|HH:MM:SS format. Otherwise, it will not be sent until you manually send it.", + "title": "Scheduled Send At", + "type": "string" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed for attachments.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "recipient_email_addresses", + "body", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_drafts", + "description": "Search or show a list of your drafts.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "recipient_email": { + "default": null, + "description": "Filter emails by recipient email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Recipient Email", + "type": "string" + }, + "attachment": { + "default": null, + "description": "Filter drafts by attachment status.", + "title": "Attachment", + "type": "boolean" + }, + "scheduled": { + "default": null, + "description": "Filter drafts by whether they are scheduled to be sent in the future.", + "title": "Scheduled", + "type": "boolean" + }, + "belongs_to_email_thread_id": { + "default": null, + "description": "Filter drafts by email thread ID that the draft belongs to.", + "title": "Belongs To Email Thread Id", + "type": "integer" + }, + "response_to_email_id": { + "default": null, + "description": "Filter drafts by email ID that the draft responds to.", + "title": "Response To Email Id", + "type": "integer" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter drafts by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter drafts by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the drafts by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__update_draft", + "description": "Update draft information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email_addresses": { + "default": null, + "description": "List of recipient email addresses.", + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "subject": { + "default": null, + "description": "The updated subject of the draft.", + "title": "Subject", + "type": "string" + }, + "body": { + "default": null, + "description": "The updated body of the draft.", + "title": "Body", + "type": "string" + }, + "belongs_to_email_thread_id": { + "default": null, + "description": "The updated ID of the email thread that the draft should belong to. Must be passed if it's a reply to an email.", + "title": "Belongs To Email Thread Id", + "type": "integer" + }, + "response_to_email_id": { + "default": null, + "description": "The updated ID of the email in the thread that the draft is a response to.", + "title": "Response To Email Id", + "type": "integer" + }, + "scheduled_send_at": { + "default": null, + "description": "The updated time at which the draft should be sent in YYYY-MM-DD|HH:MM:SS format.To remove the scheduled delivery, pass 'None' string, as leaving it empty or null is for when you don't want to update it.", + "title": "Scheduled Send At", + "type": "string" + }, + "draft_id": { + "description": "The ID of the draft to update.", + "title": "Draft Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "draft_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__delete_draft", + "description": "Delete draft information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "draft_id": { + "description": "The ID of the draft to delete.", + "title": "Draft Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "draft_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__show_draft", + "description": "Show detailed draft information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "draft_id": { + "description": "The ID of the draft to show.", + "title": "Draft Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "draft_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__send_email_from_draft", + "description": "Send a new email from a draft right away.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed if draft has attachments.", + "title": "File System Access Token", + "type": "string" + }, + "draft_id": { + "description": "The ID of the draft to send.", + "title": "Draft Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "draft_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__download_attachment", + "description": "Download a file attachment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "attachment_id": { + "description": "The ID of the attachment to download.", + "title": "Attachment Id", + "type": "integer" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the attachment to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "attachment_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__upload_attachments_to_draft", + "description": "Upload attachments to a draft.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "attachment_file_paths": { + "description": "List of absolute file paths (starting with /) from the file_system app to attach to the email.", + "items": { + "type": "string" + }, + "title": "Attachment File Paths", + "type": "array" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the attachment if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "draft_id": { + "description": "The ID of the draft to upload the attachment to.", + "title": "Draft Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "attachment_file_paths", + "file_system_access_token", + "draft_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "gmail__remove_attachment_from_draft", + "description": "Delete an attachment from a draft.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "draft_id": { + "description": "The ID of the draft to delete the attachment from.", + "title": "Draft Id", + "type": "integer" + }, + "attachment_id": { + "description": "The ID of the attachment to delete.", + "title": "Attachment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from gmail app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "draft_id", + "attachment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__search_users", + "description": "Search Splitwise users by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "include_self": { + "default": false, + "description": "Whether to include the current user in the search results.", + "title": "Include Self", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_activity", + "description": "Show a history of your expenses and payments combined.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "show_expenses": { + "default": true, + "description": "Whether to show expenses in the history.", + "title": "Show Expenses", + "type": "boolean" + }, + "show_payments": { + "default": true, + "description": "Whether to show payments in the history.", + "title": "Show Payments", + "type": "boolean" + }, + "sort_by": { + "default": "-created_at", + "description": "The attribute to sort the expenses/payments by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__create_group", + "description": "Create a new group of friends or family to share expenses with.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "description": "The name of the group.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "member_emails": { + "description": "Emails of the users to add to the group besides yourself.", + "items": { + "type": "string" + }, + "title": "Member Emails", + "type": "array" + }, + "description": { + "default": null, + "description": "The description of the group.", + "title": "Description", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "name", + "member_emails", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_groups", + "description": "Get a list of groups you are a member of.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "delete": { + "default": false, + "description": "Filter groups by whether they are deleted or not.", + "title": "Delete", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 9223372036854775807, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_group", + "description": "Show group details based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_group", + "description": "Update group information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "default": null, + "description": "The updated name of the group.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "description": { + "default": null, + "description": "The updated description of the group.", + "title": "Description", + "type": "string" + }, + "group_id": { + "description": "The ID of the group.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_group", + "description": "Delete a group you are a member of. Any member can undelete it later.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group to delete.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__undelete_group", + "description": "Undelete a group you are a member of.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group to undelete.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__add_member_to_group", + "description": "Add a member to a group.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_email": { + "description": "Email of the user to add to the group.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "group_id": { + "description": "The ID of the group.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "user_email", + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__remove_member_from_group", + "description": "Remove a member from a group.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group.", + "title": "Group Id", + "type": "integer" + }, + "user_email": { + "description": "Email of the user to remove from the group.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "User Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "user_email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__exit_group", + "description": "Exit from a group you are a part of.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group to exit.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__regenerate_invitation_code", + "description": "Regenerate the invitation code for a group. Anyone with this code can join the group. The old code will be invalidated.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group.", + "title": "Group Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__accept_group_invitation", + "description": "Accept a group invitation shared with you by one of its members.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "invitation_code": { + "description": "The group invitation shared with you by one of its members.", + "title": "Invitation Code", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "invitation_code", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__record_expense", + "description": "Record a new expense to share with others.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "description": { + "description": "A short note or description of the expense.", + "title": "Description", + "type": "string" + }, + "paid_amount": { + "description": "The total amount of the expense paid.", + "title": "Paid Amount", + "type": "number" + }, + "payer_email": { + "description": "Email of the user who paid for the expense.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Payer Email", + "type": "string" + }, + "debtor_emails": { + "description": "Emails of the users who owe a share of the expense.", + "items": { + "type": "string" + }, + "title": "Debtor Emails", + "type": "array" + }, + "debt_amounts": { + "default": null, + "description": "The amounts owed by each debtor. If not passed, it is assumed that each debtor owes an equal share. If passed, the list must be of the same length as debtor_ids.", + "items": { + "type": "number" + }, + "title": "Debt Amounts", + "type": "array" + }, + "group_id": { + "default": null, + "description": "The ID of the group this expense should belong to. Keep it none if it should not belong to any group.", + "title": "Group Id", + "type": "integer" + }, + "receipt_file_path": { + "default": null, + "description": "Absolute file path (starting with /) from the file_system app to attach as a receipt for this expense.", + "title": "Receipt File Path", + "type": "string" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed for attaching receipt file.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "description", + "paid_amount", + "payer_email", + "debtor_emails", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__attach_expense_receipt_file", + "description": "Attach a receipt file to an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "receipt_file_path": { + "description": "Absolute file path (starting with /) from the file_system app to attach as a receipt for this expense.", + "title": "Receipt File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the existing receipt file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "expense_id": { + "description": "The ID of the expense to attach receipt file to.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "receipt_file_path", + "file_system_access_token", + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_expense_receipt_file", + "description": "Delete the receipt file from an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense to detach receipt file from.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__download_expense_receipt_file", + "description": "Download a receipt file attachment for an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense to download receipt for.", + "title": "Expense Id", + "type": "integer" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_expense", + "description": "Show detailed expense information based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_expense", + "description": "Update expense information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "description": { + "default": null, + "description": "The updated note or description of the expense.", + "title": "Description", + "type": "string" + }, + "paid_amount": { + "default": null, + "description": "The updated total paid amount of the expense.", + "title": "Paid Amount", + "type": "number" + }, + "payer_email": { + "default": null, + "description": "Email of updated user who paid for the expense.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Payer Email", + "type": "string" + }, + "debtor_emails": { + "default": null, + "description": "Emails of updated users who owe a share of the expense.", + "items": { + "type": "string" + }, + "title": "Debtor Emails", + "type": "array" + }, + "debt_amounts": { + "default": null, + "description": "The updated amounts owed by each debtor. If debtor_ids is passed and debt_amounts is not passed, each debtor will owe an equal share. If debtor_ids is passed and debt_amounts is passed, the list must be of the same length as debtor_ids.If debt_amount is passed, debtor_ids must be passed.", + "items": { + "type": "number" + }, + "title": "Debt Amounts", + "type": "array" + }, + "expense_id": { + "description": "The ID of the expense.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_expense", + "description": "Mark the expense you are involved in as deleted. Anyone involved in the expense or a member of its group can undelete it later.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense to delete.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_group_expenses", + "description": "Show or search your expenses from a given group based on various criteria.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group to filter expenses.", + "title": "Group Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "participant_email": { + "default": null, + "description": "Email of the payer or debtors to filter expenses.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Participant Email", + "type": "string" + }, + "min_amount": { + "default": 0.0, + "description": "Filter expenses by minimum amount.", + "minimum": 0.0, + "title": "Min Amount", + "type": "number" + }, + "max_amount": { + "default": 9.223372036854776e+18, + "description": "Filter expenses by maximum amount.", + "minimum": 0.0, + "title": "Max Amount", + "type": "number" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "deleted": { + "default": false, + "description": "Whether to limit the results to expenses marked as deleted or not.", + "title": "Deleted", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the expenses by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and amount. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_no_group_expenses", + "description": "Show or search your expenses that are not part of any group based on various criteria.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "participant_email": { + "default": null, + "description": "Email of the payer or debtors to filter expenses.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Participant Email", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "min_amount": { + "default": 0.0, + "description": "Filter expenses by minimum amount.", + "minimum": 0.0, + "title": "Min Amount", + "type": "number" + }, + "max_amount": { + "default": 9.223372036854776e+18, + "description": "Filter expenses by maximum amount.", + "minimum": 0.0, + "title": "Max Amount", + "type": "number" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "deleted": { + "default": false, + "description": "Whether to limit the results to expenses marked as deleted or not.", + "title": "Deleted", + "type": "boolean" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the expenses by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__undelete_expense", + "description": "Restore a previously deleted expense you are involved in.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense to undelete.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__post_expense_comment", + "description": "Post a a comment on an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "The comment for the expense.", + "maxLength": 1000, + "minLength": 1, + "title": "Comment", + "type": "string" + }, + "expense_id": { + "description": "The ID of the expense.", + "title": "Expense Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_expense_comments", + "description": "Get a list of expense comments.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expense_id": { + "description": "The ID of the expense.", + "title": "Expense Id", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "expense_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_expense_comment", + "description": "Update a comment you posted on an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "The updated comment for the expense.", + "minLength": 1, + "title": "Comment", + "type": "string" + }, + "comment_id": { + "description": "The ID of the expense comment to update.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_expense_comment", + "description": "Delete a comment you posted on an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "The ID of the expense comment to delete.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_expense_comment", + "description": "Get information about a comment posted on an expense.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "The ID of the expense comment.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__record_payment", + "description": "Record a new payment for an expense. This only records payment on splitwise and does not move real money.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "default": null, + "description": "The ID of the group to record the payment in.", + "title": "Group Id", + "type": "integer" + }, + "payer_email": { + "description": "Email of the user who made the payment.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Payer Email", + "type": "string" + }, + "receiver_email": { + "description": "Email of the user who received the payment.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Receiver Email", + "type": "string" + }, + "amount": { + "description": "The amount of the payment.", + "title": "Amount", + "type": "number" + }, + "description": { + "default": null, + "description": "A short note or description of the payment.", + "title": "Description", + "type": "string" + }, + "receipt_file_path": { + "default": null, + "description": "Receipt file path to attach as an evidence of this payment made (e.g., snapshot of bank transfer, venmo, etc.)", + "title": "Receipt File Path", + "type": "string" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed for attaching receipt file.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payer_email", + "receiver_email", + "amount", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__attach_payment_receipt_file", + "description": "Attach a receipt file to a payment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "receipt_file_path": { + "description": "The file path of the receipt file to attach to this payment.", + "title": "Receipt File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the existing receipt file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "payment_id": { + "description": "The ID of the payment to attach receipt to.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "receipt_file_path", + "file_system_access_token", + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_payment_receipt_file", + "description": "Delete the receipt file from a payment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment to delete receipt for.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__download_payment_receipt_file", + "description": "Download receipt file attachment for a payment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment to download receipt for.", + "title": "Payment Id", + "type": "integer" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the receipt file to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_payment", + "description": "Show payment details based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_payment", + "description": "Update payment information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "amount": { + "default": null, + "description": "The updated amount of the payment.", + "title": "Amount", + "type": "number" + }, + "description": { + "default": null, + "description": "The updated note or description of the payment.", + "title": "Description", + "type": "string" + }, + "payment_id": { + "description": "The ID of the payment.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_payment", + "description": "Mark the payment you are involved in as deleted. Anyone involved in the payment or a member of its group can undelete it later.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment to delete.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_group_payments", + "description": "Search or show the payments that are part of the given group.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "description": "The ID of the group to filter payments.", + "title": "Group Id", + "type": "integer" + }, + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "participant_email": { + "default": null, + "description": "Email of the payer or receiver to filter payments.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Participant Email", + "type": "string" + }, + "min_amount": { + "default": 0.0, + "description": "Filter expenses by minimum amount.", + "minimum": 0.0, + "title": "Min Amount", + "type": "number" + }, + "max_amount": { + "default": 9.223372036854776e+18, + "description": "Filter expenses by maximum amount.", + "minimum": 0.0, + "title": "Max Amount", + "type": "number" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "deleted": { + "default": false, + "description": "Filter expenses by deleted status.", + "title": "Deleted", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the payments by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and amount. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "group_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_no_group_payments", + "description": "Search or show payments you are involved in that are not part of any group.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "participant_email": { + "default": null, + "description": "Email of the payer or receiver to filter payments.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Participant Email", + "type": "string" + }, + "min_amount": { + "default": 0.0, + "description": "Filter expenses by minimum amount.", + "minimum": 0.0, + "title": "Min Amount", + "type": "number" + }, + "max_amount": { + "default": 9.223372036854776e+18, + "description": "Filter expenses by maximum amount.", + "minimum": 0.0, + "title": "Max Amount", + "type": "number" + }, + "min_created_at": { + "default": "1500-01-01", + "description": "Filter emails by minimum created_at date in YYYY-MM-DD format.", + "title": "Min Created At", + "type": "string" + }, + "max_created_at": { + "default": "3000-01-01", + "description": "Filter emails by maximum created_at date in YYYY-MM-DD format.", + "title": "Max Created At", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "deleted": { + "default": false, + "description": "Filter payments by deleted status.", + "title": "Deleted", + "type": "boolean" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the payments by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and amount. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__undelete_payment", + "description": "Restore a previously deleted payment you are involved in.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment to delete.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_person_balance", + "description": "Show the amounts you and a given person owe to each other, broken down by group. The non-grouped expenses/payments will be shown as a group with ID None.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "The email of the person to show your balance with.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_people_balance", + "description": "Show the aggregate amounts you owe to each person and they owe to you.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_group_balance", + "description": "For the given group, show the detailed breakdown of amounts different members owe to each other. If group_id is not passed, show the amounts you owe to others and vice-versa for non-grouped expenses/payments.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "default": null, + "description": "The ID of the group to show balance of. If not passed, it will show balance from non-grouped expenses/payments.", + "title": "Group Id", + "type": "integer" + }, + "email": { + "default": null, + "description": "If passed, only balance of this member of the group will be shown. Otherwise, balance of all members will be shown. If group_id is not passed, email can only be your own email or None.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_groups_balance", + "description": "Show the aggregate amounts you owe to others or others owe you for each group you are member of. The non-grouped expenses/payments will be shown as a group with group_id of None.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__settle_up", + "description": "Settle up outstanding balance with a user in a group. This only records payment on splitwise and does not move real money.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "group_id": { + "default": null, + "description": "The ID of the group to settle balance.", + "title": "Group Id", + "type": "integer" + }, + "email": { + "description": "The email of the user to settle balance with.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "description": { + "default": "Settle up balance.", + "description": "The description of the payment.", + "title": "Description", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__post_payment_comment", + "description": "Create a new payment comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "The comment for the payment.", + "title": "Comment", + "type": "string" + }, + "payment_id": { + "description": "The ID of the payment.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_payment_comments", + "description": "Get a list of payment comments.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "payment_id": { + "description": "The ID of the payment.", + "title": "Payment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "payment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__update_payment_comment", + "description": "Update a payment comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment": { + "description": "The updated comment for the payment.", + "minLength": 1, + "title": "Comment", + "type": "string" + }, + "comment_id": { + "description": "The ID of the payment comment to update.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment", + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_payment_comment", + "description": "Delete a comment you posted on a payment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "The ID of the payment comment to delete.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_payment_comment", + "description": "Get information about a comment posted on a payment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "comment_id": { + "description": "The ID of the payment comment.", + "title": "Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_notifications", + "description": "Get a list of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "read": { + "default": null, + "description": "Filter notifications by read status. Will skip filtering if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_notifications", + "description": "Delete all of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__mark_notifications", + "description": "Mark all notifications as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Read status of the notification.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__show_notifications_count", + "description": "Get the count of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "default": null, + "description": "Count notifications by read status. Will count all if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__delete_notification", + "description": "Delete a notification.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "notification_id": { + "description": "The ID of the notification.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "splitwise__mark_notification", + "description": "Mark a notification as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Mark the notification as read or unread.", + "title": "Read", + "type": "boolean" + }, + "notification_id": { + "description": "ID of the notification to mark.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from splitwise app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__search_notes", + "description": "Search your notes. This will not show contents of the notes. Pinned notes will be shown first by default, except when dont_reorder_pinned is true.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query for the notes.", + "title": "Query", + "type": "string" + }, + "tags": { + "default": null, + "description": "Tags to filter the notes by. Notes not having any of these tags will be filtered out.", + "items": { + "type": "string" + }, + "title": "Tags", + "type": "array" + }, + "pinned": { + "default": null, + "description": "Filter notes by pinned status.", + "title": "Pinned", + "type": "boolean" + }, + "dont_reorder_pinned": { + "default": null, + "description": "If true, pinned notes will not be reordered to be shown at the top.", + "title": "Dont Reorder Pinned", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the notes by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at and updated_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -updated_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__create_note", + "description": "Create a new note.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "description": "Title of the note", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "content": { + "description": "Content of the note", + "title": "Content", + "type": "string" + }, + "tags": { + "default": null, + "description": "Tags for the note.", + "items": { + "type": "string" + }, + "title": "Tags", + "type": "array" + }, + "pinned": { + "default": false, + "description": "Pinned status of the note.", + "title": "Pinned", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "title", + "content", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__show_note", + "description": "Show detailed information of a note, including its content.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "note_id": { + "description": "ID of the note to be shown.", + "title": "Note Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "note_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__update_note", + "description": "Update a note's title, content, tags, and/or pinned status.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "default": null, + "description": "Updated title of the note.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "content": { + "default": null, + "description": "Updated content of the note.", + "title": "Content", + "type": "string" + }, + "tags": { + "default": null, + "description": "Updated tags for the note.", + "items": { + "type": "string" + }, + "title": "Tags", + "type": "array" + }, + "pinned": { + "default": null, + "description": "Updated pinned status of the note.", + "title": "Pinned", + "type": "boolean" + }, + "note_id": { + "description": "ID of the note to update.", + "title": "Note Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "note_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__delete_note", + "description": "Delete a note.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "note_id": { + "description": "ID of the note to be deleted.", + "title": "Note Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "note_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "simple_note__add_content_to_note", + "description": "Append or prepend content to a note.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "added_content": { + "description": "Content to append or prepend on the note's existing content. It will add a new line between the existing content and the added content.", + "title": "Added Content", + "type": "string" + }, + "note_id": { + "description": "ID of the note to add content to.", + "title": "Note Id", + "type": "integer" + }, + "append_or_prepend": { + "description": "Whether to append or prepend the content.", + "enum": [ + "append", + "prepend" + ], + "title": "Append Or Prepend", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from simple_note app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "added_content", + "note_id", + "append_or_prepend", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_account", + "description": "Show your account information. Unlike show_profile, this includes private information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__signup", + "description": "Sign up to create account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "description": "Your first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Your last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password": { + "description": "Your password.", + "minLength": 5, + "title": "Password", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_account", + "description": "Delete your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_account_name", + "description": "Update your first or last name in the account profile.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "first_name": { + "default": null, + "description": "Your updated first name.", + "minLength": 1, + "title": "First Name", + "type": "string" + }, + "last_name": { + "default": null, + "description": "Your updated last name.", + "minLength": 1, + "title": "Last Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__login", + "description": "Login to your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "username": { + "description": "Your account email.", + "title": "Username", + "type": "string" + }, + "password": { + "description": "Your account password.", + "title": "Password", + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__logout", + "description": "Logout from your account.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__send_verification_code", + "description": "Send account verification code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__verify_account", + "description": "Verify your account using the verification code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "verification_code": { + "description": "The verification code sent to your email address.", + "title": "Verification Code", + "type": "string" + } + }, + "required": [ + "email", + "verification_code" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__send_password_reset_code", + "description": "Send password reset code to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__reset_password", + "description": "Reset your password using the password reset code sent to your email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Your email address.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "password_reset_code": { + "description": "The password reset code sent to your email address.", + "title": "Password Reset Code", + "type": "string" + }, + "new_password": { + "description": "Your new password.", + "minLength": 5, + "title": "New Password", + "type": "string" + } + }, + "required": [ + "email", + "password_reset_code", + "new_password" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_profile", + "description": "Show public profile information of a user.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "default": null, + "description": "Email of the person you want to see the profile information of.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__search_users", + "description": "Search Todoist users by name or email address.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "Search query string.", + "title": "Query", + "type": "string" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + } + }, + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__create_project", + "description": "Create a new project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "description": "The name of the project.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "color": { + "default": "charcoal", + "description": "Color of the project.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the project.", + "title": "Description", + "type": "string" + }, + "is_favorite": { + "default": false, + "description": "Whether the project is marked as favorite.", + "title": "Is Favorite", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "name", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_projects", + "description": "Show or search a list of your projects.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query for projects.", + "title": "Query", + "type": "string" + }, + "color": { + "default": null, + "description": "The color of the project to filter by.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "is_favorite": { + "default": null, + "description": "Project's favorite status to filter by.", + "title": "Is Favorite", + "type": "boolean" + }, + "is_archived": { + "default": null, + "description": "Project's archival status to filter by.", + "title": "Is Archived", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 9223372036854775807, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "sort_by": { + "default": null, + "description": "The attribute to sort the projects by prefixed with +/- to reflect ascending/descending. Valid attributes: created_at. If both query and sort_by are given and non-empty, results will be first ranked by query relevance, then paginated, and will then be sorted by the given attribute within each page. If both query and sort_by are not given, null, or empty, sort_by will default to -created_at.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_project", + "description": "Show project details based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "project_id": { + "description": "The ID of the project. You can use 0 for your Inbox project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_project", + "description": "Update project information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "default": null, + "description": "The updated name of the project.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "color": { + "default": null, + "description": "The updated color of the project.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "description": { + "default": null, + "description": "The updated description of the project.", + "title": "Description", + "type": "string" + }, + "is_favorite": { + "default": null, + "description": "Whether the project is marked as favorite.", + "title": "Is Favorite", + "type": "boolean" + }, + "is_archived": { + "default": null, + "description": "Whether the project is marked as archived.", + "title": "Is Archived", + "type": "boolean" + }, + "project_id": { + "description": "The ID of the project. You can use 0 for your Inbox project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_project", + "description": "Delete a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "project_id": { + "description": "The ID of the project to delete.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__create_section", + "description": "Create a new section within a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "description": "The name of the section.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "order_index": { + "default": -1, + "description": "Index at which this section should be inserted in the current list of sections. 0 to insert at the top of the list, 1 to insert below the first section, etc. You can also use a negative value to insert from the end: -1 to insert at the end, -2 to insert just above the last section, etc.", + "title": "Order Index", + "type": "integer" + }, + "project_id": { + "description": "The ID of the project. You can use 0 for your Inbox project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "name", + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_sections", + "description": "Get a list of sections within a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "project_id": { + "description": "The ID of the project. You can use 0 for your Inbox project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_section", + "description": "Update section information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "default": null, + "description": "The updated name of the section.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "order_index": { + "default": null, + "description": "The updated order index of this section in the list of project sections.", + "title": "Order Index", + "type": "integer" + }, + "section_id": { + "description": "The ID of the section.", + "title": "Section Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "section_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_section", + "description": "Delete a section within a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "section_id": { + "description": "The ID of the section to delete.", + "title": "Section Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "section_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__remove_collaborator_from_project", + "description": "Remove a collaborator (or self) from a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "project_id": { + "description": "The ID of the project.", + "title": "Project Id", + "type": "integer" + }, + "email": { + "description": "Email of the user to remove as a collaborator.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "email", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__send_project_invite", + "description": "Invite a collaborator to join a project. They will be notified via email.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "email": { + "description": "Email of the user to invite as a collaborator.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Email", + "type": "string" + }, + "project_id": { + "description": "The ID of the project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "email", + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__accept_project_invite", + "description": "Accept an invite to join a project using an invite code generated by a project collaborator.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "invite_code": { + "description": "The invite code sent to you by a project collaborator.", + "title": "Invite Code", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "invite_code", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_project_invite", + "description": "Delete a project invite that you sent or received.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "invite_code": { + "description": "The invite code sent to you by a project collaborator.", + "title": "Invite Code", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "invite_code", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__create_task", + "description": "Create a new task within a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "section_id": { + "default": null, + "description": "The ID of the section within the project.", + "title": "Section Id", + "type": "integer" + }, + "title": { + "description": "The title of the task.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the task.", + "title": "Description", + "type": "string" + }, + "due_date": { + "default": null, + "description": "The due date of the task in YYYY-MM-DD format.", + "title": "Due Date", + "type": "string" + }, + "duration": { + "default": null, + "description": "The duration of the task.", + "minimum": 0.0, + "title": "Duration", + "type": "number" + }, + "duration_unit": { + "default": null, + "description": "The unit of the task duration.", + "enum": [ + "minutes", + "hours", + "days" + ], + "title": "Duration Unit", + "type": "string" + }, + "order_index": { + "default": -1, + "description": "Index at which this task should be inserted in the current list of tasks. 0 to insert at the top of the list, 1 to insert below the first task, etc. You can also use a negative value to insert from the end: -1 to insert at the end, -2 to insert just above the last task, etc.", + "title": "Order Index", + "type": "integer" + }, + "priority": { + "default": "medium", + "description": "The priority of the task.", + "enum": [ + "high", + "medium", + "low" + ], + "title": "Priority", + "type": "string" + }, + "project_id": { + "description": "The ID of the project. If set to 0, the task will be created in your default/inbox project.", + "title": "Project Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "title", + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_tasks", + "description": "Get a list of tasks within a project.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "project_id": { + "description": "The ID of the project. You can use 0 for your Inbox project.", + "title": "Project Id", + "type": "integer" + }, + "section_id": { + "default": null, + "description": "The ID of the section to filter tasks by. Set it to 0 for tasks without a section.", + "title": "Section Id", + "type": "integer" + }, + "assignee_email": { + "default": null, + "description": "Email address of the assignee to filter tasks by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Assignee Email", + "type": "string" + }, + "assigner_email": { + "default": null, + "description": "Email address of the assigner to filter tasks by.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Assigner Email", + "type": "string" + }, + "priority": { + "default": null, + "description": "The priority to filter tasks by.", + "enum": [ + "high", + "medium", + "low" + ], + "title": "Priority", + "type": "string" + }, + "is_completed": { + "default": null, + "description": "Completion status to filter by.", + "title": "Is Completed", + "type": "boolean" + }, + "due_today": { + "default": false, + "description": "If true, only tasks due today will be returned. If false, all tasks will be returned.", + "title": "Due Today", + "type": "boolean" + }, + "label_id": { + "default": null, + "description": "The ID of the label to filter tasks by.", + "title": "Label Id", + "type": "integer" + }, + "overdue": { + "default": false, + "description": "If true, only tasks that are overdue will be returned. If false, all tasks will be returned.", + "title": "Overdue", + "type": "boolean" + }, + "min_due_date": { + "default": "1500-01-01", + "description": "Only tasks with a due date greater than or equal to this date will be returned. Date should be in YYYY-MM-DD format.", + "title": "Min Due Date", + "type": "string" + }, + "max_due_date": { + "default": "3000-01-01", + "description": "Only tasks with a due date less than or equal to this date will be returned. Date should be in YYYY-MM-DD format.", + "title": "Max Due Date", + "type": "string" + }, + "sort_by": { + "default": "+order_index", + "description": "The attribute to sort the tasks by prefixed with +/- to reflect ascending/descending. Valid attributes: order_index, due_date and priority.", + "title": "Sort By", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "project_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__assign_or_unassign_task", + "description": "Assign or unassign a task to a user. If assignee_email is null, the task will be unassigned.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "assignee_email": { + "default": null, + "description": "Email of the user to assign the task to.", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "title": "Assignee Email", + "type": "string" + }, + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_task", + "description": "Show detailed information about the task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_task", + "description": "Update task information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "default": null, + "description": "The updated title of the task.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "description": { + "default": null, + "description": "The updated description of the task.", + "title": "Description", + "type": "string" + }, + "due_date": { + "default": null, + "description": "The updated due date of the task in YYYY-MM-DD format. Use 'None' (string) to remove the due date as leaving it empty or null is to not change the due date.", + "title": "Due Date", + "type": "string" + }, + "duration": { + "default": null, + "description": "The updated duration of the task. Use 0 to remove the duration (and duration_unit) as leaving it empty or null is to not change the duration.", + "minimum": 0.0, + "title": "Duration", + "type": "number" + }, + "duration_unit": { + "default": null, + "description": "The updated unit of the task duration.", + "enum": [ + "minutes", + "hours", + "days" + ], + "title": "Duration Unit", + "type": "string" + }, + "is_completed": { + "default": null, + "description": "Whether the task is completed.", + "title": "Is Completed", + "type": "boolean" + }, + "order_index": { + "default": null, + "description": "The updated order index of this task in the list of section tasks.", + "title": "Order Index", + "type": "integer" + }, + "priority": { + "default": null, + "description": "The updated priority of the task.", + "enum": [ + "high", + "medium", + "low" + ], + "title": "Priority", + "type": "string" + }, + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_task", + "description": "Delete a task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_id": { + "description": "The ID of the task to delete.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__create_sub_task", + "description": "Create a new sub_task within a task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "description": "The title of the sub_task.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the sub_task.", + "title": "Description", + "type": "string" + }, + "due_date": { + "default": null, + "description": "The due date of the sub_task in YYYY-MM-DD format.", + "title": "Due Date", + "type": "string" + }, + "duration": { + "default": null, + "description": "The duration of the sub_task.", + "minimum": 0.0, + "title": "Duration", + "type": "number" + }, + "duration_unit": { + "default": null, + "description": "The unit of the sub_task duration.", + "enum": [ + "minutes", + "hours", + "days" + ], + "title": "Duration Unit", + "type": "string" + }, + "priority": { + "default": "medium", + "description": "The priority of the sub_task.", + "enum": [ + "high", + "medium", + "low" + ], + "title": "Priority", + "type": "string" + }, + "order_index": { + "default": -1, + "description": "Index at which this sub_task should be inserted in the current list of sub_tasks. 0 to insert at the top of the list, 1 to insert below the first sub_task, etc. You can also use a negative value to insert from the end: -1 to insert at the end, -2 to insert just above the last sub_task, etc.", + "title": "Order Index", + "type": "integer" + }, + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "title", + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_sub_tasks", + "description": "Get a list of sub_tasks within a task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_sub_task", + "description": "Update sub_task information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "title": { + "default": null, + "description": "The updated title of the sub_task.", + "minLength": 1, + "title": "Title", + "type": "string" + }, + "description": { + "default": null, + "description": "The updated description of the sub_task.", + "title": "Description", + "type": "string" + }, + "due_date": { + "default": null, + "description": "The updated due date of the sub_task in YYYY-MM-DD format. Use 'None' (string) to remove the due date as leaving it empty or null is to not change the due date.", + "title": "Due Date", + "type": "string" + }, + "duration": { + "default": null, + "description": "The updated duration of the sub_task. Use 0 to remove the duration (and duration_unit) as leaving it empty or null is to not change the duration.", + "title": "Duration", + "type": "number" + }, + "duration_unit": { + "default": null, + "description": "The updated unit of the task duration.", + "enum": [ + "minutes", + "hours", + "days" + ], + "title": "Duration Unit", + "type": "string" + }, + "priority": { + "default": null, + "description": "The updated priority of the sub_task.", + "enum": [ + "high", + "medium", + "low" + ], + "title": "Priority", + "type": "string" + }, + "is_completed": { + "default": null, + "description": "The updated completion status of the sub_task. True if completed, False otherwise.", + "title": "Is Completed", + "type": "boolean" + }, + "order_index": { + "default": null, + "description": "The updated index at which this sub_task should be inserted in the current list of sub_tasks.", + "title": "Order Index", + "type": "integer" + }, + "sub_task_id": { + "description": "The ID of the sub_task.", + "title": "Sub Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "sub_task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_sub_task", + "description": "Delete a sub_task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "sub_task_id": { + "description": "The ID of the sub_task to delete.", + "title": "Sub Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "sub_task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__create_label", + "description": "Create a new label.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "description": "The name of the label.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "color": { + "default": "charcoal", + "description": "The color of the label.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "name", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__search_labels", + "description": "Search your or task labels.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "default": "", + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "task_id": { + "default": null, + "description": "The ID of the task to filter by.", + "title": "Task Id", + "type": "integer" + }, + "color": { + "default": null, + "description": "The color of the label to filter by.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "task_attached": { + "default": true, + "description": "If true, and task_id is passed, labels attached to the task will be returned. If false, and task_id is passed, your labels that are not already attached to the task will be returned. It will be ignored if task_id is not passed.", + "title": "Task Attached", + "type": "boolean" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 9223372036854775807, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_label", + "description": "Show label details based on its ID.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "label_id": { + "description": "The ID of the label.", + "title": "Label Id", + "type": "integer" + } + }, + "required": [ + "label_id" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_label", + "description": "Update label information.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "name": { + "default": null, + "description": "The updated name of the label.", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "color": { + "default": null, + "description": "The updated color of the label.", + "enum": [ + "charcoal", + "red", + "blue", + "green", + "orange", + "yellow" + ], + "title": "Color", + "type": "string" + }, + "label_id": { + "description": "The ID of the label.", + "title": "Label Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "label_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_label", + "description": "Delete a label.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "label_id": { + "description": "The ID of the label to delete.", + "title": "Label Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "label_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__add_label_to_task", + "description": "Add a label to a task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "label_id": { + "description": "The ID of the label to add to the task.", + "title": "Label Id", + "type": "integer" + }, + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "label_id", + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__remove_label_from_task", + "description": "Remove a label from a task.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "label_id": { + "description": "The ID of the label to remove from the task.", + "title": "Label Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "label_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__post_task_comment", + "description": "Post a comment on a task, optionally with a file attachment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "content": { + "description": "The content of the comment.", + "title": "Content", + "type": "string" + }, + "attachment_file_paths": { + "default": null, + "description": "Paths to files to be attached in this comment.", + "items": { + "type": "string" + }, + "title": "Attachment File Paths", + "type": "array" + }, + "file_system_access_token": { + "default": null, + "description": "Access token obtained from file_system app login. Only needed if comment has a file attachment.", + "title": "File System Access Token", + "type": "string" + }, + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "content", + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_task_comments", + "description": "Get a list of task comments.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_id": { + "description": "The ID of the task.", + "title": "Task Id", + "type": "integer" + }, + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_task_comment", + "description": "Show a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_comment_id": { + "description": "The ID of the task comment.", + "title": "Task Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__update_task_comment", + "description": "Update a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "content": { + "default": null, + "description": "The updated content of the comment.", + "title": "Content", + "type": "string" + }, + "task_comment_id": { + "description": "The ID of the task comment.", + "title": "Task Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_task_comment", + "description": "Delete a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_comment_id": { + "description": "The ID of the comment to delete.", + "title": "Task Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__upload_attachment", + "description": "Upload an attachment to a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "upload_from_file_path": { + "description": "The file path in file system app to upload as an attachment.", + "title": "Upload From File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the attachment if one with the same name already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "task_comment_id": { + "description": "The ID of the task comment to attach the file to.", + "title": "Task Comment Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "upload_from_file_path", + "file_system_access_token", + "task_comment_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__download_attachment", + "description": "Download the attachment of a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_comment_id": { + "description": "The ID of the comment to download the attachment from.", + "title": "Task Comment Id", + "type": "integer" + }, + "attachment_file_name": { + "description": "Name of the file attached to the task comment.", + "title": "Attachment File Name", + "type": "string" + }, + "download_to_file_path": { + "default": null, + "description": "The file path to download the file attachment to in file system app. Path can be absolute, starting with '/', or relative to the user's home directory, starting with '~/'. If not passed, it will be saved in your ~/downloads directory.", + "title": "Download To File Path", + "type": "string" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the file if it already exists.", + "title": "Overwrite", + "type": "boolean" + }, + "file_system_access_token": { + "description": "Access token obtained from file_system app login.", + "title": "File System Access Token", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_comment_id", + "attachment_file_name", + "file_system_access_token", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_attachment", + "description": "Delete an attachment of a task comment.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "task_comment_id": { + "description": "The ID of the task comment to delete the attachment from.", + "title": "Task Comment Id", + "type": "integer" + }, + "attachment_file_name": { + "description": "Name of the file attached to the task comment.", + "title": "Attachment File Name", + "type": "string" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "task_comment_id", + "attachment_file_name", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_notifications", + "description": "Get a list of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "page_index": { + "default": 0, + "description": "The index of the page to return.", + "minimum": 0, + "title": "Page Index", + "type": "integer" + }, + "page_limit": { + "default": 5, + "description": "The maximum number of results to return per page.", + "maximum": 20, + "minimum": 1, + "title": "Page Limit", + "type": "integer" + }, + "read": { + "default": null, + "description": "Filter notifications by read status. Will skip filtering if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_notifications", + "description": "Delete all of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__mark_notifications", + "description": "Mark all notifications as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Read status of the notification.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__show_notifications_count", + "description": "Get the count of your notifications.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "default": null, + "description": "Count notifications by read status. Will count all if not passed.", + "title": "Read", + "type": "boolean" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__delete_notification", + "description": "Delete a notification.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "notification_id": { + "description": "The ID of the notification to delete.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + }, + { + "name": "todoist__mark_notification", + "description": "Mark a notification as read or unread.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "read": { + "description": "Mark the notification as read or unread.", + "title": "Read", + "type": "boolean" + }, + "notification_id": { + "description": "ID of the notification to mark.", + "title": "Notification Id", + "type": "integer" + }, + "access_token": { + "description": "Access token obtained from todoist app login.", + "title": "Access Token", + "type": "string" + } + }, + "required": [ + "read", + "notification_id", + "access_token" + ], + "title": "DynamicModel", + "type": "object" + } + } + ], + "task_id": "f3f60f0_3" +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl new file mode 100644 index 00000000..03a20c7f --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl @@ -0,0 +1,63 @@ +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 0, "observation": {"result": null, "invoking_actions": []}, "initial": true, "agent_cost": 0, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 1, "action": {"name": "supervisor__show_account_passwords", "arguments": {}, "id": "fe008c02-888f-478e-9aa3-0d29f5da7dab", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.49676000000000003, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 1, "observation": {"result": [{"account_name": "amazon", "password": "0i}WIpn"}, {"account_name": "file_system", "password": "Pqt[Yzd"}, {"account_name": "gmail", "password": "0L-CG0w"}, {"account_name": "phone", "password": "iGT$ymP"}, {"account_name": "simple_note", "password": "-Bw+D)w"}, {"account_name": "splitwise", "password": "W$eVtvV"}, {"account_name": "spotify", "password": "Vum}&9h"}, {"account_name": "todoist", "password": "1UAMVeE"}, {"account_name": "venmo", "password": "D7}ed$N"}], "invoking_actions": [{"name": "supervisor__show_account_passwords", "arguments": {}, "id": "fe008c02-888f-478e-9aa3-0d29f5da7dab", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.49676000000000003, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 2, "action": {"name": "supervisor__show_profile", "arguments": {}, "id": "43095531-617a-45fd-9f94-50d9e03b9129", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.994195, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 2, "observation": {"result": {"first_name": "Caitlin", "last_name": "Burch", "email": "caiburc@gmail.com", "phone_number": "5896230193", "birthday": "1962-09-05", "sex": "female"}, "invoking_actions": [{"name": "supervisor__show_profile", "arguments": {}, "id": "43095531-617a-45fd-9f94-50d9e03b9129", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.994195, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 3, "action": {"name": "spotify__login", "arguments": {"username": "caiburc@gmail.com", "password": "Vum}&9h"}, "id": "389bf808-3a0c-4299-aaa1-960dd77a134d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.493355, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 3, "observation": {"result": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0", "token_type": "Bearer"}, "invoking_actions": [{"name": "spotify__login", "arguments": {}, "id": "389bf808-3a0c-4299-aaa1-960dd77a134d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.493355, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 4, "action": {"name": "spotify__show_song_library", "arguments": {"query": "", "page_index": 0, "page_limit": 100, "sort_by": null, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "7af5dfb8-3919-43e9-b7df-9b1f0d893424", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}, {"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 4, "observation": {"result": "Error: Validation error. Reason: \npage_limit: Input should be less than or equal to 20", "invoking_actions": [{"name": "spotify__show_song_library", "arguments": {}, "id": "7af5dfb8-3919-43e9-b7df-9b1f0d893424", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}, {"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}]}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 5, "action": {"name": "spotify__show_album_library", "arguments": {"query": "", "page_index": 0, "page_limit": 100, "sort_by": null, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "5697268d-2243-4289-8f4a-dfbcf7c47a32", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}, {"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 5, "observation": {"result": "Error: Validation error. Reason: \npage_limit: Input should be less than or equal to 20", "invoking_actions": [{"name": "spotify__show_album_library", "arguments": {}, "id": "5697268d-2243-4289-8f4a-dfbcf7c47a32", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}, {"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}]}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 6, "action": {"name": "spotify__show_liked_songs", "arguments": {"page_index": 0, "page_limit": 100, "sort_by": "-liked_at", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "a11ca50b-b723-499c-80f2-e5d5f75d3441", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}]}}}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 6, "observation": {"result": "Error: Validation error. Reason: \npage_limit: Input should be less than or equal to 20", "invoking_actions": [{"name": "spotify__show_liked_songs", "arguments": {}, "id": "a11ca50b-b723-499c-80f2-e5d5f75d3441", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}]}}}]}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 7, "action": {"name": "spotify__show_liked_albums", "arguments": {"page_index": 0, "page_limit": 100, "sort_by": "-liked_at", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "2b16275c-0e9a-44d9-9cb5-f7996c3df003", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}]}}}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 7, "observation": {"result": "Error: Validation error. Reason: \npage_limit: Input should be less than or equal to 20", "invoking_actions": [{"name": "spotify__show_liked_albums", "arguments": {}, "id": "2b16275c-0e9a-44d9-9cb5-f7996c3df003", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.page_limit': Input should be less than or equal to 20 (received 100 of type int).", "details": {"errors": [{"type": "less_than_equal", "loc": ["arguments", "page_limit"], "msg": "Input should be less than or equal to 20", "input": 100, "ctx": {"le": 20}, "url": "https://errors.pydantic.dev/2.12/v/less_than_equal"}]}}}]}, "initial": false, "agent_cost": 2.01317, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 8, "action": {"name": "spotify__show_song_library", "arguments": {"query": "", "page_index": 0, "page_limit": 20, "sort_by": null, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "7cc41938-6441-42d7-a5d3-87d917193d62", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 8, "observation": {"result": [{"song_id": 284, "title": "Under the Weight of Regret", "album_id": null, "album_title": null, "duration": 203, "artists": [{"id": 30, "name": "Marcus Lane"}], "added_at": "2023-05-13T04:58:42"}, {"song_id": 217, "title": "Torn Between Two Worlds", "album_id": null, "album_title": null, "duration": 224, "artists": [{"id": 21, "name": "Oceanic Odyssey"}], "added_at": "2023-03-31T06:58:52"}, {"song_id": 4, "title": "The Puzzle of My Heart", "album_id": 1, "album_title": "Echo Chamber Chronicles", "duration": 181, "artists": [{"id": 3, "name": "Jasper Skye"}, {"id": 26, "name": "Isabella Cruz"}, {"id": 6, "name": "Seraphina Dawn"}], "added_at": "2023-03-24T02:35:55"}, {"song_id": 272, "title": "Torn Apart", "album_id": null, "album_title": null, "duration": 260, "artists": [{"id": 28, "name": "Evelyn Rose"}], "added_at": "2023-03-21T13:29:32"}, {"song_id": 101, "title": "Morning Haze", "album_id": null, "album_title": null, "duration": 181, "artists": [{"id": 4, "name": "Marigold Muse"}], "added_at": "2023-03-21T08:57:03"}, {"song_id": 17, "title": "Dancing Through the Veil of Dreams", "album_id": 4, "album_title": "Neon Echoes", "duration": 238, "artists": [{"id": 36, "name": "Noah Bennett"}], "added_at": "2023-03-12T05:06:59"}, {"song_id": 318, "title": "Mystic Voyage to Avalon", "album_id": null, "album_title": null, "duration": 203, "artists": [{"id": 35, "name": "Nova Harmony"}], "added_at": "2022-11-23T22:09:30"}, {"song_id": 94, "title": "Bridges Burned and Rivers Crossed", "album_id": null, "album_title": null, "duration": 250, "artists": [{"id": 3, "name": "Jasper Skye"}], "added_at": "2022-11-08T12:05:23"}, {"song_id": 146, "title": "Drifting Through Eternal Skies", "album_id": null, "album_title": null, "duration": 294, "artists": [{"id": 11, "name": "Eliana Harper"}], "added_at": "2022-10-13T18:24:49"}, {"song_id": 194, "title": "Crumbling Castles in the Sand", "album_id": null, "album_title": null, "duration": 259, "artists": [{"id": 18, "name": "Astrid Nightshade"}], "added_at": "2022-10-04T14:09:22"}, {"song_id": 128, "title": "Silent Sorrow", "album_id": null, "album_title": null, "duration": 297, "artists": [{"id": 8, "name": "Aria Sterling"}], "added_at": "2022-08-26T16:37:30"}, {"song_id": 62, "title": "Crimson Sunset Sonata", "album_id": 12, "album_title": "Astral Journey", "duration": 255, "artists": [{"id": 9, "name": "Zoey James"}], "added_at": "2022-08-18T05:42:01"}, {"song_id": 143, "title": "Chasing Ghosts", "album_id": null, "album_title": null, "duration": 199, "artists": [{"id": 10, "name": "Zoe Mitchell"}], "added_at": "2022-08-06T13:14:32"}, {"song_id": 34, "title": "Under the Gaze of a Watchful Moon", "album_id": 7, "album_title": "Vibrant Visions", "duration": 196, "artists": [{"id": 11, "name": "Eliana Harper"}], "added_at": "2022-05-24T09:05:46"}], "invoking_actions": [{"name": "spotify__show_song_library", "arguments": {}, "id": "7cc41938-6441-42d7-a5d3-87d917193d62", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}]}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 9, "action": {"name": "spotify__show_album_library", "arguments": {"query": "", "page_index": 0, "page_limit": 20, "sort_by": null, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "9b45173b-7f10-4328-af49-dadf3551340c", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 9, "observation": {"result": [{"album_id": 8, "title": "Velvet Underground", "genre": "jazz", "artists": [{"id": 8, "name": "Aria Sterling"}], "rating": 3.6, "like_count": 38, "review_count": 17, "release_date": "2021-06-10T10:41:47", "song_ids": [37, 38, 39, 40, 41, 42, 43], "added_at": "2023-04-29T10:24:51"}, {"album_id": 3, "title": "Nocturnal Melodies", "genre": "R&B", "artists": [{"id": 16, "name": "Apollo Serenade"}, {"id": 2, "name": "Phoenix Rivers"}], "rating": 3.5, "like_count": 34, "review_count": 24, "release_date": "2022-11-08T06:55:52", "song_ids": [11, 12, 13, 14, 15], "added_at": "2023-04-23T10:54:43"}, {"album_id": 5, "title": "Velvet Shadows", "genre": "R&B", "artists": [{"id": 32, "name": "Lucas Grey"}, {"id": 25, "name": "Velvet Echo"}], "rating": 3.5, "like_count": 38, "review_count": 22, "release_date": "2022-07-15T17:46:36", "song_ids": [23, 24, 25, 26, 27, 28], "added_at": "2023-04-16T04:55:09"}, {"album_id": 4, "title": "Neon Echoes", "genre": "indie", "artists": [{"id": 36, "name": "Noah Bennett"}], "rating": 3.4, "like_count": 33, "review_count": 20, "release_date": "2021-03-23T13:53:26", "song_ids": [16, 17, 18, 19, 20, 21, 22], "added_at": "2023-03-07T07:57:08"}, {"album_id": 11, "title": "Synaptic Serenity", "genre": "EDM", "artists": [{"id": 5, "name": "Ava Morgan"}], "rating": 3.9, "like_count": 46, "review_count": 18, "release_date": "2022-12-11T06:22:44", "song_ids": [54, 55, 56, 57], "added_at": "2023-02-05T14:12:20"}, {"album_id": 18, "title": "Echoes of Eternity", "genre": "classical", "artists": [{"id": 33, "name": "Felix Blackwood"}], "rating": 3.7, "like_count": 37, "review_count": 21, "release_date": "2022-07-06T10:24:33", "song_ids": [80, 81, 82], "added_at": "2022-12-28T05:26:11"}, {"album_id": 9, "title": "Mystical Crescendo", "genre": "jazz", "artists": [{"id": 8, "name": "Aria Sterling"}], "rating": 3.4, "like_count": 36, "review_count": 12, "release_date": "2022-09-27T00:38:12", "song_ids": [44, 45, 46], "added_at": "2022-09-29T15:27:31"}, {"album_id": 14, "title": "Midnight Serenade", "genre": "rock", "artists": [{"id": 34, "name": "Lily Moon"}, {"id": 9, "name": "Zoey James"}], "rating": 3.4, "like_count": 44, "review_count": 14, "release_date": "2022-02-21T17:34:33", "song_ids": [67, 68, 69], "added_at": "2022-07-17T00:52:57"}], "invoking_actions": [{"name": "spotify__show_album_library", "arguments": {}, "id": "9b45173b-7f10-4328-af49-dadf3551340c", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.sort_by': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "string_type", "loc": ["arguments", "sort_by"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}]}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 10, "action": {"name": "spotify__show_liked_songs", "arguments": {"page_index": 0, "page_limit": 20, "sort_by": "-liked_at", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "13147fce-e18d-4194-8475-03ed0b008d0d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 10, "observation": {"result": [{"song_id": 279, "title": "The Silence After the Storm", "album_id": null, "album_title": null, "duration": 193, "artists": [{"id": 29, "name": "Lucas Diaz"}], "liked_at": "2023-05-05T07:06:05"}, {"song_id": 24, "title": "The Unseen Scars of Love", "album_id": 5, "album_title": "Velvet Shadows", "duration": 281, "artists": [{"id": 32, "name": "Lucas Grey"}, {"id": 25, "name": "Velvet Echo"}], "liked_at": "2023-04-11T10:42:07"}, {"song_id": 86, "title": "Shattered", "album_id": null, "album_title": null, "duration": 250, "artists": [{"id": 1, "name": "Olivia Roberts"}], "liked_at": "2023-03-06T05:53:51"}, {"song_id": 322, "title": "Lost in the Labyrinth of Love", "album_id": null, "album_title": null, "duration": 281, "artists": [{"id": 36, "name": "Noah Bennett"}], "liked_at": "2023-02-21T20:53:03"}, {"song_id": 57, "title": "Silver Lining", "album_id": 11, "album_title": "Synaptic Serenity", "duration": 287, "artists": [{"id": 5, "name": "Ava Morgan"}], "liked_at": "2023-01-10T17:58:43"}, {"song_id": 154, "title": "The Ghosts of Our Past", "album_id": null, "album_title": null, "duration": 204, "artists": [{"id": 12, "name": "Mia Sullivan"}], "liked_at": "2022-12-02T06:50:57"}, {"song_id": 202, "title": "Summer's End", "album_id": null, "album_title": null, "duration": 277, "artists": [{"id": 19, "name": "Ethan Wallace"}], "liked_at": "2022-11-22T03:25:34"}, {"song_id": 285, "title": "Wading Through the Ashes of Love", "album_id": null, "album_title": null, "duration": 195, "artists": [{"id": 30, "name": "Marcus Lane"}], "liked_at": "2022-10-10T19:08:42"}, {"song_id": 203, "title": "Lost in the Echoes of Time", "album_id": null, "album_title": null, "duration": 245, "artists": [{"id": 19, "name": "Ethan Wallace"}], "liked_at": "2022-10-06T16:53:59"}, {"song_id": 277, "title": "Hidden Scars", "album_id": null, "album_title": null, "duration": 297, "artists": [{"id": 29, "name": "Lucas Diaz"}], "liked_at": "2022-09-08T21:53:57"}, {"song_id": 195, "title": "Rhapsody of the Ocean's Embrace", "album_id": null, "album_title": null, "duration": 298, "artists": [{"id": 18, "name": "Astrid Nightshade"}], "liked_at": "2022-08-01T23:42:15"}, {"song_id": 106, "title": "The Irony of a Perfect Moment", "album_id": null, "album_title": null, "duration": 185, "artists": [{"id": 5, "name": "Ava Morgan"}], "liked_at": "2022-06-01T05:40:47"}, {"song_id": 17, "title": "Dancing Through the Veil of Dreams", "album_id": 4, "album_title": "Neon Echoes", "duration": 238, "artists": [{"id": 36, "name": "Noah Bennett"}], "liked_at": "2022-05-28T22:29:45"}], "invoking_actions": [{"name": "spotify__show_liked_songs", "arguments": {}, "id": "13147fce-e18d-4194-8475-03ed0b008d0d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 11, "action": {"name": "spotify__show_liked_albums", "arguments": {"page_index": 0, "page_limit": 20, "sort_by": "-liked_at", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "086dc0d6-0bcf-4bba-9674-c078f462287b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 11, "observation": {"result": [{"album_id": 17, "title": "Enchanted Melodies", "genre": "classical", "artists": [{"id": 12, "name": "Mia Sullivan"}, {"id": 27, "name": "Carter Knight"}], "rating": 3.6, "like_count": 40, "review_count": 17, "release_date": "2020-10-15T13:57:09", "song_ids": [77, 78, 79], "liked_at": "2023-02-08T15:57:56"}, {"album_id": 15, "title": "Whispers in the Wind", "genre": "pop", "artists": [{"id": 29, "name": "Lucas Diaz"}, {"id": 7, "name": "Orion Steele"}], "rating": 3.8, "like_count": 34, "review_count": 17, "release_date": "2022-08-17T08:43:21", "song_ids": [70, 71, 72, 73], "liked_at": "2023-01-11T11:28:09"}, {"album_id": 2, "title": "Celestial Harmonies", "genre": "R&B", "artists": [{"id": 32, "name": "Lucas Grey"}], "rating": 3.6, "like_count": 44, "review_count": 17, "release_date": "2022-05-21T21:55:36", "song_ids": [8, 9, 10], "liked_at": "2022-12-29T00:01:55"}, {"album_id": 6, "title": "Ethereal Rhapsody", "genre": "indie", "artists": [{"id": 36, "name": "Noah Bennett"}, {"id": 19, "name": "Ethan Wallace"}, {"id": 35, "name": "Nova Harmony"}], "rating": 3.9, "like_count": 37, "review_count": 18, "release_date": "2021-04-02T04:32:45", "song_ids": [29, 30, 31, 32], "liked_at": "2022-12-24T14:10:12"}, {"album_id": 10, "title": "Dreamscape Delights", "genre": "jazz", "artists": [{"id": 8, "name": "Aria Sterling"}], "rating": 3.0, "like_count": 30, "review_count": 10, "release_date": "2022-11-11T09:54:16", "song_ids": [47, 48, 49, 50, 51, 52, 53], "liked_at": "2022-12-12T15:31:55"}, {"album_id": 7, "title": "Vibrant Visions", "genre": "hip-hop", "artists": [{"id": 11, "name": "Eliana Harper"}], "rating": 3.4, "like_count": 44, "review_count": 20, "release_date": "2021-02-05T04:10:20", "song_ids": [33, 34, 35, 36], "liked_at": "2022-11-22T14:11:18"}, {"album_id": 1, "title": "Echo Chamber Chronicles", "genre": "EDM", "artists": [{"id": 3, "name": "Jasper Skye"}, {"id": 26, "name": "Isabella Cruz"}, {"id": 6, "name": "Seraphina Dawn"}], "rating": 3.2, "like_count": 34, "review_count": 12, "release_date": "2022-10-29T01:54:53", "song_ids": [1, 2, 3, 4, 5, 6, 7], "liked_at": "2022-10-28T09:08:14"}, {"album_id": 16, "title": "Electric Dreamscape", "genre": "rock", "artists": [{"id": 34, "name": "Lily Moon"}, {"id": 9, "name": "Zoey James"}], "rating": 3.8, "like_count": 32, "review_count": 16, "release_date": "2022-08-02T08:03:50", "song_ids": [74, 75, 76], "liked_at": "2022-10-12T04:22:15"}, {"album_id": 13, "title": "Starlight Serenades", "genre": "EDM", "artists": [{"id": 31, "name": "Hazel Winter"}, {"id": 5, "name": "Ava Morgan"}], "rating": 3.3, "like_count": 39, "review_count": 17, "release_date": "2021-03-02T14:08:32", "song_ids": [63, 64, 65, 66], "liked_at": "2022-09-12T18:51:25"}, {"album_id": 4, "title": "Neon Echoes", "genre": "indie", "artists": [{"id": 36, "name": "Noah Bennett"}], "rating": 3.4, "like_count": 33, "review_count": 20, "release_date": "2021-03-23T13:53:26", "song_ids": [16, 17, 18, 19, 20, 21, 22], "liked_at": "2022-08-25T20:36:43"}, {"album_id": 11, "title": "Synaptic Serenity", "genre": "EDM", "artists": [{"id": 5, "name": "Ava Morgan"}], "rating": 3.9, "like_count": 46, "review_count": 18, "release_date": "2022-12-11T06:22:44", "song_ids": [54, 55, 56, 57], "liked_at": "2022-06-24T14:53:19"}, {"album_id": 12, "title": "Astral Journey", "genre": "rock", "artists": [{"id": 9, "name": "Zoey James"}], "rating": 3.7, "like_count": 43, "review_count": 16, "release_date": "2021-10-02T22:00:08", "song_ids": [58, 59, 60, 61, 62], "liked_at": "2022-05-26T18:37:41"}], "invoking_actions": [{"name": "spotify__show_liked_albums", "arguments": {}, "id": "086dc0d6-0bcf-4bba-9674-c078f462287b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.5384650000000004, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 12, "action": {"name": "spotify__like_song", "arguments": {"song_id": 284, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "6378d779-84dd-4661-8c24-415d938a6e9e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 12, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "6378d779-84dd-4661-8c24-415d938a6e9e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 13, "action": {"name": "spotify__like_song", "arguments": {"song_id": 217, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "cef80a8b-4f00-4da2-8387-c82fb4e8001e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 13, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "cef80a8b-4f00-4da2-8387-c82fb4e8001e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 14, "action": {"name": "spotify__like_song", "arguments": {"song_id": 4, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "f7882044-96dc-4bb1-9db3-9c9f2b2e2d87", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 14, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "f7882044-96dc-4bb1-9db3-9c9f2b2e2d87", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 15, "action": {"name": "spotify__like_song", "arguments": {"song_id": 272, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "9f77799f-da2b-4d4c-9a41-ac5ad7aa6d3e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 15, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "9f77799f-da2b-4d4c-9a41-ac5ad7aa6d3e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 16, "action": {"name": "spotify__like_song", "arguments": {"song_id": 101, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "f0e109be-84bf-4442-82b2-db8d33980f5e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 16, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "f0e109be-84bf-4442-82b2-db8d33980f5e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 17, "action": {"name": "spotify__like_song", "arguments": {"song_id": 318, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "7b722a74-b1a1-4c41-a6c0-b6013481ba30", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 17, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "7b722a74-b1a1-4c41-a6c0-b6013481ba30", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 18, "action": {"name": "spotify__like_song", "arguments": {"song_id": 94, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "ca64d869-ec0f-4e8f-9562-fe0a90c268b2", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 18, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "ca64d869-ec0f-4e8f-9562-fe0a90c268b2", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 19, "action": {"name": "spotify__like_song", "arguments": {"song_id": 146, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "351bf9fe-9e27-4f91-9c83-8da1b8a13570", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 19, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "351bf9fe-9e27-4f91-9c83-8da1b8a13570", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 20, "action": {"name": "spotify__like_song", "arguments": {"song_id": 194, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "155999da-6a27-4292-bbe7-8c6dd1b1ea48", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 20, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "155999da-6a27-4292-bbe7-8c6dd1b1ea48", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 21, "action": {"name": "spotify__like_song", "arguments": {"song_id": 128, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "30a785ef-7ef3-4f67-8bdd-2d90d424176f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 21, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "30a785ef-7ef3-4f67-8bdd-2d90d424176f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 22, "action": {"name": "spotify__like_song", "arguments": {"song_id": 62, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "d65efdec-de17-48bf-b510-f0fb09fa5473", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 22, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "d65efdec-de17-48bf-b510-f0fb09fa5473", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 23, "action": {"name": "spotify__like_song", "arguments": {"song_id": 143, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "041084be-b4d9-4e1e-b0e8-1cf8bd1d8f5d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 23, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "041084be-b4d9-4e1e-b0e8-1cf8bd1d8f5d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 24, "action": {"name": "spotify__like_song", "arguments": {"song_id": 34, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "f758a3f0-a9b8-47b7-a393-956023c867ca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 24, "observation": {"result": {"message": "Song liked."}, "invoking_actions": [{"name": "spotify__like_song", "arguments": {}, "id": "f758a3f0-a9b8-47b7-a393-956023c867ca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 25, "action": {"name": "spotify__like_album", "arguments": {"album_id": 8, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "b7fc2451-c992-414c-93b1-a7baaf05b88c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 25, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "b7fc2451-c992-414c-93b1-a7baaf05b88c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 26, "action": {"name": "spotify__like_album", "arguments": {"album_id": 3, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "f53d1648-77d8-471c-844a-19bcce0dc524", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 26, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "f53d1648-77d8-471c-844a-19bcce0dc524", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 27, "action": {"name": "spotify__like_album", "arguments": {"album_id": 5, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "5797fd42-eff8-494e-b7ce-9074d09c68bb", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 27, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "5797fd42-eff8-494e-b7ce-9074d09c68bb", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 28, "action": {"name": "spotify__like_album", "arguments": {"album_id": 18, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "855a8cde-f525-40d3-bd11-1c5c73480999", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 28, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "855a8cde-f525-40d3-bd11-1c5c73480999", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 29, "action": {"name": "spotify__like_album", "arguments": {"album_id": 9, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "cc589b5e-f138-4065-9688-fbf29f1f0058", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 29, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "cc589b5e-f138-4065-9688-fbf29f1f0058", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 30, "action": {"name": "spotify__like_album", "arguments": {"album_id": 14, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcG90aWZ5K2NhaWJ1cmNAZ21haWwuY29tIiwiZXhwIjoxNjg0NDEyMDk4fQ.7pIZgwN8dQjGbediu4d8La107fF_vrxRODqJOls1Ek0"}, "id": "68e74b9b-0922-4e43-afe8-a0333ce293d0", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 30, "observation": {"result": {"message": "Album liked."}, "invoking_actions": [{"name": "spotify__like_album", "arguments": {}, "id": "68e74b9b-0922-4e43-afe8-a0333ce293d0", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.17682, "benchmark_cost": 0} +{"event": "action", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 31, "action": {"name": "finish", "arguments": {"answer": null, "status": "success"}, "id": "589d540c-af44-4931-a7dc-33a4c5823b96", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.answer.float': Input should be a valid number (received None of type NoneType).Field 'arguments.answer.int': Input should be a valid integer (received None of type NoneType).Field 'arguments.answer.str': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "float_type", "loc": ["arguments", "answer", "float"], "msg": "Input should be a valid number", "input": null, "url": "https://errors.pydantic.dev/2.12/v/float_type"}, {"type": "int_type", "loc": ["arguments", "answer", "int"], "msg": "Input should be a valid integer", "input": null, "url": "https://errors.pydantic.dev/2.12/v/int_type"}, {"type": "string_type", "loc": ["arguments", "answer", "str"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}, "initial": false, "agent_cost": 3.7826250000000003, "benchmark_cost": 0} +{"event": "observation", "run_id": "0c890a5dde8c", "session_id": "9e993ba4", "session_number": 39, "task_id": "f3f60f0_3", "step": 31, "observation": {"result": {"message": "Marked the active task complete."}, "invoking_actions": [{"name": "finish", "arguments": {}, "id": "589d540c-af44-4931-a7dc-33a4c5823b96", "validation": {"valid": false, "name_valid": true, "args_valid": false, "error": "Field 'arguments.answer.float': Input should be a valid number (received None of type NoneType).Field 'arguments.answer.int': Input should be a valid integer (received None of type NoneType).Field 'arguments.answer.str': Input should be a valid string (received None of type NoneType).", "details": {"errors": [{"type": "float_type", "loc": ["arguments", "answer", "float"], "msg": "Input should be a valid number", "input": null, "url": "https://errors.pydantic.dev/2.12/v/float_type"}, {"type": "int_type", "loc": ["arguments", "answer", "int"], "msg": "Input should be a valid integer", "input": null, "url": "https://errors.pydantic.dev/2.12/v/int_type"}, {"type": "string_type", "loc": ["arguments", "answer", "str"], "msg": "Input should be a valid string", "input": null, "url": "https://errors.pydantic.dev/2.12/v/string_type"}]}}}]}, "initial": false, "agent_cost": 3.7826250000000003, "benchmark_cost": 0} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/recording.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/recording.json new file mode 100644 index 00000000..635f6557 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/recording.json @@ -0,0 +1,5 @@ +{ + "benchmark": "browsecompplus", + "task_id": "62", + "expected_score": 1.0 +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/results.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/results.json new file mode 100644 index 00000000..e9a91dce --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/results.json @@ -0,0 +1,206 @@ +{ + "session_id": "2cfd9083", + "success": true, + "score": 1.0, + "is_finished": true, + "steps": 27, + "agent_cost": 6.4443850000000005, + "benchmark_cost": 0.0019039999999999999, + "execution_time": 2.1636428833007812, + "details": { + "score": 1.0, + "success": true, + "is_finished": true, + "session_metrics": { + "Accuracy": 1, + "Retrieval_recall": 1.0, + "Citation_metrics_positives": { + "num_citations": 4, + "num_relevant": 4, + "precision": 1.0, + "recall": 1.0 + }, + "Confidence": [ + 85.0 + ] + }, + "session_metadata": { + "instance": { + "task_id": 62, + "query_id": 1262, + "query": "Set in 19th-century London, the story follows a protagonist who, while in disguise, becomes involved in helping a businessman avert a disastrous deal. However, the situation takes a dire turn when the businessman accuses the protagonist of stealing a vital document. As the events unravel, the two uncover the real culprit\u2014a schemer plotting to send the encrypted document abroad. In a dramatic climax during a fierce sea storm, the businessman risks their life to rescue the protagonist, securing their safety. Name this historical fiction novel published before 2017.", + "gold_answer": "Storm and Silence", + "gold_docs": [ + "71659", + "84371", + "90027", + "25654" + ], + "evidence_docs": [ + "71659", + "84371", + "90027", + "25654" + ], + "negative_docs": [ + "98862", + "51847", + "51328", + "78562", + "45315", + "49802", + "22514", + "96201", + "77939", + "19305", + "81929", + "53914", + "17573", + "3290", + "21054", + "25997", + "97644", + "37321", + "99618", + "21656", + "43110", + "67862", + "72751", + "43897", + "577", + "22504", + "77980", + "77101", + "24890", + "52189", + "48934", + "50537", + "48165", + "7436", + "29367", + "48523", + "42402", + "4486", + "72600", + "42367", + "59534", + "62735", + "34365", + "44910", + "96757", + "48063", + "802", + "28378", + "4235", + "74026", + "29859", + "24479", + "82097", + "29668", + "2526", + "7498", + "29526", + "61641", + "3054", + "81725", + "34147", + "34764", + "99787", + "85973", + "44817", + "50973", + "69577", + "12540", + "51857", + "68049", + "42735", + "36626", + "73898", + "95393", + "67018", + "91840", + "39072", + "87057", + "13060", + "17199", + "6198", + "63278", + "82109", + "32850", + "59389", + "93904", + "22055", + "11299", + "64166", + "69228" + ] + }, + "retrieved_docids": [ + "20048", + "93624", + "97644", + "78562", + "71386", + "1891", + "65464", + "41618", + "45315", + "29367", + "79197", + "81929", + "1351", + "95934", + "99787", + "72764", + "74026", + "18321", + "66065", + "72751", + "82350", + "67868", + "38361", + "19305", + "33677", + "32872", + "5574", + "90027", + "71659", + "77939", + "53914", + "13697", + "51328", + "29859", + "36824", + "60123", + "96202", + "7830", + "48916", + "84371", + "42402", + "27430", + "25654" + ], + "response": "{\"exact_answer\": \"Storm and Silence\", \"explanation\": \"The historical fiction novel described is \\\"Storm and Silence\\\" by Robert Thier, published in March 2016. The novel is set in 19th-century London and follows Lilly Linton, a young feminist who disguises herself as a man to work for the ruthless financial business magnate Mr. Rikkard Ambrose. [90027, 25654, 71659]\\n\\nAccording to the author interview, \\\"The story follows the adventures of 19-year-old feminist Lilly and her budding romance with the powerful and ruthless business mogul Rikkard Ambrose. A firm believer in the equality of men and women, Lilly is forced to dress up as a man in order to work for Mr. Ambrose and achieve her independence. The two of them butt heads all the time, but when an important document is stolen from Mr. Ambrose's safe, and he has fought a battle of wits and intrigue with his arch-enemy Lord Dalgliesh, she stands by him through thick and thin.\\\" [84371]\\n\\nThe chapter list includes titles like \\\"The Thief,\\\" \\\"File Fight,\\\" \\\"Isle Marbeau,\\\" \\\"Rising Waves,\\\" \\\"Shots in the Dark,\\\" and \\\"Man and Woman\\\" at the end of the book, indicating the plot elements involving a stolen document, an island setting, and dramatic sea climax with the rescue. [90027]\\n\\nThe novel was first uploaded on Wattpad in April 2014 and was officially published as a paperback and ebook in March 2016, meeting the criteria of being published before 2017. [90027, 25654]\", \"confidence\": 85.0}", + "extracted_final_answer": "Storm and Silence", + "judge_model": "openai/Azure/gpt-4.1", + "is_complete": true, + "judge_parse_error": false, + "tool_call_counts": { + "search": 22, + "get_document": 4, + "submit": 1 + } + } + }, + "cost_reports": { + "agent": { + "model_name": "openai/aws/claude-opus-4-5", + "total_cost": 6.4443850000000005 + }, + "benchmark": { + "model_name": "openai/Azure/gpt-4.1", + "total_cost": 0.0019039999999999999 + } + }, + "task_key": "1262" +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/session.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/session.json new file mode 100644 index 00000000..aa18cfbb --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/session.json @@ -0,0 +1,79 @@ +{ + "task": "Answer the provided question by performing search and document expansion as needed, and submit your final answer. \nQuestion: Set in 19th-century London, the story follows a protagonist who, while in disguise, becomes involved in helping a businessman avert a disastrous deal. However, the situation takes a dire turn when the businessman accuses the protagonist of stealing a vital document. As the events unravel, the two uncover the real culprit—a schemer plotting to send the encrypted document abroad. In a dramatic climax during a fierce sea storm, the businessman risks their life to rescue the protagonist, securing their safety. Name this historical fiction novel published before 2017.\nNote:\n- The question has an answer discoverable through proper search.\n- The question requires putting together information from different sources.\n\nYour performance is scored based on:\n 1. Most importantly, the correctness of the answer you assembled from different searches.\n 2. Your effective use of search and your ability to retrieve all relevant information for the question.\n 3. How efficiently you find all the relevant information, using as few searches as possible.\n\nImportant: During your work, Do NOT interact with the user or send any messages at any point — messages will be ignored and are NOT considered a valid final answer. The ONLY acceptable way to finish is by calling 'submit' with the required structured fields.\n\nFinish the session always by calling `submit`. If you fail to find the answer, submit with exact_answer: \"Can't find the answer.\".", + "context": {}, + "actions": [ + { + "name": "search", + "description": "Perform a search on a knowledge source: supply a single 'query' string; the action retrieves the 5 top most relevant results, each trimmed to 512 tokens.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "query": { + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "BrowseCompPlusSearchArgs", + "type": "object" + } + }, + { + "name": "submit", + "description": "Submit final answer and complete", + "is_finish": true, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "exact_answer": { + "description": "Your succinct, final answer", + "title": "Exact Answer", + "type": "string" + }, + "explanation": { + "description": "Your explanation for your final answer. For this explanation section only, you should cite your evidence documents inline by enclosing their docids in square brackets [] at the end of sentences. For example, [20].", + "title": "Explanation", + "type": "string" + }, + "confidence": { + "description": "Your confidence score between 0% and 100% for your answer", + "title": "Confidence", + "type": "number" + } + }, + "required": [ + "exact_answer", + "explanation", + "confidence" + ], + "title": "BrowseCompPlusFinishArgs", + "type": "object" + } + }, + { + "name": "get_document", + "description": "Retrieve the full document using its document id", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "docid": { + "title": "Docid", + "type": "string" + } + }, + "required": [ + "docid" + ], + "title": "BrowseCompPlusGetDocumentsArgs", + "type": "object" + } + } + ] +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/trajectory.jsonl b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/trajectory.jsonl new file mode 100644 index 00000000..67f5da84 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/browsecompplus/trajectory.jsonl @@ -0,0 +1,55 @@ +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 0, "observation": {"result": null, "invoking_actions": []}, "initial": true, "agent_cost": 0, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 1, "action": {"name": "search", "arguments": {"query": "19th century London historical fiction novel disguise businessman stolen document sea storm rescue"}, "id": "35e7bc3a-50a6-4078-bc33-7acc7c0128fd", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.00807, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 1, "observation": {"result": "[\n {\n \"docid\": \"29367\",\n \"score\": 0.5636506080627441,\n \"snippet\": \"---\\ntitle: Historical Fictionistas discussion\\nauthor: Author add link photo\\ndate: 2012-10-11\\n---\\nHistorical Fictionistas discussion\\n\\nRecommendations?\\n\\n>\\n\\nCrime in 19th Century London?\\n\\ndate\\n\\nnewest \\u00bb\\n\\nmessage 1:\\n\\nby\\n\\nAllison\\n\\n(new)\\n\\nOct 11, 2012 07:57AM\\n\\nHey you guys...I would love some recommendations on books set in 19th century London revolving around crime (other than Jack the Ripper). Fiction and non-fiction would both be great!\\n\\nreply\\n\\n|\\n\\nflag\\n\\nAnything by Anne Perry in the Inspector Monk and the Thomas Pitt series. They start with The Face of a Stranger and The Cater Street Hangman, respectively. Monk is 1860s and Pitt somewhat later.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDavid Liss's Benjamin Weaver series is my favorite. It begins with A Conspiracy of Paper, a HF murder/mystery/thriller novel about a financial crisis in 19th century London. You'll love Benjamin and learn a lot about economics (without being bored to tears) because Liss is such a master at inserting historical details in creative ways. David Liss\\n\\nI love historical mysteries, so here's a few of my favorites set in London in the 19th century:\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of aristocratic series:\\n\\nC.S. Harris, Regency era with the first in the series being What Angels Fear\\n\\nCharles Finch,A Beautiful Blue Death is first in the series\\n\\nEarly forensics:\\n\\nDevoured by D.E. Meredith\\n\\nEnjoy!\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of arist\"\n },\n {\n \"docid\": \"19305\",\n \"score\": 0.5559988617897034,\n \"snippet\": \"---\\ntitle: Ten of the best disguises in literature\\nauthor: John Mullan\\ndate: 2010-09-25\\n---\\nThe Odyssey, by Homer\\n\\nOdysseus arrives back at his island of Ithaca disguised as a beggar. He is recognised only by his old dog Argus (animals always see through disguises), which dies of joy on the spot. In his disguise, our hero is able to see who has been loyal to him and who has not.\\n\\nMeasure for Measure, by William Shakespeare\\n\\nThe Duke who governs Vienna wants to see what his underlings will get up to in his absence. So he asks his friend Friar Thomas for some monkish garb: \\\"Supply me with the habit and instruct me / How I may formally in person bear me / Like a true friar\\\". It works, and not even his most devoted courtiers recognise him until he finally unveils himself.\\n\\nThe Monk, by Matthew Lewis\\n\\nAnother monkish disguise. Sexy young Matilda lusts after Father Ambrosio, the most pious monk in Madrid. So she dresses up as a young novice monk and finds her way into the monastery. In her cell she reveals herself to Ambrosio, who cannot resist her charms. It turns out that she is in fact a demon.\\n\\nJane Eyre, by Charlotte Bront\\u00eb\\n\\nOne of the great episodes of transvestism in literature comes when Rochester togs himself up as a Gypsy woman to read the palms of the guests he has invited to Thornfield. Blanche Ingram, Jane's rival for his affections, gets uncomforting news, but Jane is told \\\"the cup of bliss\\\" is going to be offered to her.\\n\\nEast Lynne, by Mrs Henry Wood\\n\\nLady Isabel Vane loses her happy home and family when she conducts an adulterous affair with the utterly caddish Francis Levinson. Having learned the error of her ways, she returns to be governess to her own children, disguised by blue-lensed glasses, hair turned white from shock after a train crash and a scarred mouth.\\n\\nThe Mystery of Edwin Drood, by Charles Dickens\\n\\nDick Datchery arrives in the town of Cloisterham, apparently a detective in disguise (he wears a wig). He (or she?) keeps watch over John Jasper, choirmaster and secret drug addict. Drood has disappeared: is he disguised as Datchery? Or is it another character, investigating Drood's murder? Dickens did\"\n },\n {\n \"docid\": \"1351\",\n \"score\": 0.5189502239227295,\n \"snippet\": \"---\\ntitle: 18th Century Historical Fiction\\nauthor: Charles Dickens\\ndate: 2025-01-01\\n---\\n| 1 |\\n\\n|\\n\\nA Tale of Two Cities\\n\\nby\\n\\n3.88 avg rating \\u2014 992,065 ratings\\n\\n|\\n\\n|\\n\\n| 2 |\\n\\n|\\n\\nLes Liaisons dangereuses\\n\\nby\\n\\n4.05 avg rating \\u2014 56,816 ratings\\n\\n|\\n\\n|\\n\\n| 3 |\\n\\n|\\n\\nOutlander (Outlander, #1)\\n\\nby\\n\\n4.26 avg rating \\u2014 1,126,189 ratings\\n\\n|\\n\\n|\\n\\n| 4 |\\n\\n|\\n\\nTreasure Island\\n\\nby\\n\\n3.85 avg rating \\u2014 524,777 ratings\\n\\n|\\n\\n|\\n\\n| 5 |\\n\\n|\\n\\nThe London Monster\\n\\nby\\n\\n4.10 avg rating \\u2014 185 ratings\\n\\n|\\n\\n|\\n\\n| 6 |\\n\\n|\\n\\nRoss Poldark (Poldark, #1)\\n\\nby\\n\\n4.08 avg rating \\u2014 34,572 ratings\\n\\n|\\n\\n|\\n\\n| 7 |\\n\\n|\\n\\nThe Winter Sea (Slains, #1)\\n\\nby\\n\\n4.11 avg rating \\u2014 68,603 ratings\\n\\n|\\n\\n|\\n\\n| 8 |\\n\\n|\\n\\nThe Scarlet Pimpernel\\n\\nby\\n\\n4.07 avg rating \\u2014 139,637 ratings\\n\\n|\\n\\n|\\n\\n| 9 |\\n\\n|\\n\\nThrough a Glass Darkly (Tamworth Saga, #2)\\n\\nby\\n\\n3.97 avg rating \\u2014 11,323 ratings\\n\\n|\\n\\n|\\n\\n| 10 |\\n\\n|\\n\\nLord John And The Private Matter (Lord John Grey Book 1)\\n\\nby\\n\\n3.76 avg rating \\u2014 49,766 ratings\\n\\n|\\n\\n|\\n\\n| 11 |\\n\\n|\\n\\nAnn & Frederick\\n\\nby\\n\\n3.88 avg rating \\u2014 8 ratings\\n\\n|\\n\\n|\\n\\n| 12 |\\n\\n|\\n\\nBecoming Marie Antoinette (Marie Antoinette, #1)\\n\\nby\\n\\n3.81 avg rating \\u2014 6,665 ratings\\n\\n|\\n\\n|\\n\\n| 13 |\\n\\n|\\n\\nThe Mysteries of Udolpho\\n\\nby\\n\\n3.42 avg rating \\u2014 16,480 ratings\\n\\n|\\n\\n|\\n\\n| 14 |\\n\\n|\\n\\nMadame Tussaud: A Novel of the French Revolution\\n\\nby\\n\\n3.97 avg rating \\u2014 22,264 ratings\\n\\n|\\n\\n|\\n\\n|\"\n },\n {\n \"docid\": \"27430\",\n \"score\": 0.5145552158355713,\n \"snippet\": \"---\\ntitle: Read like it's 1821: 10 books that hit the book market 200 years ago for you to enjoy\\nauthor: Anna M Thane\\ndate: 2021-01-20\\n---\\nIf you were a time traveller in 1821 longing for a good read, what would be your options?\\n\\nCheck out my list of popular fiction and non-fiction releases. I have added links to online versions of each book, so you can actually read like its 1821!\\n\\nBonus feature: Suggestions for further reading on each topic from today's experts on the 18th century.\\n\\nJames Fenimore Cooper: \\\"The Spy: a Tale of the Neutral Ground\\\".\\n\\nIt is 1780, a period of conflicts and espionage between military and guerrilla forces in America. Main character Harvey Birch comes under suspicion for being a British spy. Also around: the mysterious Mr. Harper, actually George Washington in disguise\\u2026 . Mr. Cooper's second novel is based on a true story of the Revolutionary War.\\n\\nRead it online here: \\n\\nAre you interested in the Revolutionary War? Here are suggestions for further reading:\\n\\n- Washington's Revolutionary War Generals (Campaigns and Commanders) by Stephen R. Taaffe\\n\\n- A History of the Royal Navy : The American Revolutionary War by Martin Robson\\n\\n- A War of Ideas: British Attitudes to the Wars Against Revolutionary France, 1792-1802 (Routledge Revivals) by Emma Vincent Macleod\\n\\nAnna Maria Porter's \\\"The village of Mariendorpt\\\"\\n\\nThe historical romance takes you to the year 1631. Rupert and Meeta have to fight for their happiness during the Protestant War in Germany and the siege of Magdeburg. It's the 14. novel by the prolific writer, and it was turned into a stage drama in 1838. The critics are mixed: \\\"(It is) full of the most touching passages, but, as a whole, it drags. Her knowledge of military details appears to me marvellous\\\", writes fellow author Sarah Harriet Burney in a letter.\\n\\nRead it online here: \\n\\nAre you interested in female authors of the the 18th century? Here is a suggestion for further reading:\\n\\n- Material Enlightenment: Women Writers and the Science of Mind, 1770-1830 (Studies in the Eighteenth Century) by Joanna Wharton\"\n },\n {\n \"docid\": \"42402\",\n \"score\": 0.5138538479804993,\n \"snippet\": \"---\\ntitle: The Scarlet Pimpernel - Wikipedia\\nauthor: Authority control databases\\ndate: 2002-09-10\\n---\\nname: The Scarlet Pimpernel\\nauthor: Baroness Orczy\\ncountry: United Kingdom\\nlanguage: English\\ngenre: Historical fiction, adventure fiction\\nset_in: 1792, during the early stages of the French Revolution\\npublisher: Greening\\npub_date: 1905\\npages: 319\\npreceded_by: The First Sir Percy\\nfollowed_by: Sir Percy Leads the Band\\n\\nThe Scarlet Pimpernel is the first novel in a series of historical fiction by Baroness Orczy, published in 1905. It was written after her stage play of the same title (co-authored with her husband Montague Barstow) enjoyed a long run in London, having opened in Nottingham in 1903.\\n\\nThe novel is set during the Reign of Terror following the start of the French Revolution. The title is the nom de guerre of its hero and protagonist, a chivalrous Englishman who rescues aristocrats before they are sent to the guillotine. Sir Percy Blakeney leads a double life: apparently nothing more than a wealthy fop, but in reality, a formidable swordsman and a quick-thinking master of disguise and escape artist. The band of gentlemen who assist him are the only ones who know of his secret identity. He is known by his symbol, a simple flower, the scarlet pimpernel (Anagallis arvensis).\\n\\nOpening at the New Theatre in London's West End on 5 January 1905, the play became a favourite of British audiences, eventually playing more than 2,000 performances and becoming one of the most popular shows staged in London. Published after the success of the play, the novel was an immediate success, gaining Orczy a following of readers in Britain and the rest of the world. The stage play and subsequent novel, with their hero and villain, were so popular that they inspired a revival of classic villainy at the time.\\n\\nOrczy's premise of a daring hero who cultivates a secret identity disguised by a meek or ineffectual manner proved enduring. Zorro, Doctor Syn, the Shadow, the Spider, the Green Hornet, the Phantom, Superman and Batman followed within a few decades, and the trope remains a popular one in serial fiction today. Read by\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "35e7bc3a-50a6-4078-bc33-7acc7c0128fd", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.00807, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 2, "action": {"name": "search", "arguments": {"query": "historical fiction Victorian London encrypted document stolen accusation sea storm rescue businessman"}, "id": "8cb4737d-f313-4b87-a55a-eb0d77b950ab", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.035750000000000004, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 2, "observation": {"result": "[\n {\n \"docid\": \"29367\",\n \"score\": 0.6082387566566467,\n \"snippet\": \"---\\ntitle: Historical Fictionistas discussion\\nauthor: Author add link photo\\ndate: 2012-10-11\\n---\\nHistorical Fictionistas discussion\\n\\nRecommendations?\\n\\n>\\n\\nCrime in 19th Century London?\\n\\ndate\\n\\nnewest \\u00bb\\n\\nmessage 1:\\n\\nby\\n\\nAllison\\n\\n(new)\\n\\nOct 11, 2012 07:57AM\\n\\nHey you guys...I would love some recommendations on books set in 19th century London revolving around crime (other than Jack the Ripper). Fiction and non-fiction would both be great!\\n\\nreply\\n\\n|\\n\\nflag\\n\\nAnything by Anne Perry in the Inspector Monk and the Thomas Pitt series. They start with The Face of a Stranger and The Cater Street Hangman, respectively. Monk is 1860s and Pitt somewhat later.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDavid Liss's Benjamin Weaver series is my favorite. It begins with A Conspiracy of Paper, a HF murder/mystery/thriller novel about a financial crisis in 19th century London. You'll love Benjamin and learn a lot about economics (without being bored to tears) because Liss is such a master at inserting historical details in creative ways. David Liss\\n\\nI love historical mysteries, so here's a few of my favorites set in London in the 19th century:\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of aristocratic series:\\n\\nC.S. Harris, Regency era with the first in the series being What Angels Fear\\n\\nCharles Finch,A Beautiful Blue Death is first in the series\\n\\nEarly forensics:\\n\\nDevoured by D.E. Meredith\\n\\nEnjoy!\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of arist\"\n },\n {\n \"docid\": \"77939\",\n \"score\": 0.5877675414085388,\n \"snippet\": \"---\\ntitle: 12 Historical Fiction Books That Bring the Victorian Era to Life\\ndate: 2024-07-09\\n---\\nFeatures summary\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life\\n\\nImmerse yourself in the Victorian era with these 12 captivating historical fiction novels. Each book brings to life the complexities, challenges, and beauty of the period, offering a detailed look at the social, cultural, and technological changes of the time.\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life (Picture Credit - Instagram)\\n\\nThe Victorian era, spanning from 1837 to 1901, was a period of significant social, cultural, and technological change. This era saw the rise of the British Empire, industrial advancements, and shifts in societal norms. Historical fiction set in this time offers a captivating glimpse into the past, blending rich storytelling with historical details. Here are 12 historical fiction books that beautifully capture the essence of the Victorian era.\\n\\n1. The Crimson Petal and the White by Michel Faber\\n\\nMichel Faber's 'The Crimson Petal and the White' provides a vivid and gritty portrayal of Victorian London. The novel follows Sugar, a determined and intelligent prostitute, as she navigates the city's dark underbelly and attempts to climb the social ladder. Faber's meticulous attention to historical detail and complex characters make this a compelling read that immerses readers in the era's harsh realities and opulent surroundings.\\n\\n2. Fingersmith by Sarah Waters\\n\\n'Fingersmith' by Sarah Waters is a riveting tale of deception, betrayal, and unexpected twists. Set in Victorian England, the story revolves around Sue Trinder, a young woman raised among thieves, and her involvement in a con to defraud a wealthy heiress. Waters' masterful storytelling and rich depiction of Victorian society's class divides and criminal underworld makes this novel a gripping exploration of love and trust.\\n\\n3. The Essex Serpent by Sarah Perry\\n\\nSarah Perry's 'The Essex Serpent' is a beautifully written novel that blends historical fiction with gothic elements. Set in 1893, the story follows Cora Seaborne, a widow with a passion for natural history, as she moves to Essex and investigates rumours of a mythical serpent. Perry's evocative prose and exploration of themes like faith, science, and friendship capture the intellectual and societal currents of the Victorian era.\\n\\n4. The Light Between Oceans by M.L. Sted\"\n },\n {\n \"docid\": \"81929\",\n \"score\": 0.5688050985336304,\n \"snippet\": \"---\\ntitle: 12 Captivating Historical Fiction Books Set in London\\u2014Romance, Mystery and Drama Await!\\ndate: 2025-02-20\\n---\\n12 Captivating Historical Fiction Books Set in London\\u2014Romance, Mystery and Drama Await!\\n\\nFrom intrigue to passionate romance and adventure, these novels are your ticket to a bygone era!\\n\\nEvery major city in the world brims with rich history, storied landmarks and vibrant culture\\u2014but there's nothing quite like historical London. In fact, London's history spans nearly two millennia from Roman times to medieval times, Georgian times and beyond, making it home to some of the most iconic places in the world. Whether you're enthralled by the grandeur of Buckingham Palace or the Houses of Parliament or you can't get enough of certain time periods like the Victorian era or WWII, London truly provides a perfect backdrop for historical fiction novels.\\n\\nSo if you're looking to escape into the past through a London lens, you're in luck! Here, we gathered up 12 historical fiction books set in London\\u2014across bygone eras\\u2014that deliver dynamic storylines (many based on real-life events), memorable characters, high-stakes danger and passionate war-torn love stories.\\n\\nDoes stepping foot into Kew Gardens in 1916 London and embarking on a quest for women's suffrage sound fascinating? Pick up The Kew Garden Girls by Posy Lovell. Lovell's book is inspired by true events and bestselling author Natasha Lester called it \\\"an absolutely charming story about the strength and beauty of female friendship.\\\" If you're more in the mood to experience 1930s London alongside five of the most legendary female writers\\u2014including Agatha Christie\\u2014then pick up The Queens of Crime by Marie Benedict. This historical mystery is part whodunit, part adventure and fully immersive!\\n\\nContinue scrolling for all of our compelling, must-read book recommendations. Cheerio!\\n\\n'The Queens of Crime' by Marie Benedict\\n\\nDanger and intrigue come alive in this tale set in 1930s London and inspired by a true story. The five greatest female crime writers, including Agatha Christie and her legendary rival Dorothy Sayers, form a secret society with one goal: to show their refusal to be treated poorly by their male counterparts. To prove it, they'll solve the actual murder of May Daniels. But the culprit targets Sayers next and threatens to expose a dark secret.\\n\\nWhat readers are saying: \\\"This book is a captivating blend of\"\n },\n {\n \"docid\": \"79197\",\n \"score\": 0.5605930685997009,\n \"snippet\": \"---\\ntitle: Mysterious Circumstances\\nauthor: David Grann\\ndate: 2004-12-13\\n---\\nRichard Lancelyn Green, the world's foremost expert on Sherlock Holmes, believed that he had finally solved the case of the missing papers. Over the past two decades, he had been looking for a trove of letters, diary entries, and manuscripts written by Sir Arthur Conan Doyle, the creator of Holmes. The archive was estimated to be worth nearly four million dollars, and was said by some to carry a deadly curse, like the one in the most famous Holmes story \\\"The Hound of the Baskervilles.\\\"\\n\\nThe papers had disappeared after Conan Doyle died, in 1930, and without them no one had been able to write a definitive biography\\u2014a task that Green was determined to complete. Many scholars feared that the archive had been discarded or destroyed; as the London Times noted earlier this year, its whereabouts had become \\\"a mystery as tantalizing as any to unfold at 221B Baker Street,\\\" the fictional den of Holmes and his fellow-sleuth, Dr. Watson.\\n\\nNot long after Green launched his investigation, he discovered that one of Conan Doyle's five children, Adrian, had, with the other heirs' agreement, stashed the papers in a locked room of a ch\\u00e2teau that he owned in Switzerland. Green then learned that Adrian had spirited some of the papers out of the ch\\u00e2teau without his siblings' knowledge, hoping to sell them to collectors. In the midst of this scheme, he died of a heart attack\\u2014giving rise to the legend of the curse. After Adrian's death, the papers apparently vanished. And whenever Green tried to probe further he found himself caught in an impenetrable web of heirs\\u2014including a self-styled Russian princess\\u2014who seemed to have deceived and double-crossed each other in their efforts to control the archive.\\n\\nFor years, Green continued to sort through evidence and interview relatives, until one day the muddled trail led to London\\u2014and the doorstep of Jean Conan Doyle, the youngest of the author's children. Tall and elegant, with silver hair, she was an imposing woman in her late sixties. (\\\"Something very strong and forceful seems to be at the back of that wee body,\\\" her father had written of Jean when she was five. \\\"Her will is tremendous.\\\") Whereas her brother Adrian had been kicked out of the British Navy for insubordination, and her elder brother Denis was a playboy who had\"\n },\n {\n \"docid\": \"97644\",\n \"score\": 0.5432103872299194,\n \"snippet\": \"---\\ntitle: The 12 Most Electrifying Historical Thrillers Set in Victorian Era\\n---\\nYou need to enable JavaScript to run this app.\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "8cb4737d-f313-4b87-a55a-eb0d77b950ab", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.035750000000000004, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 3, "action": {"name": "search", "arguments": {"query": "protagonist disguise businessman stolen document sea storm dramatic rescue Victorian novel"}, "id": "25b83571-eb9d-4413-8581-1ef74f8c696b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.07896500000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 3, "observation": {"result": "[\n {\n \"docid\": \"19305\",\n \"score\": 0.5724257826805115,\n \"snippet\": \"---\\ntitle: Ten of the best disguises in literature\\nauthor: John Mullan\\ndate: 2010-09-25\\n---\\nThe Odyssey, by Homer\\n\\nOdysseus arrives back at his island of Ithaca disguised as a beggar. He is recognised only by his old dog Argus (animals always see through disguises), which dies of joy on the spot. In his disguise, our hero is able to see who has been loyal to him and who has not.\\n\\nMeasure for Measure, by William Shakespeare\\n\\nThe Duke who governs Vienna wants to see what his underlings will get up to in his absence. So he asks his friend Friar Thomas for some monkish garb: \\\"Supply me with the habit and instruct me / How I may formally in person bear me / Like a true friar\\\". It works, and not even his most devoted courtiers recognise him until he finally unveils himself.\\n\\nThe Monk, by Matthew Lewis\\n\\nAnother monkish disguise. Sexy young Matilda lusts after Father Ambrosio, the most pious monk in Madrid. So she dresses up as a young novice monk and finds her way into the monastery. In her cell she reveals herself to Ambrosio, who cannot resist her charms. It turns out that she is in fact a demon.\\n\\nJane Eyre, by Charlotte Bront\\u00eb\\n\\nOne of the great episodes of transvestism in literature comes when Rochester togs himself up as a Gypsy woman to read the palms of the guests he has invited to Thornfield. Blanche Ingram, Jane's rival for his affections, gets uncomforting news, but Jane is told \\\"the cup of bliss\\\" is going to be offered to her.\\n\\nEast Lynne, by Mrs Henry Wood\\n\\nLady Isabel Vane loses her happy home and family when she conducts an adulterous affair with the utterly caddish Francis Levinson. Having learned the error of her ways, she returns to be governess to her own children, disguised by blue-lensed glasses, hair turned white from shock after a train crash and a scarred mouth.\\n\\nThe Mystery of Edwin Drood, by Charles Dickens\\n\\nDick Datchery arrives in the town of Cloisterham, apparently a detective in disguise (he wears a wig). He (or she?) keeps watch over John Jasper, choirmaster and secret drug addict. Drood has disappeared: is he disguised as Datchery? Or is it another character, investigating Drood's murder? Dickens did\"\n },\n {\n \"docid\": \"20048\",\n \"score\": 0.5056356191635132,\n \"snippet\": \"---\\ntitle: The Old Curiosity Shop - Wikipedia\\ndate: 2004-07-13\\n---\\nname: The Old Curiosity Shop\\nalt: Cover of Master Humphrey's Clock, in which the serial editions were published\\nauthor: Charles Dickens\\nillustrator: George CattermoleHablot Knight Browne (Phiz)Samuel WilliamsDaniel Maclise\\ncover_artist: George Cattermole\\ncountry: England\\nlanguage: English\\ngenre: Novel\\npublisher: Chapman & Hall London\\npublished: Serialised April 1840 \\u2013 February 1841, Old Curiosity Shop book format 1841\\nmedia_type: Print\\npreceded_by: Nicholas Nickleby\\nfollowed_by: Barnaby Rudge\\nwikisource: The Old Curiosity Shop\\n\\nThe Old Curiosity Shop is the fourth novel by English author Charles Dickens; being one of his two novels (the other being Barnaby Rudge) published along with short stories in his weekly serial Master Humphrey's Clock, from 1840 to 1841. It was so popular that New York readers reputedly stormed the wharf when the ship bearing the final instalment arrived in 1841.\\n\\nThe Old Curiosity Shop was printed in book form in 1841. Queen Victoria read the novel that year and found it \\\"very interesting and cleverly written\\\".\\n\\nThe plot follows the journey of Nell Trent and her grandfather, both residents of The Old Curiosity Shop in London, whose lives are thrown into disarray and destitution due to the machinations of an evil moneylender and the grandfather's addiction to gambling.\\n\\nSynopsis\\n\\nThe Old Curiosity Shop tells the story of Nell Trent, a beautiful and virtuous young girl of \\\"not quite fourteen\\\". An orphan, she lives with her maternal grandfather (whose name is never revealed) in his shop of odds and ends. Her grandfather loves her dearly, and Nell does not complain, but she lives a lonely existence with almost no friends her own age. Her only friend is Kit, an honest boy employed at the shop, whom she is teaching to write.\\n\\nSecretly obsessed with ensuring that Nell does not die in poverty as her parents did, her grandfather attempts to provide Nell with a good inheritance through gambling at cards. He keeps his nocturnal games a secret, but borrows heavily from the evil Daniel Quilp, a malicious, grotesquely de\"\n },\n {\n \"docid\": \"42402\",\n \"score\": 0.5011107325553894,\n \"snippet\": \"---\\ntitle: The Scarlet Pimpernel - Wikipedia\\nauthor: Authority control databases\\ndate: 2002-09-10\\n---\\nname: The Scarlet Pimpernel\\nauthor: Baroness Orczy\\ncountry: United Kingdom\\nlanguage: English\\ngenre: Historical fiction, adventure fiction\\nset_in: 1792, during the early stages of the French Revolution\\npublisher: Greening\\npub_date: 1905\\npages: 319\\npreceded_by: The First Sir Percy\\nfollowed_by: Sir Percy Leads the Band\\n\\nThe Scarlet Pimpernel is the first novel in a series of historical fiction by Baroness Orczy, published in 1905. It was written after her stage play of the same title (co-authored with her husband Montague Barstow) enjoyed a long run in London, having opened in Nottingham in 1903.\\n\\nThe novel is set during the Reign of Terror following the start of the French Revolution. The title is the nom de guerre of its hero and protagonist, a chivalrous Englishman who rescues aristocrats before they are sent to the guillotine. Sir Percy Blakeney leads a double life: apparently nothing more than a wealthy fop, but in reality, a formidable swordsman and a quick-thinking master of disguise and escape artist. The band of gentlemen who assist him are the only ones who know of his secret identity. He is known by his symbol, a simple flower, the scarlet pimpernel (Anagallis arvensis).\\n\\nOpening at the New Theatre in London's West End on 5 January 1905, the play became a favourite of British audiences, eventually playing more than 2,000 performances and becoming one of the most popular shows staged in London. Published after the success of the play, the novel was an immediate success, gaining Orczy a following of readers in Britain and the rest of the world. The stage play and subsequent novel, with their hero and villain, were so popular that they inspired a revival of classic villainy at the time.\\n\\nOrczy's premise of a daring hero who cultivates a secret identity disguised by a meek or ineffectual manner proved enduring. Zorro, Doctor Syn, the Shadow, the Spider, the Green Hornet, the Phantom, Superman and Batman followed within a few decades, and the trope remains a popular one in serial fiction today. Read by\"\n },\n {\n \"docid\": \"72751\",\n \"score\": 0.4939073622226715,\n \"snippet\": \"---\\ntitle: 5 Powerful Examples of Disguise As A Device In Fiction\\ndate: 2020-04-14\\n---\\nIn this blog, we show you five examples of disguise as a device in fiction. The examples from classic and modern literature explain how writers have used disguise in their stories to remarkable effect.\\n\\n5 Powerful Examples of Disguise As A Device In Fiction\\n\\nYou will discover:\\n\\n- How Shakespeare mastered disguise in his plays.\\n\\n- How Charlotte Bronte showed another side to her imposing hero Rochester.\\n\\n- How Virginia Woolf smashed gender stereotypes in her feminist masterpiece Orlando.\\n\\n- How Anne Fine helped a divorced father connect with his family in Madame Doubtfire.\\n\\n- How Anne Rice showed us human weakness and desire through immortal beings in Interview With The Vampire.\\n\\nWhy Use Disguise In Your Fiction?\\n\\nWe all know that disguise is a common storytelling device. It can be used to express a theme, illumine a character, or drive a plot. When a character in a short story, script, or novel disguises their identity, they could be seeking a truth or revealing a deceit.\\n\\nThe obfuscation always has a reason that fits in with the plot or theme of the story.\\n\\nAs a writer, you can use this sort of clever concealment or literary trickery to subvert stereotypes or play with readers' expectations. It is a powerful resource available to you as a writer.\\n\\nIt is also, I'm sure you will agree, a lot of fun to write.\\n\\nBrilliant Disguise\\n\\nWhile a superhero may wear a mask, he wears it as a concealment. A mask seems to 'seal' or close off an identity \\u2013 whereas a disguise is more about taking on or absorbing another identity, sometimes more than one identity, without abandoning a core character or personality. It's a form of character camouflage that is important to the story or a character's inner transformation.\\n\\n1. Mastering Disguise\\n\\nRecently, I was reading about Giuseppe Dosi. In the 1920s, he was possibly one of Italy's greatest detectives, so much so that he could have easily been a fictional creation.\\n\\nThis former actor was a master of disguises and he used disguise as a method to solve crime. In fact, his approach became known as detectival transformation. His personas included a femme fatal, priest, banker, and a war veteran with a 'bum' leg. All his identities had their own fake IDs and penmanship.\\n\\nShakespeare was, of course\"\n },\n {\n \"docid\": \"18321\",\n \"score\": 0.48267534375190735,\n \"snippet\": \"---\\ntitle: Bleak House - Wikipedia\\nauthor: Authority control databases\\ndate: 2004-01-25\\n---\\nname: Bleak House\\nimage_size: 200px\\nauthor: Charles Dickens\\nillustrator: Hablot Knight Browne (Phiz)\\ncover_artist: Hablot Knight Browne (Phiz)\\ncountry: England\\nlanguage: English\\ngenre: Novel\\npublisher: Bradbury & Evans\\npublished: Serialised 12 March 1852 \\u2013 12 September 1853, book form 12 September 1853\\npreceded_by: David Copperfield\\nfollowed_by: A Child's History of England\\n\\nBleak House is a novel by English author Charles Dickens, first published as a 20-episode serial between 12 March 1852 and 12 September 1853. The novel has many characters and several subplots, and is told partly by the novel's heroine, Esther Summerson, and partly by an omniscient narrator. At the centre of Bleak House is a long-running legal case in the Court of Chancery, Jarndyce and Jarndyce, which comes about because a testator has written several conflicting wills. In a preface to the 1853 first edition, Dickens said there were many actual precedents for his fictional case. One such was probably Thellusson v Woodford, in which a will read in 1797Constantine, Alison. The Restoration of Brodsworth Hall & Gardens, February 2007 historical address, at Tickhill & District Local History Society was contested and not determined until 1859. Though many in the legal profession criticised Dickens's satire as exaggerated, Bleak House helped support a judicial reform movement that culminated in the enactment of legal reform in the 1870s.\\n\\nSome scholars debate when Bleak House is set. The English legal historian Sir William Holdsworth sets the action in 1827; however, reference to preparation for the building of a railway in Chapter LV suggests the 1830s. A work of Gothic fiction depicting London as a murky city swathed in fog, Bleak House is credited with introducing urban fog to the novel, which would become a frequent characteristic of urban Gothic literature and film.Mighall, Robert (2007). \\\"Gothic\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "25b83571-eb9d-4413-8581-1ef74f8c696b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.07896500000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 4, "action": {"name": "search", "arguments": {"query": "novel encrypted document smuggle abroad cipher codes 19th century London historical"}, "id": "10fc0814-765c-4c01-b8b3-f41a0194c9ba", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.07896500000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 4, "observation": {"result": "[\n {\n \"docid\": \"38361\",\n \"score\": 0.5048168897628784,\n \"snippet\": \"---\\ntitle: The Secrets We Kept by Lara Prescott Reveals the Story Behind Doctor Zhivago\\nauthor: Author Alana White\\ndate: 2014-04-05\\n---\\nThe Secrets We Kept by Lara Prescott Reveals the Story Behind Doctor Zhivago\\n\\nOne of the most highly anticipated historical novels to hit shelves this fall was Lara Prescott's The Secrets We Kept (Hutchinson, September 2019). Selected for Reese Witherspoon's Hello Sunshine Book Club for September 2019, the novel was also nominated as a Best Debut Novel in the 11th Annual Goodreads Choice Awards. The film and TV rights have already been optioned.\\n\\nPrescott's novel reveals the intriguing true-life espionage plot behind Boris Pasternak's beloved novel, Doctor Zhivago. Set mainly in the US and the USSR in the 1950s, the two main protagonists in The Secrets We Kept are Sally Forrester and Irina Drozdov. Both women work as typists in the male-dominated CIA, but they are actually spies. Sally, the veteran spy, trains Irina and their mission is to first smuggle Boris Pasternak's Doctor Zhivago out of the USSR in order to publish it and then they must disseminate banned copies of Doctor Zhivago to Soviet citizens. The goal of this mission is to make people in the USSR question their government and expose the realities of communist life. The other half of The Secrets We Kept is told from the perspective of Boris Pasternak and his lover Olga Ivinskaya, the inspiration behind the character of Lara in Doctor Zhivago.\\n\\nWhile Sally and Irina are fictional characters, the covert spycraft behind Doctor Zhivago is entirely factual. The true-life back story of Doctor Zhivago first came to light in 2014, when 130 documents and declassified CIA files were released, exposing the CIA \\\"Books Program\\\" of the Cold War era. Up until that point, not many people knew about the story behind Doctor Zhivago and that the CIA viewed books as weapons during the Cold War, utilizing banned books to fight the Soviets and their propaganda.[1]\\n\\nLara Prescott first learned about the history behind Doctor Zhivago in 2014 when her father sent her a Washington Post article about these declassified CIA documents. Intrigued and wanting to know more, Prescott started to do some digging of her\"\n },\n {\n \"docid\": \"93624\",\n \"score\": 0.49482470750808716,\n \"snippet\": \"---\\ntitle: THE AGONY COLUMN OF THE \\\"TIMES\\\" 1800-1870\\ndate: 2019-01-01\\n---\\n*** START OF THE PROJECT GUTENBERG EBOOK 54658 ***\\n\\nPlease see the Transcriber's Notes at the end of the text.\\n\\nOF THE \\\"TIMES\\\"\\n\\n1800-1870\\n\\nWITH AN INTRODUCTION\\n\\nEDITED BY ALICE CLAY\\n\\nLondon\\n\\nCHATTO AND WINDUS, PICCADILLY\\n\\n1881\\n\\n[All rights reserved]\\n\\nPRINTED BY WILLIAM CLOWES AND SONS, LIMITED, LONDON AND BECCLES.\\n\\nThe contents of the little volume now presented to the public have been taken from the second column (commonly called the \\\"Agony Column\\\") of the Times newspaper, from the commencement of the present century to the end of the year 1870.\\n\\nReaders of newspapers (more especially of the Times) cannot fail to be struck by the mysterious communications which daily appear, and I venture to hope my selection of some of the most remarkable may interest those who peruse these pages.\\n\\nMost of the advertisements selected show a curious phase of life, interesting to an observer of human existence and human eccentricities. They are veiled in an air of mystery, with a view of blinding the general public, but at the same time give a clue unmistakable to those for whom they were intended.\\n\\nAt the early period of 1800 the \\\"Agony Column\\\" seems to have been the chief medium for matrimonial advertisements; but, unfortunately, we are left considerably in the dark, and our curiosity as to whether the young nobleman (in advertisement[vi] No. 2) eventually married the unknown \\\"Catholic widow\\\" is not gratified; but we do learn something, namely, that love at first sight was not so rare in those days as it is supposed to be in the present unromantic age.\\n\\nThere is little doubt that lovers separated by unfortunate circumstances, or by angry parents, as well as bachelors meditating matrimony, have found in the \\\"Agony Column\\\" a safe means of secret correspondence. With what despair did \\\"One-winged Dove\\\" (advertisement No. 214) beseech her lover, the \\\"Crane,\\\" to return to her! Sorely must her patience have been tried as she scanned the paper in vain day\"\n },\n {\n \"docid\": \"29367\",\n \"score\": 0.49336209893226624,\n \"snippet\": \"---\\ntitle: Historical Fictionistas discussion\\nauthor: Author add link photo\\ndate: 2012-10-11\\n---\\nHistorical Fictionistas discussion\\n\\nRecommendations?\\n\\n>\\n\\nCrime in 19th Century London?\\n\\ndate\\n\\nnewest \\u00bb\\n\\nmessage 1:\\n\\nby\\n\\nAllison\\n\\n(new)\\n\\nOct 11, 2012 07:57AM\\n\\nHey you guys...I would love some recommendations on books set in 19th century London revolving around crime (other than Jack the Ripper). Fiction and non-fiction would both be great!\\n\\nreply\\n\\n|\\n\\nflag\\n\\nAnything by Anne Perry in the Inspector Monk and the Thomas Pitt series. They start with The Face of a Stranger and The Cater Street Hangman, respectively. Monk is 1860s and Pitt somewhat later.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDavid Liss's Benjamin Weaver series is my favorite. It begins with A Conspiracy of Paper, a HF murder/mystery/thriller novel about a financial crisis in 19th century London. You'll love Benjamin and learn a lot about economics (without being bored to tears) because Liss is such a master at inserting historical details in creative ways. David Liss\\n\\nI love historical mysteries, so here's a few of my favorites set in London in the 19th century:\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of aristocratic series:\\n\\nC.S. Harris, Regency era with the first in the series being What Angels Fear\\n\\nCharles Finch,A Beautiful Blue Death is first in the series\\n\\nEarly forensics:\\n\\nDevoured by D.E. Meredith\\n\\nEnjoy!\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of arist\"\n },\n {\n \"docid\": \"1891\",\n \"score\": 0.4854259192943573,\n \"snippet\": \"---\\ntitle: Kryptos - Wikipedia\\ndate: 2004-02-06\\n---\\ntitle: Kryptos\\nartist: Jim Sanborn\\nyear: 1990\\ndimensions: 11\\u201312 feet \\u00d7 20 feet\\nmuseum: George Bush Center for Intelligence\\ncity: Langley, Virginia\\ncoordinates: 38.95227 -77.14573 type:landmark_region:US-VA dms inline, title\\n\\nKryptos is a sculpture by the American artist Jim Sanborn located on the grounds of the Central Intelligence Agency (CIA) headquarters, the George Bush Center for Intelligence in Langley, Virginia.\\n\\nSince its dedication on November 3, 1990, there has been much speculation about the meaning of the four encrypted messages it bears. Of these four messages, the first three have been solved, while the fourth message remains one of the most famous unsolved codes in the world. It is said that a fifth message will reveal itself after the first four are solved. The sculpture continues to be of interest to cryptanalysts, both amateur and professional, who are attempting to decode the fourth passage. The artist has so far given four clues to this passage.\\n\\nDescription\\n\\nThe sculpture comprises four large copper plates with other elements consisting of water, wood, plants, red and green granite, white quartz, and petrified wood. The most prominent feature of the entire piece is a large vertical S-shaped copper screen resembling a scroll or a piece of paper emerging from a computer printer, half of which consists of encrypted text, that is located in the northwest corner of the New Headquarters Building courtyard, outside of the agency's cafeteria. The characters are all found within the 26 letters of the Latin alphabet, along with question marks, and are cut out of the copper plates. The main sculpture contains four separate enigmatic messages, three of which have been deciphered.\\n\\nIn addition to the main part of the sculpture, Sanborn also placed other pieces of art on the CIA grounds, such as several large granite slabs with sandwiched copper sheets outside the entrance to the New Headquarters Building. Several Morse code messages are found on these copper sheets, and one of the stone slabs has an engraving of a compass rose pointing to a lodestone. The ciphers' increasing \\\"complexity\\\" through the entrance into the courtyard is intended to be as if it \\\"were a fossil\\\". Other elements of San\"\n },\n {\n \"docid\": \"81929\",\n \"score\": 0.4801541864871979,\n \"snippet\": \"---\\ntitle: 12 Captivating Historical Fiction Books Set in London\\u2014Romance, Mystery and Drama Await!\\ndate: 2025-02-20\\n---\\n12 Captivating Historical Fiction Books Set in London\\u2014Romance, Mystery and Drama Await!\\n\\nFrom intrigue to passionate romance and adventure, these novels are your ticket to a bygone era!\\n\\nEvery major city in the world brims with rich history, storied landmarks and vibrant culture\\u2014but there's nothing quite like historical London. In fact, London's history spans nearly two millennia from Roman times to medieval times, Georgian times and beyond, making it home to some of the most iconic places in the world. Whether you're enthralled by the grandeur of Buckingham Palace or the Houses of Parliament or you can't get enough of certain time periods like the Victorian era or WWII, London truly provides a perfect backdrop for historical fiction novels.\\n\\nSo if you're looking to escape into the past through a London lens, you're in luck! Here, we gathered up 12 historical fiction books set in London\\u2014across bygone eras\\u2014that deliver dynamic storylines (many based on real-life events), memorable characters, high-stakes danger and passionate war-torn love stories.\\n\\nDoes stepping foot into Kew Gardens in 1916 London and embarking on a quest for women's suffrage sound fascinating? Pick up The Kew Garden Girls by Posy Lovell. Lovell's book is inspired by true events and bestselling author Natasha Lester called it \\\"an absolutely charming story about the strength and beauty of female friendship.\\\" If you're more in the mood to experience 1930s London alongside five of the most legendary female writers\\u2014including Agatha Christie\\u2014then pick up The Queens of Crime by Marie Benedict. This historical mystery is part whodunit, part adventure and fully immersive!\\n\\nContinue scrolling for all of our compelling, must-read book recommendations. Cheerio!\\n\\n'The Queens of Crime' by Marie Benedict\\n\\nDanger and intrigue come alive in this tale set in 1930s London and inspired by a true story. The five greatest female crime writers, including Agatha Christie and her legendary rival Dorothy Sayers, form a secret society with one goal: to show their refusal to be treated poorly by their male counterparts. To prove it, they'll solve the actual murder of May Daniels. But the culprit targets Sayers next and threatens to expose a dark secret.\\n\\nWhat readers are saying: \\\"This book is a captivating blend of\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "10fc0814-765c-4c01-b8b3-f41a0194c9ba", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.07896500000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 5, "action": {"name": "search", "arguments": {"query": "historical fiction woman disguise businessperson accused theft document schemer storm rescue"}, "id": "4a1ca3f6-3b42-43d8-bb94-1583246b1cca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.15477000000000002, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 5, "observation": {"result": "[\n {\n \"docid\": \"51328\",\n \"score\": 0.5671062469482422,\n \"snippet\": \"---\\ntitle: Celebrating History's Bad-Ass Women: PW Talks with Greer Macallister\\nauthor: Claire Kirch\\ndate: 2017-02-28\\n---\\nGreer Macallister's historical novel Girl in Disguise (Sourcebooks) is a rollicking mashup of the real and imagined exploits of Kate Warne, the first female operative employed by the famed Pinkerton Detective Agency.\\n\\nHow did you come to write Girl in Disguise?\\n\\nWhen I first heard Kate's story a few years ago, I was floored. This woman was such a pioneer. She was a female detective in the 1850s, when it was completely unheard-of, and she was so good at it that she was assigned by Allan Pinkerton to run her own division. She helped save Abraham Lincoln's life as he made his way to his inauguration. She was an undercover spy for the Union during the Civil War. Somebody needed to get her story out there, and I figured it might as well be me!\\n\\nYou mention in your author's note that very little is known about Kate Warne; there aren't even verified photos of her--partially because she was a spy and also because Pinkerton Agency records were lost during the Chicago Fire of 1871. How much of her story is based on historical sources and how much did you have to imagine?\\n\\nThose gaps in the historical record made Kate the perfect subject for historical fiction--a little history and a lot of fiction. We know the facts on a handful of cases she worked, like the Adams Express case and one where she posed as a fortune-teller to catch a poisoner. Her role in thwarting the Lincoln assassination attempt in Baltimore is documented. But a lot of the rest is just open space, so I got to choose how to fill it in.\\n\\nWere the lives of Warne's male colleagues--including Allan Pinkerton--similarly shrouded in mystery?\\n\\nNot to the same degree, although some operatives' lives were better-documented than others. Pinkerton himself wrote and published a lot about his own prowess, so with him it's the opposite--tons of information, but not all reliable. For the cast of characters around Kate, I drew on some information about her real colleagues at the time, but mostly I combined and synthesized and created. It's all about balancing story and history to make the best possible experience for the reader.\\n\\nKate Warne is a mature woman, a widow, when she\"\n },\n {\n \"docid\": \"99787\",\n \"score\": 0.5656360983848572,\n \"snippet\": \"---\\ntitle: Girl in Disguise Hardcover \\u2013 March 21, 2017\\nauthor: Greer Macallister\\ndate: 2017-03-21\\n---\\n$25.99$25.99\\n\\n$7.59 delivery\\n\\nShips from: Amazon.com Sold by: Amazon.com\\n\\n$9.75$9.75\\n\\nDelivery Thursday, June 19\\n\\nShips from: Amazon Sold by: t.rey treasures\\n\\nReturn this item for free\\n\\nFree returns are available for the shipping address you chose. You can return the item for any reason in new and unused condition: no return shipping charges.\\n\\nLearn more about free returns.- Go to your orders and start the return\\n\\n- Select your preferred free shipping option\\n\\n- Drop off and leave!\\n\\nSorry, there was a problem.\\n\\nThere was an error retrieving your Wish Lists. Please try again.Sorry, there was a problem.\\n\\nList unavailable.Download the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nGirl in Disguise Hardcover \\u2013 March 21, 2017\\n\\nPurchase options and add-ons\\n\\nFrom the USA Today Bestselling author of The Magician's Lie\\n\\n\\\"Macallister is becoming a leading voice in strong, female-driven historical fiction. Exciting, frightening, and unspeakably moving...\\\"\\u2015Erika Robuck, bestselling author of Hemingways's Girl\\n\\nFor the first daring female Pinkerton detective, respect is hard to come by, but danger and spies are everywhere.\\n\\nIn the tumultuous years of the Civil War, the streets of Chicago offer a woman mostly danger and ruin\\u2015unless that woman is Kate Warne. As an undercover Pinkerton detective, Kate is able to infiltrate the seedy side of the city in disguises that her fellow spies just can't manage. She's a seductress, an exotic foreign medium, a rich train passenger\\u2015all depending on the day and the robber, thief, or murderer she's been assigned to nab.\\n\\nBut is it only her detective work that makes her a daring spy and a clever liar? Or is the real disguise the good girl she always thought she was? As the Civil War marches closer, Kate takes on her most pressing job\"\n },\n {\n \"docid\": \"53914\",\n \"score\": 0.5568761825561523,\n \"snippet\": \"---\\ntitle: Search This Blog\\nauthor: Sarah Johnson\\ndate: 2014-08-29\\n---\\nThese novels aren't fanciful in premise. In actuality, there were many women who disguised their sex and fought in the US Civil War and in earlier battles, but recognition of and pride in their accomplishments has often been long in coming. These works of fiction, some of which are based on the lives of specific historical women, help to spread word about their deeds and heroism in the popular consciousness.\\n\\nA young woman who had been fighting for the Union in disguise has to hide her loyalties after she's wounded and gets trapped behind Confederate lines. RiverNorth, June 2014.\\n\\nA rare novel that looks at this scenario from the Confederate side, as two Southern sisters enlist in the Confederate army as new recruits, their secret known only to one another. The co-authors are sisters as well. Houghton Mifflin Harcourt, March 2015.\\n\\nHer husband being too weak to go to war, an Indiana farm wife dons male garb and marches off to fight for the Union. I'll have a review of this new literary novel shortly. Little Brown, September 2014.\\n\\nBelieving her place is with her newly-wed husband, Rosetta Wakefield secretly follows him into the Union ranks, fighting alongside him and proving her worth in battle. Loosely based on the life of Sarah Rosetta Wakeman. Crown, January 2014; out in paperback in September, with a beautiful new cover.\\n\\nThis novel about Massachusetts heroine Deborah Sampson shows her external and internal transformations during her service in the Revolutionary War. See my review of Revolutionary as well as Alex Myers' guest post here. Simon & Schuster, January 2014.\\n\\nFrom the author of the 4-book Far Western Civil War series comes a new novel about Emma Edmonds, who signed on with the 2nd Michigan Volunteers under the name Frank Thompson. BookView Cafe, April 2014.\\n\\nOne of my secondary characters in my Civil War novels Promise & Honor and Honor & Glory disguised herself. I had a reviewer on Amazon say that she liked the character but found it unbelievable that a woman could succeed in hiding herself that way.\\n\\nReplyDeleteI would love to write a novel on an unknown female soldier that I found in an article from an 1863 Missouri Democrat. I've blogged about her on a couple of\"\n },\n {\n \"docid\": \"41618\",\n \"score\": 0.5525739192962646,\n \"snippet\": \"---\\ntitle: Women Looking Away in Period Costume\\ndate: 2021-08-10\\n---\\nWomen Looking Away in Period Costume\\n\\nHistorical fiction with one little thing in common.\\n\\nAugust 10, 2021\\n\\nWe're going to let you in on a librarian secret: sometimes we judge a book by its cover. The fastest way to find historical fiction is to look for a woman in period costume looking (or walking) away!\\n\\nThe Address\\n\\nFiona Davis\\n\\nInterior designer Bailey Camdenis leaps at a chance to renovate her heiress cousin's lavish apartment at The Dakota, and learns the scandalous history of a distant ancestor's connection to the murder of the building's architect a century earlier.\\n\\nThe Alice Network\\n\\nKate Quinn\\n\\nIn 1947, pregnant Charlie St. Clair, an American college girl banished from her family, arrives in London to find out what happened to her beloved cousin Rose, who disappeared in Nazi-occupied France during the war, and meets a former spy who, torn apart by betrayal, agrees to help her on her mission.\\n\\nAll the Flowers in Paris\\n\\nSarah Jio\\n\\nA tale told from alternating viewpoints follows the experiences of a Parisian woman who awakens with no memory of her past before discovering a mysterious cache of letters written by a young woman of Jewish ancestry during the Nazi occupation.\\n\\nThe Atomic City Girls\\n\\nJanet Beard\\n\\nWorking in support of the war effort, June Walker begins an affair with a young Jewish physicist in hopes of uncovering what the government's end goal is, until the bombing of Hiroshima reveals the truth about what they are doing.\\n\\nAtomic Love\\n\\nJennie Fields\\n\\nRecruited by the FBI to spy on her former lover, a guilt-riddled Manhattan Project physicist becomes torn between lingering feelings for her ex and her growing attraction to a special agent, a former prisoner of war.\\n\\nBand of Sisters\\n\\nLauren Willig\\n\\nEschewed by her wealthy graduated classmates, a former scholarship student reluctantly volunteers to help World War I French civilians before finding herself surrounded by desperate families in villages decimated by German bombs\\n\\nThe Book of Lost Names\\n\\nKristin Harmel\\n\\nEscaping from Paris in 1942 after the arrest of her father, a Polish Jew, a graduate student finds refuge in a small mountain town, where she forges identity documents to help hundreds of Jewish children flee the Nazis.\\n\\nChurchill's Secret Messenger\\n\\nAlan Hlad\\n\\nRecruited from Churchill's typing pool to become an undercover spy\"\n },\n {\n \"docid\": \"95934\",\n \"score\": 0.550140380859375,\n \"snippet\": \"---\\ntitle: What's the Name of That Book??? discussion\\nauthor: Author add link photo\\ndate: 2017-09-06\\n---\\nWhat's the Name of That Book??? discussion\\n\\n\\u25ba Suggest books for me\\n\\n>\\n\\nWoman disguised as a man\\n\\nYou might try looking through the whole \\\"Suggest books for me\\\" folder. There are multiple threads with this theme.\\n\\nThe Masqueraders this one has it all--sister disguised as a man and brother disguised as a woman. What more can anyone ask, right? It is also by Georgette Heyer which means it's almost guaranteed to be good.\\n\\nUnmasking Miss Appleby - this Regency fantasy romance has the heroine using actual magic to turn herself into a man as opposed to dressing as one, which paradoxically makes the charade a lot more believable.\\n\\nThe protagonist of The Third Witch, a young girl, disguises herself as a serving boy to infiltrate the castle of her enemy, Macbeth (of the Shakespeare play).\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nRidiculous by D.L. Carter. Regency era. Woman dresses as a man to save her family from financial ruin.\\n\\nThe Horse and His Boy by C. S. Lewis has someone doing this, and she's an important character, although not the protagonist. The Horse and His Boy\\n\\nSisters of Shiloh by Kathy Hepinstall features 2 sisters who disguise themselves as men and join the Confederate Army\\u2014one seeking vengeance on the battlefield, the other finding love.\\n\\nI\\n\\nI\\n\\nPaladin by Sally Slater.\\n\\nthe h is not your typical maria clara. she enjoys sword work and is good at it. in order to join a quest, she has to pretend to be a boy, and she is on par with other males in terms of one on one fights. the H is a team mate on this quest, one of the 1st to diacover her secret, he is scarred emotionally.\\n\\nthe h is not your typical maria clara. she enjoys sword work and\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "4a1ca3f6-3b42-43d8-bb94-1583246b1cca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.15477000000000002, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 6, "action": {"name": "search", "arguments": {"query": "\"Girl in Disguise\" Kate Warne businessman document theft sea storm rescue"}, "id": "6c30154a-9867-4038-b80b-69091e75d611", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.24781500000000004, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 6, "observation": {"result": "[\n {\n \"docid\": \"51328\",\n \"score\": 0.7458477020263672,\n \"snippet\": \"---\\ntitle: Celebrating History's Bad-Ass Women: PW Talks with Greer Macallister\\nauthor: Claire Kirch\\ndate: 2017-02-28\\n---\\nGreer Macallister's historical novel Girl in Disguise (Sourcebooks) is a rollicking mashup of the real and imagined exploits of Kate Warne, the first female operative employed by the famed Pinkerton Detective Agency.\\n\\nHow did you come to write Girl in Disguise?\\n\\nWhen I first heard Kate's story a few years ago, I was floored. This woman was such a pioneer. She was a female detective in the 1850s, when it was completely unheard-of, and she was so good at it that she was assigned by Allan Pinkerton to run her own division. She helped save Abraham Lincoln's life as he made his way to his inauguration. She was an undercover spy for the Union during the Civil War. Somebody needed to get her story out there, and I figured it might as well be me!\\n\\nYou mention in your author's note that very little is known about Kate Warne; there aren't even verified photos of her--partially because she was a spy and also because Pinkerton Agency records were lost during the Chicago Fire of 1871. How much of her story is based on historical sources and how much did you have to imagine?\\n\\nThose gaps in the historical record made Kate the perfect subject for historical fiction--a little history and a lot of fiction. We know the facts on a handful of cases she worked, like the Adams Express case and one where she posed as a fortune-teller to catch a poisoner. Her role in thwarting the Lincoln assassination attempt in Baltimore is documented. But a lot of the rest is just open space, so I got to choose how to fill it in.\\n\\nWere the lives of Warne's male colleagues--including Allan Pinkerton--similarly shrouded in mystery?\\n\\nNot to the same degree, although some operatives' lives were better-documented than others. Pinkerton himself wrote and published a lot about his own prowess, so with him it's the opposite--tons of information, but not all reliable. For the cast of characters around Kate, I drew on some information about her real colleagues at the time, but mostly I combined and synthesized and created. It's all about balancing story and history to make the best possible experience for the reader.\\n\\nKate Warne is a mature woman, a widow, when she\"\n },\n {\n \"docid\": \"99787\",\n \"score\": 0.7182636857032776,\n \"snippet\": \"---\\ntitle: Girl in Disguise Hardcover \\u2013 March 21, 2017\\nauthor: Greer Macallister\\ndate: 2017-03-21\\n---\\n$25.99$25.99\\n\\n$7.59 delivery\\n\\nShips from: Amazon.com Sold by: Amazon.com\\n\\n$9.75$9.75\\n\\nDelivery Thursday, June 19\\n\\nShips from: Amazon Sold by: t.rey treasures\\n\\nReturn this item for free\\n\\nFree returns are available for the shipping address you chose. You can return the item for any reason in new and unused condition: no return shipping charges.\\n\\nLearn more about free returns.- Go to your orders and start the return\\n\\n- Select your preferred free shipping option\\n\\n- Drop off and leave!\\n\\nSorry, there was a problem.\\n\\nThere was an error retrieving your Wish Lists. Please try again.Sorry, there was a problem.\\n\\nList unavailable.Download the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nGirl in Disguise Hardcover \\u2013 March 21, 2017\\n\\nPurchase options and add-ons\\n\\nFrom the USA Today Bestselling author of The Magician's Lie\\n\\n\\\"Macallister is becoming a leading voice in strong, female-driven historical fiction. Exciting, frightening, and unspeakably moving...\\\"\\u2015Erika Robuck, bestselling author of Hemingways's Girl\\n\\nFor the first daring female Pinkerton detective, respect is hard to come by, but danger and spies are everywhere.\\n\\nIn the tumultuous years of the Civil War, the streets of Chicago offer a woman mostly danger and ruin\\u2015unless that woman is Kate Warne. As an undercover Pinkerton detective, Kate is able to infiltrate the seedy side of the city in disguises that her fellow spies just can't manage. She's a seductress, an exotic foreign medium, a rich train passenger\\u2015all depending on the day and the robber, thief, or murderer she's been assigned to nab.\\n\\nBut is it only her detective work that makes her a daring spy and a clever liar? Or is the real disguise the good girl she always thought she was? As the Civil War marches closer, Kate takes on her most pressing job\"\n },\n {\n \"docid\": \"45315\",\n \"score\": 0.6723828911781311,\n \"snippet\": \"---\\ntitle: America's First Female Detective Was a Chicago 'Girl in Disguise'\\nauthor: Karen Abbott\\ndate: 2018-05-21\\n---\\nGreer Macallister's most recent novel, Girl In Disguise, based on the real-life first woman detective in the U.S., is now out in paperback. Girl In Disguise was inspired by Kate Warne, who was hired by Allan Pinkerton in 1856 Chicago to solve cases and fight crime.\\n\\nI recently sat down with Greer to discuss the facts and fiction behind her version of this groundbreaking detective, how she researched Kate and her contemporaries, and some of her recent reads.\\n\\nKaren Abbott\\n\\nWhat can you tell us about your research process? There is not much information about Kate Warne, and certainly not much primary source material. Did you find anything surprising that shed light on her character, or is she mostly a figment of your imagination?\\n\\nGreer Macallister\\n\\nI tell people that this particular work of historical fiction is a little history and a lot of fiction. You're right that provable historical information on Kate is hard to come by. If I were a biographer I would have thrown up my hands in defeat after I visited the Pinkerton Agency archives in the Library of Congress and it didn't even take me a full day to locate and review every document that named Kate. Instead I decided that for a historical novelist, those gaps in the record were invitations, and I'd unwittingly found just the right subject for a novel-length fictional treatment. A nonfiction account of Kate that stuck to known facts would be an article at most \\u2014 and much of that would be\\n\\ndrawn from Allan Pinkerton's books, stretching the definition of \\\"known facts,\\\" given that he's not always reliable either.\\n\\nI tried to use my imagination to interpret and extrapolate from what we know instead of substituting for it. We know she didn't have children, but not how she felt about it, so I had to imagine what her reasoning was behind that choice, or whether it was a choice at all. She was likely a widow \\u2014 Pinkerton said she was \\u2014 so I spun a story around that too. She must have been a talented actress with a gift for mimicking accents, given that she was able to blend in with native Southerners when she went undercover in their midst as a woman from Alabama. From all the \\\"musts\\\" and \\\"likelies,\\\" I wove my version\"\n },\n {\n \"docid\": \"53914\",\n \"score\": 0.5337740182876587,\n \"snippet\": \"---\\ntitle: Search This Blog\\nauthor: Sarah Johnson\\ndate: 2014-08-29\\n---\\nThese novels aren't fanciful in premise. In actuality, there were many women who disguised their sex and fought in the US Civil War and in earlier battles, but recognition of and pride in their accomplishments has often been long in coming. These works of fiction, some of which are based on the lives of specific historical women, help to spread word about their deeds and heroism in the popular consciousness.\\n\\nA young woman who had been fighting for the Union in disguise has to hide her loyalties after she's wounded and gets trapped behind Confederate lines. RiverNorth, June 2014.\\n\\nA rare novel that looks at this scenario from the Confederate side, as two Southern sisters enlist in the Confederate army as new recruits, their secret known only to one another. The co-authors are sisters as well. Houghton Mifflin Harcourt, March 2015.\\n\\nHer husband being too weak to go to war, an Indiana farm wife dons male garb and marches off to fight for the Union. I'll have a review of this new literary novel shortly. Little Brown, September 2014.\\n\\nBelieving her place is with her newly-wed husband, Rosetta Wakefield secretly follows him into the Union ranks, fighting alongside him and proving her worth in battle. Loosely based on the life of Sarah Rosetta Wakeman. Crown, January 2014; out in paperback in September, with a beautiful new cover.\\n\\nThis novel about Massachusetts heroine Deborah Sampson shows her external and internal transformations during her service in the Revolutionary War. See my review of Revolutionary as well as Alex Myers' guest post here. Simon & Schuster, January 2014.\\n\\nFrom the author of the 4-book Far Western Civil War series comes a new novel about Emma Edmonds, who signed on with the 2nd Michigan Volunteers under the name Frank Thompson. BookView Cafe, April 2014.\\n\\nOne of my secondary characters in my Civil War novels Promise & Honor and Honor & Glory disguised herself. I had a reviewer on Amazon say that she liked the character but found it unbelievable that a woman could succeed in hiding herself that way.\\n\\nReplyDeleteI would love to write a novel on an unknown female soldier that I found in an article from an 1863 Missouri Democrat. I've blogged about her on a couple of\"\n },\n {\n \"docid\": \"78562\",\n \"score\": 0.5296750068664551,\n \"snippet\": \"---\\ntitle: Disguised or living as a man (Sapphic Reading Challenge #33)\\ndate: 2021-08-26\\n---\\nThis week's category of the Sapphic Reading Challenge features books about main characters who are either women who disguise themselves as men out of necessity or genderqueer people who were assigned female at birth but might identify as nonbinary or as trans men (even though the book might not use those terms since most books on this list are historical romances).\\n\\nRead up on the rules of the Sapphic Reading Challenge\\n\\nIf you are only discovering the Sapphic Reading Challenge now, the good news is you can still join! It's a year-long challenge, so it runs until December 31, 2021. Read up on the rules of the Sapphic Reading Challenge and download your PDF so you can keep track of the books you read this year.\\n\\n15 sapphic books featuring main characters who are disguised or living as men\\n\\nI put together a list of 15 sapphic books featuring a character who lives their life as a man at least for major parts of the book. I hope you'll find at least one you'll enjoy.\\n\\nBackwards to Oregon by Jae\\n\\nLuke Hamilton has always been sure that she'd never marry. She accepted that she would spend her life alone when she chose to live her life disguised as a man.\\n\\nAfter working in a brothel for three years, Nora Macauley has lost all illusions about love. She no longer hopes for a man who will sweep her off her feet and take her away to begin a new, respectable life.\\n\\nBut now they find themselves married and on the way to Oregon in a covered wagon, with two thousand miles ahead of them.\\n\\nAvailable at:\\n\\nCrossing the Wide Forever by Missouri Vaun\\n\\nCody Walsh leaves Arkansas for California. Lured by stories of opportunity, even for women, Cody disguises herself as a man and prepares for the arduous journey west.\\n\\nLillie Ellis leaves New York to accept a post as a schoolteacher on the frontier near a small homestead she just inherited from her uncle. Lillie's ultimate desire is to become a painter, and she hopes the Kansas frontier will offer her the freedom to follow that dream. In the nineteenth century, a young woman has few options in the East that don't revolve around marriage and motherhood. Lillie is interested in neither.\\n\\nCody rescues Lill\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "6c30154a-9867-4038-b80b-69091e75d611", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.24781500000000004, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 7, "action": {"name": "search", "arguments": {"query": "19th century London novel woman man disguise businessman disastrous deal stolen document accusation"}, "id": "e597d7c7-bfa3-4158-8d45-bf01379bba4d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.358665, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 7, "observation": {"result": "[\n {\n \"docid\": \"19305\",\n \"score\": 0.6351392865180969,\n \"snippet\": \"---\\ntitle: Ten of the best disguises in literature\\nauthor: John Mullan\\ndate: 2010-09-25\\n---\\nThe Odyssey, by Homer\\n\\nOdysseus arrives back at his island of Ithaca disguised as a beggar. He is recognised only by his old dog Argus (animals always see through disguises), which dies of joy on the spot. In his disguise, our hero is able to see who has been loyal to him and who has not.\\n\\nMeasure for Measure, by William Shakespeare\\n\\nThe Duke who governs Vienna wants to see what his underlings will get up to in his absence. So he asks his friend Friar Thomas for some monkish garb: \\\"Supply me with the habit and instruct me / How I may formally in person bear me / Like a true friar\\\". It works, and not even his most devoted courtiers recognise him until he finally unveils himself.\\n\\nThe Monk, by Matthew Lewis\\n\\nAnother monkish disguise. Sexy young Matilda lusts after Father Ambrosio, the most pious monk in Madrid. So she dresses up as a young novice monk and finds her way into the monastery. In her cell she reveals herself to Ambrosio, who cannot resist her charms. It turns out that she is in fact a demon.\\n\\nJane Eyre, by Charlotte Bront\\u00eb\\n\\nOne of the great episodes of transvestism in literature comes when Rochester togs himself up as a Gypsy woman to read the palms of the guests he has invited to Thornfield. Blanche Ingram, Jane's rival for his affections, gets uncomforting news, but Jane is told \\\"the cup of bliss\\\" is going to be offered to her.\\n\\nEast Lynne, by Mrs Henry Wood\\n\\nLady Isabel Vane loses her happy home and family when she conducts an adulterous affair with the utterly caddish Francis Levinson. Having learned the error of her ways, she returns to be governess to her own children, disguised by blue-lensed glasses, hair turned white from shock after a train crash and a scarred mouth.\\n\\nThe Mystery of Edwin Drood, by Charles Dickens\\n\\nDick Datchery arrives in the town of Cloisterham, apparently a detective in disguise (he wears a wig). He (or she?) keeps watch over John Jasper, choirmaster and secret drug addict. Drood has disappeared: is he disguised as Datchery? Or is it another character, investigating Drood's murder? Dickens did\"\n },\n {\n \"docid\": \"72751\",\n \"score\": 0.5329743027687073,\n \"snippet\": \"---\\ntitle: 5 Powerful Examples of Disguise As A Device In Fiction\\ndate: 2020-04-14\\n---\\nIn this blog, we show you five examples of disguise as a device in fiction. The examples from classic and modern literature explain how writers have used disguise in their stories to remarkable effect.\\n\\n5 Powerful Examples of Disguise As A Device In Fiction\\n\\nYou will discover:\\n\\n- How Shakespeare mastered disguise in his plays.\\n\\n- How Charlotte Bronte showed another side to her imposing hero Rochester.\\n\\n- How Virginia Woolf smashed gender stereotypes in her feminist masterpiece Orlando.\\n\\n- How Anne Fine helped a divorced father connect with his family in Madame Doubtfire.\\n\\n- How Anne Rice showed us human weakness and desire through immortal beings in Interview With The Vampire.\\n\\nWhy Use Disguise In Your Fiction?\\n\\nWe all know that disguise is a common storytelling device. It can be used to express a theme, illumine a character, or drive a plot. When a character in a short story, script, or novel disguises their identity, they could be seeking a truth or revealing a deceit.\\n\\nThe obfuscation always has a reason that fits in with the plot or theme of the story.\\n\\nAs a writer, you can use this sort of clever concealment or literary trickery to subvert stereotypes or play with readers' expectations. It is a powerful resource available to you as a writer.\\n\\nIt is also, I'm sure you will agree, a lot of fun to write.\\n\\nBrilliant Disguise\\n\\nWhile a superhero may wear a mask, he wears it as a concealment. A mask seems to 'seal' or close off an identity \\u2013 whereas a disguise is more about taking on or absorbing another identity, sometimes more than one identity, without abandoning a core character or personality. It's a form of character camouflage that is important to the story or a character's inner transformation.\\n\\n1. Mastering Disguise\\n\\nRecently, I was reading about Giuseppe Dosi. In the 1920s, he was possibly one of Italy's greatest detectives, so much so that he could have easily been a fictional creation.\\n\\nThis former actor was a master of disguises and he used disguise as a method to solve crime. In fact, his approach became known as detectival transformation. His personas included a femme fatal, priest, banker, and a war veteran with a 'bum' leg. All his identities had their own fake IDs and penmanship.\\n\\nShakespeare was, of course\"\n },\n {\n \"docid\": \"95934\",\n \"score\": 0.5266444683074951,\n \"snippet\": \"---\\ntitle: What's the Name of That Book??? discussion\\nauthor: Author add link photo\\ndate: 2017-09-06\\n---\\nWhat's the Name of That Book??? discussion\\n\\n\\u25ba Suggest books for me\\n\\n>\\n\\nWoman disguised as a man\\n\\nYou might try looking through the whole \\\"Suggest books for me\\\" folder. There are multiple threads with this theme.\\n\\nThe Masqueraders this one has it all--sister disguised as a man and brother disguised as a woman. What more can anyone ask, right? It is also by Georgette Heyer which means it's almost guaranteed to be good.\\n\\nUnmasking Miss Appleby - this Regency fantasy romance has the heroine using actual magic to turn herself into a man as opposed to dressing as one, which paradoxically makes the charade a lot more believable.\\n\\nThe protagonist of The Third Witch, a young girl, disguises herself as a serving boy to infiltrate the castle of her enemy, Macbeth (of the Shakespeare play).\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nRidiculous by D.L. Carter. Regency era. Woman dresses as a man to save her family from financial ruin.\\n\\nThe Horse and His Boy by C. S. Lewis has someone doing this, and she's an important character, although not the protagonist. The Horse and His Boy\\n\\nSisters of Shiloh by Kathy Hepinstall features 2 sisters who disguise themselves as men and join the Confederate Army\\u2014one seeking vengeance on the battlefield, the other finding love.\\n\\nI\\n\\nI\\n\\nPaladin by Sally Slater.\\n\\nthe h is not your typical maria clara. she enjoys sword work and is good at it. in order to join a quest, she has to pretend to be a boy, and she is on par with other males in terms of one on one fights. the H is a team mate on this quest, one of the 1st to diacover her secret, he is scarred emotionally.\\n\\nthe h is not your typical maria clara. she enjoys sword work and\"\n },\n {\n \"docid\": \"65464\",\n \"score\": 0.5240164995193481,\n \"snippet\": \"---\\ntitle: Top 30: Historical Romances Where the Heroine Masquerades as a Man\\nauthor: Anne\\ndate: 2020-06-12\\n---\\nI love a good masquerade Regency. If you look on the Books by Plot Type, which categorizes books we have reviewed by general plot or trope, you will see its one of my largest trope categories\\u2026for a reason (it means I read them a lot because I love this trope a lot!). But I haven't distinguished the type of masquerade. Per reader request, I have compiled a list of some of my favorite (and reader favorite) Regencies where the heroine masquerades as a man.\\n\\nIn no particular order, the book covers are linked to Amazon for purchase (and your purchase helps sponsor the blog) and where the author and title are highlighted, you can click to read a Regency Reader review.\\n\\nSarah MacLean: Never Judge a Lady by Her Cover. By day she is Lady Georgiana, a Duke's sister, and by night a founder of London's most notorious gaming hell.\\n\\nLiana De la Rosa: To Tame a Scandalous Lady The best way to learn about horse breeding is to masquerade as the Earl's assistant horse trainer\\u2026even if that means giving up the luxuries of her former life.\\n\\nDarcy Burke: One Night of Scandal The Duke's sister has a side hustle as a male gossip columnist that is threatened by a sexy MP's discovery of her ruse.\\n\\nCat Sebastian: Unmasked by the Marquess. Readers call it Sebastian's take on Heyer's Frederica, with a non-binary MC who presents as both man and woman.\\n\\nEloisa James: Duchess By Night An older title, this features a heroine masquerading as a rake.\\n\\nGeorgette Heyer: The Corinthian An OG Regency featuring a heroine who cross-dresses to escape villainous family members and runs into a Corinthian.\\n\\nMary Jo Putney: Silk and Secrets A Victorian heroine seeks adventure as a man, but must reunite with her estranged husband to rescue her missing brother.\\n\\nSarah MacLean: Nine Rules to Break When Romancing a Rake. Heroine decides to break some rules and explore what rakes get to do.\\n\\nLynsay Sands: The Switch. Escaping a villainous Uncle, twins take turns masquerading as the other's brother.\\n\\nMary Jo Putney: The Rake Heroine masquerading as a steward\"\n },\n {\n \"docid\": \"29367\",\n \"score\": 0.5197827816009521,\n \"snippet\": \"---\\ntitle: Historical Fictionistas discussion\\nauthor: Author add link photo\\ndate: 2012-10-11\\n---\\nHistorical Fictionistas discussion\\n\\nRecommendations?\\n\\n>\\n\\nCrime in 19th Century London?\\n\\ndate\\n\\nnewest \\u00bb\\n\\nmessage 1:\\n\\nby\\n\\nAllison\\n\\n(new)\\n\\nOct 11, 2012 07:57AM\\n\\nHey you guys...I would love some recommendations on books set in 19th century London revolving around crime (other than Jack the Ripper). Fiction and non-fiction would both be great!\\n\\nreply\\n\\n|\\n\\nflag\\n\\nAnything by Anne Perry in the Inspector Monk and the Thomas Pitt series. They start with The Face of a Stranger and The Cater Street Hangman, respectively. Monk is 1860s and Pitt somewhat later.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDavid Liss's Benjamin Weaver series is my favorite. It begins with A Conspiracy of Paper, a HF murder/mystery/thriller novel about a financial crisis in 19th century London. You'll love Benjamin and learn a lot about economics (without being bored to tears) because Liss is such a master at inserting historical details in creative ways. David Liss\\n\\nI love historical mysteries, so here's a few of my favorites set in London in the 19th century:\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of aristocratic series:\\n\\nC.S. Harris, Regency era with the first in the series being What Angels Fear\\n\\nCharles Finch,A Beautiful Blue Death is first in the series\\n\\nEarly forensics:\\n\\nDevoured by D.E. Meredith\\n\\nEnjoy!\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of arist\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "e597d7c7-bfa3-4158-8d45-bf01379bba4d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.358665, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 8, "action": {"name": "search", "arguments": {"query": "Victorian novel sea storm rescue climax encrypted document spy villain send abroad"}, "id": "1e828f5c-c72f-4eb9-b664-ebd9dedab014", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.48636, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 8, "observation": {"result": "[\n {\n \"docid\": \"77939\",\n \"score\": 0.5182424783706665,\n \"snippet\": \"---\\ntitle: 12 Historical Fiction Books That Bring the Victorian Era to Life\\ndate: 2024-07-09\\n---\\nFeatures summary\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life\\n\\nImmerse yourself in the Victorian era with these 12 captivating historical fiction novels. Each book brings to life the complexities, challenges, and beauty of the period, offering a detailed look at the social, cultural, and technological changes of the time.\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life (Picture Credit - Instagram)\\n\\nThe Victorian era, spanning from 1837 to 1901, was a period of significant social, cultural, and technological change. This era saw the rise of the British Empire, industrial advancements, and shifts in societal norms. Historical fiction set in this time offers a captivating glimpse into the past, blending rich storytelling with historical details. Here are 12 historical fiction books that beautifully capture the essence of the Victorian era.\\n\\n1. The Crimson Petal and the White by Michel Faber\\n\\nMichel Faber's 'The Crimson Petal and the White' provides a vivid and gritty portrayal of Victorian London. The novel follows Sugar, a determined and intelligent prostitute, as she navigates the city's dark underbelly and attempts to climb the social ladder. Faber's meticulous attention to historical detail and complex characters make this a compelling read that immerses readers in the era's harsh realities and opulent surroundings.\\n\\n2. Fingersmith by Sarah Waters\\n\\n'Fingersmith' by Sarah Waters is a riveting tale of deception, betrayal, and unexpected twists. Set in Victorian England, the story revolves around Sue Trinder, a young woman raised among thieves, and her involvement in a con to defraud a wealthy heiress. Waters' masterful storytelling and rich depiction of Victorian society's class divides and criminal underworld makes this novel a gripping exploration of love and trust.\\n\\n3. The Essex Serpent by Sarah Perry\\n\\nSarah Perry's 'The Essex Serpent' is a beautifully written novel that blends historical fiction with gothic elements. Set in 1893, the story follows Cora Seaborne, a widow with a passion for natural history, as she moves to Essex and investigates rumours of a mythical serpent. Perry's evocative prose and exploration of themes like faith, science, and friendship capture the intellectual and societal currents of the Victorian era.\\n\\n4. The Light Between Oceans by M.L. Sted\"\n },\n {\n \"docid\": \"67868\",\n \"score\": 0.5018195509910583,\n \"snippet\": \"---\\ntitle: 25 of the best spy thrillers\\ndate: 2024-01-09\\n---\\nBooks\\n\\n25 of the best spy thrillers\\n\\nWe can't resist a dip into the mysterious world of the spy thriller. With secrets, lies, conspiracies and undercover plots abound, these books span the national and the international, blending the personal and the political and showing how they are inextricably linked.\\n\\nFrom the classic to the contemporary, here are some of the best spy thrillers around.\\n\\nThe best spy thriller books\\n\\nCasino Royale by Ian Fleming\\n\\nCasino Royale by Ian Fleming\\n\\nIn Casino Royale, the first of Ian Fleming's 007 adventures, a game of cards is James Bond's only chance to bring down Le Chiffre, French communist and paymaster of the Soviet murder organization SMERSH. But Bond soon discovers that there is far more at stake than money.\\n\\nThis is a book that's packed will thrills and suspense. It's an incredibly satisfying read that showcases everything we know and love about the Bond stories \\u2013 chilling, sadistic villains, sensuous, fiery love affairs and the smooth, sophisticated secret agent himself.\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nMaggie Bird lives a quiet life as a retiree in a seaside town. She never talks about her past life as a former spy. But when an unidentified body is left on Maggie's doorway, she knows it's a calling card from old times. Enter the 'Martini Club' \\u2013 Maggie's group of former spy friends. Together, they must solve the mystery and stay one step ahead of law enforcement.\\n\\nThis is the first instalment in a brand-new espionage series from Tess Gerritsen, author of the beloved Rizzoli and Isles detective series.\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThis book by former RAF pilot and investigative journalist Frederick Forsyth is one of the most celebrated thrillers ever written. It's intelligent, chilling and 100% unputdownable.\\n\\nIt is 1963 and an anonymous Englishman has been hired by the O.A.S. to murder General de Galle. A failed attempt means the target will be nearly impossible to get to. But this latest plot involves a lethal weapon: an assassin of legendary talent. Known only as The Jackal, this remorseless and deadly killer must be stopped \\u2013 but\"\n },\n {\n \"docid\": \"42402\",\n \"score\": 0.49579551815986633,\n \"snippet\": \"---\\ntitle: The Scarlet Pimpernel - Wikipedia\\nauthor: Authority control databases\\ndate: 2002-09-10\\n---\\nname: The Scarlet Pimpernel\\nauthor: Baroness Orczy\\ncountry: United Kingdom\\nlanguage: English\\ngenre: Historical fiction, adventure fiction\\nset_in: 1792, during the early stages of the French Revolution\\npublisher: Greening\\npub_date: 1905\\npages: 319\\npreceded_by: The First Sir Percy\\nfollowed_by: Sir Percy Leads the Band\\n\\nThe Scarlet Pimpernel is the first novel in a series of historical fiction by Baroness Orczy, published in 1905. It was written after her stage play of the same title (co-authored with her husband Montague Barstow) enjoyed a long run in London, having opened in Nottingham in 1903.\\n\\nThe novel is set during the Reign of Terror following the start of the French Revolution. The title is the nom de guerre of its hero and protagonist, a chivalrous Englishman who rescues aristocrats before they are sent to the guillotine. Sir Percy Blakeney leads a double life: apparently nothing more than a wealthy fop, but in reality, a formidable swordsman and a quick-thinking master of disguise and escape artist. The band of gentlemen who assist him are the only ones who know of his secret identity. He is known by his symbol, a simple flower, the scarlet pimpernel (Anagallis arvensis).\\n\\nOpening at the New Theatre in London's West End on 5 January 1905, the play became a favourite of British audiences, eventually playing more than 2,000 performances and becoming one of the most popular shows staged in London. Published after the success of the play, the novel was an immediate success, gaining Orczy a following of readers in Britain and the rest of the world. The stage play and subsequent novel, with their hero and villain, were so popular that they inspired a revival of classic villainy at the time.\\n\\nOrczy's premise of a daring hero who cultivates a secret identity disguised by a meek or ineffectual manner proved enduring. Zorro, Doctor Syn, the Shadow, the Spider, the Green Hornet, the Phantom, Superman and Batman followed within a few decades, and the trope remains a popular one in serial fiction today. Read by\"\n },\n {\n \"docid\": \"36824\",\n \"score\": 0.49507761001586914,\n \"snippet\": \"---\\ntitle: Kidnapped (novel) - Wikipedia\\nauthor: Authority control databases National Germany Spain\\ndate: 2005-01-30\\n---\\nname: Kidnapped\\nauthor: Robert Louis Stevenson\\ncountry: Scotland\\nlanguage: Victorian era Scottish English, Lowland Scots, Highland English\\ngenre: Adventure novelHistorical novel\\npublisher: Cassell and Company Ltd\\nrelease_date: 1886\\npages: 136\\ndewey: 823/.8 21\\ncongress: PR5484 .K5 2000\\noclc: 43167976\\nfollowed_by: Catriona (1893)\\n\\nKidnapped is a historical fiction adventure novel by Scottish author Robert Louis Stevenson, written as a boys' novel and first published in the magazine Young Folks from May to July 1886. The novel has attracted the praise and admiration of writers as diverse as Henry James, Jorge Luis Borges, and Hilary Mantel. A sequel, Catriona, was published in 1893.\\n\\nThe narrative is written in English with some dialogue in Lowland Scots, a Germanic language that evolved from an earlier incarnation of English.\\n\\nKidnapped is set around real 18th-century Scottish events, notably the \\\"Appin Murder\\\" and the Highland Clearances, which occurred in the aftermath of the Jacobite rising of 1745. Many of the characters are real people, including one of the principals, Alan Breck Stewart. The political situation of the time is portrayed from multiple viewpoints, and the Scottish Highlanders are treated sympathetically.\\n\\nThe full title of the book is Kidnapped: Being Memoirs of the Adventures of David Balfour in the Year 1751: How he was Kidnapped and Cast away; his Sufferings in a Desert Isle; His Journey in the Wild Highlands; his acquaintance with Alan Breck Stewart and other notorious Highland Jacobites; with all that he suffered at the hands of his Uncle, Ebenezer Balfour of Shaws, falsely so-called: Written by Himself and now set forth by Robert Louis Stevenson.\\n\\nPlot\\n\\nThe novel opens in the (fictional) village of Essendean in the Ettrick Forest area of the Scottish Lowlands in 1751. The main character and narrator is 17-year-old David Balfour\"\n },\n {\n \"docid\": \"48916\",\n \"score\": 0.4882397949695587,\n \"snippet\": \"---\\ntitle: Plotting the Mystery Novel\\n---\\n|\\n\\n|\\n\\nPlotting the Mystery NovelThe classic mystery is popular fiction which follows a specific formula. Clever writers may try to change the formula, but the most clever will cling to it for a very good reason. They work within the bounds of the formula because it works! The following outline serves the modern mystery novel, as defined by editors and publishers. A typical story will contain 60,000 to 65,000 words (205 manuscript pages) and will be divided into 12 chapters, each approximately 17 pages in length.\\n\\nThe Classic 12-Chapter Mystery Formula\\n\\nAct IIntroduction of the crime (mystery) and the sleuth Chapter 1 A. Disclose the crime and mystery to be solved. The crime must capture the imagination. It should have been committed in an extraordinary way and either the victim the perpetuator, or both, should be unusual. Give the reader enough information about the victim to make them truly care that the perpetrator is found out and that justice is served. B. Early in the story, clues should be revealed which suggest both physical and psychological aspects of the initial crime. Those clues should point to suspects and motive which will cary the sleuth to the end of Act I. Some clues should point the sleuth in the right direction, others may not be obvious or be recognized as actual clues unto later in the story. C. Introduce the sleuth who will solve the crime early, and have him or her do or say something very clever or unexpected which will establish that person as unique. Create this character with care. His or her personality should be interesting enough to sustain the interest of the reader to the very last page. (or through an entire series of books). It is not necessary to disclose all aspects of the sleuth's personality at the onset. Let the description unfold gradually to sustain interest. Do reveal enough background to let the reader understand the world in which the protagonist functions. (Small town sheriff, Scotland Yard detective, Pinkerton agent in the old West, country squire, investigative reporter in New York City, etc.) D. Ground the reader in the time and place where the crime occurs. It is often useful to include some sort of symbol, an object or a person, in the opening scene which serves as a metaphor for what occurs in the story. The reappearance of this symbol at the conclusion of the story will create a certain organic unity. E. Begin\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "1e828f5c-c72f-4eb9-b664-ebd9dedab014", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.48636, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 9, "action": {"name": "search", "arguments": {"query": "\"woman disguised as man\" Victorian London accused theft businessman rescue storm ship"}, "id": "3061017b-47d1-47e6-a98d-70e4eff41105", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.6306100000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 9, "observation": {"result": "[\n {\n \"docid\": \"19305\",\n \"score\": 0.6136258840560913,\n \"snippet\": \"---\\ntitle: Ten of the best disguises in literature\\nauthor: John Mullan\\ndate: 2010-09-25\\n---\\nThe Odyssey, by Homer\\n\\nOdysseus arrives back at his island of Ithaca disguised as a beggar. He is recognised only by his old dog Argus (animals always see through disguises), which dies of joy on the spot. In his disguise, our hero is able to see who has been loyal to him and who has not.\\n\\nMeasure for Measure, by William Shakespeare\\n\\nThe Duke who governs Vienna wants to see what his underlings will get up to in his absence. So he asks his friend Friar Thomas for some monkish garb: \\\"Supply me with the habit and instruct me / How I may formally in person bear me / Like a true friar\\\". It works, and not even his most devoted courtiers recognise him until he finally unveils himself.\\n\\nThe Monk, by Matthew Lewis\\n\\nAnother monkish disguise. Sexy young Matilda lusts after Father Ambrosio, the most pious monk in Madrid. So she dresses up as a young novice monk and finds her way into the monastery. In her cell she reveals herself to Ambrosio, who cannot resist her charms. It turns out that she is in fact a demon.\\n\\nJane Eyre, by Charlotte Bront\\u00eb\\n\\nOne of the great episodes of transvestism in literature comes when Rochester togs himself up as a Gypsy woman to read the palms of the guests he has invited to Thornfield. Blanche Ingram, Jane's rival for his affections, gets uncomforting news, but Jane is told \\\"the cup of bliss\\\" is going to be offered to her.\\n\\nEast Lynne, by Mrs Henry Wood\\n\\nLady Isabel Vane loses her happy home and family when she conducts an adulterous affair with the utterly caddish Francis Levinson. Having learned the error of her ways, she returns to be governess to her own children, disguised by blue-lensed glasses, hair turned white from shock after a train crash and a scarred mouth.\\n\\nThe Mystery of Edwin Drood, by Charles Dickens\\n\\nDick Datchery arrives in the town of Cloisterham, apparently a detective in disguise (he wears a wig). He (or she?) keeps watch over John Jasper, choirmaster and secret drug addict. Drood has disappeared: is he disguised as Datchery? Or is it another character, investigating Drood's murder? Dickens did\"\n },\n {\n \"docid\": \"95934\",\n \"score\": 0.5420852899551392,\n \"snippet\": \"---\\ntitle: What's the Name of That Book??? discussion\\nauthor: Author add link photo\\ndate: 2017-09-06\\n---\\nWhat's the Name of That Book??? discussion\\n\\n\\u25ba Suggest books for me\\n\\n>\\n\\nWoman disguised as a man\\n\\nYou might try looking through the whole \\\"Suggest books for me\\\" folder. There are multiple threads with this theme.\\n\\nThe Masqueraders this one has it all--sister disguised as a man and brother disguised as a woman. What more can anyone ask, right? It is also by Georgette Heyer which means it's almost guaranteed to be good.\\n\\nUnmasking Miss Appleby - this Regency fantasy romance has the heroine using actual magic to turn herself into a man as opposed to dressing as one, which paradoxically makes the charade a lot more believable.\\n\\nThe protagonist of The Third Witch, a young girl, disguises herself as a serving boy to infiltrate the castle of her enemy, Macbeth (of the Shakespeare play).\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nToward the end of Dating Hamlet: Ophelia's Story, Ophelia dresses as a boy to reenter Elsinore after faking her death.\\n\\nLove Disguised also features a girl dressing up as a man.\\n\\nRidiculous by D.L. Carter. Regency era. Woman dresses as a man to save her family from financial ruin.\\n\\nThe Horse and His Boy by C. S. Lewis has someone doing this, and she's an important character, although not the protagonist. The Horse and His Boy\\n\\nSisters of Shiloh by Kathy Hepinstall features 2 sisters who disguise themselves as men and join the Confederate Army\\u2014one seeking vengeance on the battlefield, the other finding love.\\n\\nI\\n\\nI\\n\\nPaladin by Sally Slater.\\n\\nthe h is not your typical maria clara. she enjoys sword work and is good at it. in order to join a quest, she has to pretend to be a boy, and she is on par with other males in terms of one on one fights. the H is a team mate on this quest, one of the 1st to diacover her secret, he is scarred emotionally.\\n\\nthe h is not your typical maria clara. she enjoys sword work and\"\n },\n {\n \"docid\": \"72751\",\n \"score\": 0.5347636342048645,\n \"snippet\": \"---\\ntitle: 5 Powerful Examples of Disguise As A Device In Fiction\\ndate: 2020-04-14\\n---\\nIn this blog, we show you five examples of disguise as a device in fiction. The examples from classic and modern literature explain how writers have used disguise in their stories to remarkable effect.\\n\\n5 Powerful Examples of Disguise As A Device In Fiction\\n\\nYou will discover:\\n\\n- How Shakespeare mastered disguise in his plays.\\n\\n- How Charlotte Bronte showed another side to her imposing hero Rochester.\\n\\n- How Virginia Woolf smashed gender stereotypes in her feminist masterpiece Orlando.\\n\\n- How Anne Fine helped a divorced father connect with his family in Madame Doubtfire.\\n\\n- How Anne Rice showed us human weakness and desire through immortal beings in Interview With The Vampire.\\n\\nWhy Use Disguise In Your Fiction?\\n\\nWe all know that disguise is a common storytelling device. It can be used to express a theme, illumine a character, or drive a plot. When a character in a short story, script, or novel disguises their identity, they could be seeking a truth or revealing a deceit.\\n\\nThe obfuscation always has a reason that fits in with the plot or theme of the story.\\n\\nAs a writer, you can use this sort of clever concealment or literary trickery to subvert stereotypes or play with readers' expectations. It is a powerful resource available to you as a writer.\\n\\nIt is also, I'm sure you will agree, a lot of fun to write.\\n\\nBrilliant Disguise\\n\\nWhile a superhero may wear a mask, he wears it as a concealment. A mask seems to 'seal' or close off an identity \\u2013 whereas a disguise is more about taking on or absorbing another identity, sometimes more than one identity, without abandoning a core character or personality. It's a form of character camouflage that is important to the story or a character's inner transformation.\\n\\n1. Mastering Disguise\\n\\nRecently, I was reading about Giuseppe Dosi. In the 1920s, he was possibly one of Italy's greatest detectives, so much so that he could have easily been a fictional creation.\\n\\nThis former actor was a master of disguises and he used disguise as a method to solve crime. In fact, his approach became known as detectival transformation. His personas included a femme fatal, priest, banker, and a war veteran with a 'bum' leg. All his identities had their own fake IDs and penmanship.\\n\\nShakespeare was, of course\"\n },\n {\n \"docid\": \"78562\",\n \"score\": 0.5340186357498169,\n \"snippet\": \"---\\ntitle: Disguised or living as a man (Sapphic Reading Challenge #33)\\ndate: 2021-08-26\\n---\\nThis week's category of the Sapphic Reading Challenge features books about main characters who are either women who disguise themselves as men out of necessity or genderqueer people who were assigned female at birth but might identify as nonbinary or as trans men (even though the book might not use those terms since most books on this list are historical romances).\\n\\nRead up on the rules of the Sapphic Reading Challenge\\n\\nIf you are only discovering the Sapphic Reading Challenge now, the good news is you can still join! It's a year-long challenge, so it runs until December 31, 2021. Read up on the rules of the Sapphic Reading Challenge and download your PDF so you can keep track of the books you read this year.\\n\\n15 sapphic books featuring main characters who are disguised or living as men\\n\\nI put together a list of 15 sapphic books featuring a character who lives their life as a man at least for major parts of the book. I hope you'll find at least one you'll enjoy.\\n\\nBackwards to Oregon by Jae\\n\\nLuke Hamilton has always been sure that she'd never marry. She accepted that she would spend her life alone when she chose to live her life disguised as a man.\\n\\nAfter working in a brothel for three years, Nora Macauley has lost all illusions about love. She no longer hopes for a man who will sweep her off her feet and take her away to begin a new, respectable life.\\n\\nBut now they find themselves married and on the way to Oregon in a covered wagon, with two thousand miles ahead of them.\\n\\nAvailable at:\\n\\nCrossing the Wide Forever by Missouri Vaun\\n\\nCody Walsh leaves Arkansas for California. Lured by stories of opportunity, even for women, Cody disguises herself as a man and prepares for the arduous journey west.\\n\\nLillie Ellis leaves New York to accept a post as a schoolteacher on the frontier near a small homestead she just inherited from her uncle. Lillie's ultimate desire is to become a painter, and she hopes the Kansas frontier will offer her the freedom to follow that dream. In the nineteenth century, a young woman has few options in the East that don't revolve around marriage and motherhood. Lillie is interested in neither.\\n\\nCody rescues Lill\"\n },\n {\n \"docid\": \"65464\",\n \"score\": 0.5245835781097412,\n \"snippet\": \"---\\ntitle: Top 30: Historical Romances Where the Heroine Masquerades as a Man\\nauthor: Anne\\ndate: 2020-06-12\\n---\\nI love a good masquerade Regency. If you look on the Books by Plot Type, which categorizes books we have reviewed by general plot or trope, you will see its one of my largest trope categories\\u2026for a reason (it means I read them a lot because I love this trope a lot!). But I haven't distinguished the type of masquerade. Per reader request, I have compiled a list of some of my favorite (and reader favorite) Regencies where the heroine masquerades as a man.\\n\\nIn no particular order, the book covers are linked to Amazon for purchase (and your purchase helps sponsor the blog) and where the author and title are highlighted, you can click to read a Regency Reader review.\\n\\nSarah MacLean: Never Judge a Lady by Her Cover. By day she is Lady Georgiana, a Duke's sister, and by night a founder of London's most notorious gaming hell.\\n\\nLiana De la Rosa: To Tame a Scandalous Lady The best way to learn about horse breeding is to masquerade as the Earl's assistant horse trainer\\u2026even if that means giving up the luxuries of her former life.\\n\\nDarcy Burke: One Night of Scandal The Duke's sister has a side hustle as a male gossip columnist that is threatened by a sexy MP's discovery of her ruse.\\n\\nCat Sebastian: Unmasked by the Marquess. Readers call it Sebastian's take on Heyer's Frederica, with a non-binary MC who presents as both man and woman.\\n\\nEloisa James: Duchess By Night An older title, this features a heroine masquerading as a rake.\\n\\nGeorgette Heyer: The Corinthian An OG Regency featuring a heroine who cross-dresses to escape villainous family members and runs into a Corinthian.\\n\\nMary Jo Putney: Silk and Secrets A Victorian heroine seeks adventure as a man, but must reunite with her estranged husband to rescue her missing brother.\\n\\nSarah MacLean: Nine Rules to Break When Romancing a Rake. Heroine decides to break some rules and explore what rakes get to do.\\n\\nLynsay Sands: The Switch. Escaping a villainous Uncle, twins take turns masquerading as the other's brother.\\n\\nMary Jo Putney: The Rake Heroine masquerading as a steward\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "3061017b-47d1-47e6-a98d-70e4eff41105", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.6306100000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 10, "action": {"name": "search", "arguments": {"query": "historical fiction secretary clerk businessman theft accused cipher code smuggle villain"}, "id": "2b09b377-b2e3-4e3b-9754-617d59461ed4", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.7923000000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 10, "observation": {"result": "[\n {\n \"docid\": \"42402\",\n \"score\": 0.4924449026584625,\n \"snippet\": \"---\\ntitle: The Scarlet Pimpernel - Wikipedia\\nauthor: Authority control databases\\ndate: 2002-09-10\\n---\\nname: The Scarlet Pimpernel\\nauthor: Baroness Orczy\\ncountry: United Kingdom\\nlanguage: English\\ngenre: Historical fiction, adventure fiction\\nset_in: 1792, during the early stages of the French Revolution\\npublisher: Greening\\npub_date: 1905\\npages: 319\\npreceded_by: The First Sir Percy\\nfollowed_by: Sir Percy Leads the Band\\n\\nThe Scarlet Pimpernel is the first novel in a series of historical fiction by Baroness Orczy, published in 1905. It was written after her stage play of the same title (co-authored with her husband Montague Barstow) enjoyed a long run in London, having opened in Nottingham in 1903.\\n\\nThe novel is set during the Reign of Terror following the start of the French Revolution. The title is the nom de guerre of its hero and protagonist, a chivalrous Englishman who rescues aristocrats before they are sent to the guillotine. Sir Percy Blakeney leads a double life: apparently nothing more than a wealthy fop, but in reality, a formidable swordsman and a quick-thinking master of disguise and escape artist. The band of gentlemen who assist him are the only ones who know of his secret identity. He is known by his symbol, a simple flower, the scarlet pimpernel (Anagallis arvensis).\\n\\nOpening at the New Theatre in London's West End on 5 January 1905, the play became a favourite of British audiences, eventually playing more than 2,000 performances and becoming one of the most popular shows staged in London. Published after the success of the play, the novel was an immediate success, gaining Orczy a following of readers in Britain and the rest of the world. The stage play and subsequent novel, with their hero and villain, were so popular that they inspired a revival of classic villainy at the time.\\n\\nOrczy's premise of a daring hero who cultivates a secret identity disguised by a meek or ineffectual manner proved enduring. Zorro, Doctor Syn, the Shadow, the Spider, the Green Hornet, the Phantom, Superman and Batman followed within a few decades, and the trope remains a popular one in serial fiction today. Read by\"\n },\n {\n \"docid\": \"29367\",\n \"score\": 0.4854736626148224,\n \"snippet\": \"---\\ntitle: Historical Fictionistas discussion\\nauthor: Author add link photo\\ndate: 2012-10-11\\n---\\nHistorical Fictionistas discussion\\n\\nRecommendations?\\n\\n>\\n\\nCrime in 19th Century London?\\n\\ndate\\n\\nnewest \\u00bb\\n\\nmessage 1:\\n\\nby\\n\\nAllison\\n\\n(new)\\n\\nOct 11, 2012 07:57AM\\n\\nHey you guys...I would love some recommendations on books set in 19th century London revolving around crime (other than Jack the Ripper). Fiction and non-fiction would both be great!\\n\\nreply\\n\\n|\\n\\nflag\\n\\nAnything by Anne Perry in the Inspector Monk and the Thomas Pitt series. They start with The Face of a Stranger and The Cater Street Hangman, respectively. Monk is 1860s and Pitt somewhat later.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDickens, of course. But no one does 19th-century London crime quite like Perry.\\n\\nSome of Tasha Alexander's Lady Emily Ashton novels, starting with And Only to Deceive, are also set in London, but they are more oriented toward elite crime than the nitty-gritty grim reality of the Isle of Dogs.\\n\\nDavid Liss's Benjamin Weaver series is my favorite. It begins with A Conspiracy of Paper, a HF murder/mystery/thriller novel about a financial crisis in 19th century London. You'll love Benjamin and learn a lot about economics (without being bored to tears) because Liss is such a master at inserting historical details in creative ways. David Liss\\n\\nI love historical mysteries, so here's a few of my favorites set in London in the 19th century:\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of aristocratic series:\\n\\nC.S. Harris, Regency era with the first in the series being What Angels Fear\\n\\nCharles Finch,A Beautiful Blue Death is first in the series\\n\\nEarly forensics:\\n\\nDevoured by D.E. Meredith\\n\\nEnjoy!\\n\\nThe Yard\\n\\nThe Solitary House\\n\\nA gritty series:\\n\\nWill Thomas, the first title isSome Danger Involved\\n\\nA couple of arist\"\n },\n {\n \"docid\": \"41618\",\n \"score\": 0.47932177782058716,\n \"snippet\": \"---\\ntitle: Women Looking Away in Period Costume\\ndate: 2021-08-10\\n---\\nWomen Looking Away in Period Costume\\n\\nHistorical fiction with one little thing in common.\\n\\nAugust 10, 2021\\n\\nWe're going to let you in on a librarian secret: sometimes we judge a book by its cover. The fastest way to find historical fiction is to look for a woman in period costume looking (or walking) away!\\n\\nThe Address\\n\\nFiona Davis\\n\\nInterior designer Bailey Camdenis leaps at a chance to renovate her heiress cousin's lavish apartment at The Dakota, and learns the scandalous history of a distant ancestor's connection to the murder of the building's architect a century earlier.\\n\\nThe Alice Network\\n\\nKate Quinn\\n\\nIn 1947, pregnant Charlie St. Clair, an American college girl banished from her family, arrives in London to find out what happened to her beloved cousin Rose, who disappeared in Nazi-occupied France during the war, and meets a former spy who, torn apart by betrayal, agrees to help her on her mission.\\n\\nAll the Flowers in Paris\\n\\nSarah Jio\\n\\nA tale told from alternating viewpoints follows the experiences of a Parisian woman who awakens with no memory of her past before discovering a mysterious cache of letters written by a young woman of Jewish ancestry during the Nazi occupation.\\n\\nThe Atomic City Girls\\n\\nJanet Beard\\n\\nWorking in support of the war effort, June Walker begins an affair with a young Jewish physicist in hopes of uncovering what the government's end goal is, until the bombing of Hiroshima reveals the truth about what they are doing.\\n\\nAtomic Love\\n\\nJennie Fields\\n\\nRecruited by the FBI to spy on her former lover, a guilt-riddled Manhattan Project physicist becomes torn between lingering feelings for her ex and her growing attraction to a special agent, a former prisoner of war.\\n\\nBand of Sisters\\n\\nLauren Willig\\n\\nEschewed by her wealthy graduated classmates, a former scholarship student reluctantly volunteers to help World War I French civilians before finding herself surrounded by desperate families in villages decimated by German bombs\\n\\nThe Book of Lost Names\\n\\nKristin Harmel\\n\\nEscaping from Paris in 1942 after the arrest of her father, a Polish Jew, a graduate student finds refuge in a small mountain town, where she forges identity documents to help hundreds of Jewish children flee the Nazis.\\n\\nChurchill's Secret Messenger\\n\\nAlan Hlad\\n\\nRecruited from Churchill's typing pool to become an undercover spy\"\n },\n {\n \"docid\": \"51328\",\n \"score\": 0.4763755202293396,\n \"snippet\": \"---\\ntitle: Celebrating History's Bad-Ass Women: PW Talks with Greer Macallister\\nauthor: Claire Kirch\\ndate: 2017-02-28\\n---\\nGreer Macallister's historical novel Girl in Disguise (Sourcebooks) is a rollicking mashup of the real and imagined exploits of Kate Warne, the first female operative employed by the famed Pinkerton Detective Agency.\\n\\nHow did you come to write Girl in Disguise?\\n\\nWhen I first heard Kate's story a few years ago, I was floored. This woman was such a pioneer. She was a female detective in the 1850s, when it was completely unheard-of, and she was so good at it that she was assigned by Allan Pinkerton to run her own division. She helped save Abraham Lincoln's life as he made his way to his inauguration. She was an undercover spy for the Union during the Civil War. Somebody needed to get her story out there, and I figured it might as well be me!\\n\\nYou mention in your author's note that very little is known about Kate Warne; there aren't even verified photos of her--partially because she was a spy and also because Pinkerton Agency records were lost during the Chicago Fire of 1871. How much of her story is based on historical sources and how much did you have to imagine?\\n\\nThose gaps in the historical record made Kate the perfect subject for historical fiction--a little history and a lot of fiction. We know the facts on a handful of cases she worked, like the Adams Express case and one where she posed as a fortune-teller to catch a poisoner. Her role in thwarting the Lincoln assassination attempt in Baltimore is documented. But a lot of the rest is just open space, so I got to choose how to fill it in.\\n\\nWere the lives of Warne's male colleagues--including Allan Pinkerton--similarly shrouded in mystery?\\n\\nNot to the same degree, although some operatives' lives were better-documented than others. Pinkerton himself wrote and published a lot about his own prowess, so with him it's the opposite--tons of information, but not all reliable. For the cast of characters around Kate, I drew on some information about her real colleagues at the time, but mostly I combined and synthesized and created. It's all about balancing story and history to make the best possible experience for the reader.\\n\\nKate Warne is a mature woman, a widow, when she\"\n },\n {\n \"docid\": \"67868\",\n \"score\": 0.47207605838775635,\n \"snippet\": \"---\\ntitle: 25 of the best spy thrillers\\ndate: 2024-01-09\\n---\\nBooks\\n\\n25 of the best spy thrillers\\n\\nWe can't resist a dip into the mysterious world of the spy thriller. With secrets, lies, conspiracies and undercover plots abound, these books span the national and the international, blending the personal and the political and showing how they are inextricably linked.\\n\\nFrom the classic to the contemporary, here are some of the best spy thrillers around.\\n\\nThe best spy thriller books\\n\\nCasino Royale by Ian Fleming\\n\\nCasino Royale by Ian Fleming\\n\\nIn Casino Royale, the first of Ian Fleming's 007 adventures, a game of cards is James Bond's only chance to bring down Le Chiffre, French communist and paymaster of the Soviet murder organization SMERSH. But Bond soon discovers that there is far more at stake than money.\\n\\nThis is a book that's packed will thrills and suspense. It's an incredibly satisfying read that showcases everything we know and love about the Bond stories \\u2013 chilling, sadistic villains, sensuous, fiery love affairs and the smooth, sophisticated secret agent himself.\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nMaggie Bird lives a quiet life as a retiree in a seaside town. She never talks about her past life as a former spy. But when an unidentified body is left on Maggie's doorway, she knows it's a calling card from old times. Enter the 'Martini Club' \\u2013 Maggie's group of former spy friends. Together, they must solve the mystery and stay one step ahead of law enforcement.\\n\\nThis is the first instalment in a brand-new espionage series from Tess Gerritsen, author of the beloved Rizzoli and Isles detective series.\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThis book by former RAF pilot and investigative journalist Frederick Forsyth is one of the most celebrated thrillers ever written. It's intelligent, chilling and 100% unputdownable.\\n\\nIt is 1963 and an anonymous Englishman has been hired by the O.A.S. to murder General de Galle. A failed attempt means the target will be nearly impossible to get to. But this latest plot involves a lethal weapon: an assassin of legendary talent. Known only as The Jackal, this remorseless and deadly killer must be stopped \\u2013 but\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "2b09b377-b2e3-4e3b-9754-617d59461ed4", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.7923000000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 11, "action": {"name": "search", "arguments": {"query": "novel Victorian business deal heroine secretary shipwreck tempest rescue accused steal papers"}, "id": "d6174f08-75c7-4361-8b65-2d9cb4385b65", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.97135, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 11, "observation": {"result": "[\n {\n \"docid\": \"77939\",\n \"score\": 0.5016008615493774,\n \"snippet\": \"---\\ntitle: 12 Historical Fiction Books That Bring the Victorian Era to Life\\ndate: 2024-07-09\\n---\\nFeatures summary\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life\\n\\nImmerse yourself in the Victorian era with these 12 captivating historical fiction novels. Each book brings to life the complexities, challenges, and beauty of the period, offering a detailed look at the social, cultural, and technological changes of the time.\\n\\n12 Historical Fiction Books That Bring the Victorian Era to Life (Picture Credit - Instagram)\\n\\nThe Victorian era, spanning from 1837 to 1901, was a period of significant social, cultural, and technological change. This era saw the rise of the British Empire, industrial advancements, and shifts in societal norms. Historical fiction set in this time offers a captivating glimpse into the past, blending rich storytelling with historical details. Here are 12 historical fiction books that beautifully capture the essence of the Victorian era.\\n\\n1. The Crimson Petal and the White by Michel Faber\\n\\nMichel Faber's 'The Crimson Petal and the White' provides a vivid and gritty portrayal of Victorian London. The novel follows Sugar, a determined and intelligent prostitute, as she navigates the city's dark underbelly and attempts to climb the social ladder. Faber's meticulous attention to historical detail and complex characters make this a compelling read that immerses readers in the era's harsh realities and opulent surroundings.\\n\\n2. Fingersmith by Sarah Waters\\n\\n'Fingersmith' by Sarah Waters is a riveting tale of deception, betrayal, and unexpected twists. Set in Victorian England, the story revolves around Sue Trinder, a young woman raised among thieves, and her involvement in a con to defraud a wealthy heiress. Waters' masterful storytelling and rich depiction of Victorian society's class divides and criminal underworld makes this novel a gripping exploration of love and trust.\\n\\n3. The Essex Serpent by Sarah Perry\\n\\nSarah Perry's 'The Essex Serpent' is a beautifully written novel that blends historical fiction with gothic elements. Set in 1893, the story follows Cora Seaborne, a widow with a passion for natural history, as she moves to Essex and investigates rumours of a mythical serpent. Perry's evocative prose and exploration of themes like faith, science, and friendship capture the intellectual and societal currents of the Victorian era.\\n\\n4. The Light Between Oceans by M.L. Sted\"\n },\n {\n \"docid\": \"90027\",\n \"score\": 0.47871720790863037,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"18321\",\n \"score\": 0.4750952124595642,\n \"snippet\": \"---\\ntitle: Bleak House - Wikipedia\\nauthor: Authority control databases\\ndate: 2004-01-25\\n---\\nname: Bleak House\\nimage_size: 200px\\nauthor: Charles Dickens\\nillustrator: Hablot Knight Browne (Phiz)\\ncover_artist: Hablot Knight Browne (Phiz)\\ncountry: England\\nlanguage: English\\ngenre: Novel\\npublisher: Bradbury & Evans\\npublished: Serialised 12 March 1852 \\u2013 12 September 1853, book form 12 September 1853\\npreceded_by: David Copperfield\\nfollowed_by: A Child's History of England\\n\\nBleak House is a novel by English author Charles Dickens, first published as a 20-episode serial between 12 March 1852 and 12 September 1853. The novel has many characters and several subplots, and is told partly by the novel's heroine, Esther Summerson, and partly by an omniscient narrator. At the centre of Bleak House is a long-running legal case in the Court of Chancery, Jarndyce and Jarndyce, which comes about because a testator has written several conflicting wills. In a preface to the 1853 first edition, Dickens said there were many actual precedents for his fictional case. One such was probably Thellusson v Woodford, in which a will read in 1797Constantine, Alison. The Restoration of Brodsworth Hall & Gardens, February 2007 historical address, at Tickhill & District Local History Society was contested and not determined until 1859. Though many in the legal profession criticised Dickens's satire as exaggerated, Bleak House helped support a judicial reform movement that culminated in the enactment of legal reform in the 1870s.\\n\\nSome scholars debate when Bleak House is set. The English legal historian Sir William Holdsworth sets the action in 1827; however, reference to preparation for the building of a railway in Chapter LV suggests the 1830s. A work of Gothic fiction depicting London as a murky city swathed in fog, Bleak House is credited with introducing urban fog to the novel, which would become a frequent characteristic of urban Gothic literature and film.Mighall, Robert (2007). \\\"Gothic\"\n },\n {\n \"docid\": \"5574\",\n \"score\": 0.4695277810096741,\n \"snippet\": \"---\\ntitle: My Ultimate List of 5-Star Historical Romance Novel Recommendations\\nauthor: Katherine Grant\\ndate: 2020-10-23\\n---\\n(This post is updated every time I read a historical romance that I consider 5-stars! Jump straight to the recommendations by clicking here)\\n\\nDear fellow Historical Romance fan,\\n\\nEvery now and then, I read a historical romance that leaves me with a massive book hangover. You know the feeling: I just want to be with those characters for the rest of my life, and I absolutely don't want to reemerge into the real world.\\n\\nThe only cure I know of is to find another fantastic historical romance. In case you suffer the same fate, I thought I'd share an ultimate, growing list of my 5-star reads (aka BEST ROMANCES EVER). Most of these will probably be Regency romances, but the list also includes historical romance subgenres like Victorian, Scottish, American, and more.\\n\\nBy the way, these are MY all-time favorite 5-star reads. I'll also tell you the average Goodreads rating, but I warn you up front that my opinion often differs from the herd. Also, I'm linking to where you can buy the paperbacks with my 10% discount, but you can also purchase them from your preferred retailer, of course! Finally, apologies for the repeating authors. I think it is clear who my favorites are!\\n\\nI hope you enjoy these book recommendations.\\n\\nWishing you lots of love (stories),\\n\\nKatherine\\n\\nMy Ultimate List of 5-Star Historical Romance Novel Recommendations\\n\\n(in no particular order)\\n\\nFive-Star Regency England Romance Novels\\n\\nA Lady Awakened by Cecilia Grant\\n\\nAverage Goodreads Rating: 3.71\\n\\nWhy I Loved It: Martha is the ultimate starchy heroine. She lives by black-and-white rules, and she believes them, too. I loved how she remained an archetypal \\\"frigid widow\\\" - complete with lectures on the benefits of education - yet also learned how to be more fluid in the world. On top of that, the ever-changing power dynamics in the steamy scenes makes this a masterclass in sexy romances for me!\\n\\nThe Synopsis:\\n\\nNewly widowed and desperate to protect her estate and beloved servants from her malevolent brother-in-law, Martha Russell conceives a daring plan. Or rather, a daring plan to conceive. After all, if she has an heir on the way, her future will be secured. Forsaking\"\n },\n {\n \"docid\": \"96202\",\n \"score\": 0.46788012981414795,\n \"snippet\": \"---\\ntitle: Rebecca (novel) - Wikipedia\\nauthor: Authority control databases\\ndate: 2004-12-09\\n---\\nname: Rebecca\\nauthor: Daphne du Maurier\\nlanguage: English\\ncountry: United Kingdom\\ngenre: Crime, Gothic, mystery, romance\\npublisher: Victor Gollancz Ltd\\npages: 446 pp\\noclc: 527505\\nrelease_date: 5 August 1938 Daily News (London) 2 August 1938\\n\\nRebecca is a 1938 Gothic novel by the English author Daphne du Maurier. It depicts an unnamed young woman who impetuously marries a wealthy widower, before discovering that both he and his household are haunted by the memory of his late first wife, the title character.\\n\\nA bestseller which has never gone out of print, Rebecca sold 2.8\\u00a0million copies between its publication in 1938 and 1965. It has been adapted numerous times for stage and screen, including a 1939 play by du Maurier herself, the film Rebecca (1940), directed by Alfred Hitchcock, which won the Academy Award for Best Picture, and the 2020 remake directed by Ben Wheatley for Netflix. The story has been adapted as a musical.\\n\\nThe novel is remembered especially for the character of Mrs. Danvers, the West Country estate Manderley, and its opening line: \\\"Last night, I dreamt I went to Manderley again.\\\"\\n\\nPlot\\n\\nWhile working as the companion to a rich American woman on holiday in Monte Carlo, the unnamed narrator, a na\\u00efve young woman in her early 20s, becomes acquainted with a wealthy Englishman, Maxim de Winter, a 42-year-old widower. After a fortnight of courtship, she agrees to marry him and, after the wedding and honeymoon, accompanies him to his mansion in Cornwall, the beautiful estate Manderley.\\n\\nMrs Danvers, the sinister housekeeper, was profoundly devoted to the first Mrs de Winter, Rebecca, who died in a sailing accident about a year before Maxim and the second Mrs de Winter met. She continually attempts to undermine the narrator psychologically, subtly suggesting to her that she will never attain the beauty, urbanity, and charm her predecessor possessed. When the narrator makes small requests, Mrs Danvers and the other staff describe how Rebecca ran M\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "d6174f08-75c7-4361-8b65-2d9cb4385b65", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.97135, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 12, "action": {"name": "get_document", "arguments": {"docid": "90027"}, "id": "43d536bb-1bba-46f1-a1d3-e69c07cc8e89", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.16775, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 12, "observation": {"result": "{\n \"docid\": \"90027\",\n \"text\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The Discovery\\n\\n- Threats and Secrets\\n\\n- I Defend my Honour, More's the Pity\\n\\n- My All-important Task\\n\\n- Little Ifrit\\n\\n- The Beauty and the Vegetables\\n\\n- I Go Dress-Shopping\\n\\n- My Little Secret\\n\\n- The Thief\\n\\n- Improving My Skirt\\n\\n- The Key to Him\\n\\n- I Make Lieutenant-Pancake\\n\\n- Prospects of Matrimonial Misery\\n\\n- More Misery Behind the Bush\\n\\n|\\n\\n- What to Do with Pink?\\n\\n- Going to the Room that Doesn't Exist\\n\\n- Problems? What Problems?\\n\\n- Sisters' Battles\\n\\n- Ambrosian Waste Disposal Squad\\n\\n- The Adversary\\n\\n- Pink Espionage\\n\\n- Dysfunctional Dismissal\\n\\n- To Meet without Trousers\\n\\n- In Tow\\n\\n- Twice Surprise\\n\\n- A Duel of Eyes\\n\\n- To Dance with him\\n\\n- Secret Plans and Politics\\n\\n- The Message Lock\\n\\n- Woes of Love\\n\\n- And a few more Woes of Love\\n\\n- Threats and Decisions\\n\\n- The Great Hunt of Green Park\\n\\n- Pinching and Planning\\n\\n- On Dates\\n\\n- Bloody Work\\n\\n- My lies run away with me\\n\\n- The Importance of Being Nice\\n\\n- Am I a Chimpanzee?\\n\\n- The Speech\\n\\n- The Other Speech\\n\\n- I Realize I Danced with a Criminal Mastermind\\n\\n- Cozy Little Coach Ride\\n\\n- I Mash and Bend Myself\\n\\n- I Bend Myself A Little Further\\n\\n- Napoleon and all the Little Piggies\\n\\n- Fighting Spirit\\n\\n|\\n\\n- Hallucination Manicure\\n\\n- Unluckily Unlocked\\n\\n- Looking for Truffles and Butterflies\\n\\n- Seeing Stars\\n\\n- A Trace of Fire Brings the Winter\\n\\n- I Polish My Housebreaking Skills\\n\\n- Unreal Dream of a Really Wonderful Nightmare\\n\\n- Victory Party\\n\\n- Sisterly Love\\n\\n- Biting Metaphorical Heads\\n\\n- Secrets of the Toilet\\n\\n- Different Sorts of Silence\\n\\n- Competition\\n\\n- A Waist of Tigers\\n\\n- Behind the Mask\\n\\n- Trapped\\n\\n- Pneumatic Freedom\\n\\n- A Man's Work\\n\\n- Bifurcated\\n\\n- Lion's Den\\n\\n- Lion's Jaws\\n\\n- Nemesis\\n\\n- Danger! Explosive Cargo!\\n\\n- Lessons in Power\\n\\n- A Special Person\\n\\n- Isle Marbeau\\n\\n- Mine and Yours\\n\\n- The Tortoise and the other Tortoise and no Hare\\n\\n- Shots in the Dark\\n\\n- Urania\\n\\n- Rising Waves\\n\\n- Man and Woman\\n\\n|\\n\\nRadish/Published Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- Practicing Impertinence\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Discovery\\n\\n- I Defend my Honour, More's the Pity\\n\\n- Little Ifrit\\n\\n- I Go Dress-Shopping\\n\\n- The Thief\\n\\n- Making Lieutenant-Pancake\\n\\n- What to Do with Pink?\\n\\n- Problems? What Problems?\\n\\n|\\n\\n- Ambrosian Waste Disposal Squad\\n\\n- The Adversary\\n\\n- Dysfunctional Dismissal\\n\\n- Twice Surprised\\n\\n- Duelling on the Dance Floor\\n\\n- Secret Plans and Politics\\n\\n- Woes of Love\\n\\n- The Great Hunt of Green Park\\n\\n- Bloody Work\\n\\n- The Importance of Being Nice\\n\\n- Am I a Chimpanzee?\\n\\n- Memorable Speeches\\n\\n- I Realize I Danced with a Criminal Mastermind\\n\\n- I Mash and Bend Myself\\n\\n- Napoleon and all the Little Piggies\\n\\n- Fighting Spirit\\n\\n- Hallucination Manicure\\n\\n- Looking for Truffles and Butterflies\\n\\n- Seeing Stars\\n\\n|\\n\\n- A Trace of Fire Brings the Winter\\n\\n- Unreal Dream of a Really Wonderful Nightmare\\n\\n- Victory Party?\\n\\n- Toilet Secrets\\n\\n- Different Sorts of Silence\\n\\n- A Waist of Tigers\\n\\n- Behind the Mask\\n\\n- Trapped\\n\\n- A Man's Work\\n\\n- Bifurcated\\n\\n- Lion's Den\\n\\n- Danger! Explosive Cargo!\\n\\n- Lessons in Power\\n\\n- \\u00cele Marbeau\\n\\n- Mine and Yours\\n\\n- The Tortoise and the other Tortoise and no Hare\\n\\n- Shots in the Dark\\n\\n- Rising Waves\\n\\n- Man and Woman\\n\\n|\\n\\nAmbrose POV Chapter:\\n\\n- None of Your Business\\n\\n(Available in published version only)\\n\\nCharacters[]\\n\\n- Flora Milton\\n\\n- Gertrude Linton\\n\\n- Lisbeth Linton\\n\\n- Maria Linton\\n\\n- Anne Linton\\n\\n- Thomas Warren\\n\\n- Edgar Stone\\n\\n- Hieronymous Pearson\\n\\n- Eloise Hamilton\\n\\n- Lt Ellingham\\n\\n|\"\n}", "invoking_actions": [{"name": "get_document", "arguments": {}, "id": "43d536bb-1bba-46f1-a1d3-e69c07cc8e89", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.16775, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 13, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" Robert Thier Lilly Ambrose businessman document encrypted sea storm rescue"}, "id": "b1d52267-9cbb-4411-b1c9-a1377584838e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.3779150000000002, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 13, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.5981267690658569,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5688382387161255,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"72764\",\n \"score\": 0.5286663174629211,\n \"snippet\": \"---\\ntitle: Overboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\nauthor: Michael J Tougias\\ndate: 2010-03-16\\n---\\n-24% $18.15$18.15\\n\\nDelivery Sunday, June 15\\n\\nShips from: Amazon Sold by: Bookdream\\n\\n$13.79$13.79\\n\\nDelivery Thursday, June 19\\n\\nShips from: Amazon Sold by: Zoom Books Company\\n\\nReturn this item for free\\n\\nFree returns are available for the shipping address you chose. You can return the item for any reason in new and unused condition: no return shipping charges.\\n\\nLearn more about free returns.- Go to your orders and start the return\\n\\n- Select your preferred free shipping option\\n\\n- Drop off and leave!\\n\\nSorry, there was a problem.\\n\\nThere was an error retrieving your Wish Lists. Please try again.Sorry, there was a problem.\\n\\nList unavailable.Download the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nOverboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\n\\nPurchase options and add-ons\\n\\nIn May 2005, Tom Tighe, captain of a forty-five-foot-long sailboat named the Almeisan, and his first mate, Loch Reidy, welcomed three new crewmembers for a five-day voyage from Connecticut across the blue waters of the Gulf Stream to sun-drenched Bermuda. The new crew included forty-six-year-old Kathy Gilchrist, seventy-year-old Ron Burd, and thirty-four-year-old Chris Ferrer. Although Tighe had made the trip forty-eight times, with Reidy accompanying him on twenty of those voyages, the rest of the crew had joined to learn more about offshore sailing.\\n\\nFour days into the voyage, an enormous storm struck, sweeping two of the crew into the towering sea. The remaining crewmembers managed to stay aboard the vessel as it was slowly torn apart by the rampaging ocean. Overboard! follows the simultaneous desperate struggles of both those still on the boat and those fighting for their lives in the sea.\\n\\nThe Coast Guard, alerted to the Almeisan's\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.5275611877441406,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"74026\",\n \"score\": 0.4970148205757141,\n \"snippet\": \"---\\ntitle: Buying and sending eBooks to others\\nauthor: Sebastian Junger\\ndate: 1997-05-17\\n---\\nLearn more\\n\\nThese promotions will be applied to this item:\\n\\nSome promotions may be combined; others are not eligible to be combined with other offers. For details, please see the Terms & Conditions associated with these promotions.\\n\\n- Highlight, take notes, and search in the book\\n\\n- In this edition, page numbers are just like the physical edition\\n\\nYour Memberships & Subscriptions\\n\\nDownload the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nThe Perfect Storm: A True Story of Men Against the Sea Media tie-in Edition, Kindle Edition\\n\\n\\\"There is nothing imaginary about Junger's book; it is all terrifyingly, awesomely real.\\\" \\u2014Los Angeles Times\\n\\nIt was the storm of the century, boasting waves over one hundred feet high\\u2014a tempest created by so rare a combination of factors that meteorologists deemed it \\\"the perfect storm.\\\" In a book that has become a classic, Sebastian Junger explores the history of the fishing industry, the science of storms, and the candid accounts of the people whose lives the storm touched. The Perfect Storm is a real-life thriller that makes us feel like we've been caught, helpless, in the grip of a force of nature beyond our understanding or control.\\n\\nWinner of the American Library Association's 1998 Alex Award.\\n\\n- ISBN-13978-0393076615\\n\\n- EditionMedia tie-in\\n\\n- PublisherW. W. Norton & Company\\n\\n- Publication dateMay 17, 1997\\n\\n- LanguageEnglish\\n\\n- File size1.7 MB\\n\\nSee all supported devices\\n\\nKindle E-Readers\\n\\n- Kindle Paperwhite (5th Generation)\\n\\n- Kindle (11th Generation, 2024 Release)\\n\\n- Kindle Paperwhite\\n\\n- Kindle\\n\\n- Kindle Paperwhite (12th Generation)\\n\\n- Kindle Voyage\\n\\n- All New Kindle E-reader (11th Generation)\\n\\n- All New Kindle E-reader\\n\\n- Kindle Oasis\\n\\n- Kindle Oasis (10th Generation)\\n\\n- Kindle Paperwhite (10th Generation)\\n\\n- Kindle (10th Generation)\\n\\n- Kindle Scribe\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "b1d52267-9cbb-4411-b1c9-a1377584838e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.3779150000000002, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 14, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" Robert Thier plot document theft accusation schemer cipher file villain"}, "id": "a534f3c0-a6da-462d-9fb6-87eaaa30e981", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.604945, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 14, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.4984514117240906,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.4966529309749603,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"84371\",\n \"score\": 0.4604397714138031,\n \"snippet\": \"---\\ntitle: Author Interview: Rob Thier Talks 'Storm and Silence'\\nauthor: Amber Topping\\ndate: 2024-12-13\\n---\\nToday, we're excited to have author Rob Thier (known to his fans as \\\"Sir Rob\\\") of 'Storm and Silence' and 'The Robber Knight Series' here to discuss his background and latest book!\\n\\nThank you for agreeing to do this interview. Can you tell us a little bit about yourself and how you came to be a writer?\\n\\nI must admit I started writing so early in life that I don't really remember when or how. Maybe when I was nine or ten years old? But it didn't develop into anything serious until I discovered online writing platforms like Wattpad and started studying at an English university a few years ago. Unlike my failed writing attempts in my native language, German, here for the first time I had an opportunity to share my writing with a big audience, ask feedback and gather experience. And my university studies gave me the practice I needed to perfect my grasp on the English language. From there it was just a small step to deciding that I wanted to be a professional writer, and that has been my dream ever since.\\n\\nHow does being a historian affect the way you write stories?\\n\\nIt has made me a little bit of a stickler for accuracy. I do a lot of research for my stories. Even when it is a contemporary story, I want to make sure that the characters behave in a realistic way. Even if I were to write a fantasy story, I would research various mythologies and create a world that is in some way related to real human ideas and conceptions out there in the world. It makes for a very interesting writing process because during the research you always discover things that you never knew before.\\n\\nCongratulations on your latest story, Storm and Silence. For those who aren't familiar, can you explain what it's about?\\n\\nThe story follows the adventures of 19-year-old feminist Lilly and her budding romance with the powerful and ruthless business mogul Rikkard Ambrose. A firm believer in the equality of men and women, Lilly is forced to dress up as a man in order to work for Mr. Ambrose and achieve her independence. The two of them butt heads all the time, but when an important document is stolen from Mr. Ambrose's safe, and he has fought a battle of wits and intrigue with his arch-enemy Lord Dalgliesh, she stands by him through thick and\"\n },\n {\n \"docid\": \"60123\",\n \"score\": 0.45862993597984314,\n \"snippet\": \"---\\ntitle: Thief II - Wikipedia\\nauthor: Authority control databases National France BnF data\\ndate: 2005-07-10\\n---\\ntitle: Thief II: The Metal Age\\ndeveloper: Looking Glass Studios\\npublisher: Eidos Interactive\\ndirector: Steve Pearsall\\ndesigner: Tim StellmachRandy Smith\\nprogrammer: Alex DuranWilliam FarquharPat McElhatton\\nartist: Mark Lizotte\\ncomposer: Eric Brosius\\nseries: Thief\\nengine: Dark Engine\\nplatforms: Windows\\nreleased: NA March 23, 2000 EU March 31, 2000\\ngenre: Stealth\\nmodes: Single-player\\n\\nThief II: The Metal Age is a 2000 stealth video game developed by Looking Glass Studios and published by Eidos Interactive in March 2000. Like its predecessor Thief: The Dark Project, the game follows Garrett, a master thief who works in and around a steampunk metropolis called the City. The player assumes the role of Garrett as he unravels a conspiracy related to a new religious sect. Garrett takes on missions such as burglaries and frameups, while trying to avoid detection by guards and automated security.\\n\\nThief II was designed to build on the foundation of its predecessor. In response to feedback from players of Thief, the team placed a heavy focus on urban stealth in the sequel, and they minimized the use of monsters and maze-like levels. The game was made with the third iteration of the Dark Engine, which had been used previously to develop Thief and System Shock 2. Thief II was announced at the 1999 Electronic Entertainment Expo, as part of an extended contract between Looking Glass and Eidos to release games in the Thief series. Looking Glass neared bankruptcy as the game was developed, and the company was kept running by advances from Eidos.\\n\\nThief II received positive reviews from critics, and its initial sales were stronger than those of its predecessor. However, the game's royalties were processed slowly, which compounded Looking Glass's financial troubles. As a result, the company closed in May 2000, with plans for Thief III cancelled. The third game in the series, entitled Thief: Deadly Shadows, was developed by Ion Storm and published by Eidos in 2004. Thief 2X: Shadows of the Metal Age, a widely praised expansion mod for Thief II, was released in 20\"\n },\n {\n \"docid\": \"13697\",\n \"score\": 0.45751625299453735,\n \"snippet\": \"---\\ntitle: Can you agree that Mr. Hands is the most evil and horrible character in Cyberpunk?\\ndate: 2025-03-28\\n---\\nNot that Hands horrifies me...in some ways I even like him. But...for the sake of fairness, I'll put my purely subjective view of this man \\\"on the court of public opinion\\\" )\\n\\nIt's time to voice our thoughts on Night City's main villain - Wade Bleecker aka Mr. Hands! Let's start the discussion with the intercepted correspondence between Hands and Lt. Col. Bennett on Hands' laptop in his office:\\n\\nAnd some people believe that Mr. Hands is \\\"just a fixer, just one of them\\\"...\\n\\nOk, what do we see here?\\n\\n1. Cynosure is one of the most secret projects of the NUSA and Militech (as well as Netwatch!), a project that is personally supervised by President Rosalind Myers and which is generally the key to the survival of the NUSA in the fight against \\\"Arasaka\\\" and their rival technologies (Relic\\\\Soulkiller).\\n\\nHow many other fixers in the game are aware of the existence of this project?\\n\\nWho are of these \\\"fixers\\\" and under what conditions they can access this level of information?\\n\\nAnd how many of them will be left alive after, given Netwacht's methods of preventing information leaks (see the gig \\\"Shot from both sides\\\").\\n\\nNot only does Hands have the necessary security clearance, but he also allows himself to operate on this information about the project in a conversation with a third party - in this case, Colonel Hansen's first deputy. Who can afford it?\\n\\nI'll be brief here: Hands is not a fixer. It is impossible for a fixer to participate in a project of this level.\\n\\n2. Hands knows the name and purpose of the Cynosure project. Concerned about maintaining the secrecy of the project. Aware of the existence of \\\"underground bunkers\\\". Is it logical to assume he knows everything else? - What exactly is the tech hidden underground, and what are the risks of trying to access it? I believe the answer is positive here.\\n\\nNext: Message log from Shuttle 1 (probably Songbird's workstation).\\n\\nWhat do we have here?\\n\\n1. The FIA was interested in the attempted theft of Relic's chip even before the immediate events in Kompeki Plaza. So Mi lies to V when she says that he was chosen at random from all the mercenaries simply because his\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "a534f3c0-a6da-462d-9fb6-87eaaa30e981", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.604945, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 15, "action": {"name": "get_document", "arguments": {"docid": "84371"}, "id": "2ce54757-7e7f-4905-a574-98e96a7f43e3", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.8492050000000002, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 15, "observation": {"result": "{\n \"docid\": \"84371\",\n \"text\": \"---\\ntitle: Author Interview: Rob Thier Talks 'Storm and Silence'\\nauthor: Amber Topping\\ndate: 2024-12-13\\n---\\nToday, we're excited to have author Rob Thier (known to his fans as \\\"Sir Rob\\\") of 'Storm and Silence' and 'The Robber Knight Series' here to discuss his background and latest book!\\n\\nThank you for agreeing to do this interview. Can you tell us a little bit about yourself and how you came to be a writer?\\n\\nI must admit I started writing so early in life that I don't really remember when or how. Maybe when I was nine or ten years old? But it didn't develop into anything serious until I discovered online writing platforms like Wattpad and started studying at an English university a few years ago. Unlike my failed writing attempts in my native language, German, here for the first time I had an opportunity to share my writing with a big audience, ask feedback and gather experience. And my university studies gave me the practice I needed to perfect my grasp on the English language. From there it was just a small step to deciding that I wanted to be a professional writer, and that has been my dream ever since.\\n\\nHow does being a historian affect the way you write stories?\\n\\nIt has made me a little bit of a stickler for accuracy. I do a lot of research for my stories. Even when it is a contemporary story, I want to make sure that the characters behave in a realistic way. Even if I were to write a fantasy story, I would research various mythologies and create a world that is in some way related to real human ideas and conceptions out there in the world. It makes for a very interesting writing process because during the research you always discover things that you never knew before.\\n\\nCongratulations on your latest story, Storm and Silence. For those who aren't familiar, can you explain what it's about?\\n\\nThe story follows the adventures of 19-year-old feminist Lilly and her budding romance with the powerful and ruthless business mogul Rikkard Ambrose. A firm believer in the equality of men and women, Lilly is forced to dress up as a man in order to work for Mr. Ambrose and achieve her independence. The two of them butt heads all the time, but when an important document is stolen from Mr. Ambrose's safe, and he has fought a battle of wits and intrigue with his arch-enemy Lord Dalgliesh, she stands by him through thick and thin, and the two of them grow closer, until their relationship finally heats up.\\n\\nRELATED YA Advanced Book Review: Storm and Silence \\u2013 Bridget Jones Meets James Bond\\n\\nCan you share some insight behind the story and your main characters Lilly and Mr. Ambrose?\\n\\nOriginally, the story grew out of my study of 19th-century suffragism and Chartism. I read about a chartist demonstration where several dozen people were beaten to death by policeman for wanting the vote, and I thought \\u2013 if that's what it was like for men, how much worse must it have been for the women, who, back then, had no chance to learn how to defend themselves?\\n\\nLilly developed as a mix of my favorite female fictional characters and some leading early-day feminists and suffragists. She is strong, quirky, determined, and will let nothing and no one stand in the way of her dreams. I needed a male character who would be strong enough to stand up to her and clash with her occasionally. A Victorian industrialist-financier type, utterly ruthless, chauvinistic and stingy, seemed the ideal contrast to her, and an interesting character to have as a 'hero.' Thus, Lilly and Mr. Ambrose were developed.\\n\\nSomething I loved about Storm and Silence was the connection to the suffragette movement. What specifically drew you to write about this particular time period?\\n\\nIt was the first period in which a feminist female character could, with historical accuracy, be portrayed as fighting for her rights. True, there were exceptional women before then who ventured into the male world, such as Joan of Arc, but mostly women distinguished themselves in areas that were traditionally associated with their stereotypical gender role. The thing about the Victorian era that attracted me was that during this period, women began for the first time to speak up against the injustices in their lives in greater numbers. It was an era of great change, and that fascinated me.\\n\\nWhat are some of the challenges you faced finishing the book?\\n\\nGetting Lilly and Ambrose to relent, and to grow closer. They can be quite stubborn characters in that regard. It also wasn't easy to give an accurate description of some of the parts of 19th century London. It was rather difficult to do a subject on English history here from Germany. But it was well worth it in the end.\\n\\nCan you tell us a little about some of the other books you've written?\\n\\nThe only other book of mine currently published is the first volume of The Robber Knight Series, following the adventures of the mysterious, bewitched knight Sir Reuben and his romance with Lady Ayla von Luntberg. Sir Reuben is a fallen knight, the medieval version of a bad boy who has renounced all notions of honor and makes his living by robbing people. His motto: \\\"I steal from the rich to give to myself.\\\" But when Lady Ayla saves his life, he feels bound to help her defend her lands against the approaching army of the Margrave von Falkenstein, and the two of them become entangled in a dangerous romance.\\n\\nI do have other stories as drafts on Wattpad.com, the online writing platform where I first started making my writing public, and I'm planning to publish more of them as paperbacks and ebooks soon, including the sequel to Storm and Silence, which is currently in the works.\\n\\nAre there any writers who came before that influence your own writing style?\\n\\nYes, definitely. The top four writers who influenced me probably are Terry Pratchett, Meg Cabot, Roald Dahl and Jane Austen. A very varied collection \\u2013 they write (or wrote) everything from humorous fantasy over romance to children's novels. But what they all have in common is a good dose of humor which I have tried to incorporate into my own works.\\n\\nWhat writing project are you working on next?\\n\\nOf course, there is the Storm and Silence sequel, In the Eye of the Storm. But apart from that, I am working on developing ideas for a new project, something that will give me a chance to explore my penchant for dark humor. If you enjoyed the humor in Storm and Silence, you definitely have things to look forward to!\\n\\nWhat's the best way for readers to follow you and your projects?\\n\\nI regularly post news about my writing projects on social media, including Facebook, Twitter, and Google+. Here are the links to my pages:\\n\\nFacebook: \\n\\nTwitter: TheSirRob at \\n\\nGoogle+: \\n\\nI also post drafts of my stories on Wattpad and Radish Fiction. Anyone who wants to be the first to get their hands on new chapters can do so online with just a few clicks. Here's my Wattpad profile: \\n\\nAnd here's the download page for the Radish Fiction App, which gets somewhat earlier updates: \\n\\nHave fun reading! \\ud83d\\ude42\\n\\nRELATED:\\n\\nYou can read our other interviews HERE.\\n\\nARE YOU A ROMANCE FAN? FOLLOW THE SILVER PETTICOAT REVIEW:\\n\\nOur romance-themed entertainment site is on a mission to help you find the best period dramas, romance movies, TV shows, and books. Other topics include Jane Austen, Classic Hollywood, TV Couples, Fairy Tales, Romantic Living, Romanticism, and more. We're damsels not in distress fighting for the all-new optimistic Romantic Revolution. Join us and subscribe. For more information, see our About, Old-Fashioned Romance 101, Modern Romanticism 101, and Romantic Living 101.\"\n}", "invoking_actions": [{"name": "get_document", "arguments": {}, "id": "2ce54757-7e7f-4905-a574-98e96a7f43e3", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.8492050000000002, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 16, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" Lilly Mr Ambrose Lord Dalgliesh encrypted file stolen accusation rescue sea storm"}, "id": "36580397-72de-4b2d-981a-af1c3ee917dd", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.1058900000000005, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 16, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.5573393106460571,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5317371487617493,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"79197\",\n \"score\": 0.5141761302947998,\n \"snippet\": \"---\\ntitle: Mysterious Circumstances\\nauthor: David Grann\\ndate: 2004-12-13\\n---\\nRichard Lancelyn Green, the world's foremost expert on Sherlock Holmes, believed that he had finally solved the case of the missing papers. Over the past two decades, he had been looking for a trove of letters, diary entries, and manuscripts written by Sir Arthur Conan Doyle, the creator of Holmes. The archive was estimated to be worth nearly four million dollars, and was said by some to carry a deadly curse, like the one in the most famous Holmes story \\\"The Hound of the Baskervilles.\\\"\\n\\nThe papers had disappeared after Conan Doyle died, in 1930, and without them no one had been able to write a definitive biography\\u2014a task that Green was determined to complete. Many scholars feared that the archive had been discarded or destroyed; as the London Times noted earlier this year, its whereabouts had become \\\"a mystery as tantalizing as any to unfold at 221B Baker Street,\\\" the fictional den of Holmes and his fellow-sleuth, Dr. Watson.\\n\\nNot long after Green launched his investigation, he discovered that one of Conan Doyle's five children, Adrian, had, with the other heirs' agreement, stashed the papers in a locked room of a ch\\u00e2teau that he owned in Switzerland. Green then learned that Adrian had spirited some of the papers out of the ch\\u00e2teau without his siblings' knowledge, hoping to sell them to collectors. In the midst of this scheme, he died of a heart attack\\u2014giving rise to the legend of the curse. After Adrian's death, the papers apparently vanished. And whenever Green tried to probe further he found himself caught in an impenetrable web of heirs\\u2014including a self-styled Russian princess\\u2014who seemed to have deceived and double-crossed each other in their efforts to control the archive.\\n\\nFor years, Green continued to sort through evidence and interview relatives, until one day the muddled trail led to London\\u2014and the doorstep of Jean Conan Doyle, the youngest of the author's children. Tall and elegant, with silver hair, she was an imposing woman in her late sixties. (\\\"Something very strong and forceful seems to be at the back of that wee body,\\\" her father had written of Jean when she was five. \\\"Her will is tremendous.\\\") Whereas her brother Adrian had been kicked out of the British Navy for insubordination, and her elder brother Denis was a playboy who had\"\n },\n {\n \"docid\": \"82350\",\n \"score\": 0.4930114448070526,\n \"snippet\": \"---\\ntitle: \\u2630OTHER BOOKS\\ndate: 2023-01-05\\n---\\nThe Sleeping and the Dead\\n\\nIn this vivid psychological suspense novel, a diving instructor makes a gruesome discovery in Cranwell Lake - the body of a teenager who has clearly been in the water for many years.\\n\\nDetective Peter Porteous is called to Cranwell Lake where the body of a teenager has been discovered. After trawling through the missing persons files, he comes to the conclusion that the corpse is Michael Grey, an enigmatic and secretive young man who was reported missing by his foster parents in 1972.\\n\\nThe news report that a body has been found leaves prison officer Hannah Morton in shock. Michael had been her boyfriend, and she had been with him the night he disappeared. And now the discovery is bringing back dreaded and long buried memories from her past ...\\n\\nThe Sleeping and the Dead was first published in the UK in 2001. It was reissued by Pan MacMillan in their 'Ann Cleeves Classic Crime' series on 5th January 2023. Order a copy via the publisher's website, or from any bookshop or library (ISBN: 978-1-5290-7051-4).\\n\\nThe audiobook, read by John Telfer, is available in a choice of formats from the Reading House, or as an audio download from Amazon.\\n\\nBurial of Ghosts\\n\\nFor Lizzie Bartholomew, a holiday in Morocco will change life forever. But not in the way she had hoped...\\n\\nLizzie had planned her trip to Marrakech as the perfect escape from her life - and her nightmares - in Northumberland. Abandoned as a baby, and having spent her childhood moving between foster homes, Lizzie certainly has much to escape from. And for Lizzie, Morocco is the exotic paradise that she had imagined. Especially when she finds herself on a bus sitting next to a fellow tourist, who is also travelling to fulfil his dreams.\\n\\nAfter a brief affair, Lizzie returns to England. In the days that follow, she is distracted by thoughts of her mysterious lover, hoping against hope that Philip might come and find her. But suddenly she receives a letter from a firm of solicitors. Philip Samson has died. In his will, he has left Lizzie a gift of \\u00a315,000. But there are conditions attached to this unexpected legacy, conditions that will alter the\"\n },\n {\n \"docid\": \"71386\",\n \"score\": 0.4850735366344452,\n \"snippet\": \"---\\ntitle: A Taste for Death\\nauthor: Gingerninjasz\\ndate: 2025-01-01\\n---\\nSir Paul Berowne a prominent Government Minister turns to his old friend Adam Dalgleish following a series of threatening letters delivered to his London home.Sir Paul Berowne a prominent Government Minister turns to his old friend Adam Dalgleish following a series of threatening letters delivered to his London home.Sir Paul Berowne a prominent Government Minister turns to his old friend Adam Dalgleish following a series of threatening letters delivered to his London home.\\n\\nBrowse episodes\\n\\nFeatured reviews\\n\\nThis is the first P. D. James mystery I ever remember watching. Not that I'm sure it was meant for kids, but I was fortunate to see it when it was first broadcast in 1988 and it stayed in my memory ever since - and no wonder! It is without doubt one of the creepiest mysteries ever filmed, full of atmosphere and foreboding - something P. D. James was especially good at in her novels. Indeed, having read the book years later it is remarkable how they have managed to adapt this. For those familiar to James' novels, they are not always conventional in their structure, and with A Taste For Death much of what happens in the first 3 episodes has already happened when the novel begins with the discovery of Sir Paul Berrowne's body. To see how they have adapted it here with so much back story to build up is remarkable and a triumph for the screenwriter Alick Rowe, who creates a masterpiece.\\n\\nThe story here begins with Tory MP Sir Paul Berrowne (Bosco Hogan) meeting up with Adam Dalgliesh in the park, where he shows him a series of anonymous letters written to him. They allege various things, including his involvement in his 1st wife's death in a car crash and his brother Hugo's murder, whose fiance Barbara Swayne he then married. But as Dalgliesh investigates, Berrowne begins behaving rather oddly to those who know him after having a religious conversion when visiting St Matthew's Church one day. He informs his council that he intends to resign as an MP, tells his wife Barbara that he plans to sell the family home and lets her cousin (and lover) Stephen Lampart know that he intends to withdraw all funding from his abortion clinic. More disturbing is when he is seen leaving an embankment where a young girl, Diana Travers (a brave part for London\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "36580397-72de-4b2d-981a-af1c3ee917dd", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.1058900000000005, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 17, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" detailed plot Ambrose file document cipher encrypted abroad Dalgliesh storm ending rescue"}, "id": "4db7a864-4361-47ec-9f85-24cd1321aa74", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.377825, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 17, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.48288753628730774,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.4817203879356384,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"79197\",\n \"score\": 0.47309380769729614,\n \"snippet\": \"---\\ntitle: Mysterious Circumstances\\nauthor: David Grann\\ndate: 2004-12-13\\n---\\nRichard Lancelyn Green, the world's foremost expert on Sherlock Holmes, believed that he had finally solved the case of the missing papers. Over the past two decades, he had been looking for a trove of letters, diary entries, and manuscripts written by Sir Arthur Conan Doyle, the creator of Holmes. The archive was estimated to be worth nearly four million dollars, and was said by some to carry a deadly curse, like the one in the most famous Holmes story \\\"The Hound of the Baskervilles.\\\"\\n\\nThe papers had disappeared after Conan Doyle died, in 1930, and without them no one had been able to write a definitive biography\\u2014a task that Green was determined to complete. Many scholars feared that the archive had been discarded or destroyed; as the London Times noted earlier this year, its whereabouts had become \\\"a mystery as tantalizing as any to unfold at 221B Baker Street,\\\" the fictional den of Holmes and his fellow-sleuth, Dr. Watson.\\n\\nNot long after Green launched his investigation, he discovered that one of Conan Doyle's five children, Adrian, had, with the other heirs' agreement, stashed the papers in a locked room of a ch\\u00e2teau that he owned in Switzerland. Green then learned that Adrian had spirited some of the papers out of the ch\\u00e2teau without his siblings' knowledge, hoping to sell them to collectors. In the midst of this scheme, he died of a heart attack\\u2014giving rise to the legend of the curse. After Adrian's death, the papers apparently vanished. And whenever Green tried to probe further he found himself caught in an impenetrable web of heirs\\u2014including a self-styled Russian princess\\u2014who seemed to have deceived and double-crossed each other in their efforts to control the archive.\\n\\nFor years, Green continued to sort through evidence and interview relatives, until one day the muddled trail led to London\\u2014and the doorstep of Jean Conan Doyle, the youngest of the author's children. Tall and elegant, with silver hair, she was an imposing woman in her late sixties. (\\\"Something very strong and forceful seems to be at the back of that wee body,\\\" her father had written of Jean when she was five. \\\"Her will is tremendous.\\\") Whereas her brother Adrian had been kicked out of the British Navy for insubordination, and her elder brother Denis was a playboy who had\"\n },\n {\n \"docid\": \"32872\",\n \"score\": 0.43913784623146057,\n \"snippet\": \"---\\ntitle: 39 Clues - A Complete List!\\nauthor: Rick Riordan\\ndate: 2000-01-01\\n---\\n39 Clues - A Complete List!\\n\\nThere are so many books in the puzzle mystery series 39 clues, and they all have different authors, but don't worry, we've got you covered with this comprehensive list of all the books in order. Let the fun begin!\\n\\nP.S.- Remember: a lot of these titles are also available in Audiobook, E-book, and E-audio! Just search the catalog!\\n\\nThe Maze Of Bones\\n\\nAuthor(s):\\n\\nDescription:\\n\\nWhat would happen if you discovered that your family was one of the most powerful in human history? What if you were told that the source of the family's power was hidden around the world, in the form of 39 clues? What if you were given a choice - take a million dollars and walk away ... or get the first clue? If you're Amy and Dan Cahill, you take the clue - and begin a very dangerous race.\\n\\nFormat:\\n\\nBook\\n\\nCall Number:\\n\\nJ FIC Rio\\n\\nOne False Note\\n\\nAuthor(s):\\n\\nDescription:\\n\\nA million dollars, or a clue? Police report a break-in at an elite hotel, and the suspects sound suspiciously like Amy and Dan. There's a car and speedboat chase and an angry mob! When there's a Clue on the line, anything can happen.\\n\\nFormat:\\n\\nBook\\n\\nCall Number:\\n\\nJ FIC Kor\\n\\nThe Sword Thief\\n\\nAuthor(s):\\n\\nDescription:\\n\\nWhen Amy and Dan Cahill's quest to find the million dollars takes them to Japan, they must decide whether or not to enter into an alliance with their uncle, Alastair Oh, whose motives for helping them are extremely questionable.\\n\\nFormat:\\n\\nBook\\n\\nCall Number:\\n\\nJ FIC Ler\\n\\nBeyond The Grave\\n\\nAuthor(s):\\n\\nDescription:\\n\\nA Clue found in Book 3 sends Amy and Dan jetting off to find out just what's behind the fierce rivalry between the Tomas and Ekaterina branches of the Cahill family. Was a Clue stolen from the Tomas branch? Where is it now? And most important, can Amy and Dan get their hands on it before their rivals do?\\n\\nFormat:\\n\\nBook\\n\\nCall Number:\\n\\nJ FIC Wat\\n\\nThe Black Circle\\n\\nAuthor(s):\\n\\nDescription:\\n\\n\\\"Where are Amy and Dan Cahill? The two kids were last seen in Egypt, hunting for one of the 39 Clues that could make them\"\n },\n {\n \"docid\": \"67868\",\n \"score\": 0.4347360134124756,\n \"snippet\": \"---\\ntitle: 25 of the best spy thrillers\\ndate: 2024-01-09\\n---\\nBooks\\n\\n25 of the best spy thrillers\\n\\nWe can't resist a dip into the mysterious world of the spy thriller. With secrets, lies, conspiracies and undercover plots abound, these books span the national and the international, blending the personal and the political and showing how they are inextricably linked.\\n\\nFrom the classic to the contemporary, here are some of the best spy thrillers around.\\n\\nThe best spy thriller books\\n\\nCasino Royale by Ian Fleming\\n\\nCasino Royale by Ian Fleming\\n\\nIn Casino Royale, the first of Ian Fleming's 007 adventures, a game of cards is James Bond's only chance to bring down Le Chiffre, French communist and paymaster of the Soviet murder organization SMERSH. But Bond soon discovers that there is far more at stake than money.\\n\\nThis is a book that's packed will thrills and suspense. It's an incredibly satisfying read that showcases everything we know and love about the Bond stories \\u2013 chilling, sadistic villains, sensuous, fiery love affairs and the smooth, sophisticated secret agent himself.\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nThe Spy Coast by Tess Gerritsen\\n\\nMaggie Bird lives a quiet life as a retiree in a seaside town. She never talks about her past life as a former spy. But when an unidentified body is left on Maggie's doorway, she knows it's a calling card from old times. Enter the 'Martini Club' \\u2013 Maggie's group of former spy friends. Together, they must solve the mystery and stay one step ahead of law enforcement.\\n\\nThis is the first instalment in a brand-new espionage series from Tess Gerritsen, author of the beloved Rizzoli and Isles detective series.\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThe Day of the Jackal by Frederick Forsyth\\n\\nThis book by former RAF pilot and investigative journalist Frederick Forsyth is one of the most celebrated thrillers ever written. It's intelligent, chilling and 100% unputdownable.\\n\\nIt is 1963 and an anonymous Englishman has been hired by the O.A.S. to murder General de Galle. A failed attempt means the target will be nearly impossible to get to. But this latest plot involves a lethal weapon: an assassin of legendary talent. Known only as The Jackal, this remorseless and deadly killer must be stopped \\u2013 but\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "4db7a864-4361-47ec-9f85-24cd1321aa74", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.377825, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 18, "action": {"name": "search", "arguments": {"query": "Robert Thier Storm Silence Ambrose Lilly Linton climax shipwreck island rescue save"}, "id": "816bfa41-a34b-4252-bbae-91cb75e8736b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.66776, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 18, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.6299043893814087,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5731695890426636,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.5616115927696228,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"29859\",\n \"score\": 0.5189592242240906,\n \"snippet\": \"---\\ntitle: The Tides of Time\\nauthor: Sarah M Eden this is a placeholder\\ndate: 2025-01-01\\n---\\nWhat do you think?\\n\\nRate this book\\n\\nIn 1793, a storm propels Lili forward through time, kindling a love that transcends the ages.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And he can sense that she is not telling him something of great import to her. Only Armitage's grandfather, a man seasoned by the mysteries of the sea, can seem to penetrate Lili's defenses to offer her support. But as Lili heals from the physical and emotional wounds of her ordeal and Armitage continues to offer light and safety to her, a tender friendship blossoms between the two.\\n\\nYet the shadow of danger looms as the threat that chased Lili from France all those years ago reemerges in her new present. Together Lili and Armitage must navigate the challenges of a romance that grows to defy the boundaries of time and the perils that reach across the decades to ensnare Lili. As the storm clouds gather, Lili and Armitage face the ultimate test\\u2014discovering whether their bond is strong enough to rewrite the pages of history itself to save them and their love.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And\"\n },\n {\n \"docid\": \"72764\",\n \"score\": 0.5175468325614929,\n \"snippet\": \"---\\ntitle: Overboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\nauthor: Michael J Tougias\\ndate: 2010-03-16\\n---\\n-24% $18.15$18.15\\n\\nDelivery Sunday, June 15\\n\\nShips from: Amazon Sold by: Bookdream\\n\\n$13.79$13.79\\n\\nDelivery Thursday, June 19\\n\\nShips from: Amazon Sold by: Zoom Books Company\\n\\nReturn this item for free\\n\\nFree returns are available for the shipping address you chose. You can return the item for any reason in new and unused condition: no return shipping charges.\\n\\nLearn more about free returns.- Go to your orders and start the return\\n\\n- Select your preferred free shipping option\\n\\n- Drop off and leave!\\n\\nSorry, there was a problem.\\n\\nThere was an error retrieving your Wish Lists. Please try again.Sorry, there was a problem.\\n\\nList unavailable.Download the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nOverboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\n\\nPurchase options and add-ons\\n\\nIn May 2005, Tom Tighe, captain of a forty-five-foot-long sailboat named the Almeisan, and his first mate, Loch Reidy, welcomed three new crewmembers for a five-day voyage from Connecticut across the blue waters of the Gulf Stream to sun-drenched Bermuda. The new crew included forty-six-year-old Kathy Gilchrist, seventy-year-old Ron Burd, and thirty-four-year-old Chris Ferrer. Although Tighe had made the trip forty-eight times, with Reidy accompanying him on twenty of those voyages, the rest of the crew had joined to learn more about offshore sailing.\\n\\nFour days into the voyage, an enormous storm struck, sweeping two of the crew into the towering sea. The remaining crewmembers managed to stay aboard the vessel as it was slowly torn apart by the rampaging ocean. Overboard! follows the simultaneous desperate struggles of both those still on the boat and those fighting for their lives in the sea.\\n\\nThe Coast Guard, alerted to the Almeisan's\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "816bfa41-a34b-4252-bbae-91cb75e8736b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.66776, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 19, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" \"file\" \"thief\" accused \"stolen\" Lilly Ambrose Dalgliesh island"}, "id": "f6ae43d2-ea12-4115-81ab-ce3b67f6bc2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.97542, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 19, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.5347017049789429,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5029191970825195,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"79197\",\n \"score\": 0.48140305280685425,\n \"snippet\": \"---\\ntitle: Mysterious Circumstances\\nauthor: David Grann\\ndate: 2004-12-13\\n---\\nRichard Lancelyn Green, the world's foremost expert on Sherlock Holmes, believed that he had finally solved the case of the missing papers. Over the past two decades, he had been looking for a trove of letters, diary entries, and manuscripts written by Sir Arthur Conan Doyle, the creator of Holmes. The archive was estimated to be worth nearly four million dollars, and was said by some to carry a deadly curse, like the one in the most famous Holmes story \\\"The Hound of the Baskervilles.\\\"\\n\\nThe papers had disappeared after Conan Doyle died, in 1930, and without them no one had been able to write a definitive biography\\u2014a task that Green was determined to complete. Many scholars feared that the archive had been discarded or destroyed; as the London Times noted earlier this year, its whereabouts had become \\\"a mystery as tantalizing as any to unfold at 221B Baker Street,\\\" the fictional den of Holmes and his fellow-sleuth, Dr. Watson.\\n\\nNot long after Green launched his investigation, he discovered that one of Conan Doyle's five children, Adrian, had, with the other heirs' agreement, stashed the papers in a locked room of a ch\\u00e2teau that he owned in Switzerland. Green then learned that Adrian had spirited some of the papers out of the ch\\u00e2teau without his siblings' knowledge, hoping to sell them to collectors. In the midst of this scheme, he died of a heart attack\\u2014giving rise to the legend of the curse. After Adrian's death, the papers apparently vanished. And whenever Green tried to probe further he found himself caught in an impenetrable web of heirs\\u2014including a self-styled Russian princess\\u2014who seemed to have deceived and double-crossed each other in their efforts to control the archive.\\n\\nFor years, Green continued to sort through evidence and interview relatives, until one day the muddled trail led to London\\u2014and the doorstep of Jean Conan Doyle, the youngest of the author's children. Tall and elegant, with silver hair, she was an imposing woman in her late sixties. (\\\"Something very strong and forceful seems to be at the back of that wee body,\\\" her father had written of Jean when she was five. \\\"Her will is tremendous.\\\") Whereas her brother Adrian had been kicked out of the British Navy for insubordination, and her elder brother Denis was a playboy who had\"\n },\n {\n \"docid\": \"82350\",\n \"score\": 0.4643110930919647,\n \"snippet\": \"---\\ntitle: \\u2630OTHER BOOKS\\ndate: 2023-01-05\\n---\\nThe Sleeping and the Dead\\n\\nIn this vivid psychological suspense novel, a diving instructor makes a gruesome discovery in Cranwell Lake - the body of a teenager who has clearly been in the water for many years.\\n\\nDetective Peter Porteous is called to Cranwell Lake where the body of a teenager has been discovered. After trawling through the missing persons files, he comes to the conclusion that the corpse is Michael Grey, an enigmatic and secretive young man who was reported missing by his foster parents in 1972.\\n\\nThe news report that a body has been found leaves prison officer Hannah Morton in shock. Michael had been her boyfriend, and she had been with him the night he disappeared. And now the discovery is bringing back dreaded and long buried memories from her past ...\\n\\nThe Sleeping and the Dead was first published in the UK in 2001. It was reissued by Pan MacMillan in their 'Ann Cleeves Classic Crime' series on 5th January 2023. Order a copy via the publisher's website, or from any bookshop or library (ISBN: 978-1-5290-7051-4).\\n\\nThe audiobook, read by John Telfer, is available in a choice of formats from the Reading House, or as an audio download from Amazon.\\n\\nBurial of Ghosts\\n\\nFor Lizzie Bartholomew, a holiday in Morocco will change life forever. But not in the way she had hoped...\\n\\nLizzie had planned her trip to Marrakech as the perfect escape from her life - and her nightmares - in Northumberland. Abandoned as a baby, and having spent her childhood moving between foster homes, Lizzie certainly has much to escape from. And for Lizzie, Morocco is the exotic paradise that she had imagined. Especially when she finds herself on a bus sitting next to a fellow tourist, who is also travelling to fulfil his dreams.\\n\\nAfter a brief affair, Lizzie returns to England. In the days that follow, she is distracted by thoughts of her mysterious lover, hoping against hope that Philip might come and find her. But suddenly she receives a letter from a firm of solicitors. Philip Samson has died. In his will, he has left Lizzie a gift of \\u00a315,000. But there are conditions attached to this unexpected legacy, conditions that will alter the\"\n },\n {\n \"docid\": \"66065\",\n \"score\": 0.46270695328712463,\n \"snippet\": \"---\\ntitle: James Patterson \\u2013 Books \\u2013 Standalone Thrillers\\ndate: 1996-12-01\\n---\\nJames Patterson \\u2013 Books \\u2013 Standalone Thrillers\\n\\nSTANDALONE THRILLERS\\n\\nTitles List\\n\\n-\\n\\nA $100 million painting. A previously unknown Picasso is discovered in the attic of a French villa. Everyone wants to possess it. Filthy-rich Manhattan art people. Organized crime bosses. Power-hungry government officials. A notorious forger. A glamorous twenty-two-year-old art thief. Only one person\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nFrom #1 New York Times bestselling author James Patterson, six kidnappers pursue a whopping ten-figure payout in this thrilling novel featuring \\\"breakneck pacing and loop-the-loop plotting\\\" (Publishers Weekly). Five members of a billionaire's family. In different locations. All kidnapped at\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\n\\\"Bill Clinton reveals key White House details in murderous new political thriller\\u2026 It's a twisty thriller with plenty of inside jobs, political sabotage and many, many deaths.\\\" \\u2013USA TodayAmerica has a powerful new president\\u2026 And her husband's on trial for murder. Clinton\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\n\\\"Consider blocking out a few hours of uninterrupted reading time\\\" for The Writer, #1 New York Times bestselling author James Patterson's \\\"Excellent\\u2026perfectly executed\\u2026genuinely suspenseful\\\" (Booklist) thriller about a true-crime author swept up in a murder plot.\\\"Entertaining\\u2026one gonzo plot twist follows the next\\u2026loads of fun.\\\" (Publishers Weekly)\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nThe police are called to stop two teens ransacking a small-town grocery store. They have no identification. And they won't speak a word. And the small town of Kokanee Creek is suddenly plunged into a sinister mystery that only #1 bestseller James Patterson\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nIn this thrilling novel from the authors of the bestselling Death of the Black Widow, the police of a small New England town are confronted with a baffling series of crimes. Hollows Bend, New Hampshire, is a picture-perfect New England\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nIt's the perfect getaway. But the past will always find you . . . When ex-cop Bill Robinson takes over The Inn by the Sea, all he wants is a quiet escape from the city. But when a crime boss\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "f6ae43d2-ea12-4115-81ab-ce3b67f6bc2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.97542, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 20, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" summary plot Lilly accused thief Ambrose save rescue sea isle Marbeau climax"}, "id": "2c22b0f4-7e6d-443b-a46f-d7e3ff465bca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.30073, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 20, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.6362643241882324,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5757802724838257,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.5378220081329346,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"29859\",\n \"score\": 0.5128501653671265,\n \"snippet\": \"---\\ntitle: The Tides of Time\\nauthor: Sarah M Eden this is a placeholder\\ndate: 2025-01-01\\n---\\nWhat do you think?\\n\\nRate this book\\n\\nIn 1793, a storm propels Lili forward through time, kindling a love that transcends the ages.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And he can sense that she is not telling him something of great import to her. Only Armitage's grandfather, a man seasoned by the mysteries of the sea, can seem to penetrate Lili's defenses to offer her support. But as Lili heals from the physical and emotional wounds of her ordeal and Armitage continues to offer light and safety to her, a tender friendship blossoms between the two.\\n\\nYet the shadow of danger looms as the threat that chased Lili from France all those years ago reemerges in her new present. Together Lili and Armitage must navigate the challenges of a romance that grows to defy the boundaries of time and the perils that reach across the decades to ensnare Lili. As the storm clouds gather, Lili and Armitage face the ultimate test\\u2014discovering whether their bond is strong enough to rewrite the pages of history itself to save them and their love.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And\"\n },\n {\n \"docid\": \"7830\",\n \"score\": 0.470270037651062,\n \"snippet\": \"---\\ntitle: The Light Between Oceans\\nauthor: Author this is a placeholder\\ndate: 2012-03-20\\n---\\nWhat do you think?\\n\\nRate this book\\n\\nGoodreads Choice Award\\n\\nWinner for Readers' Favorite Historical Fiction (2012)Librarian's note: An alternate cover edition can be found here\\n\\nAustralia, 1926. After four harrowing years fighting on the Western Front, Tom Sherbourne returns home to take a job as the lighthouse keeper on Janus Rock, nearly half a day's journey from the coast. To this isolated island, where the supply boat comes once a season and shore leaves are granted every other year at best, Tom brings a young, bold, and loving wife, Isabel. Years later, after two miscarriages and one stillbirth, the grieving Isabel hears a baby's cries on the wind. A boat has washed up onshore carrying a dead man and a living baby.\\n\\nTom, whose records as a lighthouse keeper are meticulous and whose moral principles have withstood a horrific war, wants to report the man and infant immediately. But Isabel has taken the tiny baby to her breast. Against Tom's judgment, they claim her as their own and name her Lucy. When she is two, Tom and Isabel return to the mainland and are reminded that there are other people in the world. Their choice has devastated one of them.\\n\\nM. L. Stedman's mesmerizing, beautifully written debut novel seduces us into accommodating Isabel's decision to keep this \\\"gift from God.\\\" And we are swept into a story about extraordinarily compelling characters seeking to find their North Star in a world where there is no right answer, where justice for one person is another's tragic loss.\\n\\nAustralia, 1926. After four harrowing years fighting on the Western Front, Tom Sherbourne returns home to take a job as the lighthouse keeper on Janus Rock, nearly half a day's journey from the coast. To this isolated island, where the supply boat comes once a season and shore leaves are granted every other year at best, Tom brings a young, bold, and loving wife, Isabel. Years later, after two miscarriages and one stillbirth, the grieving Isabel hears a baby's cries on the wind. A boat has washed up onshore carrying a dead man and a living baby.\\n\\nTom, whose records as a lighthouse keeper are meticulous and whose moral principles have withstood a horrific war, wants to report the man and infant immediately\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "2c22b0f4-7e6d-443b-a46f-d7e3ff465bca", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.30073, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 21, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" ending Ile Marbeau island sea rescue Ambrose saves Lilly climax"}, "id": "7f1241b2-d1a6-40ee-a157-07e9f09e9d17", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 3.644375, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 21, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.550203800201416,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"29859\",\n \"score\": 0.4979217052459717,\n \"snippet\": \"---\\ntitle: The Tides of Time\\nauthor: Sarah M Eden this is a placeholder\\ndate: 2025-01-01\\n---\\nWhat do you think?\\n\\nRate this book\\n\\nIn 1793, a storm propels Lili forward through time, kindling a love that transcends the ages.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And he can sense that she is not telling him something of great import to her. Only Armitage's grandfather, a man seasoned by the mysteries of the sea, can seem to penetrate Lili's defenses to offer her support. But as Lili heals from the physical and emotional wounds of her ordeal and Armitage continues to offer light and safety to her, a tender friendship blossoms between the two.\\n\\nYet the shadow of danger looms as the threat that chased Lili from France all those years ago reemerges in her new present. Together Lili and Armitage must navigate the challenges of a romance that grows to defy the boundaries of time and the perils that reach across the decades to ensnare Lili. As the storm clouds gather, Lili and Armitage face the ultimate test\\u2014discovering whether their bond is strong enough to rewrite the pages of history itself to save them and their love.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.48678192496299744,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"72764\",\n \"score\": 0.4727753698825836,\n \"snippet\": \"---\\ntitle: Overboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\nauthor: Michael J Tougias\\ndate: 2010-03-16\\n---\\n-24% $18.15$18.15\\n\\nDelivery Sunday, June 15\\n\\nShips from: Amazon Sold by: Bookdream\\n\\n$13.79$13.79\\n\\nDelivery Thursday, June 19\\n\\nShips from: Amazon Sold by: Zoom Books Company\\n\\nReturn this item for free\\n\\nFree returns are available for the shipping address you chose. You can return the item for any reason in new and unused condition: no return shipping charges.\\n\\nLearn more about free returns.- Go to your orders and start the return\\n\\n- Select your preferred free shipping option\\n\\n- Drop off and leave!\\n\\nSorry, there was a problem.\\n\\nThere was an error retrieving your Wish Lists. Please try again.Sorry, there was a problem.\\n\\nList unavailable.Download the free Kindle app and start reading Kindle books instantly on your smartphone, tablet, or computer - no Kindle device required.\\n\\nRead instantly on your browser with Kindle for Web.\\n\\nUsing your mobile phone camera - scan the code below and download the Kindle app.\\n\\nAudible sample\\n\\nFollow the author\\n\\nOK\\n\\nOverboard!: A True Blue-water Odyssey of Disaster and Survival Hardcover \\u2013 March 16, 2010\\n\\nPurchase options and add-ons\\n\\nIn May 2005, Tom Tighe, captain of a forty-five-foot-long sailboat named the Almeisan, and his first mate, Loch Reidy, welcomed three new crewmembers for a five-day voyage from Connecticut across the blue waters of the Gulf Stream to sun-drenched Bermuda. The new crew included forty-six-year-old Kathy Gilchrist, seventy-year-old Ron Burd, and thirty-four-year-old Chris Ferrer. Although Tighe had made the trip forty-eight times, with Reidy accompanying him on twenty of those voyages, the rest of the crew had joined to learn more about offshore sailing.\\n\\nFour days into the voyage, an enormous storm struck, sweeping two of the crew into the towering sea. The remaining crewmembers managed to stay aboard the vessel as it was slowly torn apart by the rampaging ocean. Overboard! follows the simultaneous desperate struggles of both those still on the boat and those fighting for their lives in the sea.\\n\\nThe Coast Guard, alerted to the Almeisan's\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.45753324031829834,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "7f1241b2-d1a6-40ee-a157-07e9f09e9d17", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 3.644375, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 22, "action": {"name": "get_document", "arguments": {"docid": "25654"}, "id": "4e09f051-6c36-479f-a15c-cbdc65ec5c1c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 4.004180000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 22, "observation": {"result": "{\n \"docid\": \"25654\",\n \"text\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014)\\n\\nSpin-off\\n\\nA spin-off story takes place after the seventh book of the main series. It focuses on the adventure of Lilly's friend, Amy Weston, and Lord Patrick Day.\\n\\n- Lord Day and Lady Night (2019 | \\u2014)\\n\\nAlternative spin-off\\n\\nSometime while Storm and Silence was being written, the author was offered an opportunity to write a spin-off story in an alternate reality by Lionsgate Films to promote its film, The Age of Adaline. The story is set within the theme of the film and is non-canon in regards to the main series.\\n\\n- Up and Down (2015 | \\u2014)\\n\\nTrivia\\n\\n- The inspiration for the series stemmed from the Suffragist and Chartist movements in the 19th-century that the author extensively studied during university.[1]\\n\\n- Publishers did not want to publish the first book of the main series as they thought it was too long. Thus, the author decided to self-publish and was able to cover publication costs through crowdfunding on Indiegogo.[2] The subsequent books in the main series followed suit and have also been self-published.\\n\\nGallery\\n\\nThe front covers of the books from the main series, featuring Mr Ambrose.\\n\\nReferences\\n\\n- \\u2191 \\n\\n- \\u2191 \"\n}", "invoking_actions": [{"name": "get_document", "arguments": {}, "id": "4e09f051-6c36-479f-a15c-cbdc65ec5c1c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 4.004180000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 23, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" detailed plot summary Ambrose Lilly document Dalgliesh encrypted file stolen accusation France abroad"}, "id": "07322661-e5a0-4fdd-8e07-7a7c83563108", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 4.369885000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 23, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.5491895079612732,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5177220702171326,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"79197\",\n \"score\": 0.49903059005737305,\n \"snippet\": \"---\\ntitle: Mysterious Circumstances\\nauthor: David Grann\\ndate: 2004-12-13\\n---\\nRichard Lancelyn Green, the world's foremost expert on Sherlock Holmes, believed that he had finally solved the case of the missing papers. Over the past two decades, he had been looking for a trove of letters, diary entries, and manuscripts written by Sir Arthur Conan Doyle, the creator of Holmes. The archive was estimated to be worth nearly four million dollars, and was said by some to carry a deadly curse, like the one in the most famous Holmes story \\\"The Hound of the Baskervilles.\\\"\\n\\nThe papers had disappeared after Conan Doyle died, in 1930, and without them no one had been able to write a definitive biography\\u2014a task that Green was determined to complete. Many scholars feared that the archive had been discarded or destroyed; as the London Times noted earlier this year, its whereabouts had become \\\"a mystery as tantalizing as any to unfold at 221B Baker Street,\\\" the fictional den of Holmes and his fellow-sleuth, Dr. Watson.\\n\\nNot long after Green launched his investigation, he discovered that one of Conan Doyle's five children, Adrian, had, with the other heirs' agreement, stashed the papers in a locked room of a ch\\u00e2teau that he owned in Switzerland. Green then learned that Adrian had spirited some of the papers out of the ch\\u00e2teau without his siblings' knowledge, hoping to sell them to collectors. In the midst of this scheme, he died of a heart attack\\u2014giving rise to the legend of the curse. After Adrian's death, the papers apparently vanished. And whenever Green tried to probe further he found himself caught in an impenetrable web of heirs\\u2014including a self-styled Russian princess\\u2014who seemed to have deceived and double-crossed each other in their efforts to control the archive.\\n\\nFor years, Green continued to sort through evidence and interview relatives, until one day the muddled trail led to London\\u2014and the doorstep of Jean Conan Doyle, the youngest of the author's children. Tall and elegant, with silver hair, she was an imposing woman in her late sixties. (\\\"Something very strong and forceful seems to be at the back of that wee body,\\\" her father had written of Jean when she was five. \\\"Her will is tremendous.\\\") Whereas her brother Adrian had been kicked out of the British Navy for insubordination, and her elder brother Denis was a playboy who had\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.4766990840435028,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"66065\",\n \"score\": 0.4661906063556671,\n \"snippet\": \"---\\ntitle: James Patterson \\u2013 Books \\u2013 Standalone Thrillers\\ndate: 1996-12-01\\n---\\nJames Patterson \\u2013 Books \\u2013 Standalone Thrillers\\n\\nSTANDALONE THRILLERS\\n\\nTitles List\\n\\n-\\n\\nA $100 million painting. A previously unknown Picasso is discovered in the attic of a French villa. Everyone wants to possess it. Filthy-rich Manhattan art people. Organized crime bosses. Power-hungry government officials. A notorious forger. A glamorous twenty-two-year-old art thief. Only one person\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nFrom #1 New York Times bestselling author James Patterson, six kidnappers pursue a whopping ten-figure payout in this thrilling novel featuring \\\"breakneck pacing and loop-the-loop plotting\\\" (Publishers Weekly). Five members of a billionaire's family. In different locations. All kidnapped at\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\n\\\"Bill Clinton reveals key White House details in murderous new political thriller\\u2026 It's a twisty thriller with plenty of inside jobs, political sabotage and many, many deaths.\\\" \\u2013USA TodayAmerica has a powerful new president\\u2026 And her husband's on trial for murder. Clinton\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\n\\\"Consider blocking out a few hours of uninterrupted reading time\\\" for The Writer, #1 New York Times bestselling author James Patterson's \\\"Excellent\\u2026perfectly executed\\u2026genuinely suspenseful\\\" (Booklist) thriller about a true-crime author swept up in a murder plot.\\\"Entertaining\\u2026one gonzo plot twist follows the next\\u2026loads of fun.\\\" (Publishers Weekly)\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nThe police are called to stop two teens ransacking a small-town grocery store. They have no identification. And they won't speak a word. And the small town of Kokanee Creek is suddenly plunged into a sinister mystery that only #1 bestseller James Patterson\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nIn this thrilling novel from the authors of the bestselling Death of the Black Widow, the police of a small New England town are confronted with a baffling series of crimes. Hollows Bend, New Hampshire, is a picture-perfect New England\\u2026 Read More\\n\\nLittle, Brown and Company -\\n\\nIt's the perfect getaway. But the past will always find you . . . When ex-cop Bill Robinson takes over The Inn by the Sea, all he wants is a quiet escape from the city. But when a crime boss\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "07322661-e5a0-4fdd-8e07-7a7c83563108", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 4.369885000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 24, "action": {"name": "get_document", "arguments": {"docid": "71659"}, "id": "700da1df-4a6c-4d71-b05b-f6f07ae028f8", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 4.753415, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 24, "observation": {"result": "{\n \"docid\": \"71659\",\n \"text\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book has a really interesting concept. The thing that got my attention most is Lilly being a feminist. I, aware of the differences in gender roles between today and the past, was really intrigued. However, I thought there were many unnecessary parts that made the book too long, and sometimes exhausting to read. While I understand that the book was originally published on Wattpad (which means that at least a chapter is published every other day or depending on the author's preferred schedule), I thought it was repetitive and still needs more editing for the final copy. I have to admit that I skipped a lot of pages (I'm allowed to, right?) but didn't miss anything important.\\nI also find the main character nosy and irrational at some points. For someone claiming to be mature, she acts like a little kid whose candies got stolen! Her persistence could be a little too much and just makes her look stubborn, not strong.\\nIt took me a while before getting hooked. Actually, based on my notes, that's 800/2659 pages in an eBook. WHICH IS A LOT.\\nI also appreciate the writer's attention to detail and the book being historically accurate, plus the annotations were really helpful (and cool) because you get to learn stuff about history.\\nThis book is very promising, but I thought it still has lots of room for improvement. The ending was also so disappointing, at the end I was like\\u2026 that's it?I mean, I know there's a sequel coming up, but a first book should make you feel satisfied and keen to read the next one! This book didn't. However, I would still read the second book (because I'm a rebel) in hopes of getting answers to my questions, if and only if it's 400 pages and below. Ha.\\nShare this:\\n\\nTweet\\nShare on Tumblr\\nClick to email a link to a friend (Opens in new window)\\n Email\\n\\nLike Loading...\\nRelated\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book has a really interesting concept. The thing that got my attention most is Lilly being a feminist. I, aware of the differences in gender roles between today and the past, was really intrigued. However, I thought there were many unnecessary parts that made the book too long, and sometimes exhausting to read. While I understand that the book was originally published on Wattpad (which means that at least a chapter is published every other day or depending on the author's preferred schedule), I thought it was repetitive and still needs more editing for the final copy. I have to admit that I skipped a lot of pages (I'm allowed to, right?) but didn't miss anything important.\\nI also find the main character nosy and irrational at some points. For someone claiming to be mature, she acts like a little kid whose candies got stolen! Her persistence could be a little too much and just makes her look stubborn, not strong.\\nIt took me a while before getting hooked. Actually, based on my notes, that's 800/2659 pages in an eBook. WHICH IS A LOT.\\nI also appreciate the writer's attention to detail and the book being historically accurate, plus the annotations were really helpful (and cool) because you get to learn stuff about history.\\nThis book is very promising, but I thought it still has lots of room for improvement. The ending was also so disappointing, at the end I was like\\u2026 that's it?I mean, I know there's a sequel coming up, but a first book should make you feel satisfied and keen to read the next one! This book didn't. However, I would still read the second book (because I'm a rebel) in hopes of getting answers to my questions, if and only if it's 400 pages and below. Ha.\\nShare this:\\n\\nTweet\\nShare on Tumblr\\nClick to email a link to a friend (Opens in new window)\\n Email\\n\\nLike Loading...\\nRelated\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book has a really interesting concept. The thing that got my attention most is Lilly being a feminist\"\n}", "invoking_actions": [{"name": "get_document", "arguments": {}, "id": "700da1df-4a6c-4d71-b05b-f6f07ae028f8", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 4.753415, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 25, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" review book full summary stolen file Lilly secretary Ambrose Dalgliesh France storm sea rescue"}, "id": "da4e4b42-8b56-440e-97a9-b0f74486530b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 5.149675000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 25, "observation": {"result": "[\n {\n \"docid\": \"90027\",\n \"score\": 0.574070930480957,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"25654\",\n \"score\": 0.5394290685653687,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"29859\",\n \"score\": 0.5252851247787476,\n \"snippet\": \"---\\ntitle: The Tides of Time\\nauthor: Sarah M Eden this is a placeholder\\ndate: 2025-01-01\\n---\\nWhat do you think?\\n\\nRate this book\\n\\nIn 1793, a storm propels Lili forward through time, kindling a love that transcends the ages.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And he can sense that she is not telling him something of great import to her. Only Armitage's grandfather, a man seasoned by the mysteries of the sea, can seem to penetrate Lili's defenses to offer her support. But as Lili heals from the physical and emotional wounds of her ordeal and Armitage continues to offer light and safety to her, a tender friendship blossoms between the two.\\n\\nYet the shadow of danger looms as the threat that chased Lili from France all those years ago reemerges in her new present. Together Lili and Armitage must navigate the challenges of a romance that grows to defy the boundaries of time and the perils that reach across the decades to ensnare Lili. As the storm clouds gather, Lili and Armitage face the ultimate test\\u2014discovering whether their bond is strong enough to rewrite the pages of history itself to save them and their love.\\n\\nFleeing the clutches of Robespierre's revolutionary Tribunal in France, Lili Minet makes a desperate escape on a ship headed to England, but her dangerous flight takes an unexpected turn when a violent storm catapults her off the ship and eighty years into the future, leaving her stranded in the unfamiliar world of 1873 England.\\n\\nWhen lighthouse keeper Armitage Pierce rescues a woman from the tumultuous sea, he does not anticipate her silent, cold response to his gallant efforts. Though he is wary of this woman and her odd behaviors, he finds she is just as wary of him. And\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.5212535262107849,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"82350\",\n \"score\": 0.49659380316734314,\n \"snippet\": \"---\\ntitle: \\u2630OTHER BOOKS\\ndate: 2023-01-05\\n---\\nThe Sleeping and the Dead\\n\\nIn this vivid psychological suspense novel, a diving instructor makes a gruesome discovery in Cranwell Lake - the body of a teenager who has clearly been in the water for many years.\\n\\nDetective Peter Porteous is called to Cranwell Lake where the body of a teenager has been discovered. After trawling through the missing persons files, he comes to the conclusion that the corpse is Michael Grey, an enigmatic and secretive young man who was reported missing by his foster parents in 1972.\\n\\nThe news report that a body has been found leaves prison officer Hannah Morton in shock. Michael had been her boyfriend, and she had been with him the night he disappeared. And now the discovery is bringing back dreaded and long buried memories from her past ...\\n\\nThe Sleeping and the Dead was first published in the UK in 2001. It was reissued by Pan MacMillan in their 'Ann Cleeves Classic Crime' series on 5th January 2023. Order a copy via the publisher's website, or from any bookshop or library (ISBN: 978-1-5290-7051-4).\\n\\nThe audiobook, read by John Telfer, is available in a choice of formats from the Reading House, or as an audio download from Amazon.\\n\\nBurial of Ghosts\\n\\nFor Lizzie Bartholomew, a holiday in Morocco will change life forever. But not in the way she had hoped...\\n\\nLizzie had planned her trip to Marrakech as the perfect escape from her life - and her nightmares - in Northumberland. Abandoned as a baby, and having spent her childhood moving between foster homes, Lizzie certainly has much to escape from. And for Lizzie, Morocco is the exotic paradise that she had imagined. Especially when she finds herself on a bus sitting next to a fellow tourist, who is also travelling to fulfil his dreams.\\n\\nAfter a brief affair, Lizzie returns to England. In the days that follow, she is distracted by thoughts of her mysterious lover, hoping against hope that Philip might come and find her. But suddenly she receives a letter from a firm of solicitors. Philip Samson has died. In his will, he has left Lizzie a gift of \\u00a315,000. But there are conditions attached to this unexpected legacy, conditions that will alter the\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "da4e4b42-8b56-440e-97a9-b0f74486530b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 5.149675000000001, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 26, "action": {"name": "search", "arguments": {"query": "\"Storm and Silence\" Robert Thier Wattpad published March 2016 first book published"}, "id": "63dc0cc0-c7bc-48ac-8ecd-81f6e603f57b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 5.5642700000000005, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 26, "observation": {"result": "[\n {\n \"docid\": \"25654\",\n \"score\": 0.7052953243255615,\n \"snippet\": \"---\\ntitle: Storm and Silence (Book Series)\\ndate: 2015-08-12\\n---\\nStorm and Silence is a historical fiction and romance book series written by Robert Thier. As of 2018, there are eight books in the entire series canonically. The series has gained extensive popularity online, with over one hundred million cumulative reads on the online community Wattpad and over six million reads on the Radish Fiction App.\\n\\nThe story follows the adventures of Lilly Linton, a fiery feminist living in 19th-century London, determined to earn her freedom and independence when her life is turned upside down after a chance encounter with the ruthless financial business magnate, Mr Ambrose.\\n\\nBooks\\n\\nMain series\\n\\nCurrently, there are six books in the main series. For each book initially, the chapters are individually uploaded on a weekly basis onto Radish and then Wattpad. When completed, they are edited and published as an individual book both electronically and in paperback. However, the first book is an exception to this as chapters were uploaded to Wattpad only at the time and then the book was self-published since the author did not start using Radish until the second book.\\n\\nThe following is a chronological list of the main books formatted as: Title (year initially uploaded online | year officially published).\\n\\n- Storm and Silence (2014 | 2016)\\n\\n- In the Eye of the Storm (2016 | 2016)\\n\\n- Silence is Golden (2016 | 2017)\\n\\n- Silence Breaking (2017 | 2018)\\n\\n- Hunting for Silence (2018 | TBA)\\n\\n- Storm of Bells (2018 | TBA)\\n\\n- New Storm Rising (2020 | TBA)\\n\\n- Storm over the Caribbean\\n\\n- Silence no More\\n\\nPrequel\\n\\nThe prequel takes place shortly before the beginning of the first book in the main series. It is a stand-alone novella that was commissioned by Radish Fiction and is exclusively on the mobile app.\\n\\n- Before the Storm (2016 | \\u2014)\\n\\nSide story\\n\\nThe following work takes place in between the second and third book of the main series. It was written in thanks from the author to the fanbase for voting for the first book from the main series in the Romance category for the 2016 Goodreads Choice Awards.\\n\\n- Silent Night (2016 | \\u2014\"\n },\n {\n \"docid\": \"90027\",\n \"score\": 0.6753263473510742,\n \"snippet\": \"---\\ntitle: Storm and Silence\\ndate: 2014-04-02\\n---\\n| 'Blast, blast, blast!'\\n\\nThis article is a stub and thus, inadequate. Help Storm and Silence Wiki by expanding it. |\\n\\nStorm and Silence is the first novel in the eponymous series written by Robert Thier. It is free to read on the online community Wattpad and the Radish Fiction Mobile App. The first chapter was uploaded onto Wattpad on April 2014 and the novel was published electronically and in paperback on March 2016. It won the 2015 Award for Story of the Year and currently has a massive fanbase, with over 100 million reads on Wattpad.\\n\\nSynopsis[]\\n\\nWattpad Version[]\\n\\n\\\"It is your choice,\\\" he said, stepping so close to me that our lips were almost touching.\\n\\n\\\"Either do what I say - or get another job.\\\"\\n\\nMy heart stood still as I gazed up into his deep, dark, dangerous eyes...\\n\\nIn a world where women's only role in life is to sit at home and look pretty, Lilly is determined to fight for her freedom. There's only one problem: a powerful man blocking her way. [1]\\n\\nRadish/Published Version[]\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\n\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever...[2]\\n\\nPlot[]\\n\\nTBA\\n\\nChapters[]\\n\\nWattpad Version[]\\n\\n- Arrested for Good Manners\\n\\n- Ape Bobby\\n\\n- Who He Really Is\\n\\n- Sweet and Solid\\n\\n- Driving Me Wild(ly)\\n\\n- Empire House\\n\\n- His Indecent Demands\\n\\n- Inventing a Sibling and Getting Poked in the Eye\\n\\n- File Fight\\n\\n- The Worst Fate Imaginable\\n\\n- The Dragon's Den\\n\\n- Practicing Impertinence\\n\\n- Ballroom Battle\\n\\n- The Sins of Mr Rikkard Ambrose\\n\\n- It gets mushy-gushy\\n\\n- Unsuitable Suitors\\n\\n- Return to the Game\\n\\n- The Peril of Flowers\\n\\n- The\"\n },\n {\n \"docid\": \"84371\",\n \"score\": 0.5980746746063232,\n \"snippet\": \"---\\ntitle: Author Interview: Rob Thier Talks 'Storm and Silence'\\nauthor: Amber Topping\\ndate: 2024-12-13\\n---\\nToday, we're excited to have author Rob Thier (known to his fans as \\\"Sir Rob\\\") of 'Storm and Silence' and 'The Robber Knight Series' here to discuss his background and latest book!\\n\\nThank you for agreeing to do this interview. Can you tell us a little bit about yourself and how you came to be a writer?\\n\\nI must admit I started writing so early in life that I don't really remember when or how. Maybe when I was nine or ten years old? But it didn't develop into anything serious until I discovered online writing platforms like Wattpad and started studying at an English university a few years ago. Unlike my failed writing attempts in my native language, German, here for the first time I had an opportunity to share my writing with a big audience, ask feedback and gather experience. And my university studies gave me the practice I needed to perfect my grasp on the English language. From there it was just a small step to deciding that I wanted to be a professional writer, and that has been my dream ever since.\\n\\nHow does being a historian affect the way you write stories?\\n\\nIt has made me a little bit of a stickler for accuracy. I do a lot of research for my stories. Even when it is a contemporary story, I want to make sure that the characters behave in a realistic way. Even if I were to write a fantasy story, I would research various mythologies and create a world that is in some way related to real human ideas and conceptions out there in the world. It makes for a very interesting writing process because during the research you always discover things that you never knew before.\\n\\nCongratulations on your latest story, Storm and Silence. For those who aren't familiar, can you explain what it's about?\\n\\nThe story follows the adventures of 19-year-old feminist Lilly and her budding romance with the powerful and ruthless business mogul Rikkard Ambrose. A firm believer in the equality of men and women, Lilly is forced to dress up as a man in order to work for Mr. Ambrose and achieve her independence. The two of them butt heads all the time, but when an important document is stolen from Mr. Ambrose's safe, and he has fought a battle of wits and intrigue with his arch-enemy Lord Dalgliesh, she stands by him through thick and\"\n },\n {\n \"docid\": \"71659\",\n \"score\": 0.5406280755996704,\n \"snippet\": \"---\\ntitle: REVIEW: Storm and Silence by Robert Thier\\nauthor: Nikka\\ndate: 2016-04-03\\n---\\nP A P E R & T R E E S\\nA Book Blog\\n\\nREVIEW: Storm and Silence by Robert Thier\\nA copy of this book was provided by the authorin exchange for an honest review. This does not affect my opinion of the book or the content of my review.\\n\\nSeries: Storm and Silence #1Publication Date: March 19 2016 by Robert Thier\\nGenres: Romance, Historical Fiction\\nNumber of Pages: 596\\nMy Rating: \\u2605\\u2605\\u2605\\u2729\\u2729\\nAmazon // Book Depository // iBooksGoodreads\\n\\nFreedom \\u2013 that is what Lilly Linton wants most in life. Not marriage, not a brood of squalling brats, and certainly not love, thank you very much!\\nBut freedom is a rare commodity in 19th-century London, where girls are expected to spend their lives sitting at home, fully occupied with looking pretty. Lilly is at her wits' end \\u2013 until a chance encounter with a dark, dangerous and powerful stranger changes her life forever\\u2026\\nEnter the world of Mr Rikkard Ambrose, where the only rule is: Knowledge is power is time is money!\\n\\nThe story is set in 19th-century London where women, although dressed beautifully and often treated as fragile flowers, do not have the liberty to pursue their own interests. From education to marriage to rights, women were oppressed and seen as naive and incapable of living on their own.\\nAs Tiffany Stelle retold, \\\"She was expected to keep the house clean, cook the meals, raise the children, decorate the house, and keep her children and husband on the moral high ground. When a husband came home from work he was expecting a smile on his wife, who is dressed perfectly, a house that is spotless and bright, and a forgiving happy environment that would lure him into wanting to return home each day.\\\"\\nBut for Lilly Linton, this is not the way things are supposed to be. She is a suffragette, a feminist, and she needs freedom as much as the next guy needs his daily dose of vanity. So she did what a feminist woman did during the Victorian era\\u2014she pretended to be a man. But unlike any other woman, she was offered a job and she took it, for this could be the key to the freedom she's longing for.\\nThis book\"\n },\n {\n \"docid\": \"33677\",\n \"score\": 0.5101686716079712,\n \"snippet\": \"---\\ntitle: All Books\\ndate: 2025-01-01\\n---\\nInternational Bestselling Master of Suspense\\n\\nAll Books\\n\\nClick any header to sort by that column.\\n\\n| Title | Year | Original | Current | Pen Name |\\n\\n| The Bad Weather Friend | 2024 | Thomas & Mercer | Thomas & Mercer | |\\n\\n| The Forest of Lost Souls | 2024 | Thomas & Mercer | Thomas & Mercer | |\\n\\n| After Death | 2023 | Thomas & Mercer | Thomas & Mercer | |\\n\\n| The House at the End of the World | 2023 | Thomas & Mercer | Thomas & Mercer | |\\n\\n| Quicksilver | 2022 | Amazon Publishing | Amazon Publishing | |\\n\\n| The Big Dark Sky | 2022 | Amazon Publishing | Amazon Publishing | |\\n\\n| Corkscrew | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Gentle Is the Angel of Death | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Kaleidoscope | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Light Has Weight, but Darkness Does Not | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Nameless: The Killer Finale | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| The Lost Soul of the City | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| The Other Emily | 2021 | Amazon Publishing | Amazon Publishing | |\\n\\n| Zero In | 2021 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Devoted | 2020 | Amazon Publishing | Amazon Publishing | |\\n\\n| Elsewhere | 2020 | Amazon Publishing | Amazon Publishing | |\\n\\n| In the Heart of the Fire | 2019 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Memories of Tomorrow | 2019 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Nameless: The Collection | 2019 | Amazon Publishing | Amazon Publishing | |\\n\\n| Photographing the Dead | 2019 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| Red Rain | 2019 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| The Mercy of Snakes | 2019 | Amazon Original Stories | Amazon Original Stories | |\\n\\n| The Night Window\"\n }\n]", "invoking_actions": [{"name": "search", "arguments": {}, "id": "63dc0cc0-c7bc-48ac-8ecd-81f6e603f57b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 5.5642700000000005, "benchmark_cost": 0.0} +{"event": "action", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 27, "action": {"name": "submit", "arguments": {"exact_answer": "Storm and Silence", "explanation": "The historical fiction novel described is \"Storm and Silence\" by Robert Thier, published in March 2016. The novel is set in 19th-century London and follows Lilly Linton, a young feminist who disguises herself as a man to work for the ruthless financial business magnate Mr. Rikkard Ambrose. [90027, 25654, 71659]\n\nAccording to the author interview, \"The story follows the adventures of 19-year-old feminist Lilly and her budding romance with the powerful and ruthless business mogul Rikkard Ambrose. A firm believer in the equality of men and women, Lilly is forced to dress up as a man in order to work for Mr. Ambrose and achieve her independence. The two of them butt heads all the time, but when an important document is stolen from Mr. Ambrose's safe, and he has fought a battle of wits and intrigue with his arch-enemy Lord Dalgliesh, she stands by him through thick and thin.\" [84371]\n\nThe chapter list includes titles like \"The Thief,\" \"File Fight,\" \"Isle Marbeau,\" \"Rising Waves,\" \"Shots in the Dark,\" and \"Man and Woman\" at the end of the book, indicating the plot elements involving a stolen document, an island setting, and dramatic sea climax with the rescue. [90027]\n\nThe novel was first uploaded on Wattpad in April 2014 and was officially published as a paperback and ebook in March 2016, meeting the criteria of being published before 2017. [90027, 25654]", "confidence": 85.0}, "id": "47ed5618-3bc2-4552-9656-3befeafa5cf9", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 6.011570000000001, "benchmark_cost": 0.0} +{"event": "observation", "run_id": "2026-01-27T20--21--58.499985", "session_id": "55db42d7_1179_4f71_ac1e_de0fe4fb0ac0", "session_number": 63, "task_key": "1262", "step": 27, "observation": null, "initial": false, "agent_cost": 6.011570000000001, "benchmark_cost": 0.0} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/recording.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/recording.json new file mode 100644 index 00000000..5f92b49d --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/recording.json @@ -0,0 +1,5 @@ +{ + "benchmark": "swebench", + "task_id": "django__django-16116", + "expected_score": 1.0 +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json new file mode 100644 index 00000000..1aa68219 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json @@ -0,0 +1,119 @@ +{ + "session_id": "1d4aa07c", + "success": true, + "score": 1.0, + "is_finished": true, + "steps": 32, + "agent_cost": 2.2978650000000003, + "benchmark_cost": 0.0, + "execution_time": 494.2505888938904, + "details": { + "score": 1, + "success": true, + "is_finished": true, + "session_metrics": {}, + "session_metadata": {}, + "instance_id": "django__django-16116", + "agent": { + "num_steps": 32, + "commands": [ + "find /testbed -type f -name \"*.py\" | xargs grep -l \"makemigrations\" | head -20", + "cat /testbed/django/core/management/commands/makemigrations.py", + "cat /testbed/django/core/management/commands/makemigrations.py | head -200", + "cat /testbed/django/core/management/commands/makemigrations.py | head -300 | tail -100", + "grep -n \"check_changes\" /testbed/django/core/management/commands/makemigrations.py", + "sed -n '248,260p' /testbed/django/core/management/commands/makemigrations.py", + "grep -n \"check\" /testbed/django/core/management/commands/migrate.py | head -30", + "sed -n '80,90p' /testbed/django/core/management/commands/migrate.py", + "sed -n '254,265p' /testbed/django/core/management/commands/migrate.py", + "grep -n \"check\" /testbed/django/core/management/commands/optimizemigration.py", + "sed -n '25,40p' /testbed/django/core/management/commands/optimizemigration.py", + "sed -n '70,90p' /testbed/django/core/management/commands/optimizemigration.py", + "grep -A 20 \"check\" /testbed/tests/migrations/test_commands.py | head -60", + "grep -n \"check_changes\" /testbed/tests/migrations/test_commands.py", + "grep -n \"makemigrations.*check\" /testbed/tests/migrations/test_commands.py", + "sed -n '2385,2410p' /testbed/tests/migrations/test_commands.py", + "sed -n '238,260p' /testbed/django/core/management/commands/makemigrations.py", + "cat > /tmp/fix.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Old code\nold_code = \"\"\" else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\"\"\"\n\n# New code - exit before writing migrations when --check is used\nnew_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix.py", + "sed -n '238,265p' /testbed/django/core/management/commands/makemigrations.py", + "sed -n '68,90p' /testbed/django/core/management/commands/optimizemigration.py", + "grep -n \"log\" /testbed/django/core/management/commands/makemigrations.py | head -30", + "cat > /tmp/fix2.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Current code (after first fix)\nold_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# New code - just exit without extra message (consistent with migrate --check)\nnew_code = \"\"\" else:\n if check_changes:\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix2.py", + "sed -n '238,265p' /testbed/django/core/management/commands/makemigrations.py", + "cd /testbed && python -m pytest tests/migrations/test_commands.py::MakeMigrationsTests::test_makemigrations_check -xvs 2>&1 | head -50", + "cd /testbed && python -m django test migrations.test_commands.MakeMigrationsTests.test_makemigrations_check --settings=tests.test_sqlite 2>&1", + "cd /testbed && python tests/runtests.py migrations.test_commands.MakeMigrationsTests.test_makemigrations_check 2>&1", + "cd /testbed && cat > /tmp/test_check.py << 'EOF'\nimport os\nimport sys\nimport tempfile\nimport shutil\n\n# Set up Django settings\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings')\n\n# Create a temporary directory for our test app\ntest_dir = tempfile.mkdtemp()\napp_dir = os.path.join(test_dir, 'myapp')\nos.makedirs(app_dir)\n\n# Create app files\nwith open(os.path.join(app_dir, '__init__.py'), 'w') as f:\n f.write('')\n\nwith open(os.path.join(app_dir, 'models.py'), 'w') as f:\n f.write('''\nfrom django.db import models\n\nclass TestModel(models.Model):\n name = models.CharField(max_length=100)\n''')\n\nmigrations_dir = os.path.join(app_dir, 'migrations')\nos.makedirs(migrations_dir)\nwith open(os.path.join(migrations_dir, '__init__.py'), 'w') as f:\n f.write('')\n\n# Create settings file \nwith open(os.path.join(test_dir, 'test_settings.py'), 'w') as f:\n f.write(f'''\nSECRET_KEY = 'test-secret-key'\nINSTALLED_APPS = [\n 'django.contrib.contenttypes',\n 'django.contrib.auth',\n 'myapp',\n]\nDATABASES = {{\n 'default': {{\n 'ENGINE': 'django.db.backends.sqlite3',\n 'NAME': ':memory:',\n }}\n}}\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n''')\n\n# Add test_dir to sys.path\nsys.path.insert(0, test_dir)\n\n# Configure Django\nimport django\ndjango.setup()\n\nfrom django.core.management import call_command\nimport io\n\n# Test 1: Check that --check exits with code 1 and does NOT create migrations\nprint(\"Test 1: --check should exit with code 1 without creating migrations\")\nout = io.StringIO()\nerr = io.StringIO()\n\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"FAIL: Expected SystemExit(1)\")\n sys.exit(1)\nexcept SystemExit as e:\n if e.code != 1:\n print(f\"FAIL: Expected exit code 1, got {e.code}\")\n sys.exit(1)\n\n# Check that no migration file was created\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif migration_files:\n print(f\"FAIL: Migration files were created: {migration_files}\")\n sys.exit(1)\n\nprint(\"PASS: --check exited with code 1 and no migration files were created\")\n\n# Test 2: Without --check, migrations should be created\nprint(\"\\nTest 2: Without --check, migrations should be created\")\nout = io.StringIO()\ncall_command('makemigrations', 'myapp', stdout=out, stderr=err, verbosity=0)\n\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif not migration_files:\n print(\"FAIL: Migration files were not created\")\n sys.exit(1)\n\nprint(f\"PASS: Migration files were created: {migration_files}\")\n\n# Test 3: With migrations present and no changes, --check should succeed (exit 0)\nprint(\"\\nTest 3: With no changes, --check should succeed (exit 0)\")\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"PASS: --check exited with code 0 when no changes\")\nexcept SystemExit as e:\n if e.code != 0:\n print(f\"FAIL: Expected exit code 0, got {e.code}\")\n sys.exit(1)\n print(\"PASS: --check exited with code 0 when no changes\")\n\n# Cleanup\nshutil.rmtree(test_dir)\nprint(\"\\nAll tests passed!\")\nEOF\npython /tmp/test_check.py", + "cd /testbed && python tests/runtests.py migrations.test_commands.MakeMigrationsTests -v 2 2>&1 | tail -50", + "rm /tmp/fix.py /tmp/fix2.py /tmp/test_check.py 2>/dev/null; echo \"Cleaned up\"", + "cd /testbed && git diff", + "cd /testbed && python tests/runtests.py migrations.test_commands -v 1 2>&1 | tail -20" + ], + "edit_commands": [ + "cat > /tmp/fix.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Old code\nold_code = \"\"\" else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\"\"\"\n\n# New code - exit before writing migrations when --check is used\nnew_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix.py", + "cat > /tmp/fix2.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Current code (after first fix)\nold_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# New code - just exit without extra message (consistent with migrate --check)\nnew_code = \"\"\" else:\n if check_changes:\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix2.py", + "cd /testbed && cat > /tmp/test_check.py << 'EOF'\nimport os\nimport sys\nimport tempfile\nimport shutil\n\n# Set up Django settings\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings')\n\n# Create a temporary directory for our test app\ntest_dir = tempfile.mkdtemp()\napp_dir = os.path.join(test_dir, 'myapp')\nos.makedirs(app_dir)\n\n# Create app files\nwith open(os.path.join(app_dir, '__init__.py'), 'w') as f:\n f.write('')\n\nwith open(os.path.join(app_dir, 'models.py'), 'w') as f:\n f.write('''\nfrom django.db import models\n\nclass TestModel(models.Model):\n name = models.CharField(max_length=100)\n''')\n\nmigrations_dir = os.path.join(app_dir, 'migrations')\nos.makedirs(migrations_dir)\nwith open(os.path.join(migrations_dir, '__init__.py'), 'w') as f:\n f.write('')\n\n# Create settings file \nwith open(os.path.join(test_dir, 'test_settings.py'), 'w') as f:\n f.write(f'''\nSECRET_KEY = 'test-secret-key'\nINSTALLED_APPS = [\n 'django.contrib.contenttypes',\n 'django.contrib.auth',\n 'myapp',\n]\nDATABASES = {{\n 'default': {{\n 'ENGINE': 'django.db.backends.sqlite3',\n 'NAME': ':memory:',\n }}\n}}\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n''')\n\n# Add test_dir to sys.path\nsys.path.insert(0, test_dir)\n\n# Configure Django\nimport django\ndjango.setup()\n\nfrom django.core.management import call_command\nimport io\n\n# Test 1: Check that --check exits with code 1 and does NOT create migrations\nprint(\"Test 1: --check should exit with code 1 without creating migrations\")\nout = io.StringIO()\nerr = io.StringIO()\n\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"FAIL: Expected SystemExit(1)\")\n sys.exit(1)\nexcept SystemExit as e:\n if e.code != 1:\n print(f\"FAIL: Expected exit code 1, got {e.code}\")\n sys.exit(1)\n\n# Check that no migration file was created\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif migration_files:\n print(f\"FAIL: Migration files were created: {migration_files}\")\n sys.exit(1)\n\nprint(\"PASS: --check exited with code 1 and no migration files were created\")\n\n# Test 2: Without --check, migrations should be created\nprint(\"\\nTest 2: Without --check, migrations should be created\")\nout = io.StringIO()\ncall_command('makemigrations', 'myapp', stdout=out, stderr=err, verbosity=0)\n\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif not migration_files:\n print(\"FAIL: Migration files were not created\")\n sys.exit(1)\n\nprint(f\"PASS: Migration files were created: {migration_files}\")\n\n# Test 3: With migrations present and no changes, --check should succeed (exit 0)\nprint(\"\\nTest 3: With no changes, --check should succeed (exit 0)\")\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"PASS: --check exited with code 0 when no changes\")\nexcept SystemExit as e:\n if e.code != 0:\n print(f\"FAIL: Expected exit code 0, got {e.code}\")\n sys.exit(1)\n print(\"PASS: --check exited with code 0 when no changes\")\n\n# Cleanup\nshutil.rmtree(test_dir)\nprint(\"\\nAll tests passed!\")\nEOF\npython /tmp/test_check.py", + "rm /tmp/fix.py /tmp/fix2.py /tmp/test_check.py 2>/dev/null; echo \"Cleaned up\"" + ], + "call_submit": true + }, + "patch": { + "generated": true, + "length": 808, + "structurally_valid": true + }, + "container": { + "required": true, + "build_base": null, + "build_env": null, + "build_instance": null, + "started": true, + "patch_exists": true, + "applying_patch": true, + "patch_applied": true, + "removed": true + }, + "evaluation": { + "grading": true, + "resolved": true, + "test_results": { + "FAIL_TO_PASS": { + "expected": 1, + "success": 1, + "failure": 0, + "rate": 100.0, + "display": "expected 1: success 1, failure 0. success rate: 100.0%" + }, + "PASS_TO_PASS": { + "expected": 137, + "success": 137, + "failure": 0, + "rate": 100.0, + "display": "expected 137: success 137, failure 0. success rate: 100.0%" + } + } + }, + "summary": { + "num_steps": 32, + "steps_limit": null, + "num_edit_commands": 4, + "agent_call_submit": 1, + "patch_non_empty": 1, + "container_status": 1, + "fail_to_pass_rate": 100.0, + "pass_to_pass_rate": 100.0, + "score": 1 + } + }, + "cost_reports": { + "agent": { + "model_name": "openai/aws/claude-opus-4-5", + "total_cost": 2.2978650000000003 + }, + "benchmark": { + "model_name": "", + "total_cost": 0 + } + }, + "task_key": "django__django-16116" +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/session.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/session.json new file mode 100644 index 00000000..d468a205 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/session.json @@ -0,0 +1,48 @@ +{ + "task": "Resolve the given issue by editing the repository files directly on a remote machine.\n\nRepository directory on the remote machine: /testbed\n\n## Issue to resolve:\nmakemigrations --check generating migrations is inconsistent with other uses of --check\nDescription\n\t\nTo script a check for missing migrations but without actually intending to create the migrations, it is necessary to use both --check and --dry-run, which is inconsistent with migrate --check and optimizemigration --check, which just exit (after possibly logging a bit).\nI'm suggesting that makemigrations --check should just exit without making migrations.\nThe choice to write the migrations anyway was not discussed AFAICT on ticket:25604 or ​https://groups.google.com/g/django-developers/c/zczdY6c9KSg/m/ZXCXQsGDDAAJ.\nNoticed when reading ​PR to adjust the documentation of migrate --check. I think the current documentation is silent on this question.\n\n\n## Execution Environment & Access (STRICT):\nAll commands are executed on a remote machine that already contains the full\nrepository and all required system dependencies and prerequisites.\n\nThe remote machine is accessible **ONLY** via the `bash` action.\nAll interactions with the filesystem and environment — including reading files,\nediting files, and running scripts — can be performed by definition only by using `bash`.\nEach command is executed in a separate shell invocation; working directory changes\nand environment variables do not persist between commands. Only files written to disk persist.\nAll bash commands are executed with the working directory set to the root of the remote machine (/).\n\n## Instructions:\nYou must fix the issue by directly modifying files in this repository, in place.\n\n**ALL file edits MUST be performed using `bash` commands on the remote machine.**\nExplanations or code snippets in chat are not sufficient.\n\nOnly changes written to files on disk and present in the working tree will be\nincluded in the final patch. Any fix not reflected in repository files is ignored.\n\nMake changes only to non-test source files in a way that is general and consistent\nwith the existing codebase.\n\n## Hard Boundaries:\n- MODIFY ONLY: regular source code files in /testbed\n- DO NOT MODIFY: tests or configuration files (pyproject.toml, setup.cfg, etc.)\n- DO NOT use interactive editors (vi, nano, etc.)\n- DO NOT suggest changes without implementing them\n\n## Patch & Submission Mechanics:\nSubmission captures the entire working tree using:\n\n `git add -A && git diff --staged C0`\n\nThis means:\n- All intended fixes MUST appear in repository files\n- Any file present at submission time WILL be included in the patch\n- Temporary files, debug artifacts, or helper scripts MUST be removed before submission\n\n## Recommended Workflow:\n1. Analyze the codebase\n2. Reproduce the issue\n3. Edit source files using `bash`\n4. Verify the fix\n5. Test edge cases\n\n## Submission (FINAL STEP):\nWhen finished, use `submit_patch` exactly once with a short summary.\nEnsure the repository contains ONLY the intended final changes.\nAfter submission, no further reading, editing, or testing is allowed.\n\n## Evaluation:\nYour patch will be applied and validated using a hidden test suite.\n\nThe evaluation verifies that:\n1. The reported issue is fully resolved\n2. All previously passing functionality and tests remain unbroken\n\nSuccess requires all tests to pass.\n", + "context": {}, + "actions": [ + { + "name": "bash", + "description": "Run a bash command in the repo root and get the output", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "command": { + "description": "Bash command to execute", + "title": "Command", + "type": "string" + } + }, + "required": [ + "command" + ], + "title": "BashArgs", + "type": "object" + } + }, + { + "name": "finish", + "description": "Finish the task by submitting a brief summary; he system automatically computes the git patch from the repository changes.", + "is_finish": true, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "summary": { + "description": "Brief textual summary of the fix (no diff/patch)", + "title": "Summary", + "type": "string" + } + }, + "required": [ + "summary" + ], + "title": "SubmitPatchArgs", + "type": "object" + } + } + ] +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/trajectory.jsonl b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/trajectory.jsonl new file mode 100644 index 00000000..3d817919 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/trajectory.jsonl @@ -0,0 +1,65 @@ +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 0, "observation": {"result": null, "invoking_actions": []}, "initial": true, "agent_cost": 0, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 1, "action": {"name": "bash", "arguments": {"command": "find /testbed -type f -name \"*.py\" | xargs grep -l \"makemigrations\" | head -20"}, "id": "4a8c3335-9eab-4f15-b0a8-305cbce35bed", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.00965, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 1, "observation": {"result": {"output": "/testbed/django/core/management/commands/makemigrations.py\n/testbed/django/core/management/commands/migrate.py\n/testbed/django/db/migrations/loader.py\n/testbed/tests/migrations/test_base.py\n/testbed/tests/migrations/test_commands.py\n/testbed/tests/migrations/test_writer.py\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "4a8c3335-9eab-4f15-b0a8-305cbce35bed", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.00965, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 2, "action": {"name": "bash", "arguments": {"command": "cat /testbed/django/core/management/commands/makemigrations.py"}, "id": "6a0ad148-833c-4786-a310-82f750d04c6a", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.020185, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 2, "observation": {"result": {"output": "import os\nimport sys\nimport warnings\nfrom itertools import takewhile\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.management.base import BaseCommand, CommandError, no_translations\nfrom django.core.management.utils import run_formatters\nfrom django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router\nfrom django.db.migrations import Migration\nfrom django.db.migrations.autodetector import MigrationAutodetector\nfrom django.db.migrations.loader import MigrationLoader\nfrom django.db.migrations.migration import SwappableTuple\nfrom django.db.migrations.optimizer import MigrationOptimizer\nfrom django.db.migrations.questioner import (\n InteractiveMigrationQuestioner,\n MigrationQuestioner,\n NonInteractiveMigrationQuestioner,\n)\nfrom django.db.migrations.state import ProjectState\nfrom django.db.migrations.utils import get_migration_name_timestamp\nfrom django.db.migrations.writer import MigrationWriter\n\n\nclass Command(BaseCommand):\n help = \"Creates new migration(s) for apps.\"\n\n def add_arguments(self, parser):\n parser.add_argument(\n \"args\",\n metavar=\"app_label\",\n nargs=\"*\",\n help=\"Specify the app label(s) to create migrations for.\",\n )\n parser.add_argument(\n \"--dry-run\",\n action=\"store_true\",\n help=\"Just show what migrations would be made; don't actually write them.\",\n )\n parser.add_argument(\n \"--merge\",\n action=\"store_true\",\n help=\"Enable fixing of migration conflicts.\",\n )\n parser.add_argument(\n \"--empty\",\n action=\"store_true\",\n help=\"Create an empty migration.\",\n )\n parser.add_argument(\n \"--noinput\",\n \"--no-input\",\n action=\"store_false\",\n dest=\"interactive\",\n help=\"Tells Django to NOT prompt the user for input of any kind.\",\n )\n parser.add_argument(\n \"-n\",\n \"--name\",\n help=\"Use this name for migration file(s).\",\n )\n parser.add_argument(\n \"--no-header\",\n action=\"store_false\",\n dest=\"include_header\",\n help=\"Do not add header comments to new migration file(s).\",\n )\n parser.add_argument(\n \"--check\",\n action=\"store_true\",\n dest=\"check_changes\",\n help=\"Exit with a non-zero status if model changes are missing migrations.\",\n )\n parser.add_argument(\n \"--scriptable\",\n action=\"store_true\",\n dest=\"scriptable\",\n help=(\n \"Divert log output and input prompts to stderr, writing only \"\n \"paths of generated migration files to stdout.\"\n ),\n )\n parser.add_argument(\n \"--update\",\n action=\"store_true\",\n dest=\"update\",\n help=(\n \"Merge model changes into the latest migration and optimize the \"\n \"resulting operations.\"\n ),\n )\n\n @property\n def log_output(self):\n return self.stderr if self.scriptable else self.stdout\n\n def log(self, msg):\n self.log_output.write(msg)\n\n @no_translations\n def handle(self, *app_labels, **options):\n self.written_files = []\n self.verbosity = options[\"verbosity\"]\n self.interactive = options[\"interactive\"]\n self.dry_run = options[\"dry_run\"]\n self.merge = options[\"merge\"]\n self.empty = options[\"empty\"]\n self.migration_name = options[\"name\"]\n if self.migration_name and not self.migration_name.isidentifier():\n raise CommandError(\"The migration name must be a valid Python identifier.\")\n self.include_header = options[\"include_header\"]\n check_changes = options[\"check_changes\"]\n self.scriptable = options[\"scriptable\"]\n self.update = options[\"update\"]\n # If logs and prompts are diverted to stderr, remove the ERROR style.\n if self.scriptable:\n self.stderr.style_func = None\n\n # Make sure the app they asked for exists\n app_labels = set(app_labels)\n has_bad_labels = False\n for app_label in app_labels:\n try:\n apps.get_app_config(app_label)\n except LookupError as err:\n self.stderr.write(str(err))\n has_bad_labels = True\n if has_bad_labels:\n sys.exit(2)\n\n # Load the current graph state. Pass in None for the connection so\n # the loader doesn't try to resolve replaced migrations from DB.\n loader = MigrationLoader(None, ignore_no_migrations=True)\n\n # Raise an error if any migrations are applied before their dependencies.\n consistency_check_labels = {config.label for config in apps.get_app_configs()}\n # Non-default databases are only checked if database routers \n\n- OMITTED 12253 chars -\n\ntiveMigrationQuestioner(prompt_output=self.log_output)\n else:\n questioner = MigrationQuestioner(defaults={\"ask_merge\": True})\n\n for app_label, migration_names in conflicts.items():\n # Grab out the migrations in question, and work out their\n # common ancestor.\n merge_migrations = []\n for migration_name in migration_names:\n migration = loader.get_migration(app_label, migration_name)\n migration.ancestry = [\n mig\n for mig in loader.graph.forwards_plan((app_label, migration_name))\n if mig[0] == migration.app_label\n ]\n merge_migrations.append(migration)\n\n def all_items_equal(seq):\n return all(item == seq[0] for item in seq[1:])\n\n merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations))\n common_ancestor_count = sum(\n 1\n for common_ancestor_generation in takewhile(\n all_items_equal, merge_migrations_generations\n )\n )\n if not common_ancestor_count:\n raise ValueError(\n \"Could not find common ancestor of %s\" % migration_names\n )\n # Now work out the operations along each divergent branch\n for migration in merge_migrations:\n migration.branch = migration.ancestry[common_ancestor_count:]\n migrations_ops = (\n loader.get_migration(node_app, node_name).operations\n for node_app, node_name in migration.branch\n )\n migration.merged_operations = sum(migrations_ops, [])\n # In future, this could use some of the Optimizer code\n # (can_optimize_through) to automatically see if they're\n # mergeable. For now, we always just prompt the user.\n if self.verbosity > 0:\n self.log(self.style.MIGRATE_HEADING(\"Merging %s\" % app_label))\n for migration in merge_migrations:\n self.log(self.style.MIGRATE_LABEL(\" Branch %s\" % migration.name))\n for operation in migration.merged_operations:\n self.log(\" - %s\" % operation.describe())\n if questioner.ask_merge(app_label):\n # If they still want to merge it, then write out an empty\n # file depending on the migrations needing merging.\n numbers = [\n MigrationAutodetector.parse_number(migration.name)\n for migration in merge_migrations\n ]\n try:\n biggest_number = max(x for x in numbers if x is not None)\n except ValueError:\n biggest_number = 1\n subclass = type(\n \"Migration\",\n (Migration,),\n {\n \"dependencies\": [\n (app_label, migration.name)\n for migration in merge_migrations\n ],\n },\n )\n parts = [\"%04i\" % (biggest_number + 1)]\n if self.migration_name:\n parts.append(self.migration_name)\n else:\n parts.append(\"merge\")\n leaf_names = \"_\".join(\n sorted(migration.name for migration in merge_migrations)\n )\n if len(leaf_names) > 47:\n parts.append(get_migration_name_timestamp())\n else:\n parts.append(leaf_names)\n migration_name = \"_\".join(parts)\n new_migration = subclass(migration_name, app_label)\n writer = MigrationWriter(new_migration, self.include_header)\n\n if not self.dry_run:\n # Write the merge migrations file to the disk\n with open(writer.path, \"w\", encoding=\"utf-8\") as fh:\n fh.write(writer.as_string())\n run_formatters([writer.path])\n if self.verbosity > 0:\n self.log(\"\\nCreated new merge migration %s\" % writer.path)\n if self.scriptable:\n self.stdout.write(writer.path)\n elif self.verbosity == 3:\n # Alternatively, makemigrations --merge --dry-run --verbosity 3\n # will log the merge migrations rather than saving the file\n # to the disk.\n self.log(\n self.style.MIGRATE_HEADING(\n \"Full merge migrations file '%s':\" % writer.filename\n )\n )\n self.log(writer.as_string())\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "6a0ad148-833c-4786-a310-82f750d04c6a", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.020185, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 3, "action": {"name": "bash", "arguments": {"command": "cat /testbed/django/core/management/commands/makemigrations.py | head -200"}, "id": "fa172013-0591-45d5-9e89-02c8ca9f8196", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.046025, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 3, "observation": {"result": {"output": "import os\nimport sys\nimport warnings\nfrom itertools import takewhile\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.management.base import BaseCommand, CommandError, no_translations\nfrom django.core.management.utils import run_formatters\nfrom django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router\nfrom django.db.migrations import Migration\nfrom django.db.migrations.autodetector import MigrationAutodetector\nfrom django.db.migrations.loader import MigrationLoader\nfrom django.db.migrations.migration import SwappableTuple\nfrom django.db.migrations.optimizer import MigrationOptimizer\nfrom django.db.migrations.questioner import (\n InteractiveMigrationQuestioner,\n MigrationQuestioner,\n NonInteractiveMigrationQuestioner,\n)\nfrom django.db.migrations.state import ProjectState\nfrom django.db.migrations.utils import get_migration_name_timestamp\nfrom django.db.migrations.writer import MigrationWriter\n\n\nclass Command(BaseCommand):\n help = \"Creates new migration(s) for apps.\"\n\n def add_arguments(self, parser):\n parser.add_argument(\n \"args\",\n metavar=\"app_label\",\n nargs=\"*\",\n help=\"Specify the app label(s) to create migrations for.\",\n )\n parser.add_argument(\n \"--dry-run\",\n action=\"store_true\",\n help=\"Just show what migrations would be made; don't actually write them.\",\n )\n parser.add_argument(\n \"--merge\",\n action=\"store_true\",\n help=\"Enable fixing of migration conflicts.\",\n )\n parser.add_argument(\n \"--empty\",\n action=\"store_true\",\n help=\"Create an empty migration.\",\n )\n parser.add_argument(\n \"--noinput\",\n \"--no-input\",\n action=\"store_false\",\n dest=\"interactive\",\n help=\"Tells Django to NOT prompt the user for input of any kind.\",\n )\n parser.add_argument(\n \"-n\",\n \"--name\",\n help=\"Use this name for migration file(s).\",\n )\n parser.add_argument(\n \"--no-header\",\n action=\"store_false\",\n dest=\"include_header\",\n help=\"Do not add header comments to new migration file(s).\",\n )\n parser.add_argument(\n \"--check\",\n action=\"store_true\",\n dest=\"check_changes\",\n help=\"Exit with a non-zero status if model changes are missing migrations.\",\n )\n parser.add_argument(\n \"--scriptable\",\n action=\"store_true\",\n dest=\"scriptable\",\n help=(\n \"Divert log output and input prompts to stderr, writing only \"\n \"paths of generated migration files to stdout.\"\n ),\n )\n parser.add_argument(\n \"--update\",\n action=\"store_true\",\n dest=\"update\",\n help=(\n \"Merge model changes into the latest migration and optimize the \"\n \"resulting operations.\"\n ),\n )\n\n @property\n def log_output(self):\n return self.stderr if self.scriptable else self.stdout\n\n def log(self, msg):\n self.log_output.write(msg)\n\n @no_translations\n def handle(self, *app_labels, **options):\n self.written_files = []\n self.verbosity = options[\"verbosity\"]\n self.interactive = options[\"interactive\"]\n self.dry_run = options[\"dry_run\"]\n self.merge = options[\"merge\"]\n self.empty = options[\"empty\"]\n self.migration_name = options[\"name\"]\n if self.migration_name and not self.migration_name.isidentifier():\n raise CommandError(\"The migration name must be a valid Python identifier.\")\n self.include_header = options[\"include_header\"]\n check_changes = options[\"check_changes\"]\n self.scriptable = options[\"scriptable\"]\n self.update = options[\"update\"]\n # If logs and prompts are diverted to stderr, remove the ERROR style.\n if self.scriptable:\n self.stderr.style_func = None\n\n # Make sure the app they asked for exists\n app_labels = set(app_labels)\n has_bad_labels = False\n for app_label in app_labels:\n try:\n apps.get_app_config(app_label)\n except LookupError as err:\n self.stderr.write(str(err))\n has_bad_labels = True\n if has_bad_labels:\n sys.exit(2)\n\n # Load the current graph state. Pass in None for the connection so\n # the loader doesn't try to resolve replaced migrations from DB.\n loader = MigrationLoader(None, ignore_no_migrations=True)\n\n # Raise an error if any migrations are applied before their dependencies.\n consistency_check_labels = {config.label for config in apps.get_app_configs()}\n # Non-default databases are only checked if database routers used.\n aliases_to_check = (\n connections if settings.DATABASE_ROUTERS else [DEFAULT_DB_ALIAS]\n )\n for alias in sorted(aliases_to_check):\n connection = connections[alias]\n if connection.settings_dict[\"ENGINE\"] != \"django.db.backends.dummy\" and any(\n # At least one model must be migrated to the database.\n router.allow_migrate(\n connection.alias, app_label, model_name=model._meta.object_name\n )\n for app_label in consistency_check_labels\n for model in apps.get_app_config(app_label).get_models()\n ):\n try:\n loader.check_consistent_history(connection)\n except OperationalError as error:\n warnings.warn(\n \"Got an error checking a consistent migration history \"\n \"performed for database connection '%s': %s\" % (alias, error),\n RuntimeWarning,\n )\n # Before anything else, see if there's conflicting apps and drop out\n # hard if there are any and they don't want to merge\n conflicts = loader.detect_conflicts()\n\n # If app_labels is specified, filter out conflicting migrations for\n # unspecified apps.\n if app_labels:\n conflicts = {\n app_label: conflict\n for app_label, conflict in conflicts.items()\n if app_label in app_labels\n }\n\n if conflicts and not self.merge:\n name_str = \"; \".join(\n \"%s in %s\" % (\", \".join(names), app) for app, names in conflicts.items()\n )\n raise CommandError(\n \"Conflicting migrations detected; multiple leaf nodes in the \"\n \"migration graph: (%s).\\nTo fix them run \"\n \"'python manage.py makemigrations --merge'\" % name_str\n )\n\n # If they want to merge and there's nothing to merge, then politely exit\n if self.merge and not conflicts:\n self.log(\"No conflicts detected to merge.\")\n return\n\n # If they want to merge and there is something to merge, then\n # divert into the merge code\n if self.merge and conflicts:\n return self.handle_merge(loader, conflicts)\n\n if self.interactive:\n questioner = InteractiveMigrationQuestioner(\n specified_apps=app_labels,\n dry_run=self.dry_run,\n prompt_output=self.log_output,\n )\n else:\n questioner = NonInteractiveMigrationQuestioner(\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "fa172013-0591-45d5-9e89-02c8ca9f8196", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.046025, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 4, "action": {"name": "bash", "arguments": {"command": "cat /testbed/django/core/management/commands/makemigrations.py | head -300 | tail -100"}, "id": "7fb52e87-e89b-48a1-8033-965f57a3aa38", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.084305, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 4, "observation": {"result": {"output": " specified_apps=app_labels,\n dry_run=self.dry_run,\n verbosity=self.verbosity,\n log=self.log,\n )\n # Set up autodetector\n autodetector = MigrationAutodetector(\n loader.project_state(),\n ProjectState.from_apps(apps),\n questioner,\n )\n\n # If they want to make an empty migration, make one for each app\n if self.empty:\n if not app_labels:\n raise CommandError(\n \"You must supply at least one app label when using --empty.\"\n )\n # Make a fake changes() result we can pass to arrange_for_graph\n changes = {app: [Migration(\"custom\", app)] for app in app_labels}\n changes = autodetector.arrange_for_graph(\n changes=changes,\n graph=loader.graph,\n migration_name=self.migration_name,\n )\n self.write_migration_files(changes)\n return\n\n # Detect changes\n changes = autodetector.changes(\n graph=loader.graph,\n trim_to_apps=app_labels or None,\n convert_apps=app_labels or None,\n migration_name=self.migration_name,\n )\n\n if not changes:\n # No changes? Tell them.\n if self.verbosity >= 1:\n if app_labels:\n if len(app_labels) == 1:\n self.log(\"No changes detected in app '%s'\" % app_labels.pop())\n else:\n self.log(\n \"No changes detected in apps '%s'\"\n % (\"', '\".join(app_labels))\n )\n else:\n self.log(\"No changes detected\")\n else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\n\n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n new_changes = {}\n update_previous_migration_paths = {}\n for app_label, app_migrations in changes.items():\n # Find last migration.\n leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label)\n if len(leaf_migration_nodes) == 0:\n raise CommandError(\n f\"App {app_label} has no migration, cannot update last migration.\"\n )\n leaf_migration_node = leaf_migration_nodes[0]\n # Multiple leaf nodes have already been checked earlier in command.\n leaf_migration = loader.graph.nodes[leaf_migration_node]\n # Updated migration cannot be a squash migration, a dependency of\n # another migration, and cannot be already applied.\n if leaf_migration.replaces:\n raise CommandError(\n f\"Cannot update squash migration '{leaf_migration}'.\"\n )\n if leaf_migration_node in loader.applied_migrations:\n raise CommandError(\n f\"Cannot update applied migration '{leaf_migration}'.\"\n )\n depending_migrations = [\n migration\n for migration in loader.disk_migrations.values()\n if leaf_migration_node in migration.dependencies\n ]\n if depending_migrations:\n formatted_migrations = \", \".join(\n [f\"'{migration}'\" for migration in depending_migrations]\n )\n raise CommandError(\n f\"Cannot update migration '{leaf_migration}' that migrations \"\n f\"{formatted_migrations} depend on.\"\n )\n # Build new migration.\n for migration in app_migrations:\n leaf_migration.operations.extend(migration.operations)\n\n for dependency in migration.dependencies:\n if isinstance(dependency, SwappableTuple):\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "7fb52e87-e89b-48a1-8033-965f57a3aa38", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.084305, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 5, "action": {"name": "bash", "arguments": {"command": "grep -n \"check_changes\" /testbed/django/core/management/commands/makemigrations.py"}, "id": "20b13125-852f-47a4-a924-03870d726a3f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.130555, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 5, "observation": {"result": {"output": "72: dest=\"check_changes\",\n113: check_changes = options[\"check_changes\"]\n255: if check_changes:\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "20b13125-852f-47a4-a924-03870d726a3f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.130555, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 6, "action": {"name": "bash", "arguments": {"command": "sed -n '248,260p' /testbed/django/core/management/commands/makemigrations.py"}, "id": "b68f17a3-4b19-44c2-bc66-21ba4d2eec2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.17609500000000003, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 6, "observation": {"result": {"output": " else:\n self.log(\"No changes detected\")\n else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\n\n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n new_changes = {}\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "b68f17a3-4b19-44c2-bc66-21ba4d2eec2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.17609500000000003, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 7, "action": {"name": "bash", "arguments": {"command": "grep -n \"check\" /testbed/django/core/management/commands/migrate.py | head -30"}, "id": "fa64ba41-a35c-4d00-aaa7-2a14dfc5ae03", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.225885, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 7, "observation": {"result": {"output": "21: requires_system_checks = []\n25: \"--skip-checks\",\n27: help=\"Skip system checks.\",\n66: \"migration before using this flag. Django will only check for an \"\n81: \"--check\",\n83: dest=\"check_unapplied\",\n99: if not options[\"skip_checks\"]:\n100: self.check(databases=[database])\n120: executor.loader.check_consistent_history(connection)\n257: if options[\"check_unapplied\"]:\n260: if options[\"check_unapplied\"]:\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "fa64ba41-a35c-4d00-aaa7-2a14dfc5ae03", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.225885, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 8, "action": {"name": "bash", "arguments": {"command": "sed -n '80,90p' /testbed/django/core/management/commands/migrate.py"}, "id": "94a8160e-b10c-412a-8cd0-e80dd039aab0", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.27476000000000006, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 8, "observation": {"result": {"output": " parser.add_argument(\n \"--check\",\n action=\"store_true\",\n dest=\"check_unapplied\",\n help=(\n \"Exits with a non-zero status if unapplied migrations exist and does \"\n \"not actually apply migrations.\"\n ),\n )\n parser.add_argument(\n \"--prune\",\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "94a8160e-b10c-412a-8cd0-e80dd039aab0", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.27476000000000006, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 9, "action": {"name": "bash", "arguments": {"command": "sed -n '254,265p' /testbed/django/core/management/commands/migrate.py"}, "id": "9adb4207-7589-48e2-8c0d-e9251604cf6c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.324865, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 9, "observation": {"result": {"output": " )\n style = self.style.WARNING if is_error else None\n self.stdout.write(\" \" + message, style)\n if options[\"check_unapplied\"]:\n sys.exit(1)\n return\n if options[\"check_unapplied\"]:\n if plan:\n sys.exit(1)\n return\n if options[\"prune\"]:\n return\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "9adb4207-7589-48e2-8c0d-e9251604cf6c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.324865, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 10, "action": {"name": "bash", "arguments": {"command": "grep -n \"check\" /testbed/django/core/management/commands/optimizemigration.py"}, "id": "faa72cdc-d3e4-49e2-b0e4-3c913bb658cc", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.37767, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 10, "observation": {"result": {"output": "27: \"--check\",\n36: check = options[\"check\"]\n75: if check:\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "faa72cdc-d3e4-49e2-b0e4-3c913bb658cc", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.37767, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 11, "action": {"name": "bash", "arguments": {"command": "sed -n '25,40p' /testbed/django/core/management/commands/optimizemigration.py"}, "id": "d54beaea-0cdf-4f6b-a846-b8c559c16a29", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.43021000000000004, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 11, "observation": {"result": {"output": " )\n parser.add_argument(\n \"--check\",\n action=\"store_true\",\n help=\"Exit with a non-zero status if the migration can be optimized.\",\n )\n\n def handle(self, *args, **options):\n verbosity = options[\"verbosity\"]\n app_label = options[\"app_label\"]\n migration_name = options[\"migration_name\"]\n check = options[\"check\"]\n\n # Validate app_label.\n try:\n apps.get_app_config(app_label)\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "d54beaea-0cdf-4f6b-a846-b8c559c16a29", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.43021000000000004, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 12, "action": {"name": "bash", "arguments": {"command": "sed -n '70,90p' /testbed/django/core/management/commands/optimizemigration.py"}, "id": "c8bfd35a-2688-423d-a361-298ad9e16f38", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.4842600000000001, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 12, "observation": {"result": {"output": " if verbosity > 0:\n self.stdout.write(\n \"Optimizing from %d operations to %d operations.\"\n % (len(migration.operations), len(new_operations))\n )\n if check:\n sys.exit(1)\n\n # Set the new migration optimizations.\n migration.operations = new_operations\n\n # Write out the optimized migration file.\n writer = MigrationWriter(migration)\n migration_file_string = writer.as_string()\n if writer.needs_manual_porting:\n if migration.replaces:\n raise CommandError(\n \"Migration will require manual porting but is already a squashed \"\n \"migration.\\nTransition to a normal migration first: \"\n \"https://docs.djangoproject.com/en/%s/topics/migrations/\"\n \"#squashing-migrations\" % get_docs_version()\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "c8bfd35a-2688-423d-a361-298ad9e16f38", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.4842600000000001, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 13, "action": {"name": "bash", "arguments": {"command": "grep -A 20 \"check\" /testbed/tests/migrations/test_commands.py | head -60"}, "id": "60f237fe-fc3a-4d55-9e79-5b8ee21231df", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.547195, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 13, "observation": {"result": {"output": " def test_migrate_with_system_checks(self):\n out = io.StringIO()\n call_command(\"migrate\", skip_checks=False, no_color=True, stdout=out)\n self.assertIn(\"Apply all migrations: migrated_app\", out.getvalue())\n\n @override_settings(\n INSTALLED_APPS=[\n \"migrations\",\n \"migrations.migrations_test_apps.unmigrated_app_syncdb\",\n ]\n )\n def test_app_without_migrations(self):\n msg = \"App 'unmigrated_app_syncdb' does not have migrations.\"\n with self.assertRaisesMessage(CommandError, msg):\n call_command(\"migrate\", app_label=\"unmigrated_app_syncdb\")\n\n @override_settings(\n MIGRATION_MODULES={\"migrations\": \"migrations.test_migrations_clashing_prefix\"}\n )\n def test_ambiguous_prefix(self):\n msg = (\n \"More than one migration matches 'a' in app 'migrations'. Please \"\n \"be more specific.\"\n--\n that check.\n \"\"\"\n # Make sure no tables are created\n for db in self.databases:\n self.assertTableNotExists(\"migrations_author\", using=db)\n self.assertTableNotExists(\"migrations_tribble\", using=db)\n # Run the migrations to 0001 only\n call_command(\"migrate\", \"migrations\", \"0001\", verbosity=0)\n call_command(\"migrate\", \"migrations\", \"0001\", verbosity=0, database=\"other\")\n # Make sure the right tables exist\n self.assertTableExists(\"migrations_author\")\n self.assertTableNotExists(\"migrations_tribble\")\n # Also check the \"other\" database\n self.assertTableNotExists(\"migrations_author\", using=\"other\")\n self.assertTableExists(\"migrations_tribble\", using=\"other\")\n\n # Fake a roll-back\n call_command(\"migrate\", \"migrations\", \"zero\", fake=True, verbosity=0)\n call_command(\n \"migrate\", \"migrations\", \"zero\", fake=True, verbosity=0, database=\"other\"\n )\n # Make sure the tables still exist\n self.assertTableExists(\"migrations_author\")\n self.assertTableExists(\"migrations_tribble\", using=\"other\")\n # Try to run initial migration\n with self.assertRaises(DatabaseError):\n call_command(\"migrate\", \"migrations\", \"0001\", verbosity=0)\n # Run initial migration with an explicit --fake-initial\n out = io.StringIO()\n with mock.patch(\n \"django.core.management.color.supports_color\", lambda *args: False\n ):\n call_command(\n--\n def test_migrate_check(self):\n with self.assertRaises(SystemExit):\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "60f237fe-fc3a-4d55-9e79-5b8ee21231df", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.547195, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 14, "action": {"name": "bash", "arguments": {"command": "grep -n \"check_changes\" /testbed/tests/migrations/test_commands.py"}, "id": "635fbcd7-4ffd-4578-ac44-b24c1901d9a3", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.609625, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 14, "observation": {"result": {"output": "", "returncode": 1}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "635fbcd7-4ffd-4578-ac44-b24c1901d9a3", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.609625, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 15, "action": {"name": "bash", "arguments": {"command": "grep -n \"makemigrations.*check\" /testbed/tests/migrations/test_commands.py"}, "id": "22459745-242f-4784-a920-cefdd7d81ec6", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.672735, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 15, "observation": {"result": {"output": "1500: def test_makemigrations_consistency_checks_respect_routers(self):\n2389: def test_makemigrations_check(self):\n2391: makemigrations --check should exit with a non-zero status when\n2396: call_command(\"makemigrations\", \"--check\", \"migrations\", verbosity=0)\n2401: call_command(\"makemigrations\", \"--check\", \"migrations\", verbosity=0)\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "22459745-242f-4784-a920-cefdd7d81ec6", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.672735, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 16, "action": {"name": "bash", "arguments": {"command": "sed -n '2385,2410p' /testbed/tests/migrations/test_commands.py"}, "id": "b9aedf06-38c6-41aa-889e-c7d345f5d83b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.737175, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 16, "observation": {"result": {"output": " call_command(\n \"makemigrations\", \"migrations\", \"--name\", \"invalid name\", \"--empty\"\n )\n\n def test_makemigrations_check(self):\n \"\"\"\n makemigrations --check should exit with a non-zero status when\n there are changes to an app requiring migrations.\n \"\"\"\n with self.temporary_migration_module():\n with self.assertRaises(SystemExit):\n call_command(\"makemigrations\", \"--check\", \"migrations\", verbosity=0)\n\n with self.temporary_migration_module(\n module=\"migrations.test_migrations_no_changes\"\n ):\n call_command(\"makemigrations\", \"--check\", \"migrations\", verbosity=0)\n\n def test_makemigrations_migration_path_output(self):\n \"\"\"\n makemigrations should print the relative paths to the migrations unless\n they are outside of the current tree, in which case the absolute path\n should be shown.\n \"\"\"\n out = io.StringIO()\n apps.register_model(\"migrations\", UnicodeModel)\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "b9aedf06-38c6-41aa-889e-c7d345f5d83b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.737175, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 17, "action": {"name": "bash", "arguments": {"command": "sed -n '238,260p' /testbed/django/core/management/commands/makemigrations.py"}, "id": "8b2e1b7d-6696-4efb-8f36-f8808f221e18", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.80659, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 17, "observation": {"result": {"output": " # No changes? Tell them.\n if self.verbosity >= 1:\n if app_labels:\n if len(app_labels) == 1:\n self.log(\"No changes detected in app '%s'\" % app_labels.pop())\n else:\n self.log(\n \"No changes detected in apps '%s'\"\n % (\"', '\".join(app_labels))\n )\n else:\n self.log(\"No changes detected\")\n else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\n\n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n new_changes = {}\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "8b2e1b7d-6696-4efb-8f36-f8808f221e18", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.80659, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 18, "action": {"name": "bash", "arguments": {"command": "cat > /tmp/fix.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Old code\nold_code = \"\"\" else:\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n if check_changes:\n sys.exit(1)\"\"\"\n\n# New code - exit before writing migrations when --check is used\nnew_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix.py"}, "id": "1669dbf3-9573-4641-bd39-5ecbf88eb708", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.88393, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 18, "observation": {"result": {"output": "Fix applied successfully!\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "1669dbf3-9573-4641-bd39-5ecbf88eb708", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.88393, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 19, "action": {"name": "bash", "arguments": {"command": "sed -n '238,265p' /testbed/django/core/management/commands/makemigrations.py"}, "id": "f2e557d2-5770-45c2-9b6d-ca26bbc55364", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.9555350000000001, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 19, "observation": {"result": {"output": " # No changes? Tell them.\n if self.verbosity >= 1:\n if app_labels:\n if len(app_labels) == 1:\n self.log(\"No changes detected in app '%s'\" % app_labels.pop())\n else:\n self.log(\n \"No changes detected in apps '%s'\"\n % (\"', '\".join(app_labels))\n )\n else:\n self.log(\"No changes detected\")\n else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n\n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n new_changes = {}\n update_previous_migration_paths = {}\n for app_label, app_migrations in changes.items():\n # Find last migration.\n leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label)\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "f2e557d2-5770-45c2-9b6d-ca26bbc55364", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.9555350000000001, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 20, "action": {"name": "bash", "arguments": {"command": "sed -n '68,90p' /testbed/django/core/management/commands/optimizemigration.py"}, "id": "5bc90588-d58b-4d0d-ad4c-73e45a52ce1c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.030645, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 20, "observation": {"result": {"output": " return\n else:\n if verbosity > 0:\n self.stdout.write(\n \"Optimizing from %d operations to %d operations.\"\n % (len(migration.operations), len(new_operations))\n )\n if check:\n sys.exit(1)\n\n # Set the new migration optimizations.\n migration.operations = new_operations\n\n # Write out the optimized migration file.\n writer = MigrationWriter(migration)\n migration_file_string = writer.as_string()\n if writer.needs_manual_porting:\n if migration.replaces:\n raise CommandError(\n \"Migration will require manual porting but is already a squashed \"\n \"migration.\\nTransition to a normal migration first: \"\n \"https://docs.djangoproject.com/en/%s/topics/migrations/\"\n \"#squashing-migrations\" % get_docs_version()\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "5bc90588-d58b-4d0d-ad4c-73e45a52ce1c", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.030645, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 21, "action": {"name": "bash", "arguments": {"command": "grep -n \"log\" /testbed/django/core/management/commands/makemigrations.py | head -30"}, "id": "b9115f25-c1fc-402e-9246-160fee8bf87f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.1096500000000002, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 21, "observation": {"result": {"output": "80: \"Divert log output and input prompts to stderr, writing only \"\n95: def log_output(self):\n98: def log(self, msg):\n99: self.log_output.write(msg)\n116: # If logs and prompts are diverted to stderr, remove the ERROR style.\n185: self.log(\"No conflicts detected to merge.\")\n197: prompt_output=self.log_output,\n204: log=self.log,\n242: self.log(\"No changes detected in app '%s'\" % app_labels.pop())\n244: self.log(\n249: self.log(\"No changes detected\")\n252: self.log(\"Detected changes.\")\n338: self.log(self.style.MIGRATE_HEADING(\"Migrations for '%s':\" % app_label))\n346: self.log(\" %s\\n\" % self.style.MIGRATE_LABEL(migration_string))\n348: self.log(\" - %s\" % operation.describe())\n370: self.log(\n380: self.log(f\"Deleted {rel_prev_path}\")\n383: # will log the migrations rather than saving the file to\n385: self.log(\n390: self.log(writer.as_string())\n409: questioner = InteractiveMigrationQuestioner(prompt_output=self.log_output)\n452: self.log(self.style.MIGRATE_HEADING(\"Merging %s\" % app_label))\n454: self.log(self.style.MIGRATE_LABEL(\" Branch %s\" % migration.name))\n456: self.log(\" - %s\" % operation.describe())\n500: self.log(\"\\nCreated new merge migration %s\" % writer.path)\n505: # will log the merge migrations rather than saving the file\n507: self.log(\n512: self.log(writer.as_string())\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "b9115f25-c1fc-402e-9246-160fee8bf87f", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.1096500000000002, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 22, "action": {"name": "bash", "arguments": {"command": "cat > /tmp/fix2.py << 'EOF'\nimport sys\n\n# Read the file\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'r') as f:\n content = f.read()\n\n# Current code (after first fix)\nold_code = \"\"\" else:\n if check_changes:\n self.log(\"Detected changes.\")\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# New code - just exit without extra message (consistent with migrate --check)\nnew_code = \"\"\" else:\n if check_changes:\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\"\"\"\n\n# Replace the old code with the new code\nif old_code not in content:\n print(\"Old code not found!\")\n sys.exit(1)\n\ncontent = content.replace(old_code, new_code)\n\n# Write the file back\nwith open('/testbed/django/core/management/commands/makemigrations.py', 'w') as f:\n f.write(content)\n\nprint(\"Fix applied successfully!\")\nEOF\npython /tmp/fix2.py"}, "id": "a87fdf01-3e1b-4c2d-9380-0a63d326e8d5", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.202385, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 22, "observation": {"result": {"output": "Fix applied successfully!\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "a87fdf01-3e1b-4c2d-9380-0a63d326e8d5", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.202385, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 23, "action": {"name": "bash", "arguments": {"command": "sed -n '238,265p' /testbed/django/core/management/commands/makemigrations.py"}, "id": "68825302-05fb-4da0-bf03-a8c293933270", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.285685, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 23, "observation": {"result": {"output": " # No changes? Tell them.\n if self.verbosity >= 1:\n if app_labels:\n if len(app_labels) == 1:\n self.log(\"No changes detected in app '%s'\" % app_labels.pop())\n else:\n self.log(\n \"No changes detected in apps '%s'\"\n % (\"', '\".join(app_labels))\n )\n else:\n self.log(\"No changes detected\")\n else:\n if check_changes:\n sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n\n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n new_changes = {}\n update_previous_migration_paths = {}\n for app_label, app_migrations in changes.items():\n # Find last migration.\n leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label)\n if len(leaf_migration_nodes) == 0:\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "68825302-05fb-4da0-bf03-a8c293933270", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.285685, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 24, "action": {"name": "bash", "arguments": {"command": "cd /testbed && python -m pytest tests/migrations/test_commands.py::MakeMigrationsTests::test_makemigrations_check -xvs 2>&1 | head -50"}, "id": "dc419fed-54c8-4d5d-8b12-6b460d3fcdcc", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.372265, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 24, "observation": {"result": {"output": "/opt/miniconda3/envs/testbed/bin/python: No module named pytest\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "dc419fed-54c8-4d5d-8b12-6b460d3fcdcc", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.372265, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 25, "action": {"name": "bash", "arguments": {"command": "cd /testbed && python -m django test migrations.test_commands.MakeMigrationsTests.test_makemigrations_check --settings=tests.test_sqlite 2>&1"}, "id": "73d08cd3-06bd-465b-9bdc-208275a27afa", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.459205, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 25, "observation": {"result": {"output": "Found 1 test(s).\nSystem check identified no issues (0 silenced).\nE\n======================================================================\nERROR: migrations (unittest.loader._FailedTest)\n----------------------------------------------------------------------\nImportError: Failed to import test module: migrations\nTraceback (most recent call last):\n File \"/opt/miniconda3/envs/testbed/lib/python3.9/unittest/loader.py\", line 154, in loadTestsFromName\n module = __import__(module_name)\nModuleNotFoundError: No module named 'migrations'\n\n\n----------------------------------------------------------------------\nRan 1 test in 0.003s\n\nFAILED (errors=1)\n", "returncode": 1}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "73d08cd3-06bd-465b-9bdc-208275a27afa", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.459205, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 26, "action": {"name": "bash", "arguments": {"command": "cd /testbed && python tests/runtests.py migrations.test_commands.MakeMigrationsTests.test_makemigrations_check 2>&1"}, "id": "74320f2d-8707-4824-8091-9829dc2c09d1", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.54765, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 26, "observation": {"result": {"output": "Creating test database for alias 'default'...\nTesting against Django installed in '/testbed/django' with up to 7 processes\nFound 1 test(s).\nCreating test database for alias 'other'...\nSystem check identified no issues (0 silenced).\n.\n----------------------------------------------------------------------\nRan 1 test in 0.186s\n\nOK\nDestroying test database for alias 'default'...\nDestroying test database for alias 'other'...\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "74320f2d-8707-4824-8091-9829dc2c09d1", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.54765, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 27, "action": {"name": "bash", "arguments": {"command": "cd /testbed && cat > /tmp/test_check.py << 'EOF'\nimport os\nimport sys\nimport tempfile\nimport shutil\n\n# Set up Django settings\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings')\n\n# Create a temporary directory for our test app\ntest_dir = tempfile.mkdtemp()\napp_dir = os.path.join(test_dir, 'myapp')\nos.makedirs(app_dir)\n\n# Create app files\nwith open(os.path.join(app_dir, '__init__.py'), 'w') as f:\n f.write('')\n\nwith open(os.path.join(app_dir, 'models.py'), 'w') as f:\n f.write('''\nfrom django.db import models\n\nclass TestModel(models.Model):\n name = models.CharField(max_length=100)\n''')\n\nmigrations_dir = os.path.join(app_dir, 'migrations')\nos.makedirs(migrations_dir)\nwith open(os.path.join(migrations_dir, '__init__.py'), 'w') as f:\n f.write('')\n\n# Create settings file \nwith open(os.path.join(test_dir, 'test_settings.py'), 'w') as f:\n f.write(f'''\nSECRET_KEY = 'test-secret-key'\nINSTALLED_APPS = [\n 'django.contrib.contenttypes',\n 'django.contrib.auth',\n 'myapp',\n]\nDATABASES = {{\n 'default': {{\n 'ENGINE': 'django.db.backends.sqlite3',\n 'NAME': ':memory:',\n }}\n}}\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n''')\n\n# Add test_dir to sys.path\nsys.path.insert(0, test_dir)\n\n# Configure Django\nimport django\ndjango.setup()\n\nfrom django.core.management import call_command\nimport io\n\n# Test 1: Check that --check exits with code 1 and does NOT create migrations\nprint(\"Test 1: --check should exit with code 1 without creating migrations\")\nout = io.StringIO()\nerr = io.StringIO()\n\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"FAIL: Expected SystemExit(1)\")\n sys.exit(1)\nexcept SystemExit as e:\n if e.code != 1:\n print(f\"FAIL: Expected exit code 1, got {e.code}\")\n sys.exit(1)\n\n# Check that no migration file was created\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif migration_files:\n print(f\"FAIL: Migration files were created: {migration_files}\")\n sys.exit(1)\n\nprint(\"PASS: --check exited with code 1 and no migration files were created\")\n\n# Test 2: Without --check, migrations should be created\nprint(\"\\nTest 2: Without --check, migrations should be created\")\nout = io.StringIO()\ncall_command('makemigrations', 'myapp', stdout=out, stderr=err, verbosity=0)\n\nmigration_files = [f for f in os.listdir(migrations_dir) if f.endswith('.py') and f != '__init__.py']\nif not migration_files:\n print(\"FAIL: Migration files were not created\")\n sys.exit(1)\n\nprint(f\"PASS: Migration files were created: {migration_files}\")\n\n# Test 3: With migrations present and no changes, --check should succeed (exit 0)\nprint(\"\\nTest 3: With no changes, --check should succeed (exit 0)\")\ntry:\n call_command('makemigrations', 'myapp', '--check', stdout=out, stderr=err, verbosity=0)\n print(\"PASS: --check exited with code 0 when no changes\")\nexcept SystemExit as e:\n if e.code != 0:\n print(f\"FAIL: Expected exit code 0, got {e.code}\")\n sys.exit(1)\n print(\"PASS: --check exited with code 0 when no changes\")\n\n# Cleanup\nshutil.rmtree(test_dir)\nprint(\"\\nAll tests passed!\")\nEOF\npython /tmp/test_check.py"}, "id": "a7cbe6fa-310f-4106-899b-96a613757c9d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.6643600000000003, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 27, "observation": {"result": {"output": "Test 1: --check should exit with code 1 without creating migrations\nPASS: --check exited with code 1 and no migration files were created\n\nTest 2: Without --check, migrations should be created\nPASS: Migration files were created: ['0001_initial.py']\n\nTest 3: With no changes, --check should succeed (exit 0)\nPASS: --check exited with code 0 when no changes\n\nAll tests passed!\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "a7cbe6fa-310f-4106-899b-96a613757c9d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.6643600000000003, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 28, "action": {"name": "bash", "arguments": {"command": "cd /testbed && python tests/runtests.py migrations.test_commands.MakeMigrationsTests -v 2 2>&1 | tail -50"}, "id": "8f5e355a-7293-4108-8ec8-1ce5ec94bb49", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.7615600000000002, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 28, "observation": {"result": {"output": "test_makemigrations_migrations_modules_path_not_exist (migrations.test_commands.MakeMigrationsTests)\nmakemigrations creates migrations when specifying a custom location ... ok\ntest_makemigrations_model_rename_interactive (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_no_apps_initial (migrations.test_commands.MakeMigrationsTests)\nmakemigrations should detect initial is needed on empty migration ... ok\ntest_makemigrations_no_changes (migrations.test_commands.MakeMigrationsTests)\nmakemigrations exits when there are no changes to an app. ... ok\ntest_makemigrations_no_changes_no_apps (migrations.test_commands.MakeMigrationsTests)\nmakemigrations exits when there are no changes and no apps are specified. ... ok\ntest_makemigrations_no_common_ancestor (migrations.test_commands.MakeMigrationsTests)\nmakemigrations fails to merge migrations with no common ancestor. ... ok\ntest_makemigrations_no_init (migrations.test_commands.MakeMigrationsTests)\nMigration directories without an __init__.py file are allowed. ... ok\ntest_makemigrations_non_interactive_auto_now_add_addition (migrations.test_commands.MakeMigrationsTests)\nNon-interactive makemigrations fails when a default is missing on a ... ok\ntest_makemigrations_non_interactive_no_field_rename (migrations.test_commands.MakeMigrationsTests)\nmakemigrations adds and removes a possible field rename in ... ok\ntest_makemigrations_non_interactive_no_model_rename (migrations.test_commands.MakeMigrationsTests)\nmakemigrations adds and removes a possible model rename in ... ok\ntest_makemigrations_non_interactive_not_null_addition (migrations.test_commands.MakeMigrationsTests)\nNon-interactive makemigrations fails when a default is missing on a ... ok\ntest_makemigrations_non_interactive_not_null_alteration (migrations.test_commands.MakeMigrationsTests)\nNon-interactive makemigrations fails when a default is missing on a ... ok\ntest_makemigrations_non_interactive_unique_callable_default_addition (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_order (migrations.test_commands.MakeMigrationsTests)\nmakemigrations should recognize number-only migrations (0001.py). ... ok\ntest_makemigrations_scriptable (migrations.test_commands.MakeMigrationsTests)\nWith scriptable=True, log output is diverted to stderr, and only the ... ok\ntest_makemigrations_scriptable_merge (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_unspecified_app_with_conflict_merge (migrations.test_commands.MakeMigrationsTests)\nmakemigrations does not create a merge for an unspecified app even if ... ok\ntest_makemigrations_unspecified_app_with_conflict_no_merge (migrations.test_commands.MakeMigrationsTests)\nmakemigrations does not raise a CommandError when an unspecified app ... ok\ntest_makemigrations_update (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_applied_migration (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_dependency_migration (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_existing_name (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_manual_porting (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_no_migration (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_update_squash_migration (migrations.test_commands.MakeMigrationsTests) ... ok\ntest_makemigrations_with_custom_name (migrations.test_commands.MakeMigrationsTests)\nmakemigrations --name generate a custom migration name. ... ok\ntest_makemigrations_with_invalid_custom_name (migrations.test_commands.MakeMigrationsTests) ... ok\n\n----------------------------------------------------------------------\nRan 61 tests in 58.478s\n\nOK\nDestroying test database for alias 'default' ('file:memorydb_default?mode=memory&cache=shared')...\nDestroying test database for alias 'other' ('file:memorydb_other?mode=memory&cache=shared')...\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "8f5e355a-7293-4108-8ec8-1ce5ec94bb49", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.7615600000000002, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 29, "action": {"name": "bash", "arguments": {"command": "rm /tmp/fix.py /tmp/fix2.py /tmp/test_check.py 2>/dev/null; echo \"Cleaned up\""}, "id": "e60a8fde-8d0d-4963-93ca-4d23b1c17334", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.8653050000000002, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 29, "observation": {"result": {"output": "Cleaned up\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "e60a8fde-8d0d-4963-93ca-4d23b1c17334", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.8653050000000002, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 30, "action": {"name": "bash", "arguments": {"command": "cd /testbed && git diff"}, "id": "6f4ad6f4-c580-4e05-972a-acc31ca3935b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 1.9690100000000001, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 30, "observation": {"result": {"output": "diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py\nindex 2e97d9064b..f257e947ba 100644\n--- a/django/core/management/commands/makemigrations.py\n+++ b/django/core/management/commands/makemigrations.py\n@@ -248,12 +248,12 @@ class Command(BaseCommand):\n else:\n self.log(\"No changes detected\")\n else:\n+ if check_changes:\n+ sys.exit(1)\n if self.update:\n self.write_to_last_migration_files(changes)\n else:\n self.write_migration_files(changes)\n- if check_changes:\n- sys.exit(1)\n \n def write_to_last_migration_files(self, changes):\n loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "6f4ad6f4-c580-4e05-972a-acc31ca3935b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 1.9690100000000001, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 31, "action": {"name": "bash", "arguments": {"command": "cd /testbed && python tests/runtests.py migrations.test_commands -v 1 2>&1 | tail -20"}, "id": "c8ae62f9-a98d-4191-95c3-ee4edcb5869d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.07892, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 31, "observation": {"result": {"output": "Cloning test database for alias 'other'...\nCloning test database for alias 'other'...\nSystem check identified no issues (0 silenced).\n............................................................................................................................................\n----------------------------------------------------------------------\nRan 140 tests in 70.451s\n\nOK\nDestroying test database for alias 'default'...\nDestroying test database for alias 'default'...\nDestroying test database for alias 'default'...\nDestroying test database for alias 'default'...\nDestroying test database for alias 'default'...\nDestroying test database for alias 'default'...\nDestroying test database for alias 'other'...\nDestroying test database for alias 'other'...\nDestroying test database for alias 'other'...\nDestroying test database for alias 'other'...\nDestroying test database for alias 'other'...\nDestroying test database for alias 'other'...\n", "returncode": 0}, "invoking_actions": [{"name": "bash", "arguments": {}, "id": "c8ae62f9-a98d-4191-95c3-ee4edcb5869d", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 2.07892, "benchmark_cost": 0} +{"event": "action", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 32, "action": {"name": "finish", "arguments": {"summary": "Fixed the issue where `makemigrations --check` was generating migrations before exiting. \n\nThe fix changes the behavior so that when `--check` is specified and there are changes, the command exits with code 1 immediately WITHOUT creating migration files. This makes `makemigrations --check` consistent with `migrate --check` and `optimizemigration --check`, which also just exit without making changes.\n\nThe change was minimal - simply moving the `check_changes` exit statement to before the migration file writing logic in `django/core/management/commands/makemigrations.py`."}, "id": "81cb699a-3d6b-4db1-a77b-3a77f576f368", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 2.190265, "benchmark_cost": 0} +{"event": "observation", "run_id": "2026-01-28T07--33--40.579306", "session_id": "c4ab7ec0_ce8c_41f0_a585_cf37ca9b94f0", "session_number": 19, "task_key": "", "step": 32, "observation": null, "initial": false, "agent_cost": 2.190265, "benchmark_cost": 0} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/recording.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/recording.json new file mode 100644 index 00000000..a5d44355 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/recording.json @@ -0,0 +1,5 @@ +{ + "benchmark": "tau2", + "task_id": "47", + "expected_score": 1.0 +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/results.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/results.json new file mode 100644 index 00000000..762400b4 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/results.json @@ -0,0 +1,77 @@ +{ + "session_id": "9b03dde8", + "success": true, + "score": 1.0, + "is_finished": true, + "steps": 8, + "action_count": 8, + "invalid_action_count": 0, + "agent_cost": 0.34320500000000004, + "benchmark_cost": 0.015026, + "execution_time": 46.77257513999939, + "details": { + "score": 1.0, + "success": true, + "is_finished": true, + "session_metrics": { + "reward": 1.0, + "db_check_db_match": true, + "db_check_db_reward": 1.0 + }, + "session_metadata": { + "reward_info": { + "reward": 1.0, + "db_check": { + "db_match": true, + "db_reward": 1.0 + }, + "env_assertions": [], + "action_checks": [ + { + "action": { + "action_id": "47_0", + "requestor": "assistant", + "name": "get_reservation_details", + "arguments": { + "reservation_id": "H8Q05L" + }, + "info": null, + "compare_args": null + }, + "action_match": true, + "action_reward": 1.0 + } + ], + "nl_assertions": null, + "communicate_checks": null, + "reward_basis": [ + "DB", + "COMMUNICATE" + ], + "reward_breakdown": { + "DB": 1.0, + "COMMUNICATE": 1.0 + }, + "info": { + "env": null, + "nl": null, + "communicate": { + "note": "No communicate_info to evaluate" + }, + "action": null + } + } + } + }, + "cost_reports": { + "agent": { + "model_name": "openai/aws/claude-opus-4-5", + "total_cost": 0.34320500000000004 + }, + "benchmark": { + "model_name": "openai/Azure/gpt-4.1", + "total_cost": 0.015026 + } + }, + "task_key": "47" +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/session.json b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/session.json new file mode 100644 index 00000000..e96ba4e5 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/session.json @@ -0,0 +1,642 @@ +{ + "task": "You are a customer service agent that helps the user according to the provided below. Try to be helpful and always follow the policy.", + "context": { + "policy": "# Airline Agent Policy\n\nThe current time is 2024-05-15 15:00:00 EST.\n\nAs an airline agent, you can help users **book**, **modify**, or **cancel** flight reservations. You also handle **refunds and compensation**.\n\nBefore taking any actions that update the booking database (booking, modifying flights, editing baggage, changing cabin class, or updating passenger information), you must list the action details and obtain explicit user confirmation (yes) to proceed.\n\nYou should not provide any information, knowledge, or procedures not provided by the user or available tools, or give subjective recommendations or comments.\n\nYou should only make one tool call at a time, and if you make a tool call, you should not respond to the user simultaneously. If you respond to the user, you should not make a tool call at the same time.\n\nYou should deny user requests that are against this policy.\n\nYou should transfer the user to a human agent if and only if the request cannot be handled within the scope of your actions. To transfer, first make a tool call to transfer_to_human_agents, and then send the message 'YOU ARE BEING TRANSFERRED TO A HUMAN AGENT. PLEASE HOLD ON.' to the user.\n\n## Domain Basic\n\n### User\nEach user has a profile containing:\n- user id\n- email\n- addresses\n- date of birth\n- payment methods\n- membership level\n- reservation numbers\n\nThere are three types of payment methods: **credit card**, **gift card**, **travel certificate**.\n\nThere are three membership levels: **regular**, **silver**, **gold**.\n\n### Flight\nEach flight has the following attributes:\n- flight number\n- origin\n- destination\n- scheduled departure and arrival time (local time)\n\nA flight can be available at multiple dates. For each date:\n- If the status is **available**, the flight has not taken off, available seats and prices are listed.\n- If the status is **delayed** or **on time**, the flight has not taken off, cannot be booked.\n- If the status is **flying**, the flight has taken off but not landed, cannot be booked.\n\nThere are three cabin classes: **basic economy**, **economy**, **business**. **basic economy** is its own class, completely distinct from **economy**.\n\nSeat availability and prices are listed for each cabin class.\n\n### Reservation\nEach reservation specifies the following:\n- reservation id\n- user id\n- trip type\n- flights\n- passengers\n- payment methods\n- created time\n- baggages\n- travel insurance information\n\nThere are two types of trip: **one way** and **round trip**.\n\n## Book flight\n\nThe agent must first obtain the user id from the user. \n\nThe agent should then ask for the trip type, origin, destination.\n\nCabin:\n- Cabin class must be the same across all the flights in a reservation. \n\nPassengers: \n- Each reservation can have at most five passengers. \n- The agent needs to collect the first name, last name, and date of birth for each passenger. \n- All passengers must fly the same flights in the same cabin.\n\nPayment: \n- Each reservation can use at most one travel certificate, at most one credit card, and at most three gift cards. \n- The remaining amount of a travel certificate is not refundable. \n- All payment methods must already be in user profile for safety reasons.\n\nChecked bag allowance: \n- If the booking user is a regular member:\n - 0 free checked bag for each basic economy passenger\n - 1 free checked bag for each economy passenger\n - 2 free checked bags for each business passenger\n- If the booking user is a silver member:\n - 1 free checked bag for each basic economy passenger\n - 2 free checked bag for each economy passenger\n - 3 free checked bags for each business passenger\n- If the booking user is a gold member:\n - 2 free checked bag for each basic economy passenger\n - 3 free checked bag for each economy passenger\n - 4 free checked bags for each business passenger\n- Each extra baggage is 50 dollars.\n\nDo not add checked bags that the user does not need.\n\nTravel insurance: \n- The agent should ask if the user wants to buy the travel insurance.\n- The travel insurance is 30 dollars per passenger and enables full refund if the user needs to cancel the flight given health or weather reasons.\n\n## Modify flight\n\nFirst, the agent must obtain the user id and reservation id. \n- The user must provide their user id. \n- If the user doesn't know their reservation id, the agent should help locate it using available tools.\n\nChange flights: \n- Basic economy flights cannot be modified.\n- Other reservations can be modified without changing the origin, destination, and trip type.\n- Some flight segments can be kept, but their prices will not be updated based on the current price.\n- The API does not check these for the agent, so the agent must make sure the rules apply before calling the API!\n\nChange cabin: \n- Cabin cannot be changed if any flight in the reservation has already been flown.\n- In other cases, all reservations, including basic economy, can change cabin without changing the flights.\n- Cabin class must remain the same across all the flights in the same reservation; changing cabin for just one flight segment is not possible.\n- If the price after cabin change is higher than the original price, the user is required to pay for the difference.\n- If the price after cabin change is lower than the original price, the user is should be refunded the difference.\n\nChange baggage and insurance: \n- The user can add but not remove checked bags.\n- The user cannot add insurance after initial booking.\n\nChange passengers:\n- The user can modify passengers but cannot modify the number of passengers.\n- Even a human agent cannot modify the number of passengers.\n\nPayment: \n- If the flights are changed, the user needs to provide a single gift card or credit card for payment or refund method. The payment method must already be in user profile for safety reasons.\n\n## Cancel flight\n\nFirst, the agent must obtain the user id and reservation id. \n- The user must provide their user id. \n- If the user doesn't know their reservation id, the agent should help locate it using available tools.\n\nThe agent must also obtain the reason for cancellation (change of plan, airline cancelled flight, or other reasons)\n\nIf any portion of the flight has already been flown, the agent cannot help and transfer is needed.\n\nOtherwise, flight can be cancelled if any of the following is true:\n- The booking was made within the last 24 hrs\n- The flight is cancelled by airline\n- It is a business flight\n- The user has travel insurance and the reason for cancellation is covered by insurance.\n\nThe API does not check that cancellation rules are met, so the agent must make sure the rules apply before calling the API!\n\nRefund:\n- The refund will go to original payment methods within 5 to 7 business days.\n\n## Refunds and Compensation\nDo not proactively offer a compensation unless the user explicitly asks for one.\n\nDo not compensate if the user is regular member and has no travel insurance and flies (basic) economy.\n\nAlways confirms the facts before offering compensation.\n\nOnly compensate if the user is a silver/gold member or has travel insurance or flies business.\n\n- If the user complains about cancelled flights in a reservation, the agent can offer a certificate as a gesture after confirming the facts, with the amount being $100 times the number of passengers.\n\n- If the user complains about delayed flights in a reservation and wants to change or cancel the reservation, the agent can offer a certificate as a gesture after confirming the facts and changing or cancelling the reservation, with the amount being $50 times the number of passengers.\n\nDo not offer compensation for any other reason than the ones listed above." + }, + "actions": [ + { + "name": "message", + "description": "Send a message to the user.", + "is_finish": false, + "is_message": true, + "is_hidden": false, + "arguments_schema": { + "properties": { + "content": { + "title": "Content", + "type": "string" + } + }, + "required": [ + "content" + ], + "title": "Message", + "type": "object" + } + }, + { + "name": "book_reservation", + "description": "Book a reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "$defs": { + "FlightInfo": { + "properties": { + "flight_number": { + "description": "Flight number, such as 'HAT001'.", + "title": "Flight Number", + "type": "string" + }, + "date": { + "description": "The date for the flight in the format 'YYYY-MM-DD', such as '2024-05-01'.", + "title": "Date", + "type": "string" + } + }, + "required": [ + "flight_number", + "date" + ], + "title": "FlightInfo", + "type": "object" + }, + "Passenger": { + "properties": { + "first_name": { + "description": "Passenger's first name", + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Passenger's last name", + "title": "Last Name", + "type": "string" + }, + "dob": { + "description": "Date of birth in YYYY-MM-DD format", + "title": "Dob", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "dob" + ], + "title": "Passenger", + "type": "object" + }, + "Payment": { + "properties": { + "payment_id": { + "description": "Unique identifier for the payment", + "title": "Payment Id", + "type": "string" + }, + "amount": { + "description": "Payment amount in dollars", + "title": "Amount", + "type": "integer" + } + }, + "required": [ + "payment_id", + "amount" + ], + "title": "Payment", + "type": "object" + } + }, + "properties": { + "user_id": { + "description": "The ID of the user to book the reservation such as 'sara_doe_496'`.", + "title": "User Id", + "type": "string" + }, + "origin": { + "description": "The IATA code for the origin city such as 'SFO'.", + "title": "Origin", + "type": "string" + }, + "destination": { + "description": "The IATA code for the destination city such as 'JFK'.", + "title": "Destination", + "type": "string" + }, + "flight_type": { + "description": "The type of flight such as 'one_way' or 'round_trip'.", + "enum": [ + "round_trip", + "one_way" + ], + "title": "Flight Type", + "type": "string" + }, + "cabin": { + "description": "The cabin class such as 'basic_economy', 'economy', or 'business'.", + "enum": [ + "business", + "economy", + "basic_economy" + ], + "title": "Cabin", + "type": "string" + }, + "flights": { + "description": "An array of objects containing details about each piece of flight.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/FlightInfo" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "title": "Flights", + "type": "array" + }, + "passengers": { + "description": "An array of objects containing details about each passenger.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Passenger" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "title": "Passengers", + "type": "array" + }, + "payment_methods": { + "description": "An array of objects containing details about each payment method.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Payment" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "title": "Payment Methods", + "type": "array" + }, + "total_baggages": { + "description": "The total number of baggage items to book the reservation.", + "title": "Total Baggages", + "type": "integer" + }, + "nonfree_baggages": { + "description": "The number of non-free baggage items to book the reservation.", + "title": "Nonfree Baggages", + "type": "integer" + }, + "insurance": { + "description": "Whether the reservation has insurance.", + "enum": [ + "yes", + "no" + ], + "title": "Insurance", + "type": "string" + } + }, + "required": [ + "user_id", + "origin", + "destination", + "flight_type", + "cabin", + "flights", + "passengers", + "payment_methods", + "total_baggages", + "nonfree_baggages", + "insurance" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "calculate", + "description": "Calculate the result of a mathematical expression.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "expression": { + "description": "The mathematical expression to calculate, such as '2 + 2'. The expression can contain numbers, operators (+, -, *, /), parentheses, and spaces.", + "title": "Expression", + "type": "string" + } + }, + "required": [ + "expression" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "cancel_reservation", + "description": "Cancel the whole reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "reservation_id": { + "description": "The reservation ID, such as 'ZFA04Y'.", + "title": "Reservation Id", + "type": "string" + } + }, + "required": [ + "reservation_id" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "get_reservation_details", + "description": "Get the details of a reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "reservation_id": { + "description": "The reservation ID, such as '8JX2WO'.", + "title": "Reservation Id", + "type": "string" + } + }, + "required": [ + "reservation_id" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "get_user_details", + "description": "Get the details of a user, including their reservations.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_id": { + "description": "The user ID, such as 'sara_doe_496'.", + "title": "User Id", + "type": "string" + } + }, + "required": [ + "user_id" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "list_all_airports", + "description": "Returns a list of all available airports.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": {}, + "title": "parameters", + "type": "object" + } + }, + { + "name": "search_direct_flight", + "description": "Search for direct flights between two cities on a specific date.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "origin": { + "description": "The origin city airport in three letters, such as 'JFK'.", + "title": "Origin", + "type": "string" + }, + "destination": { + "description": "The destination city airport in three letters, such as 'LAX'.", + "title": "Destination", + "type": "string" + }, + "date": { + "description": "The date of the flight in the format 'YYYY-MM-DD', such as '2024-01-01'.", + "title": "Date", + "type": "string" + } + }, + "required": [ + "origin", + "destination", + "date" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "search_onestop_flight", + "description": "Search for one-stop flights between two cities on a specific date.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "origin": { + "description": "The origin city airport in three letters, such as 'JFK'.", + "title": "Origin", + "type": "string" + }, + "destination": { + "description": "The destination city airport in three letters, such as 'LAX'.", + "title": "Destination", + "type": "string" + }, + "date": { + "description": "The date of the flight in the format 'YYYY-MM-DD', such as '2024-05-01'.", + "title": "Date", + "type": "string" + } + }, + "required": [ + "origin", + "destination", + "date" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "send_certificate", + "description": "Send a certificate to a user. Be careful!", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "user_id": { + "description": "The ID of the user to book the reservation, such as 'sara_doe_496'.", + "title": "User Id", + "type": "string" + }, + "amount": { + "description": "The amount of the certificate to send.", + "title": "Amount", + "type": "integer" + } + }, + "required": [ + "user_id", + "amount" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "transfer_to_human_agents", + "description": "Transfer the user to a human agent, with a summary of the user's issue.\n\nOnly transfer if\n - the user explicitly asks for a human agent\n - given the policy and the available tools, you cannot solve the user's issue.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "summary": { + "description": "A summary of the user's issue.", + "title": "Summary", + "type": "string" + } + }, + "required": [ + "summary" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "update_reservation_baggages", + "description": "Update the baggage information of a reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "reservation_id": { + "description": "The reservation ID, such as 'ZFA04Y'", + "title": "Reservation Id", + "type": "string" + }, + "total_baggages": { + "description": "The updated total number of baggage items included in the reservation.", + "title": "Total Baggages", + "type": "integer" + }, + "nonfree_baggages": { + "description": "The updated number of non-free baggage items included in the reservation.", + "title": "Nonfree Baggages", + "type": "integer" + }, + "payment_id": { + "description": "The payment id stored in user profile, such as 'credit_card_7815826', 'gift_card_7815826', 'certificate_7815826'.", + "title": "Payment Id", + "type": "string" + } + }, + "required": [ + "reservation_id", + "total_baggages", + "nonfree_baggages", + "payment_id" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "update_reservation_flights", + "description": "Update the flight information of a reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "$defs": { + "FlightInfo": { + "properties": { + "flight_number": { + "description": "Flight number, such as 'HAT001'.", + "title": "Flight Number", + "type": "string" + }, + "date": { + "description": "The date for the flight in the format 'YYYY-MM-DD', such as '2024-05-01'.", + "title": "Date", + "type": "string" + } + }, + "required": [ + "flight_number", + "date" + ], + "title": "FlightInfo", + "type": "object" + } + }, + "properties": { + "reservation_id": { + "description": "The reservation ID, such as 'ZFA04Y'.", + "title": "Reservation Id", + "type": "string" + }, + "cabin": { + "description": "The cabin class of the reservation", + "enum": [ + "business", + "economy", + "basic_economy" + ], + "title": "Cabin", + "type": "string" + }, + "flights": { + "description": "An array of objects containing details about each piece of flight in the ENTIRE new reservation. Even if the a flight segment is not changed, it should still be included in the array.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/FlightInfo" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "title": "Flights", + "type": "array" + }, + "payment_id": { + "description": "The payment id stored in user profile, such as 'credit_card_7815826', 'gift_card_7815826', 'certificate_7815826'.", + "title": "Payment Id", + "type": "string" + } + }, + "required": [ + "reservation_id", + "cabin", + "flights", + "payment_id" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "update_reservation_passengers", + "description": "Update the passenger information of a reservation.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "$defs": { + "Passenger": { + "properties": { + "first_name": { + "description": "Passenger's first name", + "title": "First Name", + "type": "string" + }, + "last_name": { + "description": "Passenger's last name", + "title": "Last Name", + "type": "string" + }, + "dob": { + "description": "Date of birth in YYYY-MM-DD format", + "title": "Dob", + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "dob" + ], + "title": "Passenger", + "type": "object" + } + }, + "properties": { + "reservation_id": { + "description": "The reservation ID, such as 'ZFA04Y'.", + "title": "Reservation Id", + "type": "string" + }, + "passengers": { + "description": "An array of objects containing details about each passenger.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Passenger" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "title": "Passengers", + "type": "array" + } + }, + "required": [ + "reservation_id", + "passengers" + ], + "title": "parameters", + "type": "object" + } + }, + { + "name": "get_flight_status", + "description": "Get the status of a flight.", + "is_finish": false, + "is_message": false, + "is_hidden": false, + "arguments_schema": { + "properties": { + "flight_number": { + "description": "The flight number.", + "title": "Flight Number", + "type": "string" + }, + "date": { + "description": "The date of the flight.", + "title": "Date", + "type": "string" + } + }, + "required": [ + "flight_number", + "date" + ], + "title": "parameters", + "type": "object" + } + } + ] +} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/trajectory.jsonl b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/trajectory.jsonl new file mode 100644 index 00000000..7ed88996 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/recordings/tau2/trajectory.jsonl @@ -0,0 +1,17 @@ +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 0, "observation": {"result": {"sender": "user", "message": "Hi, I need to cancel my flight."}, "invoking_actions": []}, "initial": true, "agent_cost": 0, "benchmark_cost": 0.000936} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 1, "action": {"name": "message", "arguments": {"content": "Hi! I'd be happy to help you cancel your flight. To proceed, I'll need the following information:\n\n1. Your user ID\n2. Your reservation ID (if you don't know it, I can help you find it once you provide your user ID)\n3. The reason for cancellation (e.g., change of plans, airline cancelled the flight, or other reasons)\n\nPlease provide these details so I can assist you."}, "id": "843f6abf-65df-4935-89a1-bca0a93405c2", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.03374, "benchmark_cost": 0.000936} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 1, "observation": {"result": {"sender": "user", "message": "Sure, my user ID is sophia_silva_7557 and my confirmation number is H8Q05L. I need to cancel because the flight is on the same day as my best friend's birthday."}, "invoking_actions": [{"name": "message", "arguments": {}, "id": "843f6abf-65df-4935-89a1-bca0a93405c2", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.03374, "benchmark_cost": 0.0023480000000000003} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 2, "action": {"name": "get_reservation_details", "arguments": {"reservation_id": "H8Q05L"}, "id": "fba8ffc5-3522-41ac-8c7d-6a453d715a2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.06775500000000001, "benchmark_cost": 0.0023480000000000003} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 2, "observation": {"result": "{\"reservation_id\": \"H8Q05L\", \"user_id\": \"sophia_silva_7557\", \"origin\": \"JFK\", \"destination\": \"ATL\", \"flight_type\": \"one_way\", \"cabin\": \"basic_economy\", \"flights\": [{\"flight_number\": \"HAT268\", \"origin\": \"JFK\", \"destination\": \"ATL\", \"date\": \"2024-05-24\", \"price\": 74}], \"passengers\": [{\"first_name\": \"Harper\", \"last_name\": \"Kovacs\", \"dob\": \"1973-10-26\"}], \"payment_history\": [{\"payment_id\": \"credit_card_4196779\", \"amount\": 104}], \"created_at\": \"2024-05-03T15:12:00\", \"total_baggages\": 0, \"nonfree_baggages\": 0, \"insurance\": \"yes\", \"status\": null}", "invoking_actions": [{"name": "get_reservation_details", "arguments": {}, "id": "fba8ffc5-3522-41ac-8c7d-6a453d715a2b", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.06775500000000001, "benchmark_cost": 0.0023480000000000003} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 3, "action": {"name": "get_user_details", "arguments": {"user_id": "sophia_silva_7557"}, "id": "29f4b212-fa08-4692-9a83-5236cd8d9499", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.06775500000000001, "benchmark_cost": 0.0023480000000000003} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 3, "observation": {"result": "{\"user_id\": \"sophia_silva_7557\", \"name\": {\"first_name\": \"Sophia\", \"last_name\": \"Silva\"}, \"address\": {\"address1\": \"141 Cedar Avenue\", \"address2\": \"Suite 436\", \"city\": \"Columbus\", \"country\": \"USA\", \"state\": \"OH\", \"zip\": \"43282\"}, \"email\": \"sophia.silva5929@example.com\", \"dob\": \"1957-10-05\", \"payment_methods\": {\"certificate_8045380\": {\"source\": \"certificate\", \"id\": \"certificate_8045380\", \"amount\": 500.0}, \"certificate_3887113\": {\"source\": \"certificate\", \"id\": \"certificate_3887113\", \"amount\": 150.0}, \"credit_card_4196779\": {\"source\": \"credit_card\", \"id\": \"credit_card_4196779\", \"brand\": \"visa\", \"last_four\": \"7238\"}, \"gift_card_5094406\": {\"source\": \"gift_card\", \"id\": \"gift_card_5094406\", \"amount\": 274.0}}, \"saved_passengers\": [{\"first_name\": \"Yara\", \"last_name\": \"Silva\", \"dob\": \"1972-06-16\"}, {\"first_name\": \"Isabella\", \"last_name\": \"Moore\", \"dob\": \"1988-06-04\"}], \"membership\": \"regular\", \"reservations\": [\"NM1VX1\", \"KC18K6\", \"S61CZX\", \"H8Q05L\", \"WUNA5K\"]}", "invoking_actions": [{"name": "get_user_details", "arguments": {}, "id": "29f4b212-fa08-4692-9a83-5236cd8d9499", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.06775500000000001, "benchmark_cost": 0.0023480000000000003} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 4, "action": {"name": "message", "arguments": {"content": "I found your reservation. Here are the details:\n\n**Reservation H8Q05L:**\n- **Flight:** HAT268 from JFK to ATL on May 24, 2024\n- **Cabin:** Basic Economy\n- **Passenger:** Harper Kovacs\n- **Travel Insurance:** Yes\n- **Total Paid:** $104\n\nI understand you need to cancel because the flight conflicts with your best friend's birthday. However, I need to let you know about the cancellation policy:\n\nYour reservation is a **basic economy** flight, and the booking was made on May 3, 2024 (more than 24 hours ago). While you do have travel insurance, the insurance only covers cancellations due to **health or weather reasons**, not personal schedule conflicts like a birthday celebration.\n\nUnfortunately, based on our policy, I cannot cancel this reservation because:\n- It's a basic economy flight\n- The booking was not made within the last 24 hours\n- The flight hasn't been cancelled by the airline\n- The reason for cancellation (attending a birthday) is not covered by your travel insurance\n\nI'm sorry I cannot proceed with this cancellation. Is there anything else I can help you with?"}, "id": "cd6f89e9-14d3-4e04-b5fe-cc90391c20b4", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.11136, "benchmark_cost": 0.0023480000000000003} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 4, "observation": {"result": {"sender": "user", "message": "I really need to cancel this flight and get a full refund. Is there any way you can make an exception? This is very important to me."}, "invoking_actions": [{"name": "message", "arguments": {}, "id": "cd6f89e9-14d3-4e04-b5fe-cc90391c20b4", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.11136, "benchmark_cost": 0.00424} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 5, "action": {"name": "message", "arguments": {"content": "I completely understand this is important to you, and I wish I could help more. However, I'm not able to make exceptions to the cancellation policy. The rules are in place for all reservations:\n\nFor a basic economy flight to be eligible for cancellation, at least one of these conditions must be met:\n1. The booking was made within the last 24 hours\n2. The flight was cancelled by the airline\n3. The cancellation reason is covered by travel insurance (health or weather-related issues)\n\nSince your situation doesn't meet any of these criteria, I cannot process the cancellation with a refund.\n\nIf you believe there are exceptional circumstances that warrant further review, I can transfer you to a human agent who may have additional options available. Would you like me to do that?\n\nIs there anything else I can assist you with?"}, "id": "837be399-58a0-4f6b-bc1a-f6ae3e577b8e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.15483, "benchmark_cost": 0.00424} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 5, "observation": {"result": {"sender": "user", "message": "I really don’t want to be transferred. I just want to get a full refund for this flight. Can you please check again if there’s anything you can do? This is a special situation for me."}, "invoking_actions": [{"name": "message", "arguments": {}, "id": "837be399-58a0-4f6b-bc1a-f6ae3e577b8e", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.15483, "benchmark_cost": 0.006632} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 6, "action": {"name": "message", "arguments": {"content": "I truly understand your frustration, and I appreciate you explaining how important this is to you. However, I must be honest with you - I've already checked all the applicable policies, and I'm not authorized to make exceptions to the cancellation rules.\n\nTo summarize what I've verified:\n- ✗ Basic economy flights are non-refundable for personal reasons\n- ✗ Your booking is outside the 24-hour cancellation window (booked May 3rd)\n- ✗ The airline has not cancelled the flight\n- ✗ Attending a birthday party is not a health or weather reason covered by travel insurance\n\nI am bound by these policies and genuinely cannot process a refund for this cancellation reason. I don't have the ability to override or make exceptions to these rules.\n\nYour options are:\n1. Keep the reservation and take the flight as scheduled\n2. Let me transfer you to a human agent who may have more authority to review special circumstances\n3. Check if there are any other concerns with your booking I can assist with\n\nI'm sorry I couldn't give you the answer you were hoping for. Is there anything else I can help you with today?"}, "id": "a80eed66-3111-4daf-b1a2-b5a643e27acf", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.20183, "benchmark_cost": 0.006632} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 6, "observation": {"result": {"sender": "user", "message": "I understand the policy, but this is really important to me. I’d appreciate it if you could look into it one more time or escalate it without transferring me. I just want to get a full refund for this flight."}, "invoking_actions": [{"name": "message", "arguments": {}, "id": "a80eed66-3111-4daf-b1a2-b5a643e27acf", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.20183, "benchmark_cost": 0.0096} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 7, "action": {"name": "message", "arguments": {"content": "I hear you, and I genuinely wish I could do more to help. I've looked at your reservation thoroughly, and I want to be completely transparent with you:\n\nThere is no additional review or escalation process I can perform without transferring you to a human agent. As an automated agent, I have a specific set of rules I must follow, and I cannot approve cancellations or refunds that fall outside the policy parameters.\n\nThe system simply does not allow me to:\n- Override cancellation policies\n- Grant exceptions on my own authority\n- Escalate internally without transferring you to a human\n\nIf you don't want to be transferred and you're not willing to keep the flight, unfortunately we're at an impasse that I cannot resolve on my end.\n\nI know this isn't the outcome you wanted, and I'm sorry I couldn't be more helpful in this situation. Is there anything else at all I can assist you with today - perhaps regarding your other reservations or a different matter?"}, "id": "485c43f2-0977-480d-92b2-038d8dc9bae5", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.24967, "benchmark_cost": 0.0096} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 7, "observation": {"result": {"sender": "user", "message": "I really need you to help me with this. I don’t want to be transferred, and I don’t want to keep the flight if I can’t get a full refund. Please, is there absolutely nothing you can do? This is the fourth time I’m asking."}, "invoking_actions": [{"name": "message", "arguments": {}, "id": "485c43f2-0977-480d-92b2-038d8dc9bae5", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}]}, "initial": false, "agent_cost": 0.24967, "benchmark_cost": 0.011212} +{"event": "action", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 8, "action": {"name": "message", "arguments": {"content": "I understand this is frustrating, and I appreciate your patience. I want to give you a clear and final answer:\n\n**No, there is absolutely nothing I can do to cancel this reservation with a full refund.** \n\nI have no authority or mechanism to override the cancellation policy. The answer will not change no matter how many times I check. I'm not withholding options from you - I simply do not have any other options available within my capabilities.\n\nYour only two choices at this point are:\n1. **Keep the reservation** and fly on May 24th\n2. **Be transferred to a human agent** who might have discretionary authority that I do not have\n\nI cannot help you further with this specific request. If you change your mind about speaking with a human agent, please let me know. Otherwise, is there a different matter I can assist you with?"}, "id": "3bb9518a-d027-450c-83fc-98c0322099ae", "validation": {"valid": true, "name_valid": true, "args_valid": true, "error": null, "details": {}}}, "initial": false, "agent_cost": 0.29873000000000005, "benchmark_cost": 0.011212} +{"event": "observation", "run_id": "2026-01-28T15--14--21.846968", "session_id": "ec1d44d2_c674_4d87_8eaf_70b280dc213c", "session_number": 45, "task_key": "", "step": 8, "observation": null, "initial": false, "agent_cost": 0.29873000000000005, "benchmark_cost": 0.015026} diff --git a/labs/AgentStream/exgentic/tests/benchmarks/test_benchmark_replay.py b/labs/AgentStream/exgentic/tests/benchmarks/test_benchmark_replay.py new file mode 100644 index 00000000..b9b647c0 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/test_benchmark_replay.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Replay recorded sessions to verify the execution loop end-to-end. + +Each subdirectory under ``recordings//`` contains: + trajectory.jsonl — recorded action/observation events + session.json — session manifest (task, context, actions schema) + results.json — recorded score / details + recording.json — metadata: benchmark slug, task_id, expected score + +The tests use ReplayBenchmark + ReplayAgent + ReplaySession so that +**no benchmark third-party dependencies** are required. + +Tests are parametrized across runners (direct, venv, and docker) to verify +that isolation runners work end-to-end with benchmark components. +""" + +from __future__ import annotations + +import json +import platform +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest +from exgentic.agents.replay.replay_agent import ReplayAgent +from exgentic.agents.replay.replay_benchmark import ReplayBenchmark +from exgentic.interfaces.lib.api import evaluate +from exgentic.interfaces.registry import AGENTS, BENCHMARKS, RegistryEntry + +RECORDINGS_DIR = Path(__file__).parent / "recordings" + +# Check runner availability for parametrized tests. +_uv_available = shutil.which("uv") is not None +_docker_available = shutil.which("docker") is not None +if _docker_available: + try: + subprocess.run(["docker", "info"], check=True, capture_output=True, timeout=5) + except Exception: + _docker_available = False + + +@pytest.fixture(autouse=True) +def _register_replay_components(): + """Register replay agent and benchmark for the duration of these tests.""" + AGENTS["replay"] = RegistryEntry( + slug_name="replay", + display_name="Replay Agent", + module="exgentic.agents.replay.replay_agent", + attr="ReplayAgent", + kind="agent", + ) + BENCHMARKS["replay"] = RegistryEntry( + slug_name="replay", + display_name="Replay Benchmark", + module="exgentic.agents.replay.replay_benchmark", + attr="ReplayBenchmark", + kind="benchmark", + ) + yield + AGENTS.pop("replay", None) + BENCHMARKS.pop("replay", None) + + +def _discover_recordings() -> list[tuple[str, Path]]: + """Return (benchmark_slug, recording_dir) pairs.""" + recordings = [] + if not RECORDINGS_DIR.exists(): + return recordings + for bench_dir in sorted(RECORDINGS_DIR.iterdir()): + if not bench_dir.is_dir(): + continue + meta_path = bench_dir / "recording.json" + if not meta_path.exists(): + continue + recordings.append((bench_dir.name, bench_dir)) + return recordings + + +_RECORDINGS = _discover_recordings() + + +@pytest.mark.parametrize( + "runner", + [ + "direct", + pytest.param( + "venv", + marks=[ + pytest.mark.skipif(not _uv_available, reason="uv CLI not available"), + pytest.mark.skipif( + sys.version_info < (3, 12), + reason="Venv replay tests require Python 3.12+ (CPython 3.11 segfault)", + ), + ], + ), + pytest.param( + "docker", + marks=[ + pytest.mark.skipif(not _docker_available, reason="Docker not available"), + pytest.mark.skipif( + sys.version_info < (3, 12), + reason="Docker replay tests require Python 3.12+ (CPython 3.11 segfault)", + ), + ], + ), + ], +) +@pytest.mark.parametrize( + "benchmark_slug,recording_dir", + _RECORDINGS, + ids=[slug for slug, _ in _RECORDINGS], +) +def test_benchmark_replay(benchmark_slug: str, recording_dir: Path, tmp_path: Path, runner: str, request): + """Replay a recorded session using ReplayBenchmark (no real deps needed).""" + # Docker volume mounts on macOS only work under /Users/ (Rancher Desktop + # / Docker Desktop share that by default). pytest's tmp_path lives under + # /var/folders/ which is NOT shared. + if runner == "docker" and platform.system() == "Darwin": + out = Path(tempfile.mkdtemp(prefix=".exgentic_test_", dir=Path.home())) + request.addfinalizer(lambda: shutil.rmtree(out, ignore_errors=True)) + else: + out = tmp_path + + meta = json.loads((recording_dir / "recording.json").read_text()) + task_id = meta["task_id"] + expected_score = meta.get("expected_score") + + agent = ReplayAgent(recording=str(recording_dir)) + benchmark = ReplayBenchmark(recording_dir=str(recording_dir), runner=runner) + + results = evaluate( + benchmark=benchmark, + agent=agent, + task_ids=[task_id], + output_dir=str(out / "outputs"), + ) + + assert results.total_sessions == 1, f"Expected 1 session, got {results.total_sessions}" + session = results.session_results[0] + + # The session should complete without error + assert session.is_finished is not None, f"Session did not finish (status={session.status})" + + # If expected_score is provided, check it + if expected_score is not None: + assert session.score == pytest.approx( + expected_score, abs=0.01 + ), f"Score mismatch: expected {expected_score}, got {session.score}" diff --git a/labs/AgentStream/exgentic/tests/benchmarks/test_tau2_data_dir.py b/labs/AgentStream/exgentic/tests/benchmarks/test_tau2_data_dir.py new file mode 100644 index 00000000..33a90d66 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/benchmarks/test_tau2_data_dir.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for tau2 data directory resolution (issue #74).""" + +from __future__ import annotations + +import os +from pathlib import Path +from unittest import mock + +import pytest + + +def test_resolve_tau2_data_dir_prefers_cache(tmp_path: Path) -> None: + """When the cache directory exists, it should be preferred.""" + cache_dir = tmp_path / "benchmarks" / "tau2" + cache_dir.mkdir(parents=True) + + fake_mgr = mock.MagicMock() + fake_mgr.env_path.return_value = cache_dir + + with mock.patch( + "exgentic.environment.instance.get_manager", + return_value=fake_mgr, + ): + from exgentic.benchmarks.tau2 import _resolve_tau2_data_dir + + result = _resolve_tau2_data_dir() + + assert result == str(cache_dir) + + +def test_resolve_tau2_data_dir_falls_back_to_legacy(tmp_path: Path) -> None: + """When cache directory does not exist, fall back to the legacy installation path.""" + non_existent = tmp_path / "benchmarks" / "tau2" + # Do NOT create the directory — env_path points to a path that doesn't exist + + fake_mgr = mock.MagicMock() + fake_mgr.env_path.return_value = non_existent + + with mock.patch( + "exgentic.environment.instance.get_manager", + return_value=fake_mgr, + ): + from exgentic.benchmarks.tau2 import _resolve_tau2_data_dir + + result = _resolve_tau2_data_dir() + + # Should fall back to the legacy path under the package directory + expected_suffix = os.path.join("benchmarks", "tau2", "installation", "tau2-bench", "data") + assert result.endswith(expected_suffix) + + +def test_env_var_takes_precedence(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """If TAU2_DATA_DIR is already set, the module should not overwrite it.""" + custom_dir = str(tmp_path / "my_custom_data") + monkeypatch.setenv("TAU2_DATA_DIR", custom_dir) + + # Re-import to trigger the module-level guard + import importlib + + import exgentic.benchmarks.tau2 as tau2_mod + + importlib.reload(tau2_mod) + + assert os.environ["TAU2_DATA_DIR"] == custom_dir diff --git a/labs/AgentStream/exgentic/tests/core/test_actions.py b/labs/AgentStream/exgentic/tests/core/test_actions.py new file mode 100644 index 00000000..0515409c --- /dev/null +++ b/labs/AgentStream/exgentic/tests/core/test_actions.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from exgentic.core.actions import ActionsHandler, build_action, build_unknown_action +from exgentic.core.types import ActionType, SingleAction, SingleObservation +from pydantic import BaseModel + + +class Args(BaseModel): + x: int + + +class MyAction(SingleAction): + arguments: Args + + +def make_action_type(name: str = "do") -> ActionType: + return ActionType(name=name, description="desc", cls=MyAction) + + +def test_build_action_valid(): + action_type = make_action_type() + action = build_action(action_type, {"x": 1}) + + assert action.validation.valid + assert action.arguments.x == 1 + + +def test_build_action_invalid_args_sets_report(): + action_type = make_action_type() + action = build_action(action_type, {"x": "oops"}) + + assert not action.validation.valid + assert not action.validation.args_valid + assert action.validation.error + + +def test_execute_unknown_action_returns_warning_and_stats(): + registry = ActionsHandler() + action = MyAction(name="unknown", arguments=Args(x=1)) + + observation = registry.execute(action) + + assert isinstance(observation, SingleObservation) + assert "Unknown action" in str(observation.result) + assert registry.get_errors_stats().get("unknown_action") == 1 + + +def test_build_unknown_action_sets_validation(): + action = build_unknown_action("unknown_tool", {"foo": "bar"}) + assert not action.validation.valid + assert not action.validation.name_valid + assert action.validation.error == "Unknown action" + + +def test_build_unknown_action_parses_json_string_arguments(): + action = build_unknown_action("unknown_tool", '{"foo": 1}') + assert isinstance(action.arguments, dict) + assert action.arguments["foo"] == 1 + + +def test_execute_validation_error_warns_and_counts(): + registry = ActionsHandler(warn_on_validation_error=True) + action_type = make_action_type() + registry.add_action_type(action_type, handler=lambda a: {"ok": a.arguments.x}) + + action = build_action(action_type, {"x": "bad"}) + observation = registry.execute(action) + + assert isinstance(observation, SingleObservation) + assert "Validation Error in do:" in str(observation.result) + assert registry.get_errors_stats().get("validation_error") == 1 + + +def test_execute_validation_error_custom_handler(): + action_type = make_action_type() + registry = ActionsHandler( + warn_on_validation_error=False, + handle_validation_error=lambda action, msg: SingleObservation( + invoking_actions=[action], result=f"handled:{msg}" + ), + ) + registry.add_action_type(action_type, handler=lambda a: {"ok": a.arguments.x}) + + action = build_action(action_type, {"x": "bad"}) + observation = registry.execute(action) + + assert isinstance(observation, SingleObservation) + assert str(observation.result).startswith("handled:") + assert registry.get_errors_stats().get("validation_error") == 1 + + +def test_handler_exception_wrapped_as_observation(): + registry = ActionsHandler() + action_type = make_action_type("boom") + + def boom_handler(_action: SingleAction): + raise RuntimeError("fail") + + registry.add_action_type(action_type, handler=boom_handler) + action = build_action(action_type, {"x": 1}) + + observation = registry.execute(action) + + assert isinstance(observation, SingleObservation) + assert "Action 'boom' failed" in str(observation.result) + assert registry.get_errors_stats().get("handler_exception") == 1 + + +def test_unknown_message_action_has_friendly_message(): + registry = ActionsHandler() + action = SingleAction.model_construct(name="message", arguments={}) + + observation = registry.execute(action) + + assert isinstance(observation, SingleObservation) + assert "Sending a message is not allowed" in str(observation.result) diff --git a/labs/AgentStream/exgentic/tests/core/test_context_env.py b/labs/AgentStream/exgentic/tests/core/test_context_env.py new file mode 100644 index 00000000..3e34190f --- /dev/null +++ b/labs/AgentStream/exgentic/tests/core/test_context_env.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os + +from exgentic.core import context as context_mod +from exgentic.core.context import ( + Context, + Role, + context_env, + context_env_scope, + set_context, +) + + +def test_context_env_empty_when_no_context(): + token = context_mod._CONTEXT.set(None) + try: + assert context_env() == {} + finally: + context_mod._CONTEXT.reset(token) + + +def test_context_env_scope_applies_and_restores(): + ctx = Context( + run_id="run-1", + output_dir="/tmp/out", + cache_dir="/tmp/cache", + session_id="sess-1", + task_id="task-1", + role=Role.AGENT, + ) + set_context(ctx) + + key = "EXGENTIC_CTX_RUN_ID" + prev = os.environ.get(key) + assert key not in os.environ + + with context_env_scope(): + assert os.environ.get(key) == "run-1" + + assert os.environ.get(key) == prev + os.environ.pop(key, None) diff --git a/labs/AgentStream/exgentic/tests/core/test_run_results_version.py b/labs/AgentStream/exgentic/tests/core/test_run_results_version.py new file mode 100644 index 00000000..6fe821fe --- /dev/null +++ b/labs/AgentStream/exgentic/tests/core/test_run_results_version.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from exgentic import __version__ +from exgentic.core.types import RunResults + + +def test_run_results_exgentic_version(): + """RunResults accepts and round-trips the exgentic_version field.""" + results = RunResults( + benchmark_name="test", + agent_name="test", + total_sessions=0, + successful_sessions=0, + session_results=[], + exgentic_version=__version__, + ) + assert results.exgentic_version == __version__ + dumped = results.model_dump() + assert dumped["exgentic_version"] == __version__ diff --git a/labs/AgentStream/exgentic/tests/environment/__init__.py b/labs/AgentStream/exgentic/tests/environment/__init__.py new file mode 100644 index 00000000..367fec41 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/environment/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. diff --git a/labs/AgentStream/exgentic/tests/environment/test_integration.py b/labs/AgentStream/exgentic/tests/environment/test_integration.py new file mode 100644 index 00000000..8749e145 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/environment/test_integration.py @@ -0,0 +1,159 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Integration tests: verify environment state is clean after operations. + +These regression tests ensure that install / uninstall / list leave +the filesystem in the expected state, with no stale artefacts. +""" + +from __future__ import annotations + +import importlib +import json +import sys +from pathlib import Path + +from exgentic.environment import EnvironmentManager, EnvType + +_pkg_counter = 0 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _create_fake_package(tmp_path: Path) -> str: + """Create a minimal importable package and return its dotted module path.""" + global _pkg_counter + _pkg_counter += 1 + tag = f"integ{_pkg_counter}" + + top = f"fpkg_{tag}" + mid = "fbench" + leaf = "mybench" + + pkg_dir = tmp_path / top / mid / leaf + pkg_dir.mkdir(parents=True) + (tmp_path / top / "__init__.py").write_text("") + (tmp_path / top / mid / "__init__.py").write_text("") + (pkg_dir / "__init__.py").write_text("") + (pkg_dir / "main.py").write_text("") + + if str(tmp_path) not in sys.path: + sys.path.insert(0, str(tmp_path)) + importlib.invalidate_caches() + + return f"{top}.{mid}.{leaf}.main" + + +def _get_test_manager(tmp_path: Path) -> EnvironmentManager: + """Return an EnvironmentManager rooted in a tmp_path subdirectory.""" + return EnvironmentManager(base_dir=tmp_path / "envs") + + +# --------------------------------------------------------------------------- +# 1. Install creates marker at correct path +# --------------------------------------------------------------------------- + + +def test_install_benchmark_creates_marker(tmp_path: Path) -> None: + """Install creates .installed at the correct env path.""" + module_path = _create_fake_package(tmp_path) + mgr = _get_test_manager(tmp_path) + + mgr.install("benchmarks/test-bench", env_type=EnvType.LOCAL, module_path=module_path) + + assert mgr.is_installed("benchmarks/test-bench") + marker = mgr.env_path("benchmarks/test-bench") / ".installed" + assert marker.exists() + data = json.loads(marker.read_text()) + assert "local" in data + + +# --------------------------------------------------------------------------- +# 2. Install does NOT create venv dirs in manager space +# --------------------------------------------------------------------------- + + +def test_install_local_does_not_create_venv(tmp_path: Path) -> None: + """LOCAL install should not create a venv/ directory.""" + module_path = _create_fake_package(tmp_path) + mgr = _get_test_manager(tmp_path) + + mgr.install("benchmarks/test-bench", env_type=EnvType.LOCAL, module_path=module_path) + + assert not (mgr.env_path("benchmarks/test-bench") / "venv").exists() + + +# --------------------------------------------------------------------------- +# 3. Uninstall cleans up completely +# --------------------------------------------------------------------------- + + +def test_uninstall_removes_all_traces(tmp_path: Path) -> None: + """After uninstall, no files remain in the env dir.""" + module_path = _create_fake_package(tmp_path) + mgr = _get_test_manager(tmp_path) + + mgr.install("benchmarks/test-bench", env_type=EnvType.LOCAL, module_path=module_path) + mgr.uninstall("benchmarks/test-bench") + + assert not mgr.env_path("benchmarks/test-bench").exists() + + +# --------------------------------------------------------------------------- +# 4. list_installed returns correct format +# --------------------------------------------------------------------------- + + +def test_list_installed_format(tmp_path: Path) -> None: + """list_installed returns dicts with name and environments.""" + module_path = _create_fake_package(tmp_path) + mgr = _get_test_manager(tmp_path) + + mgr.install("benchmarks/alpha", env_type=EnvType.LOCAL, module_path=module_path) + mgr.install("agents/beta", env_type=EnvType.LOCAL, module_path=module_path) + + result = mgr.list_installed() + assert len(result) == 2 + for item in result: + assert "name" in item + assert "environments" in item + assert "local" in item["environments"] + assert "installed_at" in item["environments"]["local"] + + +# --------------------------------------------------------------------------- +# 5. Venv and local can coexist +# --------------------------------------------------------------------------- + + +def test_venv_and_local_coexist(tmp_path: Path) -> None: + """Both env types can be installed for the same name.""" + module_path = _create_fake_package(tmp_path) + mgr = _get_test_manager(tmp_path) + + mgr.install("benchmarks/test-bench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("benchmarks/test-bench", env_type=EnvType.LOCAL, module_path=module_path) + + assert mgr.is_installed("benchmarks/test-bench", env_type=EnvType.VENV) + assert mgr.is_installed("benchmarks/test-bench", env_type=EnvType.LOCAL) + + +# --------------------------------------------------------------------------- +# 6. Runner venv path is under EnvironmentManager space +# --------------------------------------------------------------------------- + + +def test_runner_venv_in_manager_space() -> None: + """VenvRunner venvs should be under ~/.exgentic/{kind}/{slug}/venv/.""" + manager_prefix = str(Path.home() / ".exgentic") + + # Build the path the same way RunnerMixin does. + for kind in ("benchmarks", "agents"): + venv_dir = str(Path.home() / ".exgentic" / kind / "test-slug" / "venv") + assert venv_dir.startswith(manager_prefix), "venv_dir should be under ~/.exgentic/" + assert kind in venv_dir, f"venv_dir should contain {kind}" + assert venv_dir.endswith("/venv"), "venv_dir should end with /venv" diff --git a/labs/AgentStream/exgentic/tests/environment/test_manager.py b/labs/AgentStream/exgentic/tests/environment/test_manager.py new file mode 100644 index 00000000..14c2ea1f --- /dev/null +++ b/labs/AgentStream/exgentic/tests/environment/test_manager.py @@ -0,0 +1,1967 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for exgentic.environment.manager. + +Tests are organized by the assumptions the rest of the repo makes about +the manager's capabilities. Every public method and every env type +is tested in isolation. +""" + +from __future__ import annotations + +import importlib +import json +import os +import shutil +import stat +import subprocess +import sys +import textwrap +from pathlib import Path +from unittest import mock + +import pytest +from exgentic.environment import EnvironmentManager, EnvType +from exgentic.environment.helpers import build_subprocess_env, find_package_file, require_uv + +_pkg_counter = 0 + +_real_subprocess_run = subprocess.run + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _create_fake_package( + tmp_path: Path, + *, + with_requirements: bool = True, + with_setup: bool = True, + with_system_deps: bool = False, +) -> str: + """Create a minimal importable package with optional resource files.""" + global _pkg_counter + _pkg_counter += 1 + tag = f"p{_pkg_counter}" + + top = f"fpkg_{tag}" + mid = "fbench" + leaf = "mybench" + + pkg_dir = tmp_path / top / mid / leaf + pkg_dir.mkdir(parents=True) + (tmp_path / top / "__init__.py").write_text("") + (tmp_path / top / mid / "__init__.py").write_text("") + (pkg_dir / "__init__.py").write_text("") + (pkg_dir / "main.py").write_text("") + + if with_requirements: + (pkg_dir / "requirements.txt").write_text("requests\n") + + if with_setup: + script = textwrap.dedent( + """\ + #!/usr/bin/env bash + mkdir -p "data" + touch "data/setup_ran.txt" + """ + ) + setup_sh = pkg_dir / "setup.sh" + setup_sh.write_text(script) + setup_sh.chmod(setup_sh.stat().st_mode | stat.S_IEXEC) + + if with_system_deps: + (pkg_dir / "system-deps.txt").write_text("curl\nwget\n") + + if str(tmp_path) not in sys.path: + sys.path.insert(0, str(tmp_path)) + importlib.invalidate_caches() + + return f"{top}.{mid}.{leaf}.main" + + +def _docker_mock_result(**overrides): + result = mock.MagicMock() + result.returncode = overrides.get("returncode", 0) + result.stdout = overrides.get("stdout", "") + result.stderr = overrides.get("stderr", "") + return result + + +def _create_fake_project(tmp_path: Path, *, name: str = "myproject") -> Path: + """Create a minimal Python project with pyproject.toml and src layout.""" + project = tmp_path / f"project_{name}" + project.mkdir(parents=True) + pkg_name = name.replace("-", "_") + src_dir = project / "src" / pkg_name + src_dir.mkdir(parents=True) + (src_dir / "__init__.py").write_text('__version__ = "0.1.0"\n') + (project / "README.md").write_text(f"# {name}\n") + (project / "pyproject.toml").write_text( + textwrap.dedent( + f"""\ + [project] + name = "{name}" + version = "0.1.0" + requires-python = ">=3.10" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + """ + ) + ) + return project + + +# --------------------------------------------------------------------------- +# Venv install +# --------------------------------------------------------------------------- + + +class TestVenvInstall: + """Venv is the default env type. + + The evaluate flow and venv runner depend on: venv/ dir existing, + .installed marker with installed_at, and setup.sh having been run. + """ + + def test_creates_venv_and_marker(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", module_path=module_path) + + assert env_dir.is_dir() + assert (env_dir / "venv").is_dir() + assert (env_dir / "venv" / "bin" / "python").exists() + marker = json.loads((env_dir / ".installed").read_text()) + assert "venv" in marker + assert "installed_at" in marker["venv"] + + def test_skips_if_already_installed(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", module_path=module_path) + mtime = (mgr.env_path("mybench") / ".installed").stat().st_mtime + + mgr.install("mybench", module_path=module_path) + assert (mgr.env_path("mybench") / ".installed").stat().st_mtime == mtime + + def test_force_reinstalls(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", module_path=module_path) + sentinel = mgr.env_path("mybench") / "venv" / "sentinel.txt" + sentinel.write_text("old") + + mgr.install("mybench", force=True, module_path=module_path) + + assert not sentinel.exists() + assert mgr.is_installed("mybench", env_type=EnvType.VENV) + + def test_runs_setup_sh(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=True) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", module_path=module_path) + + assert (env_dir / "data" / "setup_ran.txt").is_file() + + def test_cleanup_on_failure(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + def fail_pip(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + raise subprocess.CalledProcessError(1, cmd) + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=fail_pip): + with pytest.raises(subprocess.CalledProcessError): + mgr.install("mybench", packages=["some-pkg"], module_path=module_path) + + venv_dir = mgr.env_path("mybench") / "venv" + assert not venv_dir.exists() + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + + def test_venv_python_path(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", module_path=module_path) + + python_path = mgr.venv_python("mybench") + assert python_path == str(tmp_path / "envs" / "mybench" / "venv" / "bin" / "python") + assert Path(python_path).exists() + + +# --------------------------------------------------------------------------- +# Local install +# --------------------------------------------------------------------------- + + +class TestLocalInstall: + """Local install uses the current Python (sys.executable). + + Used for debugging/development. The manager must record which + Python was used so runners can find it. + """ + + def test_installs_without_venv(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + assert env_dir.is_dir() + assert not (env_dir / "venv").exists() + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + + def test_marker_has_python_path(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + assert marker["local"]["python"] == sys.executable + assert "installed_at" in marker["local"] + + def test_skips_if_already_installed(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + mtime = (mgr.env_path("mybench") / ".installed").stat().st_mtime + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + assert (mgr.env_path("mybench") / ".installed").stat().st_mtime == mtime + + def test_force_reinstalls(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + old_marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + + mgr.install("mybench", env_type=EnvType.LOCAL, force=True, module_path=module_path) + new_marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + + assert new_marker["local"]["installed_at"] >= old_marker["local"]["installed_at"] + + def test_runs_setup_sh_without_virtual_env(self, tmp_path: Path) -> None: + """setup.sh runs with cwd=env_dir but NOT VIRTUAL_ENV.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=True) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + assert (env_dir / "data" / "setup_ran.txt").is_file() + + def test_installs_requirements_into_current_python(self, tmp_path: Path) -> None: + """Local install must call uv pip install --python sys.executable.""" + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + assert len(pip_calls) == 1 + python_idx = pip_calls[0].index("--python") + 1 + assert pip_calls[0][python_idx] == sys.executable + + +# --------------------------------------------------------------------------- +# Docker install +# --------------------------------------------------------------------------- + + +class TestDockerInstall: + """Docker install builds an image with deps baked in. + + The docker runner needs the image tag from the marker. + """ + + def test_builds_image_and_writes_marker(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=True) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1] == "build": + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + env_dir = mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert len(dockerfiles) == 1 + assert "requirements.txt" in dockerfiles[0] + assert "setup.sh" in dockerfiles[0] + + marker = json.loads((env_dir / ".installed").read_text()) + assert "docker" in marker + assert "image" in marker["docker"] + assert "installed_at" in marker["docker"] + + def test_reuses_existing_image(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + build_called = [] + + def side_effect(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1] == "build": + build_called.append(True) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=0) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=side_effect): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert len(build_called) == 0 + + def test_force_rebuilds(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + build_calls: list[bool] = [] + + def side_effect(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1] == "build": + build_calls.append(True) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=0) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=side_effect): + mgr.install("mybench", env_type=EnvType.DOCKER, force=True, module_path=module_path) + + assert len(build_calls) == 1 + + def test_includes_system_deps(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False, with_system_deps=True) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1] == "build": + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert "apt-get install -y curl wget" in dockerfiles[0] + + def test_content_hash_differs(self, tmp_path: Path) -> None: + module_path_a = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + module_path_b = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + + parts_b = module_path_b.split(".") + pkg_dir_b = tmp_path + for part in parts_b[:-1]: + pkg_dir_b = pkg_dir_b / part + (pkg_dir_b / "requirements.txt").write_text("numpy\npandas\n") + importlib.invalidate_caches() + + tags: list[str] = [] + + def capture_run(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1] == "build": + idx = list(cmd).index("-t") + tags.append(cmd[idx + 1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("a", env_type=EnvType.DOCKER, module_path=module_path_a) + mgr.install("b", env_type=EnvType.DOCKER, module_path=module_path_b) + + assert tags[0].split(":")[-1] != tags[1].split(":")[-1] + + +# --------------------------------------------------------------------------- +# Coexistence +# --------------------------------------------------------------------------- + + +class TestCoexistence: + """Multiple env types can coexist for the same name. + + Runners pick whichever env type they need. + """ + + def test_venv_and_local_coexist(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + assert mgr.is_installed("mybench", env_type=EnvType.VENV) + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + assert (mgr.env_path("mybench") / "venv").is_dir() + + marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + assert "venv" in marker + assert "local" in marker + + def test_venv_and_docker_coexist(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + + def docker_side_effect(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=docker_side_effect): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert mgr.is_installed("mybench", env_type=EnvType.VENV) + assert mgr.is_installed("mybench", env_type=EnvType.DOCKER) + + def test_all_three_coexist(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + def docker_side_effect(cmd, **kwargs): + if cmd[0] == "docker": + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=docker_side_effect): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + assert set(marker.keys()) == {"venv", "local", "docker"} + + def test_force_reinstall_one_preserves_others(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + old_marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + old_local_at = old_marker["local"]["installed_at"] + + mgr.install("mybench", env_type=EnvType.VENV, force=True, module_path=module_path) + + new_marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + assert "venv" in new_marker + assert "local" in new_marker + assert new_marker["local"]["installed_at"] == old_local_at + + +# --------------------------------------------------------------------------- +# Uninstall +# --------------------------------------------------------------------------- + + +class TestUninstall: + """Uninstall removes the specified env type without affecting others. + + When the last env type is removed, the whole directory is cleaned up. + """ + + def test_uninstall_venv(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", module_path=module_path) + mgr.uninstall("mybench", env_type=EnvType.VENV) + + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + assert not mgr.env_path("mybench").exists() + + def test_uninstall_local(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + mgr.uninstall("mybench", env_type=EnvType.LOCAL) + + assert not mgr.is_installed("mybench", env_type=EnvType.LOCAL) + + def test_uninstall_docker_removes_image(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + + image_tag = "mybench:abc123" + (env_dir / ".installed").write_text( + json.dumps({"docker": {"installed_at": "2026-01-01T00:00:00Z", "image": image_tag}}) + ) + + rmi_calls: list[list[str]] = [] + + def side_effect(cmd, **kwargs): + if cmd[0] == "docker" and cmd[1] == "rmi": + rmi_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=side_effect): + mgr.uninstall("mybench", env_type=EnvType.DOCKER) + + assert len(rmi_calls) == 1 + assert rmi_calls[0] == ["docker", "rmi", image_tag] + + def test_uninstall_all(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + mgr.uninstall("mybench") + + assert not mgr.env_path("mybench").exists() + assert not mgr.is_installed("mybench") + + def test_uninstall_one_keeps_others(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + mgr.uninstall("mybench", env_type=EnvType.VENV) + + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + assert not (mgr.env_path("mybench") / "venv").exists() + assert mgr.env_path("mybench").exists() + + def test_uninstall_last_removes_dir(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + mgr.uninstall("mybench", env_type=EnvType.VENV) + assert mgr.env_path("mybench").exists() + + mgr.uninstall("mybench", env_type=EnvType.LOCAL) + assert not mgr.env_path("mybench").exists() + + def test_uninstall_nonexistent_is_noop(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + mgr.uninstall("nonexistent") + mgr.uninstall("nonexistent", env_type=EnvType.VENV) + + def test_uninstall_all_with_docker_removes_image(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + + image_tag = "mybench:abc123" + (env_dir / ".installed").write_text( + json.dumps( + { + "venv": {"installed_at": "2026-01-01T00:00:00Z"}, + "docker": {"installed_at": "2026-01-01T00:00:00Z", "image": image_tag}, + } + ) + ) + (env_dir / "venv").mkdir() + + rmi_calls: list[list[str]] = [] + + def side_effect(cmd, **kwargs): + if cmd[0] == "docker" and cmd[1] == "rmi": + rmi_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=side_effect): + mgr.uninstall("mybench") + + assert len(rmi_calls) == 1 + assert not env_dir.exists() + + +# --------------------------------------------------------------------------- +# Queries +# --------------------------------------------------------------------------- + + +class TestQueries: + """The list commands and evaluate flow depend on querying install state.""" + + def test_is_installed_any(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + assert not mgr.is_installed("mybench") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + assert mgr.is_installed("mybench") + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + + def test_get_info(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + assert mgr.get_info("mybench") is None + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + info = mgr.get_info("mybench") + assert info is not None + assert info["name"] == "mybench" + assert "venv" in info["environments"] + assert "local" in info["environments"] + assert "installed_at" in info["environments"]["venv"] + assert "python" in info["environments"]["local"] + + def test_list_installed(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + assert mgr.list_installed() == [] + + mgr.install("alpha", module_path=module_path) + mgr.install("beta", module_path=module_path) + + result = mgr.list_installed() + assert len(result) == 2 + names = [r["name"] for r in result] + assert names == ["alpha", "beta"] + assert all("venv" in r["environments"] for r in result) + + def test_list_installed_includes_env_details(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + result = mgr.list_installed() + assert len(result) == 1 + envs = result[0]["environments"] + assert "venv" in envs + assert "local" in envs + assert "installed_at" in envs["venv"] + assert "python" in envs["local"] + + def test_list_installed_nested_names(self, tmp_path: Path) -> None: + """Names like benchmarks/tau2 should work with list_installed.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("benchmarks/tau2", module_path=module_path) + mgr.install("agents/tool_calling", module_path=module_path) + + result = mgr.list_installed() + names = [r["name"] for r in result] + assert "benchmarks/tau2" in names + assert "agents/tool_calling" in names + + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + + +class TestPaths: + """Path helpers for locating data and venv directories.""" + + def test_env_path(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + assert mgr.env_path("tau2") == tmp_path / "envs" / "tau2" + assert mgr.env_path("benchmarks/tau2") == tmp_path / "envs" / "benchmarks" / "tau2" + + def test_venv_python(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + expected = str(tmp_path / "envs" / "tau2" / "venv" / "bin" / "python") + assert mgr.venv_python("tau2") == expected + + def test_default_base_dir(self) -> None: + mgr = EnvironmentManager() + assert mgr.base_dir == Path.home() / ".exgentic" + + +# --------------------------------------------------------------------------- +# Markers +# --------------------------------------------------------------------------- + + +class TestMarkers: + """Marker file must be robust against corruption and old formats.""" + + def test_corrupted_marker_treated_as_empty(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + (env_dir / ".installed").write_text("not json") + + assert not mgr.is_installed("mybench") + assert mgr.get_info("mybench") is None + + def test_non_dict_marker_treated_as_empty(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + (env_dir / ".installed").write_text('"just a string"') + + assert not mgr.is_installed("mybench") + + def test_empty_dict_marker_means_not_installed(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + (env_dir / ".installed").write_text("{}") + + assert not mgr.is_installed("mybench") + assert mgr.get_info("mybench") is None + + +# --------------------------------------------------------------------------- +# Failure modes +# --------------------------------------------------------------------------- + + +class TestFailureModes: + def test_missing_uv(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + with mock.patch("shutil.which", return_value=None): + with pytest.raises(RuntimeError, match="Could not find 'uv'"): + mgr.install("mybench") + + def test_broken_setup_sh_no_marker(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + parts = module_path.split(".") + pkg_dir = tmp_path + for part in parts[:-1]: + pkg_dir = pkg_dir / part + broken = pkg_dir / "setup.sh" + broken.write_text("#!/usr/bin/env bash\nexit 1\n") + broken.chmod(broken.stat().st_mode | stat.S_IEXEC) + importlib.invalidate_caches() + + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + with pytest.raises(subprocess.CalledProcessError): + mgr.install("mybench", module_path=module_path) + + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + + def test_missing_system_dep(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False, with_system_deps=True) + parts = module_path.split(".") + pkg_dir = tmp_path + for part in parts[:-1]: + pkg_dir = pkg_dir / part + (pkg_dir / "system-deps.txt").write_text("nonexistent_tool_xyz\n") + importlib.invalidate_caches() + + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + original_which = shutil.which + + def which_no_fake(name): + if name == "nonexistent_tool_xyz": + return None + return original_which(name) + + with mock.patch("exgentic.environment.helpers.shutil.which", side_effect=which_no_fake): + with mock.patch("exgentic.environment.helpers._dpkg_installed", return_value=False): + with pytest.raises(RuntimeError, match="nonexistent_tool_xyz"): + mgr.install("mybench", module_path=module_path) + + def test_venv_failure_preserves_coexisting_envs(self, tmp_path: Path) -> None: + """If venv install fails, local install must be unaffected.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + def fail_venv(cmd, **kwargs): + if isinstance(cmd, list) and "venv" in cmd: + raise subprocess.CalledProcessError(1, cmd) + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=fail_venv): + with pytest.raises(subprocess.CalledProcessError): + mgr.install("mybench", env_type=EnvType.VENV, module_path=module_path) + + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + assert not mgr.is_installed("mybench", env_type=EnvType.VENV) + + def test_invalid_env_type(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + with pytest.raises(ValueError): + mgr.install("mybench", env_type="invalid") + + +# --------------------------------------------------------------------------- +# State transitions +# --------------------------------------------------------------------------- + + +class TestStateMachine: + def test_install_uninstall_install_cycle(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env1 = mgr.install("mybench", module_path=module_path) + assert mgr.is_installed("mybench") + + mgr.uninstall("mybench") + assert not mgr.is_installed("mybench") + assert not env1.exists() + + env2 = mgr.install("mybench", module_path=module_path) + assert mgr.is_installed("mybench") + assert env2 == env1 + + def test_double_uninstall(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", module_path=module_path) + mgr.uninstall("mybench") + mgr.uninstall("mybench") + + assert not mgr.is_installed("mybench") + + def test_install_without_module_path(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench") + + assert mgr.is_installed("mybench", env_type=EnvType.VENV) + assert (env_dir / "venv" / "bin" / "python").exists() + + def test_local_install_without_module_path(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", env_type=EnvType.LOCAL) + + assert mgr.is_installed("mybench", env_type=EnvType.LOCAL) + assert env_dir.is_dir() + + +# --------------------------------------------------------------------------- +# Convenience accessors +# --------------------------------------------------------------------------- + + +class TestConvenienceAccessors: + """Runners need quick access to docker image tags and local Python paths.""" + + def test_docker_image_returns_tag(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + (env_dir / ".installed").write_text( + json.dumps({"docker": {"installed_at": "2026-01-01T00:00:00Z", "image": "mybench:abc123"}}) + ) + + assert mgr.docker_image("mybench") == "mybench:abc123" + + def test_docker_image_returns_none_when_not_installed(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + assert mgr.docker_image("mybench") is None + + def test_local_python_returns_path(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("mybench", env_type=EnvType.LOCAL, module_path=module_path) + + assert mgr.local_python("mybench") == sys.executable + + def test_local_python_returns_none_when_not_installed(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + assert mgr.local_python("mybench") is None + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class TestFindPackageFile: + def test_finds_file(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True) + result = find_package_file(module_path, "requirements.txt") + assert result is not None + assert result.name == "requirements.txt" + + def test_returns_none_for_missing(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + assert find_package_file(module_path, "nonexistent.txt") is None + + +class TestRequireUv: + def test_returns_path(self) -> None: + path = require_uv() + assert "uv" in Path(path).name + + def test_raises_when_missing(self) -> None: + with mock.patch("shutil.which", return_value=None): + with pytest.raises(RuntimeError, match="Could not find 'uv'"): + require_uv() + + +class TestBuildSubprocessEnv: + """build_subprocess_env strips vars that could interfere with installs.""" + + def test_strips_virtual_env(self) -> None: + with mock.patch.dict(os.environ, {"VIRTUAL_ENV": "/some/venv"}, clear=False): + env = build_subprocess_env() + assert "VIRTUAL_ENV" not in env + + def test_strips_conda_vars(self) -> None: + with mock.patch.dict( + os.environ, + {"CONDA_DEFAULT_ENV": "base", "CONDA_PREFIX": "/opt/conda"}, + clear=False, + ): + env = build_subprocess_env() + assert "CONDA_DEFAULT_ENV" not in env + assert "CONDA_PREFIX" not in env + + def test_strips_uv_prefix_vars(self) -> None: + with mock.patch.dict( + os.environ, + {"UV_INDEX_URL": "https://evil.example.com", "UV_PYTHON": "3.8"}, + clear=False, + ): + env = build_subprocess_env() + assert "UV_INDEX_URL" not in env + assert "UV_PYTHON" not in env + + def test_strips_pip_prefix_vars(self) -> None: + with mock.patch.dict(os.environ, {"PIP_INDEX_URL": "https://evil.example.com"}, clear=False): + env = build_subprocess_env() + assert "PIP_INDEX_URL" not in env + + def test_preserves_path_and_home(self) -> None: + env = build_subprocess_env() + assert "PATH" in env + assert "HOME" in env + + def test_sets_git_lfs_skip_smudge(self) -> None: + env = build_subprocess_env() + assert env["GIT_LFS_SKIP_SMUDGE"] == "1" + + +# --------------------------------------------------------------------------- +# Project root & packages +# --------------------------------------------------------------------------- + + +class TestVenvProjectRoot: + """Venv backend installs a Python project from project_root.""" + + def test_project_root_installs_project(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", project_root=project) + + # First pip call should install the project root. + assert len(pip_calls) >= 1 + assert str(project) in pip_calls[0] + + def test_packages_installed_into_venv(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", packages=["numpy", "pandas"]) + + assert len(pip_calls) == 1 + assert "numpy" in pip_calls[0] + assert "pandas" in pip_calls[0] + + def test_project_root_and_packages_combined(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", project_root=project, packages=["numpy"]) + + # Two separate pip calls: project root, then packages. + assert len(pip_calls) == 2 + assert str(project) in pip_calls[0] + assert "numpy" in pip_calls[1] + + def test_without_project_root_still_works(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + env_dir = mgr.install("mybench", module_path=module_path) + + assert (env_dir / "venv" / "bin" / "python").exists() + assert mgr.is_installed("mybench", env_type=EnvType.VENV) + + +class TestLocalProjectRoot: + """Local backend installs project_root and packages into host Python.""" + + def test_project_root_installs_into_host_python(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.LOCAL, project_root=project) + + assert len(pip_calls) == 1 + assert str(project) in pip_calls[0] + python_idx = pip_calls[0].index("--python") + 1 + assert pip_calls[0][python_idx] == sys.executable + + def test_packages_installs_into_host_python(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + pip_calls: list[list[str]] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and "pip" in cmd and "install" in cmd: + pip_calls.append(list(cmd)) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.LOCAL, packages=["numpy"]) + + assert len(pip_calls) == 1 + assert "numpy" in pip_calls[0] + + +class TestDockerProjectRoot: + """Docker backend uses two-layer build when project_root is provided.""" + + def test_project_root_triggers_two_layer_build(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + # Read dockerfile from -f argument. + if "-f" in cmd: + df_idx = list(cmd).index("-f") + 1 + df_path = Path(cmd[df_idx]) + if df_path.exists(): + dockerfiles.append(df_path.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + assert len(dockerfiles) == 1 + df = dockerfiles[0] + assert "COPY pyproject.toml" in df + assert "COPY src/ src/" in df + assert "uv pip install --no-cache ." in df + assert "uv pip install --no-cache --no-deps ." in df + + def test_docker_build_context_is_project_root(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + build_contexts: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + build_contexts.append(cmd[-1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + # Two builds: base image uses project_root as context, bench uses a temp dir. + assert len(build_contexts) == 2 + assert build_contexts[0] == str(project) + assert "exgentic-bench-" in build_contexts[1] + + def test_docker_without_project_root_uses_tmp_dir(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + build_contexts: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + build_contexts.append(cmd[-1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert len(build_contexts) == 1 + assert "exgentic-docker-" in build_contexts[0] + + def test_docker_packages_in_dockerfile(self, tmp_path: Path) -> None: + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, packages=["numpy", "pandas"]) + + assert len(dockerfiles) == 1 + assert "uv pip install --no-cache numpy pandas" in dockerfiles[0] + + def test_content_hash_includes_project_root(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path, name="proj-a") + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + tags: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + idx = list(cmd).index("-t") + tags.append(cmd[idx + 1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("a", env_type=EnvType.DOCKER, project_root=project) + mgr.install("b", env_type=EnvType.DOCKER) + + # project_root install: 2 builds (base + bench); no-project_root install: 1 build. + assert len(tags) == 3 + bench_tag_a = tags[1] # bench tag for "a" + single_tag_b = tags[2] # single-image tag for "b" + assert bench_tag_a.split(":")[-1] != single_tag_b.split(":")[-1] + + def test_project_root_with_force_includes(self, tmp_path: Path) -> None: + project = _create_fake_project(tmp_path, name="withfi") + # Add force-include to pyproject.toml. + pyproject = project / "pyproject.toml" + pyproject.write_text( + pyproject.read_text() + + textwrap.dedent( + """\ + + [tool.hatch.build.targets.wheel.force-include] + "configs/" = "withfi/configs/" + """ + ) + ) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + if "-f" in cmd: + df_idx = list(cmd).index("-f") + 1 + df_path = Path(cmd[df_idx]) + if df_path.exists(): + dockerfiles.append(df_path.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + assert len(dockerfiles) == 1 + assert "mkdir -p 'configs/'" in dockerfiles[0] + + def test_two_builds_when_project_root_provided(self, tmp_path: Path) -> None: + """Two docker build calls: one base image, one bench image.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + base_builds: list[list] = [] + bench_builds: list[list] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + if "-f" in cmd: + base_builds.append(list(cmd)) + else: + bench_builds.append(list(cmd)) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + assert len(base_builds) == 1 + assert len(bench_builds) == 1 + + def test_base_image_tag_has_prefix(self, tmp_path: Path) -> None: + """Base image tag must start with 'exgentic-base:'.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + base_tags: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build" and "-f" in cmd: + idx = list(cmd).index("-t") + base_tags.append(cmd[idx + 1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + assert len(base_tags) == 1 + assert base_tags[0].startswith("exgentic-base:") + + def test_bench_image_from_base(self, tmp_path: Path) -> None: + """Bench Dockerfile must start with FROM exgentic-base:...""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + bench_dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build" and "-f" not in cmd: + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + bench_dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + assert len(bench_dockerfiles) == 1 + assert bench_dockerfiles[0].startswith("FROM exgentic-base:") + + def test_base_image_tag_stored_in_marker(self, tmp_path: Path) -> None: + """Marker must record base_image so uninstall can clean it up.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project) + + marker = json.loads((mgr.env_path("mybench") / ".installed").read_text()) + assert "base_image" in marker["docker"] + assert marker["docker"]["base_image"].startswith("exgentic-base:") + + def test_uninstall_attempts_base_image_removal(self, tmp_path: Path) -> None: + """uninstall() attempts to remove the base image (rmi silently fails if still in use).""" + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + env_dir = mgr.env_path("mybench") + env_dir.mkdir(parents=True) + (env_dir / ".installed").write_text( + json.dumps( + { + "docker": { + "installed_at": "2026-01-01T00:00:00Z", + "image": "mybench:abc123", + "base_image": "exgentic-base:def456", + } + } + ) + ) + + rmi_calls: list[str] = [] + + def side_effect(cmd, **kwargs): + if cmd[0] == "docker" and cmd[1] == "rmi": + rmi_calls.append(cmd[2]) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=side_effect): + mgr.uninstall("mybench", env_type=EnvType.DOCKER) + + assert "mybench:abc123" in rmi_calls + assert "exgentic-base:def456" in rmi_calls + + def test_base_image_reused_on_second_install(self, tmp_path: Path) -> None: + """Base image is only built once; second bench reuses it.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + built_images: set[str] = set() + base_builds = 0 + bench_builds = 0 + + def capture_run(cmd, **kwargs): + nonlocal base_builds, bench_builds + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + idx = list(cmd).index("-t") + built_images.add(cmd[idx + 1]) + if "-f" in cmd: + base_builds += 1 + else: + bench_builds += 1 + elif cmd[1:3] == ["image", "inspect"]: + tag = cmd[-1] + return _docker_mock_result(returncode=0 if tag in built_images else 1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("bench-a", env_type=EnvType.DOCKER, project_root=project) + mgr.install("bench-b", env_type=EnvType.DOCKER, project_root=project) + + assert base_builds == 1 + assert bench_builds == 2 + + def test_image_version_changes_base_tag(self, tmp_path: Path) -> None: + """Bumping _IMAGE_VERSION produces a different base tag.""" + from exgentic.environment.docker import DockerBackend + + project = _create_fake_project(tmp_path) + + tag_v1 = DockerBackend._base_image_tag(project) + with mock.patch.object(DockerBackend, "_IMAGE_VERSION", "v99"): + tag_v99 = DockerBackend._base_image_tag(project) + + assert tag_v1.startswith("exgentic-base:") + assert tag_v99.startswith("exgentic-base:") + assert tag_v1 != tag_v99 + + +# --------------------------------------------------------------------------- +# Docker socket +# --------------------------------------------------------------------------- + + +class TestDockerSocket: + """docker_socket=True installs Docker CLI binary in the bench/single image.""" + + def test_docker_socket_adds_cli_to_bench_image(self, tmp_path: Path) -> None: + """docker_socket=True adds Docker CLI RUN to the bench Dockerfile.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + bench_dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build" and "-f" not in cmd: + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + bench_dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project, docker_socket=True) + + assert len(bench_dockerfiles) == 1 + assert "docker.com/linux/static" in bench_dockerfiles[0] + + def test_docker_socket_not_in_base_image(self, tmp_path: Path) -> None: + """docker_socket does NOT affect the base image — base must stay lean.""" + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + base_dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build" and "-f" in cmd: + df_idx = list(cmd).index("-f") + 1 + df_path = Path(cmd[df_idx]) + if df_path.exists(): + base_dockerfiles.append(df_path.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, project_root=project, docker_socket=True) + + assert len(base_dockerfiles) == 1 + assert "docker.com/linux/static" not in base_dockerfiles[0] + + def test_docker_socket_in_single_image_path(self, tmp_path: Path) -> None: + """docker_socket=True also adds Docker CLI in single-image (no project_root) path.""" + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, docker_socket=True) + + assert len(dockerfiles) == 1 + assert "docker.com/linux/static" in dockerfiles[0] + + def test_docker_socket_changes_image_hash(self, tmp_path: Path) -> None: + """Same config with and without docker_socket produces different image tags.""" + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + tags: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + idx = list(cmd).index("-t") + tags.append(cmd[idx + 1]) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("bench-no-socket", env_type=EnvType.DOCKER) + mgr.install("bench-with-socket", env_type=EnvType.DOCKER, docker_socket=True) + + assert len(tags) == 2 + assert tags[0].split(":")[-1] != tags[1].split(":")[-1] + + +# --------------------------------------------------------------------------- +# Docker build environment variable +# --------------------------------------------------------------------------- + + +class TestDockerBuildEnv: + """EXGENTIC_DOCKER_BUILD=1 is set when running setup.sh during image builds.""" + + def test_exgentic_docker_build_in_bench_setup_sh(self, tmp_path: Path) -> None: + """EXGENTIC_DOCKER_BUILD=1 is prepended to the setup.sh RUN in bench image.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=True) + project = _create_fake_project(tmp_path) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + bench_dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build" and "-f" not in cmd: + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + bench_dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install( + "mybench", + env_type=EnvType.DOCKER, + project_root=project, + module_path=module_path, + ) + + assert len(bench_dockerfiles) == 1 + assert "EXGENTIC_DOCKER_BUILD=1 bash /tmp/setup.sh" in bench_dockerfiles[0] + + def test_exgentic_docker_build_in_single_image_setup_sh(self, tmp_path: Path) -> None: + """EXGENTIC_DOCKER_BUILD=1 is also present in the single-image (no project_root) path.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=True) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + dockerfiles: list[str] = [] + + def capture_run(cmd, **kwargs): + if isinstance(cmd, list) and cmd[0] == "docker": + if cmd[1] == "build": + df = Path(cmd[-1]) / "Dockerfile" + if df.exists(): + dockerfiles.append(df.read_text()) + if cmd[1:3] == ["image", "inspect"]: + return _docker_mock_result(returncode=1) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + with mock.patch("subprocess.run", side_effect=capture_run): + mgr.install("mybench", env_type=EnvType.DOCKER, module_path=module_path) + + assert len(dockerfiles) == 1 + assert "EXGENTIC_DOCKER_BUILD=1 bash /tmp/setup.sh" in dockerfiles[0] + + +# --------------------------------------------------------------------------- +# DockerBackend Dockerfile generation +# --------------------------------------------------------------------------- + + +class TestDockerBackendDockerfile: + """Verify the Dockerfile content DockerBackend generates. + + These tests mock subprocess.run to capture the Dockerfile instead of + actually building. They verify critical assumptions: + - exgentic is installed from source when a source dir is provided + - requirements.txt and setup.sh are included + - docker socket installs Docker CLI + - extra dependencies are installed + """ + + def _capture_dockerfiles(self, tmp_path, **install_kwargs): + """Run DockerBackend.install() with mocked docker, return list of Dockerfile contents.""" + from exgentic.environment.docker import DockerBackend + + dockerfiles: list[str] = [] + + def side_effect(cmd, **kwargs): + cmd = list(cmd) + if cmd[0] == "docker" and cmd[1:3] == ["image", "inspect"]: + # Image doesn't exist yet. + return _docker_mock_result(returncode=1) + if cmd[0] == "docker" and cmd[1] == "build": + # Find the Dockerfile. + if "-f" in cmd: + df_path = cmd[cmd.index("-f") + 1] + else: + df_path = str(Path(cmd[-1]) / "Dockerfile") + dockerfiles.append(Path(df_path).read_text()) + return _docker_mock_result() + return _real_subprocess_run(cmd, **kwargs) + + backend = DockerBackend() + env_dir = tmp_path / "env" + env_dir.mkdir(parents=True, exist_ok=True) + + with mock.patch("subprocess.run", side_effect=side_effect): + backend.install(env_dir, **install_kwargs) + + return dockerfiles + + def _capture_dockerfile(self, tmp_path, **install_kwargs): + """Run DockerBackend.install() with mocked docker, return single Dockerfile content.""" + dockerfiles = self._capture_dockerfiles(tmp_path, **install_kwargs) + assert len(dockerfiles) == 1, f"Expected 1 docker build, got {len(dockerfiles)}" + return dockerfiles[0] + + def test_source_install_copies_source(self, tmp_path: Path) -> None: + """When project_root is given, build a base + bench image pair.""" + # Create a fake project root. + proj = tmp_path / "project" + proj.mkdir() + (proj / "pyproject.toml").write_text('[project]\nname = "fakepkg"\nversion = "0.1"\n') + (proj / "README.md").write_text("# Fake\n") + src = proj / "src" / "exgentic" + src.mkdir(parents=True) + (src / "__init__.py").write_text("") + + dockerfiles = self._capture_dockerfiles( + tmp_path, + name="benchmarks/test", + module_path=None, + project_root=proj, + ) + + assert len(dockerfiles) == 2, f"Expected 2 docker builds (base + bench), got {len(dockerfiles)}" + base_df, bench_df = dockerfiles + + # Base image installs exgentic from source. + assert "COPY pyproject.toml" in base_df + assert "COPY src/ src/" in base_df + assert "uv pip install --no-cache ." in base_df + assert "uv pip install --no-cache --no-deps ." in base_df + + # Bench image is layered on top. + assert "FROM exgentic-base:" in bench_df + + def test_pypi_install(self, tmp_path: Path) -> None: + """When packages are given, RUN uv pip install.""" + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=None, + packages=["exgentic==1.2.3"], + ) + + assert "uv pip install --no-cache exgentic==1.2.3" in df + assert "COPY src/" not in df + + def test_requirements_included(self, tmp_path: Path) -> None: + """requirements.txt from module_path is installed.""" + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=module_path, + packages=["exgentic==1.0"], + ) + + assert "requirements.txt" in df + assert "uv pip install --no-cache -r" in df + + def test_setup_sh_included(self, tmp_path: Path) -> None: + """setup.sh from module_path is run during build.""" + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=True) + + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=module_path, + packages=["exgentic==1.0"], + ) + + assert "setup.sh" in df + assert "EXGENTIC_DOCKER_BUILD=1 bash /tmp/setup.sh" in df + + def test_docker_socket_installs_cli(self, tmp_path: Path) -> None: + """docker_socket=True installs Docker CLI in the image.""" + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=None, + docker_socket=True, + ) + + assert "docker-27.5.1.tgz" in df + assert "/usr/local/bin docker/docker" in df + + def test_extra_dependencies(self, tmp_path: Path) -> None: + """Extra packages are pip-installed.""" + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=None, + packages=["numpy", "pandas"], + ) + + assert "uv pip install --no-cache numpy pandas" in df + + def test_no_exgentic_without_packages(self, tmp_path: Path) -> None: + """Without packages or project_root, no exgentic install lines.""" + df = self._capture_dockerfile( + tmp_path, + name="benchmarks/test", + module_path=None, + ) + + assert "COPY src/" not in df + assert "exgentic" not in df.lower() or "exgentic-docker" in df.lower() + + def test_image_tag_deterministic(self, tmp_path: Path) -> None: + """Same inputs produce the same image tag.""" + from exgentic.environment.docker import DockerBackend + + tag1 = DockerBackend._image_tag("benchmarks/test", None, docker_socket=True) + tag2 = DockerBackend._image_tag("benchmarks/test", None, docker_socket=True) + assert tag1 == tag2 + + def test_image_tag_changes_with_packages(self, tmp_path: Path) -> None: + """Different packages produce different tags.""" + from exgentic.environment.docker import DockerBackend + + tag1 = DockerBackend._image_tag("benchmarks/test", None, packages=["exgentic==1.0"]) + tag2 = DockerBackend._image_tag("benchmarks/test", None, packages=["exgentic==2.0"]) + assert tag1 != tag2 + + +# --------------------------------------------------------------------------- +# Docker integration (requires Docker/Podman running) +# --------------------------------------------------------------------------- + +_docker_available = shutil.which("docker") is not None +if _docker_available: + try: + subprocess.run(["docker", "info"], check=True, capture_output=True, timeout=5) + except Exception: + _docker_available = False + + +@pytest.mark.skipif(not _docker_available, reason="Docker not available") +class TestDockerIntegration: + """Real Docker tests -- actually build and remove images.""" + + def test_docker_build_and_marker(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest", env_type=EnvType.DOCKER, module_path=module_path) + + assert mgr.is_installed("inttest", env_type=EnvType.DOCKER) + image_tag = mgr.docker_image("inttest") + assert image_tag is not None + + result = subprocess.run(["docker", "image", "inspect", image_tag], check=False, capture_output=True, text=True) + assert result.returncode == 0 + + mgr.uninstall("inttest", env_type=EnvType.DOCKER) + result = subprocess.run(["docker", "image", "inspect", image_tag], check=False, capture_output=True, text=True) + assert result.returncode != 0 + + def test_docker_reuses_existing_image(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest2", env_type=EnvType.DOCKER, module_path=module_path) + tag1 = mgr.docker_image("inttest2") + + mgr.install("inttest2", env_type=EnvType.DOCKER, force=True, module_path=module_path) + tag2 = mgr.docker_image("inttest2") + + assert tag1 == tag2 + mgr.uninstall("inttest2") + + def test_docker_with_requirements(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=True, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest3", env_type=EnvType.DOCKER, module_path=module_path) + + image_tag = mgr.docker_image("inttest3") + assert image_tag is not None + + result = subprocess.run( + ["docker", "run", "--rm", image_tag, "python", "-c", "import requests; print(requests.__version__)"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert result.stdout.strip() + + mgr.uninstall("inttest3") + + def test_docker_coexists_with_venv(self, tmp_path: Path) -> None: + module_path = _create_fake_package(tmp_path, with_requirements=False, with_setup=False) + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest4", env_type=EnvType.VENV, module_path=module_path) + mgr.install("inttest4", env_type=EnvType.DOCKER, module_path=module_path) + + assert mgr.is_installed("inttest4", env_type=EnvType.VENV) + assert mgr.is_installed("inttest4", env_type=EnvType.DOCKER) + + mgr.uninstall("inttest4", env_type=EnvType.DOCKER) + assert mgr.is_installed("inttest4", env_type=EnvType.VENV) + assert not mgr.is_installed("inttest4", env_type=EnvType.DOCKER) + + mgr.uninstall("inttest4") + + def test_docker_project_root_installs_package(self, tmp_path: Path) -> None: + """Build Docker image with project_root and verify the package is importable inside.""" + project = _create_fake_project(tmp_path, name="mypkg") + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest5", env_type=EnvType.DOCKER, project_root=project) + + image_tag = mgr.docker_image("inttest5") + assert image_tag is not None + + result = subprocess.run( + ["docker", "run", "--rm", image_tag, "python", "-c", "import mypkg; print(mypkg.__version__)"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "0.1.0" + + mgr.uninstall("inttest5") + + def test_venv_project_root_installs_package(self, tmp_path: Path) -> None: + """Create venv with project_root and verify the package is importable inside.""" + project = _create_fake_project(tmp_path, name="mypkg2") + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest6", env_type=EnvType.VENV, project_root=project) + + venv_py = mgr.venv_python("inttest6") + result = subprocess.run( + [venv_py, "-c", "import mypkg2; print(mypkg2.__version__)"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "0.1.0" + + mgr.uninstall("inttest6") + + def test_docker_socket_creates_working_docker_cli(self, tmp_path: Path) -> None: + """docker_socket=True installs a functional Docker CLI binary inside the image.""" + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + mgr.install("inttest-ds", env_type=EnvType.DOCKER, docker_socket=True) + + image_tag = mgr.docker_image("inttest-ds") + assert image_tag is not None + + result = subprocess.run( + ["docker", "run", "--rm", image_tag, "docker", "--version"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert "Docker version" in result.stdout + + mgr.uninstall("inttest-ds") + + def test_base_image_shared_between_benchmarks(self, tmp_path: Path) -> None: + """Two benchmarks with the same project_root share the base image (1 base build, 2 bench builds).""" + project = _create_fake_project(tmp_path, name="shared") + mgr = EnvironmentManager(base_dir=tmp_path / "envs") + + # Use a unique name prefix to avoid collisions with other test runs. + mgr.install("inttest-shared-a", env_type=EnvType.DOCKER, project_root=project) + mgr.install("inttest-shared-b", env_type=EnvType.DOCKER, project_root=project) + + tag_a = mgr.docker_image("inttest-shared-a") + tag_b = mgr.docker_image("inttest-shared-b") + assert tag_a is not None + assert tag_b is not None + # Same project_root → same base tag prefix, different bench tags (different names). + assert tag_a != tag_b + assert tag_a.startswith("inttest-shared-a:") + assert tag_b.startswith("inttest-shared-b:") + + # Both images must be runnable. + for tag in (tag_a, tag_b): + result = subprocess.run( + ["docker", "run", "--rm", tag, "python", "-c", "import shared; print(shared.__version__)"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "0.1.0" + + mgr.uninstall("inttest-shared-a") + mgr.uninstall("inttest-shared-b") diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_async_key.py b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_async_key.py new file mode 100644 index 00000000..f79dcfcd --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_async_key.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any + +import pytest +from exgentic.integrations.litellm.cache import CustomCache + + +def _build_response() -> dict[str, Any]: + return { + "model": "gpt-4o-mini", + "choices": [ + { + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + } + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +@pytest.mark.anyio +async def test_async_cache_key_reuse_for_call_id() -> None: + cache = CustomCache(type="local") + base_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-1", + } + assert await cache.async_get_cache(**base_kwargs) is None + + mutated_kwargs = dict(base_kwargs) + mutated_kwargs["messages"] = [{"role": "user", "content": "different"}] + response = _build_response() + await cache.async_add_cache(response, **mutated_kwargs) + + followup_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-2", + } + assert await cache.async_get_cache(**followup_kwargs) is not None diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_cache_logger_context.py b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_cache_logger_context.py new file mode 100644 index 00000000..c0fc7b82 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_cache_logger_context.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic.core.context import Context, Role, set_context +from exgentic.integrations.litellm.cache.log import CacheLogger + + +def test_cache_logger_uses_context_session_path(tmp_path) -> None: + ctx = Context( + run_id="run-cache", + output_dir=str(tmp_path), + cache_dir=str(tmp_path / "cache"), + session_id="sess-1", + role=Role.AGENT, + ) + set_context(ctx) + + logger = CacheLogger(disk_cache_dir=str(tmp_path / "cache"), strip_time=False) + logger.hit() + + expected = tmp_path / "run-cache" / "sessions" / "sess-1" / "agent" / "litellm" / "cache.log" + assert expected.exists() diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_key.py b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_key.py new file mode 100644 index 00000000..4b2baa7d --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_key.py @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic.integrations.litellm.cache import ( + CustomCache, + strip_date_time_from_text, +) + + +def test_strip_date_time_from_text_removes_date_time() -> None: + text = "Schedule 2024-05-01 at 09:30 AM for review." + cleaned = strip_date_time_from_text(text) + assert "2024-05-01" not in cleaned + assert "09:30" not in cleaned + + +def test_cache_key_ignores_date_time_when_enabled() -> None: + cache = CustomCache(type="local", delete_time_from_messages=True) + key_one = cache.get_cache_key( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Today is 2024-05-01 10:30."}], + ) + key_two = cache.get_cache_key( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Today is 2025-06-02 22:15."}], + ) + assert key_one == key_two + + +def test_cache_key_keeps_date_time_when_disabled() -> None: + cache = CustomCache(type="local", delete_time_from_messages=False) + key_one = cache.get_cache_key( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Today is 2024-05-01 10:30."}], + ) + key_two = cache.get_cache_key( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Today is 2025-06-02 22:15."}], + ) + assert key_one != key_two + + +def test_strip_date_does_not_remove_adjacent_numbers() -> None: + r"""Regression test: numbers following a date should not be stripped. + + The original regex pattern `(?:,?\s+\d{2,4})?` was too permissive, + matching 2-4 digit numbers after a space as if they were years. + + Example 1 - Broken datetime parsing: + Input: "Jan 20 15:36" + Bug: Date regex matches "Jan 20 15" (treats hour "15" as a year) + Result: Leaves ":36" orphaned, time regex fails (no word boundary before ":") + + Example 2 - Lost data: + Input: "May 15 30 items" + Bug: Date regex matches "May 15 30" (treats "30" as a year) + Result: Loses the number "30" entirely + + The fix uses `(?:,\s*\d{2}|\s+\d{4})?` which requires: + - Comma followed by exactly 2-digit year (e.g., "May 15,23") + - Space followed by exactly 4-digit year (e.g., "May 15 2023") + """ + # Example 1: Datetime parsing - hour mistaken for year + # Original bug: "Jan 20 15:36" -> matches "Jan 20 15", orphans ":36" + text = "Event at Jan 20 15:36 in the main hall" + cleaned = strip_date_time_from_text(text) + assert "Jan 20" not in cleaned + assert "15:36" not in cleaned # time should be stripped properly + assert ":36" not in cleaned # no orphaned time fragment + assert cleaned == "Event at in the main hall" + + # Example 2: Adjacent numbers - count mistaken for year + # Original bug: "May 15 30" -> stripped entirely, losing "30" + text2 = "May 15 30 items in stock" + cleaned2 = strip_date_time_from_text(text2) + assert "May 15" not in cleaned2 + assert "30" in cleaned2 + assert cleaned2 == "30 items in stock" + + # Example 3: 3-digit numbers are also not years + # Original bug: "May 15 300" -> stripped entirely + text3 = "May 15 300 users joined" + cleaned3 = strip_date_time_from_text(text3) + assert "300" in cleaned3 + assert cleaned3 == "300 users joined" + + # Valid year formats should still be stripped correctly + assert strip_date_time_from_text("May 15 2023 was great") == "was great" + assert strip_date_time_from_text("May 15, 23 ended") == "ended" diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_settings.py b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_settings.py new file mode 100644 index 00000000..57b6afc1 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_settings.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +from pathlib import Path + +from exgentic.core.context import run_scope, try_get_context +from exgentic.core.types import RunConfig +from exgentic.integrations.litellm.cache import build_litellm_cache +from exgentic.integrations.litellm.config import configure_litellm +from exgentic.integrations.litellm.trace_logger import ( + FILE_ENV, + AsyncTraceLogger, + SyncTraceLogger, + TraceLogger, +) +from exgentic.utils.settings import ExgenticSettings, resolve_cache_path + + +def test_resolve_cache_path_uses_base_dir_for_relative_paths() -> None: + assert resolve_cache_path(".exgentic", ".litellm_cache") == ".exgentic/.litellm_cache" + + +def test_resolve_cache_path_keeps_absolute_paths() -> None: + assert resolve_cache_path(".exgentic", "/tmp/litellm") == "/tmp/litellm" + + +def test_build_litellm_cache_resolves_relative_path_under_cache_dir(tmp_path) -> None: + base_dir = tmp_path / "cache-root" + settings = ExgenticSettings( + cache_dir=str(base_dir), + litellm_cache_dir=".litellm_cache", + ) + cache = build_litellm_cache(settings) + assert cache.cache.disk_cache.directory == str(base_dir / ".litellm_cache") + + +def test_run_config_to_session_config_preserves_cache_dir() -> None: + run_config = RunConfig( + benchmark="tau2", + agent="tool_calling", + cache_dir="/tmp/exgentic-cache", + ) + session_config = run_config.to_session_config("task-1") + assert session_config.cache_dir == "/tmp/exgentic-cache" + + +def test_run_scope_sets_and_restores_cache_env() -> None: + before = try_get_context() + with run_scope( + run_id="cache-test", + output_dir="./outputs", + cache_dir="./cache", + ): + ctx = try_get_context() + assert ctx is not None + assert ctx.cache_dir == str(Path("./cache").resolve()) + after = try_get_context() + if before is None: + assert after is None + else: + assert after == before + + +def test_configure_litellm_always_registers_trace_logger_callbacks() -> None: + import litellm + + original_callbacks = litellm.callbacks + original_success = litellm.success_callback + original_failure = litellm.failure_callback + original_async_success = litellm._async_success_callback + original_async_failure = litellm._async_failure_callback + try: + litellm.callbacks = [] + litellm.success_callback = [] + litellm.failure_callback = [] + litellm._async_success_callback = [] + litellm._async_failure_callback = [] + + settings = ExgenticSettings(litellm_caching=False) + configure_litellm(config=settings.to_litellm_config(), cache_only=False) + configure_litellm(config=settings.to_litellm_config(), cache_only=False) + + assert any(isinstance(cb, SyncTraceLogger) for cb in litellm.success_callback) + assert any(isinstance(cb, AsyncTraceLogger) for cb in litellm.success_callback) + assert any(isinstance(cb, SyncTraceLogger) for cb in litellm.failure_callback) + assert any(isinstance(cb, AsyncTraceLogger) for cb in litellm.failure_callback) + assert sum(isinstance(cb, SyncTraceLogger) for cb in litellm.success_callback) == 1 + assert sum(isinstance(cb, AsyncTraceLogger) for cb in litellm.success_callback) == 1 + assert sum(isinstance(cb, SyncTraceLogger) for cb in litellm.failure_callback) == 1 + assert sum(isinstance(cb, AsyncTraceLogger) for cb in litellm.failure_callback) == 1 + assert any(isinstance(cb, SyncTraceLogger) for cb in litellm._async_success_callback) + assert any(isinstance(cb, AsyncTraceLogger) for cb in litellm._async_success_callback) + assert any(isinstance(cb, SyncTraceLogger) for cb in litellm._async_failure_callback) + assert any(isinstance(cb, AsyncTraceLogger) for cb in litellm._async_failure_callback) + finally: + litellm.callbacks = original_callbacks + litellm.success_callback = original_success + litellm.failure_callback = original_failure + litellm._async_success_callback = original_async_success + litellm._async_failure_callback = original_async_failure + + +def test_trace_logger_callback_registered_and_writes_by_default(tmp_path, monkeypatch) -> None: + import litellm + + original_callbacks = litellm.callbacks + original_success = litellm.success_callback + original_failure = litellm.failure_callback + original_async_success = litellm._async_success_callback + original_async_failure = litellm._async_failure_callback + try: + litellm.callbacks = [] + litellm.success_callback = [] + litellm.failure_callback = [] + litellm._async_success_callback = [] + litellm._async_failure_callback = [] + + log_path = tmp_path / "trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(log_path)) + + settings = ExgenticSettings(litellm_caching=False) + configure_litellm(config=settings.to_litellm_config(), cache_only=False) + + registered = [cb for cb in litellm.success_callback if isinstance(cb, TraceLogger)] + assert len(registered) == 2 + + kwargs = { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hi"}], + "response_cost": 0.0, + } + response = { + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + "choices": [ + { + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + } + registered[0].log_success_event(kwargs, response, None, None) + + assert log_path.exists() + record = json.loads(log_path.read_text(encoding="utf-8").splitlines()[0]) + assert record["status"] == "success" + assert record["model"] == "openai/gpt-4o-mini" + finally: + litellm.callbacks = original_callbacks + litellm.success_callback = original_success + litellm.failure_callback = original_failure + litellm._async_success_callback = original_async_success + litellm._async_failure_callback = original_async_failure diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_sync_key.py b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_sync_key.py new file mode 100644 index 00000000..6da48f80 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/cache/test_sync_key.py @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from typing import Any + +from exgentic.integrations.litellm.cache import CustomCache + + +def _build_response() -> dict[str, Any]: + return { + "model": "gpt-4o-mini", + "choices": [ + { + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + } + + +def test_sync_cache_key_reuse_for_call_id() -> None: + cache = CustomCache(type="local") + base_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-1", + } + assert cache.get_cache(**base_kwargs) is None + + mutated_kwargs = dict(base_kwargs) + mutated_kwargs["messages"] = [{"role": "user", "content": "different"}] + cache.add_cache(_build_response(), **mutated_kwargs) + + followup_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-2", + } + assert cache.get_cache(**followup_kwargs) is not None + + +def test_sync_cache_without_call_id_uses_computed_key() -> None: + cache = CustomCache(type="local") + kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + } + assert cache.get_cache(**kwargs) is None + cache.add_cache(_build_response(), **kwargs) + assert cache.get_cache(**kwargs) is not None + + +def test_sync_cache_key_reuse_scoped_to_call_id() -> None: + cache = CustomCache(type="local") + base_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-1", + } + assert cache.get_cache(**base_kwargs) is None + + mutated_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "different"}], + "litellm_call_id": "call-2", + } + cache.add_cache(_build_response(), **mutated_kwargs) + + followup_kwargs = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hello"}], + "litellm_call_id": "call-3", + } + assert cache.get_cache(**followup_kwargs) is None diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/conftest.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/conftest.py new file mode 100644 index 00000000..8bbe5a1c --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/conftest.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any + +import pytest + + +class FakeOpenAIHandler(BaseHTTPRequestHandler): + request_count = 0 + + def do_GET(self) -> None: # noqa: N802 + if self.path.endswith("/v1/models"): + payload = {"object": "list", "data": [{"id": "openai/gpt-4o-mini"}]} + self._write_json(payload) + return + self.send_error(404) + + def do_POST(self) -> None: # noqa: N802 + if self.path.endswith("/v1/chat/completions"): + length = int(self.headers.get("content-length", "0")) + body = self.rfile.read(length).decode("utf-8") if length else "{}" + data = json.loads(body or "{}") + FakeOpenAIHandler.request_count += 1 + model = data.get("model", "openai/gpt-4o-mini") + payload = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + } + self._write_json(payload) + return + self.send_error(404) + + def log_message(self, fmt: str, *args: Any) -> None: + return + + def _write_json(self, payload: dict[str, Any]) -> None: + data = json.dumps(payload).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + +@pytest.fixture() +def fake_openai_server() -> ThreadingHTTPServer: + try: + server = ThreadingHTTPServer(("127.0.0.1", 0), FakeOpenAIHandler) + except PermissionError as exc: + pytest.skip(f"Socket binding not permitted: {exc}") + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield server + finally: + server.shutdown() + server.server_close() diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_cache_execution.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_cache_execution.py new file mode 100644 index 00000000..77e81488 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_cache_execution.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +import pytest +import requests +from exgentic.integrations.litellm import LitellmProxy + + +@pytest.mark.skipif( + not (Path(sys.executable).parent / "litellm").exists(), + reason="litellm CLI not installed in active venv", +) +def test_proxy_reuses_cached_completion_response(tmp_path, fake_openai_server) -> None: + fake_openai_server.RequestHandlerClass.request_count = 0 + backend_port = fake_openai_server.server_address[1] + backend_base = f"http://127.0.0.1:{backend_port}/v1" + + venv_bin = Path(sys.executable).parent + env = { + "OPENAI_API_BASE": backend_base, + "OPENAI_API_KEY": "test-key", # pragma: allowlist secret + "EXGENTIC_CACHE_DIR": str(tmp_path / "cache"), + "EXGENTIC_litellm_cache_dir": ".litellm_cache", + "EXGENTIC_litellm_caching": "true", + "EXGENTIC_LOG_LEVEL": "DEBUG", + "PATH": f"{venv_bin}{os.pathsep}{os.environ.get('PATH', '')}", + } + payload = { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello"}], + "temperature": 0.0, + } + + with LitellmProxy( + model="openai/gpt-4o-mini", + env=env, + startup_timeout=10.0, + ) as proxy: + url = f"{proxy.base_url}/v1/chat/completions" + first = requests.post(url, json=payload, timeout=10) + first.raise_for_status() + second = requests.post(url, json=payload, timeout=10) + second.raise_for_status() + + assert fake_openai_server.RequestHandlerClass.request_count == 1 diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_config.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_config.py new file mode 100644 index 00000000..312b62ae --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_config.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +from exgentic.integrations.litellm import LitellmProxy + + +def test_proxy_writes_exgentic_trace_callback_to_litellm_settings_config(tmp_path, monkeypatch) -> None: + import exgentic.integrations.litellm.proxy as proxy_mod + + class _DummyProc: + def poll(self): + return None + + def terminate(self) -> None: + return None + + def wait(self, timeout=None) -> int: + return 0 + + def kill(self) -> None: + return None + + def _fake_popen(*args, **kwargs): + return _DummyProc() + + monkeypatch.setattr(proxy_mod.subprocess, "Popen", _fake_popen) + monkeypatch.setattr(proxy_mod, "_is_port_open", lambda *_args, **_kwargs: True) + monkeypatch.setattr(proxy_mod, "_is_proxy_ready", lambda *_args, **_kwargs: True) + + log_path = tmp_path / "litellm.log" + + with LitellmProxy( + model="openai/gpt-4o-mini", + port=49999, + log_path=str(log_path), + startup_timeout=1.0, + ): + config_path = log_path.with_name("litellm_config.json") + assert config_path.exists() + config_data = json.loads(config_path.read_text(encoding="utf-8")) + callback = "exgentic.integrations.litellm.trace_logger.trace_logger" + async_callback = "exgentic.integrations.litellm.trace_logger.async_trace_logger" + assert set(config_data["litellm_settings"]["success_callback"]) == { + callback, + async_callback, + } + assert set(config_data["litellm_settings"]["failure_callback"]) == { + callback, + async_callback, + } diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_execution.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_execution.py new file mode 100644 index 00000000..ca11c700 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_callback_execution.py @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json +import os +import socket +import subprocess +import sys +import time +from pathlib import Path + +import pytest +import requests +from exgentic.integrations.litellm import LitellmProxy + + +def _free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +def _wait_for_proxy(url: str, timeout: float = 10.0) -> None: + deadline = time.time() + timeout + while time.time() < deadline: + try: + if requests.get(url, timeout=1.0).status_code == 200: + return + except Exception: + pass + time.sleep(0.1) + raise RuntimeError("LiteLLM proxy did not become ready") + + +@pytest.mark.skipif( + not (Path(sys.executable).parent / "litellm").exists(), + reason="litellm CLI not installed in active venv", +) +def test_exgentic_proxy_executes_default_trace_callback_and_writes_trace(tmp_path, fake_openai_server) -> None: + backend_port = fake_openai_server.server_address[1] + backend_base = f"http://127.0.0.1:{backend_port}/v1" + trace_path = tmp_path / "trace.jsonl" + + env = os.environ.copy() + env.update( + { + "OPENAI_API_BASE": backend_base, + "OPENAI_API_KEY": "test-key", # pragma: allowlist secret + "EXGENTIC_LLM_LOG_FILE": str(trace_path), + } + ) + + with LitellmProxy( + model="openai/gpt-4o-mini", + env=env, + startup_timeout=10.0, + ) as proxy: + response = requests.post( + f"{proxy.base_url}/v1/chat/completions", + json={ + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello"}], + }, + timeout=10, + ) + response.raise_for_status() + + assert trace_path.exists() + rows = [json.loads(line) for line in trace_path.read_text().splitlines() if line] + assert rows and rows[0]["status"] == "success" + + +@pytest.mark.skipif( + not (Path(sys.executable).parent / "litellm").exists(), + reason="litellm CLI not installed in active venv", +) +def test_raw_litellm_proxy_executes_litellm_settings_callbacks_and_writes_trace(tmp_path, fake_openai_server) -> None: + backend_port = fake_openai_server.server_address[1] + backend_base = f"http://127.0.0.1:{backend_port}/v1" + proxy_port = _free_port() + trace_path = tmp_path / "trace.jsonl" + config_path = tmp_path / "litellm_config.json" + + callback_path = "exgentic.integrations.litellm.trace_logger.trace_logger" + async_callback_path = "exgentic.integrations.litellm.trace_logger.async_trace_logger" + config_path.write_text( + json.dumps( + { + "model_list": [ + { + "model_name": "openai/gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + } + ], + "litellm_settings": { + "success_callback": [callback_path, async_callback_path], + "failure_callback": [callback_path, async_callback_path], + }, + } + ), + encoding="utf-8", + ) + + env = os.environ.copy() + env.update( + { + "OPENAI_API_BASE": backend_base, + "OPENAI_API_KEY": "test-key", # pragma: allowlist secret + "EXGENTIC_LLM_LOG_FILE": str(trace_path), + } + ) + repo_root = Path(__file__).resolve().parents[4] + src_path = repo_root / "src" + env["PYTHONPATH"] = os.pathsep.join( + [ + str(src_path), + str(repo_root), + env.get("PYTHONPATH", ""), + ] + ) + + proc = subprocess.Popen( + [ + str(Path(sys.executable).parent / "litellm"), + "--config", + str(config_path), + "--port", + str(proxy_port), + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=env, + ) + try: + _wait_for_proxy(f"http://127.0.0.1:{proxy_port}/v1/models") + response = requests.post( + f"http://127.0.0.1:{proxy_port}/v1/chat/completions", + json={ + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello"}], + }, + timeout=10, + ) + response.raise_for_status() + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + assert trace_path.exists() + rows = [json.loads(line) for line in trace_path.read_text().splitlines() if line] + assert rows and rows[0]["status"] == "success" diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_env.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_env.py new file mode 100644 index 00000000..0d68e236 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_env.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic.core.context import Context, set_context +from exgentic.integrations.litellm import LitellmProxy + + +def test_proxy_passes_context_env(monkeypatch): + captured = {} + + class _DummyProc: + def poll(self): + return None + + def terminate(self) -> None: + return None + + def wait(self, timeout=None) -> int: + return 0 + + def kill(self) -> None: + return None + + def _fake_popen(*args, **kwargs): + captured["env"] = kwargs.get("env", {}) + return _DummyProc() + + monkeypatch.setattr("exgentic.integrations.litellm.proxy.subprocess.Popen", _fake_popen) + monkeypatch.setattr("exgentic.integrations.litellm.proxy._is_port_open", lambda *_a, **_k: True) + monkeypatch.setattr("exgentic.integrations.litellm.proxy._is_proxy_ready", lambda *_a, **_k: True) + + ctx = Context(run_id="run-proxy", output_dir="/tmp/out", cache_dir="/tmp/cache") + set_context(ctx) + + with LitellmProxy(model="openai/gpt-4o-mini", port=49998, startup_timeout=1.0): + env = captured["env"] + assert env["EXGENTIC_CTX_RUN_ID"] == "run-proxy" diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_subprocess_integration.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_subprocess_integration.py new file mode 100644 index 00000000..4bf58dcc --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_proxy_subprocess_integration.py @@ -0,0 +1,110 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Integration test for proxy subprocess config validation and trace detail. + +Complements test_proxy_callback_execution.py (which already exercises the full +subprocess path via LitellmProxy) by adding: + +- Config file validation: checks that the generated JSON config contains the + expected callback entries before any request is made. +- Explicit subprocess liveness assertions (proxy._proc is not None / running). +- Detailed trace field validation: model name, token counts, request/response + presence — fields not checked by the existing execution test. +""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +import pytest +import requests +from exgentic.integrations.litellm import LitellmProxy + + +@pytest.mark.skipif( + not (Path(sys.executable).parent / "litellm").exists(), + reason="litellm CLI not installed in active venv", +) +def test_proxy_subprocess_writes_trace_end_to_end(tmp_path, fake_openai_server) -> None: + """Verify config file contents, subprocess liveness, and detailed trace fields. + + Adds assertions that go beyond test_proxy_callback_execution.py: + - Config JSON contains the expected success_callback entries. + - The subprocess process object is alive during the request. + - Trace entries include model, token counts, request/response, and timestamp. + """ + backend_port = fake_openai_server.server_address[1] + backend_base = f"http://127.0.0.1:{backend_port}/v1" + trace_path = tmp_path / "trace.jsonl" + log_path = tmp_path / "proxy.log" + + env = os.environ.copy() + env.update( + { + "OPENAI_API_BASE": backend_base, + "OPENAI_API_KEY": "test-key", # pragma: allowlist secret + "EXGENTIC_LLM_LOG_FILE": str(trace_path), + } + ) + + # Start proxy as actual subprocess (not mocked) + with LitellmProxy( + model="openai/gpt-4o-mini", + env=env, + log_path=str(log_path), + startup_timeout=15.0, + ) as proxy: + # Verify config was written with callbacks + config_path = log_path.with_name("litellm_config.json") + assert config_path.exists(), "Config file should be written" + + config_data = json.loads(config_path.read_text(encoding="utf-8")) + assert "litellm_settings" in config_data + assert "success_callback" in config_data["litellm_settings"] + + expected_callbacks = { + "exgentic.integrations.litellm.trace_logger.trace_logger", + "exgentic.integrations.litellm.trace_logger.async_trace_logger", + } + actual_callbacks = set(config_data["litellm_settings"]["success_callback"]) + assert actual_callbacks == expected_callbacks + + # Verify proxy is running as subprocess (not in-process) + assert proxy._proc is not None + assert proxy._proc.poll() is None + + # Send request through subprocess proxy + response = requests.post( + f"{proxy.base_url}/v1/chat/completions", + json={ + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Test message"}], + }, + timeout=10, + ) + response.raise_for_status() + response_data = response.json() + + assert "choices" in response_data + assert len(response_data["choices"]) > 0 + assert response_data["choices"][0]["message"]["content"] == "ok" + + # Verify trace was written by subprocess + assert trace_path.exists(), f"Trace file should exist at {trace_path}" + + trace_lines = trace_path.read_text().strip().splitlines() + assert len(trace_lines) > 0, "Trace file should have at least one entry" + + trace_entry = json.loads(trace_lines[0]) + assert trace_entry["status"] == "success" + assert trace_entry["model"] == "openai/gpt-4o-mini" + assert trace_entry["prompt_tokens"] == 1 + assert trace_entry["completion_tokens"] == 1 + assert trace_entry["total_tokens"] == 2 + assert "request" in trace_entry + assert "response" in trace_entry + assert "timestamp" in trace_entry diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_trace_logger_env.py b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_trace_logger_env.py new file mode 100644 index 00000000..10ab2248 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/proxy/test_trace_logger_env.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import asyncio +import json + +from exgentic.core.context import Context, Role +from exgentic.integrations.litellm.trace_logger import ( + FILE_ENV, + TraceLogger, +) + + +def _sample_payload() -> tuple[dict, dict]: + kwargs = { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hi"}], + "response_cost": 0.0, + } + response = { + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + "choices": [{"message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}], + } + return kwargs, response + + +def test_trace_logger_writes(tmp_path, monkeypatch) -> None: + log_path = tmp_path / "trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + logger.log_success_event(kwargs, response, None, None) + + assert log_path.exists() + lines = log_path.read_text().strip().splitlines() + assert len(lines) == 1 + record = json.loads(lines[0]) + assert record["status"] == "success" + assert record["model"] == "openai/gpt-4o-mini" + + +def test_trace_logger_writes_without_toggle(tmp_path, monkeypatch) -> None: + log_path = tmp_path / "trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + logger.log_success_event(kwargs, response, None, None) + + assert log_path.exists() + lines = log_path.read_text().strip().splitlines() + assert len(lines) == 1 + + +def test_trace_logger_reads_context_from_metadata(tmp_path, monkeypatch) -> None: + env_log_path = tmp_path / "env_trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(env_log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + kwargs["litellm_metadata"] = { + "context": Context( + run_id="run_meta", + output_dir=str(tmp_path), + cache_dir=str(tmp_path / "cache"), + session_id="sess_meta", + role=Role.AGENT, + ) + } + logger.log_success_event(kwargs, response, None, None) + + expected = tmp_path / "run_meta" / "sessions" / "sess_meta" / "agent" / "litellm" / "trace.jsonl" + assert expected.exists() + assert not env_log_path.exists() + + +def test_trace_logger_reads_context_from_nested_metadata(tmp_path, monkeypatch) -> None: + env_log_path = tmp_path / "env_trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(env_log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + kwargs["litellm_params"] = { + "litellm_metadata": { + "context": Context( + run_id="run_nested", + output_dir=str(tmp_path), + cache_dir=str(tmp_path / "cache"), + session_id="sess_nested", + role=Role.AGENT, + ) + } + } + logger.log_success_event(kwargs, response, None, None) + + expected = tmp_path / "run_nested" / "sessions" / "sess_nested" / "agent" / "litellm" / "trace.jsonl" + assert expected.exists() + assert not env_log_path.exists() + + +def test_trace_logger_async_writes_with_kwargs_context(tmp_path, monkeypatch) -> None: + env_log_path = tmp_path / "env_trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(env_log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + kwargs["litellm_metadata"] = { + "context": Context( + run_id="run_async", + output_dir=str(tmp_path), + cache_dir=str(tmp_path / "cache"), + session_id="sess_async", + role=Role.AGENT, + ) + } + + asyncio.run(logger.async_log_success_event(kwargs, response, None, None)) + + expected = tmp_path / "run_async" / "sessions" / "sess_async" / "agent" / "litellm" / "trace.jsonl" + assert expected.exists() + lines = expected.read_text().strip().splitlines() + assert len(lines) == 1 + record = json.loads(lines[0]) + assert record["status"] == "success" + assert not env_log_path.exists() + + +def test_trace_logger_uses_kwargs_context_for_log_path(tmp_path, monkeypatch) -> None: + env_log_path = tmp_path / "env_trace.jsonl" + monkeypatch.setenv(FILE_ENV, str(env_log_path)) + + logger = TraceLogger() + kwargs, response = _sample_payload() + kwargs["context"] = Context( + run_id="run_abc", + output_dir=str(tmp_path), + cache_dir=str(tmp_path / "cache"), + session_id="sess_123", + role=Role.AGENT, + ) + logger.log_success_event(kwargs, response, None, None) + + expected = tmp_path / "run_abc" / "sessions" / "sess_123" / "agent" / "litellm" / "trace.jsonl" + assert expected.exists() + assert not env_log_path.exists() + + +def test_trace_logger_get_context_falls_back_to_try_get_context(monkeypatch) -> None: + fallback = Context( + run_id="run_fallback", + output_dir="/tmp/out", + cache_dir="/tmp/cache", + session_id="sess_fallback", + role=Role.AGENT, + ) + monkeypatch.setattr("exgentic.core.context.try_get_context", lambda: fallback) + + logger = TraceLogger() + assert logger.get_context({}) == fallback diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/test_health.py b/labs/AgentStream/exgentic/tests/integrations/litellm/test_health.py new file mode 100644 index 00000000..aa4a9566 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/test_health.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for LiteLLM health check error handling.""" + +from __future__ import annotations + +import logging +from unittest.mock import patch + +import pytest +from exgentic.integrations.litellm.health import check_model_accessible_sync + + +class MockLiteLLMError(Exception): + """Mock exception that mimics LiteLLM exceptions with .message attribute.""" + + def __init__(self, message: str): + self.message = message + super().__init__() + + def __str__(self) -> str: + """Return empty string to simulate LiteLLM exceptions that don't implement __str__.""" + return "" + + +def test_health_check_extracts_message_attribute_from_exception(caplog): + """Test that health check extracts error details from exception.message attribute.""" + caplog.set_level(logging.ERROR) + + with patch("exgentic.utils.sync.run_sync") as mock_run_sync: + exc = MockLiteLLMError("API key authentication failed") + mock_run_sync.side_effect = exc + + logger = logging.getLogger("test") + + with pytest.raises(RuntimeError) as exc_info: + check_model_accessible_sync("test-model", logger) + + error_msg = str(exc_info.value) + assert "API key authentication failed" in error_msg + assert "test-model" in error_msg + assert error_msg == "Model test-model is not accessible: API key authentication failed" + + +def test_health_check_falls_back_to_str_when_no_message_attribute(caplog): + """Test that health check falls back to str(exc) when .message is not available.""" + caplog.set_level(logging.ERROR) + + with patch("exgentic.utils.sync.run_sync") as mock_run_sync: + exc = ValueError("Standard error message") + mock_run_sync.side_effect = exc + + logger = logging.getLogger("test") + + with pytest.raises(RuntimeError) as exc_info: + check_model_accessible_sync("test-model", logger) + + error_msg = str(exc_info.value) + assert "Standard error message" in error_msg + assert "test-model" in error_msg + + +def test_health_check_uses_repr_as_last_resort(caplog): + """Test that health check uses repr(exc) when both .message and str(exc) are empty.""" + caplog.set_level(logging.ERROR) + + class EmptyError(Exception): + """Exception that returns empty string from __str__.""" + + def __str__(self) -> str: + return "" + + with patch("exgentic.utils.sync.run_sync") as mock_run_sync: + exc = EmptyError("hidden") + mock_run_sync.side_effect = exc + + logger = logging.getLogger("test") + + with pytest.raises(RuntimeError) as exc_info: + check_model_accessible_sync("test-model", logger) + + error_msg = str(exc_info.value) + assert "EmptyError" in error_msg + assert "test-model" in error_msg diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_cost.py b/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_cost.py new file mode 100644 index 00000000..850da09b --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_cost.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import json + +from exgentic.integrations.litellm.trace_cost import load_trace_cost + + +def test_load_trace_cost_uses_explicit_cost_field(tmp_path, monkeypatch) -> None: + trace_path = tmp_path / "trace.jsonl" + trace_path.write_text( + "\n".join( + [ + json.dumps({"cost": 1.25, "prompt_tokens": 1, "completion_tokens": 2}), + "not-json", + json.dumps({"cost": "2.5"}), + ] + ), + encoding="utf-8", + ) + + def _should_not_be_called(**_kwargs): + raise AssertionError("token fallback should not be used when cost is explicit") + + import exgentic.integrations.litellm.trace_cost as trace_cost_mod + + monkeypatch.setattr(trace_cost_mod, "litellm_tokens_cost", _should_not_be_called) + + assert load_trace_cost(trace_path, "openai/gpt-4o-mini") == 3.75 + + +def test_load_trace_cost_falls_back_to_token_estimate(tmp_path, monkeypatch) -> None: + trace_path = tmp_path / "trace.jsonl" + trace_path.write_text( + "\n".join( + [ + json.dumps({"prompt_tokens": 10, "completion_tokens": 20}), + json.dumps({"cost": None, "prompt_tokens": 5, "completion_tokens": 5}), + ] + ), + encoding="utf-8", + ) + + class _Cost: + def __init__(self, total_cost: float) -> None: + self.total_cost = total_cost + + def _fake_token_cost(*, model_name: str, input_tokens: int, output_tokens: int): + assert model_name == "openai/gpt-4o-mini" + return _Cost(total_cost=(input_tokens + output_tokens) / 1000.0) + + import exgentic.integrations.litellm.trace_cost as trace_cost_mod + + monkeypatch.setattr(trace_cost_mod, "litellm_tokens_cost", _fake_token_cost) + + assert load_trace_cost(trace_path, "openai/gpt-4o-mini") == 0.04 diff --git a/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_logger_context.py b/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_logger_context.py new file mode 100644 index 00000000..25214bcf --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/litellm/test_trace_logger_context.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import os +from pathlib import Path + +from exgentic.core.context import ( + ENV_OTEL_SPAN_ID, + ENV_OTEL_TRACE_ID, + Context, + init_context_from_env, +) +from exgentic.integrations.litellm.trace_logger import TraceLogger + + +def test_trace_logger_initializes_context_from_env(tmp_path: Path): + ctx = Context(run_id="run-env", output_dir=str(tmp_path), cache_dir=str(tmp_path)) + env = ctx.to_env() + os.environ.update(env) + os.environ.pop(ENV_OTEL_TRACE_ID, None) + os.environ.pop(ENV_OTEL_SPAN_ID, None) + + # Force env init. + init_context_from_env() + logger = TraceLogger() + path = logger._resolve_log_path({}) + + assert "run-env" in path + + for k in env: + os.environ.pop(k, None) diff --git a/labs/AgentStream/exgentic/tests/integrations/test_mcp_agent.py b/labs/AgentStream/exgentic/tests/integrations/test_mcp_agent.py new file mode 100644 index 00000000..6cc18c78 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/test_mcp_agent.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import threading +import time +from typing import Literal + +import pytest +from exgentic.adapters.actions.functions import action_type_to_function +from exgentic.adapters.agents import mcp_agent as mcp +from exgentic.core.types import ( + ActionType, + MultiObservation, + ParallelAction, + SingleAction, + SingleObservation, +) +from pydantic import BaseModel + + +class FakeMCPServer: + def __init__(self, mcp=None, *args, **kwargs) -> None: + tools = kwargs.get("tools") or [] + if mcp is None: + mcp = FakeMCP() + for fn in tools: + mcp.tool(fn) + self.mcp = mcp + self.started = False + self.stopped = False + self.stop_calls = [] + self.host = "127.0.0.1" + self.connect_host = "127.0.0.1" + self.port = 12345 + + def start(self, timeout: float = 5.0) -> None: + self.started = True + + def stop( + self, + timeout: float = 10.0, + *, + error: BaseException | None = None, + raise_on_timeout: bool = True, + ) -> None: + self.stopped = True + self.stop_calls.append((timeout, error, raise_on_timeout)) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc, tb): + self.stop(error=exc, raise_on_timeout=True) + + +class FakeMCP: + def __init__(self) -> None: + self.tools: list = [] + + def tool(self, fn): + self.tools.append(fn) + + +class DummyMCPAgent(mcp.MCPAgent): + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> str: + self._last_mcp = (mcp_host, mcp_port) + return "ok" + + +class ErrorMCPAgent(mcp.MCPAgent): + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> str: + raise ValueError("boom") + + +def _patch_mcp(monkeypatch): + monkeypatch.setattr(mcp, "MCPServer", FakeMCPServer) + + +@pytest.fixture +def env(tmp_path): + from exgentic.core.context import run_scope + + with run_scope(run_id="test_run", output_dir=str(tmp_path)): + yield tmp_path + + +def test_run_code_agent_success_stops_server(env, monkeypatch): + _patch_mcp(monkeypatch) + agent = DummyMCPAgent("session") + agent.task = "task" + agent.context = {} + agent.actions = [] + + result = agent.run_code_agent([lambda: None]) + + assert result == "ok" + assert isinstance(agent.mcp, FakeMCP) + assert agent.mcp.tools + assert agent._mcp_server is not None + assert agent._mcp_server.started is True + assert agent._mcp_server.stopped is True + + +def test_run_code_agent_propagates_error_and_cleans_up(env, monkeypatch): + _patch_mcp(monkeypatch) + agent = ErrorMCPAgent("session") + agent.task = "task" + agent.context = {} + agent.actions = [] + + with pytest.raises(ValueError, match="boom"): + agent.run_code_agent([lambda: None]) + + assert agent._mcp_server is not None + assert agent._mcp_server.started is True + assert agent._mcp_server.stopped is True + + +def test_run_code_agent_ping_timeout_still_cleans_up(env, monkeypatch): + _patch_mcp(monkeypatch) + + def _raise_timeout(self, timeout: float = 5.0) -> None: + raise TimeoutError("ping timeout") + + monkeypatch.setattr(FakeMCPServer, "start", _raise_timeout) + agent = DummyMCPAgent("session") + agent.task = "task" + agent.context = {} + agent.actions = [] + + with pytest.raises(TimeoutError, match="ping timeout"): + agent.run_code_agent([lambda: None]) + + assert agent._mcp_server is not None + assert agent._mcp_server.started is False + assert agent._mcp_server.stopped is False + + +class ToolArgs(BaseModel): + value: int + + +class ToolA(SingleAction): + name: Literal["tool.a"] = "tool.a" + arguments: ToolArgs + + +class ToolB(SingleAction): + name: Literal["tool.b"] = "tool.b" + arguments: ToolArgs + + +class ParallelToolMCPAgent(mcp.MCPAgent): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.results: list = [] + + def run_mcp_agent(self, mcp_host: str, mcp_port: int) -> str: + tools = list(self.mcp.tools) + results = [] + + def _call(fn, value): + results.append(fn(value=value)) + + t1 = threading.Thread(target=_call, args=(tools[0], 1)) + t2 = threading.Thread(target=_call, args=(tools[1], 2)) + t1.start() + t2.start() + t1.join(timeout=2.0) + t2.join(timeout=2.0) + if t1.is_alive() or t2.is_alive(): + raise RuntimeError("Tool calls did not complete") + self.results = results + return "ok" + + +def test_mcp_agent_parallel_tool_calls_return_parallel_action(env, monkeypatch): + _patch_mcp(monkeypatch) + + actions = [ + ActionType(name="tool.a", description="tool a", cls=ToolA), + ActionType(name="tool.b", description="tool b", cls=ToolB), + ] + agent = ParallelToolMCPAgent("session") + agent.task = "task" + agent.context = {} + agent.actions = actions + functions = [action_type_to_function(act, agent.execute) for act in actions] + + worker = threading.Thread(target=agent.run_code_agent, args=(functions,)) + worker.start() + + deadline = time.time() + 1.0 + with agent._condition: + while len(agent._pending_actions) < 2 and time.time() < deadline: + agent._condition.wait(timeout=0.05) + pending = list(agent._pending_actions) + + observations = [ + SingleObservation(invoking_actions=[action], result={"ok": action.arguments.value}) + for action in pending + if isinstance(action, SingleAction) + ] + act = agent.react(MultiObservation(observations=observations)) + assert isinstance(act, ParallelAction) + worker.join(timeout=1.0) + if worker.is_alive(): + agent.close() + pytest.fail("MCP agent did not finish after parallel tool calls") + agent.close() + + assert len(agent.results) == 2 + assert all(isinstance(r, dict) and r.get("ok") in (1, 2) for r in agent.results) diff --git a/labs/AgentStream/exgentic/tests/integrations/test_mcp_server.py b/labs/AgentStream/exgentic/tests/integrations/test_mcp_server.py new file mode 100644 index 00000000..e6dcbe39 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/integrations/test_mcp_server.py @@ -0,0 +1,342 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import logging +import time +from datetime import timedelta + +import httpx +import pytest +from exgentic.adapters.agents import mcp_server as mcp_srv +from exgentic.utils.sync import run_sync +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client +from mcp.shared.exceptions import McpError + + +class FakeMCP: + def __init__(self, *args, **kwargs) -> None: + fake_ts = type( + "FakeTransportSecurity", + (), + {"allowed_hosts": [], "allowed_origins": []}, + )() + self.settings = type( + "FakeSettings", + (), + {"streamable_http_path": "/mcp", "transport_security": fake_ts}, + )() + return + + def add_tool(self, fn, **kwargs) -> None: + return None + + def streamable_http_app(self): + return object() + + +class FakeConfig: + def __init__(self, app, host, port, log_config=None) -> None: + self.app = app + self.host = host + self.port = port + self.log_config = log_config + + +class FakeServer: + def __init__(self, config) -> None: + self.config = config + self.should_exit = False + + def run(self, *args, **kwargs) -> None: + while not self.should_exit: + time.sleep(0.01) + + +def _patch_uvicorn(monkeypatch): + monkeypatch.setattr(mcp_srv.uvicorn, "Config", FakeConfig) + monkeypatch.setattr(mcp_srv.uvicorn, "Server", lambda cfg: FakeServer(cfg)) + monkeypatch.setattr(mcp_srv, "FastMCP", FakeMCP) + monkeypatch.setattr(mcp_srv, "wait_for_tcp", lambda *args, **kwargs: None) + + def _noop_run_sync(coro, timeout=None): + coro.close() + return + + monkeypatch.setattr(mcp_srv, "run_sync", _noop_run_sync) + monkeypatch.setattr(mcp_srv.socket, "socket", _fake_socket_factory()) + + +def _fake_socket_factory(): + class FakeSocket: + _next_port = 10000 + + def __init__(self, *args, **kwargs) -> None: + self._port = None + + def setsockopt(self, *args, **kwargs) -> None: + return None + + def bind(self, addr) -> None: + _, port = addr + if port == 0: + FakeSocket._next_port += 1 + port = FakeSocket._next_port + self._port = port + + def listen(self, backlog) -> None: + return None + + def getsockname(self): + return ("127.0.0.1", self._port) + + def close(self) -> None: + return None + + return FakeSocket + + +def test_mcp_server_start_stop(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server") + server = mcp_srv.MCPServer( + FakeMCP(), + host="127.0.0.1", + port=12345, + log_dir=tmp_path, + logger=logger, + ) + + server.start(timeout=1.0) + assert server.server is not None + assert server.thread is not None and server.thread.is_alive() + + server.stop(timeout=1.0) + assert server.server.should_exit is True + assert server.thread is None or not server.thread.is_alive() + + server.stop(timeout=1.0) + + +def test_mcp_server_start_stop_idempotent(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server.idempotent") + server = mcp_srv.MCPServer( + FakeMCP(), + log_dir=tmp_path, + logger=logger, + ) + + server.start(timeout=1.0) + first_thread = server.thread + server.start(timeout=1.0) + assert server.thread is first_thread + + server.stop(timeout=1.0) + server.stop(timeout=1.0) + + +def test_mcp_server_start_tcp_failure_stops(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server.tcp_fail") + server = mcp_srv.MCPServer( + FakeMCP(), + log_dir=tmp_path, + logger=logger, + ) + + def _raise_tcp(*args, **kwargs): + raise TimeoutError("tcp timeout") + + monkeypatch.setattr(mcp_srv, "wait_for_tcp", _raise_tcp) + + with pytest.raises(TimeoutError, match="tcp timeout"): + server.start(timeout=1.0) + + assert server.thread is None or not server.thread.is_alive() + + +def test_mcp_server_start_ping_failure_stops(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server.ping_fail") + server = mcp_srv.MCPServer( + FakeMCP(), + log_dir=tmp_path, + logger=logger, + ) + + def _raise_timeout(coro, timeout=None): + coro.close() + raise TimeoutError("ping timeout") + + monkeypatch.setattr(mcp_srv, "run_sync", _raise_timeout) + + with pytest.raises(TimeoutError, match="ping timeout"): + server.start(timeout=1.0) + + assert server.thread is None or not server.thread.is_alive() + + +def test_mcp_server_allocates_unique_ports(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server.ports") + + ports = set() + servers = [] + for _ in range(50): + server = mcp_srv.MCPServer( + FakeMCP(), + log_dir=tmp_path, + logger=logger, + ) + servers.append(server) + server.start(timeout=1.0) + ports.add(server.port) + + assert len(ports) == len(servers) + for server in servers: + server.stop(timeout=1.0) + + +def test_mcp_server_start_timeout(tmp_path, monkeypatch): + _patch_uvicorn(monkeypatch) + logger = logging.getLogger("test.mcp_server.timeout") + server = mcp_srv.MCPServer( + FakeMCP(), + host="127.0.0.1", + port=12345, + log_dir=tmp_path, + logger=logger, + ) + + def _no_start(): + time.sleep(0.2) + + monkeypatch.setattr(server, "_thread_entry", _no_start) + + with pytest.raises(RuntimeError, match="did not signal startup"): + server.start(timeout=0.05) + + if server.thread and server.thread.is_alive(): + server.thread.join(timeout=0.5) + + +async def _call_tool(app, name: str, arguments: dict, read_timeout_seconds=None): + async with app.router.lifespan_context(app): + transport = httpx.ASGITransport(app=app) + base_url = "http://127.0.0.1:9999" + async with httpx.AsyncClient( + transport=transport, + base_url=base_url, + ) as client: + async with streamable_http_client( + f"{base_url}/mcp", + http_client=client, + ) as (read_stream, write_stream, _): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + return await session.call_tool( + name, + arguments=arguments, + read_timeout_seconds=read_timeout_seconds, + ) + + +def test_mcp_server_unknown_tool_returns_mcp_error(tmp_path): + logger = logging.getLogger("test.mcp_server.unknown_tool") + + def echo(text: str) -> str: + return text + + server = mcp_srv.MCPServer( + tools=[echo], + log_dir=tmp_path, + logger=logger, + ) + app = server.mcp.streamable_http_app() + + result = run_sync(_call_tool(app, "does_not_exist", {})) + assert result.isError is True + assert any(block.text == "Unknown tool: does_not_exist" for block in result.content) + + +def test_mcp_server_invalid_tool_argument_name(tmp_path): + logger = logging.getLogger("test.mcp_server.bad_arg_name") + + def echo(text: str) -> str: + return text + + server = mcp_srv.MCPServer( + tools=[echo], + log_dir=tmp_path, + logger=logger, + ) + app = server.mcp.streamable_http_app() + + result = run_sync(_call_tool(app, "echo", {"wrong": "hi"})) + assert result.isError is True + assert any( + "Error executing tool echo:" in block.text and "validation error" in block.text.lower() + for block in result.content + ) + + +def test_mcp_server_invalid_tool_argument_type(tmp_path): + logger = logging.getLogger("test.mcp_server.bad_arg_type") + + def echo(text: str) -> str: + return text + + server = mcp_srv.MCPServer( + tools=[echo], + log_dir=tmp_path, + logger=logger, + ) + app = server.mcp.streamable_http_app() + + result = run_sync(_call_tool(app, "echo", {"text": 123})) + assert result.isError is True + assert any( + "Error executing tool echo:" in block.text and "validation error" in block.text.lower() + for block in result.content + ) + + +def test_mcp_server_call_tool_timeout(tmp_path): + logger = logging.getLogger("test.mcp_server.call_timeout") + + def slow(text: str) -> str: + time.sleep(0.2) + return text + + server = mcp_srv.MCPServer( + tools=[slow], + log_dir=tmp_path, + logger=logger, + ) + app = server.mcp.streamable_http_app() + + timeout = timedelta(milliseconds=10) + exc: BaseException | None = None + try: + run_sync(_call_tool(app, "slow", {"text": "hi"}, timeout)) + except BaseExceptionGroup as err: + exc = err + except McpError as err: + exc = err + + assert exc is not None + + def _find_mcp_error(err: BaseException) -> McpError | None: + if isinstance(err, McpError): + return err + if isinstance(err, BaseExceptionGroup): + for sub in err.exceptions: + found = _find_mcp_error(sub) + if found is not None: + return found + return None + + mcp_error = _find_mcp_error(exc) + assert mcp_error is not None + assert "Timed out while waiting for response" in str(mcp_error) diff --git a/labs/AgentStream/exgentic/tests/setup/__init__.py b/labs/AgentStream/exgentic/tests/setup/__init__.py new file mode 100644 index 00000000..5769a11d --- /dev/null +++ b/labs/AgentStream/exgentic/tests/setup/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Tests for the ``exgentic setup`` flow under tool-install-like isolation.""" diff --git a/labs/AgentStream/exgentic/tests/setup/test_tool_install.py b/labs/AgentStream/exgentic/tests/setup/test_tool_install.py new file mode 100644 index 00000000..58df2e65 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/setup/test_tool_install.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +"""Integration test that simulates ``uv tool install exgentic``. + +The test creates an isolated venv (the way ``uv tool install`` would), +installs exgentic into it, then runs ``exgentic setup --benchmark `` +from a clean working directory that has **no** ``pyproject.toml`` in any +parent — exactly the situation a user hits after a global tool install. +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +_uv_available = shutil.which("uv") is not None + +# Root of the exgentic source tree (two levels up from this file). +_REPO_ROOT = Path(__file__).resolve().parents[2] + +# Per-benchmark setup timeout in seconds (5 minutes). +_SETUP_TIMEOUT = 300 + + +_ALL_BENCHMARKS = [ + "tau2", + "gsm8k", + "appworld", + "bfcl", + "browsecompplus", + "hotpotqa", + "swebench", +] + + +@pytest.mark.skipif(not _uv_available, reason="uv CLI not available") +@pytest.mark.parametrize("benchmark", _ALL_BENCHMARKS) +def test_setup_in_tool_install_venv(benchmark: str, tmp_path: Path) -> None: + """Install exgentic into a fresh venv, then run ``exgentic setup``.""" + venv_dir = tmp_path / "venv" + work_dir = tmp_path / "workdir" + work_dir.mkdir() + + # 1. Create an isolated venv using uv. + subprocess.run( + ["uv", "venv", str(venv_dir), "--python", f"{sys.version_info.major}.{sys.version_info.minor}"], + check=True, + capture_output=True, + timeout=60, + ) + + # 2. Install exgentic from the local source tree into the venv. + subprocess.run( + ["uv", "pip", "install", str(_REPO_ROOT), "--python", str(venv_dir / "bin" / "python")], + check=True, + capture_output=True, + timeout=300, + ) + + # 3. Locate the ``exgentic`` entry-point inside the venv. + exgentic_bin = venv_dir / "bin" / "exgentic" + assert exgentic_bin.exists(), f"exgentic CLI not found at {exgentic_bin}" + + # 4. Run ``exgentic setup --benchmark `` from the clean workdir. + # The working directory deliberately has no pyproject.toml ancestors. + result = subprocess.run( + [str(exgentic_bin), "setup", "--benchmark", benchmark, "--force"], + cwd=str(work_dir), + capture_output=True, + text=True, + timeout=_SETUP_TIMEOUT, + ) + + assert result.returncode == 0, ( + f"exgentic setup --benchmark {benchmark} failed " + f"(rc={result.returncode}).\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + + # 5. Verify the installation marker was written. + venv_python = str(venv_dir / "bin" / "python") + check = subprocess.run( + [ + venv_python, + "-c", + ( + "from exgentic.environment.instance import get_manager; " + f"assert get_manager().is_installed('benchmarks/{benchmark}'), " + f"'installation marker not found for {benchmark}'" + ), + ], + cwd=str(work_dir), + capture_output=True, + text=True, + timeout=30, + ) + assert check.returncode == 0, ( + f"Installation marker check failed for {benchmark}.\n" f"stdout:\n{check.stdout}\nstderr:\n{check.stderr}" + ) diff --git a/labs/AgentStream/exgentic/tests/test_coordinator.py b/labs/AgentStream/exgentic/tests/test_coordinator.py new file mode 100644 index 00000000..2fb7fe3b --- /dev/null +++ b/labs/AgentStream/exgentic/tests/test_coordinator.py @@ -0,0 +1,298 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import threading +import time + +from exgentic.adapters.agents.coordinator import AgentCoordinator, CoordinatedAgent +from exgentic.core.types import ( + MultiObservation, + ParallelAction, + SingleAction, + SingleObservation, +) +from pydantic import BaseModel + + +class ScriptedAgent(CoordinatedAgent): + def __init__(self): + self.ready = threading.Event() + self.done = threading.Event() + self.seen = [] + + def run(self, adapter): + self.ready.set() + obs = adapter.get_observation() + self.seen.append(obs) + obs2 = adapter.execute("a0") + self.seen.append(obs2) + adapter.execute(None) + self.done.set() + + +def shutdown(coord: AgentCoordinator, timeout: float = 2.0): + coord.close() + if hasattr(coord, "join"): + coord.join(timeout=timeout) + + +def test_basic_handshake(): + internal = ScriptedAgent() + coord = AgentCoordinator("basic", internal) + coord.start(task="", context={}, actions=[]) + + assert internal.ready.wait(timeout=1.0) + + act = coord.react("obs0") + assert act == "a0" + + act = coord.react("obs1") + assert act is None + + shutdown(coord) + assert internal.done.wait(timeout=1.0) + + assert internal.seen[0] == "obs0" + assert len(internal.seen) == 2 + assert internal.seen[1] in ("obs1", None) + + +class TermAgent(CoordinatedAgent): + def __init__(self): + self.started = threading.Event() + self.done = threading.Event() + + def run(self, adapter): + self.started.set() + obs = adapter.get_observation() + if obs is None: + self.done.set() + return + adapter.execute("go") + obs2 = adapter.get_observation() + assert obs2 is None + adapter.execute(None) + self.done.set() + + +def test_terminal_observation_unblocks(): + internal = TermAgent() + coord = AgentCoordinator("term", internal) + coord.start(task="", context={}, actions=[]) + + assert internal.started.wait(timeout=1.0) + + act = coord.react("init") + assert act == "go" + + act = coord.react(None) + assert act is None + + shutdown(coord) + assert internal.done.wait(timeout=1.0) + + +def test_parallel_execute_threads_return_parallel_action(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + start = threading.Event() + + def _call(action): + start.wait() + coord.execute(action) + + t1 = threading.Thread(target=_call, args=(action_a,)) + t2 = threading.Thread(target=_call, args=(action_b,)) + t1.start() + t2.start() + start.set() + + deadline = time.time() + 1.0 + with coord._condition: + while len(coord._pending_actions) < 2 and time.time() < deadline: + coord._condition.wait(timeout=0.05) + + act = coord.react("obs") + assert isinstance(act, ParallelAction) + assert {a.id for a in act.actions} == {action_a.id, action_b.id} + + coord.close() + t1.join(timeout=1.0) + t2.join(timeout=1.0) + + +def test_no_accumulation_without_window(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + start = threading.Event() + + def _call(action, delay=0.0): + start.wait() + if delay: + time.sleep(delay) + coord.execute(action) + + t1 = threading.Thread(target=_call, args=(action_a,)) + t2 = threading.Thread(target=_call, args=(action_b, 0.5)) + t1.start() + t2.start() + start.set() + + deadline = time.time() + 1.0 + with coord._condition: + while len(coord._pending_actions) < 1 and time.time() < deadline: + coord._condition.wait(timeout=0.05) + + act = coord.react("obs") + assert isinstance(act, SingleAction) + assert act.id == action_a.id + + act2 = coord.react("obs2") + assert isinstance(act2, SingleAction) + assert act2.id == action_b.id + + coord.close() + t1.join(timeout=1.0) + t2.join(timeout=1.0) + + +def test_accumulation_window_batches_actions(): + coord = _coordinator(accumulate_window_seconds=0.3) + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + start = threading.Event() + + def _call(action, delay=0.0): + start.wait() + if delay: + time.sleep(delay) + coord.execute(action) + + t1 = threading.Thread(target=_call, args=(action_a,)) + t2 = threading.Thread(target=_call, args=(action_b, 0.1)) + t1.start() + t2.start() + start.set() + + deadline = time.time() + 1.0 + with coord._condition: + while len(coord._pending_actions) < 1 and time.time() < deadline: + coord._condition.wait(timeout=0.05) + + act = coord.react("obs") + assert isinstance(act, ParallelAction) + assert {a.id for a in act.actions} == {action_a.id, action_b.id} + + coord.close() + t1.join(timeout=1.0) + t2.join(timeout=1.0) + + +def test_execute_returns_observation_for_action(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + start = threading.Event() + results = {} + + def _call(action): + start.wait() + results[action.name] = coord.execute(action) + + t1 = threading.Thread(target=_call, args=(action_a,)) + t2 = threading.Thread(target=_call, args=(action_b,)) + t1.start() + t2.start() + start.set() + + deadline = time.time() + 1.0 + with coord._condition: + while len(coord._pending_actions) < 2 and time.time() < deadline: + coord._condition.wait(timeout=0.05) + + act = coord.react( + MultiObservation( + observations=[ + SingleObservation(invoking_actions=[action_a], result="ra"), + SingleObservation(invoking_actions=[action_b], result="rb"), + ] + ) + ) + assert isinstance(act, ParallelAction) + + t1.join(timeout=1.0) + t2.join(timeout=1.0) + coord.close() + + assert results["tool.a"].result == "ra" + assert results["tool.b"].result == "rb" + + +class DummyArgs(BaseModel): + value: int + + +def _action(name: str, value: int) -> SingleAction: + return SingleAction(name=name, arguments=DummyArgs(value=value)) + + +def _coordinator(accumulate_window_seconds: float | None = None) -> AgentCoordinator: + class NoopAgent(CoordinatedAgent): + def run(self, adapter): + return None + + return AgentCoordinator( + "rewire", + NoopAgent(), + accumulate_window_seconds=accumulate_window_seconds, + ) + + +def test_rewire_observation_assigns_by_order(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + coord._last_actions = [action_a, action_b] + + observation = MultiObservation( + observations=[ + SingleObservation(result="ra"), + SingleObservation(result="rb"), + ] + ) + rewired = coord._rewire_observation(observation) + + assert rewired.observations[0].invoking_actions == [action_a] + assert rewired.observations[1].invoking_actions == [action_b] + + +def test_rewire_single_observation_attaches_all_actions(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + coord._last_actions = [action_a, action_b] + + observation = SingleObservation(result="combined") + rewired = coord._rewire_observation(observation) + + assert rewired.invoking_actions == [action_a, action_b] + + +def test_rewire_preserves_existing_invoking_actions(): + coord = _coordinator() + action_a = _action("tool.a", 1) + action_b = _action("tool.b", 2) + coord._last_actions = [action_a, action_b] + + observation = MultiObservation( + observations=[ + SingleObservation(result="rb", invoking_actions=[action_b]), + SingleObservation(result="ra"), + ] + ) + rewired = coord._rewire_observation(observation) + + assert rewired.observations[0].invoking_actions == [action_b] + assert rewired.observations[1].invoking_actions == [action_a] diff --git a/labs/AgentStream/exgentic/tests/test_env_loading.py b/labs/AgentStream/exgentic/tests/test_env_loading.py new file mode 100644 index 00000000..db11c9d6 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/test_env_loading.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +import importlib +import os +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + + +def test_dotenv_loads_settings_and_env_vars() -> None: + with TemporaryDirectory() as tmpdir: + env_path = Path(tmpdir) / ".env" + env_path.write_text( + "EXGENTIC_LOG_LEVEL=DEBUG\nWATSONX_API_KEY=abc123\n" # pragma: allowlist secret + ) + + with patch.dict(os.environ, {"EXGENTIC_DOTENV_PATH": str(env_path)}, clear=False): + os.environ.pop("EXGENTIC_LOG_LEVEL", None) + os.environ.pop("WATSONX_API_KEY", None) + + sys.modules.pop("exgentic.utils.settings", None) + settings_module = importlib.import_module("exgentic.utils.settings") + + settings_module.get_settings.cache_clear() + settings = settings_module.get_settings() + + assert settings.log_level == "DEBUG" + assert os.environ.get("WATSONX_API_KEY") == "abc123" + assert settings.dotenv_path == str(env_path) diff --git a/labs/AgentStream/exgentic/tests/test_integrations_functions.py b/labs/AgentStream/exgentic/tests/test_integrations_functions.py new file mode 100644 index 00000000..dc65847e --- /dev/null +++ b/labs/AgentStream/exgentic/tests/test_integrations_functions.py @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import pytest +from exgentic.adapters.actions.functions import action_type_to_function, bind_arguments +from exgentic.core.types import Action, ActionType, SingleAction, SingleObservation +from pydantic import BaseModel + + +def test_bind(): + class Args(BaseModel): + arg1: int + arg2: int + arg3: int + arg4: int + + # normal flow + expected = {"arg1": 1, "arg2": 2, "arg3": 3, "arg4": 4} + res = bind_arguments(cls=Args, args=[1, 2], kwargs={"arg3": 3, "arg4": 4}) + assert res == expected + + # missing positional arguments + expected = {"arg1": 1, "arg3": 3, "arg4": 4} + res = bind_arguments(cls=Args, args=[1], kwargs={"arg3": 3, "arg4": 4}) + assert res == expected + + # no positional arguments + expected = {"arg3": 3, "arg4": 4} + res = bind_arguments(cls=Args, args=[], kwargs={"arg3": 3, "arg4": 4}) + assert res == expected + + # missing keyword arguments + expected = {"arg1": 1, "arg2": 2, "arg4": 4} + res = bind_arguments(cls=Args, args=[1, 2], kwargs={"arg4": 4}) + assert res == expected + + # no keyword arguments + expected = {"arg1": 1, "arg2": 2} + res = bind_arguments(cls=Args, args=[1, 2], kwargs={}) + assert res == expected + + # duplicates + with pytest.raises(TypeError): + bind_arguments(cls=Args, args=[1, 2], kwargs={"arg2": 2, "arg3": 3}) + + # too many args + with pytest.raises(TypeError): + bind_arguments(cls=Args, args=[1, 2, 3, 4, 5], kwargs={}) + + +def test_action_type_to_function(): + def internal_function(action: Action): + return SingleObservation(result=action) + + class MyArgs(BaseModel): + arg1: int + arg2: int + arg3: int = 0 + arg4: int + + class MyAction(SingleAction): + name: str = "my_action" + arguments: MyArgs + + action_type = ActionType(name="my_action", description="my description", cls=MyAction) + + function = action_type_to_function(action_type, internal_function) + + expected_arguments = MyArgs(arg1=1, arg2=2, arg3=3, arg4=4) + action = function(1, 2, 3, 4) + assert action.arguments == expected_arguments + + expected_arguments = MyArgs(arg1=1, arg2=2, arg3=3, arg4=4) + action = function(1, 2, 3, arg4=4) + assert action.arguments == expected_arguments + + expected_arguments = MyArgs(arg1=1, arg2=2, arg4=4) + action = function(1, 2, arg4=4) + assert action.arguments == expected_arguments + + """invalid cases allows only by model_construct""" + # # missing required argument + # with pytest.raises(ValidationError): + # action = function(1,arg4=4) + + # # non existing argname + # with pytest.raises(ValidationError): + # action = function(1,2,3,arg4=4,arg5=5) + + # with pytest.raises(ValidationError): + # action = function(1,2,3, arg5=5) diff --git a/labs/AgentStream/exgentic/tests/utils/test_cost.py b/labs/AgentStream/exgentic/tests/utils/test_cost.py new file mode 100644 index 00000000..03e4340c --- /dev/null +++ b/labs/AgentStream/exgentic/tests/utils/test_cost.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +import os +import sys + +import pytest + +# Make package importable +sys.path.insert(0, os.path.abspath("src")) +from exgentic.utils.cost import TokensCost, litellm_tokens_cost # noqa: E402 + +# All models mentioned across examples and scripts +EXAMPLE_MODELS = [ + "watsonx/meta-llama/llama-3-3-70b-instruct", + "watsonx/meta-llama/llama-3-2-90b-vision-instruct", + "watsonx/openai/gpt-oss-120b", + "openai/Azure/gpt-4.1", + "openrouter/openai/gpt-oss-120b", +] + + +@pytest.mark.parametrize( + "name,expected", + [ + ( + "watsonx/meta-llama/llama-3-3-70b-instruct", + TokensCost( + input_cost=7.099999999999999e-05, + output_cost=7.099999999999999e-05, + total_cost=0.00014199999999999998, + ), + ), + ( + "watsonx/meta-llama/llama-3-2-90b-vision-instruct", + TokensCost( + input_cost=0.00019999999999999998, + output_cost=0.00019999999999999998, + total_cost=0.00039999999999999996, + ), + ), + ( + "watsonx/openai/gpt-oss-120b", + TokensCost( + input_cost=1.4999999999999999e-05, + output_cost=5.9999999999999995e-05, + total_cost=7.5e-05, + ), + ), + ( + "openai/Azure/gpt-4.1", + TokensCost( + input_cost=0.00019999999999999998, + output_cost=0.0007999999999999999, + total_cost=0.001, + ), + ), + ( + "openai/GCP/gemini-2.5-flash", + TokensCost( + input_cost=2.9999999999999997e-05, + output_cost=0.00025, + total_cost=0.00028, + ), + ), + ( + "openai/GCP/claude-haiku-4-5-20251001", + TokensCost( + input_cost=9.999999999999999e-05, + output_cost=0.0005, + total_cost=0.0006000000000000001, + ), + ), + ( + "openrouter/openai/gpt-oss-120b", + TokensCost( + input_cost=1.8e-05, + output_cost=7.999999999999999e-05, + total_cost=9.8e-05, + ), + ), + ], +) +def test_cost_per_token(name, expected): + cost = litellm_tokens_cost(model_name=name, input_tokens=100, output_tokens=100) + print(name, cost) + assert cost == expected diff --git a/labs/AgentStream/exgentic/tests/utils/test_litellm_cache_settings.py b/labs/AgentStream/exgentic/tests/utils/test_litellm_cache_settings.py new file mode 100644 index 00000000..458c83b1 --- /dev/null +++ b/labs/AgentStream/exgentic/tests/utils/test_litellm_cache_settings.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026, The Exgentic organization and its contributors. + +from __future__ import annotations + +from exgentic.core.context import run_scope, try_get_context +from exgentic.core.types import RunConfig +from exgentic.integrations.litellm.cache_utils import build_litellm_cache +from exgentic.utils.settings import ExgenticSettings, resolve_cache_path + + +def test_resolve_cache_path_uses_base_dir_for_relative_paths() -> None: + assert resolve_cache_path(".exgentic", ".litellm_cache") == ".exgentic/.litellm_cache" + + +def test_resolve_cache_path_keeps_absolute_paths() -> None: + assert resolve_cache_path(".exgentic", "/tmp/litellm") == "/tmp/litellm" + + +def test_build_litellm_cache_resolves_relative_path_under_cache_dir(tmp_path) -> None: + base_dir = tmp_path / "cache-root" + settings = ExgenticSettings( + cache_dir=str(base_dir), + litellm_cache_dir=".litellm_cache", + ) + cache = build_litellm_cache(settings) + assert cache.cache.disk_cache.directory == str(base_dir / ".litellm_cache") + + +def test_run_config_to_session_config_preserves_cache_dir() -> None: + run_config = RunConfig( + benchmark="gsm8k", + agent="tool_calling", + cache_dir="/tmp/exgentic-cache", + ) + session_config = run_config.to_session_config("task-1") + assert session_config.cache_dir == "/tmp/exgentic-cache" + + +def test_run_context_sets_and_restores_cache_env(monkeypatch) -> None: + monkeypatch.delenv("EXGENTIC_CTX_CACHE_DIR", raising=False) + with run_scope( + run_id="cache-test", + output_dir="./outputs", + cache_dir="./cache", + ): + ctx = try_get_context() + assert ctx is not None + # Context resolves relative paths to absolute. + assert ctx.cache_dir.endswith("/cache") + assert not ctx.cache_dir.startswith(".") diff --git a/labs/AgentStream/exgentic/uv.lock b/labs/AgentStream/exgentic/uv.lock new file mode 100644 index 00000000..bdb2f2e5 --- /dev/null +++ b/labs/AgentStream/exgentic/uv.lock @@ -0,0 +1,4101 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[manifest] +overrides = [{ name = "rich", specifier = ">=13.9.4,<14" }] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "detect-secrets" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/67/382a863fff94eae5a0cf05542179169a1c49a4c8784a9480621e2066ca7d/detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a", size = 97351, upload-time = "2024-05-06T17:46:19.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5e/4f5fe4b89fde1dc3ed0eb51bd4ce4c0bca406246673d370ea2ad0c58d747/detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060", size = 120341, upload-time = "2024-05-06T17:46:16.628Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, +] + +[[package]] +name = "exgentic" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "diskcache" }, + { name = "filelock" }, + { name = "json-schema-to-pydantic" }, + { name = "litellm" }, + { name = "mcp" }, + { name = "nicegui" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +amem = [ + { name = "scikit-learn" }, + { name = "sentence-transformers" }, +] +analysis = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy" }, + { name = "statsmodels" }, +] +dev = [ + { name = "codespell" }, + { name = "detect-secrets" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] +otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1.7,<9" }, + { name = "cloudpickle", specifier = ">=3,<4" }, + { name = "codespell", marker = "extra == 'dev'", specifier = ">=2.0.0,<3" }, + { name = "detect-secrets", marker = "extra == 'dev'", specifier = ">=1.0.0,<2" }, + { name = "diskcache", specifier = ">=5,<6" }, + { name = "filelock", specifier = ">=3,<4" }, + { name = "json-schema-to-pydantic", specifier = ">=0.4,<1" }, + { name = "litellm", specifier = ">=1.65.0,!=1.82.7,!=1.82.8,<2" }, + { name = "matplotlib", marker = "extra == 'analysis'", specifier = ">=3,<4" }, + { name = "mcp", specifier = ">=1.24,<2" }, + { name = "nicegui", specifier = ">=3,<4" }, + { name = "numpy", marker = "extra == 'analysis'", specifier = ">=2,<3" }, + { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1,<2" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'otel'", specifier = ">=1,<2" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'", specifier = ">=1,<2" }, + { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1,<2" }, + { name = "opentelemetry-semantic-conventions-ai", marker = "extra == 'otel'", specifier = ">=0.4.0,<1" }, + { name = "pandas", marker = "extra == 'analysis'", specifier = ">=3,<4" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0,<5" }, + { name = "pydantic", specifier = ">=2.9.2,<3" }, + { name = "pydantic-settings", specifier = ">=2,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0,<10" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0,<2" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.0.0,<4" }, + { name = "python-dotenv", specifier = ">=1,<2" }, + { name = "rich", specifier = ">=13,<14" }, + { name = "rich-click", specifier = ">=1,<2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0,<1" }, + { name = "scikit-learn", marker = "extra == 'amem'", specifier = ">=1,<2" }, + { name = "scipy", marker = "extra == 'analysis'", specifier = ">=1,<2" }, + { name = "sentence-transformers", marker = "extra == 'amem'", specifier = ">=3,<5" }, + { name = "statsmodels", marker = "extra == 'analysis'", specifier = ">=0.14,<1" }, + { name = "typing-extensions", specifier = ">=4,<5" }, +] +provides-extras = ["amem", "analysis", "dev", "otel"] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.1,<10" }] + +[[package]] +name = "fastapi" +version = "0.122.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b", size = 346436, upload-time = "2025-11-24T19:17:47.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json-schema-to-pydantic" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/b1/fbcbda5e23ecb5ff987076d9caccb11d182a249cedd5424236aaca4e65b8/json_schema_to_pydantic-0.4.7.tar.gz", hash = "sha256:a6384825fee7609715641a1e5095ddf41aa3bdd06cd91d4a6b6d88a3dfcad920", size = 50054, upload-time = "2025-11-03T19:28:30.65Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/af/f64664df8d4d52a371195e16c6ff4419968ee3eb6f96b21e596e3623dcf3/json_schema_to_pydantic-0.4.7-py3-none-any.whl", hash = "sha256:e329506e42b63f9a0ae0a17f7082ec7fc7cb3326138ec5c24904e0af5a660e63", size = 14709, upload-time = "2025-11-03T19:28:29.168Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "litellm" +version = "1.82.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/12/010a86643f12ac0b004032d5927c260094299a84ed38b5ed20a8f8c7e3c4/litellm-1.82.2.tar.gz", hash = "sha256:f5f4c4049f344a88bf80b2e421bb927807687c99624515d7ff4152d533ec9dcb", size = 17353218, upload-time = "2026-03-13T21:24:24.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/e4/87e3ca82a8bf6e6bfffb42a539a1350dd6ced1b7169397bd439ba56fde10/litellm-1.82.2-py3-none-any.whl", hash = "sha256:641ed024774fa3d5b4dd9347f0efb1e31fa422fba2a6500aabedee085d1194cb", size = 15524224, upload-time = "2026-03-13T21:24:21.288Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdown2" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652, upload-time = "2025-07-27T16:16:24.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954, upload-time = "2025-07-27T16:16:23.026Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nicegui" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "certifi" }, + { name = "docutils" }, + { name = "fastapi" }, + { name = "h11" }, + { name = "httpx" }, + { name = "ifaddr" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markdown2" }, + { name = "orjson", marker = "platform_machine != 'i386' and platform_machine != 'i686' and platform_python_implementation != 'PyPy'" }, + { name = "pydantic-core" }, + { name = "pygments" }, + { name = "python-engineio" }, + { name = "python-multipart" }, + { name = "python-socketio", extra = ["asyncio-client"] }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/76/0fe2a54f0ba6141747fc1413ffd1657dd809bf5c8105f6832628d9b5f86e/nicegui-3.7.1.tar.gz", hash = "sha256:dc6ef68083ce15d92848e91908eb5313962dc0cd8a3350536b18c425ad5d5ca5", size = 21343701, upload-time = "2026-02-05T14:39:21.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a8/91f47b20368bdfb4c818416df0d5cbd867a07364d05cff56d095d248b81e/nicegui-3.7.1-py3-none-any.whl", hash = "sha256:202c39b415eb6aa08cc74b29cadc8b9357c5ba1bf67840c989437caf111c54b4", size = 22008333, upload-time = "2026-02-05T14:39:17.209Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "openai" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/75/455c15f8360b475dd31101a87eab316420388486f7941bf019cbf4e63d5b/opentelemetry_semantic_conventions_ai-0.4.15.tar.gz", hash = "sha256:12de172d1e11d21c6e82bbf578c7e8a713589a7fda76af9ed785632564a28b81", size = 18595, upload-time = "2026-03-02T15:36:50.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/49/819fb212386f77cfd93f81bd916d674f0e735f87c8ac2262ed14e3b852c2/opentelemetry_semantic_conventions_ai-0.4.15-py3-none-any.whl", hash = "sha256:011461f1fba30f27035c49ab3b8344367adc72da0a6c8d3c7428303c6779edc9", size = 5999, upload-time = "2026-03-02T15:36:51.44Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, +] + +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815, upload-time = "2024-07-28T19:59:01.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643, upload-time = "2024-07-28T19:58:59.335Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-engineio" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "python-socketio" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, +] + +[package.optional-dependencies] +asyncio-client = [ + { name = "aiohttp" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, + { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rich-click" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838, upload-time = "2026-01-31T04:29:27.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pillow" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/84/b30d1b29ff58cfdff423e36a50efd622c8e31d7039b1a0d5e72066620da1/sentence_transformers-4.1.0.tar.gz", hash = "sha256:f125ffd1c727533e0eca5d4567de72f84728de8f7482834de442fd90c2c3d50b", size = 272420, upload-time = "2025-04-15T13:46:13.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/2d/1151b371f28caae565ad384fdc38198f1165571870217aedda230b9d7497/sentence_transformers-4.1.0-py3-none-any.whl", hash = "sha256:382a7f6be1244a100ce40495fb7523dbe8d71b3c10b299f81e6b735092b3b8ca", size = 345695, upload-time = "2025-04-15T13:46:12.44Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/labs/AgentStream/exgentic/whitesource.config b/labs/AgentStream/exgentic/whitesource.config new file mode 100644 index 00000000..8abafd8c --- /dev/null +++ b/labs/AgentStream/exgentic/whitesource.config @@ -0,0 +1,2 @@ +python.path=python3.12 +python.invokePipAsModule=true diff --git a/labs/AgentStream/figs/evaluation_compare.png b/labs/AgentStream/figs/evaluation_compare.png new file mode 100644 index 0000000000000000000000000000000000000000..eb09128f48a2564e112b0ba1ed1e4d92c8818018 GIT binary patch literal 311885 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
Vhx3jWw6QgP+p0S=dgT(6W_c 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≪UnWdr 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%0AU@Ozu$IjTjCd!+RC|;}Hbh+;Dysv49tn>$d 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 z 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 R<}>g=b!Dwx_jj0{{~!4n?T`Qf literal 0 HcmV?d00001 From 61470f03ba58596fcdd9720269bcf0a417113a43 Mon Sep 17 00:00:00 2001 From: Dawson Date: Tue, 18 Aug 2026 16:16:04 +0800 Subject: [PATCH 2/4] fix(labs): align AgentStream with Sico conventions --- CHANGELOG.md | 3 +- CONTRIBUTING.md | 17 +- README.md | 2 + labs/AgentStream/README.md | 10 +- .../exgentic/.github/workflows/pre-commit.yml | 46 ------ .../.github/workflows/publish-pypi.yml | 59 ------- .../exgentic/.github/workflows/tests.yml | 64 -------- labs/AgentStream/exgentic/.whitesource | 9 -- labs/AgentStream/exgentic/CODE_OF_CONDUCT.md | 128 --------------- labs/AgentStream/exgentic/CONTRIBUTING.md | 92 +++-------- labs/AgentStream/exgentic/DCO.txt | 34 ---- labs/AgentStream/exgentic/DEVELOPMENT.md | 19 ++- labs/AgentStream/exgentic/README.md | 28 +++- labs/AgentStream/exgentic/SECURITY.md | 152 ------------------ labs/AgentStream/exgentic/docs/README.md | 10 +- labs/AgentStream/exgentic/docs/releasing.md | 119 -------------- labs/AgentStream/exgentic/pyproject.toml | 13 +- labs/AgentStream/exgentic/renovate.json | 13 -- labs/AgentStream/exgentic/scripts/release.sh | 76 --------- labs/AgentStream/exgentic/uv.lock | 1 + labs/AgentStream/exgentic/whitesource.config | 2 - 21 files changed, 82 insertions(+), 815 deletions(-) delete mode 100644 labs/AgentStream/exgentic/.github/workflows/pre-commit.yml delete mode 100644 labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml delete mode 100644 labs/AgentStream/exgentic/.github/workflows/tests.yml delete mode 100644 labs/AgentStream/exgentic/.whitesource delete mode 100644 labs/AgentStream/exgentic/CODE_OF_CONDUCT.md delete mode 100644 labs/AgentStream/exgentic/DCO.txt delete mode 100644 labs/AgentStream/exgentic/SECURITY.md delete mode 100644 labs/AgentStream/exgentic/docs/releasing.md delete mode 100644 labs/AgentStream/exgentic/renovate.json delete mode 100644 labs/AgentStream/exgentic/scripts/release.sh delete mode 100644 labs/AgentStream/exgentic/whitesource.config diff --git a/CHANGELOG.md b/CHANGELOG.md index 545112d8..a24c533b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,8 @@ Guidelines for editors: ### Added -- **AgentStream:** add a streaming evaluation framework for self-evolving LLM agents under `labs/AgentStream`. +- **AgentStream:** add an experimental streaming evaluation framework for + self-evolving LLM agents under `labs/AgentStream` (#72). ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ce381a3..ed234c3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,10 @@ # Contributing to Sico -Thanks for your interest in contributing! This project is MIT-licensed: by -submitting a pull request you agree that your contribution will be distributed -under the same terms. +Thanks for your interest in contributing! Sico is MIT-licensed, except for +[`labs/AgentStream`](labs/AgentStream), which is separately licensed under the +Apache License 2.0. By submitting a pull request, you agree that your +contribution will be distributed under the license applicable to the files you +modify. ## Start here @@ -53,9 +55,12 @@ service-specific commands, and troubleshooting notes. ## License headers -Every new source file (Go, Python, TypeScript, JavaScript, proto, shell, YAML, -Dockerfile, ...) must carry the MIT header. The pre-commit hook adds it -automatically. Generated files are intentionally excluded; see the +Every new source file outside `labs/AgentStream` (Go, Python, TypeScript, +JavaScript, proto, shell, YAML, Dockerfile, ...) must carry the MIT header. The +pre-commit hook adds it automatically. Files under `labs/AgentStream` instead +use the Apache-2.0 SPDX header described in the +[AgentStream contribution guide](labs/AgentStream/exgentic/CONTRIBUTING.md). +Generated files are intentionally excluded; see the [Development guide](docs/development.md#license-headers) and [pre-commit configuration](.pre-commit-config.yaml) for the exact ignore list. diff --git a/README.md b/README.md index 36807707..563b7510 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ sico/ ├── proto/ # Protobuf definitions shared by all services ├── sandbox/ # Sandbox runtimes (Android emulator, ...) ├── examples/ # Runnable workflow examples (auth, LLM Hub, conversation, sandbox, ...) +├── labs/ +│ └── AgentStream/ # Experimental streaming evaluation for self-evolving LLM agents ├── deploy/ │ ├── docker/ # docker-compose stack │ └── kind/ # Kind + Helm setup diff --git a/labs/AgentStream/README.md b/labs/AgentStream/README.md index a304f3a2..304f324d 100644 --- a/labs/AgentStream/README.md +++ b/labs/AgentStream/README.md @@ -50,7 +50,15 @@ Overall, we advocate that self-evolving agents should be evaluated under realist ## ⚡️ Getting Started -AgentStream is built on the [`Exgentic`](./exgentic) framework, which is bundled in this repository. The five self-evolving agents live under [`exgentic/src/exgentic/agents`](./exgentic/src/exgentic/agents), and the benchmarks are orchestrated through `exgentic`'s installation and runner infrastructure. +AgentStream is built on a locally adapted snapshot of the +[Exgentic](https://github.com/Exgentic/exgentic) framework, bundled under +[`exgentic`](./exgentic). This copy adds the self-evolving agents and streaming +experiment runners used by AgentStream. AgentStream-specific changes to the +snapshot are maintained in Sico, and the bundled package is not published +independently from this repository. The five self-evolving agents live under +[`exgentic/src/exgentic/agents`](./exgentic/src/exgentic/agents), and the +benchmarks are orchestrated through `exgentic`'s installation and runner +infrastructure. ### 1. Requirements diff --git a/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml b/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml deleted file mode 100644 index b76a8097..00000000 --- a/labs/AgentStream/exgentic/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Pre-commit Checks - -on: - push: - branches: [ main, master, develop ] - pull_request: - branches: [ main, master, develop ] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Cache pre-commit hooks - uses: actions/cache@v4 - with: - path: ~/.cache/pre-commit - key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - pre-commit- - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pre-commit - - - name: Run pre-commit hooks - run: pre-commit run --all-files --show-diff-on-failure - - - name: Upload pre-commit results - if: failure() - uses: actions/upload-artifact@v4 - with: - name: pre-commit-results - path: | - **/*.log - .pre-commit-config.yaml - -# Made with Bob diff --git a/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml b/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml deleted file mode 100644 index 9d5d203e..00000000 --- a/labs/AgentStream/exgentic/.github/workflows/publish-pypi.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Publish to PyPI - -on: - push: - tags: - - "v*" - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Ensure tag commit is on main - run: | - git fetch origin main - tag_commit="$(git rev-list -n 1 "$GITHUB_REF_NAME")" - git merge-base --is-ancestor "$tag_commit" origin/main - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Build distributions - run: | - python -m pip install --upgrade pip - python -m pip install build twine - python -m build - python -m twine check dist/* - - - name: Upload distributions - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - publish: - needs: build - runs-on: ubuntu-latest - permissions: - id-token: write - environment: - name: pypi - url: https://pypi.org/project/exgentic/ - steps: - - name: Download distributions - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/labs/AgentStream/exgentic/.github/workflows/tests.yml b/labs/AgentStream/exgentic/.github/workflows/tests.yml deleted file mode 100644 index 9316f916..00000000 --- a/labs/AgentStream/exgentic/.github/workflows/tests.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Tests - -on: - push: - branches: [main, master, develop] - pull_request: - branches: [main, master, develop] - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11", "3.12"] - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --frozen --extra dev --extra analysis - - - name: Run core tests - run: uv run --frozen pytest tests -v --ignore=tests/integrations --ignore=tests/adapters/runners --tb=short - - - name: Run runner tests - run: uv run --frozen pytest tests/adapters/runners -v --tb=short -p no:faulthandler - - - name: Upload test results - if: failure() - uses: actions/upload-artifact@v4 - with: - name: test-results-${{ matrix.python-version }} - path: "**/*.log" - - docker-integration: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: "3.12" - - - name: Install dependencies - run: uv sync --frozen --extra dev --extra analysis - - - name: Run Docker integration tests - run: uv run --frozen pytest tests/environment/test_manager.py -v -k "Integration" --tb=short diff --git a/labs/AgentStream/exgentic/.whitesource b/labs/AgentStream/exgentic/.whitesource deleted file mode 100644 index 5e1a3914..00000000 --- a/labs/AgentStream/exgentic/.whitesource +++ /dev/null @@ -1,9 +0,0 @@ -{ - "settingsInheritedFrom": "whitesource-config/whitesource-config@master", - "scanSettingsSAST": { - "enableScan": true - }, - "scanSettings": { - "configMode": "LOCAL" - } -} diff --git a/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md b/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md deleted file mode 100644 index c8e52a2e..00000000 --- a/labs/AgentStream/exgentic/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socioeconomic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible. - -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/labs/AgentStream/exgentic/CONTRIBUTING.md b/labs/AgentStream/exgentic/CONTRIBUTING.md index 2a03a7c2..d3939f57 100644 --- a/labs/AgentStream/exgentic/CONTRIBUTING.md +++ b/labs/AgentStream/exgentic/CONTRIBUTING.md @@ -1,78 +1,32 @@ -# How to contribute to Exgentic +# Contributing to the bundled Exgentic component -Thank you for your interest in contributing! +This directory contains a locally adapted snapshot of +[Exgentic](https://github.com/Exgentic/exgentic) used by AgentStream. Changes +specific to this bundled copy are contributed through the +[Sico repository](https://github.com/microsoft/Sico), not through the upstream +Exgentic repository. -## Development Setup +## Contribution process -```bash -# Install dependencies using the pinned lock file — never plain `uv sync` -uv sync --frozen --extra dev --extra analysis +Follow Sico's root [contribution guide](../../../CONTRIBUTING.md) for the fork, +branch, commit, pull-request, review, code-of-conduct, and security-reporting +processes. For local setup and component-specific checks, see +[DEVELOPMENT.md](./DEVELOPMENT.md). -# To intentionally upgrade a specific package: -uv lock --upgrade-package -# Review the uv.lock diff carefully before committing -``` - -> **Security note:** Always use `uv sync --frozen` locally. Running plain `uv sync` may silently -> upgrade packages and introduce untested or malicious versions. Dependency upgrades should be -> explicit, reviewed, and go through a PR. - -## How to Contribute - -1. Fork the [repository](https://github.com/exgentic/exgentic). -2. Create a new branch for your changes. -3. Sign your commits using the `-s` flag (see [Legal](#legal)) -4. Submit a pull request to the `main` branch with a clear title and description. -Reference any issues fixed, for example `Fixes #1234`. -Ensure your PR title follows [semantic commit conventions](https://www.conventionalcommits.org/). -5. A maintainer will review your PR and may request changes. - -## Legal - -### License - -This project is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE). - -Each source code file must include the following SPDX headers at the top of the file: - -**For Python files:** -```python -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2025, The Exgentic organization and its contributors. - -"""Module docstring here.""" -import ... -``` +If a change applies to the original Exgentic project rather than this +AgentStream-specific copy, contribute it to the +[upstream repository](https://github.com/Exgentic/exgentic) separately. -**For other file types:** Use the appropriate comment syntax for that language. +## License -### Developer Certificate of Origin (DCO) +The contents of `labs/AgentStream`, including this bundled component, are +licensed under the Apache License 2.0; see [LICENSE](./LICENSE) and the +[AgentStream license](../LICENSE). This is an exception to Sico's root MIT +license. -We require all commits to be **signed off** to indicate agreement with the [DCO](DCO.txt). +New source files in this directory must include the following SPDX identifier +using the appropriate comment syntax: -By signing off a commit, you certify: - -> “I have the right to submit this contribution under the Apache License, Version 2.0 (or the open source license indicated in the file), and understand this project and my contribution are public.” - -### How to sign off your commits - -The easiest way is to use the `-s` flag when committing: - -```bash -git commit -s -m "Fix: Correct spelling in README" -``` - -This uses your Git configuration. Make sure your name and email are set: -```bash -git config --global user.name "Your Name" -git config --global user.email "your.email@example.com" +```text +SPDX-License-Identifier: Apache-2.0 ``` - -Alternatively you can manually sign your commit by adding this line to the commit message: -``` -Signed-off-by: Your Name -``` - -## Development Environment Setup - -For detailed instructions on setting up your local development environment, see [DEVELOPMENT.md](./DEVELOPMENT.md). diff --git a/labs/AgentStream/exgentic/DCO.txt b/labs/AgentStream/exgentic/DCO.txt deleted file mode 100644 index 49b8cb05..00000000 --- a/labs/AgentStream/exgentic/DCO.txt +++ /dev/null @@ -1,34 +0,0 @@ -Developer Certificate of Origin -Version 1.1 - -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - - -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. diff --git a/labs/AgentStream/exgentic/DEVELOPMENT.md b/labs/AgentStream/exgentic/DEVELOPMENT.md index 814ea061..35cfbcef 100644 --- a/labs/AgentStream/exgentic/DEVELOPMENT.md +++ b/labs/AgentStream/exgentic/DEVELOPMENT.md @@ -5,9 +5,9 @@ This guide covers setting up exgentic for local development, editing, and debugg ## Setup ```bash -git clone https://github.com/Exgentic/exgentic.git -cd exgentic -uv sync +git clone https://github.com/microsoft/Sico.git +cd Sico/labs/AgentStream/exgentic +uv sync --frozen ``` ## Setup Benchmarks & Agents @@ -119,11 +119,10 @@ export EXGENTIC_OTEL_ENABLED=true See [`OTEL_SEMANTIC_CONVENTIONS.md`](./OTEL_SEMANTIC_CONVENTIONS.md) for details. -## Releases +## Packaging -- Release process guide: `docs/releasing.md` -- Benchmark adapter design guide: `docs/adding-benchmarks.md` -- Create and push a release tag: `scripts/release.sh 0.2.0 --push` -- After PyPI publish succeeds, create the GitHub Release manually: `gh release create v0.2.0 --generate-notes --title "v0.2.0"` -- Release versions come from Git tags via `hatch-vcs` -- PyPI publishing uses GitHub Actions Trusted Publishing +The bundled Exgentic component is installed from source for AgentStream and is +not published independently from Sico. Do not create Exgentic release tags or +publish this copy to PyPI from this repository. + +For benchmark adapter design guidance, see `docs/adding-benchmarks.md`. diff --git a/labs/AgentStream/exgentic/README.md b/labs/AgentStream/exgentic/README.md index 11de4d8b..ca235afd 100644 --- a/labs/AgentStream/exgentic/README.md +++ b/labs/AgentStream/exgentic/README.md @@ -1,5 +1,14 @@ Exgentic Banner +> [!NOTE] +> This directory contains a locally adapted snapshot of the upstream +> [Exgentic](https://github.com/Exgentic/exgentic) project used by +> [AgentStream](../README.md). AgentStream-specific changes to this copy are +> maintained in Sico, and it is not published as a separate Exgentic package +> from this repository. Report issues with this bundled copy in the +> [Sico issue tracker](https://github.com/microsoft/Sico/issues); report issues +> with the original project to the upstream repository. +

Evaluate any agent on any benchmark in the simplest way possible

@@ -24,9 +33,15 @@ Exgentic is a universal evaluation framework that enables standardized testing o ### Installation ```bash -uv tool install exgentic +git clone https://github.com/microsoft/Sico.git +cd Sico/labs/AgentStream/exgentic +uv sync --frozen +source .venv/bin/activate ``` +The commands below assume that this local environment is active. Alternatively, +prefix each command with `uv run`. + ### API Credentials ```bash @@ -71,11 +86,7 @@ exgentic evaluate --benchmark tau2 --agent tool_calling --subset retail --num-ta ### Python API -To use exgentic as a library, install it first: - -```bash -uv add exgentic # or: pip install exgentic -``` +After syncing the bundled environment above, use exgentic as a library: ```python from exgentic import evaluate @@ -246,4 +257,7 @@ Apache License 2.0 — see [LICENSE](LICENSE). ## Support -For questions and support, [open an issue](https://github.com/Exgentic/exgentic/issues) on GitHub. +For questions about this bundled copy, +[open an issue](https://github.com/microsoft/Sico/issues) in Sico. For the +original Exgentic project, use the +[upstream issue tracker](https://github.com/Exgentic/exgentic/issues). diff --git a/labs/AgentStream/exgentic/SECURITY.md b/labs/AgentStream/exgentic/SECURITY.md deleted file mode 100644 index db6c7c92..00000000 --- a/labs/AgentStream/exgentic/SECURITY.md +++ /dev/null @@ -1,152 +0,0 @@ -# Exgentic Security Policy & Responsible Disclosure - -## Security Policy - -This security policy applies to all public projects under the Exgentic organization on GitHub. We prioritize security and continuously work to safeguard our systems. However, vulnerabilities can still exist. If you identify a security issue, please report it to us so we can address it promptly. - -### Security/Bugfix Versions - -- Fixes are released either as part of the next minor version (e.g., 1.3.0 → 1.4.0) or as an on-demand patch version (e.g., 1.3.0 → 1.3.1) -- Security fixes are given priority and might be enough to cause a new version to be released - -## Reporting a Vulnerability - -We encourage responsible disclosure of security vulnerabilities. If you find something suspicious, we encourage and appreciate your report! - -### How to Report - -Use the "Report a vulnerability" button under the "Security" tab of the [repository](https://github.com/exgentic/exgentic/security). This creates a private communication channel between you and the maintainers. - -### Reporting Guidelines - -- Provide clear details to help us reproduce and fix the issue quickly -- Include steps to reproduce, potential impact, and any suggested fixes -- Your report will be kept confidential, and your details will not be shared without your consent - -### Response Timeline - -- We will acknowledge your report within 5 business days -- We will provide an estimated resolution timeline -- We will keep you updated on our progress - -### Disclosure Guidelines - -- Do not publicly disclose vulnerabilities until we have assessed, resolved, and notified affected users -- If you plan to present your research (e.g., at a conference or in a blog), share a draft with us at least 30 days in advance for review -- Avoid including: - - Data from any customer projects - - User/customer information - - Details about employees, contractors, or partners - -We appreciate your efforts in helping us maintain a secure platform and look forward to working together to resolve any issues responsibly. - -## Dependency Management & Supply Chain Security - -### Version Capping Policy - -All direct dependencies in `pyproject.toml` are capped at the next major version (e.g., `litellm>=1.65.0,<2`). This policy limits the blast radius of supply chain attacks by preventing automatic upgrades to arbitrary future versions. - -**Why we cap dependencies:** -- **Supply chain attack mitigation**: Malicious packages can be uploaded to PyPI at any time. By capping at major versions, we limit exposure to known version ranges. -- **Controlled upgrades**: Major version bumps require explicit review and testing before adoption. -- **Stability**: Prevents breaking changes from being automatically pulled in. - -**Enforcement:** -- A pre-commit hook (`enforce-dependency-caps`) validates that all dependencies have upper bounds. -- CI will fail if any direct dependency lacks an upper bound. -- The hook runs automatically on every commit and in CI. - -### Automated Dependency Updates via Renovate - -We use Renovate to keep dependencies up to date while maintaining security: - -**14-Day Release Age Gate:** -- Renovate is configured with `minimumReleaseAge: 14 days` for all Python dependencies. -- New package versions are not proposed until 2 weeks after their PyPI release. -- This reduces exposure to day-zero malicious uploads and gives the community time to identify compromised packages. - -**Major Version Bumps:** -- Renovate uses `rangeStrategy: "bump"` to update both `uv.lock` and the upper bounds in `pyproject.toml` when a new major version is stable. -- Major version PRs require careful review of breaking changes and thorough testing. - -### Reviewing Renovate PRs - -When reviewing Renovate PRs that update `uv.lock`: - -1. **Check the PR description** for the list of updated packages and their version changes. -2. **Review the lockfile diff** to understand what's changing: - ```bash - gh pr diff -- uv.lock - ``` -3. **Verify the release age**: Ensure the new version has been available for at least 14 days. -4. **Check for security advisories**: Look for any CVEs or security issues in the changelog. -5. **Review changelogs**: For major updates, read the package's changelog for breaking changes. -6. **Test thoroughly**: Run the full test suite and any relevant integration tests. - -### Lockfile Integrity - -A `uv-lock --locked` pre-commit hook (added in PR #65) ensures `uv.lock` stays in sync with `pyproject.toml`: -- The hook rejects commits where the lockfile is out of sync. -- This prevents accidental lockfile drift and ensures reproducible builds. -- If the hook fails, run `uv lock` to regenerate the lockfile, review the changes, and commit. - -### Incident Response: Malicious Package Detected - -If a malicious package version is discovered in our dependencies: - -1. **Immediate containment:** - ```bash - # Pin the malicious version as excluded in pyproject.toml - # Example: "litellm>=1.65.0,!=1.82.7,!=1.82.8,<2" - ``` - -2. **Rotate credentials:** - - Assume any secrets or credentials accessible to the compromised environment may be compromised. - - Rotate API keys, tokens, and passwords that were accessible during the infection window. - -3. **Clean infected environments:** - ```bash - # Remove all virtual environments - rm -rf .venv venv .exgentic/ - - # Reinstall with the patched dependency specification - uv sync - ``` - -4. **Audit for data exfiltration:** - - Review logs and network traffic for suspicious outbound connections. - - Check for unauthorized access to systems or data. - -5. **Update lockfile:** - ```bash - uv lock - git add uv.lock pyproject.toml - git commit -m "Pin malicious package version as excluded" - ``` - -6. **Notify the team** and document the incident. - -### CVE Scanning with uv audit - -The `uv audit` command scans dependencies for known CVEs: - -```bash -uv audit -``` - -**Current status:** -- `uv audit` is temporarily unavailable due to the litellm quarantine (versions 1.82.7 and 1.82.8 are excluded). -- Once the quarantine is lifted and a clean version is available, re-enable regular `uv audit` checks. -- Consider adding `uv audit` to CI once it's operational again. - -### Best Practices - -- **Never commit lockfiles without review**: Always inspect `uv.lock` diffs before committing. -- **Keep dependencies minimal**: Only add dependencies that are truly necessary. -- **Monitor security advisories**: Subscribe to security mailing lists for critical dependencies. -- **Test updates thoroughly**: Don't merge Renovate PRs without running tests. -- **Document exceptions**: If you must exclude a version (e.g., `!=1.82.8`), document why in a comment or commit message. - -## Known Vulnerabilities - -There are currently no known vulnerabilities. diff --git a/labs/AgentStream/exgentic/docs/README.md b/labs/AgentStream/exgentic/docs/README.md index 225c3f78..c917d4f9 100644 --- a/labs/AgentStream/exgentic/docs/README.md +++ b/labs/AgentStream/exgentic/docs/README.md @@ -31,16 +31,8 @@ Welcome to the Exgentic docs. Use the table below to find what you need. | [Quick Start](./observability/quickstart.md) | Set up OpenTelemetry tracing with Jaeger in five minutes | | [Semantic Conventions](./observability/semantic-conventions.md) | Full reference of every span and attribute Exgentic emits | -## Maintainers - -| Document | Description | -|----------|-------------| -| [Releasing](./releasing.md) | Cut a release, publish to PyPI, and create a GitHub Release | - ---- - ## Other resources - [README.md](../README.md) — project overview, quick start, CLI reference, and available benchmarks/agents -- [DEVELOPMENT.md](../DEVELOPMENT.md) — local setup, running tests, linting, and the release process +- [DEVELOPMENT.md](../DEVELOPMENT.md) — local setup, running tests, and linting - [CONTRIBUTING.md](../CONTRIBUTING.md) — contribution workflow, legal requirements, and PR guidelines diff --git a/labs/AgentStream/exgentic/docs/releasing.md b/labs/AgentStream/exgentic/docs/releasing.md deleted file mode 100644 index 9ef4c0a3..00000000 --- a/labs/AgentStream/exgentic/docs/releasing.md +++ /dev/null @@ -1,119 +0,0 @@ -# Releasing Exgentic - -**Related docs:** -[docs/](./README.md) · [DEVELOPMENT.md](../DEVELOPMENT.md) · [CONTRIBUTING.md](../CONTRIBUTING.md) - -## Release model - -Exgentic uses Git tags as the single source of truth for released versions. -There is no manually maintained version string in the source tree. - -- A release tag must look like `vX.Y.Z`, for example `v0.2.0` -- Package versions are derived from Git tags via `hatch-vcs` -- PyPI publishing runs from GitHub Actions only for pushed `v*` tags -- The publishing workflow verifies that the tagged commit is reachable from `main` -- GitHub Releases are created manually after PyPI publish succeeds - -## One-time repository setup - -1. In PyPI, create the `exgentic` project if it does not exist yet. -2. In PyPI project settings, add a Trusted Publisher for this GitHub repository. -3. Use these GitHub values when configuring the publisher: - - Owner: `Exgentic` - - Repository: `exgentic` - - Workflow name: `publish-pypi.yml` - - Environment name: `pypi` -4. In GitHub, keep the `pypi` environment enabled for this workflow if you want environment-level protections. - -See the official docs for the exact PyPI setup steps: -- https://docs.pypi.org/trusted-publishers/using-a-publisher/ -- https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ - -## Release steps - -1. Make sure the release commit is already merged to `main`. -2. Update local refs: - ```bash - git checkout main - git pull --ff-only origin main - ``` -3. Create the annotated tag: - ```bash - scripts/release.sh 0.2.0 - ``` -4. Push the tag: - ```bash - git push origin v0.2.0 - ``` - -Or do steps 3 and 4 in one command: - -```bash -scripts/release.sh 0.2.0 --push -``` - -5. After the PyPI workflow succeeds, create the GitHub Release manually: - ```bash - gh release create v0.2.0 --generate-notes --title "v0.2.0" - ``` - -## GitHub Release notes - -Use generated notes as the base, then edit the release text to keep it short and useful. - -The release description should include: - -- What changed for users -- Any packaging, CLI, or behavior changes worth calling out -- Any migration or upgrade note if behavior changed -- A short verification note when helpful, for example that the version is on PyPI - -Good default structure: - -```md -## Summary -- Short user-facing change 1 -- Short user-facing change 2 - -## Notes -- Optional upgrade or compatibility note -``` - -Avoid: - -- Raw internal implementation details unless they affect users -- Huge changelogs pasted into the release body -- Empty releases with only the tag name when there was a meaningful change - -## What happens after the tag is pushed - -1. GitHub Actions checks out the tagged commit. -2. The workflow confirms that commit belongs to `main`. -3. The package is built from that exact tag. -4. GitHub exchanges its OIDC identity with PyPI using Trusted Publishing. -5. The distribution is uploaded to PyPI. -6. After that succeeds, create the GitHub Release page for the same tag. - -## Verifying the release locally - -You can inspect the version derived from a tag before pushing: - -```bash -git tag -a v0.2.0 -m "Release v0.2.0" -python -m build -``` - -The built wheel and sdist should report version `0.2.0`. -If you created a test tag by mistake, delete it locally before pushing: - -```bash -git tag -d v0.2.0 -``` - ---- - -## See also - -- [DEVELOPMENT.md](../DEVELOPMENT.md) — local setup and testing -- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR workflow and legal requirements -- [docs/](./README.md) — documentation index diff --git a/labs/AgentStream/exgentic/pyproject.toml b/labs/AgentStream/exgentic/pyproject.toml index 9f7f8692..4ef94a7f 100644 --- a/labs/AgentStream/exgentic/pyproject.toml +++ b/labs/AgentStream/exgentic/pyproject.toml @@ -1,10 +1,11 @@ [build-system] -requires = ["hatchling", "hatch-vcs"] +requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "exgentic" -dynamic = ["version"] +# Bundled AgentStream snapshot; not published independently from Sico. +version = "0.0.0+agentstream" description = "Exgentic - General agent evaluation" authors = [{name = "Exgentic Team"}] license = {text = "Apache-2.0"} @@ -65,14 +66,6 @@ dev = [ [tool.hatch.metadata] allow-direct-references = true -[tool.hatch.version] -source = "vcs" -tag-pattern = "^v(?P.*)$" -fallback-version = "0.0.0" - -[tool.hatch.build.hooks.vcs] -version-file = "src/exgentic/_version.py" - [tool.hatch.build.targets.wheel] packages = ["src/exgentic"] include = [ diff --git a/labs/AgentStream/exgentic/renovate.json b/labs/AgentStream/exgentic/renovate.json deleted file mode 100644 index 97380307..00000000 --- a/labs/AgentStream/exgentic/renovate.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], - "packageRules": [ - { - "matchManagers": ["uv"], - "minimumReleaseAge": "14 days", - "groupName": "python dependencies", - "rangeStrategy": "bump" - } - ], - "schedule": ["every weekend"] -} diff --git a/labs/AgentStream/exgentic/scripts/release.sh b/labs/AgentStream/exgentic/scripts/release.sh deleted file mode 100644 index 67c3b826..00000000 --- a/labs/AgentStream/exgentic/scripts/release.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2026, The AgentStream organization and its contributors. - -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: scripts/release.sh [--push] - -Creates an annotated release tag on the current main HEAD. -The Git tag is the single source of truth for package versioning. - -Examples: - scripts/release.sh 0.1.1 - scripts/release.sh 0.1.1 --push -EOF -} - -if [[ $# -lt 1 || $# -gt 2 ]]; then - usage - exit 1 -fi - -version="$1" -push_changes="${2:-}" - -if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){2}([A-Za-z0-9._-]+)?$ ]]; then - echo "Version must look like 1.2.3 or 1.2.3rc1" >&2 - exit 1 -fi - -if [[ -n "$push_changes" && "$push_changes" != "--push" ]]; then - usage - exit 1 -fi - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$repo_root" - -current_branch="$(git branch --show-current)" -if [[ "$current_branch" != "main" ]]; then - echo "Release tags must be created from main. Current branch: $current_branch" >&2 - exit 1 -fi - -if [[ -n "$(git status --short)" ]]; then - echo "Working tree is dirty. Commit or stash changes before releasing." >&2 - exit 1 -fi - -git fetch origin main --tags -local_head="$(git rev-parse HEAD)" -remote_head="$(git rev-parse origin/main)" -if [[ "$local_head" != "$remote_head" ]]; then - echo "Local main is not at origin/main. Pull or push before releasing." >&2 - exit 1 -fi - -tag="v$version" -if git rev-parse "$tag" >/dev/null 2>&1; then - echo "Tag $tag already exists." >&2 - exit 1 -fi - -git tag -a "$tag" -m "Release $tag" - -if [[ "$push_changes" == "--push" ]]; then - git push origin "$tag" -fi - -echo "Created release tag $tag at $local_head" -if [[ "$push_changes" != "--push" ]]; then - echo "Push when ready with:" - echo " git push origin $tag" -fi diff --git a/labs/AgentStream/exgentic/uv.lock b/labs/AgentStream/exgentic/uv.lock index bdb2f2e5..6e34b1a5 100644 --- a/labs/AgentStream/exgentic/uv.lock +++ b/labs/AgentStream/exgentic/uv.lock @@ -680,6 +680,7 @@ wheels = [ [[package]] name = "exgentic" +version = "0.0.0+agentstream" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/labs/AgentStream/exgentic/whitesource.config b/labs/AgentStream/exgentic/whitesource.config deleted file mode 100644 index 8abafd8c..00000000 --- a/labs/AgentStream/exgentic/whitesource.config +++ /dev/null @@ -1,2 +0,0 @@ -python.path=python3.12 -python.invokePipAsModule=true From 55a8b22f7b657937b013564f2807850451de7132 Mon Sep 17 00:00:00 2001 From: Dawson Date: Wed, 19 Aug 2026 13:06:49 +0800 Subject: [PATCH 3/4] fix(labs): make AgentStream checks work from repo root --- .../exgentic/.pre-commit-config.yaml | 42 ++++++++++++------- labs/AgentStream/exgentic/DEVELOPMENT.md | 9 ++-- .../exgentic/misc/utils/.secrets.baseline | 10 ++--- .../misc/utils/enforce_dependency_caps.py | 7 ++-- .../misc/utils/enforce_spdx_header.py | 19 +++++---- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/labs/AgentStream/exgentic/.pre-commit-config.yaml b/labs/AgentStream/exgentic/.pre-commit-config.yaml index b7d44f52..63e20842 100644 --- a/labs/AgentStream/exgentic/.pre-commit-config.yaml +++ b/labs/AgentStream/exgentic/.pre-commit-config.yaml @@ -1,32 +1,45 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +# These component-specific checks are intentionally run manually; see DEVELOPMENT.md. +# Paths are evaluated from the Sico repository root. +files: ^labs/AgentStream/exgentic/ +exclude: ^labs/AgentStream/exgentic/\.venv/ + repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.1.6 hooks: - # Run the linter on all files except the specific one - id: ruff - args: [--fix] - - id: ruff-format + # Keep the bundled snapshot unchanged while checking syntax and + # undefined names. The explicit config avoids inheriting Sico's root + # Ruff settings when this hook is run from the monorepo root. + args: + - --config + - labs/AgentStream/exgentic/pyproject.toml + - --select + - E9,F63,F7,F82 - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - id: detect-secrets - args: [--baseline, misc/utils/.secrets.baseline] - exclude: misc/utils/.secrets.baseline + args: [--baseline, labs/AgentStream/exgentic/misc/utils/.secrets.baseline] + exclude: ^labs/AgentStream/exgentic/misc/utils/\.secrets\.baseline$ - repo: https://github.com/astral-sh/uv-pre-commit rev: 0.7.12 hooks: - id: uv-lock - args: [--locked] + args: [--locked, --project, labs/AgentStream/exgentic] + files: ^labs/AgentStream/exgentic/(uv\.lock|pyproject\.toml|uv\.toml)$ - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell + args: [--toml, labs/AgentStream/exgentic/pyproject.toml] + exclude: ^labs/AgentStream/exgentic/(tests/benchmarks/recordings/|uv\.lock$) additional_dependencies: - tomli @@ -34,33 +47,32 @@ repos: hooks: - id: enforce-spdx-header name: Enforce SPDX Header - entry: python3 misc/utils/enforce_spdx_header.py + entry: python labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py language: system - files: \.py$ - exclude: ^\.venv/ + files: ^labs/AgentStream/exgentic/.*\.py$ types: [python] - id: enforce-relative-imports name: Enforce Relative Imports - entry: python3 misc/utils/enforce_relative_imports.py + entry: python labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py language: system # Adjust the files pattern to match your needs - files: ^src/.*\.py$ + files: ^labs/AgentStream/exgentic/src/.*\.py$ # Optional: Specify types or exclude files types: [python] - id: enforce-dependency-caps name: Enforce Dependency Version Caps - entry: python3 misc/utils/enforce_dependency_caps.py + entry: python labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py language: system - files: ^pyproject\.toml$ + files: ^labs/AgentStream/exgentic/pyproject\.toml$ pass_filenames: false - repo: local hooks: - id: enforce-library-imports name: Enforce Library Imports - entry: python3 misc/utils/enforce_library_imports.py + entry: python labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py language: system # Adjust the files pattern to match your needs - exclude: (^src/.*\.py$)|misc/utils/enforce_library_imports.py|misc/utils/enforce_relative_imports.py + exclude: ^labs/AgentStream/exgentic/(src/.*\.py|misc/utils/enforce_(library_imports|relative_imports)\.py)$ # Optional: Specify types or exclude files types: [python] diff --git a/labs/AgentStream/exgentic/DEVELOPMENT.md b/labs/AgentStream/exgentic/DEVELOPMENT.md index 35cfbcef..e580c2c8 100644 --- a/labs/AgentStream/exgentic/DEVELOPMENT.md +++ b/labs/AgentStream/exgentic/DEVELOPMENT.md @@ -101,10 +101,13 @@ The test suite includes **replay tests** that re-run recorded benchmark sessions ## Linting +These checks are not part of Sico's root CI. Contributors modifying the bundled +AgentStream snapshot should run them manually from the Sico repository root. + ```bash -pip install pre-commit -pre-commit install -pre-commit run --all-files +python -m pip install pre-commit +pre-commit run --config labs/AgentStream/exgentic/.pre-commit-config.yaml \ + --all-files --show-diff-on-failure ``` ## OpenTelemetry Tracing diff --git a/labs/AgentStream/exgentic/misc/utils/.secrets.baseline b/labs/AgentStream/exgentic/misc/utils/.secrets.baseline index 873f0f38..3e5ec796 100644 --- a/labs/AgentStream/exgentic/misc/utils/.secrets.baseline +++ b/labs/AgentStream/exgentic/misc/utils/.secrets.baseline @@ -92,7 +92,7 @@ }, { "path": "detect_secrets.filters.common.is_baseline_file", - "filename": "misc/utils/.secrets.baseline" + "filename": "labs/AgentStream/exgentic/misc/utils/.secrets.baseline" }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", @@ -127,19 +127,19 @@ } ], "results": { - "tests/benchmarks/recordings/appworld/trajectory.jsonl": [ + "labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl": [ { "type": "JSON Web Token", - "filename": "tests/benchmarks/recordings/appworld/trajectory.jsonl", + "filename": "labs/AgentStream/exgentic/tests/benchmarks/recordings/appworld/trajectory.jsonl", "hashed_secret": "0c677ddce015761585645772702fd153efbb08d9", "is_verified": false, "line_number": 7 } ], - "tests/benchmarks/recordings/swebench/results.json": [ + "labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json": [ { "type": "Secret Keyword", - "filename": "tests/benchmarks/recordings/swebench/results.json", + "filename": "labs/AgentStream/exgentic/tests/benchmarks/recordings/swebench/results.json", "hashed_secret": "d4e0e04792fd434b5dc9c4155c178f66edcf4ed3", "is_verified": false, "line_number": 46 diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py index 1f78a16b..a107dfc2 100644 --- a/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py +++ b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py @@ -95,7 +95,8 @@ def check_dependency_caps(pyproject_path: Path) -> list[str]: def main() -> int: """Main entry point.""" - pyproject_path = Path("pyproject.toml") + project_root = Path(__file__).resolve().parents[2] + pyproject_path = project_root / "pyproject.toml" if not pyproject_path.exists(): print("Error: pyproject.toml not found", file=sys.stderr) @@ -104,7 +105,7 @@ def main() -> int: uncapped = check_dependency_caps(pyproject_path) if uncapped: - print("❌ Dependencies without upper version bounds found:", file=sys.stderr) + print("ERROR: Dependencies without upper version bounds found:", file=sys.stderr) print(file=sys.stderr) for dep in uncapped: print(f" {dep}", file=sys.stderr) @@ -116,7 +117,7 @@ def main() -> int: print("See SECURITY.md for the dependency management policy.", file=sys.stderr) return 1 - print("✅ All dependencies have upper version bounds") + print("All dependencies have upper version bounds") return 0 diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py b/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py index dc39aaa2..51378aeb 100644 --- a/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py +++ b/labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py @@ -12,11 +12,14 @@ HEADER_LINES = [ "# SPDX-License-Identifier: Apache-2.0", - "# Copyright (C) 2026, The Exgentic organization and its contributors.", + "# Copyright (C) 2026, The AgentStream organization and its contributors.", ] HEADER_TEXT = "\n".join(HEADER_LINES) + "\n\n" -COPYRIGHT_RE = re.compile(r"^# Copyright \(C\) (?P\d{4}), The Exgentic organization and its contributors\.$") +COPYRIGHT_RE = re.compile( + r"^# Copyright \(C\) \d{4}, The (?:Exgentic|AgentStream) organization and its contributors\.$" +) +APACHE_LICENSE_MARKER = 'Licensed under the Apache License, Version 2.0 (the "License")' SKIP_DIRS = { ".git", @@ -35,16 +38,14 @@ def update_file(path: Path) -> bool: raw = path.read_bytes() original = raw.decode("utf-8", errors="surrogateescape") if not original: - path.write_text(HEADER_TEXT, encoding="utf-8") - return True + return False lines = original.splitlines() if len(lines) >= 2 and lines[0] == HEADER_LINES[0] and COPYRIGHT_RE.match(lines[1]): - if lines[1] != HEADER_LINES[1]: - lines[1] = HEADER_LINES[1] - updated = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - path.write_text(updated, encoding="utf-8", errors="surrogateescape") - return True + return False + + # Preserve complete Apache-2.0 notices carried by third-party source files. + if APACHE_LICENSE_MARKER in "\n".join(lines[:20]): return False updated = HEADER_TEXT + original From b79d9973d21e1984aeea99138f883087183bc7ef Mon Sep 17 00:00:00 2001 From: Dawson Date: Wed, 19 Aug 2026 17:44:48 +0800 Subject: [PATCH 4/4] fix(labs): make AgentStream hooks cross-platform --- labs/AgentStream/exgentic/.pre-commit-config.yaml | 8 ++++---- labs/AgentStream/exgentic/DEVELOPMENT.md | 9 ++++----- .../exgentic/misc/utils/enforce_dependency_caps.py | 1 - labs/AgentStream/exgentic/pyproject.toml | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/labs/AgentStream/exgentic/.pre-commit-config.yaml b/labs/AgentStream/exgentic/.pre-commit-config.yaml index 63e20842..553d4703 100644 --- a/labs/AgentStream/exgentic/.pre-commit-config.yaml +++ b/labs/AgentStream/exgentic/.pre-commit-config.yaml @@ -48,13 +48,13 @@ repos: - id: enforce-spdx-header name: Enforce SPDX Header entry: python labs/AgentStream/exgentic/misc/utils/enforce_spdx_header.py - language: system + language: python files: ^labs/AgentStream/exgentic/.*\.py$ types: [python] - id: enforce-relative-imports name: Enforce Relative Imports entry: python labs/AgentStream/exgentic/misc/utils/enforce_relative_imports.py - language: system + language: python # Adjust the files pattern to match your needs files: ^labs/AgentStream/exgentic/src/.*\.py$ # Optional: Specify types or exclude files @@ -62,7 +62,7 @@ repos: - id: enforce-dependency-caps name: Enforce Dependency Version Caps entry: python labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py - language: system + language: python files: ^labs/AgentStream/exgentic/pyproject\.toml$ pass_filenames: false @@ -71,7 +71,7 @@ repos: - id: enforce-library-imports name: Enforce Library Imports entry: python labs/AgentStream/exgentic/misc/utils/enforce_library_imports.py - language: system + language: python # Adjust the files pattern to match your needs exclude: ^labs/AgentStream/exgentic/(src/.*\.py|misc/utils/enforce_(library_imports|relative_imports)\.py)$ # Optional: Specify types or exclude files diff --git a/labs/AgentStream/exgentic/DEVELOPMENT.md b/labs/AgentStream/exgentic/DEVELOPMENT.md index e580c2c8..3ccebda9 100644 --- a/labs/AgentStream/exgentic/DEVELOPMENT.md +++ b/labs/AgentStream/exgentic/DEVELOPMENT.md @@ -102,12 +102,11 @@ The test suite includes **replay tests** that re-run recorded benchmark sessions ## Linting These checks are not part of Sico's root CI. Contributors modifying the bundled -AgentStream snapshot should run them manually from the Sico repository root. +Exgentic snapshot should run them manually from the Sico repository root. +Install [pre-commit](https://pre-commit.com/#install) for your platform, then run: ```bash -python -m pip install pre-commit -pre-commit run --config labs/AgentStream/exgentic/.pre-commit-config.yaml \ - --all-files --show-diff-on-failure +pre-commit run --config labs/AgentStream/exgentic/.pre-commit-config.yaml --all-files --show-diff-on-failure ``` ## OpenTelemetry Tracing @@ -120,7 +119,7 @@ export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export EXGENTIC_OTEL_ENABLED=true ``` -See [`OTEL_SEMANTIC_CONVENTIONS.md`](./OTEL_SEMANTIC_CONVENTIONS.md) for details. +See [OpenTelemetry semantic conventions](./docs/observability/semantic-conventions.md) for details. ## Packaging diff --git a/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py index a107dfc2..e2e07a01 100644 --- a/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py +++ b/labs/AgentStream/exgentic/misc/utils/enforce_dependency_caps.py @@ -114,7 +114,6 @@ def main() -> int: "All dependencies must have upper bounds (e.g., >=1.0.0,<2) to limit supply chain attack exposure.", file=sys.stderr, ) - print("See SECURITY.md for the dependency management policy.", file=sys.stderr) return 1 print("All dependencies have upper version bounds") diff --git a/labs/AgentStream/exgentic/pyproject.toml b/labs/AgentStream/exgentic/pyproject.toml index 4ef94a7f..af133ae1 100644 --- a/labs/AgentStream/exgentic/pyproject.toml +++ b/labs/AgentStream/exgentic/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "exgentic" -# Bundled AgentStream snapshot; not published independently from Sico. +# Bundled Exgentic snapshot used by AgentStream; not published independently from Sico. version = "0.0.0+agentstream" description = "Exgentic - General agent evaluation" authors = [{name = "Exgentic Team"}]

w2xALVW^>L3vJZP5m(tP&8qx%MOn4UB0gtpeG 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 z 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 z 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 literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/exgentic_banner_black.png b/labs/AgentStream/exgentic/misc/assets/exgentic_banner_black.png new file mode 100644 index 0000000000000000000000000000000000000000..300307762cdb3ce4fbff10e7fe31188907eb4213 GIT binary patch literal 38420 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 literal 0 HcmV?d00001 diff --git a/labs/AgentStream/exgentic/misc/assets/exgentic_banner_black_no_background.png b/labs/AgentStream/exgentic/misc/assets/exgentic_banner_black_no_background.png new file mode 100644 index 0000000000000000000000000000000000000000..e5286cc780f685955cf61ad1ae19ae06c926b8e2 GIT binary patch literal 31271 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