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
tools/filesystem_pdf.py
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"""PDF extraction mixin for FileSystem tool.
|
|
2
|
+
|
|
3
|
+
This module contains all PDF-related extraction logic extracted from filesystem.py.
|
|
4
|
+
The PdfExtractionMixin is composed into FileSystem via multiple-inheritance.
|
|
5
|
+
|
|
6
|
+
IMPORTANT: Do NOT add `from __future__ import annotations` to this file.
|
|
7
|
+
The registry inspects first-param annotations on @action closures via issubclass()
|
|
8
|
+
and stringized annotations break that routing.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
from typing import Dict, Any
|
|
17
|
+
|
|
18
|
+
import pypdf # type: ignore
|
|
19
|
+
from pypdf.errors import PdfReadError # type: ignore
|
|
20
|
+
|
|
21
|
+
from core.exceptions import ServiceError
|
|
22
|
+
from utils.time_utils import get_current_timestamp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PdfExtractionMixin:
|
|
26
|
+
"""Mixin providing PDF extraction capabilities for FileSystem."""
|
|
27
|
+
|
|
28
|
+
async def _process_pdf(self, content) -> Dict[str, Any]:
|
|
29
|
+
"""Process PDF content with enhanced extraction capabilities and validation."""
|
|
30
|
+
from io import BytesIO
|
|
31
|
+
import pypdf #type: ignore
|
|
32
|
+
from pypdf.errors import PdfReadError #type: ignore
|
|
33
|
+
import re
|
|
34
|
+
|
|
35
|
+
# Ensure content is in bytes
|
|
36
|
+
if isinstance(content, str):
|
|
37
|
+
try:
|
|
38
|
+
content = content.encode('utf-8')
|
|
39
|
+
except UnicodeEncodeError as e:
|
|
40
|
+
raise ServiceError(f"Failed to encode PDF content: {str(e)}")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Extract metadata early before any other processing
|
|
44
|
+
metadata = {}
|
|
45
|
+
title = "Untitled PDF Document"
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Create a new dedicated BytesIO for metadata extraction to avoid issues
|
|
49
|
+
with BytesIO(content) as metadata_file:
|
|
50
|
+
try:
|
|
51
|
+
# Create a PDF reader specifically for metadata extraction
|
|
52
|
+
metadata_reader = pypdf.PdfReader(metadata_file, strict=False)
|
|
53
|
+
|
|
54
|
+
# Immediately extract and copy metadata to avoid closed file issues
|
|
55
|
+
if hasattr(metadata_reader, 'metadata') and metadata_reader.metadata:
|
|
56
|
+
# Get a shallow copy of the metadata dictionary
|
|
57
|
+
metadata_dict = dict(metadata_reader.metadata or {})
|
|
58
|
+
|
|
59
|
+
# Copy values to a new dictionary to avoid references to PDF objects
|
|
60
|
+
for key, value in metadata_dict.items():
|
|
61
|
+
if isinstance(key, str):
|
|
62
|
+
clean_key = key.lstrip('/')
|
|
63
|
+
metadata[clean_key] = str(value) if value is not None else ""
|
|
64
|
+
|
|
65
|
+
# Look for title specifically
|
|
66
|
+
if clean_key.lower() == 'title' and value:
|
|
67
|
+
title = str(value)
|
|
68
|
+
except Exception as meta_err:
|
|
69
|
+
self.logger.warning(f"Failed initial metadata extraction: {str(meta_err)}")
|
|
70
|
+
except Exception as meta_outer_err:
|
|
71
|
+
self.logger.warning(f"Error during dedicated metadata extraction: {str(meta_outer_err)}")
|
|
72
|
+
|
|
73
|
+
# More lenient PDF header check
|
|
74
|
+
if not (content.startswith(b'%PDF-') or b'%PDF-' in content[:1024]):
|
|
75
|
+
self.logger.warning("PDF header not found in expected location, attempting recovery...")
|
|
76
|
+
|
|
77
|
+
# Try different PDF recovery methods
|
|
78
|
+
recovery_methods = [
|
|
79
|
+
# Method 1: Standard reading
|
|
80
|
+
lambda f: pypdf.PdfReader(f, strict=False),
|
|
81
|
+
|
|
82
|
+
# Method 2: Try with stream parsing and error recovery
|
|
83
|
+
lambda f: self._read_pdf_with_recovery(f),
|
|
84
|
+
|
|
85
|
+
# Method 3: Most permissive reading
|
|
86
|
+
lambda f: self._read_pdf_permissive(f),
|
|
87
|
+
|
|
88
|
+
# Method 4: New advanced PDF recovery
|
|
89
|
+
lambda f: self._read_pdf_advanced_recovery(f)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
last_error = None
|
|
93
|
+
pdf_reader = None
|
|
94
|
+
|
|
95
|
+
# Try each recovery method
|
|
96
|
+
for method in recovery_methods:
|
|
97
|
+
try:
|
|
98
|
+
with BytesIO(content) as pdf_file:
|
|
99
|
+
pdf_file.seek(0)
|
|
100
|
+
pdf_reader = method(pdf_file)
|
|
101
|
+
|
|
102
|
+
# Test page access more thoroughly
|
|
103
|
+
valid_pages = 0
|
|
104
|
+
for i in range(min(3, len(pdf_reader.pages))):
|
|
105
|
+
try:
|
|
106
|
+
page = pdf_reader.pages[i]
|
|
107
|
+
# If we can get any text, consider it valid
|
|
108
|
+
text = self._extract_page_text_with_fallbacks(page)
|
|
109
|
+
if text and text.strip():
|
|
110
|
+
valid_pages += 1
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.logger.debug(f"Error accessing page {i}: {str(e)}")
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if valid_pages > 0:
|
|
116
|
+
break # Found a working method
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
last_error = e
|
|
120
|
+
self.logger.debug(f"PDF recovery method failed: {str(e)}")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if pdf_reader is None:
|
|
124
|
+
# Try the raw extraction method as last resort
|
|
125
|
+
raw_text = self._extract_raw_pdf_text(content)
|
|
126
|
+
if raw_text and len(raw_text) > 100 and self._is_readable_text(raw_text):
|
|
127
|
+
return {
|
|
128
|
+
'type': 'pdf',
|
|
129
|
+
'content': raw_text,
|
|
130
|
+
'metadata': {
|
|
131
|
+
'title': 'Extracted PDF Content',
|
|
132
|
+
'pages': 1,
|
|
133
|
+
'extraction_method': 'raw_bytes',
|
|
134
|
+
'extraction_quality': 'low'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# If we still don't have content, try OCR if available
|
|
139
|
+
try:
|
|
140
|
+
if self.container and self.container.has_service('ocr_service'):
|
|
141
|
+
self.logger.info("Trying OCR service for image-based PDF")
|
|
142
|
+
ocr_service = self.container.get_service('ocr_service')
|
|
143
|
+
|
|
144
|
+
# Save content to temporary file for OCR processing
|
|
145
|
+
temp_path = None
|
|
146
|
+
try:
|
|
147
|
+
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as temp_file:
|
|
148
|
+
temp_path = temp_file.name
|
|
149
|
+
temp_file.write(content)
|
|
150
|
+
|
|
151
|
+
# Process with OCR
|
|
152
|
+
ocr_result = await ocr_service.extract_text_from_pdf(temp_path)
|
|
153
|
+
|
|
154
|
+
# If OCR was successful, return its result
|
|
155
|
+
if ocr_result and ocr_result.get('content'):
|
|
156
|
+
self.logger.info("Successfully extracted text using OCR service")
|
|
157
|
+
return ocr_result
|
|
158
|
+
except Exception as ocr_error:
|
|
159
|
+
self.logger.warning(f"OCR processing failed: {str(ocr_error)}")
|
|
160
|
+
finally:
|
|
161
|
+
# Ensure temp file is removed
|
|
162
|
+
if temp_path and os.path.exists(temp_path):
|
|
163
|
+
try:
|
|
164
|
+
os.unlink(temp_path)
|
|
165
|
+
except Exception as cleanup_error:
|
|
166
|
+
self.logger.warning(f"Failed to clean up temp file: {str(cleanup_error)}")
|
|
167
|
+
except Exception as ocr_setup_error:
|
|
168
|
+
self.logger.warning(f"Failed to setup OCR processing: {str(ocr_setup_error)}")
|
|
169
|
+
|
|
170
|
+
raise ServiceError(f"Failed to read PDF after all recovery attempts: {str(last_error)}")
|
|
171
|
+
|
|
172
|
+
# Extract text with multiple fallback methods
|
|
173
|
+
pages = []
|
|
174
|
+
full_text = []
|
|
175
|
+
total_text_length = 0
|
|
176
|
+
successful_pages = 0
|
|
177
|
+
# We already have metadata from the beginning of the method
|
|
178
|
+
|
|
179
|
+
# Get page count with fallback methods
|
|
180
|
+
try:
|
|
181
|
+
page_count = len(pdf_reader.pages)
|
|
182
|
+
except Exception as page_count_error:
|
|
183
|
+
try:
|
|
184
|
+
# Try direct access to pages array
|
|
185
|
+
page_count = len(pdf_reader._get_num_pages())
|
|
186
|
+
except Exception:
|
|
187
|
+
try:
|
|
188
|
+
# Try to access pages dict
|
|
189
|
+
page_count = len(pdf_reader._pages_cache)
|
|
190
|
+
except Exception:
|
|
191
|
+
# Last resort: binary search for valid pages
|
|
192
|
+
page_count = self._find_max_valid_page(pdf_reader)
|
|
193
|
+
|
|
194
|
+
if page_count == 0:
|
|
195
|
+
self.logger.warning(f"Could not determine PDF page count: {str(page_count_error)}")
|
|
196
|
+
page_count = 1 # Assume at least one page
|
|
197
|
+
|
|
198
|
+
# Process each page with enhanced extraction
|
|
199
|
+
for i in range(page_count):
|
|
200
|
+
try:
|
|
201
|
+
page_text = ""
|
|
202
|
+
try:
|
|
203
|
+
page = pdf_reader.pages[i]
|
|
204
|
+
page_text = self._extract_page_text_with_fallbacks(page)
|
|
205
|
+
except Exception as page_error:
|
|
206
|
+
self.logger.debug(f"Error extracting text from page {i+1}: {str(page_error)}")
|
|
207
|
+
page_text = f"[Error extracting page {i+1}]"
|
|
208
|
+
|
|
209
|
+
# Apply post-processing to improve text quality
|
|
210
|
+
page_text = self._post_process_pdf_text(page_text)
|
|
211
|
+
|
|
212
|
+
# Skip pages with no meaningful content
|
|
213
|
+
if not self._is_meaningful_content(page_text):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Skip pages that appear to contain binary data
|
|
217
|
+
if self._contains_binary_data(page_text):
|
|
218
|
+
self.logger.warning(f"Page {i+1} appears to contain binary data, skipping")
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# Add to page collection
|
|
222
|
+
pages.append({
|
|
223
|
+
'number': i + 1,
|
|
224
|
+
'content': page_text
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
full_text.append(page_text)
|
|
228
|
+
total_text_length += len(page_text)
|
|
229
|
+
successful_pages += 1
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.logger.warning(f"Error processing page {i+1}: {str(e)}")
|
|
233
|
+
|
|
234
|
+
# Check if we have any usable content
|
|
235
|
+
combined_text = "\n\n".join(full_text)
|
|
236
|
+
|
|
237
|
+
if not combined_text or len(combined_text.strip()) < 50:
|
|
238
|
+
# Try fallback methods if regular extraction failed
|
|
239
|
+
raw_text = self._extract_raw_pdf_text(content)
|
|
240
|
+
if raw_text and len(raw_text) > 100:
|
|
241
|
+
combined_text = raw_text
|
|
242
|
+
successful_pages = 1
|
|
243
|
+
else:
|
|
244
|
+
# If we still don't have good content, try OCR if available
|
|
245
|
+
try:
|
|
246
|
+
if self.container and self.container.has_service('ocr_service'):
|
|
247
|
+
self.logger.info("Trying OCR service for image-based PDF")
|
|
248
|
+
ocr_service = self.container.get_service('ocr_service')
|
|
249
|
+
|
|
250
|
+
# Save content to temporary file for OCR processing
|
|
251
|
+
temp_path = None
|
|
252
|
+
try:
|
|
253
|
+
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as temp_file:
|
|
254
|
+
temp_path = temp_file.name
|
|
255
|
+
temp_file.write(content)
|
|
256
|
+
|
|
257
|
+
# Process with OCR
|
|
258
|
+
ocr_result = await ocr_service.extract_text_from_pdf(temp_path)
|
|
259
|
+
|
|
260
|
+
# If OCR was successful, return its result
|
|
261
|
+
if ocr_result and ocr_result.get('content'):
|
|
262
|
+
self.logger.info("Successfully extracted text using OCR service")
|
|
263
|
+
return ocr_result
|
|
264
|
+
except Exception as ocr_error:
|
|
265
|
+
self.logger.warning(f"OCR processing failed: {str(ocr_error)}")
|
|
266
|
+
finally:
|
|
267
|
+
# Ensure temp file is removed
|
|
268
|
+
if temp_path and os.path.exists(temp_path):
|
|
269
|
+
try:
|
|
270
|
+
os.unlink(temp_path)
|
|
271
|
+
except Exception as cleanup_error:
|
|
272
|
+
self.logger.warning(f"Failed to clean up temp file: {str(cleanup_error)}")
|
|
273
|
+
except Exception as ocr_setup_error:
|
|
274
|
+
self.logger.warning(f"Failed to setup OCR processing: {str(ocr_setup_error)}")
|
|
275
|
+
|
|
276
|
+
# If OCR failed or is not available, use a placeholder
|
|
277
|
+
combined_text = "[PDF CONTENT REQUIRES OCR PROCESSING - TEXT EXTRACTION FAILED]"
|
|
278
|
+
|
|
279
|
+
# Validate extracted content
|
|
280
|
+
if self._contains_binary_data(combined_text):
|
|
281
|
+
# If it looks like binary, try more aggressive cleaning
|
|
282
|
+
combined_text = self._aggressive_binary_cleanup(combined_text)
|
|
283
|
+
|
|
284
|
+
# Final validation
|
|
285
|
+
if not self._is_readable_text(combined_text):
|
|
286
|
+
self.logger.warning("Extracted PDF content does not appear to be readable text")
|
|
287
|
+
# Add indicator for clients to know content may need further processing
|
|
288
|
+
combined_text = "[WARNING: PDF CONTENT MAY BE IMAGE-BASED OR CORRUPTED]\n\n" + combined_text
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
'type': 'pdf',
|
|
292
|
+
'content': combined_text,
|
|
293
|
+
'title': title,
|
|
294
|
+
'structure': {
|
|
295
|
+
'pages': pages,
|
|
296
|
+
'total_pages': page_count
|
|
297
|
+
},
|
|
298
|
+
'metadata': {
|
|
299
|
+
**metadata,
|
|
300
|
+
'processor_version': '2.0',
|
|
301
|
+
'processing_time': get_current_timestamp(),
|
|
302
|
+
'original_size': len(content),
|
|
303
|
+
'processed_size': total_text_length,
|
|
304
|
+
'successful_pages': successful_pages,
|
|
305
|
+
'total_pages': page_count,
|
|
306
|
+
'extraction_quality': 'high' if successful_pages == page_count else 'medium' if successful_pages > 0 else 'low'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
except ServiceError:
|
|
311
|
+
raise
|
|
312
|
+
except Exception as e:
|
|
313
|
+
self.logger.error(f"Error processing PDF: {str(e)}", exc_info=True)
|
|
314
|
+
raise ServiceError(f"Failed to process PDF: {str(e)}")
|
|
315
|
+
|
|
316
|
+
def _read_pdf_with_recovery(self, file_obj) -> pypdf.PdfReader:
|
|
317
|
+
"""Read PDF with additional recovery options."""
|
|
318
|
+
try:
|
|
319
|
+
# Create a copy of the file content to avoid "I/O operation on closed file" errors
|
|
320
|
+
file_obj.seek(0)
|
|
321
|
+
content = file_obj.read()
|
|
322
|
+
file_copy = BytesIO(content)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
reader = pypdf.PdfReader(file_copy, strict=False)
|
|
326
|
+
# Force load document info to catch early failures
|
|
327
|
+
_ = reader.metadata
|
|
328
|
+
return reader
|
|
329
|
+
except Exception as e:
|
|
330
|
+
# Try to repair common issues
|
|
331
|
+
repair_copy = BytesIO(content)
|
|
332
|
+
|
|
333
|
+
# Try to fix common PDF corruptions
|
|
334
|
+
if b'startxref' not in content:
|
|
335
|
+
# Add missing startxref
|
|
336
|
+
repaired_content = content + b'\nstartxref\n0\n%%EOF'
|
|
337
|
+
return pypdf.PdfReader(BytesIO(repaired_content), strict=False)
|
|
338
|
+
|
|
339
|
+
return pypdf.PdfReader(repair_copy, strict=False)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
self.logger.warning(f"Recovery failed: {str(e)}")
|
|
342
|
+
raise e
|
|
343
|
+
|
|
344
|
+
def _read_pdf_permissive(self, file_obj) -> pypdf.PdfReader:
|
|
345
|
+
"""Most permissive PDF reading attempt."""
|
|
346
|
+
try:
|
|
347
|
+
# Create a copy of the file content
|
|
348
|
+
file_obj.seek(0)
|
|
349
|
+
content = file_obj.read()
|
|
350
|
+
file_copy = BytesIO(content)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Try to read with minimal validation
|
|
354
|
+
reader = pypdf.PdfReader(file_copy)
|
|
355
|
+
reader.strict = False
|
|
356
|
+
return reader
|
|
357
|
+
except Exception as e:
|
|
358
|
+
# Last resort: try to extract any readable content
|
|
359
|
+
if b'obj' in content and b'endobj' in content:
|
|
360
|
+
# Create minimal PDF structure
|
|
361
|
+
pdf_content = b'%PDF-1.4\n' + content + b'\nstartxref\n0\n%%EOF'
|
|
362
|
+
return pypdf.PdfReader(BytesIO(pdf_content), strict=False)
|
|
363
|
+
raise ServiceError("Could not recover PDF structure")
|
|
364
|
+
except ServiceError:
|
|
365
|
+
raise
|
|
366
|
+
except Exception as e:
|
|
367
|
+
self.logger.warning(f"Permissive reading failed: {str(e)}")
|
|
368
|
+
raise e
|
|
369
|
+
|
|
370
|
+
def _read_pdf_advanced_recovery(self, file_obj) -> pypdf.PdfReader:
|
|
371
|
+
"""Advanced PDF recovery for damaged files."""
|
|
372
|
+
try:
|
|
373
|
+
# Create a copy of the file content
|
|
374
|
+
file_obj.seek(0)
|
|
375
|
+
content = file_obj.read()
|
|
376
|
+
|
|
377
|
+
# First try normal reading with error handling
|
|
378
|
+
try:
|
|
379
|
+
file_copy = BytesIO(content)
|
|
380
|
+
return pypdf.PdfReader(file_copy, strict=False)
|
|
381
|
+
except Exception as normal_error:
|
|
382
|
+
self.logger.debug(f"Normal reading failed: {str(normal_error)}")
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# Try to repair the file
|
|
386
|
+
# Add PDF header if missing
|
|
387
|
+
if not content.startswith(b'%PDF-'):
|
|
388
|
+
content = b'%PDF-1.4\n' + content
|
|
389
|
+
|
|
390
|
+
# Add EOF marker if missing
|
|
391
|
+
if not content.endswith(b'%%EOF'):
|
|
392
|
+
content = content + b'\n%%EOF'
|
|
393
|
+
|
|
394
|
+
# Check for and fix xref table
|
|
395
|
+
if b'xref' not in content:
|
|
396
|
+
content = content + b'\nxref\n0 1\n0000000000 65535 f\ntrailer\n<<>>\nstartxref\n0\n%%EOF'
|
|
397
|
+
|
|
398
|
+
# Try to create reader from repaired content
|
|
399
|
+
repair_copy = BytesIO(content)
|
|
400
|
+
return pypdf.PdfReader(repair_copy, strict=False)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
self.logger.warning(f"Advanced PDF recovery failed: {str(e)}")
|
|
404
|
+
raise ServiceError(f"Advanced PDF recovery failed: {str(e)}")
|
|
405
|
+
|
|
406
|
+
def _extract_page_text_with_fallbacks(self, page) -> str:
|
|
407
|
+
"""Extract text using multiple fallback methods with enhanced capabilities."""
|
|
408
|
+
all_extracted_text = []
|
|
409
|
+
methods = [
|
|
410
|
+
# Standard extraction
|
|
411
|
+
lambda p: p.extract_text(),
|
|
412
|
+
|
|
413
|
+
# Try getting text using textual objects
|
|
414
|
+
lambda p: self._extract_textual_objects(p),
|
|
415
|
+
|
|
416
|
+
# Raw extraction
|
|
417
|
+
lambda p: p.get_contents() if hasattr(p, 'get_contents') else b'',
|
|
418
|
+
|
|
419
|
+
# Extract from raw page object
|
|
420
|
+
lambda p: self._extract_from_raw_page(p),
|
|
421
|
+
|
|
422
|
+
# OCR-like extraction (if available)
|
|
423
|
+
lambda p: self._extract_text_from_images(p)
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
for method in methods:
|
|
427
|
+
try:
|
|
428
|
+
result = method(page)
|
|
429
|
+
if isinstance(result, bytes):
|
|
430
|
+
result = result.decode('utf-8', errors='ignore')
|
|
431
|
+
|
|
432
|
+
if result and result.strip():
|
|
433
|
+
all_extracted_text.append(result)
|
|
434
|
+
except Exception as e:
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
# If we have multiple extraction results, choose the best one
|
|
438
|
+
if len(all_extracted_text) > 1:
|
|
439
|
+
# Find the extraction with the most alphanumeric characters
|
|
440
|
+
best_text = ""
|
|
441
|
+
max_alphanumeric = 0
|
|
442
|
+
|
|
443
|
+
for text in all_extracted_text:
|
|
444
|
+
alphanumeric_count = sum(c.isalnum() for c in text)
|
|
445
|
+
if alphanumeric_count > max_alphanumeric:
|
|
446
|
+
max_alphanumeric = alphanumeric_count
|
|
447
|
+
best_text = text
|
|
448
|
+
|
|
449
|
+
if best_text:
|
|
450
|
+
return best_text
|
|
451
|
+
|
|
452
|
+
# Fall back to first non-empty result or empty string
|
|
453
|
+
return next((text for text in all_extracted_text if text), '')
|
|
454
|
+
|
|
455
|
+
def _extract_textual_objects(self, page) -> str:
|
|
456
|
+
"""Extract text from PDF page text objects."""
|
|
457
|
+
try:
|
|
458
|
+
if not hasattr(page, '/Contents'):
|
|
459
|
+
return ""
|
|
460
|
+
|
|
461
|
+
raw_content = ""
|
|
462
|
+
if hasattr(page, 'get_contents'):
|
|
463
|
+
raw_content = page.get_contents()
|
|
464
|
+
elif hasattr(page, '_contents'):
|
|
465
|
+
raw_content = page._contents
|
|
466
|
+
|
|
467
|
+
if not raw_content:
|
|
468
|
+
return ""
|
|
469
|
+
|
|
470
|
+
# Convert to string if needed
|
|
471
|
+
if isinstance(raw_content, bytes):
|
|
472
|
+
raw_content = raw_content.decode('utf-8', errors='ignore')
|
|
473
|
+
|
|
474
|
+
# Extract text between BT and ET markers (text objects)
|
|
475
|
+
text_objects = []
|
|
476
|
+
start = 0
|
|
477
|
+
while True:
|
|
478
|
+
bt_pos = raw_content.find('BT', start)
|
|
479
|
+
if bt_pos == -1:
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
et_pos = raw_content.find('ET', bt_pos + 2)
|
|
483
|
+
if et_pos == -1:
|
|
484
|
+
break
|
|
485
|
+
|
|
486
|
+
text_objects.append(raw_content[bt_pos:et_pos+2])
|
|
487
|
+
start = et_pos + 2
|
|
488
|
+
|
|
489
|
+
# Extract text from text objects
|
|
490
|
+
extracted_text = []
|
|
491
|
+
for obj in text_objects:
|
|
492
|
+
# Look for text in parentheses
|
|
493
|
+
in_text = False
|
|
494
|
+
text_buffer = []
|
|
495
|
+
current_text = ""
|
|
496
|
+
|
|
497
|
+
for i, char in enumerate(obj):
|
|
498
|
+
if char == '(' and (i == 0 or obj[i-1] != '\\'):
|
|
499
|
+
in_text = True
|
|
500
|
+
current_text = ""
|
|
501
|
+
elif char == ')' and (i == 0 or obj[i-1] != '\\') and in_text:
|
|
502
|
+
in_text = False
|
|
503
|
+
text_buffer.append(current_text)
|
|
504
|
+
elif in_text:
|
|
505
|
+
current_text += char
|
|
506
|
+
|
|
507
|
+
if text_buffer:
|
|
508
|
+
extracted_text.append(" ".join(text_buffer))
|
|
509
|
+
|
|
510
|
+
return "\n".join(extracted_text)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
return ""
|
|
513
|
+
|
|
514
|
+
def _clean_pdf_text(self, text: str) -> str:
|
|
515
|
+
"""Enhanced cleaning of extracted PDF text."""
|
|
516
|
+
if not text:
|
|
517
|
+
return ''
|
|
518
|
+
|
|
519
|
+
# Remove control characters
|
|
520
|
+
text = ''.join(char for char in text if ord(char) >= 32 or char in '\n\t')
|
|
521
|
+
|
|
522
|
+
# Fix common PDF extraction issues
|
|
523
|
+
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text) # Add space between words
|
|
524
|
+
text = re.sub(r'(\w)([\(\)\[\]\{\}])', r'\1 \2', text) # Add space before brackets
|
|
525
|
+
text = re.sub(r'([\(\)\[\]\{\}])(\w)', r'\1 \2', text) # Add space after brackets
|
|
526
|
+
|
|
527
|
+
# Fix spacing issues
|
|
528
|
+
text = re.sub(r'\s+', ' ', text) # Normalize whitespace
|
|
529
|
+
text = re.sub(r'[^\S\n]+', ' ', text) # Normalize spaces but keep newlines
|
|
530
|
+
text = re.sub(r'\n{3,}', '\n\n', text) # Limit consecutive newlines
|
|
531
|
+
|
|
532
|
+
# Fix hyphenated words at line breaks
|
|
533
|
+
text = re.sub(r'(\w)-\s*\n\s*(\w)', r'\1\2', text)
|
|
534
|
+
|
|
535
|
+
# Remove isolated punctuation
|
|
536
|
+
text = re.sub(r'\s+([,.;:])\s+', r'\1 ', text)
|
|
537
|
+
|
|
538
|
+
return text.strip()
|
|
539
|
+
|
|
540
|
+
def _post_process_pdf_text(self, text: str) -> str:
|
|
541
|
+
"""Apply post-processing to improve PDF text quality."""
|
|
542
|
+
if not text:
|
|
543
|
+
return ''
|
|
544
|
+
|
|
545
|
+
# First apply standard cleaning
|
|
546
|
+
text = self._clean_pdf_text(text)
|
|
547
|
+
|
|
548
|
+
# Enhanced processing
|
|
549
|
+
# Fix unicode normalization issues
|
|
550
|
+
import unicodedata
|
|
551
|
+
text = unicodedata.normalize('NFKC', text)
|
|
552
|
+
|
|
553
|
+
# Fix common PDF-specific issues
|
|
554
|
+
# 1. Remove form field placeholders
|
|
555
|
+
text = re.sub(r'\[+\s*\]+', '', text)
|
|
556
|
+
|
|
557
|
+
# 2. Fix mis-encoded characters
|
|
558
|
+
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
|
|
559
|
+
|
|
560
|
+
# 3. Fix duplicated punctuation
|
|
561
|
+
text = re.sub(r'([,.!?;:])\1+', r'\1', text)
|
|
562
|
+
|
|
563
|
+
# 4. Remove redundant spaces around newlines
|
|
564
|
+
text = re.sub(r'\s*\n\s*', '\n', text)
|
|
565
|
+
|
|
566
|
+
# 5. Try to detect and replace common character encoding issues
|
|
567
|
+
replacements = {
|
|
568
|
+
'â': "'", '’': "'", '“': '"', 'â€': '"',
|
|
569
|
+
'â€"': '-', 'â€"': '--', '…': '...',
|
|
570
|
+
'é': 'é', 'è': 'è', 'Ã': 'à',
|
|
571
|
+
'Â': '', '°': '°'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for wrong, right in replacements.items():
|
|
575
|
+
text = text.replace(wrong, right)
|
|
576
|
+
|
|
577
|
+
return text.strip()
|
|
578
|
+
|
|
579
|
+
def _is_meaningful_content(self, text: str) -> bool:
|
|
580
|
+
"""Check if text contains meaningful content."""
|
|
581
|
+
if not text or len(text.strip()) < 10:
|
|
582
|
+
return False
|
|
583
|
+
|
|
584
|
+
# Check if the text contains enough alphanumeric characters
|
|
585
|
+
alphanumeric_ratio = sum(c.isalnum() for c in text) / max(1, len(text))
|
|
586
|
+
if alphanumeric_ratio < 0.1: # Less than 10% alphanumeric characters
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
# Check for common placeholder patterns
|
|
590
|
+
placeholders = ['[Empty]', '[blank]', '[N/A]', '[?]', '[...]']
|
|
591
|
+
if any(p in text for p in placeholders):
|
|
592
|
+
return False
|
|
593
|
+
|
|
594
|
+
return True
|
|
595
|
+
|
|
596
|
+
def _contains_binary_data(self, text: str) -> bool:
|
|
597
|
+
"""Check if text appears to contain binary data."""
|
|
598
|
+
if not text:
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
# Check for high concentration of unusual characters
|
|
602
|
+
unusual_char_count = sum(1 for c in text if ord(c) > 127 or ord(c) < 32)
|
|
603
|
+
unusual_ratio = unusual_char_count / max(1, len(text))
|
|
604
|
+
|
|
605
|
+
# Binary data typically has many unusual characters - use more lenient threshold for PDFs
|
|
606
|
+
if unusual_ratio > 0.4: # Increased from 0.3 to 0.4 to be more lenient
|
|
607
|
+
return True
|
|
608
|
+
|
|
609
|
+
# Check for PDF binary stream markers
|
|
610
|
+
binary_markers = ['stream', 'endstream', 'obj', 'endobj', '/Filter', '/Length']
|
|
611
|
+
marker_count = sum(text.count(marker) for marker in binary_markers)
|
|
612
|
+
|
|
613
|
+
# PDF binary content often contains these markers - increased threshold
|
|
614
|
+
if marker_count > 10 and unusual_ratio > 0.2: # Increased from 5 to 10 and 0.1 to 0.2
|
|
615
|
+
return True
|
|
616
|
+
|
|
617
|
+
# Look for long sequences of random-looking characters
|
|
618
|
+
random_sequences = re.findall(r'[A-Za-z0-9+/=]{20,}', text)
|
|
619
|
+
if random_sequences and sum(len(seq) for seq in random_sequences) / len(text) > 0.3: # Increased from 0.2 to 0.3
|
|
620
|
+
return True
|
|
621
|
+
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
def _is_readable_text(self, text: str) -> bool:
|
|
625
|
+
"""Check if text appears to be human-readable."""
|
|
626
|
+
if not text or len(text) < 30: # Reduced from 50 to 30 characters minimum
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
# Check for reasonable distribution of characters
|
|
630
|
+
char_counts = {}
|
|
631
|
+
for char in text:
|
|
632
|
+
char_counts[char] = char_counts.get(char, 0) + 1
|
|
633
|
+
|
|
634
|
+
# Human text typically uses a variety of characters - reduced minimum
|
|
635
|
+
if len(char_counts) < 15: # Reduced from 20 to 15
|
|
636
|
+
return False
|
|
637
|
+
|
|
638
|
+
# Check for reasonable word formation
|
|
639
|
+
words = re.findall(r'\b[a-zA-Z]{1,20}\b', text)
|
|
640
|
+
|
|
641
|
+
# Human text has a reasonable number of words - reduced minimum
|
|
642
|
+
if len(words) < 8: # Reduced from 10 to 8
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
# Check distribution of word lengths
|
|
646
|
+
word_lengths = [len(word) for word in words]
|
|
647
|
+
if word_lengths:
|
|
648
|
+
avg_word_length = sum(word_lengths) / len(word_lengths)
|
|
649
|
+
# Extremely long average word length suggests non-text content
|
|
650
|
+
if avg_word_length > 15:
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
# Check for reasonable whitespace - modified ratio
|
|
654
|
+
whitespace_ratio = text.count(' ') / max(1, len(text))
|
|
655
|
+
if whitespace_ratio < 0.03 or whitespace_ratio > 0.6: # Reduced from 0.05 to 0.03, increased from 0.5 to 0.6
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
# If we have a reasonable number of words or paragraphs, consider it readable
|
|
659
|
+
# even if it doesn't strictly meet our other criteria (helps with image-based PDFs)
|
|
660
|
+
if len(words) > 30 or len(text.split('\n\n')) > 5:
|
|
661
|
+
return True
|
|
662
|
+
|
|
663
|
+
return True # More permissive - assume text is readable unless proven otherwise
|
|
664
|
+
|
|
665
|
+
def _aggressive_binary_cleanup(self, text: str) -> str:
|
|
666
|
+
"""Aggressively clean binary-looking content to extract readable text."""
|
|
667
|
+
if not text:
|
|
668
|
+
return ""
|
|
669
|
+
|
|
670
|
+
# First pass: remove all non-printable characters
|
|
671
|
+
text = re.sub(r'[^\x20-\x7E\n\t]', '', text)
|
|
672
|
+
|
|
673
|
+
# Second pass: remove PDF binary markers and their content
|
|
674
|
+
binary_sections = [
|
|
675
|
+
(r'stream.*?endstream', ''),
|
|
676
|
+
(r'obj.*?endobj', ''),
|
|
677
|
+
(r'/Filter.*?/Length \d+', ''),
|
|
678
|
+
(r'<<.*?>>', ''),
|
|
679
|
+
(r'[0-9]+ [0-9]+ obj.*?endobj', ''),
|
|
680
|
+
(r'xref.*?trailer', ''),
|
|
681
|
+
(r'startxref.*?%%EOF', '')
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
for pattern, replacement in binary_sections:
|
|
685
|
+
text = re.sub(pattern, replacement, text, flags=re.DOTALL)
|
|
686
|
+
|
|
687
|
+
# Third pass: clean up remaining issues
|
|
688
|
+
text = re.sub(r'\s+', ' ', text) # Normalize whitespace
|
|
689
|
+
text = re.sub(r'(\w)\1{5,}', r'\1\1\1', text) # Remove character repetitions
|
|
690
|
+
text = re.sub(r'[^a-zA-Z0-9\s,.;:!?"\'()-]', '', text) # Remove remaining non-text chars
|
|
691
|
+
|
|
692
|
+
# Final pass: segment into paragraphs
|
|
693
|
+
lines = text.split('\n')
|
|
694
|
+
paragraphs = []
|
|
695
|
+
current_paragraph = []
|
|
696
|
+
|
|
697
|
+
for line in lines:
|
|
698
|
+
if not line.strip():
|
|
699
|
+
if current_paragraph:
|
|
700
|
+
paragraphs.append(' '.join(current_paragraph))
|
|
701
|
+
current_paragraph = []
|
|
702
|
+
else:
|
|
703
|
+
current_paragraph.append(line.strip())
|
|
704
|
+
|
|
705
|
+
if current_paragraph:
|
|
706
|
+
paragraphs.append(' '.join(current_paragraph))
|
|
707
|
+
|
|
708
|
+
return '\n\n'.join(paragraphs)
|
|
709
|
+
|
|
710
|
+
def _extract_raw_pdf_text(self, content: bytes) -> str:
|
|
711
|
+
"""Extract any text-like content from raw PDF bytes with enhanced capabilities."""
|
|
712
|
+
try:
|
|
713
|
+
# Convert to string, ignoring errors
|
|
714
|
+
text = content.decode('utf-8', errors='ignore')
|
|
715
|
+
|
|
716
|
+
# Find text between PDF markers
|
|
717
|
+
text_markers = [
|
|
718
|
+
(b'(', b')'), # Text in parentheses
|
|
719
|
+
(b'<', b'>'), # Hex encoded text
|
|
720
|
+
(b'BT', b'ET') # Text blocks
|
|
721
|
+
]
|
|
722
|
+
|
|
723
|
+
extracted = []
|
|
724
|
+
for start_marker, end_marker in text_markers:
|
|
725
|
+
pos = 0
|
|
726
|
+
while True:
|
|
727
|
+
start = content.find(start_marker, pos)
|
|
728
|
+
if start == -1:
|
|
729
|
+
break
|
|
730
|
+
end = content.find(end_marker, start + len(start_marker))
|
|
731
|
+
if end == -1:
|
|
732
|
+
break
|
|
733
|
+
|
|
734
|
+
text_chunk = content[start+len(start_marker):end]
|
|
735
|
+
try:
|
|
736
|
+
# Try to decode as text
|
|
737
|
+
decoded = text_chunk.decode('utf-8', errors='ignore')
|
|
738
|
+
if decoded.strip():
|
|
739
|
+
extracted.append(decoded)
|
|
740
|
+
except Exception:
|
|
741
|
+
# Silently skip non-decodable chunks
|
|
742
|
+
pass
|
|
743
|
+
|
|
744
|
+
pos = end + len(end_marker)
|
|
745
|
+
|
|
746
|
+
# Clean up extracted text
|
|
747
|
+
if extracted:
|
|
748
|
+
text = '\n'.join(extracted)
|
|
749
|
+
return self._clean_pdf_text(text)
|
|
750
|
+
|
|
751
|
+
return ''
|
|
752
|
+
|
|
753
|
+
except Exception as e:
|
|
754
|
+
self.logger.warning(f"Failed to extract raw text: {str(e)}")
|
|
755
|
+
return ''
|