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,156 @@
|
|
|
1
|
+
# v2.0.0 - 2026-05-27 - S1 F1.11: thin adapter over use_cases.handoffs
|
|
2
|
+
# v1.3.1 - 2026-04-17 - fix node_id double prefix in deep handoff bundle
|
|
3
|
+
"""HTTP adapter for the handoffs domain (S1 collapse-runtime).
|
|
4
|
+
|
|
5
|
+
This router is a thin transport adapter. All search/read/validation/visibility
|
|
6
|
+
logic lives in :mod:`core.api.use_cases.handoffs` (pure, fastapi-free). Each
|
|
7
|
+
handler:
|
|
8
|
+
|
|
9
|
+
1. resolves identity into a :class:`CallerContext` (handoffs have no human-only
|
|
10
|
+
gate, so ``is_human_session=False`` — no ``Request`` needed);
|
|
11
|
+
2. resolves visibility at the boundary (DECISION 1) and passes it in;
|
|
12
|
+
3. calls the use_case inside ``try/except ServiceError`` -> ``to_http``;
|
|
13
|
+
4. owns the ``deep`` KG enrichment: rate-limit + audit log + ``kg_context``
|
|
14
|
+
(DECISION 2).
|
|
15
|
+
|
|
16
|
+
``get_current_user_or_agent`` stays as the ``Depends`` on ``search_handoffs`` so
|
|
17
|
+
the agent-facing auth regression test (``test_agent_facing_auth_dependencies``)
|
|
18
|
+
keeps passing. The pure helpers + DTOs are re-exported from the use_case so any
|
|
19
|
+
existing importer keeps working.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
import aiosqlite
|
|
26
|
+
from fastapi import APIRouter, Depends, Query
|
|
27
|
+
|
|
28
|
+
from core.api.db import get_db, get_vec_db
|
|
29
|
+
from core.api.models import HandoffSearchResult, UserInfo
|
|
30
|
+
from core.api.rbac import require_role
|
|
31
|
+
from core.api.routers._adapter import to_http
|
|
32
|
+
from core.api.security import get_current_user_or_agent
|
|
33
|
+
from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
|
|
34
|
+
from core.api.services.kg.lens import build_kg_context_for_handoff
|
|
35
|
+
from core.api.use_cases import handoffs as uc
|
|
36
|
+
from core.api.use_cases._context import CallerContext
|
|
37
|
+
from core.api.use_cases._errors import ServiceError
|
|
38
|
+
from core.api.visibility import get_visible_projects
|
|
39
|
+
|
|
40
|
+
# Re-export the pure helpers + constants from the use_case so existing importers
|
|
41
|
+
# (and any future callers) keep importing them from this router path unchanged.
|
|
42
|
+
from core.api.use_cases.handoffs import ( # noqa: F401 (re-export surface)
|
|
43
|
+
_HANDOFF_FILENAME_RE,
|
|
44
|
+
_SNIPPET_RADIUS,
|
|
45
|
+
_count_matches,
|
|
46
|
+
_extract_snippet,
|
|
47
|
+
_iter_all_slugs,
|
|
48
|
+
_parse_frontmatter,
|
|
49
|
+
_safe_read,
|
|
50
|
+
_search_semantic,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
router = APIRouter(prefix="/api/v1/handoffs", tags=["handoffs"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Endpoints (thin adapters)
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.get("/search", response_model=list[HandoffSearchResult])
|
|
64
|
+
async def search_handoffs(
|
|
65
|
+
q: str | None = Query(None, description="Full-text query (searches filename, tags, body)"),
|
|
66
|
+
project: str | None = Query(None, description="Filter by project slug"),
|
|
67
|
+
tags: str | None = Query(None, description="Comma-separated tags (OR logic)"),
|
|
68
|
+
date_start: str | None = Query(None, description="Start date filter YYYY-MM-DD (inclusive)"),
|
|
69
|
+
date_end: str | None = Query(None, description="End date filter YYYY-MM-DD (inclusive)"),
|
|
70
|
+
limit: int = Query(20, ge=1, le=200, description="Max results"),
|
|
71
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
72
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
73
|
+
vec_db: aiosqlite.Connection = Depends(get_vec_db),
|
|
74
|
+
) -> list[HandoffSearchResult]:
|
|
75
|
+
"""Full-text search across handoff files.
|
|
76
|
+
|
|
77
|
+
Parses frontmatter YAML from every handoff-*.md file found in project
|
|
78
|
+
memory/ directories and filters by query, project, tags, and date range.
|
|
79
|
+
Results are ranked by match count (descending), then date (descending).
|
|
80
|
+
"""
|
|
81
|
+
# DECISION 1 (the visibility template): resolve visibility at the boundary
|
|
82
|
+
# (needs UserInfo.teams, not carried by CallerContext) and pass it in; the
|
|
83
|
+
# use_case enforces it by silent filtering, exactly as the original router did.
|
|
84
|
+
visible_projects = await get_visible_projects(db, user)
|
|
85
|
+
|
|
86
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
87
|
+
try:
|
|
88
|
+
return await uc.search_handoffs(
|
|
89
|
+
ctx,
|
|
90
|
+
db,
|
|
91
|
+
vec_db,
|
|
92
|
+
q=q,
|
|
93
|
+
project=project,
|
|
94
|
+
tags=tags,
|
|
95
|
+
date_start=date_start,
|
|
96
|
+
date_end=date_end,
|
|
97
|
+
limit=limit,
|
|
98
|
+
visible_projects=visible_projects,
|
|
99
|
+
)
|
|
100
|
+
except ServiceError as e:
|
|
101
|
+
raise to_http(e)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.get("/{project_slug}/{filename}")
|
|
105
|
+
async def get_handoff(
|
|
106
|
+
project_slug: str,
|
|
107
|
+
filename: str,
|
|
108
|
+
deep: bool = Query(False, description="If true, append kg_context from KG Inline Lens"),
|
|
109
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
110
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
111
|
+
) -> dict:
|
|
112
|
+
"""Get a single handoff file by project slug and filename.
|
|
113
|
+
|
|
114
|
+
Returns frontmatter (parsed YAML) + body text. With ?deep=true also
|
|
115
|
+
returns kg_context built via build_kg_context_for_handoff().
|
|
116
|
+
"""
|
|
117
|
+
# DECISION 1: resolve visibility at the boundary; the use_case raises
|
|
118
|
+
# NotFoundError (404) when the project is not visible (mirrors the original
|
|
119
|
+
# check_project_access — 404 not 403, does not reveal existence).
|
|
120
|
+
visible_projects = await get_visible_projects(db, user)
|
|
121
|
+
|
|
122
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
123
|
+
try:
|
|
124
|
+
result = await uc.get_handoff(
|
|
125
|
+
ctx,
|
|
126
|
+
db,
|
|
127
|
+
project_slug=project_slug,
|
|
128
|
+
filename=filename,
|
|
129
|
+
visible_projects=visible_projects,
|
|
130
|
+
)
|
|
131
|
+
except ServiceError as e:
|
|
132
|
+
raise to_http(e)
|
|
133
|
+
|
|
134
|
+
# DECISION 2: deep KG enrichment is a per-surface adapter concern.
|
|
135
|
+
if deep:
|
|
136
|
+
check_deep_rate_limit(user.username)
|
|
137
|
+
log_kg_deep_access(user.username, "get_handoff", filename)
|
|
138
|
+
# Builder normalizes raw filename (strips .md + handoff- prefix, then
|
|
139
|
+
# prepends handoff:artifact:). Do not pre-prefix here.
|
|
140
|
+
result["kg_context"] = await build_kg_context_for_handoff(db, filename, deep=True)
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@router.post("/reindex")
|
|
146
|
+
async def reindex_handoffs(
|
|
147
|
+
project: str = Query(..., description="Project slug to reindex"),
|
|
148
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")), # Bearer-accessible for deploy scripts
|
|
149
|
+
vec_db: aiosqlite.Connection = Depends(get_vec_db),
|
|
150
|
+
) -> dict:
|
|
151
|
+
"""On-demand reindex of handoff embeddings for a project. Admin only."""
|
|
152
|
+
ctx = CallerContext.from_user_info(user, is_human_session=False)
|
|
153
|
+
try:
|
|
154
|
+
return await uc.reindex_handoffs(ctx, vec_db, project=project)
|
|
155
|
+
except ServiceError as e:
|
|
156
|
+
raise to_http(e)
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# v1.1.0 - 2026-04-14 - Single-writer: reclassify_inbox_items uses get_write_db (batch 5/6)
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import aiosqlite
|
|
10
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from core.api.db import get_db, get_write_db, write_db
|
|
14
|
+
from core.api.models import (
|
|
15
|
+
InboxGmailSyncCandidate,
|
|
16
|
+
InboxGmailSyncCompleteRequest,
|
|
17
|
+
InboxIngestBatchRequest,
|
|
18
|
+
InboxIngestBatchResponse,
|
|
19
|
+
InboxIngestRequest,
|
|
20
|
+
InboxIngestResponse,
|
|
21
|
+
InboxItemDetail,
|
|
22
|
+
InboxItemSummary,
|
|
23
|
+
InboxStatsResponse,
|
|
24
|
+
InboxStatusUpdateRequest,
|
|
25
|
+
InboxTaxonomyUpdateRequest,
|
|
26
|
+
InboxTaxonomyUpdateResponse,
|
|
27
|
+
InboxTriageDecisionRequest,
|
|
28
|
+
InboxTriageDecisionResponse,
|
|
29
|
+
UserInfo,
|
|
30
|
+
)
|
|
31
|
+
from core.api.rbac import require_role
|
|
32
|
+
from core.api.security import get_current_user_or_agent, require_agent_token_scope
|
|
33
|
+
from core.api.config import settings
|
|
34
|
+
from core.api.services.inbox import ingest_item, ingest_items_batch
|
|
35
|
+
from core.api.services.inbox_llm_classifier import schedule_classification
|
|
36
|
+
from core.api.services.inbox_sources import (
|
|
37
|
+
create_source,
|
|
38
|
+
delete_source,
|
|
39
|
+
get_source,
|
|
40
|
+
get_source_metrics,
|
|
41
|
+
list_sources,
|
|
42
|
+
update_source,
|
|
43
|
+
validate_public_url,
|
|
44
|
+
)
|
|
45
|
+
from core.api.services.inbox_digest import get_digest_stats, list_digest_items
|
|
46
|
+
from core.api.services.inbox_digest_jobs import recompute_digest_now
|
|
47
|
+
from core.api.services.inbox_gmail_sync import (
|
|
48
|
+
list_gmail_sync_candidates,
|
|
49
|
+
mark_gmail_sync_complete,
|
|
50
|
+
)
|
|
51
|
+
from core.api.services.inbox_tldr import get_or_generate_deep_research, get_or_generate_tldr
|
|
52
|
+
from core.api.services.inbox_triage import (
|
|
53
|
+
apply_triage_decision,
|
|
54
|
+
get_inbox_item_detail,
|
|
55
|
+
get_inbox_stats,
|
|
56
|
+
get_source_scores,
|
|
57
|
+
get_unread_count,
|
|
58
|
+
list_inbox_items,
|
|
59
|
+
update_inbox_status,
|
|
60
|
+
update_inbox_taxonomy,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
router = APIRouter(prefix="/api/v1/inbox", tags=["inbox"])
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Rate limit on POST /inbox/ingest (PR B)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# In-memory sliding window, per-process. 1000 req / 60s per identity — tuned
|
|
71
|
+
# for n8n RSS batch bursts (hundreds of articles per poll cycle) while still
|
|
72
|
+
# providing an emergency cap against runaway cost attacks. At higher sustained
|
|
73
|
+
# rates the LLM cost guard (app_settings daily cap) becomes the binding limit.
|
|
74
|
+
# Good enough for a single-worker FastAPI deployment; swap to Redis if we go
|
|
75
|
+
# multi-worker.
|
|
76
|
+
_INGEST_RATE_LIMITS: dict[str, list[float]] = defaultdict(list)
|
|
77
|
+
_INGEST_RATE_LIMITS_MAX_KEYS = 1000
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _check_ingest_rate_limit(
|
|
81
|
+
identity: str,
|
|
82
|
+
*,
|
|
83
|
+
limit: int = 1000,
|
|
84
|
+
window: float = 60.0,
|
|
85
|
+
) -> None:
|
|
86
|
+
now = time.time()
|
|
87
|
+
bucket = _INGEST_RATE_LIMITS[identity]
|
|
88
|
+
# Drop expired timestamps in place
|
|
89
|
+
fresh = [t for t in bucket if now - t < window]
|
|
90
|
+
_INGEST_RATE_LIMITS[identity] = fresh
|
|
91
|
+
if len(fresh) >= limit:
|
|
92
|
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
93
|
+
fresh.append(now)
|
|
94
|
+
# Evict stale keys opportunistically
|
|
95
|
+
if len(_INGEST_RATE_LIMITS) > _INGEST_RATE_LIMITS_MAX_KEYS:
|
|
96
|
+
stale = [
|
|
97
|
+
k for k, v in _INGEST_RATE_LIMITS.items() if not v or (now - v[-1]) > window
|
|
98
|
+
]
|
|
99
|
+
for k in stale:
|
|
100
|
+
del _INGEST_RATE_LIMITS[k]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SourceCreateRequest(BaseModel):
|
|
104
|
+
name: str = Field(..., min_length=1, max_length=200)
|
|
105
|
+
source_key: str = Field(..., min_length=1, max_length=200)
|
|
106
|
+
feed_url: str | None = Field(None, max_length=500)
|
|
107
|
+
source_type: str = Field("rss", pattern=r"^(rss|email|manual|api|legacy)$")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SourceUpdateRequest(BaseModel):
|
|
111
|
+
name: str | None = Field(None, min_length=1, max_length=200)
|
|
112
|
+
feed_url: str | None = Field(None, max_length=500)
|
|
113
|
+
active: bool | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@router.post(
|
|
117
|
+
"/ingest", response_model=InboxIngestResponse, status_code=status.HTTP_201_CREATED
|
|
118
|
+
)
|
|
119
|
+
async def ingest_inbox_item(
|
|
120
|
+
body: InboxIngestRequest,
|
|
121
|
+
user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
|
|
122
|
+
) -> InboxIngestResponse:
|
|
123
|
+
_check_ingest_rate_limit(f"ingest:{user.username}", limit=1000, window=60)
|
|
124
|
+
async with write_db(label="inbox.ingest.single") as db:
|
|
125
|
+
return await ingest_item(body, user=user, db=db)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post(
|
|
129
|
+
"/ingest/batch",
|
|
130
|
+
response_model=InboxIngestBatchResponse,
|
|
131
|
+
status_code=status.HTTP_200_OK,
|
|
132
|
+
)
|
|
133
|
+
async def ingest_inbox_items_batch(
|
|
134
|
+
body: InboxIngestBatchRequest,
|
|
135
|
+
user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
|
|
136
|
+
) -> InboxIngestBatchResponse:
|
|
137
|
+
# Rate limit accounts each item individually so a 500-item batch still
|
|
138
|
+
# respects the 1000/60s identity cap. A single large batch is one lock
|
|
139
|
+
# acquisition + one fsync, so this is intentionally generous.
|
|
140
|
+
for _ in range(len(body.items)):
|
|
141
|
+
_check_ingest_rate_limit(f"ingest:{user.username}", limit=1000, window=60)
|
|
142
|
+
async with write_db(label="inbox.ingest.batch") as db:
|
|
143
|
+
return await ingest_items_batch(body, user=user, db=db)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Sources CRUD (PR B)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.get("/sources")
|
|
152
|
+
async def list_inbox_sources(
|
|
153
|
+
active_only: bool = Query(False),
|
|
154
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
155
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
156
|
+
) -> list[dict]:
|
|
157
|
+
return await list_sources(
|
|
158
|
+
db, user.workspace_id or "ws_default", active_only=active_only
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@router.get("/sources/{source_id}")
|
|
163
|
+
async def get_inbox_source(
|
|
164
|
+
source_id: str,
|
|
165
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
166
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
167
|
+
) -> dict:
|
|
168
|
+
result = await get_source(db, user.workspace_id or "ws_default", source_id)
|
|
169
|
+
if result is None:
|
|
170
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.get("/sources/{source_id}/metrics")
|
|
175
|
+
async def get_inbox_source_metrics_endpoint(
|
|
176
|
+
source_id: str,
|
|
177
|
+
range: Literal["24h", "7d", "30d", "total"] = Query("total"),
|
|
178
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
179
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
180
|
+
) -> dict:
|
|
181
|
+
return await get_source_metrics(
|
|
182
|
+
db, user.workspace_id or "ws_default", source_id, range=range
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.post("/sources")
|
|
187
|
+
async def create_inbox_source(
|
|
188
|
+
body: SourceCreateRequest,
|
|
189
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")),
|
|
190
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
191
|
+
) -> dict:
|
|
192
|
+
if body.feed_url:
|
|
193
|
+
validate_public_url(body.feed_url)
|
|
194
|
+
return await create_source(
|
|
195
|
+
db,
|
|
196
|
+
user.workspace_id or "ws_default",
|
|
197
|
+
name=body.name,
|
|
198
|
+
source_key=body.source_key,
|
|
199
|
+
feed_url=body.feed_url,
|
|
200
|
+
source_type=body.source_type,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.patch("/sources/{source_id}")
|
|
205
|
+
async def update_inbox_source(
|
|
206
|
+
source_id: str,
|
|
207
|
+
body: SourceUpdateRequest,
|
|
208
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")),
|
|
209
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
210
|
+
) -> dict:
|
|
211
|
+
if body.feed_url:
|
|
212
|
+
validate_public_url(body.feed_url)
|
|
213
|
+
return await update_source(
|
|
214
|
+
db,
|
|
215
|
+
user.workspace_id or "ws_default",
|
|
216
|
+
source_id,
|
|
217
|
+
name=body.name,
|
|
218
|
+
feed_url=body.feed_url,
|
|
219
|
+
active=body.active,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@router.delete("/sources/{source_id}", status_code=204)
|
|
224
|
+
async def delete_inbox_source(
|
|
225
|
+
source_id: str,
|
|
226
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")),
|
|
227
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
228
|
+
) -> None:
|
|
229
|
+
await delete_source(db, user.workspace_id or "ws_default", source_id)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@router.get("/items", response_model=list[InboxItemSummary])
|
|
233
|
+
async def get_inbox_items(
|
|
234
|
+
needs_triage: bool = Query(
|
|
235
|
+
True, description="Default true: only items with status='unread'"
|
|
236
|
+
),
|
|
237
|
+
classified: bool | None = Query(None),
|
|
238
|
+
source: str | None = Query(None),
|
|
239
|
+
program: str | None = Query(None),
|
|
240
|
+
topic: str | None = Query(None),
|
|
241
|
+
treatment: str | None = Query(None),
|
|
242
|
+
item_status: str | None = Query(
|
|
243
|
+
None, alias="status", description="Filter by status (unread, read, saved, etc.)"
|
|
244
|
+
),
|
|
245
|
+
limit: int = Query(20, ge=1, le=200),
|
|
246
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
247
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
248
|
+
) -> list[InboxItemSummary]:
|
|
249
|
+
return await list_inbox_items(
|
|
250
|
+
db,
|
|
251
|
+
user,
|
|
252
|
+
limit=limit,
|
|
253
|
+
needs_triage=needs_triage,
|
|
254
|
+
classified=classified,
|
|
255
|
+
source=source,
|
|
256
|
+
program=program,
|
|
257
|
+
topic=topic,
|
|
258
|
+
treatment=treatment,
|
|
259
|
+
status=item_status,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@router.get("/stats", response_model=InboxStatsResponse)
|
|
264
|
+
async def get_inbox_stats_route(
|
|
265
|
+
needs_triage: bool = Query(
|
|
266
|
+
True, description="Default true: only items with status='unread'"
|
|
267
|
+
),
|
|
268
|
+
classified: bool | None = Query(None),
|
|
269
|
+
source: str | None = Query(None),
|
|
270
|
+
program: str | None = Query(None),
|
|
271
|
+
topic: str | None = Query(None),
|
|
272
|
+
treatment: str | None = Query(None),
|
|
273
|
+
item_status: str | None = Query(
|
|
274
|
+
None, alias="status", description="Filter by status (unread, read, saved, etc.)"
|
|
275
|
+
),
|
|
276
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
277
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
278
|
+
) -> InboxStatsResponse:
|
|
279
|
+
return await get_inbox_stats(
|
|
280
|
+
db,
|
|
281
|
+
user,
|
|
282
|
+
needs_triage=needs_triage,
|
|
283
|
+
classified=classified,
|
|
284
|
+
source=source,
|
|
285
|
+
program=program,
|
|
286
|
+
topic=topic,
|
|
287
|
+
treatment=treatment,
|
|
288
|
+
status=item_status,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@router.get("/items/unread-count")
|
|
293
|
+
async def get_inbox_unread_count(
|
|
294
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
295
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
296
|
+
) -> dict:
|
|
297
|
+
count = await get_unread_count(db, user)
|
|
298
|
+
return {"count": count}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@router.get("/digest/current")
|
|
302
|
+
async def get_current_digest_items(
|
|
303
|
+
domain_key: str | None = Query(None),
|
|
304
|
+
limit: int = Query(200, ge=1, le=500),
|
|
305
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
306
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
307
|
+
) -> list[dict]:
|
|
308
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
309
|
+
return await list_digest_items(
|
|
310
|
+
db,
|
|
311
|
+
workspace_id,
|
|
312
|
+
state="visible",
|
|
313
|
+
domain_key=domain_key,
|
|
314
|
+
limit=limit,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@router.get("/digest/overflow")
|
|
319
|
+
async def get_current_digest_overflow(
|
|
320
|
+
domain_key: str | None = Query(None),
|
|
321
|
+
limit: int = Query(200, ge=1, le=500),
|
|
322
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
323
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
324
|
+
) -> list[dict]:
|
|
325
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
326
|
+
return await list_digest_items(
|
|
327
|
+
db,
|
|
328
|
+
workspace_id,
|
|
329
|
+
state="overflow",
|
|
330
|
+
domain_key=domain_key,
|
|
331
|
+
limit=limit,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@router.get("/digest/stats")
|
|
336
|
+
async def get_current_digest_stats(
|
|
337
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
338
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
339
|
+
) -> dict:
|
|
340
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
341
|
+
return await get_digest_stats(db, workspace_id)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@router.post("/digest/recompute")
|
|
345
|
+
async def recompute_digest(
|
|
346
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")),
|
|
347
|
+
) -> dict:
|
|
348
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
349
|
+
return await recompute_digest_now(workspace_id)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@router.patch("/items/{inbox_item_id}/status")
|
|
353
|
+
async def patch_inbox_status(
|
|
354
|
+
inbox_item_id: str,
|
|
355
|
+
body: InboxStatusUpdateRequest,
|
|
356
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
357
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
358
|
+
) -> dict:
|
|
359
|
+
return await update_inbox_status(db, user, inbox_item_id, body)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@router.patch(
|
|
363
|
+
"/items/{inbox_item_id}/taxonomy", response_model=InboxTaxonomyUpdateResponse
|
|
364
|
+
)
|
|
365
|
+
async def patch_inbox_taxonomy(
|
|
366
|
+
inbox_item_id: str,
|
|
367
|
+
body: InboxTaxonomyUpdateRequest,
|
|
368
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
369
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
370
|
+
) -> InboxTaxonomyUpdateResponse:
|
|
371
|
+
return await update_inbox_taxonomy(db, user, inbox_item_id, body)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@router.get("/items/{inbox_item_id}", response_model=InboxItemDetail)
|
|
375
|
+
async def get_inbox_item(
|
|
376
|
+
inbox_item_id: str,
|
|
377
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
378
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
379
|
+
) -> InboxItemDetail:
|
|
380
|
+
return await get_inbox_item_detail(db, user, inbox_item_id)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@router.post(
|
|
384
|
+
"/items/{inbox_item_id}/decision", response_model=InboxTriageDecisionResponse
|
|
385
|
+
)
|
|
386
|
+
async def submit_inbox_decision(
|
|
387
|
+
inbox_item_id: str,
|
|
388
|
+
body: InboxTriageDecisionRequest,
|
|
389
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
390
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
391
|
+
) -> InboxTriageDecisionResponse:
|
|
392
|
+
return await apply_triage_decision(db, user, inbox_item_id, body)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@router.post("/items/{inbox_item_id}/tldr")
|
|
396
|
+
async def generate_inbox_tldr(
|
|
397
|
+
inbox_item_id: str,
|
|
398
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
399
|
+
) -> dict:
|
|
400
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
401
|
+
return await get_or_generate_tldr(inbox_item_id, workspace_id)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@router.post("/items/{inbox_item_id}/deep-research")
|
|
405
|
+
async def generate_inbox_deep_research(
|
|
406
|
+
inbox_item_id: str,
|
|
407
|
+
force: bool = False,
|
|
408
|
+
user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
|
|
409
|
+
) -> dict:
|
|
410
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
411
|
+
return await get_or_generate_deep_research(inbox_item_id, workspace_id, force=force)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class ReclassifyRequest(BaseModel):
|
|
415
|
+
source: str | None = Field(None, description="Filter by source name")
|
|
416
|
+
status: str = Field("unread", description="Filter by status (default: unread)")
|
|
417
|
+
limit: int = Field(100, ge=1, le=500, description="Max items to reclassify")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@router.post("/reclassify")
|
|
421
|
+
async def reclassify_inbox_items(
|
|
422
|
+
body: ReclassifyRequest,
|
|
423
|
+
user: UserInfo = Depends(require_role("admin", "super_admin")),
|
|
424
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
425
|
+
) -> dict:
|
|
426
|
+
"""Queue matching inbox items for re-classification. Admin only, fire-and-forget."""
|
|
427
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
428
|
+
|
|
429
|
+
where_clauses = ["workspace_id = ?"]
|
|
430
|
+
params: list[object] = [workspace_id]
|
|
431
|
+
|
|
432
|
+
if body.source:
|
|
433
|
+
where_clauses.append("source = ?")
|
|
434
|
+
params.append(body.source)
|
|
435
|
+
if body.status:
|
|
436
|
+
where_clauses.append("status = ?")
|
|
437
|
+
params.append(body.status)
|
|
438
|
+
|
|
439
|
+
where_sql = " AND ".join(where_clauses)
|
|
440
|
+
params.append(body.limit)
|
|
441
|
+
|
|
442
|
+
cursor = await db.execute(
|
|
443
|
+
f"SELECT id FROM inbox_items WHERE {where_sql} ORDER BY created_at DESC LIMIT ?",
|
|
444
|
+
tuple(params),
|
|
445
|
+
)
|
|
446
|
+
rows = await cursor.fetchall()
|
|
447
|
+
|
|
448
|
+
queued = 0
|
|
449
|
+
for row in rows:
|
|
450
|
+
try:
|
|
451
|
+
schedule_classification(settings.db_path, workspace_id, row["id"])
|
|
452
|
+
queued += 1
|
|
453
|
+
except Exception:
|
|
454
|
+
logger.exception("Failed to schedule reclassify for item %s", row["id"])
|
|
455
|
+
|
|
456
|
+
logger.info(
|
|
457
|
+
"Reclassify queued %d/%d items (source=%s, status=%s) by %s",
|
|
458
|
+
queued,
|
|
459
|
+
len(rows),
|
|
460
|
+
body.source,
|
|
461
|
+
body.status,
|
|
462
|
+
user.username,
|
|
463
|
+
)
|
|
464
|
+
return {"queued": queued}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@router.get("/source-scores")
|
|
468
|
+
async def get_source_scores_route(
|
|
469
|
+
user: UserInfo = Depends(get_current_user_or_agent),
|
|
470
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
471
|
+
) -> list[dict]:
|
|
472
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
473
|
+
return await get_source_scores(db, workspace_id)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@router.get(
|
|
477
|
+
"/gmail-sync/candidates",
|
|
478
|
+
response_model=list[InboxGmailSyncCandidate],
|
|
479
|
+
)
|
|
480
|
+
async def get_gmail_sync_candidates(
|
|
481
|
+
limit: int = Query(50, ge=1, le=200),
|
|
482
|
+
user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
|
|
483
|
+
db: aiosqlite.Connection = Depends(get_db),
|
|
484
|
+
) -> list[InboxGmailSyncCandidate]:
|
|
485
|
+
workspace_id = user.workspace_id or "ws_default"
|
|
486
|
+
return await list_gmail_sync_candidates(db, workspace_id, limit=limit)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@router.post("/gmail-sync/{inbox_item_id}/complete")
|
|
490
|
+
async def complete_gmail_sync(
|
|
491
|
+
inbox_item_id: str,
|
|
492
|
+
body: InboxGmailSyncCompleteRequest,
|
|
493
|
+
user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
|
|
494
|
+
db: aiosqlite.Connection = Depends(get_write_db),
|
|
495
|
+
) -> dict:
|
|
496
|
+
return await mark_gmail_sync_complete(db, user, inbox_item_id, body)
|