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,950 @@
|
|
|
1
|
+
"""KB ingestion engine + `knowledge` agent tool (Task 6).
|
|
2
|
+
|
|
3
|
+
Adds file→KB ingestion and a registerable ``knowledge`` tool on top of the
|
|
4
|
+
Task-5 registry routers. All KB I/O goes through ``modules.memory.registry``
|
|
5
|
+
(no direct provider access here).
|
|
6
|
+
|
|
7
|
+
``from __future__ import annotations`` is SAFE in this file — the param models are
|
|
8
|
+
explicit (passed to ``BaseTool.action``), so the Registry never needs to introspect
|
|
9
|
+
stringized first-arg annotations here.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import hashlib
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import subprocess
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
24
|
+
|
|
25
|
+
from tools.base_tool import BaseTool
|
|
26
|
+
from tools.controller.types import ActionResult
|
|
27
|
+
from agents.task.agent.core.secret_guard import (
|
|
28
|
+
is_secret_path,
|
|
29
|
+
is_binary_file,
|
|
30
|
+
estimate_tokens_rough,
|
|
31
|
+
)
|
|
32
|
+
from core.path_safety import is_within_root
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Env-driven knobs (read inline, not in constants.py)
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _int_env(name: str, default: int) -> int:
|
|
42
|
+
try:
|
|
43
|
+
return int(os.getenv(name, str(default)))
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
return default
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _kb_max_files() -> int:
|
|
49
|
+
return _int_env("KB_MAX_FILES", 2000)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _kb_max_bytes() -> int:
|
|
53
|
+
return _int_env("KB_MAX_BYTES", 25 * 1024 * 1024)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _kb_chunk_tokens() -> int:
|
|
57
|
+
return _int_env("KB_CHUNK_TOKENS", 800)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _kb_chunk_overlap() -> int:
|
|
61
|
+
return _int_env("KB_CHUNK_OVERLAP", 100)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Office / unsupported extension skip list
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
_OFFICE_EXTENSIONS: frozenset[str] = frozenset({
|
|
69
|
+
".doc", ".odt",
|
|
70
|
+
".xlsx", ".xls", ".ods",
|
|
71
|
+
".pptx", ".ppt",
|
|
72
|
+
".epub", ".rtf",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Extensions handled by dedicated extractors (not in _OFFICE_EXTENSIONS skip list)
|
|
76
|
+
_DOCX_EXTENSION = ".docx"
|
|
77
|
+
|
|
78
|
+
_TEXT_EXTENSIONS: frozenset[str] = frozenset({
|
|
79
|
+
".txt", ".md", ".rst", ".csv",
|
|
80
|
+
".py", ".js", ".ts", ".tsx", ".jsx",
|
|
81
|
+
".sh", ".bash", ".zsh",
|
|
82
|
+
".json", ".yaml", ".yml", ".toml", ".ini", ".cfg",
|
|
83
|
+
".html", ".xml", ".sql",
|
|
84
|
+
".go", ".rs", ".c", ".cpp", ".h", ".hpp",
|
|
85
|
+
".java", ".kt", ".swift", ".rb", ".php",
|
|
86
|
+
".r", ".scala", ".clj",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Pure engine helpers
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _iter_files(
|
|
96
|
+
root: Path,
|
|
97
|
+
*,
|
|
98
|
+
recursive: bool = True,
|
|
99
|
+
globs: Optional[List[str]] = None,
|
|
100
|
+
max_files: int = 2000,
|
|
101
|
+
max_bytes: int = 25 * 1024 * 1024,
|
|
102
|
+
) -> Tuple[List[Path], Dict[str, int]]:
|
|
103
|
+
"""Bounded, safe walk of *root*.
|
|
104
|
+
|
|
105
|
+
Returns ``(files, skipped)`` where ``skipped`` maps reason→count.
|
|
106
|
+
Prefers ``rg --files`` for gitignore-aware traversal; falls back to
|
|
107
|
+
``os.walk`` skipping hidden dirs and ``__pycache__``.
|
|
108
|
+
|
|
109
|
+
Hard-skips:
|
|
110
|
+
- ``is_secret_path`` (credentials / env files)
|
|
111
|
+
- ``is_binary_file`` (compiled binaries, images, etc.)
|
|
112
|
+
- Files beyond ``max_files`` / ``max_bytes`` cumulative size limits.
|
|
113
|
+
"""
|
|
114
|
+
root = root.resolve()
|
|
115
|
+
skipped: Dict[str, int] = {
|
|
116
|
+
"secret": 0,
|
|
117
|
+
"binary": 0,
|
|
118
|
+
"max_files": 0,
|
|
119
|
+
"max_bytes": 0,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Collect candidate paths ------------------------------------------------
|
|
123
|
+
candidates: List[Path] = []
|
|
124
|
+
|
|
125
|
+
if recursive:
|
|
126
|
+
# Try rg --files for .gitignore awareness
|
|
127
|
+
rg_paths = _rg_files(root)
|
|
128
|
+
if rg_paths is not None:
|
|
129
|
+
candidates = rg_paths
|
|
130
|
+
else:
|
|
131
|
+
# Fallback: os.walk, skip hidden dirs + __pycache__
|
|
132
|
+
for dirpath, dirnames, filenames in os.walk(str(root)):
|
|
133
|
+
# Prune hidden dirs and __pycache__ in-place
|
|
134
|
+
dirnames[:] = [
|
|
135
|
+
d for d in dirnames
|
|
136
|
+
if not d.startswith(".") and d != "__pycache__"
|
|
137
|
+
]
|
|
138
|
+
for fname in filenames:
|
|
139
|
+
candidates.append(Path(dirpath) / fname)
|
|
140
|
+
else:
|
|
141
|
+
# Non-recursive: direct children only
|
|
142
|
+
try:
|
|
143
|
+
candidates = [p for p in root.iterdir() if p.is_file()]
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Apply glob filters if requested ----------------------------------------
|
|
148
|
+
if globs:
|
|
149
|
+
import fnmatch as _fnmatch
|
|
150
|
+
filtered = []
|
|
151
|
+
for p in candidates:
|
|
152
|
+
rel = str(p.relative_to(root)) if p.is_absolute() else str(p)
|
|
153
|
+
if any(_fnmatch.fnmatch(rel, g) or _fnmatch.fnmatch(p.name, g) for g in globs):
|
|
154
|
+
filtered.append(p)
|
|
155
|
+
candidates = filtered
|
|
156
|
+
|
|
157
|
+
# Filter: secret / binary; enforce limits --------------------------------
|
|
158
|
+
collected: List[Path] = []
|
|
159
|
+
total_bytes = 0
|
|
160
|
+
|
|
161
|
+
for path in candidates:
|
|
162
|
+
if not path.is_file():
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Secret hard-skip
|
|
166
|
+
if is_secret_path(path, root=root):
|
|
167
|
+
skipped["secret"] += 1
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Binary hard-skip (don't hard-skip .pdf — extractable)
|
|
171
|
+
if is_binary_file(path):
|
|
172
|
+
skipped["binary"] += 1
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# File count cap
|
|
176
|
+
if len(collected) >= max_files:
|
|
177
|
+
skipped["max_files"] += 1
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Byte cap (skip oversized files; cumulative cap too)
|
|
181
|
+
try:
|
|
182
|
+
fsize = path.stat().st_size
|
|
183
|
+
except OSError:
|
|
184
|
+
continue
|
|
185
|
+
if total_bytes + fsize > max_bytes:
|
|
186
|
+
skipped["max_bytes"] += 1
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
collected.append(path)
|
|
190
|
+
total_bytes += fsize
|
|
191
|
+
|
|
192
|
+
return collected, skipped
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _rg_files(root: Path) -> Optional[List[Path]]:
|
|
196
|
+
"""Run ``rg --files <root>`` and return the paths, or None if unavailable."""
|
|
197
|
+
try:
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
["rg", "--files", str(root)],
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
timeout=30,
|
|
203
|
+
)
|
|
204
|
+
if result.returncode != 0:
|
|
205
|
+
return None
|
|
206
|
+
paths = []
|
|
207
|
+
for line in result.stdout.splitlines():
|
|
208
|
+
line = line.strip()
|
|
209
|
+
if line:
|
|
210
|
+
p = Path(line)
|
|
211
|
+
if not p.is_absolute():
|
|
212
|
+
p = root / p
|
|
213
|
+
paths.append(p)
|
|
214
|
+
return paths
|
|
215
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def _extract_text(path: Path) -> Tuple[Optional[str], Optional[str]]:
|
|
220
|
+
"""Extract text from *path*.
|
|
221
|
+
|
|
222
|
+
Returns ``(text, skip_reason)`` — exactly one of the two is ``None``.
|
|
223
|
+
|
|
224
|
+
- ``.pdf`` → direct ``await PdfExtractionMixin._process_pdf(content)``
|
|
225
|
+
(sync ``_extract_pdf_direct`` fallback only if the mixin raises / imports fail)
|
|
226
|
+
- text / code extensions → ``path.read_text(errors='replace')``
|
|
227
|
+
- office formats → ``(None, "office-skip")`` — no parser yet (Task 19)
|
|
228
|
+
"""
|
|
229
|
+
suffix = path.suffix.lower()
|
|
230
|
+
|
|
231
|
+
# .docx — extract via python-docx (fail-open: if lib absent → skip-with-note)
|
|
232
|
+
if suffix == _DOCX_EXTENSION:
|
|
233
|
+
text = _extract_docx(path)
|
|
234
|
+
if text is None:
|
|
235
|
+
return None, f"office-skip:{suffix}"
|
|
236
|
+
return text, None
|
|
237
|
+
|
|
238
|
+
# Other office formats — no parser available
|
|
239
|
+
if suffix in _OFFICE_EXTENSIONS:
|
|
240
|
+
return None, f"office-skip:{suffix}"
|
|
241
|
+
|
|
242
|
+
# PDF extraction (async — directly await the mixin, no sync-from-async bridge)
|
|
243
|
+
if suffix == ".pdf":
|
|
244
|
+
try:
|
|
245
|
+
content_bytes = path.read_bytes()
|
|
246
|
+
text = await _extract_pdf_text(content_bytes)
|
|
247
|
+
if text:
|
|
248
|
+
return text, None
|
|
249
|
+
return None, "pdf-empty"
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.debug("PDF extraction failed for %s: %s", path, e)
|
|
252
|
+
return None, f"pdf-error:{type(e).__name__}"
|
|
253
|
+
|
|
254
|
+
# Plain text / code
|
|
255
|
+
try:
|
|
256
|
+
text = path.read_text(errors="replace")
|
|
257
|
+
return text, None
|
|
258
|
+
except OSError as e:
|
|
259
|
+
return None, f"read-error:{type(e).__name__}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _extract_pdf_text(content_bytes: bytes) -> str:
|
|
263
|
+
"""Async adapter over ``PdfExtractionMixin._process_pdf``.
|
|
264
|
+
|
|
265
|
+
Directly ``await``s the (async) mixin from within the running loop — no
|
|
266
|
+
``run_coroutine_sync`` bridge. Falls back to the SYNC ``_extract_pdf_direct``
|
|
267
|
+
only if the mixin raises or its import fails.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
from tools.filesystem_pdf import PdfExtractionMixin
|
|
271
|
+
mixin = PdfExtractionMixin()
|
|
272
|
+
# _process_pdf reads self.logger / self.container — provide minimal stand-ins.
|
|
273
|
+
mixin.logger = logging.getLogger("knowledge_ingest.pdf") # type: ignore[attr-defined]
|
|
274
|
+
mixin.container = None # type: ignore[attr-defined]
|
|
275
|
+
result = await mixin._process_pdf(content_bytes)
|
|
276
|
+
return result.get("content", "")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.debug("PDF mixin extraction failed, using direct fallback: %s", e)
|
|
279
|
+
return _extract_pdf_direct(content_bytes)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _extract_pdf_direct(content_bytes: bytes) -> str:
|
|
283
|
+
"""SYNC-only fallback PDF extraction using pypdf directly.
|
|
284
|
+
|
|
285
|
+
Used only when ``_process_pdf`` raises or its import fails. No event-loop
|
|
286
|
+
detection, no ``run_coroutine_sync``.
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
import pypdf # type: ignore
|
|
290
|
+
from io import BytesIO
|
|
291
|
+
reader = pypdf.PdfReader(BytesIO(content_bytes), strict=False)
|
|
292
|
+
texts = []
|
|
293
|
+
for page in reader.pages:
|
|
294
|
+
try:
|
|
295
|
+
t = page.extract_text()
|
|
296
|
+
if t:
|
|
297
|
+
texts.append(t)
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
return "\n\n".join(texts)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.debug("Direct PDF extraction failed: %s", e)
|
|
303
|
+
return ""
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _extract_docx(path: Path) -> Optional[str]:
|
|
307
|
+
"""Extract plain text from a ``.docx`` file via ``python-docx``.
|
|
308
|
+
|
|
309
|
+
Lazy import — if ``python-docx`` is not installed, returns ``None`` so
|
|
310
|
+
the caller skips the file with-note (fail-open, never an error).
|
|
311
|
+
|
|
312
|
+
Paragraph text is joined with ``\\n``; table cell text is appended after
|
|
313
|
+
paragraphs so tables aren't silently dropped.
|
|
314
|
+
|
|
315
|
+
Any extraction error (corrupt file, unexpected docx layout) also returns
|
|
316
|
+
``None`` (fail-open, logged at DEBUG).
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
import docx as _docx # python-docx; import name is 'docx'
|
|
320
|
+
except ImportError:
|
|
321
|
+
logger.debug("python-docx not installed; skipping %s", path)
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
doc = _docx.Document(str(path))
|
|
326
|
+
parts: list[str] = []
|
|
327
|
+
|
|
328
|
+
# Body paragraphs
|
|
329
|
+
for para in doc.paragraphs:
|
|
330
|
+
text = para.text
|
|
331
|
+
if text.strip():
|
|
332
|
+
parts.append(text)
|
|
333
|
+
|
|
334
|
+
# Table cells (flattened, row-major)
|
|
335
|
+
for table in doc.tables:
|
|
336
|
+
for row in table.rows:
|
|
337
|
+
for cell in row.cells:
|
|
338
|
+
cell_text = cell.text.strip()
|
|
339
|
+
if cell_text:
|
|
340
|
+
parts.append(cell_text)
|
|
341
|
+
|
|
342
|
+
return "\n".join(parts) if parts else ""
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.debug("docx extraction failed for %s: %s", path, e)
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Structure-first chunker
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
# Markdown heading pattern
|
|
353
|
+
_MD_HEADING_RE = re.compile(r"^#{1,6} .+", re.MULTILINE)
|
|
354
|
+
# Top-level Python def/class
|
|
355
|
+
_PYDEF_RE = re.compile(r"^(?:def |class )\S", re.MULTILINE)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _chunk(
|
|
359
|
+
text: str,
|
|
360
|
+
*,
|
|
361
|
+
target: Optional[int] = None,
|
|
362
|
+
overlap: Optional[int] = None,
|
|
363
|
+
) -> List[str]:
|
|
364
|
+
"""Structure-first text chunker.
|
|
365
|
+
|
|
366
|
+
Splits on: markdown headings → blank-line paragraphs → top-level
|
|
367
|
+
``def``/``class`` → raw size. Chunks are sized by ``estimate_tokens_rough``
|
|
368
|
+
to ~``target`` tokens with ``overlap`` token carry.
|
|
369
|
+
"""
|
|
370
|
+
target = target if target is not None else _kb_chunk_tokens()
|
|
371
|
+
overlap = overlap if overlap is not None else _kb_chunk_overlap()
|
|
372
|
+
|
|
373
|
+
if not text or not text.strip():
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
# 1. Try markdown heading split
|
|
377
|
+
parts = _split_on_headings(text)
|
|
378
|
+
if len(parts) <= 1:
|
|
379
|
+
# 2. Try blank-line paragraph split
|
|
380
|
+
parts = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
|
|
381
|
+
if len(parts) <= 1:
|
|
382
|
+
# 3. Try top-level def/class split (Python)
|
|
383
|
+
parts = _split_on_defs(text)
|
|
384
|
+
if len(parts) <= 1:
|
|
385
|
+
# 4. Raw line split
|
|
386
|
+
parts = text.splitlines()
|
|
387
|
+
|
|
388
|
+
# Merge small parts up to target size; carry overlap into next chunk
|
|
389
|
+
return _merge_to_target(parts, target=target, overlap=overlap)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _split_on_headings(text: str) -> List[str]:
|
|
393
|
+
"""Split *text* at markdown ``#`` headings (keep heading in chunk)."""
|
|
394
|
+
positions = [m.start() for m in _MD_HEADING_RE.finditer(text)]
|
|
395
|
+
if not positions:
|
|
396
|
+
return [text]
|
|
397
|
+
parts = []
|
|
398
|
+
positions.append(len(text))
|
|
399
|
+
for i in range(len(positions) - 1):
|
|
400
|
+
chunk = text[positions[i]:positions[i + 1]].strip()
|
|
401
|
+
if chunk:
|
|
402
|
+
parts.append(chunk)
|
|
403
|
+
# Prepend any text before the first heading
|
|
404
|
+
if positions[0] > 0:
|
|
405
|
+
preamble = text[:positions[0]].strip()
|
|
406
|
+
if preamble:
|
|
407
|
+
parts.insert(0, preamble)
|
|
408
|
+
return parts if parts else [text]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _split_on_defs(text: str) -> List[str]:
|
|
412
|
+
"""Split on top-level Python ``def``/``class`` lines."""
|
|
413
|
+
positions = [m.start() for m in _PYDEF_RE.finditer(text)]
|
|
414
|
+
if not positions:
|
|
415
|
+
return [text]
|
|
416
|
+
parts = []
|
|
417
|
+
positions.append(len(text))
|
|
418
|
+
if positions[0] > 0:
|
|
419
|
+
preamble = text[:positions[0]].strip()
|
|
420
|
+
if preamble:
|
|
421
|
+
parts.append(preamble)
|
|
422
|
+
for i in range(len(positions) - 1):
|
|
423
|
+
chunk = text[positions[i]:positions[i + 1]].strip()
|
|
424
|
+
if chunk:
|
|
425
|
+
parts.append(chunk)
|
|
426
|
+
return parts if parts else [text]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _merge_to_target(parts: List[str], *, target: int, overlap: int) -> List[str]:
|
|
430
|
+
"""Merge *parts* into chunks of at most *target* tokens, carrying *overlap*.
|
|
431
|
+
|
|
432
|
+
If a single *part* exceeds *target* tokens, it is further split on word
|
|
433
|
+
boundaries so no output chunk grows unboundedly.
|
|
434
|
+
"""
|
|
435
|
+
chunks: List[str] = []
|
|
436
|
+
current_lines: List[str] = []
|
|
437
|
+
current_tokens = 0
|
|
438
|
+
overlap_carry = ""
|
|
439
|
+
|
|
440
|
+
def _flush():
|
|
441
|
+
nonlocal current_lines, current_tokens, overlap_carry
|
|
442
|
+
if not current_lines:
|
|
443
|
+
return
|
|
444
|
+
chunk_text = overlap_carry + ("\n\n" if overlap_carry else "") + "\n\n".join(current_lines)
|
|
445
|
+
chunks.append(chunk_text.strip())
|
|
446
|
+
overlap_carry = _tail_tokens(chunk_text, overlap)
|
|
447
|
+
current_lines = []
|
|
448
|
+
current_tokens = estimate_tokens_rough(overlap_carry)
|
|
449
|
+
|
|
450
|
+
for part in parts:
|
|
451
|
+
part_tokens = estimate_tokens_rough(part)
|
|
452
|
+
|
|
453
|
+
# If a single part is too large, split it on word boundaries first
|
|
454
|
+
if part_tokens > target:
|
|
455
|
+
# Flush anything accumulated so far
|
|
456
|
+
_flush()
|
|
457
|
+
# Split oversized part into word-boundary slices
|
|
458
|
+
for sub in _split_by_tokens(part, target):
|
|
459
|
+
sub_tokens = estimate_tokens_rough(sub)
|
|
460
|
+
if current_tokens + sub_tokens > target and current_lines:
|
|
461
|
+
_flush()
|
|
462
|
+
current_lines.append(sub)
|
|
463
|
+
current_tokens += sub_tokens
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
if current_tokens + part_tokens > target and current_lines:
|
|
467
|
+
_flush()
|
|
468
|
+
|
|
469
|
+
current_lines.append(part)
|
|
470
|
+
current_tokens += part_tokens
|
|
471
|
+
|
|
472
|
+
# Final flush
|
|
473
|
+
_flush()
|
|
474
|
+
|
|
475
|
+
return [c for c in chunks if c]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _split_by_tokens(text: str, target: int) -> List[str]:
|
|
479
|
+
"""Split *text* into slices of at most *target* tokens (word-boundary split)."""
|
|
480
|
+
words = text.split()
|
|
481
|
+
slices: List[str] = []
|
|
482
|
+
current_words: List[str] = []
|
|
483
|
+
current_chars = 0
|
|
484
|
+
target_chars = target * 4 # estimate_tokens_rough uses // 4
|
|
485
|
+
|
|
486
|
+
for word in words:
|
|
487
|
+
wlen = len(word) + 1 # +1 for space
|
|
488
|
+
if current_chars + wlen > target_chars and current_words:
|
|
489
|
+
slices.append(" ".join(current_words))
|
|
490
|
+
current_words = [word]
|
|
491
|
+
current_chars = wlen
|
|
492
|
+
else:
|
|
493
|
+
current_words.append(word)
|
|
494
|
+
current_chars += wlen
|
|
495
|
+
|
|
496
|
+
if current_words:
|
|
497
|
+
slices.append(" ".join(current_words))
|
|
498
|
+
|
|
499
|
+
return slices if slices else [text]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _tail_tokens(text: str, n_tokens: int) -> str:
|
|
503
|
+
"""Return the last ~*n_tokens* token-equivalents of *text* (char-based approx)."""
|
|
504
|
+
if n_tokens <= 0:
|
|
505
|
+
return ""
|
|
506
|
+
tail_chars = n_tokens * 4 # estimate_tokens_rough uses // 4
|
|
507
|
+
return text[-tail_chars:].lstrip()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
# Async ingestion entry point
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
async def kb_ingest(
|
|
516
|
+
path: str,
|
|
517
|
+
collection: str = "default",
|
|
518
|
+
recursive: bool = True,
|
|
519
|
+
globs: Optional[List[str]] = None,
|
|
520
|
+
*,
|
|
521
|
+
user_id: str,
|
|
522
|
+
session_id: str,
|
|
523
|
+
source_name: Optional[str] = None,
|
|
524
|
+
) -> Dict[str, Any]:
|
|
525
|
+
"""Ingest a file or directory into the KB.
|
|
526
|
+
|
|
527
|
+
Returns counts: ``{ingested, skipped_secret, skipped_binary, unchanged,
|
|
528
|
+
n_chunks, skipped_office}``.
|
|
529
|
+
|
|
530
|
+
Path confinement:
|
|
531
|
+
- Server sessions: confined to ``pm().get_workspace_dir(session_id, user_id)``
|
|
532
|
+
- CLI (workspace_is_project_root): confined to ``Path.cwd()``
|
|
533
|
+
|
|
534
|
+
``source_name`` (optional): a STABLE logical identity to store as the chunk's
|
|
535
|
+
``source_path`` / dedup key instead of the volatile on-disk path. Used by the
|
|
536
|
+
HTTP upload path so a re-upload of the same file dedups by its original filename
|
|
537
|
+
rather than a fresh ``mkstemp`` path. Only honored for single-file ingestion
|
|
538
|
+
(a directory walk has many sources, so ``source_name`` is ignored there). When
|
|
539
|
+
``None`` the behavior is byte-identical to before (on-disk path is the source).
|
|
540
|
+
"""
|
|
541
|
+
from modules.memory.registry import (
|
|
542
|
+
kb_ingest_chunk as _kb_ingest_chunk,
|
|
543
|
+
kb_remove as _kb_remove,
|
|
544
|
+
kb_source_hash as _kb_source_hash,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
target_path = Path(path)
|
|
548
|
+
|
|
549
|
+
# ------------------------------------------------------------------
|
|
550
|
+
# Path confinement
|
|
551
|
+
# ------------------------------------------------------------------
|
|
552
|
+
allowed_root = _resolve_confinement_root(session_id, user_id)
|
|
553
|
+
resolved = target_path.resolve() if target_path.is_absolute() else (allowed_root / path).resolve()
|
|
554
|
+
|
|
555
|
+
if not is_within_root(str(resolved), str(allowed_root)):
|
|
556
|
+
return {
|
|
557
|
+
"error": f"Path '{path}' is outside the allowed workspace root '{allowed_root}'",
|
|
558
|
+
"ingested": 0,
|
|
559
|
+
"skipped_secret": 0,
|
|
560
|
+
"skipped_binary": 0,
|
|
561
|
+
"unchanged": 0,
|
|
562
|
+
"n_chunks": 0,
|
|
563
|
+
"skipped_office": 0,
|
|
564
|
+
"skipped_too_large": 0,
|
|
565
|
+
"failed": 0,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# ------------------------------------------------------------------
|
|
569
|
+
# Walk files
|
|
570
|
+
# ------------------------------------------------------------------
|
|
571
|
+
if resolved.is_file():
|
|
572
|
+
files = [resolved]
|
|
573
|
+
walk_skipped: Dict[str, int] = {
|
|
574
|
+
"secret": 0, "binary": 0, "max_files": 0, "max_bytes": 0, "too_large": 0,
|
|
575
|
+
}
|
|
576
|
+
# Still apply secret/binary guards for single-file ingestion
|
|
577
|
+
if is_secret_path(resolved, root=allowed_root):
|
|
578
|
+
walk_skipped["secret"] += 1
|
|
579
|
+
files = []
|
|
580
|
+
elif is_binary_file(resolved):
|
|
581
|
+
walk_skipped["binary"] += 1
|
|
582
|
+
files = []
|
|
583
|
+
else:
|
|
584
|
+
# Byte cap also applies to a single file — never read an unbounded file
|
|
585
|
+
# into memory (the directory walk caps cumulative size; this caps the
|
|
586
|
+
# one-file path, which would otherwise OOM on a multi-GB file).
|
|
587
|
+
try:
|
|
588
|
+
if resolved.stat().st_size > _kb_max_bytes():
|
|
589
|
+
walk_skipped["too_large"] += 1
|
|
590
|
+
files = []
|
|
591
|
+
except OSError:
|
|
592
|
+
files = []
|
|
593
|
+
elif resolved.is_dir():
|
|
594
|
+
files, walk_skipped = _iter_files(
|
|
595
|
+
resolved,
|
|
596
|
+
recursive=recursive,
|
|
597
|
+
globs=globs,
|
|
598
|
+
max_files=_kb_max_files(),
|
|
599
|
+
max_bytes=_kb_max_bytes(),
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
return {
|
|
603
|
+
"error": f"Path '{path}' does not exist or is not accessible.",
|
|
604
|
+
"ingested": 0,
|
|
605
|
+
"skipped_secret": 0,
|
|
606
|
+
"skipped_binary": 0,
|
|
607
|
+
"unchanged": 0,
|
|
608
|
+
"n_chunks": 0,
|
|
609
|
+
"skipped_office": 0,
|
|
610
|
+
"skipped_too_large": 0,
|
|
611
|
+
"failed": 0,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
# ------------------------------------------------------------------
|
|
615
|
+
# Ingest each file
|
|
616
|
+
# ------------------------------------------------------------------
|
|
617
|
+
counts: Dict[str, int] = {
|
|
618
|
+
"ingested": 0,
|
|
619
|
+
"skipped_secret": walk_skipped.get("secret", 0),
|
|
620
|
+
"skipped_binary": walk_skipped.get("binary", 0),
|
|
621
|
+
"unchanged": 0,
|
|
622
|
+
"n_chunks": 0,
|
|
623
|
+
"skipped_office": 0,
|
|
624
|
+
# oversized single file ("too_large") + oversized files in a dir walk ("max_bytes")
|
|
625
|
+
"skipped_too_large": walk_skipped.get("too_large", 0) + walk_skipped.get("max_bytes", 0),
|
|
626
|
+
"failed": 0,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
target_tokens = _kb_chunk_tokens()
|
|
630
|
+
overlap_tokens = _kb_chunk_overlap()
|
|
631
|
+
|
|
632
|
+
# A stable source_name only makes sense for single-file ingestion (a directory
|
|
633
|
+
# walk has many distinct sources). Ignore it for multi-file walks.
|
|
634
|
+
use_source_name = source_name if (source_name and len(files) == 1) else None
|
|
635
|
+
|
|
636
|
+
for fpath in files:
|
|
637
|
+
source_path_str = use_source_name or str(fpath)
|
|
638
|
+
|
|
639
|
+
# Read + hash off the event loop — reading a large file and hashing it is
|
|
640
|
+
# synchronous CPU/IO that would otherwise stall every other session during a
|
|
641
|
+
# bulk ingest.
|
|
642
|
+
try:
|
|
643
|
+
file_hash = await asyncio.to_thread(_read_and_hash, fpath)
|
|
644
|
+
except OSError as e:
|
|
645
|
+
logger.warning("Cannot read %s: %s", fpath, e)
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
# Check existing hash via registry
|
|
649
|
+
existing_hash = await _kb_source_hash(
|
|
650
|
+
user_id=user_id,
|
|
651
|
+
collection=collection,
|
|
652
|
+
source_path=source_path_str,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if existing_hash == file_hash:
|
|
656
|
+
counts["unchanged"] += 1
|
|
657
|
+
continue
|
|
658
|
+
|
|
659
|
+
# If changed (or new): remove old chunks, re-ingest
|
|
660
|
+
if existing_hash is not None:
|
|
661
|
+
await _kb_remove(
|
|
662
|
+
user_id=user_id,
|
|
663
|
+
collection=collection,
|
|
664
|
+
source=source_path_str,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Extract text
|
|
668
|
+
text, skip_reason = await _extract_text(fpath)
|
|
669
|
+
if text is None:
|
|
670
|
+
if skip_reason and skip_reason.startswith("office-skip"):
|
|
671
|
+
counts["skipped_office"] += 1
|
|
672
|
+
else:
|
|
673
|
+
logger.debug("Skipping %s: %s", fpath, skip_reason)
|
|
674
|
+
continue
|
|
675
|
+
|
|
676
|
+
# Chunk (CPU-bound) off the event loop too.
|
|
677
|
+
chunks = await asyncio.to_thread(
|
|
678
|
+
_chunk, text, target=target_tokens, overlap=overlap_tokens
|
|
679
|
+
)
|
|
680
|
+
if not chunks:
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
# Ingest each chunk
|
|
684
|
+
mime = "application/pdf" if fpath.suffix.lower() == ".pdf" else "text/plain"
|
|
685
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
686
|
+
|
|
687
|
+
file_ok = True
|
|
688
|
+
for idx, chunk_text in enumerate(chunks):
|
|
689
|
+
ok = await _kb_ingest_chunk(
|
|
690
|
+
user_id=user_id,
|
|
691
|
+
collection=collection,
|
|
692
|
+
source_path=source_path_str,
|
|
693
|
+
source_hash=file_hash,
|
|
694
|
+
chunk_idx=idx,
|
|
695
|
+
content=chunk_text,
|
|
696
|
+
mime=mime,
|
|
697
|
+
created_at=created_at,
|
|
698
|
+
)
|
|
699
|
+
# A provider that returns False signals a real write failure (None from a
|
|
700
|
+
# legacy/no-op provider is treated as success — unchanged behavior).
|
|
701
|
+
if ok is False:
|
|
702
|
+
file_ok = False
|
|
703
|
+
break
|
|
704
|
+
|
|
705
|
+
if not file_ok:
|
|
706
|
+
# Don't leave the file half-ingested with a current source hash (a re-run
|
|
707
|
+
# would skip it as "unchanged" and never recover the dropped chunks).
|
|
708
|
+
# Remove the partial source so the next ingest retries from scratch.
|
|
709
|
+
try:
|
|
710
|
+
await _kb_remove(
|
|
711
|
+
user_id=user_id,
|
|
712
|
+
collection=collection,
|
|
713
|
+
source=source_path_str,
|
|
714
|
+
)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
logger.debug("partial-ingest cleanup failed for %s: %s", fpath, e)
|
|
717
|
+
counts["failed"] += 1
|
|
718
|
+
continue
|
|
719
|
+
|
|
720
|
+
counts["ingested"] += 1
|
|
721
|
+
counts["n_chunks"] += len(chunks)
|
|
722
|
+
|
|
723
|
+
return counts
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _read_and_hash(path: Path) -> str:
|
|
727
|
+
"""Read file bytes and return their sha256 hex digest (runs in a worker thread)."""
|
|
728
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _resolve_confinement_root(session_id: str, user_id: str) -> Path:
|
|
732
|
+
"""Return the allowed root for path confinement.
|
|
733
|
+
|
|
734
|
+
Server: ``pm().get_workspace_dir(session_id, user_id)``
|
|
735
|
+
CLI / local mode: ``Path.cwd()``.
|
|
736
|
+
|
|
737
|
+
Fail-CLOSED on the server: if the workspace root can't be resolved (``pm()``
|
|
738
|
+
raises), we must NOT silently widen confinement to the process CWD (e.g.
|
|
739
|
+
``/opt/rob`` — app code/config). Only local mode (single-user) accepts the
|
|
740
|
+
CWD fallback; on the server the error propagates so the caller refuses.
|
|
741
|
+
"""
|
|
742
|
+
try:
|
|
743
|
+
from agents.task.path import pm
|
|
744
|
+
path_manager = pm()
|
|
745
|
+
workspace = path_manager.get_workspace_dir(session_id, user_id)
|
|
746
|
+
return workspace.resolve()
|
|
747
|
+
except Exception:
|
|
748
|
+
from agents.task.constants import local_mode_enabled
|
|
749
|
+
if local_mode_enabled():
|
|
750
|
+
return Path.cwd().resolve()
|
|
751
|
+
raise
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# Param models
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class KbIngestParams(BaseModel):
|
|
760
|
+
model_config = ConfigDict(extra="forbid")
|
|
761
|
+
path: str = Field(..., description="File or directory path to ingest into the KB.")
|
|
762
|
+
collection: str = Field("default", description="KB collection name.")
|
|
763
|
+
recursive: bool = Field(True, description="Recurse into subdirectories.")
|
|
764
|
+
globs: Optional[List[str]] = Field(
|
|
765
|
+
None, description="Optional glob patterns to restrict which files are included."
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class KbSearchParams(BaseModel):
|
|
770
|
+
model_config = ConfigDict(extra="forbid")
|
|
771
|
+
query: str = Field(..., description="Search query.")
|
|
772
|
+
collection: str = Field("default", description="KB collection to search.")
|
|
773
|
+
limit: int = Field(8, ge=1, le=50, description="Maximum number of results.")
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
class KbListParams(BaseModel):
|
|
777
|
+
model_config = ConfigDict(extra="forbid")
|
|
778
|
+
collection: Optional[str] = Field(None, description="Filter by collection (omit for all).")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class KbRemoveParams(BaseModel):
|
|
782
|
+
model_config = ConfigDict(extra="forbid")
|
|
783
|
+
collection: str = Field(..., description="KB collection name.")
|
|
784
|
+
source: Optional[str] = Field(None, description="Source path to remove (omit to clear entire collection).")
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# ---------------------------------------------------------------------------
|
|
788
|
+
# KnowledgeTool
|
|
789
|
+
# ---------------------------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class KnowledgeTool(BaseTool):
|
|
793
|
+
"""Agent tool for tenant-scoped knowledge-base ingest and search."""
|
|
794
|
+
|
|
795
|
+
@staticmethod
|
|
796
|
+
def _user(execution_context) -> str:
|
|
797
|
+
return getattr(execution_context, "user_id", None) or "_anonymous_"
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def _session(execution_context) -> str:
|
|
801
|
+
return getattr(execution_context, "session_id", None) or ""
|
|
802
|
+
|
|
803
|
+
@BaseTool.action(
|
|
804
|
+
"Ingest a file or directory into the agent's knowledge base so future kb_search "
|
|
805
|
+
"calls can retrieve it. Skips secrets, binaries, and unchanged files (dedup by hash).",
|
|
806
|
+
param_model=KbIngestParams,
|
|
807
|
+
)
|
|
808
|
+
async def kb_ingest(
|
|
809
|
+
self, params: KbIngestParams, execution_context=None
|
|
810
|
+
) -> ActionResult:
|
|
811
|
+
user_id = self._user(execution_context)
|
|
812
|
+
session_id = self._session(execution_context)
|
|
813
|
+
try:
|
|
814
|
+
result = await kb_ingest(
|
|
815
|
+
params.path,
|
|
816
|
+
collection=params.collection,
|
|
817
|
+
recursive=params.recursive,
|
|
818
|
+
globs=params.globs,
|
|
819
|
+
user_id=user_id,
|
|
820
|
+
session_id=session_id,
|
|
821
|
+
)
|
|
822
|
+
except Exception as e:
|
|
823
|
+
return ActionResult(error=f"KB ingest failed: {e}", include_in_memory=True)
|
|
824
|
+
|
|
825
|
+
if "error" in result:
|
|
826
|
+
return ActionResult(error=result["error"], include_in_memory=True)
|
|
827
|
+
|
|
828
|
+
summary = (
|
|
829
|
+
f"KB ingest complete — ingested={result['ingested']}, "
|
|
830
|
+
f"unchanged={result['unchanged']}, chunks={result['n_chunks']}, "
|
|
831
|
+
f"failed={result.get('failed', 0)}, "
|
|
832
|
+
f"skipped(secret={result['skipped_secret']}, "
|
|
833
|
+
f"binary={result['skipped_binary']}, "
|
|
834
|
+
f"office={result.get('skipped_office', 0)}, "
|
|
835
|
+
f"too_large={result.get('skipped_too_large', 0)})"
|
|
836
|
+
)
|
|
837
|
+
return ActionResult(extracted_content=summary, include_in_memory=True)
|
|
838
|
+
|
|
839
|
+
@BaseTool.action(
|
|
840
|
+
"Search the agent's knowledge base for relevant content.",
|
|
841
|
+
param_model=KbSearchParams,
|
|
842
|
+
)
|
|
843
|
+
async def kb_search(
|
|
844
|
+
self, params: KbSearchParams, execution_context=None
|
|
845
|
+
) -> ActionResult:
|
|
846
|
+
from modules.memory.registry import kb_search as _kb_search
|
|
847
|
+
user_id = self._user(execution_context)
|
|
848
|
+
try:
|
|
849
|
+
result = await _kb_search(
|
|
850
|
+
params.query,
|
|
851
|
+
user_id=user_id,
|
|
852
|
+
collection=params.collection,
|
|
853
|
+
limit=params.limit,
|
|
854
|
+
)
|
|
855
|
+
except Exception as e:
|
|
856
|
+
return ActionResult(error=f"KB search failed: {e}", include_in_memory=True)
|
|
857
|
+
if not result:
|
|
858
|
+
return ActionResult(
|
|
859
|
+
extracted_content="No KB results found.", include_in_memory=True
|
|
860
|
+
)
|
|
861
|
+
return ActionResult(extracted_content=result, include_in_memory=True)
|
|
862
|
+
|
|
863
|
+
@BaseTool.action(
|
|
864
|
+
"List ingested sources in the knowledge base.",
|
|
865
|
+
param_model=KbListParams,
|
|
866
|
+
)
|
|
867
|
+
async def kb_list(
|
|
868
|
+
self, params: KbListParams, execution_context=None
|
|
869
|
+
) -> ActionResult:
|
|
870
|
+
from modules.memory.registry import kb_list_sources as _kb_list_sources
|
|
871
|
+
user_id = self._user(execution_context)
|
|
872
|
+
try:
|
|
873
|
+
sources = await _kb_list_sources(
|
|
874
|
+
user_id=user_id, collection=params.collection
|
|
875
|
+
)
|
|
876
|
+
except Exception as e:
|
|
877
|
+
return ActionResult(error=f"KB list failed: {e}", include_in_memory=True)
|
|
878
|
+
if not sources:
|
|
879
|
+
return ActionResult(
|
|
880
|
+
extracted_content="No sources ingested.", include_in_memory=True
|
|
881
|
+
)
|
|
882
|
+
lines = [f"- {s}" for s in sources]
|
|
883
|
+
return ActionResult(
|
|
884
|
+
extracted_content="KB sources:\n" + "\n".join(lines),
|
|
885
|
+
include_in_memory=True,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
@BaseTool.action(
|
|
889
|
+
"Remove a source (or entire collection) from the knowledge base.",
|
|
890
|
+
param_model=KbRemoveParams,
|
|
891
|
+
)
|
|
892
|
+
async def kb_remove(
|
|
893
|
+
self, params: KbRemoveParams, execution_context=None
|
|
894
|
+
) -> ActionResult:
|
|
895
|
+
from modules.memory.registry import kb_remove as _kb_remove
|
|
896
|
+
user_id = self._user(execution_context)
|
|
897
|
+
try:
|
|
898
|
+
removed = await _kb_remove(
|
|
899
|
+
user_id=user_id,
|
|
900
|
+
collection=params.collection,
|
|
901
|
+
source=params.source,
|
|
902
|
+
)
|
|
903
|
+
except Exception as e:
|
|
904
|
+
return ActionResult(error=f"KB remove failed: {e}", include_in_memory=True)
|
|
905
|
+
return ActionResult(
|
|
906
|
+
extracted_content=f"Removed {removed} chunk(s) from KB.",
|
|
907
|
+
include_in_memory=True,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
# ---------------------------------------------------------------------------
|
|
912
|
+
# Registration
|
|
913
|
+
# ---------------------------------------------------------------------------
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def kb_enabled() -> bool:
|
|
917
|
+
from agents.task.constants import AutonomyConfig
|
|
918
|
+
return AutonomyConfig.kb_enabled()
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def register_knowledge_tool(force: bool = False) -> bool:
|
|
922
|
+
"""Register the 'knowledge' descriptor + class IFF KB_ENABLED (or forced).
|
|
923
|
+
|
|
924
|
+
Delegates to ``register_optional_tool``. No-op when KB is off, so default
|
|
925
|
+
deploys are unaffected. ``knowledge`` is never in the default ``tool_ids``
|
|
926
|
+
— agents opt in via ``tool_ids=['knowledge']``.
|
|
927
|
+
"""
|
|
928
|
+
from tools.descriptors import (
|
|
929
|
+
ToolDescriptor,
|
|
930
|
+
ToolCategory,
|
|
931
|
+
register_optional_tool,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
return register_optional_tool(
|
|
935
|
+
"knowledge",
|
|
936
|
+
KnowledgeTool,
|
|
937
|
+
ToolDescriptor(
|
|
938
|
+
name="knowledge",
|
|
939
|
+
description=(
|
|
940
|
+
"Ingest/search a tenant-scoped knowledge base "
|
|
941
|
+
"(kb_ingest/kb_search/kb_list/kb_remove)"
|
|
942
|
+
),
|
|
943
|
+
category=ToolCategory.INTEGRATION,
|
|
944
|
+
required_config=[],
|
|
945
|
+
init_priority=80,
|
|
946
|
+
is_optional=True,
|
|
947
|
+
),
|
|
948
|
+
kb_enabled,
|
|
949
|
+
force=force,
|
|
950
|
+
)
|