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,272 @@
|
|
|
1
|
+
"""Embedding routing for Universal Ingestion E2.1."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import aiosqlite
|
|
13
|
+
|
|
14
|
+
from core.api.config import settings
|
|
15
|
+
from core.api.db import acquire_write_db
|
|
16
|
+
from core.api.services import embedding_service
|
|
17
|
+
from core.api.services.pii_redactor import redact
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
EmbedStatus = Literal["voyage", "nomic-local", "skipped"]
|
|
22
|
+
ProjectType = Literal["work", "code", "system"]
|
|
23
|
+
NOMIC_MODEL_PATH = Path(
|
|
24
|
+
os.environ.get("NOMIC_MODEL_PATH", "/data/pir/models/nomic-embed-text-v1.5")
|
|
25
|
+
)
|
|
26
|
+
_LOCAL_MODEL_MISSING_LOGGED = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def embed_and_index(
|
|
30
|
+
*,
|
|
31
|
+
ingest_id: str,
|
|
32
|
+
slug: str,
|
|
33
|
+
target_path: Path,
|
|
34
|
+
extracted_text: str | None,
|
|
35
|
+
document_type: str = "file",
|
|
36
|
+
title: str | None = None,
|
|
37
|
+
project_type: ProjectType = "work",
|
|
38
|
+
) -> EmbedStatus:
|
|
39
|
+
"""Redact, embed, and index one ingested file.
|
|
40
|
+
|
|
41
|
+
Work projects skip embeddings unless external embedding is explicitly
|
|
42
|
+
enabled. Code/system projects use local Nomic by default and route to Voyage
|
|
43
|
+
only through migration 095's project opt-in table.
|
|
44
|
+
"""
|
|
45
|
+
if not extracted_text or len(extracted_text.strip()) < 20:
|
|
46
|
+
logger.info(
|
|
47
|
+
"embedding backend: skipped ingest_id=%s reason=text-too-short",
|
|
48
|
+
ingest_id,
|
|
49
|
+
)
|
|
50
|
+
return "skipped"
|
|
51
|
+
|
|
52
|
+
allow_external = await _project_allows_external_embedding(slug)
|
|
53
|
+
if project_type == "work" and not allow_external:
|
|
54
|
+
logger.info(
|
|
55
|
+
"embedding backend: skipped ingest_id=%s reason=work-no-opt-in",
|
|
56
|
+
ingest_id,
|
|
57
|
+
)
|
|
58
|
+
return "skipped"
|
|
59
|
+
|
|
60
|
+
redacted_text = redact(extracted_text)
|
|
61
|
+
backend: Literal["voyage", "nomic-local"]
|
|
62
|
+
try:
|
|
63
|
+
if allow_external:
|
|
64
|
+
vector = await _embed_voyage_text(redacted_text)
|
|
65
|
+
backend = "voyage"
|
|
66
|
+
else:
|
|
67
|
+
if not _local_embedding_model_available():
|
|
68
|
+
_log_local_embedding_missing_once("local-model-missing")
|
|
69
|
+
logger.info(
|
|
70
|
+
"embedding backend: skipped ingest_id=%s reason=local-model-missing",
|
|
71
|
+
ingest_id,
|
|
72
|
+
)
|
|
73
|
+
return "skipped"
|
|
74
|
+
vector = await asyncio.to_thread(_embed_nomic_text_sync, redacted_text)
|
|
75
|
+
backend = "nomic-local"
|
|
76
|
+
except Exception:
|
|
77
|
+
if allow_external:
|
|
78
|
+
logger.warning(
|
|
79
|
+
"Voyage embedding failed; falling back to nomic-local", exc_info=True
|
|
80
|
+
)
|
|
81
|
+
if not _local_embedding_model_available():
|
|
82
|
+
_log_local_embedding_missing_once("local-fallback-model-missing")
|
|
83
|
+
logger.info(
|
|
84
|
+
"embedding backend: skipped ingest_id=%s reason=local-fallback-model-missing",
|
|
85
|
+
ingest_id,
|
|
86
|
+
)
|
|
87
|
+
return "skipped"
|
|
88
|
+
try:
|
|
89
|
+
vector = await asyncio.to_thread(_embed_nomic_text_sync, redacted_text)
|
|
90
|
+
backend = "nomic-local"
|
|
91
|
+
except Exception:
|
|
92
|
+
logger.warning(
|
|
93
|
+
"Local embedding fallback failed; skipping index", exc_info=True
|
|
94
|
+
)
|
|
95
|
+
return "skipped"
|
|
96
|
+
else:
|
|
97
|
+
logger.warning("Local embedding unavailable; skipping index", exc_info=True)
|
|
98
|
+
return "skipped"
|
|
99
|
+
|
|
100
|
+
await _persist_ingest_embedding(
|
|
101
|
+
slug=slug,
|
|
102
|
+
target_path=target_path,
|
|
103
|
+
document_type=document_type,
|
|
104
|
+
title=title or target_path.stem,
|
|
105
|
+
content=extracted_text,
|
|
106
|
+
vector=_coerce_dimensions(vector),
|
|
107
|
+
)
|
|
108
|
+
logger.info(
|
|
109
|
+
"embedding backend: %s ingest_id=%s project=%s", backend, ingest_id, slug
|
|
110
|
+
)
|
|
111
|
+
return backend
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _project_allows_external_embedding(slug: str) -> bool:
|
|
115
|
+
try:
|
|
116
|
+
async with acquire_write_db() as db:
|
|
117
|
+
async with db.execute(
|
|
118
|
+
"""
|
|
119
|
+
SELECT allow_external_embed
|
|
120
|
+
FROM project_external_embedding_policy
|
|
121
|
+
WHERE project_slug = ?
|
|
122
|
+
""",
|
|
123
|
+
(slug,),
|
|
124
|
+
) as cursor:
|
|
125
|
+
row = await cursor.fetchone()
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"external embedding policy unavailable; defaulting to local", exc_info=True
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
return bool(row and row["allow_external_embed"])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def _embed_voyage_text(text: str) -> list[float]:
|
|
135
|
+
if not embedding_service.is_available():
|
|
136
|
+
embedding_service.init_voyage_client()
|
|
137
|
+
if not embedding_service.is_available():
|
|
138
|
+
raise RuntimeError("Voyage AI client not initialized")
|
|
139
|
+
|
|
140
|
+
last_error: Exception | None = None
|
|
141
|
+
for attempt, delay in enumerate((0.0, 0.4, 1.5, 4.0), start=1):
|
|
142
|
+
if delay:
|
|
143
|
+
await asyncio.sleep(delay)
|
|
144
|
+
try:
|
|
145
|
+
vectors = await embedding_service.embed_texts([text], input_type="document")
|
|
146
|
+
return vectors[0]
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
last_error = exc
|
|
149
|
+
message = str(exc)
|
|
150
|
+
if any(code in message for code in ("400", "401", "403", "422")):
|
|
151
|
+
raise
|
|
152
|
+
logger.warning("Voyage attempt %d failed: %s", attempt, exc)
|
|
153
|
+
assert last_error is not None
|
|
154
|
+
raise last_error
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _embed_nomic_text_sync(text: str) -> list[float]:
|
|
158
|
+
model = _nomic_model()
|
|
159
|
+
try:
|
|
160
|
+
vector = model.encode(
|
|
161
|
+
text,
|
|
162
|
+
normalize_embeddings=True,
|
|
163
|
+
truncate_dim=embedding_service.DIMENSIONS,
|
|
164
|
+
)
|
|
165
|
+
except TypeError:
|
|
166
|
+
vector = model.encode(text, normalize_embeddings=True)
|
|
167
|
+
return vector.tolist()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _local_embedding_model_available() -> bool:
|
|
171
|
+
return NOMIC_MODEL_PATH.exists()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _log_local_embedding_missing_once(reason: str) -> None:
|
|
175
|
+
global _LOCAL_MODEL_MISSING_LOGGED
|
|
176
|
+
if _LOCAL_MODEL_MISSING_LOGGED:
|
|
177
|
+
return
|
|
178
|
+
_LOCAL_MODEL_MISSING_LOGGED = True
|
|
179
|
+
logger.warning(
|
|
180
|
+
"embedding backend: local embedding disabled reason=%s path=%s",
|
|
181
|
+
reason,
|
|
182
|
+
NOMIC_MODEL_PATH,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@lru_cache(maxsize=1)
|
|
187
|
+
def _nomic_model():
|
|
188
|
+
from sentence_transformers import SentenceTransformer
|
|
189
|
+
|
|
190
|
+
return SentenceTransformer(str(NOMIC_MODEL_PATH), trust_remote_code=True)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _coerce_dimensions(vector: list[float]) -> list[float]:
|
|
194
|
+
if len(vector) == embedding_service.DIMENSIONS:
|
|
195
|
+
return vector
|
|
196
|
+
if len(vector) > embedding_service.DIMENSIONS:
|
|
197
|
+
return vector[: embedding_service.DIMENSIONS]
|
|
198
|
+
padding = [0.0] * (embedding_service.DIMENSIONS - len(vector))
|
|
199
|
+
return [*vector, *padding]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def _persist_ingest_embedding(
|
|
203
|
+
*,
|
|
204
|
+
slug: str,
|
|
205
|
+
target_path: Path,
|
|
206
|
+
document_type: str,
|
|
207
|
+
title: str,
|
|
208
|
+
content: str,
|
|
209
|
+
vector: list[float],
|
|
210
|
+
) -> None:
|
|
211
|
+
file_path = str(target_path)
|
|
212
|
+
async with acquire_write_db() as db:
|
|
213
|
+
await db.execute(
|
|
214
|
+
"""INSERT INTO documents (file_path, project, workspace_id, doc_type, doc_title, content_hash)
|
|
215
|
+
VALUES (?, ?, 'ws_default', ?, ?, ?)
|
|
216
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
217
|
+
content_hash = excluded.content_hash,
|
|
218
|
+
project = excluded.project,
|
|
219
|
+
workspace_id = excluded.workspace_id,
|
|
220
|
+
doc_type = excluded.doc_type,
|
|
221
|
+
doc_title = excluded.doc_title""",
|
|
222
|
+
[
|
|
223
|
+
file_path,
|
|
224
|
+
slug,
|
|
225
|
+
document_type,
|
|
226
|
+
title,
|
|
227
|
+
embedding_service.content_hash(content),
|
|
228
|
+
],
|
|
229
|
+
)
|
|
230
|
+
async with db.execute(
|
|
231
|
+
"SELECT id FROM documents WHERE file_path = ?", [file_path]
|
|
232
|
+
) as cursor:
|
|
233
|
+
row = await cursor.fetchone()
|
|
234
|
+
if row is None:
|
|
235
|
+
raise RuntimeError(f"document upsert failed for {file_path}")
|
|
236
|
+
|
|
237
|
+
if await _ensure_vec_documents(db):
|
|
238
|
+
await db.execute("DELETE FROM vec_documents WHERE doc_id = ?", [row["id"]])
|
|
239
|
+
await db.execute(
|
|
240
|
+
"INSERT INTO vec_documents (doc_id, embedding) VALUES (?, ?)",
|
|
241
|
+
[row["id"], embedding_service.serialize_f32(vector)],
|
|
242
|
+
)
|
|
243
|
+
await db.commit()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def _ensure_vec_documents(db: aiosqlite.Connection) -> bool:
|
|
247
|
+
vec_path = Path(settings.vec0_path)
|
|
248
|
+
vec_so = vec_path.with_suffix(".so") if not vec_path.suffix else vec_path
|
|
249
|
+
if not vec_so.exists():
|
|
250
|
+
logger.warning(
|
|
251
|
+
"sqlite-vec extension missing at %s; document metadata stored without vector",
|
|
252
|
+
vec_so,
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
try:
|
|
256
|
+
await db._execute(db._conn.enable_load_extension, True)
|
|
257
|
+
await db.execute("SELECT load_extension(?)", [str(vec_path)])
|
|
258
|
+
await db.execute(
|
|
259
|
+
"""
|
|
260
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0(
|
|
261
|
+
doc_id INTEGER PRIMARY KEY,
|
|
262
|
+
embedding float[512]
|
|
263
|
+
)
|
|
264
|
+
"""
|
|
265
|
+
)
|
|
266
|
+
return True
|
|
267
|
+
except Exception:
|
|
268
|
+
logger.warning(
|
|
269
|
+
"sqlite-vec setup failed; document metadata stored without vector",
|
|
270
|
+
exc_info=True,
|
|
271
|
+
)
|
|
272
|
+
return False
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Best-effort websocket notifications for ingest state changes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def broadcast_ingest_changed(
|
|
11
|
+
event: str,
|
|
12
|
+
*,
|
|
13
|
+
ingest_id: str | None = None,
|
|
14
|
+
project_slug: str | None = None,
|
|
15
|
+
status: str | None = None,
|
|
16
|
+
extra: dict[str, Any] | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
payload: dict[str, Any] = {"type": "ingest_changed", "event": event}
|
|
19
|
+
if ingest_id is not None:
|
|
20
|
+
payload["ingest_id"] = ingest_id
|
|
21
|
+
if project_slug is not None:
|
|
22
|
+
payload["project_slug"] = project_slug
|
|
23
|
+
if status is not None:
|
|
24
|
+
payload["status"] = status
|
|
25
|
+
if extra:
|
|
26
|
+
payload.update(extra)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from core.api.terminal import session_manager
|
|
30
|
+
|
|
31
|
+
await session_manager.broadcast_control_message(payload)
|
|
32
|
+
except Exception:
|
|
33
|
+
logger.exception("failed to broadcast ingest_changed event")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared ignore rules for container ingest inputs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
|
|
6
|
+
MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
IGNORED_DIRECTORY_NAMES = {
|
|
9
|
+
".git",
|
|
10
|
+
".hg",
|
|
11
|
+
".svn",
|
|
12
|
+
".venv",
|
|
13
|
+
"__pycache__",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
"dist",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"tmp",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
IGNORED_FILE_NAMES = {
|
|
22
|
+
".DS_Store",
|
|
23
|
+
"Thumbs.db",
|
|
24
|
+
"desktop.ini",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
IGNORED_SUFFIXES = {
|
|
28
|
+
".app",
|
|
29
|
+
".bat",
|
|
30
|
+
".bin",
|
|
31
|
+
".cmd",
|
|
32
|
+
".com",
|
|
33
|
+
".dll",
|
|
34
|
+
".dmg",
|
|
35
|
+
".exe",
|
|
36
|
+
".iso",
|
|
37
|
+
".lnk",
|
|
38
|
+
".msi",
|
|
39
|
+
".o",
|
|
40
|
+
".out",
|
|
41
|
+
".part",
|
|
42
|
+
".pyc",
|
|
43
|
+
".so",
|
|
44
|
+
".swp",
|
|
45
|
+
".tmp",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalize_relative_path(path: str | PurePosixPath) -> PurePosixPath:
|
|
50
|
+
normalized = str(path).replace("\\", "/")
|
|
51
|
+
return PurePosixPath(normalized)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_ignored_directory_name(name: str) -> bool:
|
|
55
|
+
return name in IGNORED_DIRECTORY_NAMES
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def should_ignore(
|
|
59
|
+
relative_path: str | PurePosixPath,
|
|
60
|
+
*,
|
|
61
|
+
file_size_bytes: int | None = None,
|
|
62
|
+
) -> str | None:
|
|
63
|
+
path = normalize_relative_path(relative_path)
|
|
64
|
+
parts = [part for part in path.parts if part not in {"", "."}]
|
|
65
|
+
if not parts:
|
|
66
|
+
return "empty-path"
|
|
67
|
+
if any(part == ".." for part in parts):
|
|
68
|
+
return "path-traversal"
|
|
69
|
+
if any(is_ignored_directory_name(part) for part in parts[:-1]):
|
|
70
|
+
return "ignored-directory"
|
|
71
|
+
|
|
72
|
+
filename = parts[-1]
|
|
73
|
+
if filename in IGNORED_FILE_NAMES or filename.startswith("."):
|
|
74
|
+
return "ignored-file"
|
|
75
|
+
if PurePosixPath(filename).suffix.lower() in IGNORED_SUFFIXES:
|
|
76
|
+
return "ignored-suffix"
|
|
77
|
+
if file_size_bytes is not None and file_size_bytes > MAX_FILE_SIZE_BYTES:
|
|
78
|
+
return "file-too-large"
|
|
79
|
+
return None
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Lightweight image evidence for ingest routing decisions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from statistics import mean, pstdev
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
BILL_TERMS = {"bolletta", "fattura", "invoice", "pod", "pdr", "kwh", "energia", "gas"}
|
|
11
|
+
IDENTITY_TERMS = {
|
|
12
|
+
"carta-identita",
|
|
13
|
+
"carta_identita",
|
|
14
|
+
"identity",
|
|
15
|
+
"passport",
|
|
16
|
+
"passaporto",
|
|
17
|
+
"codice-fiscale",
|
|
18
|
+
}
|
|
19
|
+
DOCUMENT_TERMS = {"contratto", "contract", "report", "modulo", "form", "documento"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def probe_image(path: Path) -> dict[str, Any]:
|
|
23
|
+
"""Return cheap, auditable image signals without OCR or vision calls."""
|
|
24
|
+
try:
|
|
25
|
+
with Image.open(path) as image:
|
|
26
|
+
exif = image.getexif()
|
|
27
|
+
width = int(image.width)
|
|
28
|
+
height = int(image.height)
|
|
29
|
+
orientation = exif.get(0x0112) if exif else None
|
|
30
|
+
camera_hint = bool(exif.get(0x010F) or exif.get(0x0110)) if exif else False
|
|
31
|
+
stats = _pixel_stats(image)
|
|
32
|
+
aspect_ratio = round(width / height, 4) if height else None
|
|
33
|
+
suffix = path.suffix.lower()
|
|
34
|
+
name_score = _filename_document_score(path)
|
|
35
|
+
screenshot_hint = _screenshot_hint(
|
|
36
|
+
suffix=suffix,
|
|
37
|
+
width=width,
|
|
38
|
+
height=height,
|
|
39
|
+
camera_hint=camera_hint,
|
|
40
|
+
)
|
|
41
|
+
document_boundary_hint = _document_boundary_hint(width, height)
|
|
42
|
+
poor_capture_hint = _poor_capture_hint(width, height, orientation)
|
|
43
|
+
|
|
44
|
+
document_likelihood = _clamp(
|
|
45
|
+
0.12
|
|
46
|
+
+ (0.35 if document_boundary_hint else 0.0)
|
|
47
|
+
+ (0.18 if stats["white_ratio"] >= 0.45 else 0.0)
|
|
48
|
+
+ (0.16 if stats["edge_density"] >= 0.08 else 0.0)
|
|
49
|
+
+ name_score
|
|
50
|
+
- (0.18 if camera_hint and stats["white_ratio"] < 0.35 else 0.0)
|
|
51
|
+
)
|
|
52
|
+
screenshot_likelihood = _clamp(
|
|
53
|
+
0.10
|
|
54
|
+
+ (0.45 if screenshot_hint else 0.0)
|
|
55
|
+
+ (0.18 if width > height else 0.0)
|
|
56
|
+
+ (0.14 if stats["edge_density"] >= 0.10 else 0.0)
|
|
57
|
+
- (0.28 if camera_hint else 0.0)
|
|
58
|
+
)
|
|
59
|
+
photo_likelihood = _clamp(
|
|
60
|
+
0.18
|
|
61
|
+
+ (0.42 if camera_hint else 0.0)
|
|
62
|
+
+ (0.18 if stats["white_ratio"] < 0.25 else 0.0)
|
|
63
|
+
+ (0.10 if stats["contrast"] > 55 else 0.0)
|
|
64
|
+
- (0.20 if screenshot_hint else 0.0)
|
|
65
|
+
)
|
|
66
|
+
text_likelihood = _clamp(
|
|
67
|
+
0.20
|
|
68
|
+
+ (0.35 if stats["edge_density"] >= 0.10 else 0.0)
|
|
69
|
+
+ (0.18 if stats["contrast"] >= 35 else 0.0)
|
|
70
|
+
+ (0.15 if document_boundary_hint else 0.0)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"kind": "image",
|
|
75
|
+
"format": image.format,
|
|
76
|
+
"width": width,
|
|
77
|
+
"height": height,
|
|
78
|
+
"aspect_ratio": aspect_ratio,
|
|
79
|
+
"orientation": orientation,
|
|
80
|
+
"exif_camera_hint": camera_hint,
|
|
81
|
+
"exif_tag_count": len(exif or {}),
|
|
82
|
+
"screenshot_hint": screenshot_hint,
|
|
83
|
+
"document_boundary_hint": document_boundary_hint,
|
|
84
|
+
"poor_capture_hint": poor_capture_hint,
|
|
85
|
+
"white_background_ratio": stats["white_ratio"],
|
|
86
|
+
"edge_density": stats["edge_density"],
|
|
87
|
+
"brightness": stats["brightness"],
|
|
88
|
+
"contrast": stats["contrast"],
|
|
89
|
+
"document_likelihood": round(document_likelihood, 4),
|
|
90
|
+
"screenshot_likelihood": round(screenshot_likelihood, 4),
|
|
91
|
+
"photo_likelihood": round(photo_likelihood, 4),
|
|
92
|
+
"text_likelihood": round(text_likelihood, 4),
|
|
93
|
+
"image_kind": _image_kind(
|
|
94
|
+
document_likelihood=document_likelihood,
|
|
95
|
+
screenshot_likelihood=screenshot_likelihood,
|
|
96
|
+
photo_likelihood=photo_likelihood,
|
|
97
|
+
),
|
|
98
|
+
"signals": _signals(
|
|
99
|
+
screenshot_hint=screenshot_hint,
|
|
100
|
+
document_boundary_hint=document_boundary_hint,
|
|
101
|
+
poor_capture_hint=poor_capture_hint,
|
|
102
|
+
camera_hint=camera_hint,
|
|
103
|
+
name_score=name_score,
|
|
104
|
+
stats=stats,
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
except Exception:
|
|
108
|
+
return {"kind": "image", "preflight_error": "image_probe_failed"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _pixel_stats(image: Image.Image) -> dict[str, float]:
|
|
112
|
+
sample = image.convert("L")
|
|
113
|
+
sample.thumbnail((160, 160))
|
|
114
|
+
width, height = sample.size
|
|
115
|
+
pixels = list(sample.tobytes())
|
|
116
|
+
if not pixels:
|
|
117
|
+
return {"white_ratio": 0.0, "edge_density": 0.0, "brightness": 0.0, "contrast": 0.0}
|
|
118
|
+
|
|
119
|
+
white_ratio = sum(1 for value in pixels if value >= 235) / len(pixels)
|
|
120
|
+
brightness = mean(pixels)
|
|
121
|
+
contrast = pstdev(pixels) if len(pixels) > 1 else 0.0
|
|
122
|
+
edge_hits = 0
|
|
123
|
+
comparisons = 0
|
|
124
|
+
for y in range(height):
|
|
125
|
+
row = y * width
|
|
126
|
+
for x in range(width):
|
|
127
|
+
value = pixels[row + x]
|
|
128
|
+
if x + 1 < width:
|
|
129
|
+
comparisons += 1
|
|
130
|
+
edge_hits += abs(value - pixels[row + x + 1]) >= 32
|
|
131
|
+
if y + 1 < height:
|
|
132
|
+
comparisons += 1
|
|
133
|
+
edge_hits += abs(value - pixels[row + width + x]) >= 32
|
|
134
|
+
edge_density = edge_hits / comparisons if comparisons else 0.0
|
|
135
|
+
return {
|
|
136
|
+
"white_ratio": round(white_ratio, 4),
|
|
137
|
+
"edge_density": round(edge_density, 4),
|
|
138
|
+
"brightness": round(brightness, 2),
|
|
139
|
+
"contrast": round(contrast, 2),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _filename_document_score(path: Path) -> float:
|
|
144
|
+
stem = path.stem.lower()
|
|
145
|
+
if any(term in stem for term in BILL_TERMS | IDENTITY_TERMS):
|
|
146
|
+
return 0.28
|
|
147
|
+
if any(term in stem for term in DOCUMENT_TERMS):
|
|
148
|
+
return 0.18
|
|
149
|
+
return 0.0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _screenshot_hint(*, suffix: str, width: int, height: int, camera_hint: bool) -> bool:
|
|
153
|
+
if camera_hint:
|
|
154
|
+
return False
|
|
155
|
+
if suffix != ".png":
|
|
156
|
+
return False
|
|
157
|
+
return width >= 800 and height >= 500
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _document_boundary_hint(width: int, height: int) -> bool:
|
|
161
|
+
if not width or not height:
|
|
162
|
+
return False
|
|
163
|
+
aspect = width / height
|
|
164
|
+
return 0.55 <= aspect <= 1.9
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _poor_capture_hint(width: int, height: int, orientation: Any) -> bool:
|
|
168
|
+
return width * height < 600_000 or orientation in {3, 6, 8}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _image_kind(
|
|
172
|
+
*,
|
|
173
|
+
document_likelihood: float,
|
|
174
|
+
screenshot_likelihood: float,
|
|
175
|
+
photo_likelihood: float,
|
|
176
|
+
) -> str:
|
|
177
|
+
ranked = sorted(
|
|
178
|
+
[
|
|
179
|
+
("document", document_likelihood),
|
|
180
|
+
("screenshot", screenshot_likelihood),
|
|
181
|
+
("photo", photo_likelihood),
|
|
182
|
+
],
|
|
183
|
+
key=lambda item: item[1],
|
|
184
|
+
reverse=True,
|
|
185
|
+
)
|
|
186
|
+
kind, score = ranked[0]
|
|
187
|
+
return kind if score >= 0.62 else "ambiguous"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _signals(
|
|
191
|
+
*,
|
|
192
|
+
screenshot_hint: bool,
|
|
193
|
+
document_boundary_hint: bool,
|
|
194
|
+
poor_capture_hint: bool,
|
|
195
|
+
camera_hint: bool,
|
|
196
|
+
name_score: float,
|
|
197
|
+
stats: dict[str, float],
|
|
198
|
+
) -> list[str]:
|
|
199
|
+
signals: list[str] = []
|
|
200
|
+
if screenshot_hint:
|
|
201
|
+
signals.append("screenshot_hint")
|
|
202
|
+
if document_boundary_hint:
|
|
203
|
+
signals.append("document_boundary_hint")
|
|
204
|
+
if poor_capture_hint:
|
|
205
|
+
signals.append("poor_capture_hint")
|
|
206
|
+
if camera_hint:
|
|
207
|
+
signals.append("exif_camera_hint")
|
|
208
|
+
if name_score:
|
|
209
|
+
signals.append("filename_document_terms")
|
|
210
|
+
if stats["white_ratio"] >= 0.45:
|
|
211
|
+
signals.append("white_background")
|
|
212
|
+
if stats["edge_density"] >= 0.10:
|
|
213
|
+
signals.append("edge_dense")
|
|
214
|
+
return signals
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _clamp(value: float) -> float:
|
|
218
|
+
return max(0.0, min(1.0, value))
|