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,768 @@
|
|
|
1
|
+
"""Centralized JSON extraction utilities for AutoV2.
|
|
2
|
+
|
|
3
|
+
This module provides consistent JSON extraction from model outputs
|
|
4
|
+
with standardized error messages and layered parsing strategies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, Optional, List, Union, Tuple
|
|
11
|
+
from difflib import SequenceMatcher
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Common field name variations mapping
|
|
16
|
+
FIELD_SYNONYMS = {
|
|
17
|
+
'text': ['message', 'msg', 'content', 'value', 'string', 'data'],
|
|
18
|
+
'file_path': ['file_name', 'filename', 'path', 'filepath', 'file'],
|
|
19
|
+
'content': ['text', 'data', 'body', 'contents', 'value'],
|
|
20
|
+
'selector': ['element', 'target', 'locator', 'xpath', 'css'],
|
|
21
|
+
'url': ['link', 'href', 'address', 'uri', 'website'],
|
|
22
|
+
'query': ['search', 'q', 'search_query', 'search_term', 'term'],
|
|
23
|
+
'parent_pattern': ['parent', 'parent_id', 'parent_text', 'under'],
|
|
24
|
+
'pattern': ['text', 'match', 'search', 'regex', 'string'],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_CAMEL_BOUNDARY_1 = re.compile(r'(.)([A-Z][a-z]+)')
|
|
28
|
+
_CAMEL_BOUNDARY_2 = re.compile(r'([a-z0-9])([A-Z])')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def camel_to_snake(name: str) -> str:
|
|
32
|
+
"""Convert camelCase / PascalCase to snake_case; idempotent for snake_case.
|
|
33
|
+
|
|
34
|
+
``filePath`` → ``file_path``, ``maxResults`` → ``max_results``,
|
|
35
|
+
``userID`` → ``user_id``, ``file_path`` → ``file_path`` (unchanged).
|
|
36
|
+
"""
|
|
37
|
+
if not name:
|
|
38
|
+
return name
|
|
39
|
+
s1 = _CAMEL_BOUNDARY_1.sub(r'\1_\2', name)
|
|
40
|
+
return _CAMEL_BOUNDARY_2.sub(r'\1_\2', s1).lower()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reconcile_field_names_to_model(
|
|
44
|
+
params: Dict[str, Any], valid_fields: set
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""Systematic camelCase→snake_case reconciliation against a model's fields (WS-1.3).
|
|
47
|
+
|
|
48
|
+
Replaces the hand-maintained per-field whack-a-mole for the *casing* class of
|
|
49
|
+
error: a key is renamed to its snake_case form ONLY when that snake_case form
|
|
50
|
+
is a real field of the target Pydantic param model and the camelCase one is
|
|
51
|
+
not. This is provider-agnostic (any model that emits ``filePath`` for a
|
|
52
|
+
``file_path`` field is fixed) yet safe for tools whose params are genuinely
|
|
53
|
+
camelCase (e.g. some MCP servers) — there, the snake_case form is NOT a model
|
|
54
|
+
field, so the key is left untouched. Semantic renames (``message``→``text``)
|
|
55
|
+
still go through ``apply_action_field_corrections``.
|
|
56
|
+
"""
|
|
57
|
+
if not params or not valid_fields:
|
|
58
|
+
return params
|
|
59
|
+
out: Dict[str, Any] = {}
|
|
60
|
+
for key, value in params.items():
|
|
61
|
+
if key in valid_fields:
|
|
62
|
+
out[key] = value
|
|
63
|
+
continue
|
|
64
|
+
snake = camel_to_snake(key)
|
|
65
|
+
if snake != key and snake in valid_fields and snake not in params:
|
|
66
|
+
out[snake] = value
|
|
67
|
+
logger.debug(f"Reconciled camelCase field '{key}' -> '{snake}'")
|
|
68
|
+
else:
|
|
69
|
+
out[key] = value
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def apply_action_field_corrections(action_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
74
|
+
"""Apply action-specific field corrections with enhanced fuzzy matching.
|
|
75
|
+
|
|
76
|
+
This function handles common LLM mistakes by mapping incorrect field names
|
|
77
|
+
to their expected counterparts. It uses a comprehensive mapping table and
|
|
78
|
+
supports fuzzy matching for common variations.
|
|
79
|
+
"""
|
|
80
|
+
if not params:
|
|
81
|
+
return params
|
|
82
|
+
|
|
83
|
+
corrected = {}
|
|
84
|
+
|
|
85
|
+
# Enhanced corrections with more variations
|
|
86
|
+
corrections = {
|
|
87
|
+
# Core action: done (Controller action, not TaskTool)
|
|
88
|
+
'done': {
|
|
89
|
+
'message': 'text', 'result': 'text', 'output': 'text',
|
|
90
|
+
'completion_message': 'text', 'done_text': 'text', 'summary': 'text',
|
|
91
|
+
'final_message': 'text', 'completion': 'text', 'status': 'text'
|
|
92
|
+
},
|
|
93
|
+
# Namespaced task todo actions (TaskTool actions)
|
|
94
|
+
# Legacy names (todo_add, etc.) are handled via action_aliases below
|
|
95
|
+
'task_todo_add': {
|
|
96
|
+
'message': 'text', 'task': 'text', 'todo': 'text', 'item': 'text',
|
|
97
|
+
'description': 'text', 'content': 'text', 'todo_text': 'text',
|
|
98
|
+
'task_text': 'text', 'todo_message': 'text', 'task_message': 'text'
|
|
99
|
+
},
|
|
100
|
+
'task_todo_complete': {
|
|
101
|
+
'index': 'id', 'todo_id': 'id', 'item_id': 'id', 'task_id': 'id'
|
|
102
|
+
},
|
|
103
|
+
'write_file': {
|
|
104
|
+
'file_name': 'file_path', 'filename': 'file_path', 'path': 'file_path',
|
|
105
|
+
'name': 'file_path', 'file': 'file_path', 'filepath': 'file_path',
|
|
106
|
+
'data': 'content', 'text': 'content', 'file_content': 'content',
|
|
107
|
+
'body': 'content', 'contents': 'content', 'file_data': 'content'
|
|
108
|
+
},
|
|
109
|
+
'read_file': {
|
|
110
|
+
'file_name': 'file_path', 'filename': 'file_path', 'path': 'file_path',
|
|
111
|
+
'name': 'file_path', 'file': 'file_path', 'filepath': 'file_path'
|
|
112
|
+
},
|
|
113
|
+
'click_element': {
|
|
114
|
+
'element': 'index', 'target': 'index', 'locator': 'index',
|
|
115
|
+
'element_index': 'index', 'click_target': 'index',
|
|
116
|
+
'selector': 'index',
|
|
117
|
+
},
|
|
118
|
+
'click': { # Alias for click_element
|
|
119
|
+
'element': 'index', 'target': 'index', 'locator': 'index',
|
|
120
|
+
'element_index': 'index', 'selector': 'index',
|
|
121
|
+
},
|
|
122
|
+
'search_google': {
|
|
123
|
+
'search': 'query', 'q': 'query', 'search_term': 'query', 'term': 'query',
|
|
124
|
+
'search_query': 'query', 'google_query': 'query', 'text': 'query',
|
|
125
|
+
'search_text': 'query', 'keyword': 'query', 'keywords': 'query'
|
|
126
|
+
},
|
|
127
|
+
'navigate': {
|
|
128
|
+
'address': 'url', 'link': 'url', 'website': 'url', 'site': 'url',
|
|
129
|
+
'destination': 'url', 'uri': 'url', 'navigate_to': 'url', 'goto': 'url'
|
|
130
|
+
},
|
|
131
|
+
'input_text': {
|
|
132
|
+
'element': 'index', 'target': 'index', 'element_index': 'index',
|
|
133
|
+
'locator': 'index', 'selector': 'index',
|
|
134
|
+
'input': 'text', 'value': 'text', 'content': 'text',
|
|
135
|
+
'input_value': 'text', 'type_text': 'text', 'string': 'text'
|
|
136
|
+
},
|
|
137
|
+
'type_text': {
|
|
138
|
+
'input': 'text', 'content': 'text', 'value': 'text', 'string': 'text',
|
|
139
|
+
'type_value': 'text', 'input_text': 'text', 'enter_text': 'text'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Also handle action name variations (legacy -> canonical)
|
|
144
|
+
action_aliases = {
|
|
145
|
+
'click': 'click_element',
|
|
146
|
+
'goto': 'navigate',
|
|
147
|
+
'go_to': 'navigate',
|
|
148
|
+
'open': 'navigate',
|
|
149
|
+
'type': 'type_text',
|
|
150
|
+
'input': 'type_text',
|
|
151
|
+
'search': 'search_google',
|
|
152
|
+
'google': 'search_google',
|
|
153
|
+
# Todo aliases - map legacy names to namespaced names
|
|
154
|
+
'todo_add': 'task_todo_add',
|
|
155
|
+
'add_todo': 'task_todo_add',
|
|
156
|
+
'create_todo': 'task_todo_add',
|
|
157
|
+
'todo_list': 'task_todo_list',
|
|
158
|
+
'todo_complete': 'task_todo_complete',
|
|
159
|
+
'todo_progress': 'task_todo_progress',
|
|
160
|
+
'todo_next': 'task_todo_next',
|
|
161
|
+
'finish': 'done',
|
|
162
|
+
'complete': 'done',
|
|
163
|
+
'save_file': 'write_file',
|
|
164
|
+
'create_file': 'write_file',
|
|
165
|
+
'open_file': 'read_file',
|
|
166
|
+
'load_file': 'read_file'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Extract base name from namespaced action (e.g., "browser_click_element" -> "click_element")
|
|
170
|
+
base_action_name = action_name
|
|
171
|
+
if '_' in action_name:
|
|
172
|
+
parts = action_name.split('_', 1)
|
|
173
|
+
if len(parts) == 2 and (parts[1] in corrections or parts[1] in action_aliases):
|
|
174
|
+
base_action_name = parts[1]
|
|
175
|
+
logger.debug(f"Extracted base action: '{action_name}' -> '{base_action_name}'")
|
|
176
|
+
|
|
177
|
+
# Normalize action name if it's an alias
|
|
178
|
+
normalized_action = action_aliases.get(base_action_name.lower(), base_action_name)
|
|
179
|
+
|
|
180
|
+
# Try multiple lookup strategies
|
|
181
|
+
action_corrections = (
|
|
182
|
+
corrections.get(action_name, {}) or
|
|
183
|
+
corrections.get(normalized_action, {}) or
|
|
184
|
+
corrections.get(base_action_name, {})
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Process parameters with case-insensitive matching, in TWO passes so a
|
|
188
|
+
# genuine field always wins over a synonym regardless of dict ordering
|
|
189
|
+
# (e.g. done({'text': 'real', 'summary': 'short'}) must never let the
|
|
190
|
+
# 'summary'->'text' rename clobber the real 'text').
|
|
191
|
+
renames: Dict[str, str] = {} # original field -> target field
|
|
192
|
+
for field, value in params.items():
|
|
193
|
+
field_lower = field.lower()
|
|
194
|
+
|
|
195
|
+
# Check if this field needs correction
|
|
196
|
+
correct_field = action_corrections.get(field_lower, field)
|
|
197
|
+
|
|
198
|
+
# If no direct match, try to find a close match — but on a '_'-delimited
|
|
199
|
+
# TOKEN boundary, not a raw substring. A substring test against short keys
|
|
200
|
+
# ('q'->'query', 'text'->'content') mis-maps unrelated fields ('quality',
|
|
201
|
+
# 'context') and clobbers the real one. Require the shorter name to be a
|
|
202
|
+
# whole token of the longer.
|
|
203
|
+
if correct_field == field and action_corrections:
|
|
204
|
+
field_tokens = field_lower.split('_')
|
|
205
|
+
for wrong_field, right_field in action_corrections.items():
|
|
206
|
+
wf = wrong_field.lower()
|
|
207
|
+
if wf in field_tokens or field_lower in wf.split('_'):
|
|
208
|
+
correct_field = right_field
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
if correct_field == field:
|
|
212
|
+
# Pass 1: genuine/uncorrected fields land first, so they can never
|
|
213
|
+
# be clobbered by a later (or earlier) synonym rename.
|
|
214
|
+
corrected[field] = value
|
|
215
|
+
else:
|
|
216
|
+
renames[field] = correct_field
|
|
217
|
+
|
|
218
|
+
# Pass 2: apply renames only into slots absent from BOTH the original
|
|
219
|
+
# params and what we've built so far. A redundant synonym is dropped
|
|
220
|
+
# (first rename wins if two synonyms target the same field).
|
|
221
|
+
for field, correct_field in renames.items():
|
|
222
|
+
if correct_field in corrected or correct_field in params:
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"Dropped redundant field: '{field}' (target '{correct_field}' "
|
|
225
|
+
f"already supplied) for '{action_name}'"
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
corrected[correct_field] = params[field]
|
|
229
|
+
logger.debug(f"Corrected field: '{field}' -> '{correct_field}' for '{action_name}'")
|
|
230
|
+
|
|
231
|
+
return corrected
|
|
232
|
+
|
|
233
|
+
def preprocess_action_data(action: Dict[str, Any]) -> Tuple[str, Any]:
|
|
234
|
+
"""Extract action name and params from action dict.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
action: Action dictionary with single key-value pair
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple of (action_name, action_params)
|
|
241
|
+
"""
|
|
242
|
+
if not action or not isinstance(action, dict):
|
|
243
|
+
return "unknown", {}
|
|
244
|
+
|
|
245
|
+
action_name = list(action.keys())[0] if action else "unknown"
|
|
246
|
+
action_params = action.get(action_name, {})
|
|
247
|
+
return action_name, action_params
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _extract_balanced_json(text: str) -> Optional[Dict[str, Any]]:
|
|
251
|
+
"""Extract a balanced JSON object from text using brace counting.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
text: Text containing a JSON object
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Parsed JSON dict or None if extraction fails
|
|
258
|
+
"""
|
|
259
|
+
start = text.find('{')
|
|
260
|
+
if start < 0:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
brace_count = 0
|
|
264
|
+
end = start
|
|
265
|
+
for i, char in enumerate(text[start:], start):
|
|
266
|
+
if char == '{':
|
|
267
|
+
brace_count += 1
|
|
268
|
+
elif char == '}':
|
|
269
|
+
brace_count -= 1
|
|
270
|
+
if brace_count == 0:
|
|
271
|
+
end = i + 1
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
if brace_count != 0:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
json_str = text[start:end]
|
|
279
|
+
return json.loads(json_str)
|
|
280
|
+
except (json.JSONDecodeError, ValueError):
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _parse_function_args(func_args_str: str) -> Dict[str, Any]:
|
|
285
|
+
"""Parse function arguments from a string like 'arg1="val1", arg2=123'.
|
|
286
|
+
|
|
287
|
+
Handles both quoted strings and unquoted values (int, float, bool, null).
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
func_args_str: Arguments string from function call
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary of parsed arguments
|
|
294
|
+
"""
|
|
295
|
+
func_args = {}
|
|
296
|
+
|
|
297
|
+
if not func_args_str or not func_args_str.strip():
|
|
298
|
+
return func_args
|
|
299
|
+
|
|
300
|
+
# First match quoted values: key="value" or key='value'
|
|
301
|
+
quoted_pattern = r'(\w+)\s*=\s*["\']([^"\']*)["\']'
|
|
302
|
+
for arg_match in re.finditer(quoted_pattern, func_args_str):
|
|
303
|
+
func_args[arg_match.group(1)] = arg_match.group(2)
|
|
304
|
+
|
|
305
|
+
# Then match unquoted values: key=123, key=true, key=false, key=null
|
|
306
|
+
unquoted_pattern = r'(\w+)\s*=\s*([0-9]+(?:\.[0-9]+)?|true|false|null)'
|
|
307
|
+
for arg_match in re.finditer(unquoted_pattern, func_args_str, re.IGNORECASE):
|
|
308
|
+
arg_name = arg_match.group(1)
|
|
309
|
+
if arg_name not in func_args: # Don't overwrite quoted values
|
|
310
|
+
val_str = arg_match.group(2).lower()
|
|
311
|
+
if val_str == 'true':
|
|
312
|
+
func_args[arg_name] = True
|
|
313
|
+
elif val_str == 'false':
|
|
314
|
+
func_args[arg_name] = False
|
|
315
|
+
elif val_str == 'null':
|
|
316
|
+
func_args[arg_name] = None
|
|
317
|
+
elif '.' in arg_match.group(2):
|
|
318
|
+
func_args[arg_name] = float(arg_match.group(2))
|
|
319
|
+
else:
|
|
320
|
+
func_args[arg_name] = int(arg_match.group(2))
|
|
321
|
+
|
|
322
|
+
return func_args
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_function_calls(func_section: str) -> List[Dict[str, Any]]:
|
|
326
|
+
"""Parse all function calls from a Functions: section.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
func_section: Text after "Functions:" containing function calls
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List of action dicts like [{func_name: {args}}]
|
|
333
|
+
"""
|
|
334
|
+
func_call_pattern = r'(\w+)\(([^)]*)\)'
|
|
335
|
+
all_actions = []
|
|
336
|
+
|
|
337
|
+
for func_match in re.finditer(func_call_pattern, func_section):
|
|
338
|
+
func_name = func_match.group(1)
|
|
339
|
+
func_args_str = func_match.group(2)
|
|
340
|
+
func_args = _parse_function_args(func_args_str)
|
|
341
|
+
all_actions.append({func_name: func_args})
|
|
342
|
+
logger.debug(f"Parsed function '{func_name}' with args: {func_args}")
|
|
343
|
+
|
|
344
|
+
return all_actions
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _build_converted_response(
|
|
348
|
+
all_actions: List[Dict[str, Any]],
|
|
349
|
+
current_state_data: Optional[Dict[str, Any]] = None
|
|
350
|
+
) -> Dict[str, Any]:
|
|
351
|
+
"""Build a converted response dict from actions and optional current_state.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
all_actions: List of action dicts
|
|
355
|
+
current_state_data: Optional parsed current_state JSON
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Response dict with 'action' and 'current_state' fields
|
|
359
|
+
"""
|
|
360
|
+
converted = {"action": all_actions}
|
|
361
|
+
|
|
362
|
+
if current_state_data:
|
|
363
|
+
if "current_state" in current_state_data:
|
|
364
|
+
converted["current_state"] = current_state_data["current_state"]
|
|
365
|
+
else:
|
|
366
|
+
converted["current_state"] = current_state_data
|
|
367
|
+
else:
|
|
368
|
+
# Create minimal current_state for schema validation
|
|
369
|
+
first_action_name = list(all_actions[0].keys())[0] if all_actions else "unknown"
|
|
370
|
+
converted["current_state"] = {
|
|
371
|
+
"memory": "Action from function call format",
|
|
372
|
+
"evaluation_previous_goal": "Unknown",
|
|
373
|
+
"next_goal": first_action_name
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return converted
|
|
377
|
+
|
|
378
|
+
def extract_json_from_model_output(text: str) -> Dict[str, Any]:
|
|
379
|
+
"""Extract JSON from model output with consistent error handling.
|
|
380
|
+
|
|
381
|
+
Tries multiple extraction strategies:
|
|
382
|
+
1. Strip code fences and think tags
|
|
383
|
+
2. Try direct JSON parsing
|
|
384
|
+
3. Extract from various block formats
|
|
385
|
+
4. Find JSON-like structures
|
|
386
|
+
5. Validate required keys
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
text: Model output text potentially containing JSON
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Parsed JSON as dictionary
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
ValueError with standardized messages:
|
|
396
|
+
- "Empty response from model" for None/empty input
|
|
397
|
+
- "Could not parse response" for extraction failures
|
|
398
|
+
"""
|
|
399
|
+
if not text:
|
|
400
|
+
raise ValueError("Empty response from model")
|
|
401
|
+
|
|
402
|
+
# Import config for validation - single source of truth
|
|
403
|
+
from agents.task.robust_parse_config import RobustParseConfig as Config
|
|
404
|
+
|
|
405
|
+
# Debug logging - only at DEBUG level to prevent log spam
|
|
406
|
+
logger.debug(f"extract_json_from_model_output input (first 500 chars): {text[:500]}")
|
|
407
|
+
|
|
408
|
+
original_text = text
|
|
409
|
+
|
|
410
|
+
# FIX 3a: Strip code fences and think tags first
|
|
411
|
+
if Config.STRIP_CODE_FENCES:
|
|
412
|
+
# Remove code fences - properly handle content between them
|
|
413
|
+
text = re.sub(r'```[a-z]*\n?', '', text, flags=re.IGNORECASE)
|
|
414
|
+
text = re.sub(r'```\s*$', '', text)
|
|
415
|
+
# Remove triple quotes
|
|
416
|
+
text = re.sub(r'"""[a-z]*\n?', '', text, flags=re.IGNORECASE)
|
|
417
|
+
text = re.sub(r'"""', '', text)
|
|
418
|
+
|
|
419
|
+
if Config.STRIP_THINK_TAGS:
|
|
420
|
+
# Remove think tags and their content
|
|
421
|
+
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL | re.IGNORECASE)
|
|
422
|
+
text = re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL | re.IGNORECASE)
|
|
423
|
+
|
|
424
|
+
# Try direct parsing first (after stripping)
|
|
425
|
+
try:
|
|
426
|
+
parsed = json.loads(text.strip())
|
|
427
|
+
# FIX 3d: Validate required keys
|
|
428
|
+
if Config.REQUIRE_SCHEMA_KEYS:
|
|
429
|
+
if Config.validate_json_candidate(json.dumps(parsed)):
|
|
430
|
+
logger.debug(f"Direct JSON parse succeeded with validation")
|
|
431
|
+
return parsed
|
|
432
|
+
# Continue to other strategies if validation fails
|
|
433
|
+
logger.debug(f"Direct JSON parse succeeded but validation failed")
|
|
434
|
+
else:
|
|
435
|
+
logger.debug(f"Direct JSON parse succeeded (no validation)")
|
|
436
|
+
return parsed
|
|
437
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
438
|
+
logger.debug(f"Direct JSON parse failed: {e}")
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
# T-04: Enhanced pattern detection for function-calling format
|
|
442
|
+
# Try to detect OpenAI function call format first
|
|
443
|
+
function_call_pattern = r'\{\s*"function"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[^}]*\}\s*\}'
|
|
444
|
+
func_match = re.search(function_call_pattern, original_text)
|
|
445
|
+
if func_match:
|
|
446
|
+
try:
|
|
447
|
+
func_call = json.loads(func_match.group())
|
|
448
|
+
# Convert to standard action format
|
|
449
|
+
if "function" in func_call and "args" in func_call:
|
|
450
|
+
converted = {
|
|
451
|
+
"action": [{func_call["function"]: func_call.get("args", {})}]
|
|
452
|
+
}
|
|
453
|
+
# Add a minimal current_state if required
|
|
454
|
+
if Config.REQUIRE_SCHEMA_KEYS:
|
|
455
|
+
converted["current_state"] = {
|
|
456
|
+
"page_summary": "Action from function call",
|
|
457
|
+
"evaluation_previous_goal": "Unknown",
|
|
458
|
+
"memory": "",
|
|
459
|
+
"next_goal": func_call["function"]
|
|
460
|
+
}
|
|
461
|
+
logger.debug(f"Converted OpenAI function-call format to standard action")
|
|
462
|
+
return converted
|
|
463
|
+
except (json.JSONDecodeError, TypeError):
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
# T-05: Handle "Text: {...} Functions: func_name(...)" format from OpenRouter/some models
|
|
467
|
+
# Format: Text: {"current_state": {...}} \n Functions: func_name(arg1="val1", arg2="val2")
|
|
468
|
+
# CRITICAL FIX (Dec 2025): Refactored to use helper functions
|
|
469
|
+
if 'Text:' in original_text and 'Functions:' in original_text:
|
|
470
|
+
try:
|
|
471
|
+
text_idx = original_text.find('Text:')
|
|
472
|
+
func_idx = original_text.find('Functions:')
|
|
473
|
+
if text_idx >= 0 and func_idx > text_idx:
|
|
474
|
+
# Extract JSON from between Text: and Functions:
|
|
475
|
+
json_section = original_text[text_idx + 5:func_idx].strip()
|
|
476
|
+
current_state_data = _extract_balanced_json(json_section)
|
|
477
|
+
|
|
478
|
+
# Parse all function calls
|
|
479
|
+
func_section = original_text[func_idx + 10:].strip()
|
|
480
|
+
all_actions = _parse_function_calls(func_section)
|
|
481
|
+
|
|
482
|
+
if all_actions:
|
|
483
|
+
converted = _build_converted_response(all_actions, current_state_data)
|
|
484
|
+
logger.debug(f"Converted Text/Functions format to {len(all_actions)} action(s)")
|
|
485
|
+
return converted
|
|
486
|
+
except (json.JSONDecodeError, TypeError, AttributeError, ValueError) as e:
|
|
487
|
+
logger.debug(f"Failed to parse Text/Functions format: {e}")
|
|
488
|
+
|
|
489
|
+
# T-05b: Handle "Functions:" only format (without "Text:" prefix)
|
|
490
|
+
# Some models return just "Functions: func_name(args)" without any JSON brain state
|
|
491
|
+
# CRITICAL FIX (Dec 2025): Refactored to use helper functions
|
|
492
|
+
elif 'Functions:' in original_text:
|
|
493
|
+
try:
|
|
494
|
+
func_idx = original_text.find('Functions:')
|
|
495
|
+
func_section = original_text[func_idx + 10:].strip()
|
|
496
|
+
|
|
497
|
+
# Try to extract JSON from before Functions: section
|
|
498
|
+
json_before = original_text[:func_idx].strip()
|
|
499
|
+
current_state_data = _extract_balanced_json(json_before) if json_before else None
|
|
500
|
+
|
|
501
|
+
# Parse all function calls
|
|
502
|
+
all_actions = _parse_function_calls(func_section)
|
|
503
|
+
|
|
504
|
+
if all_actions:
|
|
505
|
+
converted = _build_converted_response(all_actions, current_state_data)
|
|
506
|
+
logger.debug(f"Converted Functions-only format to {len(all_actions)} action(s)")
|
|
507
|
+
return converted
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.debug(f"Failed to parse Functions-only format: {e}")
|
|
510
|
+
|
|
511
|
+
# FIX 3b: Try extracting from blocks - prefer first valid JSON
|
|
512
|
+
patterns = [
|
|
513
|
+
r'\{[^{}]*"current_state"[^{}]*"action"[^{}]*\}', # Prioritize JSON with required keys
|
|
514
|
+
r'\{.*?"current_state".*?"action".*?\}', # Larger JSON with required keys
|
|
515
|
+
r'\{[^{}]*\}', # Simple JSON objects
|
|
516
|
+
r'\{.*?\}', # Any JSON object (non-greedy)
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
for pattern in patterns:
|
|
520
|
+
matches = re.findall(pattern, original_text, re.DOTALL)
|
|
521
|
+
for match in matches:
|
|
522
|
+
try:
|
|
523
|
+
cleaned = match.strip()
|
|
524
|
+
if cleaned:
|
|
525
|
+
parsed = json.loads(cleaned)
|
|
526
|
+
# FIX 3d: Validate and return first valid JSON
|
|
527
|
+
if Config.REQUIRE_SCHEMA_KEYS:
|
|
528
|
+
if Config.validate_json_candidate(json.dumps(parsed)):
|
|
529
|
+
logger.debug(f"Successfully extracted JSON with required keys from pattern: {pattern[:30]}...")
|
|
530
|
+
return parsed
|
|
531
|
+
else:
|
|
532
|
+
return parsed
|
|
533
|
+
except (json.JSONDecodeError, TypeError):
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
# Last resort: find anything that looks like JSON
|
|
537
|
+
# Look for content between first { and last }
|
|
538
|
+
start = original_text.find('{')
|
|
539
|
+
end = original_text.rfind('}')
|
|
540
|
+
|
|
541
|
+
if start >= 0 and end > start:
|
|
542
|
+
potential_json = original_text[start:end+1]
|
|
543
|
+
try:
|
|
544
|
+
parsed = json.loads(potential_json)
|
|
545
|
+
# FIX 3d: Final validation
|
|
546
|
+
if Config.REQUIRE_SCHEMA_KEYS:
|
|
547
|
+
if Config.validate_json_candidate(json.dumps(parsed)):
|
|
548
|
+
logger.debug("Successfully extracted JSON from bracket positions")
|
|
549
|
+
return parsed
|
|
550
|
+
else:
|
|
551
|
+
# Log what keys are missing for debugging
|
|
552
|
+
missing_keys = [k for k in Config.REQUIRED_KEYS if k not in parsed]
|
|
553
|
+
logger.warning(f"JSON found but missing required keys: {missing_keys}")
|
|
554
|
+
else:
|
|
555
|
+
return parsed
|
|
556
|
+
except (json.JSONDecodeError, TypeError):
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
# FIX 3c: Keep standardized error message
|
|
560
|
+
raise ValueError("Could not parse response")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def validate_json_response(response: Any, required_fields: Optional[list] = None) -> Dict[str, Any]:
|
|
564
|
+
"""Validate and extract JSON from a model response.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
response: Response object or string from model
|
|
568
|
+
required_fields: Optional list of required field names
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Validated JSON dictionary
|
|
572
|
+
|
|
573
|
+
Raises:
|
|
574
|
+
ValueError: If validation fails with standardized message
|
|
575
|
+
"""
|
|
576
|
+
# Extract content from response object if needed
|
|
577
|
+
content = response
|
|
578
|
+
if hasattr(response, 'content'):
|
|
579
|
+
content = response.content
|
|
580
|
+
elif hasattr(response, 'text'):
|
|
581
|
+
content = response.text
|
|
582
|
+
|
|
583
|
+
# Convert to string if needed
|
|
584
|
+
if not isinstance(content, str):
|
|
585
|
+
content = str(content)
|
|
586
|
+
|
|
587
|
+
# Extract JSON
|
|
588
|
+
try:
|
|
589
|
+
result = extract_json_from_model_output(content)
|
|
590
|
+
except ValueError:
|
|
591
|
+
# Re-raise with standardized message
|
|
592
|
+
raise
|
|
593
|
+
|
|
594
|
+
# Validate required fields if specified
|
|
595
|
+
if required_fields:
|
|
596
|
+
missing = [f for f in required_fields if f not in result]
|
|
597
|
+
if missing:
|
|
598
|
+
raise ValueError(f"Could not parse response: missing required fields {missing}")
|
|
599
|
+
|
|
600
|
+
return result
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def normalize_action_schema(data: Union[Dict[str, Any], List, Any]) -> Union[Dict[str, Any], List, Any]:
|
|
607
|
+
"""Normalize field names in LLM response to handle provider inconsistencies.
|
|
608
|
+
|
|
609
|
+
RESPONSIBILITY CONTRACT:
|
|
610
|
+
- This function ONLY handles action payload field name transformations
|
|
611
|
+
- It does NOT handle tool_call format differences (that's ToolCallBuilder.normalize_tool_call's job)
|
|
612
|
+
- Keep these responsibilities separate to avoid duplication and confusion
|
|
613
|
+
|
|
614
|
+
This function centralizes field name normalization logic that was previously
|
|
615
|
+
duplicated in Agent._normalize_field_names. Fixes common issues where LLMs
|
|
616
|
+
use incorrect field names based on training patterns.
|
|
617
|
+
|
|
618
|
+
Common fixes:
|
|
619
|
+
- Function-calling format conversion: {"function": "name", "args": {...}} -> {"name": {...}}
|
|
620
|
+
- Field name mappings: done.message -> done.text, todo_add.message -> todo_add.text
|
|
621
|
+
- write_file.file_name -> write_file.file_path
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
data: Raw data from LLM response (dict, list, or primitive)
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Normalized data with corrected field names
|
|
628
|
+
"""
|
|
629
|
+
if isinstance(data, dict):
|
|
630
|
+
normalized_data = {}
|
|
631
|
+
for key, value in data.items():
|
|
632
|
+
# Recursively normalize nested objects
|
|
633
|
+
normalized_value = normalize_action_schema(value)
|
|
634
|
+
|
|
635
|
+
# Handle specific field name mappings for actions
|
|
636
|
+
if key == "action" and isinstance(normalized_value, list):
|
|
637
|
+
# Process each action in the list
|
|
638
|
+
fixed_actions = []
|
|
639
|
+
for action in normalized_value:
|
|
640
|
+
if isinstance(action, dict):
|
|
641
|
+
# Handle function-calling format {"function": "name", "args": {...}}
|
|
642
|
+
if "function" in action and "args" in action:
|
|
643
|
+
# Convert from function-calling format to ActionModel format
|
|
644
|
+
func_name = action["function"]
|
|
645
|
+
func_args = action.get("args", {})
|
|
646
|
+
|
|
647
|
+
# Apply action-specific field corrections
|
|
648
|
+
func_args = apply_action_field_corrections(func_name, func_args)
|
|
649
|
+
|
|
650
|
+
fixed_action = {func_name: func_args}
|
|
651
|
+
logger.debug(f"Converted function-calling format: {func_name}")
|
|
652
|
+
else:
|
|
653
|
+
# Standard format, process normally
|
|
654
|
+
# Preprocess the action to handle various formats
|
|
655
|
+
action_name, action_params = preprocess_action_data(action)
|
|
656
|
+
|
|
657
|
+
if isinstance(action_params, dict):
|
|
658
|
+
# Apply field corrections
|
|
659
|
+
action_params = apply_action_field_corrections(action_name, action_params)
|
|
660
|
+
|
|
661
|
+
fixed_action = {action_name: action_params}
|
|
662
|
+
fixed_actions.append(fixed_action)
|
|
663
|
+
else:
|
|
664
|
+
# Non-dict action, keep as is
|
|
665
|
+
fixed_actions.append(action)
|
|
666
|
+
normalized_data[key] = fixed_actions
|
|
667
|
+
else:
|
|
668
|
+
# Regular field, no special processing
|
|
669
|
+
normalized_data[key] = normalized_value
|
|
670
|
+
|
|
671
|
+
return normalized_data
|
|
672
|
+
elif isinstance(data, list):
|
|
673
|
+
return [normalize_action_schema(item) for item in data]
|
|
674
|
+
else:
|
|
675
|
+
return data
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
#: Keys that mark a JSON object as agent brain-state telemetry rather than an
|
|
679
|
+
#: arbitrary result. A ``current_state`` wrapper or >=2 of these ⇒ brain-state.
|
|
680
|
+
_BRAIN_STATE_KEYS = frozenset({
|
|
681
|
+
'current_state', 'next_goal', 'evaluation_previous_goal', 'page_summary',
|
|
682
|
+
'memory', 'reasoning', 'macro_goal', 'subgoal', 'immediate_step',
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def is_brain_state_content(content: Optional[str]) -> bool:
|
|
687
|
+
"""True when *content* is (purely) an agent brain-state JSON object.
|
|
688
|
+
|
|
689
|
+
POLYROB's system prompt instructs every model to emit its brain state as a
|
|
690
|
+
``{"current_state": {...}}`` JSON object in the text-content field every
|
|
691
|
+
turn. That content is internal telemetry, NOT the agent's user-facing voice
|
|
692
|
+
— it must never be streamed to output consumers (CLI / API / WebView), or it
|
|
693
|
+
surfaces as a raw JSON dump on a tool-free planning turn for any streaming
|
|
694
|
+
provider.
|
|
695
|
+
|
|
696
|
+
Conservative by design: returns True only when the trimmed content parses as
|
|
697
|
+
a JSON object carrying a ``current_state`` wrapper or >=2 brain keys.
|
|
698
|
+
Genuine prose — and partial token chunks that don't parse — return False and
|
|
699
|
+
stream normally.
|
|
700
|
+
"""
|
|
701
|
+
if not content:
|
|
702
|
+
return False
|
|
703
|
+
flat = content.strip()
|
|
704
|
+
if not (flat.startswith('{') and flat.endswith('}')):
|
|
705
|
+
return False
|
|
706
|
+
try:
|
|
707
|
+
obj = json.loads(flat)
|
|
708
|
+
except (ValueError, TypeError):
|
|
709
|
+
return False
|
|
710
|
+
if not isinstance(obj, dict):
|
|
711
|
+
return False
|
|
712
|
+
if isinstance(obj.get('current_state'), dict):
|
|
713
|
+
return True
|
|
714
|
+
return sum(1 for k in obj if k in _BRAIN_STATE_KEYS) >= 2
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def extract_brain_state_from_json(content: str) -> Dict[str, Any]:
|
|
718
|
+
"""Extract brain state from LLM content in JSON format.
|
|
719
|
+
|
|
720
|
+
This function handles brain state extraction for native tools mode where
|
|
721
|
+
the LLM provides brain state as JSON in the text content field.
|
|
722
|
+
|
|
723
|
+
Supports both formats:
|
|
724
|
+
- Nested: {"current_state": {"memory": "...", ...}}
|
|
725
|
+
- Top-level: {"memory": "...", "next_goal": "...", ...}
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
content: Text content from LLM response
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Dictionary with brain state fields (memory, evaluation_previous_goal, etc.)
|
|
732
|
+
|
|
733
|
+
Raises:
|
|
734
|
+
ValueError: If content cannot be parsed as JSON
|
|
735
|
+
"""
|
|
736
|
+
if not content or not content.strip():
|
|
737
|
+
raise ValueError("Empty content - cannot extract brain state")
|
|
738
|
+
|
|
739
|
+
# Use existing JSON extraction logic
|
|
740
|
+
brain_json = extract_json_from_model_output(content)
|
|
741
|
+
|
|
742
|
+
# Check if brain state is nested under 'current_state' (new format)
|
|
743
|
+
# or at top level (old format)
|
|
744
|
+
if 'current_state' in brain_json and isinstance(brain_json['current_state'], dict):
|
|
745
|
+
# New nested format - extract from current_state
|
|
746
|
+
source = brain_json['current_state']
|
|
747
|
+
logger.debug("Extracting brain state from nested 'current_state' field")
|
|
748
|
+
else:
|
|
749
|
+
# Old top-level format - use directly
|
|
750
|
+
source = brain_json
|
|
751
|
+
logger.debug("Extracting brain state from top-level fields")
|
|
752
|
+
|
|
753
|
+
# Map fields to AgentBrain schema
|
|
754
|
+
brain_fields = {
|
|
755
|
+
'page_summary': source.get('page_summary', ''),
|
|
756
|
+
'memory': source.get('memory', ''),
|
|
757
|
+
'evaluation_previous_goal': source.get('evaluation_previous_goal', 'Unknown'),
|
|
758
|
+
'next_goal': source.get('next_goal', ''),
|
|
759
|
+
'reasoning': source.get('reasoning', ''),
|
|
760
|
+
'phase': source.get('phase'),
|
|
761
|
+
'macro_goal': source.get('macro_goal'),
|
|
762
|
+
'subgoal': source.get('subgoal'),
|
|
763
|
+
'immediate_step': source.get('immediate_step')
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
logger.debug(f"Extracted brain state: memory={brain_fields.get('memory', '')[:100]}")
|
|
767
|
+
|
|
768
|
+
return brain_fields
|