claude-smart 0.2.41 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/README.md +1 -1
- package/bin/claude-smart.js +86 -48
- package/package.json +10 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +2 -2
- package/plugin/dashboard/next.config.ts +9 -1
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +46 -15
- package/plugin/scripts/cli.sh +29 -1
- package/plugin/scripts/codex-hook.js +72 -4
- package/plugin/scripts/dashboard-build.sh +1 -0
- package/plugin/scripts/dashboard-service.sh +1 -0
- package/plugin/scripts/ensure-plugin-root.sh +7 -14
- package/plugin/scripts/hook_entry.sh +1 -0
- package/plugin/scripts/smart-install.sh +18 -2
- package/plugin/src/claude_smart/cli.py +72 -38
- package/plugin/src/claude_smart/context_format.py +11 -12
- package/plugin/src/claude_smart/cs_cite.py +26 -12
- package/plugin/src/claude_smart/ids.py +13 -5
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +53 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +271 -0
- package/plugin/vendor/reflexio/reflexio/README.md +184 -0
- package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
- package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
- package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
- package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
- package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
- package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
- package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
- package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
- package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
- package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
- package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
- package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
- package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
- package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
- package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
- package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
- package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
- package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
- package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
- package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
- package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
- package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""Tool-calling primitives shared by agentic extraction and search pipelines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
15
|
+
|
|
16
|
+
from reflexio.server.llm.llm_utils import make_strict_json_schema
|
|
17
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AsyncRequestSpec:
|
|
25
|
+
"""Pre-persistence request produced by an asynchronous information tool."""
|
|
26
|
+
|
|
27
|
+
tool_name: str
|
|
28
|
+
dedup_key: str
|
|
29
|
+
scope: dict[str, Any]
|
|
30
|
+
question_text: str
|
|
31
|
+
answer_format: str | None = None
|
|
32
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
tags: list[str] = field(default_factory=list)
|
|
34
|
+
cache_until_seconds: int = 300
|
|
35
|
+
valid_until_seconds: int = 2_592_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Completed:
|
|
40
|
+
"""Synchronous tool result."""
|
|
41
|
+
|
|
42
|
+
result: dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class AsyncAccepted:
|
|
47
|
+
"""Accepted asynchronous tool request returned as a normal tool result."""
|
|
48
|
+
|
|
49
|
+
pending_tool_call_id: str
|
|
50
|
+
result: dict[str, Any]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
ToolOutcome = Completed | AsyncAccepted
|
|
54
|
+
ToolHandlerResult = dict[str, Any] | ToolOutcome
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Tool(BaseModel):
|
|
58
|
+
"""A single LLM-callable tool.
|
|
59
|
+
|
|
60
|
+
Arguments are defined by a Pydantic model (its schema goes to the LLM,
|
|
61
|
+
its docstring becomes the tool description). The handler takes a
|
|
62
|
+
validated args instance plus a caller-supplied context object and
|
|
63
|
+
returns a JSON-serialisable dict that is fed back as the tool result.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
args_model: type[BaseModel]
|
|
70
|
+
handler: Callable[[BaseModel, Any], ToolHandlerResult]
|
|
71
|
+
strict: bool = True
|
|
72
|
+
|
|
73
|
+
def openai_spec(self) -> dict:
|
|
74
|
+
parameters = self.args_model.model_json_schema()
|
|
75
|
+
if self.strict:
|
|
76
|
+
parameters = make_strict_json_schema(parameters)
|
|
77
|
+
return {
|
|
78
|
+
"type": "function",
|
|
79
|
+
"function": {
|
|
80
|
+
"name": self.name,
|
|
81
|
+
"description": (self.args_model.__doc__ or "").strip(),
|
|
82
|
+
"parameters": parameters,
|
|
83
|
+
"strict": self.strict,
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AsyncInfoTool(Tool):
|
|
89
|
+
"""Marker type for tools that register async work and continue the loop."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _coerce_tool_outcome(value: ToolHandlerResult) -> ToolOutcome:
|
|
93
|
+
if isinstance(value, Completed | AsyncAccepted):
|
|
94
|
+
return value
|
|
95
|
+
return Completed(result=value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tool_result_from_outcome(
|
|
99
|
+
outcome: ToolOutcome,
|
|
100
|
+
pending_tool_call_ids: list[str] | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
if isinstance(outcome, AsyncAccepted):
|
|
103
|
+
if pending_tool_call_ids is not None:
|
|
104
|
+
pending_tool_call_ids.append(outcome.pending_tool_call_id)
|
|
105
|
+
return outcome.result
|
|
106
|
+
return outcome.result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ToolRegistry:
|
|
110
|
+
def __init__(self, tools: list[Tool] | None = None) -> None:
|
|
111
|
+
self._tools: dict[str, Tool] = {}
|
|
112
|
+
for t in tools or []:
|
|
113
|
+
self.register(t)
|
|
114
|
+
|
|
115
|
+
def register(self, tool: Tool) -> None:
|
|
116
|
+
self._tools[tool.name] = tool
|
|
117
|
+
|
|
118
|
+
def openai_specs(self) -> list[dict]:
|
|
119
|
+
return [t.openai_spec() for t in self._tools.values()]
|
|
120
|
+
|
|
121
|
+
def handle_outcome(self, name: str, args_json: str, ctx: Any) -> ToolOutcome:
|
|
122
|
+
tool = self._tools.get(name)
|
|
123
|
+
if tool is None:
|
|
124
|
+
return Completed(result={"error": f"unknown tool: {name}"})
|
|
125
|
+
try:
|
|
126
|
+
raw = json.loads(args_json or "{}")
|
|
127
|
+
args = tool.args_model.model_validate(raw)
|
|
128
|
+
except (ValidationError, json.JSONDecodeError) as e:
|
|
129
|
+
return Completed(result={"error": f"invalid args for {name}: {e}"})
|
|
130
|
+
try:
|
|
131
|
+
return _coerce_tool_outcome(tool.handler(args, ctx))
|
|
132
|
+
except Exception as e: # handler errors are recoverable tool-turn errors
|
|
133
|
+
logger.exception("tool handler %s failed", name)
|
|
134
|
+
return Completed(result={"error": f"handler error: {type(e).__name__}"})
|
|
135
|
+
|
|
136
|
+
def handle(self, name: str, args_json: str, ctx: Any) -> dict:
|
|
137
|
+
return _tool_result_from_outcome(self.handle_outcome(name, args_json, ctx))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ToolLoopTurn(BaseModel):
|
|
141
|
+
"""A single tool call turn in a tool-loop trace."""
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
144
|
+
|
|
145
|
+
tool_name: str
|
|
146
|
+
args: dict[str, Any]
|
|
147
|
+
result: dict[str, Any]
|
|
148
|
+
latency_ms: int
|
|
149
|
+
# Populated from the LLM response's ``usage`` object when available
|
|
150
|
+
# (native tool-call mode). All None in capability-fallback mode and
|
|
151
|
+
# when the provider doesn't report usage.
|
|
152
|
+
model: str | None = None
|
|
153
|
+
prompt_tokens: int | None = None
|
|
154
|
+
completion_tokens: int | None = None
|
|
155
|
+
total_tokens: int | None = None
|
|
156
|
+
cost_usd: float | None = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ToolLoopTrace(BaseModel):
|
|
160
|
+
"""Full trace of a tool-loop execution."""
|
|
161
|
+
|
|
162
|
+
turns: list[ToolLoopTurn] = []
|
|
163
|
+
finished: bool = False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ToolLoopResult(BaseModel):
|
|
167
|
+
"""Outcome of ``run_tool_loop``: final ``ctx``, trace, and terminator reason."""
|
|
168
|
+
|
|
169
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
170
|
+
|
|
171
|
+
ctx: Any
|
|
172
|
+
trace: ToolLoopTrace
|
|
173
|
+
finished_reason: Literal["finish_tool", "no_tool_call", "max_steps", "error"]
|
|
174
|
+
messages: list[dict[str, Any]] = Field(default_factory=list)
|
|
175
|
+
pending_tool_call_ids: list[str] = Field(default_factory=list)
|
|
176
|
+
max_steps_remaining: int = 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Models we know support function calling per vendor docs but that litellm's
|
|
180
|
+
# model_cost registry hasn't catalogued yet. When litellm returns False
|
|
181
|
+
# (without raising) for a model whose name starts with one of these prefixes,
|
|
182
|
+
# treat that as a registry gap rather than an actual capability gap.
|
|
183
|
+
#
|
|
184
|
+
# Each entry must be justified by (a) the vendor docs and (b) a confirmed
|
|
185
|
+
# round-trip tool call against the live API. Update this list when litellm
|
|
186
|
+
# upstreams the registration so the override becomes redundant.
|
|
187
|
+
_TOOL_CALLING_OVERRIDES: tuple[str, ...] = (
|
|
188
|
+
# https://platform.minimax.io/docs/guides/text-m2-function-call says
|
|
189
|
+
# MiniMax-M2.7 supports tool use + interleaved thinking via OpenAI-compatible
|
|
190
|
+
# tools format. Verified by a live `litellm.completion(model='minimax/MiniMax-M2.7',
|
|
191
|
+
# tools=[...])` round-trip that returned a proper tool_call message.
|
|
192
|
+
# litellm 1.80.x has 'minimax/MiniMax-M2' in model_cost but not 'MiniMax-M2.7'.
|
|
193
|
+
"minimax/MiniMax-M2",
|
|
194
|
+
# MiniMax-M3 supports OpenAI-compatible tools the same way the M2 family
|
|
195
|
+
# does, but litellm 1.80.x has no 'minimax/MiniMax-M3' model_cost entry so
|
|
196
|
+
# supports_function_calling returns False. Verified by a live
|
|
197
|
+
# `litellm.completion(model='minimax/MiniMax-M3', tools=[...])` round-trip
|
|
198
|
+
# that returned a proper tool_call message (finish_reason='tool_calls').
|
|
199
|
+
"minimax/MiniMax-M3",
|
|
200
|
+
# claude-code/* models route through our local CLI provider
|
|
201
|
+
# (see providers/claude_code_provider.py). litellm has no registry
|
|
202
|
+
# entry for them, so it returns False. The provider handles tool
|
|
203
|
+
# calling explicitly by rendering tool specs into the system prompt
|
|
204
|
+
# and parsing the model's JSON output back into ChatCompletionMessageToolCall
|
|
205
|
+
# blocks. Verified end-to-end against the resumable extraction tool loop.
|
|
206
|
+
"claude-code/",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def supports_tool_calling(model: str) -> bool:
|
|
211
|
+
"""Return True when litellm reports native function-calling support.
|
|
212
|
+
|
|
213
|
+
Wrapped so tests can monkeypatch the probe without touching litellm.
|
|
214
|
+
On any internal error we optimistically assume support — cheaper to
|
|
215
|
+
attempt a real call than to wrongly fall back. When litellm returns
|
|
216
|
+
False (without raising) for a model in :data:`_TOOL_CALLING_OVERRIDES`,
|
|
217
|
+
we override to True — see the constant for the rationale.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
model (str): Fully-qualified model name.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
bool: True if litellm advertises function-calling for ``model``,
|
|
224
|
+
or the model name matches a known-good override prefix.
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
import litellm
|
|
228
|
+
|
|
229
|
+
if bool(litellm.supports_function_calling(model=model)):
|
|
230
|
+
return True
|
|
231
|
+
if any(model.startswith(prefix) for prefix in _TOOL_CALLING_OVERRIDES):
|
|
232
|
+
logger.debug(
|
|
233
|
+
"litellm.supports_function_calling returned False for %s; "
|
|
234
|
+
"applying override (see _TOOL_CALLING_OVERRIDES)",
|
|
235
|
+
model,
|
|
236
|
+
)
|
|
237
|
+
return True
|
|
238
|
+
return False
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning(
|
|
241
|
+
"supports_function_calling probe failed for %s: %s: %s — assuming True",
|
|
242
|
+
model,
|
|
243
|
+
type(e).__name__,
|
|
244
|
+
e,
|
|
245
|
+
)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Cap on tool-result payload size injected back into the message history
|
|
250
|
+
# in multi-stage mode. Without this, a single fat search response could
|
|
251
|
+
# blow the model's context window in two or three turns.
|
|
252
|
+
_MULTI_STAGE_RESULT_CHAR_CAP = 4000
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _serialize_tool_result_for_history(result: dict[str, Any]) -> str:
|
|
256
|
+
"""Render a tool result dict as a JSON string capped at a fixed size.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
result (dict[str, Any]): The tool handler's return value.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
str: A JSON string truncated to ``_MULTI_STAGE_RESULT_CHAR_CAP``
|
|
263
|
+
characters with a ``... [truncated]`` marker on overflow.
|
|
264
|
+
"""
|
|
265
|
+
payload = json.dumps(result, default=str)
|
|
266
|
+
if len(payload) <= _MULTI_STAGE_RESULT_CHAR_CAP:
|
|
267
|
+
return payload
|
|
268
|
+
return f"{payload[:_MULTI_STAGE_RESULT_CHAR_CAP]}... [truncated]"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _run_multi_stage_fallback(
|
|
272
|
+
*,
|
|
273
|
+
client: LiteLLMClient,
|
|
274
|
+
messages: list[dict[str, Any]],
|
|
275
|
+
registry: ToolRegistry,
|
|
276
|
+
model_role: ModelRole,
|
|
277
|
+
max_steps: int,
|
|
278
|
+
ctx: Any,
|
|
279
|
+
finish_tool_name: str,
|
|
280
|
+
multi_stage_schema: type[BaseModel],
|
|
281
|
+
log_label: str | None,
|
|
282
|
+
trace: ToolLoopTrace,
|
|
283
|
+
pending_tool_call_ids: list[str],
|
|
284
|
+
) -> ToolLoopResult:
|
|
285
|
+
"""Drive a multi-turn tool loop using one structured-output call per turn.
|
|
286
|
+
|
|
287
|
+
Used when the configured model lacks native tool-calling but the
|
|
288
|
+
caller wants observe-decide-act semantics (e.g. the search agent on
|
|
289
|
+
``minimax/MiniMax-M2.7``). Each turn:
|
|
290
|
+
|
|
291
|
+
1. Asks the model for a ``multi_stage_schema`` instance whose
|
|
292
|
+
``next_call`` field carries a discriminator literal naming the
|
|
293
|
+
desired tool.
|
|
294
|
+
2. Dispatches that call against the registry.
|
|
295
|
+
3. Appends the agent's plan as an assistant message and the tool
|
|
296
|
+
result as a user message, so the next turn's model call sees both.
|
|
297
|
+
|
|
298
|
+
Loop terminates when ``next_call.tool == finish_tool_name`` or
|
|
299
|
+
``max_steps`` is exhausted.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
client (LiteLLMClient): Configured client.
|
|
303
|
+
messages (list[dict]): Seed message list; extended in place.
|
|
304
|
+
registry (ToolRegistry): Tools exposed to the LLM.
|
|
305
|
+
model_role (ModelRole): Role used to resolve the target model.
|
|
306
|
+
max_steps (int): Cap on tool-calling turns.
|
|
307
|
+
ctx (Any): Per-run context passed to each tool handler.
|
|
308
|
+
finish_tool_name (str): Sentinel literal that ends the loop.
|
|
309
|
+
multi_stage_schema (type[BaseModel]): Schema with a ``next_call``
|
|
310
|
+
discriminated-union field.
|
|
311
|
+
log_label (str | None): Optional llm_io.log label.
|
|
312
|
+
trace (ToolLoopTrace): Trace to extend with per-turn entries.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
ToolLoopResult: ``ctx``, trace, and the terminator reason.
|
|
316
|
+
"""
|
|
317
|
+
if log_label:
|
|
318
|
+
from reflexio.server.services.service_utils import (
|
|
319
|
+
log_llm_messages,
|
|
320
|
+
log_model_response,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
for turn_idx in range(max_steps):
|
|
324
|
+
turn_label = f"(multi-stage turn {turn_idx + 1})"
|
|
325
|
+
if log_label:
|
|
326
|
+
log_llm_messages(logger, f"{log_label} {turn_label}", messages)
|
|
327
|
+
tool_t0 = time.monotonic()
|
|
328
|
+
parsed = client.generate_chat_response(
|
|
329
|
+
messages=messages,
|
|
330
|
+
response_format=multi_stage_schema,
|
|
331
|
+
model_role=model_role,
|
|
332
|
+
)
|
|
333
|
+
if log_label:
|
|
334
|
+
log_model_response(logger, f"{log_label} {turn_label}", parsed)
|
|
335
|
+
if not isinstance(parsed, BaseModel):
|
|
336
|
+
raise RuntimeError(
|
|
337
|
+
f"Multi-stage structured call returned unexpected type {type(parsed)}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
next_call = getattr(parsed, "next_call", None)
|
|
341
|
+
if next_call is None:
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
"Multi-stage schema must expose a 'next_call' field; "
|
|
344
|
+
f"got {type(parsed).__name__}"
|
|
345
|
+
)
|
|
346
|
+
tool_name = getattr(next_call, "tool", None)
|
|
347
|
+
if not isinstance(tool_name, str):
|
|
348
|
+
raise RuntimeError(
|
|
349
|
+
"Multi-stage next_call must carry a 'tool' discriminator literal; "
|
|
350
|
+
f"got {type(next_call).__name__}"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
reasoning = getattr(parsed, "reasoning", "") or ""
|
|
354
|
+
args_dict = next_call.model_dump(exclude={"tool"})
|
|
355
|
+
args_json = next_call.model_dump_json(exclude={"tool"})
|
|
356
|
+
|
|
357
|
+
# Echo the agent's plan back into history so subsequent turns can
|
|
358
|
+
# reason about what was tried already.
|
|
359
|
+
messages.append(
|
|
360
|
+
{
|
|
361
|
+
"role": "assistant",
|
|
362
|
+
"content": (
|
|
363
|
+
f"Reasoning: {reasoning}\nNext call: {tool_name}({args_json})"
|
|
364
|
+
),
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if tool_name == finish_tool_name:
|
|
369
|
+
# Dispatch finish through the registry so any ctx-side
|
|
370
|
+
# bookkeeping (e.g. stashing the answer) still runs.
|
|
371
|
+
outcome = registry.handle_outcome(tool_name, args_json, ctx)
|
|
372
|
+
result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
|
|
373
|
+
trace.turns.append(
|
|
374
|
+
ToolLoopTurn(
|
|
375
|
+
tool_name=tool_name,
|
|
376
|
+
args=args_dict,
|
|
377
|
+
result=result,
|
|
378
|
+
latency_ms=int((time.monotonic() - tool_t0) * 1000),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
trace.finished = True
|
|
382
|
+
return ToolLoopResult(
|
|
383
|
+
ctx=ctx,
|
|
384
|
+
trace=trace,
|
|
385
|
+
finished_reason="finish_tool",
|
|
386
|
+
messages=messages,
|
|
387
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
388
|
+
max_steps_remaining=max_steps - turn_idx - 1,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
outcome = registry.handle_outcome(tool_name, args_json, ctx)
|
|
392
|
+
result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
|
|
393
|
+
trace.turns.append(
|
|
394
|
+
ToolLoopTurn(
|
|
395
|
+
tool_name=tool_name,
|
|
396
|
+
args=args_dict,
|
|
397
|
+
result=result,
|
|
398
|
+
latency_ms=int((time.monotonic() - tool_t0) * 1000),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
messages.append(
|
|
402
|
+
{
|
|
403
|
+
"role": "user",
|
|
404
|
+
"content": (
|
|
405
|
+
f"Tool {tool_name} returned: "
|
|
406
|
+
f"{_serialize_tool_result_for_history(result)}"
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
trace.finished = False
|
|
412
|
+
return ToolLoopResult(
|
|
413
|
+
ctx=ctx,
|
|
414
|
+
trace=trace,
|
|
415
|
+
finished_reason="max_steps",
|
|
416
|
+
messages=messages,
|
|
417
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
418
|
+
max_steps_remaining=0,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def run_tool_loop(
|
|
423
|
+
client: LiteLLMClient,
|
|
424
|
+
messages: list[dict[str, Any]],
|
|
425
|
+
registry: ToolRegistry,
|
|
426
|
+
model_role: ModelRole,
|
|
427
|
+
*,
|
|
428
|
+
max_steps: int = 8,
|
|
429
|
+
ctx: Any = None,
|
|
430
|
+
finish_tool_name: str = "finish",
|
|
431
|
+
fallback_schema: type[BaseModel] | None = None,
|
|
432
|
+
fallback_tool_name: str | None = None,
|
|
433
|
+
multi_stage_schema: type[BaseModel] | None = None,
|
|
434
|
+
tool_choice: str | dict[str, Any] = "auto",
|
|
435
|
+
log_label: str | None = None,
|
|
436
|
+
) -> ToolLoopResult:
|
|
437
|
+
"""Drive an LLM through a tool-calling loop until ``finish_tool_name`` or ``max_steps``.
|
|
438
|
+
|
|
439
|
+
For providers that lack native tool-calling there are two fallback
|
|
440
|
+
modes (in priority order):
|
|
441
|
+
|
|
442
|
+
1. **Multi-stage** (``multi_stage_schema`` set): one structured-output
|
|
443
|
+
call per turn whose parsed schema carries a ``next_call``
|
|
444
|
+
discriminated-union. The server dispatches ``next_call`` against
|
|
445
|
+
the registry, appends the result to the message history, and asks
|
|
446
|
+
for the next turn — preserving observe-decide-act semantics.
|
|
447
|
+
2. **Single-shot** (``fallback_schema`` + ``fallback_tool_name``):
|
|
448
|
+
one structured-output call whose parsed list is converted into
|
|
449
|
+
synthetic tool calls dispatched against ``fallback_tool_name``.
|
|
450
|
+
All calls are planned upfront so the agent never observes any
|
|
451
|
+
tool result.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
client (LiteLLMClient): Configured client — ``generate_chat_response``
|
|
455
|
+
is invoked with ``tools=`` in native mode and with
|
|
456
|
+
``response_format=`` in either fallback mode.
|
|
457
|
+
messages (list[dict]): Seed message list; extended in place per turn.
|
|
458
|
+
registry (ToolRegistry): Tools exposed to the LLM.
|
|
459
|
+
model_role (ModelRole): Role used to resolve the target model.
|
|
460
|
+
max_steps (int): Cap on tool-calling turns.
|
|
461
|
+
ctx (Any): Caller-supplied context object passed to each tool handler.
|
|
462
|
+
finish_tool_name (str): Name of the sentinel tool that terminates the loop.
|
|
463
|
+
fallback_schema (type[BaseModel] | None): Pydantic schema for the
|
|
464
|
+
single-shot fallback path. Used only if ``multi_stage_schema``
|
|
465
|
+
is None.
|
|
466
|
+
fallback_tool_name (str | None): Name of the tool each single-shot
|
|
467
|
+
fallback item is dispatched against.
|
|
468
|
+
multi_stage_schema (type[BaseModel] | None): Pydantic schema for
|
|
469
|
+
the multi-stage fallback path. The schema must expose a
|
|
470
|
+
``next_call`` field whose value is a Pydantic model carrying a
|
|
471
|
+
``tool`` discriminator literal — that literal names the tool
|
|
472
|
+
to dispatch, all other fields become its args. Takes priority
|
|
473
|
+
over ``fallback_schema``.
|
|
474
|
+
tool_choice (str | dict): Forwarded to each native tool-calling turn.
|
|
475
|
+
Defaults to ``"auto"``. Pass an OpenAI tool-choice dict (e.g.
|
|
476
|
+
``{"type": "function", "function": {"name": "finish"}}``) to force a
|
|
477
|
+
specific tool — used to make a single-tool loop behave like a forced
|
|
478
|
+
structured-output call.
|
|
479
|
+
log_label (str | None): When set, each LLM call in the loop is
|
|
480
|
+
mirrored into ``~/.reflexio/logs/llm_io.log`` using this label
|
|
481
|
+
(suffixed with ``(turn N)``, ``(fallback)``, or
|
|
482
|
+
``(multi-stage turn N)``). Matches classic per-call logging
|
|
483
|
+
parity. Leave unset (default) to suppress file-level logging
|
|
484
|
+
for tool-loop callers like unit tests.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
ToolLoopResult: ``ctx``, trace, and the terminator reason.
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
RuntimeError: If the model lacks tool-calling AND no fallback
|
|
491
|
+
(multi-stage or single-shot) is provided.
|
|
492
|
+
"""
|
|
493
|
+
model = resolve_model_name(
|
|
494
|
+
role=model_role,
|
|
495
|
+
site_var_value=None,
|
|
496
|
+
config_override=None,
|
|
497
|
+
api_key_config=getattr(client.config, "api_key_config", None),
|
|
498
|
+
)
|
|
499
|
+
trace = ToolLoopTrace()
|
|
500
|
+
pending_tool_call_ids: list[str] = []
|
|
501
|
+
|
|
502
|
+
# Lazily import the llm_io helpers only when logging is requested —
|
|
503
|
+
# matches classic's per-call lazy-import pattern in profile_deduplicator.py.
|
|
504
|
+
if log_label:
|
|
505
|
+
from reflexio.server.services.service_utils import (
|
|
506
|
+
log_llm_messages,
|
|
507
|
+
log_model_response,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# ---- Capability fallback ------------------------------------------
|
|
511
|
+
if not supports_tool_calling(model):
|
|
512
|
+
if multi_stage_schema is not None:
|
|
513
|
+
return _run_multi_stage_fallback(
|
|
514
|
+
client=client,
|
|
515
|
+
messages=messages,
|
|
516
|
+
registry=registry,
|
|
517
|
+
model_role=model_role,
|
|
518
|
+
max_steps=max_steps,
|
|
519
|
+
ctx=ctx,
|
|
520
|
+
finish_tool_name=finish_tool_name,
|
|
521
|
+
multi_stage_schema=multi_stage_schema,
|
|
522
|
+
log_label=log_label,
|
|
523
|
+
trace=trace,
|
|
524
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
525
|
+
)
|
|
526
|
+
if fallback_schema is None or fallback_tool_name is None:
|
|
527
|
+
raise RuntimeError(
|
|
528
|
+
f"Model {model} lacks tool-calling and no fallback_schema provided"
|
|
529
|
+
)
|
|
530
|
+
if log_label:
|
|
531
|
+
log_llm_messages(logger, f"{log_label} (fallback)", messages)
|
|
532
|
+
parsed = client.generate_chat_response(
|
|
533
|
+
messages=messages,
|
|
534
|
+
response_format=fallback_schema,
|
|
535
|
+
model_role=model_role,
|
|
536
|
+
)
|
|
537
|
+
if log_label:
|
|
538
|
+
log_model_response(logger, f"{log_label} (fallback)", parsed)
|
|
539
|
+
# The fallback path always passes response_format so the client
|
|
540
|
+
# returns a parsed BaseModel instance. Narrow the type so pyright
|
|
541
|
+
# can see model_fields is available.
|
|
542
|
+
if not isinstance(parsed, BaseModel):
|
|
543
|
+
raise RuntimeError(
|
|
544
|
+
f"Fallback structured call returned unexpected type {type(parsed)}"
|
|
545
|
+
)
|
|
546
|
+
# Expect the schema's first field to be a list of items whose
|
|
547
|
+
# ``model_dump_json()`` matches the fallback tool's args model.
|
|
548
|
+
items = getattr(parsed, next(iter(type(parsed).model_fields)))
|
|
549
|
+
# Respect the configured max_steps budget even on the fallback path
|
|
550
|
+
# — otherwise a non-tool-calling provider could blow past the loop
|
|
551
|
+
# cap when the structured response includes more items than expected.
|
|
552
|
+
bounded_items = items[:max_steps]
|
|
553
|
+
for item in bounded_items:
|
|
554
|
+
tool_t0 = time.monotonic()
|
|
555
|
+
outcome = registry.handle_outcome(
|
|
556
|
+
fallback_tool_name,
|
|
557
|
+
item.model_dump_json(),
|
|
558
|
+
ctx,
|
|
559
|
+
)
|
|
560
|
+
res = _tool_result_from_outcome(outcome, pending_tool_call_ids)
|
|
561
|
+
trace.turns.append(
|
|
562
|
+
ToolLoopTurn(
|
|
563
|
+
tool_name=fallback_tool_name,
|
|
564
|
+
args=item.model_dump(),
|
|
565
|
+
result=res,
|
|
566
|
+
latency_ms=int((time.monotonic() - tool_t0) * 1000),
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
exceeded = len(items) > max_steps
|
|
570
|
+
trace.finished = not exceeded
|
|
571
|
+
return ToolLoopResult(
|
|
572
|
+
ctx=ctx,
|
|
573
|
+
trace=trace,
|
|
574
|
+
finished_reason="max_steps" if exceeded else "finish_tool",
|
|
575
|
+
messages=messages,
|
|
576
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
577
|
+
max_steps_remaining=0 if exceeded else max_steps - len(bounded_items),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# ---- Native tool loop ---------------------------------------------
|
|
581
|
+
local_msgs = list(messages)
|
|
582
|
+
try:
|
|
583
|
+
for _step in range(max_steps):
|
|
584
|
+
if log_label:
|
|
585
|
+
log_llm_messages(logger, f"{log_label} (turn {_step + 1})", local_msgs)
|
|
586
|
+
resp = client.generate_chat_response(
|
|
587
|
+
messages=local_msgs,
|
|
588
|
+
tools=registry.openai_specs(),
|
|
589
|
+
tool_choice=tool_choice,
|
|
590
|
+
model_role=model_role,
|
|
591
|
+
)
|
|
592
|
+
if log_label:
|
|
593
|
+
log_model_response(logger, f"{log_label} (turn {_step + 1})", resp)
|
|
594
|
+
|
|
595
|
+
# Extract per-turn usage from the response (populated by LiteLLMClient
|
|
596
|
+
# when the provider reports it; None otherwise).
|
|
597
|
+
turn_usage = getattr(resp, "usage", None)
|
|
598
|
+
turn_prompt_tokens = (
|
|
599
|
+
getattr(turn_usage, "prompt_tokens", None) if turn_usage else None
|
|
600
|
+
)
|
|
601
|
+
turn_completion_tokens = (
|
|
602
|
+
getattr(turn_usage, "completion_tokens", None) if turn_usage else None
|
|
603
|
+
)
|
|
604
|
+
turn_total_tokens = (
|
|
605
|
+
getattr(turn_usage, "total_tokens", None) if turn_usage else None
|
|
606
|
+
)
|
|
607
|
+
turn_cost_usd = getattr(resp, "cost_usd", None)
|
|
608
|
+
|
|
609
|
+
tool_calls = getattr(resp, "tool_calls", None)
|
|
610
|
+
if not tool_calls:
|
|
611
|
+
# The model returned a plain-text turn with no tool calls. For a
|
|
612
|
+
# text-output agent this means "done", but the finish handler did
|
|
613
|
+
# NOT run, so no structured output was committed. Report a
|
|
614
|
+
# distinct reason so callers (and logs) don't conflate this with
|
|
615
|
+
# an actual finish_extraction call. Callers that require output
|
|
616
|
+
# (extraction) already gate success on committed output, so this
|
|
617
|
+
# surfaces accurately as a non-finish termination.
|
|
618
|
+
trace.finished = True
|
|
619
|
+
return ToolLoopResult(
|
|
620
|
+
ctx=ctx,
|
|
621
|
+
trace=trace,
|
|
622
|
+
finished_reason="no_tool_call",
|
|
623
|
+
messages=local_msgs,
|
|
624
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
625
|
+
max_steps_remaining=max_steps - _step,
|
|
626
|
+
)
|
|
627
|
+
# Emit ONE assistant message carrying ALL tool_calls from this turn.
|
|
628
|
+
# OpenAI/Anthropic strict mode requires this shape.
|
|
629
|
+
local_msgs.append(
|
|
630
|
+
{"role": "assistant", "content": None, "tool_calls": list(tool_calls)}
|
|
631
|
+
)
|
|
632
|
+
# Process every tool call and append per-call tool result messages.
|
|
633
|
+
# A single response's usage is attached to every turn it produced —
|
|
634
|
+
# the summary helpers dedup by (model, prompt_tokens, completion_tokens).
|
|
635
|
+
for tc in tool_calls:
|
|
636
|
+
# Time each tool individually — using the turn-start clock
|
|
637
|
+
# would inflate later tools' latencies with model time and
|
|
638
|
+
# earlier tools' work, masking the actual per-tool cost.
|
|
639
|
+
tool_t0 = time.monotonic()
|
|
640
|
+
name = tc.function.name
|
|
641
|
+
args_json = tc.function.arguments
|
|
642
|
+
outcome = registry.handle_outcome(name, args_json, ctx)
|
|
643
|
+
result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
|
|
644
|
+
try:
|
|
645
|
+
args_dict = json.loads(args_json or "{}")
|
|
646
|
+
except json.JSONDecodeError:
|
|
647
|
+
args_dict = {}
|
|
648
|
+
trace.turns.append(
|
|
649
|
+
ToolLoopTurn(
|
|
650
|
+
tool_name=name,
|
|
651
|
+
args=args_dict,
|
|
652
|
+
result=result,
|
|
653
|
+
latency_ms=int((time.monotonic() - tool_t0) * 1000),
|
|
654
|
+
model=model,
|
|
655
|
+
prompt_tokens=turn_prompt_tokens,
|
|
656
|
+
completion_tokens=turn_completion_tokens,
|
|
657
|
+
total_tokens=turn_total_tokens,
|
|
658
|
+
cost_usd=turn_cost_usd,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
local_msgs.append(
|
|
662
|
+
{
|
|
663
|
+
"role": "tool",
|
|
664
|
+
"tool_call_id": tc.id,
|
|
665
|
+
"content": json.dumps(result),
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
# After processing ALL tool calls, check whether the finish sentinel
|
|
669
|
+
# appeared in this turn (may be alongside sibling calls).
|
|
670
|
+
if any(tc.function.name == finish_tool_name for tc in tool_calls):
|
|
671
|
+
trace.finished = True
|
|
672
|
+
return ToolLoopResult(
|
|
673
|
+
ctx=ctx,
|
|
674
|
+
trace=trace,
|
|
675
|
+
finished_reason="finish_tool",
|
|
676
|
+
messages=local_msgs,
|
|
677
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
678
|
+
max_steps_remaining=max_steps - _step - 1,
|
|
679
|
+
)
|
|
680
|
+
except Exception:
|
|
681
|
+
logger.exception("Tool loop raised an unexpected exception")
|
|
682
|
+
trace.finished = False
|
|
683
|
+
return ToolLoopResult(
|
|
684
|
+
ctx=ctx,
|
|
685
|
+
trace=trace,
|
|
686
|
+
finished_reason="error",
|
|
687
|
+
messages=local_msgs,
|
|
688
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
689
|
+
max_steps_remaining=0,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return ToolLoopResult(
|
|
693
|
+
ctx=ctx,
|
|
694
|
+
trace=trace,
|
|
695
|
+
finished_reason="max_steps",
|
|
696
|
+
messages=local_msgs,
|
|
697
|
+
pending_tool_call_ids=pending_tool_call_ids,
|
|
698
|
+
max_steps_remaining=0,
|
|
699
|
+
)
|