marvisx-cli 0.1.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.
- core/api/__init__.py +0 -0
- core/api/agents/__init__.py +0 -0
- core/api/agents/session_health.py +59 -0
- core/api/agents/session_manager.py +206 -0
- core/api/bin/marvisx-state-hook.py +182 -0
- core/api/config.py +533 -0
- core/api/db.py +1516 -0
- core/api/dependencies/__init__.py +0 -0
- core/api/dependencies/tenant.py +34 -0
- core/api/main.py +1641 -0
- core/api/mcp/__init__.py +8 -0
- core/api/mcp/_adapter.py +184 -0
- core/api/mcp/server.py +58 -0
- core/api/mcp/tools/__init__.py +59 -0
- core/api/mcp/tools/brain.py +599 -0
- core/api/mcp/tools/graph.py +380 -0
- core/api/mcp/tools/handoffs.py +112 -0
- core/api/mcp/tools/ingest.py +326 -0
- core/api/mcp/tools/learnings.py +144 -0
- core/api/mcp/tools/projects.py +99 -0
- core/api/mcp/tools/pull_requests.py +173 -0
- core/api/mcp/tools/safety.py +111 -0
- core/api/mcp/tools/search.py +79 -0
- core/api/mcp/tools/tasks.py +258 -0
- core/api/middleware/__init__.py +0 -0
- core/api/middleware/tool_call_audit.py +111 -0
- core/api/models/__init__.py +346 -0
- core/api/models/auth.py +48 -0
- core/api/models/brain.py +1006 -0
- core/api/models/common.py +76 -0
- core/api/models/costs.py +91 -0
- core/api/models/graph.py +66 -0
- core/api/models/graph_cosmo.py +125 -0
- core/api/models/graph_pr_impact.py +257 -0
- core/api/models/graph_ux.py +141 -0
- core/api/models/inbox.py +230 -0
- core/api/models/ingest_keys.py +108 -0
- core/api/models/kg.py +41 -0
- core/api/models/llm_config.py +56 -0
- core/api/models/monitoring.py +234 -0
- core/api/models/projects.py +161 -0
- core/api/models/search.py +42 -0
- core/api/models/sessions.py +322 -0
- core/api/models/tasks.py +184 -0
- core/api/models/teams.py +63 -0
- core/api/models/users.py +184 -0
- core/api/observability/__init__.py +0 -0
- core/api/observability/tracing.py +92 -0
- core/api/paths.py +26 -0
- core/api/rate_limit.py +24 -0
- core/api/rbac.py +112 -0
- core/api/routers/__init__.py +0 -0
- core/api/routers/_adapter.py +24 -0
- core/api/routers/admin_pr_impact.py +230 -0
- core/api/routers/admin_settings.py +147 -0
- core/api/routers/agent.py +1079 -0
- core/api/routers/agent_tokens.py +276 -0
- core/api/routers/app_settings.py +112 -0
- core/api/routers/audit.py +89 -0
- core/api/routers/auth.py +586 -0
- core/api/routers/bench.py +161 -0
- core/api/routers/brain.py +881 -0
- core/api/routers/brain_directions.py +527 -0
- core/api/routers/ci_checks.py +140 -0
- core/api/routers/comments.py +273 -0
- core/api/routers/costs.py +148 -0
- core/api/routers/docs_coverage.py +217 -0
- core/api/routers/docs_governance.py +63 -0
- core/api/routers/documents.py +318 -0
- core/api/routers/files.py +163 -0
- core/api/routers/finder.py +987 -0
- core/api/routers/graph.py +836 -0
- core/api/routers/handoffs.py +156 -0
- core/api/routers/inbox.py +496 -0
- core/api/routers/ingest_api_keys.py +205 -0
- core/api/routers/ingest_triage.py +1227 -0
- core/api/routers/judge.py +306 -0
- core/api/routers/kg.py +336 -0
- core/api/routers/learnings.py +253 -0
- core/api/routers/llm_config.py +130 -0
- core/api/routers/monitoring.py +347 -0
- core/api/routers/notifications.py +125 -0
- core/api/routers/pr_impact.py +315 -0
- core/api/routers/projects.py +1061 -0
- core/api/routers/pull_requests.py +312 -0
- core/api/routers/push.py +67 -0
- core/api/routers/raci.py +228 -0
- core/api/routers/search.py +125 -0
- core/api/routers/sessions.py +3100 -0
- core/api/routers/settings.py +90 -0
- core/api/routers/share_repo.py +68 -0
- core/api/routers/status_updates.py +96 -0
- core/api/routers/tags.py +45 -0
- core/api/routers/tasks.py +526 -0
- core/api/routers/teams.py +425 -0
- core/api/routers/terminal.py +105 -0
- core/api/routers/users.py +331 -0
- core/api/routers/webhooks.py +330 -0
- core/api/runtime_settings.py +84 -0
- core/api/security.py +652 -0
- core/api/services/__init__.py +0 -0
- core/api/services/audit.py +58 -0
- core/api/services/auto_approval.py +82 -0
- core/api/services/brain/__init__.py +52 -0
- core/api/services/brain/baseline.py +230 -0
- core/api/services/brain/capabilities.py +75 -0
- core/api/services/brain/cascade_rollup.py +388 -0
- core/api/services/brain/compound_bridge.py +215 -0
- core/api/services/brain/cycle.py +1242 -0
- core/api/services/brain/cycle_snapshot.py +371 -0
- core/api/services/brain/digest_collector.py +147 -0
- core/api/services/brain/direction.py +421 -0
- core/api/services/brain/drift.py +356 -0
- core/api/services/brain/drift_router.py +409 -0
- core/api/services/brain/edge_metrics.py +79 -0
- core/api/services/brain/events_reader.py +222 -0
- core/api/services/brain/findings.py +1379 -0
- core/api/services/brain/findings_reader.py +1006 -0
- core/api/services/brain/jobs.py +733 -0
- core/api/services/brain/journal.py +206 -0
- core/api/services/brain/knowledge_forms.py +92 -0
- core/api/services/brain/llm/__init__.py +37 -0
- core/api/services/brain/llm/_runner.py +62 -0
- core/api/services/brain/llm/base.py +70 -0
- core/api/services/brain/llm/cache.py +99 -0
- core/api/services/brain/llm/constants.py +46 -0
- core/api/services/brain/llm/direction_alignment.py +289 -0
- core/api/services/brain/llm/factory.py +132 -0
- core/api/services/brain/llm/finding_reasoning.py +98 -0
- core/api/services/brain/llm/finding_summary.py +92 -0
- core/api/services/brain/llm/grounding.py +46 -0
- core/api/services/brain/llm/journal_polish.py +96 -0
- core/api/services/brain/llm/local_gateway.py +426 -0
- core/api/services/brain/llm/parsers.py +71 -0
- core/api/services/brain/llm/router_glue.py +422 -0
- core/api/services/brain/memory_ops.py +1677 -0
- core/api/services/brain/models.py +140 -0
- core/api/services/brain/owner_hint.py +211 -0
- core/api/services/brain/recap.py +307 -0
- core/api/services/brain/rules/__init__.py +65 -0
- core/api/services/brain/rules/_signals.py +205 -0
- core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
- core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
- core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
- core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
- core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
- core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
- core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
- core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
- core/api/services/brain/runs_reader.py +485 -0
- core/api/services/brain/scope.py +79 -0
- core/api/services/brain/sources/__init__.py +46 -0
- core/api/services/brain/sources/base.py +86 -0
- core/api/services/brain/sources/git_kg.py +393 -0
- core/api/services/brain/sources/handoffs.py +157 -0
- core/api/services/brain/sources/ingestor.py +130 -0
- core/api/services/brain/sources/learnings.py +121 -0
- core/api/services/brain/sources/pir_tasks.py +245 -0
- core/api/services/brain/watermarks.py +147 -0
- core/api/services/brain/ws_emitter.py +170 -0
- core/api/services/cc_tasks_reader.py +76 -0
- core/api/services/ci_service.py +263 -0
- core/api/services/claude_metrics.py +796 -0
- core/api/services/codex_metrics.py +364 -0
- core/api/services/conversation_reader.py +102 -0
- core/api/services/cost_service.py +243 -0
- core/api/services/crypto.py +147 -0
- core/api/services/docs_governance/__init__.py +1 -0
- core/api/services/docs_governance/confidence.py +230 -0
- core/api/services/docs_governance/config.py +87 -0
- core/api/services/docs_governance/enrichment.py +65 -0
- core/api/services/docs_governance/frontmatter_validator.py +83 -0
- core/api/services/docs_governance/hard_gates.py +221 -0
- core/api/services/docs_governance/triage_orchestrator.py +98 -0
- core/api/services/embedding_internal.py +395 -0
- core/api/services/embedding_service.py +832 -0
- core/api/services/event_dispatcher.py +167 -0
- core/api/services/events.py +70 -0
- core/api/services/git_ops.py +621 -0
- core/api/services/graph_cosmo_service.py +440 -0
- core/api/services/graph_ranker.py +306 -0
- core/api/services/graph_service.py +1589 -0
- core/api/services/inbox.py +800 -0
- core/api/services/inbox_digest.py +221 -0
- core/api/services/inbox_digest_deep_research.py +80 -0
- core/api/services/inbox_digest_jobs.py +595 -0
- core/api/services/inbox_gmail_sync.py +167 -0
- core/api/services/inbox_llm_classifier.py +906 -0
- core/api/services/inbox_source_identity.py +116 -0
- core/api/services/inbox_sources.py +456 -0
- core/api/services/inbox_taxonomy.py +195 -0
- core/api/services/inbox_tldr.py +1079 -0
- core/api/services/inbox_triage.py +899 -0
- core/api/services/ingest/__init__.py +13 -0
- core/api/services/ingest/api_key_auth.py +136 -0
- core/api/services/ingest/auto_approve.py +120 -0
- core/api/services/ingest/classifier.py +138 -0
- core/api/services/ingest/confidence.py +173 -0
- core/api/services/ingest/dispatch.py +88 -0
- core/api/services/ingest/embedding_router.py +272 -0
- core/api/services/ingest/events.py +33 -0
- core/api/services/ingest/ignore_patterns.py +79 -0
- core/api/services/ingest/image_probe.py +218 -0
- core/api/services/ingest/ingress.py +263 -0
- core/api/services/ingest/insert_saga.py +793 -0
- core/api/services/ingest/llm/__init__.py +13 -0
- core/api/services/ingest/llm/anthropic_haiku.py +23 -0
- core/api/services/ingest/llm/base.py +59 -0
- core/api/services/ingest/llm/byok_provider.py +130 -0
- core/api/services/ingest/llm/classification_context.py +301 -0
- core/api/services/ingest/llm/config_store.py +246 -0
- core/api/services/ingest/llm/factory.py +24 -0
- core/api/services/ingest/llm/kg_enricher.py +306 -0
- core/api/services/ingest/llm/local_gateway.py +821 -0
- core/api/services/ingest/llm/local_vllm.py +23 -0
- core/api/services/ingest/llm/openai_nano.py +349 -0
- core/api/services/ingest/lock_advisory.py +57 -0
- core/api/services/ingest/parser_router.py +1756 -0
- core/api/services/ingest/parsers/__init__.py +1 -0
- core/api/services/ingest/parsers/docling_parser.py +142 -0
- core/api/services/ingest/parsers/docparse_gateway.py +178 -0
- core/api/services/ingest/parsers/docx_parser.py +127 -0
- core/api/services/ingest/parsers/folder_unpacker.py +85 -0
- core/api/services/ingest/parsers/gateway_aux.py +147 -0
- core/api/services/ingest/parsers/image_parser.py +251 -0
- core/api/services/ingest/parsers/internal_markdown.py +89 -0
- core/api/services/ingest/parsers/ocr_gateway.py +117 -0
- core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
- core/api/services/ingest/parsers/pdf_types.py +13 -0
- core/api/services/ingest/parsers/transcript_parser.py +445 -0
- core/api/services/ingest/parsers/vision_gateway.py +186 -0
- core/api/services/ingest/parsers/xlsx_parser.py +91 -0
- core/api/services/ingest/parsers/zip_unpacker.py +126 -0
- core/api/services/ingest/preflight.py +393 -0
- core/api/services/ingest/retry_voyage.py +88 -0
- core/api/services/ingest/routing_policy.py +307 -0
- core/api/services/ingest/serializers/__init__.py +1 -0
- core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
- core/api/services/ingest/skip_log.py +74 -0
- core/api/services/ingest/watcher.py +637 -0
- core/api/services/kg/__init__.py +0 -0
- core/api/services/kg/audit.py +49 -0
- core/api/services/kg/hybrid_search.py +691 -0
- core/api/services/kg/lens.py +339 -0
- core/api/services/kg/pr_impact.py +770 -0
- core/api/services/kg/queries.py +152 -0
- core/api/services/kg/ranking.py +89 -0
- core/api/services/kg/rrf.py +143 -0
- core/api/services/kg_watcher_control.py +161 -0
- core/api/services/local_llm/__init__.py +19 -0
- core/api/services/local_llm/async_client.py +385 -0
- core/api/services/local_llm/client.py +173 -0
- core/api/services/local_llm/url_validator.py +44 -0
- core/api/services/metrics_collector.py +646 -0
- core/api/services/metrics_providers.py +65 -0
- core/api/services/model_registry.py +266 -0
- core/api/services/model_router.py +137 -0
- core/api/services/n8n_client.py +77 -0
- core/api/services/newsletter_llm_gateway.py +66 -0
- core/api/services/notification_service.py +134 -0
- core/api/services/openai_responses.py +55 -0
- core/api/services/opencode_metrics.py +375 -0
- core/api/services/opencode_sessions.py +173 -0
- core/api/services/pii_redactor.py +138 -0
- core/api/services/pr_impact_pipeline/__init__.py +21 -0
- core/api/services/pr_impact_pipeline/differ.py +421 -0
- core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
- core/api/services/pr_impact_pipeline/gc.py +93 -0
- core/api/services/pr_impact_pipeline/languages.py +192 -0
- core/api/services/pr_impact_pipeline/parser.py +178 -0
- core/api/services/pr_impact_pipeline/writer.py +394 -0
- core/api/services/pr_service.py +1393 -0
- core/api/services/project_paths.py +70 -0
- core/api/services/project_status_updates.py +265 -0
- core/api/services/providers.py +276 -0
- core/api/services/push_service.py +170 -0
- core/api/services/reminder_service.py +89 -0
- core/api/services/runas.py +41 -0
- core/api/services/salience_service.py +69 -0
- core/api/services/security_collector.py +281 -0
- core/api/services/session_catalog.py +385 -0
- core/api/services/session_metrics_service.py +301 -0
- core/api/services/session_ops.py +272 -0
- core/api/services/session_state.py +173 -0
- core/api/services/share_links.py +222 -0
- core/api/services/task_transitions.py +146 -0
- core/api/services/terminal_metrics.py +462 -0
- core/api/services/terminal_metrics_dump.py +203 -0
- core/api/services/tmux.py +1205 -0
- core/api/services/webhook_service.py +422 -0
- core/api/services/workspace_sync.py +164 -0
- core/api/templates/__init__.py +1 -0
- core/api/templates/markdown_share.py +164 -0
- core/api/terminal.py +1031 -0
- core/api/tests/__init__.py +0 -0
- core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
- core/api/tests/test_audit_permissions.py +133 -0
- core/api/tests/test_backfill_session_conversations.py +90 -0
- core/api/tests/test_backfill_working_seconds_msg.py +129 -0
- core/api/tests/test_claude_metrics.py +326 -0
- core/api/tests/test_codex_metrics.py +189 -0
- core/api/tests/test_finder_paths.py +74 -0
- core/api/tests/test_git_ops_merge.py +155 -0
- core/api/tests/test_learnings_check_search.py +81 -0
- core/api/tests/test_metrics_providers.py +133 -0
- core/api/tests/test_migration_087.py +164 -0
- core/api/tests/test_migration_088.py +94 -0
- core/api/tests/test_migration_089.py +116 -0
- core/api/tests/test_openai_responses.py +24 -0
- core/api/tests/test_opencode_metrics.py +740 -0
- core/api/tests/test_opencode_sessions.py +321 -0
- core/api/tests/test_pr_workflow_e2e.py +457 -0
- core/api/tests/test_projects_handoffs.py +31 -0
- core/api/tests/test_providers.py +138 -0
- core/api/tests/test_safety_bridge.py +347 -0
- core/api/tests/test_session_catalog.py +142 -0
- core/api/tests/test_session_conversations.py +512 -0
- core/api/tests/test_session_metrics_service.py +270 -0
- core/api/tests/test_session_resume_paths.py +548 -0
- core/api/tests/test_session_theme_mode_migration.py +56 -0
- core/api/tests/test_sessions_rbac.py +131 -0
- core/api/tests/test_share_edit.py +398 -0
- core/api/tests/test_share_repo.py +200 -0
- core/api/tests/test_terminal_session_manager.py +98 -0
- core/api/tests/test_terminal_upload.py +34 -0
- core/api/tests/test_tmux.py +272 -0
- core/api/tests/test_workspace_sync.py +186 -0
- core/api/tests/test_ws_ticket_in_memory.py +73 -0
- core/api/use_cases/__init__.py +11 -0
- core/api/use_cases/_context.py +89 -0
- core/api/use_cases/_errors.py +62 -0
- core/api/use_cases/_roles.py +16 -0
- core/api/use_cases/audit.py +171 -0
- core/api/use_cases/brain.py +1232 -0
- core/api/use_cases/costs.py +249 -0
- core/api/use_cases/graph.py +1153 -0
- core/api/use_cases/handoffs.py +506 -0
- core/api/use_cases/ingest_triage.py +1229 -0
- core/api/use_cases/learnings.py +538 -0
- core/api/use_cases/projects.py +705 -0
- core/api/use_cases/pull_requests.py +415 -0
- core/api/use_cases/search.py +926 -0
- core/api/use_cases/tasks.py +1495 -0
- core/api/visibility.py +141 -0
- core/cli/__init__.py +5 -0
- core/cli/_index_source.py +632 -0
- core/cli/_runtime_ctx.py +160 -0
- core/cli/_transmute.py +241 -0
- core/cli/marvis_doctor.py +704 -0
- core/cli/marvis_feedback.py +396 -0
- core/cli/marvis_governance.py +315 -0
- core/cli/marvis_hooks.py +515 -0
- core/cli/marvis_init.py +757 -0
- core/cli/marvis_mcp.py +401 -0
- core/cli/marvis_runtime.py +855 -0
- core/cli/marvis_telemetry.py +228 -0
- core/scripts/_drift_check.py +716 -0
- core/scripts/_frontmatter.py +66 -0
- core/scripts/_graph_writer.py +189 -0
- core/scripts/ast_parser.py +1553 -0
- core/scripts/install_hooks/__init__.py +1 -0
- core/scripts/install_hooks/_config.sh +109 -0
- core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
- core/scripts/install_hooks/block-db-direct-write.sh +23 -0
- core/scripts/install_hooks/block-push-no-task.sh +23 -0
- core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
- core/scripts/install_hooks/block-subtree-push.sh +23 -0
- core/scripts/install_hooks/config.json +53 -0
- core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
- core/scripts/install_hooks/enforce-worktree.sh +23 -0
- core/scripts/install_hooks/quality-gate.sh +170 -0
- core/scripts/install_hooks/safety_bridge.py +968 -0
- core/scripts/install_hooks/secret-scan.sh +23 -0
- core/scripts/migrate_spike_node_ids.py +122 -0
- core/scripts/populate_artifacts.py +2198 -0
- core/scripts/populate_cross_project.py +2457 -0
- core/scripts/populate_inbox_nodes.py +357 -0
- core/scripts/populate_pr_impact.py +267 -0
- core/scripts/populate_project_nodes.py +603 -0
- core/scripts/populate_touch_counter.py +337 -0
- core/scripts/reparse_failed.py +57 -0
- core/scripts/safety_bridge.py +968 -0
- core/telemetry/__init__.py +9 -0
- core/telemetry/client.py +405 -0
- core/telemetry/schema.py +122 -0
- core/wizard/__init__.py +65 -0
- core/wizard/byok_vault.py +147 -0
- core/wizard/defaults.py +58 -0
- core/wizard/state.py +117 -0
- core/wizard/steps.py +70 -0
- core/wizard/validation.py +136 -0
- marvisx_cli-0.1.0.dist-info/METADATA +201 -0
- marvisx_cli-0.1.0.dist-info/RECORD +587 -0
- marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
- marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
- marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
- migrations/001_initial.sql +33 -0
- migrations/002_tasks.sql +30 -0
- migrations/003_session_management.sql +7 -0
- migrations/004_projects_comments.sql +65 -0
- migrations/005_session_intelligence.sql +15 -0
- migrations/006_settings.sql +12 -0
- migrations/007_task_scoring.sql +12 -0
- migrations/008_cost_tracking.sql +31 -0
- migrations/009_session_card_metrics.sql +3 -0
- migrations/010_monitoring.sql +55 -0
- migrations/012_agent_api.sql +21 -0
- migrations/013_session_complete.sql +8 -0
- migrations/015_pull_requests.sql +43 -0
- migrations/015_pull_requests_down.sql +5 -0
- migrations/016_users_raci.sql +116 -0
- migrations/017_task_cost_entries.sql +87 -0
- migrations/018_agents.sql +73 -0
- migrations/018_agents_down.sql +13 -0
- migrations/019_review_feedback.sql +18 -0
- migrations/020_pr_commit_sha.sql +4 -0
- migrations/021_webhook_events.sql +18 -0
- migrations/022_devx_agent_managed.sql +11 -0
- migrations/022_devx_agent_managed_down.sql +6 -0
- migrations/023_devx_p1_gate.sql +7 -0
- migrations/023_devx_p1_gate_down.sql +3 -0
- migrations/024_chat_messages.sql +16 -0
- migrations/024_pr_conversation_id.sql +8 -0
- migrations/024_task_indexes.sql +21 -0
- migrations/024_task_indexes_down.sql +7 -0
- migrations/025_audit_log.sql +17 -0
- migrations/026_agent_tokens.sql +20 -0
- migrations/027_teams_auth_phase_b.sql +35 -0
- migrations/028_learnings.sql +23 -0
- migrations/029_team_roles.sql +14 -0
- migrations/030_finder_pins.sql +10 -0
- migrations/031_pr_deploy_status.sql +9 -0
- migrations/032_task_reminders.sql +7 -0
- migrations/033_events_retry_count.sql +6 -0
- migrations/033_session_owner.sql +9 -0
- migrations/034_notifications.sql +38 -0
- migrations/035_shared_links.sql +15 -0
- migrations/036_session_index_upgrade.sql +29 -0
- migrations/037_pr_approval.sql +15 -0
- migrations/038_pr_submitted_by.sql +6 -0
- migrations/039_push_subscriptions.sql +17 -0
- migrations/040_semantic_search.sql +16 -0
- migrations/041_workspaces.sql +63 -0
- migrations/042_oidc_providers.sql +24 -0
- migrations/043_ci_checks.sql +31 -0
- migrations/044_agent_metrics.sql +30 -0
- migrations/045_documents_doc_type.sql +5 -0
- migrations/046_salience.sql +13 -0
- migrations/047_seed_missing_agents.sql +6 -0
- migrations/048_fix_agent_paths_roles.sql +5 -0
- migrations/049_agent_role_and_learnings_schema.sql +3 -0
- migrations/050_session_provider.sql +2 -0
- migrations/051_session_launch_profile.sql +4 -0
- migrations/052_session_theme_mode.sql +2 -0
- migrations/052_task_kind.sql +4 -0
- migrations/053_inbox_items.sql +31 -0
- migrations/054_inbox_triage_contract.sql +30 -0
- migrations/055_inbox_topic_treatment.sql +12 -0
- migrations/056_inbox_treatment_read_save.sql +57 -0
- migrations/057_session_theme_mode_backfill.sql +4 -0
- migrations/058_inbox_item_status_lifecycle.sql +13 -0
- migrations/059_inbox_tldr_and_source_scores.sql +18 -0
- migrations/060_newsletter.sql +16 -0
- migrations/061_inbox_redesign.sql +69 -0
- migrations/062_fix_inbox_sources_backfill.sql +37 -0
- migrations/063_task_completion_mode.sql +23 -0
- migrations/064_judge_mode_setting.sql +4 -0
- migrations/065_knowledge_graph_spike.sql +40 -0
- migrations/066_digest_ranking_inputs.sql +10 -0
- migrations/066_kg_artifact_nodes.sql +129 -0
- migrations/067_inbox_digest_selections.sql +28 -0
- migrations/067_kg_temporal.sql +53 -0
- migrations/068_inbox_digest_app_settings.sql +9 -0
- migrations/068_kg_touch_counter.sql +52 -0
- migrations/069_kg_doc_types.sql +117 -0
- migrations/070_digest_ranking_inputs_recovery.sql +3 -0
- migrations/071_inbox_digest_selections_recovery.sql +3 -0
- migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
- migrations/073_kg_cross_project.sql +216 -0
- migrations/073_kg_cross_project_down.sql +77 -0
- migrations/074_kg_infra_types.sql +208 -0
- migrations/074_kg_infra_types_down.sql +80 -0
- migrations/075_kg_file_state_recovery.sql +35 -0
- migrations/075_kg_file_state_recovery_down.sql +5 -0
- migrations/076_kg_watcher_state.sql +33 -0
- migrations/076_kg_watcher_state_down.sql +3 -0
- migrations/077_kg_doc_types_extend.sql +226 -0
- migrations/077_kg_doc_types_extend_down.sql +80 -0
- migrations/078_kg_fts5.sql +102 -0
- migrations/078_kg_fts5_down.sql +14 -0
- migrations/079_kg_missing_indexes.sql +31 -0
- migrations/079_kg_missing_indexes_down.sql +10 -0
- migrations/080_kg_fts5_extended.sql +232 -0
- migrations/080_kg_fts5_extended_down.sql +25 -0
- migrations/081_kg_lens_indexes.sql +9 -0
- migrations/081_kg_lens_indexes_down.sql +3 -0
- migrations/082_kg_pins.sql +26 -0
- migrations/082_kg_pins_down.sql +14 -0
- migrations/083_kg_graph_nodes_degree.sql +20 -0
- migrations/083_kg_graph_nodes_degree_down.sql +15 -0
- migrations/084_drop_legacy_scheduler_tables.sql +58 -0
- migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
- migrations/085_kg_edge_resolves_to.sql +142 -0
- migrations/085_kg_edge_resolves_to_down.sql +66 -0
- migrations/086_project_status_updates_feed.sql +20 -0
- migrations/086_project_status_updates_feed_down.sql +36 -0
- migrations/087_session_metrics_dual.sql +50 -0
- migrations/087_session_metrics_dual_down.sql +21 -0
- migrations/088_rename_context_pct_legacy.sql +23 -0
- migrations/088_rename_context_pct_legacy_down.sql +8 -0
- migrations/089_session_metrics_equivalent_cost.sql +26 -0
- migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
- migrations/090_kg_inbox_node_type.sql +26 -0
- migrations/090_kg_inbox_node_type_down.sql +20 -0
- migrations/091_kg_inbox_node_type_check.sql +265 -0
- migrations/091_kg_inbox_node_type_check_down.sql +129 -0
- migrations/092_sessions_activity_state_ts.sql +29 -0
- migrations/092_sessions_activity_state_ts_down.sql +14 -0
- migrations/093_sessions_activity_state_column.sql +29 -0
- migrations/093_sessions_activity_state_column_down.sql +10 -0
- migrations/094_ingest_pending.sql +55 -0
- migrations/094_ingest_pending_down.sql +15 -0
- migrations/095_kg_intent_first.sql +77 -0
- migrations/095_kg_intent_first_down.sql +25 -0
- migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
- migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
- migrations/097_ingest_change_history.sql +37 -0
- migrations/097_ingest_change_history_down.sql +13 -0
- migrations/098_kg_node_type_business.sql +254 -0
- migrations/098_kg_node_type_business_down.sql +195 -0
- migrations/099_kg_edges_restore_weight.sql +58 -0
- migrations/099_kg_edges_restore_weight_down.sql +12 -0
- migrations/100_kg_enriched_at.sql +25 -0
- migrations/100_kg_enriched_at_down.sql +12 -0
- migrations/101_local_llm_shadow_comparisons.sql +66 -0
- migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
- migrations/102_promote_llm_costs.sql +69 -0
- migrations/102_promote_llm_costs_down.sql +19 -0
- migrations/103_ingest_skipped_log.sql +46 -0
- migrations/103_ingest_skipped_log_down.sql +15 -0
- migrations/120_docs_governance.sql +50 -0
- migrations/120_docs_governance_down.sql +11 -0
- migrations/121_notification_event_fk_cleanup.sql +21 -0
- migrations/121_notification_event_fk_cleanup_down.sql +10 -0
- migrations/122_docs_drift_history.sql +34 -0
- migrations/122_docs_drift_history_down.sql +15 -0
- migrations/123_ingest_parser_waiting_status.sql +69 -0
- migrations/123_ingest_parser_waiting_status_down.sql +69 -0
- migrations/124_heypocket_recordings.sql +63 -0
- migrations/124_heypocket_recordings_down.sql +13 -0
- migrations/125_kg_node_type_record.sql +219 -0
- migrations/125_kg_node_type_record_down.sql +205 -0
- migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
- migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
- migrations/127_brain_v1_substrate.sql +200 -0
- migrations/127_brain_v1_substrate_down.sql +32 -0
- migrations/128_brain_drift_signals.sql +157 -0
- migrations/128_brain_drift_signals_down.sql +23 -0
- migrations/129_brain_memory_operations.sql +232 -0
- migrations/129_brain_memory_operations_down.sql +27 -0
- migrations/130_brain_findings.sql +258 -0
- migrations/130_brain_findings_down.sql +29 -0
- migrations/132_kg_pr_modifies.sql +242 -0
- migrations/132_kg_pr_modifies_down.sql +99 -0
- migrations/133_brain_v1_2_direction_schema.sql +476 -0
- migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
- migrations/134_brain_journal_narrative_polished.sql +8 -0
- migrations/134_brain_journal_narrative_polished_down.sql +6 -0
- migrations/135_kg_edges_provider.sql +21 -0
- migrations/136_documents_fts.sql +56 -0
- migrations/137_promote_llm_costs.sql +59 -0
- migrations/137_promote_llm_costs_down.sql +19 -0
- migrations/138_ingest_api_keys.sql +39 -0
- migrations/138_ingest_api_keys_down.sql +8 -0
- migrations/139_ingest_pending_ingress.sql +91 -0
- migrations/139_ingest_pending_ingress_down.sql +73 -0
- migrations/140_ingest_idempotency_quota.sql +45 -0
- migrations/140_ingest_idempotency_quota_down.sql +9 -0
- migrations/141_ingest_pending_metadata.sql +16 -0
- migrations/141_ingest_pending_metadata_down.sql +7 -0
- migrations/142_llm_function_config.sql +36 -0
- migrations/142_llm_function_config_down.sql +8 -0
- migrations/143_kg_code_embeddings.sql +25 -0
- migrations/143_kg_code_embeddings_down.sql +5 -0
- migrations/__init__.py +4 -0
- projects/_template/project.yaml +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""File parsers for universal ingestion."""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""PDF parser using Docling as the primary extractor."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from core.api.services.ingest.parsers.ocr_pdf_parser import parse_pdf_ocr
|
|
12
|
+
from core.api.services.ingest.parsers.docparse_gateway import parse_pdf_docparse
|
|
13
|
+
from core.api.services.ingest.parsers.gateway_aux import MissingGatewayConfig, settings
|
|
14
|
+
from core.api.services.ingest.parsers.pdf_types import PdfParseResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
DOCLING_TIMEOUT_SECONDS = 120
|
|
19
|
+
DOCLING_WORKERS = 3
|
|
20
|
+
MIN_TEXT_FOR_OCR_FALLBACK = 100
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache(maxsize=1)
|
|
24
|
+
def _executor() -> ThreadPoolExecutor:
|
|
25
|
+
return ThreadPoolExecutor(max_workers=DOCLING_WORKERS, thread_name_prefix="docling")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=1)
|
|
29
|
+
def _converter():
|
|
30
|
+
from docling.document_converter import DocumentConverter
|
|
31
|
+
|
|
32
|
+
return DocumentConverter()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_encrypted(file_path: Path) -> bool:
|
|
36
|
+
try:
|
|
37
|
+
from pypdf import PdfReader
|
|
38
|
+
|
|
39
|
+
return bool(PdfReader(str(file_path)).is_encrypted)
|
|
40
|
+
except ImportError:
|
|
41
|
+
logger.warning("pypdf not installed; skipping PDF encryption preflight")
|
|
42
|
+
return False
|
|
43
|
+
except Exception:
|
|
44
|
+
logger.warning("PDF encryption preflight failed for %s", file_path, exc_info=True)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _safe_doc_dict(document: Any) -> dict[str, Any]:
|
|
49
|
+
if hasattr(document, "export_to_dict"):
|
|
50
|
+
value = document.export_to_dict()
|
|
51
|
+
return value if isinstance(value, dict) else {"document": value}
|
|
52
|
+
if hasattr(document, "dict"):
|
|
53
|
+
value = document.dict()
|
|
54
|
+
return value if isinstance(value, dict) else {"document": value}
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _count_tables(document: Any, structure: dict[str, Any]) -> int:
|
|
59
|
+
tables = getattr(document, "tables", None)
|
|
60
|
+
if isinstance(tables, list):
|
|
61
|
+
return len(tables)
|
|
62
|
+
if hasattr(document, "iterate_items"):
|
|
63
|
+
try:
|
|
64
|
+
return sum(
|
|
65
|
+
1
|
|
66
|
+
for item in document.iterate_items()
|
|
67
|
+
if str(getattr(item, "label", "")).lower() == "table"
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
logger.debug("Docling table iteration failed", exc_info=True)
|
|
71
|
+
return _count_table_like_entries(structure)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _count_table_like_entries(value: Any) -> int:
|
|
75
|
+
if isinstance(value, dict):
|
|
76
|
+
count = 1 if str(value.get("label", "")).lower() == "table" else 0
|
|
77
|
+
return count + sum(_count_table_like_entries(item) for item in value.values())
|
|
78
|
+
if isinstance(value, list):
|
|
79
|
+
return sum(_count_table_like_entries(item) for item in value)
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _convert_with_docling(file_path: Path) -> PdfParseResult:
|
|
84
|
+
result = _converter().convert(str(file_path))
|
|
85
|
+
document = result.document
|
|
86
|
+
markdown = document.export_to_markdown()
|
|
87
|
+
structure = _safe_doc_dict(document)
|
|
88
|
+
structure.setdefault("tables_count", _count_tables(document, structure))
|
|
89
|
+
pages = getattr(document, "pages", None)
|
|
90
|
+
if pages is not None:
|
|
91
|
+
structure.setdefault("page_count", len(pages))
|
|
92
|
+
return PdfParseResult(
|
|
93
|
+
frontmatter={},
|
|
94
|
+
text=markdown,
|
|
95
|
+
structure=structure,
|
|
96
|
+
parser_used="docling",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _convert_sync(file_path: Path) -> PdfParseResult:
|
|
101
|
+
return _convert_with_docling(file_path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def parse_pdf_file(
|
|
105
|
+
file_path: Path,
|
|
106
|
+
*,
|
|
107
|
+
allow_docparse: bool = True,
|
|
108
|
+
docparse_mode: str | None = None,
|
|
109
|
+
) -> PdfParseResult:
|
|
110
|
+
"""Parse a PDF and run OCR fallback when Docling finds too little text."""
|
|
111
|
+
if _is_encrypted(file_path):
|
|
112
|
+
raise ValueError("PDF encrypted, requires password")
|
|
113
|
+
|
|
114
|
+
cfg = settings()
|
|
115
|
+
if allow_docparse and getattr(cfg, "ingest_docparse_enabled", False) and getattr(
|
|
116
|
+
cfg, "ingest_docparse_pdfs_enabled", True
|
|
117
|
+
):
|
|
118
|
+
try:
|
|
119
|
+
return await parse_pdf_docparse(file_path, mode=docparse_mode)
|
|
120
|
+
except MissingGatewayConfig:
|
|
121
|
+
logger.warning("tier-docparse not configured; falling back to Docling")
|
|
122
|
+
|
|
123
|
+
loop = asyncio.get_running_loop()
|
|
124
|
+
future = loop.run_in_executor(_executor(), _convert_sync, file_path)
|
|
125
|
+
try:
|
|
126
|
+
parsed = await asyncio.wait_for(future, timeout=DOCLING_TIMEOUT_SECONDS)
|
|
127
|
+
except asyncio.TimeoutError as exc:
|
|
128
|
+
future.cancel()
|
|
129
|
+
raise TimeoutError(f"parse timeout {DOCLING_TIMEOUT_SECONDS}s") from exc
|
|
130
|
+
except Exception:
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Docling parse failed; trying OCR fallback: %s",
|
|
133
|
+
file_path,
|
|
134
|
+
exc_info=True,
|
|
135
|
+
)
|
|
136
|
+
return await parse_pdf_ocr(file_path)
|
|
137
|
+
|
|
138
|
+
if len(parsed.text.strip()) >= MIN_TEXT_FOR_OCR_FALLBACK:
|
|
139
|
+
return parsed
|
|
140
|
+
|
|
141
|
+
logger.info("Docling extracted <100 chars, trying OCR fallback: %s", file_path)
|
|
142
|
+
return await parse_pdf_ocr(file_path)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Document parser backed by the Mac Gateway AUX /v1/docparse endpoint."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from core.api.services.ingest.parsers.gateway_aux import (
|
|
12
|
+
auth_headers,
|
|
13
|
+
aux_base_url,
|
|
14
|
+
request_gateway_with_retries,
|
|
15
|
+
settings,
|
|
16
|
+
)
|
|
17
|
+
from core.api.services.ingest.parsers.pdf_types import PdfParseResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def parse_docparse_with_gateway(
|
|
21
|
+
path: Path,
|
|
22
|
+
mime_type: str,
|
|
23
|
+
*,
|
|
24
|
+
mode: str | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
cfg = settings()
|
|
27
|
+
size = path.stat().st_size
|
|
28
|
+
if size > int(cfg.ingest_docparse_max_bytes):
|
|
29
|
+
raise ValueError(f"File too large for docparse: {size} bytes")
|
|
30
|
+
|
|
31
|
+
timeout_seconds = float(cfg.ingest_docparse_timeout_seconds)
|
|
32
|
+
timeout = httpx.Timeout(
|
|
33
|
+
connect=5.0,
|
|
34
|
+
read=timeout_seconds,
|
|
35
|
+
write=60.0,
|
|
36
|
+
pool=5.0,
|
|
37
|
+
)
|
|
38
|
+
async with httpx.AsyncClient(
|
|
39
|
+
base_url=f"{aux_base_url()}/",
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
limits=httpx.Limits(max_connections=2, max_keepalive_connections=1),
|
|
42
|
+
) as client:
|
|
43
|
+
async def post_docparse() -> httpx.Response:
|
|
44
|
+
with path.open("rb") as handle:
|
|
45
|
+
return await client.post(
|
|
46
|
+
"docparse",
|
|
47
|
+
headers=auth_headers(),
|
|
48
|
+
data={
|
|
49
|
+
"output_format": "markdown",
|
|
50
|
+
"mode": mode or cfg.ingest_docparse_mode,
|
|
51
|
+
},
|
|
52
|
+
files={"file": (path.name, handle, mime_type)},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
response = await request_gateway_with_retries(
|
|
56
|
+
post_docparse,
|
|
57
|
+
service_name="tier-docparse",
|
|
58
|
+
)
|
|
59
|
+
payload = await _resolve_docparse_response(
|
|
60
|
+
client,
|
|
61
|
+
response,
|
|
62
|
+
timeout_seconds=timeout_seconds,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
text = str(payload.get("text") or "").strip()
|
|
66
|
+
markdown = str(payload.get("markdown") or "").strip()
|
|
67
|
+
if not text and markdown:
|
|
68
|
+
text = markdown
|
|
69
|
+
if not text:
|
|
70
|
+
raise RuntimeError("tier-docparse returned empty text")
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"text": text,
|
|
74
|
+
"markdown": markdown,
|
|
75
|
+
"page_count": payload.get("page_count"),
|
|
76
|
+
"elements": payload.get("elements") or [],
|
|
77
|
+
"pages": payload.get("pages") or [],
|
|
78
|
+
"metadata": payload.get("metadata") or {},
|
|
79
|
+
"raw": payload,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def parse_pdf_docparse(path: Path, *, mode: str | None = None) -> PdfParseResult:
|
|
84
|
+
parsed = await parse_docparse_with_gateway(path, "application/pdf", mode=mode)
|
|
85
|
+
return PdfParseResult(
|
|
86
|
+
frontmatter={},
|
|
87
|
+
text=parsed["markdown"] or parsed["text"],
|
|
88
|
+
structure={
|
|
89
|
+
"page_count": parsed.get("page_count"),
|
|
90
|
+
"elements_count": len(parsed.get("elements") or []),
|
|
91
|
+
"elements": parsed.get("elements") or [],
|
|
92
|
+
"pages": parsed.get("pages") or [],
|
|
93
|
+
"metadata": parsed.get("metadata") or {},
|
|
94
|
+
"docparse_backend": "tier_docparse",
|
|
95
|
+
},
|
|
96
|
+
parser_used="tier_docparse",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _resolve_docparse_response(
|
|
101
|
+
client: httpx.AsyncClient,
|
|
102
|
+
response: httpx.Response,
|
|
103
|
+
*,
|
|
104
|
+
timeout_seconds: float,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
if response.status_code in {401, 403}:
|
|
107
|
+
raise RuntimeError("tier-docparse authorization failed")
|
|
108
|
+
if response.status_code == 429:
|
|
109
|
+
raise RuntimeError("tier-docparse rate limited")
|
|
110
|
+
if response.status_code >= 500:
|
|
111
|
+
raise RuntimeError(f"tier-docparse unavailable: HTTP {response.status_code}")
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
payload = response.json()
|
|
114
|
+
if not isinstance(payload, dict):
|
|
115
|
+
raise RuntimeError("tier-docparse returned invalid payload")
|
|
116
|
+
if response.status_code != 202:
|
|
117
|
+
return payload
|
|
118
|
+
|
|
119
|
+
status_url = str(payload.get("status_url") or "").strip()
|
|
120
|
+
if not status_url:
|
|
121
|
+
raise RuntimeError("tier-docparse async response missing status_url")
|
|
122
|
+
return await _poll_docparse_job(client, status_url, timeout_seconds=timeout_seconds)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def _poll_docparse_job(
|
|
126
|
+
client: httpx.AsyncClient,
|
|
127
|
+
status_url: str,
|
|
128
|
+
*,
|
|
129
|
+
timeout_seconds: float,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
deadline = time.monotonic() + timeout_seconds
|
|
132
|
+
request_url = _status_request_url(client, status_url)
|
|
133
|
+
while time.monotonic() < deadline:
|
|
134
|
+
async def get_status() -> httpx.Response:
|
|
135
|
+
return await client.get(request_url, headers=auth_headers())
|
|
136
|
+
|
|
137
|
+
response = await request_gateway_with_retries(
|
|
138
|
+
get_status,
|
|
139
|
+
service_name="tier-docparse-poll",
|
|
140
|
+
)
|
|
141
|
+
if response.status_code in {401, 403}:
|
|
142
|
+
raise RuntimeError("tier-docparse poll authorization failed")
|
|
143
|
+
if response.status_code == 404:
|
|
144
|
+
raise RuntimeError("tier-docparse job not found")
|
|
145
|
+
if response.status_code >= 500:
|
|
146
|
+
raise RuntimeError(f"tier-docparse poll unavailable: HTTP {response.status_code}")
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
status_payload = response.json()
|
|
149
|
+
if not isinstance(status_payload, dict):
|
|
150
|
+
raise RuntimeError("tier-docparse poll returned invalid payload")
|
|
151
|
+
|
|
152
|
+
status = status_payload.get("status")
|
|
153
|
+
if status == "done":
|
|
154
|
+
result = status_payload.get("result")
|
|
155
|
+
if not isinstance(result, dict):
|
|
156
|
+
raise RuntimeError("tier-docparse completed without result")
|
|
157
|
+
return result
|
|
158
|
+
if status == "failed":
|
|
159
|
+
error = status_payload.get("error") or {}
|
|
160
|
+
message = error.get("message") if isinstance(error, dict) else None
|
|
161
|
+
raise RuntimeError(f"tier-docparse job failed: {message or 'unknown error'}")
|
|
162
|
+
|
|
163
|
+
retry_after = response.headers.get("Retry-After")
|
|
164
|
+
try:
|
|
165
|
+
sleep_for = float(retry_after) if retry_after else 2.0
|
|
166
|
+
except ValueError:
|
|
167
|
+
sleep_for = 2.0
|
|
168
|
+
await asyncio.sleep(max(0.2, min(sleep_for, 5.0)))
|
|
169
|
+
|
|
170
|
+
raise TimeoutError(f"tier-docparse job timed out after {timeout_seconds:g}s")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _status_request_url(client: httpx.AsyncClient, status_url: str) -> str:
|
|
174
|
+
if status_url.startswith(("http://", "https://")):
|
|
175
|
+
return status_url
|
|
176
|
+
if status_url.startswith("/"):
|
|
177
|
+
return str(client.base_url.copy_with(path=status_url, query=None, fragment=None))
|
|
178
|
+
return status_url
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Minimal local DOCX parser for Ingestor 2.0."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from xml.etree import ElementTree
|
|
8
|
+
|
|
9
|
+
from core.api.config import settings
|
|
10
|
+
from core.api.services.ingest.parsers.internal_markdown import MarkdownParseResult
|
|
11
|
+
|
|
12
|
+
DOCX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
13
|
+
WORD_NS = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_docx(path: Path) -> MarkdownParseResult:
|
|
17
|
+
"""Extract paragraphs and tables from an Office Open XML document.
|
|
18
|
+
|
|
19
|
+
This deliberately avoids a broad conversion dependency. It is enough for
|
|
20
|
+
classification evidence and triage preview, while legacy binary `.doc`
|
|
21
|
+
remains unsupported.
|
|
22
|
+
"""
|
|
23
|
+
size = path.stat().st_size
|
|
24
|
+
if size > int(settings.ingest_docx_max_bytes):
|
|
25
|
+
raise ValueError(f"DOCX file too large: {size} bytes")
|
|
26
|
+
if path.suffix.lower() != ".docx":
|
|
27
|
+
raise ValueError(f"Unsupported Word extension: {path.suffix or '<none>'}")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with zipfile.ZipFile(path) as archive:
|
|
31
|
+
document_xml = archive.read("word/document.xml")
|
|
32
|
+
except KeyError as exc:
|
|
33
|
+
raise ValueError("Invalid DOCX: word/document.xml missing") from exc
|
|
34
|
+
except zipfile.BadZipFile as exc:
|
|
35
|
+
raise ValueError("Invalid DOCX container") from exc
|
|
36
|
+
|
|
37
|
+
root = ElementTree.fromstring(document_xml)
|
|
38
|
+
paragraphs: list[str] = []
|
|
39
|
+
tables: list[list[list[str]]] = []
|
|
40
|
+
|
|
41
|
+
for child in root.findall(".//w:body/*", WORD_NS):
|
|
42
|
+
tag = _local_name(child.tag)
|
|
43
|
+
if tag == "p":
|
|
44
|
+
text = _paragraph_text(child)
|
|
45
|
+
if text:
|
|
46
|
+
paragraphs.append(text)
|
|
47
|
+
elif tag == "tbl":
|
|
48
|
+
table = _table_rows(child)
|
|
49
|
+
if table:
|
|
50
|
+
tables.append(table)
|
|
51
|
+
|
|
52
|
+
markdown = _to_markdown(path, paragraphs, tables)
|
|
53
|
+
return MarkdownParseResult(
|
|
54
|
+
frontmatter=_frontmatter(path, markdown),
|
|
55
|
+
text=markdown,
|
|
56
|
+
structure={
|
|
57
|
+
"kind": "docx",
|
|
58
|
+
"bytes": size,
|
|
59
|
+
"paragraph_count": len(paragraphs),
|
|
60
|
+
"table_count": len(tables),
|
|
61
|
+
"table_cell_count": sum(len(row) for table in tables for row in table),
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _paragraph_text(node: ElementTree.Element) -> str:
|
|
67
|
+
return "".join(
|
|
68
|
+
text_node.text or ""
|
|
69
|
+
for text_node in node.findall(".//w:t", WORD_NS)
|
|
70
|
+
if text_node.text
|
|
71
|
+
).strip()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _table_rows(node: ElementTree.Element) -> list[list[str]]:
|
|
75
|
+
rows: list[list[str]] = []
|
|
76
|
+
for row in node.findall(".//w:tr", WORD_NS):
|
|
77
|
+
cells = [_paragraph_text(cell) for cell in row.findall("./w:tc", WORD_NS)]
|
|
78
|
+
if any(cells):
|
|
79
|
+
rows.append(cells)
|
|
80
|
+
return rows
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _to_markdown(path: Path, paragraphs: list[str], tables: list[list[list[str]]]) -> str:
|
|
84
|
+
parts = [f"# {path.stem}"]
|
|
85
|
+
for paragraph in paragraphs:
|
|
86
|
+
parts.extend(["", paragraph])
|
|
87
|
+
for index, table in enumerate(tables, start=1):
|
|
88
|
+
parts.extend(["", f"## Table {index}", "", *_table_markdown(table)])
|
|
89
|
+
return "\n".join(parts).strip() + "\n"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _table_markdown(rows: list[list[str]]) -> list[str]:
|
|
93
|
+
if not rows:
|
|
94
|
+
return []
|
|
95
|
+
width = max(len(row) for row in rows)
|
|
96
|
+
normalized = [row + [""] * (width - len(row)) for row in rows]
|
|
97
|
+
header = normalized[0]
|
|
98
|
+
body = normalized[1:] or [[""] * width]
|
|
99
|
+
out = [
|
|
100
|
+
"| " + " | ".join(_cell(cell) for cell in header) + " |",
|
|
101
|
+
"| " + " | ".join("---" for _ in header) + " |",
|
|
102
|
+
]
|
|
103
|
+
for row in body:
|
|
104
|
+
out.append("| " + " | ".join(_cell(cell) for cell in row) + " |")
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _cell(value: str) -> str:
|
|
109
|
+
return (value or "").replace("|", "\\|").strip()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _frontmatter(path: Path, markdown: str) -> dict[str, Any]:
|
|
113
|
+
lowered = markdown.lower()
|
|
114
|
+
tags = ["docx"]
|
|
115
|
+
doc_type = "file"
|
|
116
|
+
if any(term in lowered for term in {"contratto", "agreement", "clausola", "firma"}):
|
|
117
|
+
doc_type = "contract"
|
|
118
|
+
tags.append("contract")
|
|
119
|
+
return {
|
|
120
|
+
"type": doc_type,
|
|
121
|
+
"title": path.stem,
|
|
122
|
+
"tags": tags,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _local_name(tag: str) -> str:
|
|
127
|
+
return tag.rsplit("}", 1)[-1]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Folder traversal helpers for Universal Ingestion containers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
|
|
7
|
+
from core.api.services.ingest.ignore_patterns import (
|
|
8
|
+
is_ignored_directory_name,
|
|
9
|
+
should_ignore,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FolderFile:
|
|
15
|
+
path: Path
|
|
16
|
+
relative_path: PurePosixPath
|
|
17
|
+
size_bytes: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class FolderSkipped:
|
|
22
|
+
path: str
|
|
23
|
+
reason: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FolderScanResult:
|
|
28
|
+
files: list[FolderFile]
|
|
29
|
+
skipped: list[FolderSkipped]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def scan_folder_files(root: Path, *, max_depth: int = 5) -> FolderScanResult:
|
|
33
|
+
root = root.resolve()
|
|
34
|
+
files: list[FolderFile] = []
|
|
35
|
+
skipped: list[FolderSkipped] = []
|
|
36
|
+
visited_dirs: set[Path] = set()
|
|
37
|
+
|
|
38
|
+
def walk(directory: Path, rel_dir: PurePosixPath, depth: int) -> None:
|
|
39
|
+
if depth > max_depth:
|
|
40
|
+
skipped.append(FolderSkipped(str(rel_dir), "max-depth"))
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
resolved_dir = directory.resolve()
|
|
44
|
+
except OSError:
|
|
45
|
+
skipped.append(FolderSkipped(str(rel_dir), "unreadable"))
|
|
46
|
+
return
|
|
47
|
+
if resolved_dir in visited_dirs:
|
|
48
|
+
skipped.append(FolderSkipped(str(rel_dir), "symlink-loop"))
|
|
49
|
+
return
|
|
50
|
+
visited_dirs.add(resolved_dir)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
children = sorted(directory.iterdir(), key=lambda item: item.name.casefold())
|
|
54
|
+
except OSError:
|
|
55
|
+
skipped.append(FolderSkipped(str(rel_dir), "unreadable"))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
for child in children:
|
|
59
|
+
rel_path = rel_dir / child.name
|
|
60
|
+
if child.is_symlink():
|
|
61
|
+
skipped.append(FolderSkipped(str(rel_path), "symlink"))
|
|
62
|
+
continue
|
|
63
|
+
if child.is_dir():
|
|
64
|
+
if is_ignored_directory_name(child.name):
|
|
65
|
+
skipped.append(FolderSkipped(str(rel_path), "ignored-directory"))
|
|
66
|
+
continue
|
|
67
|
+
walk(child, rel_path, depth + 1)
|
|
68
|
+
continue
|
|
69
|
+
if not child.is_file():
|
|
70
|
+
skipped.append(FolderSkipped(str(rel_path), "not-file"))
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
size_bytes = child.stat().st_size
|
|
75
|
+
except OSError:
|
|
76
|
+
skipped.append(FolderSkipped(str(rel_path), "unreadable"))
|
|
77
|
+
continue
|
|
78
|
+
ignore_reason = should_ignore(rel_path, file_size_bytes=size_bytes)
|
|
79
|
+
if ignore_reason:
|
|
80
|
+
skipped.append(FolderSkipped(str(rel_path), ignore_reason))
|
|
81
|
+
continue
|
|
82
|
+
files.append(FolderFile(child, rel_path, size_bytes))
|
|
83
|
+
|
|
84
|
+
walk(root, PurePosixPath("."), 0)
|
|
85
|
+
return FolderScanResult(files=files, skipped=skipped)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shared helpers for Mac Gateway AUX endpoints used by ingest parsers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from email.utils import parsedate_to_datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from core.api import config as _config
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
RETRYABLE_STATUS_CODES = {408, 409, 425, 429, 500, 502, 503, 504}
|
|
18
|
+
RETRY_AFTER_CAP_SECONDS = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MissingGatewayConfig(RuntimeError):
|
|
22
|
+
"""Raised when AUX endpoints are not configured in this environment."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def settings():
|
|
26
|
+
return _config.settings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def secret_value(value: Any) -> str:
|
|
30
|
+
if hasattr(value, "get_secret_value"):
|
|
31
|
+
return str(value.get_secret_value())
|
|
32
|
+
return str(value or "")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def gateway_api_key() -> str:
|
|
36
|
+
cfg = settings()
|
|
37
|
+
if getattr(cfg, "pir_env", "") == "test":
|
|
38
|
+
raise MissingGatewayConfig("AUX gateway is disabled under PIR_ENV=test")
|
|
39
|
+
api_key = secret_value(getattr(cfg, "ingest_llm_gateway_api_key", None))
|
|
40
|
+
if not api_key:
|
|
41
|
+
raise MissingGatewayConfig("INGEST_LLM_GATEWAY_API_KEY is not configured")
|
|
42
|
+
return api_key
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def gateway_agent_name() -> str:
|
|
46
|
+
cfg = settings()
|
|
47
|
+
return str(getattr(cfg, "ingest_llm_gateway_agent_name", "") or "marvisx-ingester")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def gateway_priority() -> str:
|
|
51
|
+
cfg = settings()
|
|
52
|
+
priority = str(getattr(cfg, "ingest_llm_gateway_priority", "") or "batch").strip()
|
|
53
|
+
return priority if priority in {"interactive", "batch", "background"} else "batch"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def aux_base_url() -> str:
|
|
57
|
+
cfg = settings()
|
|
58
|
+
if getattr(cfg, "pir_env", "") == "test":
|
|
59
|
+
raise MissingGatewayConfig("AUX gateway is disabled under PIR_ENV=test")
|
|
60
|
+
base_url = cfg.llm_gateway_aux_base_url or cfg.llm_gateway_base_url
|
|
61
|
+
if not base_url:
|
|
62
|
+
raise MissingGatewayConfig("LLM_GATEWAY_BASE_URL is not configured")
|
|
63
|
+
|
|
64
|
+
base_url = base_url.rstrip("/")
|
|
65
|
+
if cfg.llm_gateway_aux_base_url:
|
|
66
|
+
return base_url
|
|
67
|
+
|
|
68
|
+
# Production chat calls use LiteLLM on :4000. The ASR/OCR AUX proxy lives on
|
|
69
|
+
# :9100 on the same Mac; public CF URLs already route by path and stay as-is.
|
|
70
|
+
if base_url.startswith("http://100.103.221.55:4000"):
|
|
71
|
+
return base_url.replace(":4000", ":9100", 1)
|
|
72
|
+
return base_url
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def auth_headers() -> dict[str, str]:
|
|
76
|
+
return {
|
|
77
|
+
"Authorization": f"Bearer {gateway_api_key()}",
|
|
78
|
+
"X-Agent-Name": gateway_agent_name(),
|
|
79
|
+
"X-Priority": gateway_priority(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_retryable_gateway_response(response: httpx.Response) -> bool:
|
|
84
|
+
return response.status_code in RETRYABLE_STATUS_CODES
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def retry_delay_seconds(response: httpx.Response | None, attempt: int) -> float:
|
|
88
|
+
retry_after = response.headers.get("Retry-After") if response is not None else None
|
|
89
|
+
if retry_after:
|
|
90
|
+
try:
|
|
91
|
+
return min(max(float(retry_after), 0.0), RETRY_AFTER_CAP_SECONDS)
|
|
92
|
+
except ValueError:
|
|
93
|
+
try:
|
|
94
|
+
parsed = parsedate_to_datetime(retry_after)
|
|
95
|
+
if parsed.tzinfo is None:
|
|
96
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
97
|
+
delta = (parsed - datetime.now(timezone.utc)).total_seconds()
|
|
98
|
+
return min(max(delta, 0.0), RETRY_AFTER_CAP_SECONDS)
|
|
99
|
+
except (TypeError, ValueError):
|
|
100
|
+
pass
|
|
101
|
+
return min(0.5 * attempt, 2.0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def request_gateway_with_retries(
|
|
105
|
+
operation: Callable[[], Awaitable[httpx.Response]],
|
|
106
|
+
*,
|
|
107
|
+
service_name: str,
|
|
108
|
+
max_attempts: int = 3,
|
|
109
|
+
) -> httpx.Response:
|
|
110
|
+
for attempt in range(1, max_attempts + 1):
|
|
111
|
+
response: httpx.Response | None = None
|
|
112
|
+
try:
|
|
113
|
+
response = await operation()
|
|
114
|
+
except (
|
|
115
|
+
httpx.ConnectError,
|
|
116
|
+
httpx.ConnectTimeout,
|
|
117
|
+
httpx.PoolTimeout,
|
|
118
|
+
httpx.ReadTimeout,
|
|
119
|
+
httpx.RemoteProtocolError,
|
|
120
|
+
httpx.WriteError,
|
|
121
|
+
httpx.WriteTimeout,
|
|
122
|
+
) as exc:
|
|
123
|
+
if attempt == max_attempts:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
f"{service_name} unavailable after {max_attempts} attempts"
|
|
126
|
+
) from exc
|
|
127
|
+
logger.warning(
|
|
128
|
+
"%s transient request error on attempt %s/%s: %s",
|
|
129
|
+
service_name,
|
|
130
|
+
attempt,
|
|
131
|
+
max_attempts,
|
|
132
|
+
exc.__class__.__name__,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
if not is_retryable_gateway_response(response) or attempt == max_attempts:
|
|
136
|
+
return response
|
|
137
|
+
logger.warning(
|
|
138
|
+
"%s transient HTTP %s on attempt %s/%s",
|
|
139
|
+
service_name,
|
|
140
|
+
response.status_code,
|
|
141
|
+
attempt,
|
|
142
|
+
max_attempts,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
await asyncio.sleep(retry_delay_seconds(response, attempt))
|
|
146
|
+
|
|
147
|
+
raise RuntimeError(f"{service_name} retry loop exited unexpectedly")
|