polyrob 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
from inspect import iscoroutinefunction, signature
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Type, TYPE_CHECKING
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
# Native types
|
|
9
|
+
from modules.llm.adapters import BaseChatModel
|
|
10
|
+
from pydantic import BaseModel, Field, create_model
|
|
11
|
+
|
|
12
|
+
from tools.browser.context import BrowserContext
|
|
13
|
+
from tools.controller.registry.views import (
|
|
14
|
+
ActionModel,
|
|
15
|
+
ActionRegistry,
|
|
16
|
+
RegisteredAction,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from tools.controller.execution_context import ActionExecutionContext
|
|
21
|
+
from tools.controller.registry.schema_generators import (
|
|
22
|
+
get_schema_generator,
|
|
23
|
+
ToolSchemaGenerator,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Controller handles all tool management
|
|
27
|
+
|
|
28
|
+
# Import logging config
|
|
29
|
+
from agents.task.logging_config import get_task_logger
|
|
30
|
+
|
|
31
|
+
# Import telemetry - handle possible import errors
|
|
32
|
+
try:
|
|
33
|
+
from agents.task.telemetry import get_telemetry
|
|
34
|
+
from agents.task.telemetry.views import (
|
|
35
|
+
ControllerRegisteredFunctionsTelemetryEvent,
|
|
36
|
+
RegisteredFunction,
|
|
37
|
+
)
|
|
38
|
+
except ImportError:
|
|
39
|
+
# Create dummy telemetry if imports fail
|
|
40
|
+
def get_telemetry():
|
|
41
|
+
class DummyTelemetry:
|
|
42
|
+
def capture(self, *args, **kwargs):
|
|
43
|
+
pass
|
|
44
|
+
return DummyTelemetry()
|
|
45
|
+
|
|
46
|
+
class ControllerRegisteredFunctionsTelemetryEvent:
|
|
47
|
+
def __init__(self, **kwargs):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
class RegisteredFunction:
|
|
51
|
+
def __init__(self, **kwargs):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Registry:
|
|
56
|
+
"""Service for registering and managing actions"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self, exclude_actions: Optional[list[str]] = None, output_model: Optional[Type[BaseModel]] = None, session_id: Optional[str] = None, enforce_execution_context: bool = False
|
|
60
|
+
):
|
|
61
|
+
"""Registry of actions that the agent can use.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
exclude_actions: List of action names to exclude from the registry.
|
|
65
|
+
output_model: Model to use for action results.
|
|
66
|
+
session_id: Session ID for telemetry purposes.
|
|
67
|
+
enforce_execution_context: If True, require execution_context and disallow legacy parameters.
|
|
68
|
+
"""
|
|
69
|
+
self.exclude_actions = exclude_actions if exclude_actions is not None else []
|
|
70
|
+
self.output_model = output_model
|
|
71
|
+
self.session_id = session_id
|
|
72
|
+
self.enforce_execution_context = enforce_execution_context
|
|
73
|
+
|
|
74
|
+
# Initialize logger
|
|
75
|
+
self.logger = get_task_logger("registry", session_id)
|
|
76
|
+
|
|
77
|
+
# Initialize telemetry
|
|
78
|
+
self.telemetry = get_telemetry()
|
|
79
|
+
|
|
80
|
+
# Initialize registry with thread safety
|
|
81
|
+
self.registry = ActionRegistry()
|
|
82
|
+
self._registry_lock = threading.RLock() # Thread-safe registry access for both sync and async
|
|
83
|
+
|
|
84
|
+
# Tool-schema memoization: provider schema lists are byte-stable for a given
|
|
85
|
+
# set of registered actions, but were regenerated on every step (~3.7k tokens'
|
|
86
|
+
# worth of work + re-send per call). Cache them keyed on (provider, action-set,
|
|
87
|
+
# exclusions); the key changes whenever actions are added/removed, so it
|
|
88
|
+
# self-invalidates without explicit bumping.
|
|
89
|
+
self._provider_schema_cache: dict = {}
|
|
90
|
+
|
|
91
|
+
# Track action registration for logging only
|
|
92
|
+
self._action_registration_logged = set() # Track logged actions
|
|
93
|
+
|
|
94
|
+
# Register default actions
|
|
95
|
+
self._register_default_actions()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _set_tool_attribution(self, function: Callable, tool_name: str) -> None:
|
|
99
|
+
"""Set tool attribution on a function (SINGLE source of truth for attribution).
|
|
100
|
+
|
|
101
|
+
Handles all the edge cases: bound methods, wrapped functions, etc.
|
|
102
|
+
This is the ONLY place where _tool and _service should be set.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
function: The callable to set attribution on
|
|
106
|
+
tool_name: Name of the tool to attribute to
|
|
107
|
+
"""
|
|
108
|
+
import inspect
|
|
109
|
+
|
|
110
|
+
# Skip if already set to avoid overwriting
|
|
111
|
+
if hasattr(function, '_tool') and function._tool == tool_name:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Try to set attributes, handle cases where it's not possible
|
|
115
|
+
try:
|
|
116
|
+
# For bound methods, we can't set attributes directly
|
|
117
|
+
# Store in a registry instead (future: use WeakKeyDictionary)
|
|
118
|
+
if inspect.ismethod(function):
|
|
119
|
+
# Can't set attributes on bound methods
|
|
120
|
+
# The RegisteredAction object will store this instead
|
|
121
|
+
pass
|
|
122
|
+
else:
|
|
123
|
+
# For regular functions and unbound methods
|
|
124
|
+
function._tool = tool_name
|
|
125
|
+
function._service = tool_name # Keep for backward compat
|
|
126
|
+
except (AttributeError, TypeError):
|
|
127
|
+
# Some callables don't support attribute assignment
|
|
128
|
+
# RegisteredAction will store the tool name instead
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def _create_param_model(self, function: Callable) -> Type[BaseModel]:
|
|
132
|
+
"""Creates a Pydantic model from function signature
|
|
133
|
+
|
|
134
|
+
Handles missing type annotations by providing reasonable defaults.
|
|
135
|
+
"""
|
|
136
|
+
sig = signature(function)
|
|
137
|
+
import inspect
|
|
138
|
+
|
|
139
|
+
params = {}
|
|
140
|
+
for name, param in sig.parameters.items():
|
|
141
|
+
# Skip self, browser, LLM, file path, and execution_context parameters
|
|
142
|
+
if name in ['self', 'browser', 'page_extraction_llm', 'available_file_paths', 'execution_context']:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Handle missing type annotations with str as default
|
|
146
|
+
if param.annotation is inspect._empty:
|
|
147
|
+
annotation = str
|
|
148
|
+
else:
|
|
149
|
+
annotation = param.annotation
|
|
150
|
+
|
|
151
|
+
# Handle missing default values
|
|
152
|
+
if param.default is inspect._empty:
|
|
153
|
+
params[name] = (annotation, ...)
|
|
154
|
+
else:
|
|
155
|
+
params[name] = (annotation, param.default)
|
|
156
|
+
|
|
157
|
+
# Create the model with a unique name based on the function
|
|
158
|
+
return create_model(
|
|
159
|
+
f'{function.__name__}_parameters',
|
|
160
|
+
__base__=ActionModel,
|
|
161
|
+
**params, # type: ignore
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def action(
|
|
165
|
+
self,
|
|
166
|
+
description: str,
|
|
167
|
+
param_model: Optional[Type[BaseModel]] = None,
|
|
168
|
+
tool: Optional[str] = None
|
|
169
|
+
):
|
|
170
|
+
"""Decorator for registering actions
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
description: Description of what the action does
|
|
174
|
+
param_model: Optional Pydantic model for parameters
|
|
175
|
+
tool: Optional tool name to attribute the action to
|
|
176
|
+
"""
|
|
177
|
+
def decorator(func: Callable):
|
|
178
|
+
# Skip registration if action is in exclude_actions
|
|
179
|
+
if self.exclude_actions and func.__name__ in self.exclude_actions:
|
|
180
|
+
return func
|
|
181
|
+
|
|
182
|
+
# Create param model from function if not provided
|
|
183
|
+
actual_param_model = param_model or self._create_param_model(func)
|
|
184
|
+
|
|
185
|
+
# Wrap sync functions to make them async
|
|
186
|
+
if not iscoroutinefunction(func):
|
|
187
|
+
async def async_wrapper(*args, **kwargs):
|
|
188
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
189
|
+
|
|
190
|
+
# Copy the signature and other metadata from the original function
|
|
191
|
+
async_wrapper.__signature__ = signature(func)
|
|
192
|
+
async_wrapper.__name__ = func.__name__
|
|
193
|
+
async_wrapper.__annotations__ = func.__annotations__
|
|
194
|
+
wrapped_func = async_wrapper
|
|
195
|
+
else:
|
|
196
|
+
wrapped_func = func
|
|
197
|
+
|
|
198
|
+
action = RegisteredAction(
|
|
199
|
+
name=func.__name__,
|
|
200
|
+
description=description,
|
|
201
|
+
function=wrapped_func,
|
|
202
|
+
param_model=actual_param_model,
|
|
203
|
+
tool=tool
|
|
204
|
+
)
|
|
205
|
+
self.registry.actions[func.__name__] = action
|
|
206
|
+
|
|
207
|
+
# Log registration with tool attribution if provided
|
|
208
|
+
if tool:
|
|
209
|
+
self.logger.debug(f"Registered action '{func.__name__}' with tool '{tool}'")
|
|
210
|
+
else:
|
|
211
|
+
self.logger.debug(f"Registered action '{func.__name__}'")
|
|
212
|
+
|
|
213
|
+
return func
|
|
214
|
+
|
|
215
|
+
return decorator
|
|
216
|
+
|
|
217
|
+
def remove_action(self, action_name: str) -> bool:
|
|
218
|
+
"""Unregister an action by name. Returns True if it existed and was removed.
|
|
219
|
+
|
|
220
|
+
Thread-safe and busts the per-provider schema cache so the removed action
|
|
221
|
+
stops appearing in generated tool schemas. Without this, Controller.remove_tool
|
|
222
|
+
was a silent no-op (it probed for a non-existent remove_action), leaving a
|
|
223
|
+
"removed" tool's actions fully registered and callable — a capability-revocation
|
|
224
|
+
hole.
|
|
225
|
+
"""
|
|
226
|
+
with self._registry_lock:
|
|
227
|
+
if action_name not in self.registry.actions:
|
|
228
|
+
return False
|
|
229
|
+
del self.registry.actions[action_name]
|
|
230
|
+
self._action_registration_logged.discard(action_name)
|
|
231
|
+
# Schema cache is keyed on the action-set; bust it so the dropped action
|
|
232
|
+
# is no longer emitted to providers.
|
|
233
|
+
self._provider_schema_cache.clear()
|
|
234
|
+
self.logger.debug(f"Removed action '{action_name}'")
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
async def execute_action(
|
|
238
|
+
self,
|
|
239
|
+
action_name: str,
|
|
240
|
+
params: dict,
|
|
241
|
+
execution_context: Optional['ActionExecutionContext'] = None,
|
|
242
|
+
# Legacy parameters for backward compatibility
|
|
243
|
+
browser: Optional[BrowserContext] = None,
|
|
244
|
+
page_extraction_llm: Optional[BaseChatModel] = None,
|
|
245
|
+
sensitive_data: Optional[Dict[str, str]] = None,
|
|
246
|
+
available_file_paths: Optional[list[str]] = None,
|
|
247
|
+
session_id: Optional[str] = None,
|
|
248
|
+
) -> Any:
|
|
249
|
+
"""Execute a registered action with thread-safe registry access"""
|
|
250
|
+
|
|
251
|
+
# Enforce execution context if required
|
|
252
|
+
if self.enforce_execution_context:
|
|
253
|
+
if not execution_context:
|
|
254
|
+
raise ValueError("execution_context is required when enforce_execution_context=True")
|
|
255
|
+
if any([browser, page_extraction_llm, sensitive_data, available_file_paths, session_id]):
|
|
256
|
+
self.logger.debug("Legacy parameters ignored when enforce_execution_context=True")
|
|
257
|
+
|
|
258
|
+
# Build execution context from parameters if not provided
|
|
259
|
+
if not execution_context:
|
|
260
|
+
# Import here to avoid circular dependency
|
|
261
|
+
from tools.controller.execution_context import ActionExecutionContext
|
|
262
|
+
execution_context = ActionExecutionContext(
|
|
263
|
+
browser_context=browser,
|
|
264
|
+
session_id=session_id or self.session_id,
|
|
265
|
+
workspace_dir=getattr(self, 'workspace_dir', None),
|
|
266
|
+
available_file_paths=available_file_paths or [],
|
|
267
|
+
sensitive_data=sensitive_data or {},
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
# Override context with explicit parameters if provided
|
|
271
|
+
if browser:
|
|
272
|
+
execution_context.browser_context = browser
|
|
273
|
+
if sensitive_data:
|
|
274
|
+
execution_context.sensitive_data = sensitive_data
|
|
275
|
+
if available_file_paths:
|
|
276
|
+
execution_context.available_file_paths = available_file_paths
|
|
277
|
+
if session_id:
|
|
278
|
+
execution_context.session_id = session_id
|
|
279
|
+
|
|
280
|
+
with self._registry_lock:
|
|
281
|
+
if action_name not in self.registry.actions:
|
|
282
|
+
raise ValueError(f'Action {action_name} not found')
|
|
283
|
+
|
|
284
|
+
action = self.registry.actions[action_name]
|
|
285
|
+
try:
|
|
286
|
+
# Handle None params - convert to empty dict
|
|
287
|
+
if params is None:
|
|
288
|
+
params = {}
|
|
289
|
+
# WS-1.3: systematic camelCase→snake_case reconciliation against the
|
|
290
|
+
# target model's fields (e.g. kimi-k2.6 emits filePath for file_path).
|
|
291
|
+
# Safe for genuinely-camelCase tool params (MCP): only renames when the
|
|
292
|
+
# snake_case form is an actual field of this action's param model.
|
|
293
|
+
if action.param_model is not None:
|
|
294
|
+
try:
|
|
295
|
+
from agents.task.utils_json import reconcile_field_names_to_model
|
|
296
|
+
params = reconcile_field_names_to_model(
|
|
297
|
+
params, set(action.param_model.model_fields.keys())
|
|
298
|
+
)
|
|
299
|
+
except Exception as _e:
|
|
300
|
+
self.logger.debug(f"Field-name reconciliation skipped: {_e}")
|
|
301
|
+
# Create the validated Pydantic model
|
|
302
|
+
validated_params = action.param_model(**params)
|
|
303
|
+
|
|
304
|
+
# Check if the first parameter is a Pydantic model
|
|
305
|
+
sig = signature(action.function)
|
|
306
|
+
parameters = list(sig.parameters.values())
|
|
307
|
+
# Guard against missing/invalid annotations before issubclass
|
|
308
|
+
is_pydantic = False
|
|
309
|
+
if parameters:
|
|
310
|
+
first_anno = parameters[0].annotation
|
|
311
|
+
try:
|
|
312
|
+
is_pydantic = isinstance(first_anno, type) and issubclass(first_anno, BaseModel)
|
|
313
|
+
except TypeError:
|
|
314
|
+
is_pydantic = False
|
|
315
|
+
# PEP 563 / `from __future__ import annotations`: the annotation is a
|
|
316
|
+
# *string* (e.g. "GoalCreateAction"), so the isinstance/issubclass
|
|
317
|
+
# check above is False and we'd wrongly splat the model fields as
|
|
318
|
+
# kwargs into a `(self, params: Model, ...)` signature → TypeError.
|
|
319
|
+
# When the action was registered with an explicit param_model, trust
|
|
320
|
+
# it: if the first param's stringized annotation names that model,
|
|
321
|
+
# route the validated model as a single positional arg.
|
|
322
|
+
if (
|
|
323
|
+
not is_pydantic
|
|
324
|
+
and isinstance(first_anno, str)
|
|
325
|
+
and action.param_model is not None
|
|
326
|
+
and first_anno == action.param_model.__name__
|
|
327
|
+
):
|
|
328
|
+
is_pydantic = True
|
|
329
|
+
parameter_names = [param.name for param in parameters]
|
|
330
|
+
|
|
331
|
+
if execution_context.sensitive_data:
|
|
332
|
+
validated_params = self._replace_sensitive_data(validated_params, execution_context.sensitive_data)
|
|
333
|
+
|
|
334
|
+
# Check if action accepts execution_context parameter
|
|
335
|
+
if 'execution_context' in parameter_names:
|
|
336
|
+
# Modern approach: pass the whole context
|
|
337
|
+
extra_args = {'execution_context': execution_context}
|
|
338
|
+
else:
|
|
339
|
+
# Legacy approach: pass individual parameters
|
|
340
|
+
if 'browser' in parameter_names and not execution_context.browser_context:
|
|
341
|
+
raise ValueError(f'Action {action_name} requires browser but none provided.')
|
|
342
|
+
if 'page_extraction_llm' in parameter_names and not page_extraction_llm:
|
|
343
|
+
raise ValueError(f'Action {action_name} requires page_extraction_llm but none provided.')
|
|
344
|
+
if 'available_file_paths' in parameter_names and not execution_context.available_file_paths:
|
|
345
|
+
raise ValueError(f'Action {action_name} requires available_file_paths but none provided.')
|
|
346
|
+
|
|
347
|
+
# Prepare arguments based on parameter type
|
|
348
|
+
extra_args = {}
|
|
349
|
+
if 'browser' in parameter_names:
|
|
350
|
+
extra_args['browser'] = execution_context.browser_context
|
|
351
|
+
if 'page_extraction_llm' in parameter_names:
|
|
352
|
+
extra_args['page_extraction_llm'] = page_extraction_llm
|
|
353
|
+
if 'available_file_paths' in parameter_names:
|
|
354
|
+
extra_args['available_file_paths'] = execution_context.available_file_paths
|
|
355
|
+
|
|
356
|
+
# NOTE: Runtime tool mutation removed per upgrade instructions
|
|
357
|
+
# All per-execution data should flow via execution_context only
|
|
358
|
+
# Tools should not be mutated at runtime
|
|
359
|
+
|
|
360
|
+
# EXCEPTION: User context propagation for tools that need it
|
|
361
|
+
# This is NOT mutation - it's telling the tool which user's credentials to use
|
|
362
|
+
# Tools like PolymarketTool and MCPTool require user_id to load correct credentials
|
|
363
|
+
if execution_context and execution_context.user_id:
|
|
364
|
+
# Check if action function is a bound method with set_user_context
|
|
365
|
+
if hasattr(action.function, '__self__'):
|
|
366
|
+
tool_instance = action.function.__self__
|
|
367
|
+
if hasattr(tool_instance, 'set_user_context'):
|
|
368
|
+
tool_instance.set_user_context(execution_context.user_id)
|
|
369
|
+
self.logger.debug(f"Set user context '{execution_context.user_id}' on {action.tool or 'unknown'} tool")
|
|
370
|
+
|
|
371
|
+
# Execute the function and explicitly await the result
|
|
372
|
+
if is_pydantic:
|
|
373
|
+
result = await action.function(validated_params, **extra_args)
|
|
374
|
+
else:
|
|
375
|
+
result = await action.function(**validated_params.model_dump(), **extra_args)
|
|
376
|
+
|
|
377
|
+
# Handle the case where result might be another coroutine (nested async calls)
|
|
378
|
+
import inspect
|
|
379
|
+
if inspect.iscoroutine(result):
|
|
380
|
+
result = await result
|
|
381
|
+
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
raise RuntimeError(f'Error executing action {action_name}: {str(e)}') from e
|
|
386
|
+
|
|
387
|
+
def _replace_sensitive_data(self, params: BaseModel, sensitive_data: Dict[str, str]) -> BaseModel:
|
|
388
|
+
"""Replaces the sensitive data in the params"""
|
|
389
|
+
# if there are any str with <secret>placeholder</secret> in the params, replace them with the actual value from sensitive_data
|
|
390
|
+
|
|
391
|
+
import re
|
|
392
|
+
|
|
393
|
+
secret_pattern = re.compile(r'<secret>(.*?)</secret>')
|
|
394
|
+
|
|
395
|
+
def replace_secrets(value):
|
|
396
|
+
if isinstance(value, str):
|
|
397
|
+
matches = secret_pattern.findall(value)
|
|
398
|
+
for placeholder in matches:
|
|
399
|
+
if placeholder in sensitive_data:
|
|
400
|
+
value = value.replace(f'<secret>{placeholder}</secret>', sensitive_data[placeholder])
|
|
401
|
+
return value
|
|
402
|
+
elif isinstance(value, dict):
|
|
403
|
+
return {k: replace_secrets(v) for k, v in value.items()}
|
|
404
|
+
elif isinstance(value, list):
|
|
405
|
+
return [replace_secrets(v) for v in value]
|
|
406
|
+
return value
|
|
407
|
+
|
|
408
|
+
# Only write back fields where a secret was actually substituted. Writing the
|
|
409
|
+
# model_dump()'d value back for EVERY field coerces non-string field types
|
|
410
|
+
# (a nested BaseModel becomes a dict, a datetime a str, etc.) even when no
|
|
411
|
+
# secret was present.
|
|
412
|
+
for key, value in params.model_dump().items():
|
|
413
|
+
new_value = replace_secrets(value)
|
|
414
|
+
if new_value != value:
|
|
415
|
+
params.__dict__[key] = new_value
|
|
416
|
+
return params
|
|
417
|
+
|
|
418
|
+
def create_action_model(self) -> Type[ActionModel]:
|
|
419
|
+
"""Creates a Pydantic model from registered actions"""
|
|
420
|
+
fields = {
|
|
421
|
+
name: (
|
|
422
|
+
Optional[action.param_model],
|
|
423
|
+
Field(default=None, description=action.description),
|
|
424
|
+
)
|
|
425
|
+
for name, action in self.registry.actions.items()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Group actions by tool for better logging
|
|
429
|
+
actions_by_tool = {}
|
|
430
|
+
# Create thread-safe snapshot to avoid iterator corruption
|
|
431
|
+
for name, action in list(self.registry.actions.items()):
|
|
432
|
+
tool = action.tool or "default"
|
|
433
|
+
if tool not in actions_by_tool:
|
|
434
|
+
actions_by_tool[tool] = []
|
|
435
|
+
actions_by_tool[tool].append(name)
|
|
436
|
+
|
|
437
|
+
# Log actions by tool
|
|
438
|
+
for tool, actions in actions_by_tool.items():
|
|
439
|
+
self.logger.debug(f"Tool '{tool}' has {len(actions)} actions: {', '.join(actions[:5])}..." if len(actions) > 5 else f"Tool '{tool}' has {len(actions)} actions: {', '.join(actions)}")
|
|
440
|
+
|
|
441
|
+
# Prepare registered functions for telemetry
|
|
442
|
+
registered_functions = [
|
|
443
|
+
RegisteredFunction(
|
|
444
|
+
name=name,
|
|
445
|
+
params=action.param_model.model_json_schema(),
|
|
446
|
+
service=action.tool or "default" # SINGULAR: tool field only, no fallback
|
|
447
|
+
)
|
|
448
|
+
for name, action in self.registry.actions.items()
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
self.telemetry.capture(
|
|
452
|
+
ControllerRegisteredFunctionsTelemetryEvent(
|
|
453
|
+
registered_functions=registered_functions
|
|
454
|
+
),
|
|
455
|
+
session_id=self.session_id
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
return create_model('ActionModel', __base__=ActionModel, **fields) # type:ignore
|
|
459
|
+
|
|
460
|
+
def get_prompt_description(self) -> str:
|
|
461
|
+
"""Get a description of all actions for the prompt"""
|
|
462
|
+
return self.registry.get_prompt_description()
|
|
463
|
+
|
|
464
|
+
def get_prompt_action_index(self) -> str:
|
|
465
|
+
"""Compact one-line-per-action index for native tool mode (T1-03)."""
|
|
466
|
+
return self.registry.get_prompt_action_index()
|
|
467
|
+
|
|
468
|
+
def get_action_schema(self, action_name: str) -> Dict[str, Any]:
|
|
469
|
+
"""Get the expected schema for a specific action
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
action_name: Name of the action
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Dictionary containing field information for the action
|
|
476
|
+
"""
|
|
477
|
+
if action_name not in self.registry.actions:
|
|
478
|
+
return {}
|
|
479
|
+
|
|
480
|
+
action = self.registry.actions[action_name]
|
|
481
|
+
schema = {}
|
|
482
|
+
|
|
483
|
+
if action.param_model:
|
|
484
|
+
try:
|
|
485
|
+
for field_name, field_info in action.param_model.model_fields.items():
|
|
486
|
+
schema[field_name] = {
|
|
487
|
+
'required': field_info.is_required(),
|
|
488
|
+
'type': str(field_info.annotation)
|
|
489
|
+
}
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
|
|
493
|
+
return schema
|
|
494
|
+
|
|
495
|
+
def get_actions(self) -> Dict[str, Any]:
|
|
496
|
+
"""Get all registered actions with their details.
|
|
497
|
+
|
|
498
|
+
This is a stable public API method to access actions without
|
|
499
|
+
directly accessing internal registry structures.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Dictionary mapping action names to their details
|
|
503
|
+
"""
|
|
504
|
+
if hasattr(self.registry, 'actions'):
|
|
505
|
+
return dict(self.registry.actions)
|
|
506
|
+
return {}
|
|
507
|
+
|
|
508
|
+
def get_action_names(self) -> list[str]:
|
|
509
|
+
"""Get list of all registered action names.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
List of action names
|
|
513
|
+
"""
|
|
514
|
+
if hasattr(self.registry, 'actions'):
|
|
515
|
+
return list(self.registry.actions.keys())
|
|
516
|
+
return []
|
|
517
|
+
|
|
518
|
+
def get_action_count(self) -> int:
|
|
519
|
+
"""Get the number of registered actions.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Number of registered actions
|
|
523
|
+
"""
|
|
524
|
+
if hasattr(self.registry, 'actions'):
|
|
525
|
+
return len(self.registry.actions)
|
|
526
|
+
return 0
|
|
527
|
+
|
|
528
|
+
def get_action_details(self, action_name: str) -> Optional[Any]:
|
|
529
|
+
"""Get details for a specific action.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
action_name: Name of the action to get details for
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Action details if found, None otherwise
|
|
536
|
+
"""
|
|
537
|
+
if hasattr(self.registry, 'actions'):
|
|
538
|
+
return self.registry.actions.get(action_name)
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
def has_action(self, action_name: str) -> bool:
|
|
542
|
+
"""Check if an action is registered.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
action_name: Name of the action to check
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
True if action is registered, False otherwise
|
|
549
|
+
"""
|
|
550
|
+
if hasattr(self.registry, 'actions'):
|
|
551
|
+
return action_name in self.registry.actions
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
def get_actions_by_service(self, service_name: str) -> Dict[str, Any]:
|
|
555
|
+
"""Get all actions for a specific tool/service.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
service_name: Name of the tool/service
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Dictionary of actions belonging to the tool
|
|
562
|
+
"""
|
|
563
|
+
if not hasattr(self.registry, 'actions'):
|
|
564
|
+
return {}
|
|
565
|
+
|
|
566
|
+
service_actions = {}
|
|
567
|
+
# Create thread-safe snapshot to avoid iterator corruption
|
|
568
|
+
for name, action in list(self.registry.actions.items()):
|
|
569
|
+
# Check both 'tool' and '_tool' attributes (tool is the primary attribute)
|
|
570
|
+
if ((hasattr(action, 'tool') and action.tool == service_name) or
|
|
571
|
+
(hasattr(action, '_tool') and action._tool == service_name)):
|
|
572
|
+
service_actions[name] = action
|
|
573
|
+
return service_actions
|
|
574
|
+
|
|
575
|
+
def get_tools(self) -> list[str]:
|
|
576
|
+
"""Get list of all tools that have registered actions.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
List of unique tool names
|
|
580
|
+
"""
|
|
581
|
+
if not hasattr(self.registry, 'actions'):
|
|
582
|
+
return []
|
|
583
|
+
|
|
584
|
+
tools = set()
|
|
585
|
+
for action in self.registry.actions.values():
|
|
586
|
+
if hasattr(action, 'tool') and action.tool:
|
|
587
|
+
tools.add(action.tool)
|
|
588
|
+
return list(tools)
|
|
589
|
+
|
|
590
|
+
# Keep deprecated alias for backward compatibility
|
|
591
|
+
def _register_default_actions(self) -> None:
|
|
592
|
+
"""Register default actions if available.
|
|
593
|
+
|
|
594
|
+
Handles core actions like 'done' which are not tool-specific.
|
|
595
|
+
"""
|
|
596
|
+
# NOTE: The 'done' action is now registered by the Controller
|
|
597
|
+
# which has more sophisticated logic including todo progress checking
|
|
598
|
+
# and quality gate enforcement. This method is kept for future
|
|
599
|
+
# default actions that might be needed at the registry level.
|
|
600
|
+
pass
|
|
601
|
+
|
|
602
|
+
def extract_tool_actions(self, tool_name: str, tool: Any) -> Dict[str, Callable]:
|
|
603
|
+
"""Extract all available actions from a tool.
|
|
604
|
+
|
|
605
|
+
This is the public API for extracting actions. It returns action functions
|
|
606
|
+
with metadata attached (_tool, _service, _description, _param_model).
|
|
607
|
+
The returned actions are NOT yet registered - call wrap_function() to register.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
tool_name: Name of the tool
|
|
611
|
+
tool: Tool instance
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Dictionary of action_name -> action_function
|
|
615
|
+
"""
|
|
616
|
+
actions = {}
|
|
617
|
+
|
|
618
|
+
# Method 1: Check for get_actions method - preferred approach
|
|
619
|
+
if hasattr(tool, 'get_actions') and callable(getattr(tool, 'get_actions')):
|
|
620
|
+
try:
|
|
621
|
+
self.logger.debug(f"Attempting to get actions from {tool_name}.get_actions()")
|
|
622
|
+
service_actions = tool.get_actions()
|
|
623
|
+
if service_actions and isinstance(service_actions, dict):
|
|
624
|
+
# Normalize actions - some services return {name: function} and others
|
|
625
|
+
# return {name: {function: func, description: desc, param_model: model}}
|
|
626
|
+
for name, action in service_actions.items():
|
|
627
|
+
if callable(action):
|
|
628
|
+
# Simple action is directly callable
|
|
629
|
+
actions[name] = action
|
|
630
|
+
# Fix missing _tool attribute - check if it's a bound method first
|
|
631
|
+
try:
|
|
632
|
+
if not hasattr(action, '_tool') or action._tool is None:
|
|
633
|
+
self._set_tool_attribution(action, tool_name)
|
|
634
|
+
# Keep _service for backward compat
|
|
635
|
+
if not hasattr(action, '_service') or action._service is None:
|
|
636
|
+
action._service = tool_name
|
|
637
|
+
self.logger.debug(f"Applied tool name '{tool_name}' to action '{name}'")
|
|
638
|
+
except AttributeError:
|
|
639
|
+
# Can't set attributes on bound methods, skip
|
|
640
|
+
self.logger.debug(f"Skipping attribute setting for bound method '{name}' from tool '{tool_name}'")
|
|
641
|
+
elif isinstance(action, dict) and 'function' in action:
|
|
642
|
+
# Complex action has additional metadata
|
|
643
|
+
if callable(action['function']):
|
|
644
|
+
# Store the function with its metadata as attributes
|
|
645
|
+
func = action['function']
|
|
646
|
+
|
|
647
|
+
# Add description as attribute if available
|
|
648
|
+
if 'description' in action:
|
|
649
|
+
func._description = action['description']
|
|
650
|
+
|
|
651
|
+
# Add param_model as attribute if available
|
|
652
|
+
if 'param_model' in action:
|
|
653
|
+
func._param_model = action['param_model']
|
|
654
|
+
|
|
655
|
+
# Add tool name as attribute
|
|
656
|
+
self._set_tool_attribution(func, tool_name)
|
|
657
|
+
# Keep _service for backward compat
|
|
658
|
+
func._service = tool_name
|
|
659
|
+
|
|
660
|
+
# Store normalized action
|
|
661
|
+
actions[name] = func
|
|
662
|
+
else:
|
|
663
|
+
self.logger.warning(f"Action '{name}' from tool '{tool_name}' has non-callable function")
|
|
664
|
+
else:
|
|
665
|
+
self.logger.warning(f"Action '{name}' from tool '{tool_name}' has invalid format")
|
|
666
|
+
except Exception as e:
|
|
667
|
+
self.logger.warning(f"❌ Error calling get_actions() on tool '{tool_name}': {e}")
|
|
668
|
+
# Try the register_decorated_actions method as a fallback
|
|
669
|
+
if hasattr(tool, 'register_decorated_actions') and callable(tool.register_decorated_actions):
|
|
670
|
+
try:
|
|
671
|
+
self.logger.debug(f"Attempting to fix tool '{tool_name}' with register_decorated_actions")
|
|
672
|
+
tool.register_decorated_actions()
|
|
673
|
+
# Try get_actions again
|
|
674
|
+
tool_actions = tool.get_actions()
|
|
675
|
+
if tool_actions and isinstance(tool_actions, dict):
|
|
676
|
+
for name, action in tool_actions.items():
|
|
677
|
+
if callable(action):
|
|
678
|
+
actions[name] = action
|
|
679
|
+
if not hasattr(action, '_tool') and not hasattr(action, '_service'):
|
|
680
|
+
self._set_tool_attribution(action, tool_name)
|
|
681
|
+
elif isinstance(action, dict) and 'function' in action:
|
|
682
|
+
func = action['function']
|
|
683
|
+
self._set_tool_attribution(func, tool_name)
|
|
684
|
+
actions[name] = func
|
|
685
|
+
except Exception as e2:
|
|
686
|
+
self.logger.warning(f"Failed to use register_decorated_actions fallback for '{tool_name}': {e2}")
|
|
687
|
+
|
|
688
|
+
# Method 2: Check for actions attribute - fallback for legacy
|
|
689
|
+
if hasattr(tool, 'actions') and isinstance(tool.actions, dict):
|
|
690
|
+
for name, action in tool.actions.items():
|
|
691
|
+
if callable(action):
|
|
692
|
+
# Set required metadata if missing
|
|
693
|
+
if not hasattr(action, '_tool'):
|
|
694
|
+
self._set_tool_attribution(action, tool_name)
|
|
695
|
+
# Keep _service for backward compat
|
|
696
|
+
if not hasattr(action, '_service'):
|
|
697
|
+
action._service = tool_name
|
|
698
|
+
if not hasattr(action, '_description'):
|
|
699
|
+
action._description = f"Execute {name} from {tool_name} tool"
|
|
700
|
+
if not hasattr(action, '_param_model'):
|
|
701
|
+
try:
|
|
702
|
+
action._param_model = self._create_param_model(action)
|
|
703
|
+
except Exception:
|
|
704
|
+
continue # Skip if can't create param model
|
|
705
|
+
|
|
706
|
+
actions[name] = action
|
|
707
|
+
else:
|
|
708
|
+
self.logger.warning(f"Action '{name}' from tool '{tool_name}' is not callable")
|
|
709
|
+
|
|
710
|
+
# Method 3: Look for methods explicitly decorated with @action
|
|
711
|
+
# STRICT: Only accept methods with explicit action_info AND param_model
|
|
712
|
+
if not actions:
|
|
713
|
+
for attr_name in dir(tool):
|
|
714
|
+
# Skip private methods, properties and special methods
|
|
715
|
+
if attr_name.startswith('_') or attr_name in ('logger', 'config', 'container', 'status'):
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
attr = getattr(tool, attr_name)
|
|
719
|
+
|
|
720
|
+
# STRICT: Only accept if explicitly marked as action with both action_info and param_model
|
|
721
|
+
if callable(attr) and not isinstance(attr, property) and hasattr(attr, 'action_info'):
|
|
722
|
+
action_info = attr.action_info
|
|
723
|
+
|
|
724
|
+
# STRICT: Require both description and param_model in action_info
|
|
725
|
+
if 'description' not in action_info:
|
|
726
|
+
self.logger.debug(f"Skipping {attr_name}: action_info missing description")
|
|
727
|
+
continue
|
|
728
|
+
|
|
729
|
+
if 'param_model' not in action_info:
|
|
730
|
+
self.logger.debug(f"Skipping {attr_name}: action_info missing param_model")
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
# Add required metadata
|
|
734
|
+
self._set_tool_attribution(attr, tool_name)
|
|
735
|
+
attr._service = tool_name # Keep for backward compat
|
|
736
|
+
attr._description = action_info['description']
|
|
737
|
+
attr._param_model = action_info['param_model']
|
|
738
|
+
|
|
739
|
+
actions[attr_name] = attr
|
|
740
|
+
|
|
741
|
+
# Log found actions
|
|
742
|
+
if actions:
|
|
743
|
+
self.logger.debug(f"Found {len(actions)} actions for service '{tool_name}': {', '.join(list(actions.keys())[:5])}{'...' if len(actions) > 5 else ''}")
|
|
744
|
+
else:
|
|
745
|
+
self.logger.debug(f"No actions found for service '{tool_name}'")
|
|
746
|
+
|
|
747
|
+
return actions
|
|
748
|
+
|
|
749
|
+
def wrap_function(self, name: str, function: Callable, description: str, tool: Optional[str] = None, param_model: Optional[Type[BaseModel]] = None) -> None:
|
|
750
|
+
"""Register a function as an action with the registry.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
name: Name of the function
|
|
754
|
+
function: The function to register
|
|
755
|
+
description: Description of what the function does
|
|
756
|
+
tool: Optional tool name to attribute the action to
|
|
757
|
+
param_model: Optional parameter model to use instead of creating one
|
|
758
|
+
"""
|
|
759
|
+
# Skip registration if action is in exclude_actions (defensive check)
|
|
760
|
+
if self.exclude_actions and name in self.exclude_actions:
|
|
761
|
+
self.logger.debug(f"Skipping excluded action: {name}")
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
# Check for collision with existing action
|
|
765
|
+
if name in self.registry.actions:
|
|
766
|
+
existing = self.registry.actions[name]
|
|
767
|
+
existing_tool = existing.tool if hasattr(existing, 'tool') else 'core'
|
|
768
|
+
new_tool = tool if tool else 'core'
|
|
769
|
+
if existing_tool != new_tool:
|
|
770
|
+
# Make cross-tool collisions a hard error to enforce proper namespacing
|
|
771
|
+
error_msg = (
|
|
772
|
+
f"Action name collision: '{name}' already registered by tool '{existing_tool}', "
|
|
773
|
+
f"cannot override with tool '{new_tool}'. Use namespaced action names (e.g., '{new_tool}_{name}')"
|
|
774
|
+
)
|
|
775
|
+
self.logger.error(error_msg)
|
|
776
|
+
if self.enforce_execution_context:
|
|
777
|
+
# In strict mode, raise an error
|
|
778
|
+
raise ValueError(error_msg)
|
|
779
|
+
else:
|
|
780
|
+
# In legacy mode, skip registration with warning
|
|
781
|
+
self.logger.warning(f"Skipping registration of '{name}' from tool '{new_tool}' due to collision")
|
|
782
|
+
return
|
|
783
|
+
else:
|
|
784
|
+
# Same tool re-registering - log at INFO level for visibility
|
|
785
|
+
# This can indicate unintentional duplicate registrations
|
|
786
|
+
self.logger.info(
|
|
787
|
+
f"Re-registering action '{name}' for tool '{new_tool}' "
|
|
788
|
+
f"(previous registration will be replaced)"
|
|
789
|
+
)
|
|
790
|
+
# Continue to update the registration
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
# Skip trying to set attributes on bound methods
|
|
794
|
+
import inspect
|
|
795
|
+
if tool and not inspect.ismethod(function):
|
|
796
|
+
try:
|
|
797
|
+
if not hasattr(function, '_tool'):
|
|
798
|
+
self._set_tool_attribution(function, tool)
|
|
799
|
+
except AttributeError:
|
|
800
|
+
# Can't set attribute on this type of callable, skip
|
|
801
|
+
pass
|
|
802
|
+
|
|
803
|
+
# Handle param_model with controlled auto-generation
|
|
804
|
+
if param_model is not None:
|
|
805
|
+
# Use the provided param_model
|
|
806
|
+
pass
|
|
807
|
+
elif hasattr(function, '_param_model') and function._param_model is not None:
|
|
808
|
+
param_model = function._param_model
|
|
809
|
+
else:
|
|
810
|
+
# Auto-generate param_model with warning
|
|
811
|
+
self.logger.warning(
|
|
812
|
+
f"Action '{name}' missing param_model - auto-generating from function signature. "
|
|
813
|
+
f"Consider providing explicit param_model for better type safety."
|
|
814
|
+
)
|
|
815
|
+
try:
|
|
816
|
+
param_model = self._create_param_model(function)
|
|
817
|
+
# Cache on function for future use
|
|
818
|
+
if not inspect.ismethod(function):
|
|
819
|
+
try:
|
|
820
|
+
function._param_model = param_model
|
|
821
|
+
except AttributeError:
|
|
822
|
+
pass # Can't cache on this type
|
|
823
|
+
except Exception as e:
|
|
824
|
+
self.logger.error(f"Failed to auto-generate param_model for '{name}': {e}")
|
|
825
|
+
return # Fail if auto-generation fails
|
|
826
|
+
|
|
827
|
+
# Validate param_model is a proper Pydantic BaseModel subclass
|
|
828
|
+
from pydantic import BaseModel
|
|
829
|
+
if param_model is None or not isinstance(param_model, type) or not issubclass(param_model, BaseModel):
|
|
830
|
+
self.logger.error(f"Invalid param_model for action '{name}': must be a subclass of BaseModel")
|
|
831
|
+
return # Fail fast if param_model is invalid
|
|
832
|
+
|
|
833
|
+
# Wrap sync functions to make them async
|
|
834
|
+
if not iscoroutinefunction(function):
|
|
835
|
+
async def async_wrapper(*args, **kwargs):
|
|
836
|
+
return await asyncio.to_thread(function, *args, **kwargs)
|
|
837
|
+
|
|
838
|
+
# Copy the signature and other metadata
|
|
839
|
+
async_wrapper.__signature__ = signature(function)
|
|
840
|
+
async_wrapper.__name__ = function.__name__ if hasattr(function, '__name__') else name
|
|
841
|
+
async_wrapper.__doc__ = function.__doc__ if hasattr(function, '__doc__') and function.__doc__ else description
|
|
842
|
+
|
|
843
|
+
if hasattr(function, '__annotations__'):
|
|
844
|
+
async_wrapper.__annotations__ = function.__annotations__
|
|
845
|
+
|
|
846
|
+
# Copy important attributes for action registration
|
|
847
|
+
if hasattr(function, '_description'):
|
|
848
|
+
async_wrapper._description = function._description
|
|
849
|
+
else:
|
|
850
|
+
async_wrapper._description = description
|
|
851
|
+
|
|
852
|
+
if hasattr(function, '_param_model'):
|
|
853
|
+
async_wrapper._param_model = function._param_model
|
|
854
|
+
else:
|
|
855
|
+
async_wrapper._param_model = param_model
|
|
856
|
+
|
|
857
|
+
if hasattr(function, '_tool'):
|
|
858
|
+
async_wrapper._tool = function._tool
|
|
859
|
+
elif tool:
|
|
860
|
+
async_wrapper._tool = tool
|
|
861
|
+
|
|
862
|
+
wrapped_func = async_wrapper
|
|
863
|
+
else:
|
|
864
|
+
wrapped_func = function
|
|
865
|
+
|
|
866
|
+
# Set missing attributes on the function (skip for method objects)
|
|
867
|
+
import inspect
|
|
868
|
+
if not inspect.ismethod(wrapped_func):
|
|
869
|
+
try:
|
|
870
|
+
if not hasattr(wrapped_func, '_description'):
|
|
871
|
+
wrapped_func._description = description
|
|
872
|
+
if not hasattr(wrapped_func, '_param_model'):
|
|
873
|
+
wrapped_func._param_model = param_model
|
|
874
|
+
if not hasattr(wrapped_func, '_tool'):
|
|
875
|
+
self._set_tool_attribution(wrapped_func, tool)
|
|
876
|
+
except AttributeError:
|
|
877
|
+
# Can't set attributes on this callable, continue without them
|
|
878
|
+
pass
|
|
879
|
+
|
|
880
|
+
# Create the action registration
|
|
881
|
+
action = RegisteredAction(
|
|
882
|
+
name=name,
|
|
883
|
+
description=description,
|
|
884
|
+
function=wrapped_func,
|
|
885
|
+
param_model=param_model,
|
|
886
|
+
tool=tool # The tool this action belongs to (None for core actions)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Check for collision with existing action
|
|
890
|
+
if name in self.registry.actions:
|
|
891
|
+
existing = self.registry.actions[name]
|
|
892
|
+
existing_tool = existing.tool if hasattr(existing, 'tool') else 'core'
|
|
893
|
+
new_tool = tool if tool else 'core'
|
|
894
|
+
|
|
895
|
+
if existing_tool != new_tool:
|
|
896
|
+
# Different tools trying to register same action name - this is a collision
|
|
897
|
+
raise ValueError(
|
|
898
|
+
f"Action name collision: '{name}' already registered by '{existing_tool}', "
|
|
899
|
+
f"cannot register for '{new_tool}'. Use namespaced names to avoid collisions."
|
|
900
|
+
)
|
|
901
|
+
# H5: same tool re-registering — actually REPLACE (the first collision
|
|
902
|
+
# check already logged "will be replaced"). The old code returned here,
|
|
903
|
+
# silently discarding the rebuilt action and leaving the stale closure
|
|
904
|
+
# bound to the previous tool instance. Bust the schema cache so the
|
|
905
|
+
# replacement is re-emitted.
|
|
906
|
+
self._provider_schema_cache.clear()
|
|
907
|
+
|
|
908
|
+
# Register with the registry
|
|
909
|
+
self.registry.actions[name] = action
|
|
910
|
+
|
|
911
|
+
# Log action registration - only if not already logged
|
|
912
|
+
log_key = f"{tool}:{name}" if tool else name
|
|
913
|
+
if log_key not in self._action_registration_logged:
|
|
914
|
+
self._action_registration_logged.add(log_key)
|
|
915
|
+
if tool:
|
|
916
|
+
self.logger.debug(f"Registered action '{name}' from tool '{tool}'")
|
|
917
|
+
else:
|
|
918
|
+
self.logger.debug(f"Registered action '{name}'")
|
|
919
|
+
except Exception as e:
|
|
920
|
+
self.logger.warning(f"Failed to register action '{name}': {str(e)}", exc_info=True)
|
|
921
|
+
|
|
922
|
+
# NOTE: fix_missing_action_descriptions() removed per upgrade instructions
|
|
923
|
+
# Metadata repairs should require explicit @action/metadata; no auto-repair scans
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def get_all_actions_for_provider(self, provider: str) -> List[Dict[str, Any]]:
|
|
927
|
+
"""Generate provider-specific schemas for ALL actions (core + tools)
|
|
928
|
+
|
|
929
|
+
This is what the LLM actually uses - both core actions and tool actions.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
provider: The LLM provider name (openai, anthropic, gemini, etc.)
|
|
933
|
+
|
|
934
|
+
Returns:
|
|
935
|
+
List of all action schemas in the provider's expected format
|
|
936
|
+
"""
|
|
937
|
+
# Memoization: if the registered-action set and exclusions are unchanged,
|
|
938
|
+
# return the previously generated schema list for this provider. The cache
|
|
939
|
+
# key embeds the full action-name set so any registration/removal busts it.
|
|
940
|
+
cache_key = (
|
|
941
|
+
provider,
|
|
942
|
+
frozenset(self.registry.actions.keys()),
|
|
943
|
+
frozenset(self.exclude_actions),
|
|
944
|
+
)
|
|
945
|
+
cached = self._provider_schema_cache.get(cache_key)
|
|
946
|
+
if cached is not None:
|
|
947
|
+
return cached
|
|
948
|
+
|
|
949
|
+
# Get all non-excluded actions, deduplicated by object identity
|
|
950
|
+
# This is necessary because create_alias() adds the same RegisteredAction
|
|
951
|
+
# under multiple keys (the original name and the alias name).
|
|
952
|
+
# Without deduplication, we'd generate duplicate schemas.
|
|
953
|
+
seen_actions = set() # Track by object id
|
|
954
|
+
all_actions = []
|
|
955
|
+
|
|
956
|
+
for name, action in self.registry.actions.items():
|
|
957
|
+
if name in self.exclude_actions:
|
|
958
|
+
continue
|
|
959
|
+
# Deduplicate by RegisteredAction object identity
|
|
960
|
+
action_id = id(action)
|
|
961
|
+
if action_id in seen_actions:
|
|
962
|
+
self.logger.debug(f"Skipping alias '{name}' -> '{action.name}' (already included)")
|
|
963
|
+
continue
|
|
964
|
+
seen_actions.add(action_id)
|
|
965
|
+
all_actions.append(action)
|
|
966
|
+
|
|
967
|
+
if not all_actions:
|
|
968
|
+
self.logger.debug(f"No actions available for provider {provider}")
|
|
969
|
+
return []
|
|
970
|
+
|
|
971
|
+
# Get the appropriate schema generator
|
|
972
|
+
generator = get_schema_generator(provider)
|
|
973
|
+
|
|
974
|
+
# Generate and return all action schemas
|
|
975
|
+
actions = generator.generate_tools_list(all_actions)
|
|
976
|
+
|
|
977
|
+
# UP-10 2.5: sanitize the emitted tools list for broad backend compatibility
|
|
978
|
+
# (nullable-union collapse, $ref-sibling strip, top-level combinator strip,
|
|
979
|
+
# bare-string/array-type normalization). Runs BEFORE the cache write and is
|
|
980
|
+
# composed downstream of DROP_TOOL (sanitize first => fewer tools dropped).
|
|
981
|
+
# Gated TOOL_SCHEMA_SANITIZE (default on; =false restores pre-port bytes).
|
|
982
|
+
if os.getenv("TOOL_SCHEMA_SANITIZE", "true").lower() != "false":
|
|
983
|
+
try:
|
|
984
|
+
from tools.controller.registry.schema_sanitizer import sanitize_emitted_tools
|
|
985
|
+
actions = sanitize_emitted_tools(actions, provider)
|
|
986
|
+
except Exception as e:
|
|
987
|
+
self.logger.warning(f"schema sanitizer skipped (fail-open): {e}")
|
|
988
|
+
|
|
989
|
+
# Log breakdown of action types
|
|
990
|
+
core_count = sum(1 for a in all_actions if a.tool is None)
|
|
991
|
+
tool_count = sum(1 for a in all_actions if a.tool is not None)
|
|
992
|
+
alias_count = len(self.registry.actions) - len(all_actions)
|
|
993
|
+
self.logger.debug(
|
|
994
|
+
f"Generated {len(actions)} action schemas for provider {provider} "
|
|
995
|
+
f"({core_count} core, {tool_count} tool, {alias_count} aliases deduplicated)"
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
self._provider_schema_cache[cache_key] = actions
|
|
999
|
+
return actions
|
|
1000
|
+
|
|
1001
|
+
# Convenience API methods for accessing registry
|
|
1002
|
+
def get_action(self, name: str) -> Optional[RegisteredAction]:
|
|
1003
|
+
"""Get a registered action by name.
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
name: The action name
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
RegisteredAction if found, None otherwise
|
|
1010
|
+
"""
|
|
1011
|
+
return self.registry.actions.get(name)
|
|
1012
|
+
|
|
1013
|
+
def create_alias(self, alias_name: str, target_name: str) -> bool:
|
|
1014
|
+
"""Create a thread-safe alias for an existing action.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
alias_name: New name for the alias
|
|
1018
|
+
target_name: Existing action name to alias
|
|
1019
|
+
|
|
1020
|
+
Returns:
|
|
1021
|
+
True if alias created, False if target doesn't exist
|
|
1022
|
+
"""
|
|
1023
|
+
with self._registry_lock:
|
|
1024
|
+
# Check if target exists
|
|
1025
|
+
if target_name not in self.registry.actions:
|
|
1026
|
+
return False
|
|
1027
|
+
|
|
1028
|
+
# Create alias by copying the RegisteredAction reference
|
|
1029
|
+
self.registry.actions[alias_name] = self.registry.actions[target_name]
|
|
1030
|
+
return True
|
|
1031
|
+
|
|
1032
|
+
def list_action_names(self) -> List[str]:
|
|
1033
|
+
"""Get list of all registered action names.
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
List of action names
|
|
1037
|
+
"""
|
|
1038
|
+
return list(self.registry.actions.keys())
|
|
1039
|
+
|
|
1040
|
+
def list_actions(self) -> List[str]:
|
|
1041
|
+
"""Get list of all registered action names.
|
|
1042
|
+
|
|
1043
|
+
Alias for list_action_names().
|
|
1044
|
+
"""
|
|
1045
|
+
return self.list_action_names()
|
|
1046
|
+
|
|
1047
|
+
def get_actions_by_tool(self, tool: str) -> List[RegisteredAction]:
|
|
1048
|
+
"""Get all actions registered for a specific tool.
|
|
1049
|
+
|
|
1050
|
+
Args:
|
|
1051
|
+
tool: The tool name
|
|
1052
|
+
|
|
1053
|
+
Returns:
|
|
1054
|
+
List of RegisteredAction instances for this tool
|
|
1055
|
+
"""
|
|
1056
|
+
return [
|
|
1057
|
+
action for action in self.registry.actions.values()
|
|
1058
|
+
if action.tool == tool
|
|
1059
|
+
]
|
|
1060
|
+
|
|
1061
|
+
def supports_native_tools(self, provider: str) -> bool:
|
|
1062
|
+
"""Check if a provider supports native tool/function calling
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
provider: The LLM provider name
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
True if the provider has native tool support
|
|
1069
|
+
"""
|
|
1070
|
+
provider_lower = provider.lower()
|
|
1071
|
+
|
|
1072
|
+
# Providers with native tool support
|
|
1073
|
+
# DeepSeek V3+ supports OpenAI-compatible function calling
|
|
1074
|
+
# Docs: https://api-docs.deepseek.com/guides/function_calling
|
|
1075
|
+
# OpenRouter models support native tool calling via OpenAI-compatible API
|
|
1076
|
+
native_providers = ["openai", "anthropic", "gemini", "google", "deepseek", "openrouter", "nvidia"]
|
|
1077
|
+
|
|
1078
|
+
return any(p in provider_lower for p in native_providers)
|
|
1079
|
+
|
|
1080
|
+
def tool_call_to_action(self, tool_name: str, args: Dict[str, Any]) -> ActionModel:
|
|
1081
|
+
"""Convert a single tool call to an ActionModel (simplified single validation path)
|
|
1082
|
+
|
|
1083
|
+
Args:
|
|
1084
|
+
tool_name: Name of the tool/action
|
|
1085
|
+
args: Arguments for the tool call
|
|
1086
|
+
|
|
1087
|
+
Returns:
|
|
1088
|
+
ActionModel instance ready for execution
|
|
1089
|
+
|
|
1090
|
+
Raises:
|
|
1091
|
+
ValueError: If the tool is not registered or validation fails
|
|
1092
|
+
"""
|
|
1093
|
+
# 1. Get action from registry (with fuzzy match)
|
|
1094
|
+
action = self._get_action_with_fuzzy_match(tool_name)
|
|
1095
|
+
|
|
1096
|
+
# 2. Validate parameters directly
|
|
1097
|
+
try:
|
|
1098
|
+
if action.param_model:
|
|
1099
|
+
validated_args = action.param_model(**args)
|
|
1100
|
+
args = validated_args.model_dump()
|
|
1101
|
+
except Exception as validation_error:
|
|
1102
|
+
# SINGULAR RESPONSIBILITY: If validation fails, caller did not normalize properly
|
|
1103
|
+
# Do NOT hide errors with fallback corrections - fail fast
|
|
1104
|
+
self.logger.error(
|
|
1105
|
+
f"Validation failed for {tool_name}. "
|
|
1106
|
+
f"Tool calls MUST be normalized with normalize_and_correct() before validation. "
|
|
1107
|
+
f"Args: {args}, Error: {validation_error}"
|
|
1108
|
+
)
|
|
1109
|
+
# P3: the raised message is what the LLM sees — keep it actionable (the field
|
|
1110
|
+
# error + params are appended by the caller). The internal
|
|
1111
|
+
# "normalize_and_correct()" hint is dev-only noise for the model, so it stays
|
|
1112
|
+
# in the logger.error above, not in the model-facing error.
|
|
1113
|
+
raise ValueError(
|
|
1114
|
+
f"Invalid arguments for {tool_name}: {validation_error}."
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
# 4. Create ActionModel — key by the RESOLVED registered action name, not the
|
|
1118
|
+
# caller's (possibly fuzzy-matched) tool_name. Keying by the unresolved name
|
|
1119
|
+
# builds an ActionModel with a key that matches no field, which ActionModel
|
|
1120
|
+
# silently drops → an empty no-op action (the fuzzy match would validate args
|
|
1121
|
+
# but then execute nothing).
|
|
1122
|
+
resolved_name = getattr(action, "name", tool_name) or tool_name
|
|
1123
|
+
action_data = {resolved_name: args if args is not None else {}}
|
|
1124
|
+
action_model_class = self.create_action_model()
|
|
1125
|
+
return action_model_class(**action_data)
|
|
1126
|
+
|
|
1127
|
+
def _get_action_with_fuzzy_match(self, tool_name: str) -> RegisteredAction:
|
|
1128
|
+
"""Get action from registry with fuzzy matching fallback."""
|
|
1129
|
+
# First try exact match
|
|
1130
|
+
if tool_name in self.registry.actions:
|
|
1131
|
+
return self.registry.actions[tool_name]
|
|
1132
|
+
|
|
1133
|
+
# Try fuzzy matching - look for actions that end with the tool name
|
|
1134
|
+
fuzzy_matches = [
|
|
1135
|
+
action_name for action_name in self.registry.actions.keys()
|
|
1136
|
+
if action_name.endswith(f"_{tool_name}") or action_name == tool_name
|
|
1137
|
+
]
|
|
1138
|
+
|
|
1139
|
+
if fuzzy_matches:
|
|
1140
|
+
tool_name = fuzzy_matches[0]
|
|
1141
|
+
self.logger.info(f"Fuzzy matched to '{tool_name}'")
|
|
1142
|
+
return self.registry.actions[tool_name]
|
|
1143
|
+
|
|
1144
|
+
# Not found - provide helpful error
|
|
1145
|
+
available = list(self.registry.actions.keys())[:10]
|
|
1146
|
+
available_str = ", ".join(available)
|
|
1147
|
+
if len(self.registry.actions) > 10:
|
|
1148
|
+
available_str += f", ... ({len(self.registry.actions)} total)"
|
|
1149
|
+
|
|
1150
|
+
similar = [
|
|
1151
|
+
name for name in self.registry.actions.keys()
|
|
1152
|
+
if tool_name in name or name.split('_')[-1] == tool_name.split('_')[-1]
|
|
1153
|
+
]
|
|
1154
|
+
error_msg = f"Unknown tool: '{tool_name}'. Available: {available_str}."
|
|
1155
|
+
if similar:
|
|
1156
|
+
error_msg += f" Similar: {similar[:5]}"
|
|
1157
|
+
|
|
1158
|
+
raise ValueError(error_msg)
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def tool_calls_to_actions(self, tool_calls: List[Dict[str, Any]]) -> List[ActionModel]:
|
|
1162
|
+
"""Convert multiple tool calls to ActionModels
|
|
1163
|
+
|
|
1164
|
+
Args:
|
|
1165
|
+
tool_calls: List of tool calls, each with 'name' or 'function.name' and 'arguments'
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
List of ActionModel instances ready for execution
|
|
1169
|
+
|
|
1170
|
+
Note:
|
|
1171
|
+
Failed validations are tracked in self._last_validation_errors as a dict mapping
|
|
1172
|
+
tool_call_id -> error_message. Callers can check this to provide error responses.
|
|
1173
|
+
"""
|
|
1174
|
+
actions = []
|
|
1175
|
+
# Track validation errors for caller to handle (keyed by tool_call_id)
|
|
1176
|
+
self._last_validation_errors: Dict[str, str] = {}
|
|
1177
|
+
|
|
1178
|
+
for call in tool_calls:
|
|
1179
|
+
tool_name = call.get('name', 'unknown')
|
|
1180
|
+
tool_call_id = call.get('id')
|
|
1181
|
+
|
|
1182
|
+
try:
|
|
1183
|
+
args = call.get('args', {})
|
|
1184
|
+
|
|
1185
|
+
if not tool_name or tool_name == 'unknown':
|
|
1186
|
+
error_msg = f"Tool call missing 'name': {call}"
|
|
1187
|
+
self.logger.warning(error_msg)
|
|
1188
|
+
if tool_call_id:
|
|
1189
|
+
self._last_validation_errors[tool_call_id] = error_msg
|
|
1190
|
+
continue
|
|
1191
|
+
|
|
1192
|
+
# Ensure args is a dict (should already be from normalization)
|
|
1193
|
+
if isinstance(args, str):
|
|
1194
|
+
import json
|
|
1195
|
+
try:
|
|
1196
|
+
args = json.loads(args) if args else {}
|
|
1197
|
+
except json.JSONDecodeError:
|
|
1198
|
+
error_msg = f"Invalid JSON arguments for {tool_name}: {args}"
|
|
1199
|
+
self.logger.warning(error_msg)
|
|
1200
|
+
if tool_call_id:
|
|
1201
|
+
self._last_validation_errors[tool_call_id] = error_msg
|
|
1202
|
+
args = {}
|
|
1203
|
+
|
|
1204
|
+
action = self.tool_call_to_action(tool_name, args)
|
|
1205
|
+
if tool_call_id:
|
|
1206
|
+
action._tool_call_id = tool_call_id
|
|
1207
|
+
actions.append(action)
|
|
1208
|
+
|
|
1209
|
+
except ValueError as e:
|
|
1210
|
+
error_str = str(e)
|
|
1211
|
+
|
|
1212
|
+
# FIXED: Differentiate between "not found" and "validation failed" errors
|
|
1213
|
+
if "Unknown tool" in error_str or "not found" in error_str.lower():
|
|
1214
|
+
self.logger.error(f"❌ Tool '{tool_name}' not found in registry!")
|
|
1215
|
+
self.logger.error(f" Available tools (first 30): {list(self.registry.actions.keys())[:30]}")
|
|
1216
|
+
error_msg = f"Tool '{tool_name}' not found. Check available tools with mcp_list_tools."
|
|
1217
|
+
else:
|
|
1218
|
+
# Validation failed - tool exists but args are wrong
|
|
1219
|
+
self.logger.error(f"❌ Validation failed for tool '{tool_name}'")
|
|
1220
|
+
self.logger.error(f" Error: {e}")
|
|
1221
|
+
error_msg = f"Validation failed for '{tool_name}': {error_str}"
|
|
1222
|
+
|
|
1223
|
+
# Show expected schema for the tool
|
|
1224
|
+
if tool_name in self.registry.actions:
|
|
1225
|
+
action_info = self.registry.actions[tool_name]
|
|
1226
|
+
if action_info.param_model:
|
|
1227
|
+
try:
|
|
1228
|
+
schema = action_info.param_model.model_json_schema()
|
|
1229
|
+
required = schema.get('required', [])
|
|
1230
|
+
props = list(schema.get('properties', {}).keys())
|
|
1231
|
+
self.logger.error(f" Required params: {required}")
|
|
1232
|
+
self.logger.error(f" All params: {props}")
|
|
1233
|
+
error_msg += f" Required: {required}. All params: {props}."
|
|
1234
|
+
except Exception:
|
|
1235
|
+
pass
|
|
1236
|
+
|
|
1237
|
+
# FIXED: Track error for caller instead of appending None
|
|
1238
|
+
if tool_call_id:
|
|
1239
|
+
self._last_validation_errors[tool_call_id] = error_msg
|
|
1240
|
+
|
|
1241
|
+
self.logger.warning(f" Skipping action '{tool_name}' due to error")
|
|
1242
|
+
continue
|
|
1243
|
+
|
|
1244
|
+
except Exception as e:
|
|
1245
|
+
error_msg = f"Failed to convert tool call {tool_name}: {e}"
|
|
1246
|
+
self.logger.error(error_msg)
|
|
1247
|
+
if tool_call_id:
|
|
1248
|
+
self._last_validation_errors[tool_call_id] = error_msg
|
|
1249
|
+
continue
|
|
1250
|
+
|
|
1251
|
+
# Log summary of validation errors
|
|
1252
|
+
if self._last_validation_errors:
|
|
1253
|
+
self.logger.warning(
|
|
1254
|
+
f"⚠️ {len(self._last_validation_errors)} tool calls failed validation: "
|
|
1255
|
+
f"{list(self._last_validation_errors.keys())}"
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
return actions
|
|
1259
|
+
|
|
1260
|
+
def get_last_validation_errors(self) -> Dict[str, str]:
|
|
1261
|
+
"""Get validation errors from the last tool_calls_to_actions call.
|
|
1262
|
+
|
|
1263
|
+
Returns:
|
|
1264
|
+
Dict mapping tool_call_id -> error_message for failed validations
|
|
1265
|
+
"""
|
|
1266
|
+
return getattr(self, '_last_validation_errors', {})
|