contextsynapse 1.0.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.
- contextsynapse/__init__.py +107 -0
- contextsynapse/__main__.py +9 -0
- contextsynapse/a2a/__init__.py +28 -0
- contextsynapse/a2a/client.py +120 -0
- contextsynapse/a2a/discovery.py +128 -0
- contextsynapse/a2a/handlers.py +164 -0
- contextsynapse/a2a/models.py +246 -0
- contextsynapse/a2a/streaming.py +92 -0
- contextsynapse/a2a/task_manager.py +275 -0
- contextsynapse/adapters/__init__.py +98 -0
- contextsynapse/adapters/_base.py +298 -0
- contextsynapse/adapters/autogen/__init__.py +4 -0
- contextsynapse/adapters/autogen/tools.py +34 -0
- contextsynapse/adapters/crewai/__init__.py +4 -0
- contextsynapse/adapters/crewai/tools.py +63 -0
- contextsynapse/adapters/langchain/__init__.py +19 -0
- contextsynapse/adapters/langchain/message_history.py +17 -0
- contextsynapse/adapters/langchain/retriever.py +158 -0
- contextsynapse/adapters/langchain/tools.py +43 -0
- contextsynapse/adapters/langgraph/__init__.py +21 -0
- contextsynapse/adapters/langgraph/checkpoint.py +223 -0
- contextsynapse/adapters/langgraph/context_tools.py +341 -0
- contextsynapse/adapters/langgraph/message_history.py +138 -0
- contextsynapse/adapters/langgraph/tools.py +43 -0
- contextsynapse/adapters/llamaindex/__init__.py +4 -0
- contextsynapse/adapters/llamaindex/tools.py +64 -0
- contextsynapse/adapters/openai/__init__.py +24 -0
- contextsynapse/adapters/openai/tools.py +206 -0
- contextsynapse/adapters/pydantic_ai/__init__.py +4 -0
- contextsynapse/adapters/pydantic_ai/tools.py +36 -0
- contextsynapse/adapters/swarm/__init__.py +4 -0
- contextsynapse/adapters/swarm/tools.py +44 -0
- contextsynapse/agents/__init__.py +6 -0
- contextsynapse/agents/auto_capture.py +80 -0
- contextsynapse/agents/dispatcher.py +299 -0
- contextsynapse/agents/notifications.py +112 -0
- contextsynapse/aiql/__init__.py +71 -0
- contextsynapse/aiql/cache.py +341 -0
- contextsynapse/aiql/compiler/__init__.py +62 -0
- contextsynapse/aiql/compiler/optimizer.py +126 -0
- contextsynapse/aiql/compiler/planner.py +366 -0
- contextsynapse/aiql/compiler/statistics.py +235 -0
- contextsynapse/aiql/compiler/validator.py +425 -0
- contextsynapse/aiql/engine/__init__.py +11 -0
- contextsynapse/aiql/engine/chunk_storage.py +2584 -0
- contextsynapse/aiql/engine/executor.py +7737 -0
- contextsynapse/aiql/engine/extraction_integration.py +442 -0
- contextsynapse/aiql/engine/graph_builder.py +405 -0
- contextsynapse/aiql/engine/node_creation_from_normalized.py +691 -0
- contextsynapse/aiql/engine/performance_monitor.py +289 -0
- contextsynapse/aiql/engine/postprocessing.py +365 -0
- contextsynapse/aiql/engine/ray_stage_executors.py +918 -0
- contextsynapse/aiql/engine/redis_cache.py +106 -0
- contextsynapse/aiql/engine/streaming_json.py +366 -0
- contextsynapse/aiql/engine/streaming_parquet.py +568 -0
- contextsynapse/aiql/engine/streaming_ray_chunking.py +419 -0
- contextsynapse/aiql/grammar/__init__.py +14 -0
- contextsynapse/aiql/grammar/aiql_grammar.py +1822 -0
- contextsynapse/aiql/grammar/base.py +71 -0
- contextsynapse/aiql/parser/__init__.py +46 -0
- contextsynapse/aiql/parser/aiql_parser.py +11330 -0
- contextsynapse/aiql/parser/base_parser.py +47 -0
- contextsynapse/api/__init__.py +5 -0
- contextsynapse/api/a2a_router.py +233 -0
- contextsynapse/api/admin_router.py +140 -0
- contextsynapse/api/agent_worker_router.py +978 -0
- contextsynapse/api/algorithms_router.py +317 -0
- contextsynapse/api/api.py +4144 -0
- contextsynapse/api/audit.py +204 -0
- contextsynapse/api/auth.py +441 -0
- contextsynapse/api/auth_router.py +537 -0
- contextsynapse/api/billing.py +261 -0
- contextsynapse/api/billing_router.py +275 -0
- contextsynapse/api/boundary_router.py +452 -0
- contextsynapse/api/cognition_router.py +116 -0
- contextsynapse/api/connector_router.py +237 -0
- contextsynapse/api/context_router.py +1619 -0
- contextsynapse/api/dashboard_router.py +11367 -0
- contextsynapse/api/errors.py +183 -0
- contextsynapse/api/events.py +88 -0
- contextsynapse/api/experiment_bootstrap.py +321 -0
- contextsynapse/api/experiment_orchestrator.py +1078 -0
- contextsynapse/api/experiment_router.py +2089 -0
- contextsynapse/api/feature_gate.py +89 -0
- contextsynapse/api/federation_router.py +108 -0
- contextsynapse/api/integrations.py +567 -0
- contextsynapse/api/integrations_router.py +461 -0
- contextsynapse/api/intelligence_router.py +1028 -0
- contextsynapse/api/metering.py +155 -0
- contextsynapse/api/metrics.py +112 -0
- contextsynapse/api/models.py +38 -0
- contextsynapse/api/monitoring_router.py +174 -0
- contextsynapse/api/pipeline_router.py +1437 -0
- contextsynapse/api/playground_conn.py +116 -0
- contextsynapse/api/projects_router.py +131 -0
- contextsynapse/api/queries_router.py +282 -0
- contextsynapse/api/quota.py +95 -0
- contextsynapse/api/rate_limit.py +64 -0
- contextsynapse/api/search_router.py +261 -0
- contextsynapse/api/shield_router.py +447 -0
- contextsynapse/api/tenant_guard.py +103 -0
- contextsynapse/api/tenant_quotas.py +231 -0
- contextsynapse/api/tenants.py +305 -0
- contextsynapse/api/users.py +646 -0
- contextsynapse/api/v1.py +167 -0
- contextsynapse/api/vertical_builder_router.py +387 -0
- contextsynapse/benchmark/__init__.py +15 -0
- contextsynapse/benchmark/__main__.py +64 -0
- contextsynapse/benchmark/agents.py +338 -0
- contextsynapse/benchmark/live_ab.py +338 -0
- contextsynapse/benchmark/reporter.py +149 -0
- contextsynapse/benchmark/runner.py +264 -0
- contextsynapse/benchmark/savings.py +266 -0
- contextsynapse/benchmark/token_counter.py +212 -0
- contextsynapse/benchmark/workloads.py +289 -0
- contextsynapse/buffer/__init__.py +154 -0
- contextsynapse/buffer/base.py +257 -0
- contextsynapse/buffer/duckdb_buffer.py +246 -0
- contextsynapse/buffer/file_buffer.py +229 -0
- contextsynapse/buffer/lmdb_buffer.py +286 -0
- contextsynapse/buffer/local_buffer.py +154 -0
- contextsynapse/buffer/redis_buffer.py +211 -0
- contextsynapse/buffer/sqlite_buffer.py +266 -0
- contextsynapse/cache.py +2 -0
- contextsynapse/cli/__init__.py +40 -0
- contextsynapse/cli/cli.py +475 -0
- contextsynapse/cli/config_cli.py +177 -0
- contextsynapse/cli/scaffold.py +465 -0
- contextsynapse/cognition/__init__.py +31 -0
- contextsynapse/cognition/derivation.py +55 -0
- contextsynapse/cognition/events.py +135 -0
- contextsynapse/cognition/feedback.py +52 -0
- contextsynapse/cognition/hallucination.py +90 -0
- contextsynapse/cognition/invalidation.py +70 -0
- contextsynapse/cognition/read_tracker.py +50 -0
- contextsynapse/collaboration.py +2 -0
- contextsynapse/comparison.py +2 -0
- contextsynapse/config/__init__.py +10 -0
- contextsynapse/config/connectors/amfi_nav.yaml +18 -0
- contextsynapse/config/connectors/auto_sector.yaml +20 -0
- contextsynapse/config/connectors/banking_sector.yaml +20 -0
- contextsynapse/config/connectors/energy_sector.yaml +20 -0
- contextsynapse/config/connectors/fmcg_sector.yaml +20 -0
- contextsynapse/config/connectors/nsdl_fpi.yaml +9 -0
- contextsynapse/config/connectors/nse_price.yaml +61 -0
- contextsynapse/config/connectors/pharma_sector.yaml +20 -0
- contextsynapse/config/connectors/rbi_dbie.yaml +12 -0
- contextsynapse/config/context_presets/ai_research.json +25 -0
- contextsynapse/config/context_presets/chatgpt_conversations.json +19 -0
- contextsynapse/config/context_presets/claude_conversations.json +19 -0
- contextsynapse/config/context_presets/legal_compliance.json +21 -0
- contextsynapse/config/context_presets/meeting_notes.json +21 -0
- contextsynapse/config/context_presets/slack_engineering.json +21 -0
- contextsynapse/config/context_presets/tech_climate.json +24 -0
- contextsynapse/config/context_presets/toi_politics.json +25 -0
- contextsynapse/config/database_config.py +453 -0
- contextsynapse/config/deployment.py +221 -0
- contextsynapse/config/gpt_action_schema.json +164 -0
- contextsynapse/config/indicators/india_macro.yaml +213 -0
- contextsynapse/config/logging_config.py +88 -0
- contextsynapse/config/mode_config.py +223 -0
- contextsynapse/config/schemas/api_spec.yaml +48 -0
- contextsynapse/config/schemas/architecture_doc.yaml +51 -0
- contextsynapse/config/schemas/chat_history.yaml +90 -0
- contextsynapse/config/schemas/chatgpt_conversation.yaml +101 -0
- contextsynapse/config/schemas/claude_conversation.yaml +148 -0
- contextsynapse/config/schemas/contract.yaml +48 -0
- contextsynapse/config/schemas/conversation.yaml +167 -0
- contextsynapse/config/schemas/database.yaml +74 -0
- contextsynapse/config/schemas/decision.yaml +72 -0
- contextsynapse/config/schemas/ecommerce.yaml +56 -0
- contextsynapse/config/schemas/education.yaml +42 -0
- contextsynapse/config/schemas/healthcare.yaml +133 -0
- contextsynapse/config/schemas/hr_document.yaml +48 -0
- contextsynapse/config/schemas/invoice.yaml +40 -0
- contextsynapse/config/schemas/knowledge_base.yaml +75 -0
- contextsynapse/config/schemas/legal.yaml +42 -0
- contextsynapse/config/schemas/marketing_content.yaml +48 -0
- contextsynapse/config/schemas/meeting_notes.yaml +51 -0
- contextsynapse/config/schemas/news_article.yaml +61 -0
- contextsynapse/config/schemas/real_estate.yaml +49 -0
- contextsynapse/config/schemas/requirements_doc.yaml +48 -0
- contextsynapse/config/schemas/research_paper.yaml +56 -0
- contextsynapse/config/schemas/resume.yaml +49 -0
- contextsynapse/config/schemas/retail.yaml +42 -0
- contextsynapse/config/schemas/rule_meta.yaml +173 -0
- contextsynapse/config/schemas/rules.yaml +72 -0
- contextsynapse/config/schemas/screener_meta.yaml +236 -0
- contextsynapse/config/schemas/sdlc.yaml +103 -0
- contextsynapse/config/schemas/session_graph.yaml +172 -0
- contextsynapse/config/schemas/support_ticket.yaml +48 -0
- contextsynapse/config/schemas/web.yaml +64 -0
- contextsynapse/config/sensors/earnings_calendar.yaml +52 -0
- contextsynapse/config/sensors/global_risk.yaml +53 -0
- contextsynapse/config/sensors/government_policy.yaml +54 -0
- contextsynapse/config/sensors/supply_chain.yaml +50 -0
- contextsynapse/config/sensors/templates/behavioral.yaml +56 -0
- contextsynapse/config/sensors/templates/equity_sentiment.yaml +48 -0
- contextsynapse/config/sensors/templates/global_macro.yaml +47 -0
- contextsynapse/config/sensors/templates/market_data.yaml +118 -0
- contextsynapse/config/sensors/templates/sector_technical.yaml +47 -0
- contextsynapse/config/sensors/weather.yaml +52 -0
- contextsynapse/config/storage_strategy.py +202 -0
- contextsynapse/config/templates/competitive_intel.yaml +39 -0
- contextsynapse/config/templates/geopolitical_risk.yaml +46 -0
- contextsynapse/config/templates/market_analysis.yaml +58 -0
- contextsynapse/connectors/__init__.py +15 -0
- contextsynapse/connectors/api_router.py +121 -0
- contextsynapse/connectors/base.py +357 -0
- contextsynapse/connectors/financial/audio.py +170 -0
- contextsynapse/connectors/financial/youtube.py +272 -0
- contextsynapse/connectors/github.py +164 -0
- contextsynapse/connectors/jira.py +120 -0
- contextsynapse/connectors/kafka_connector.py +124 -0
- contextsynapse/connectors/pipeline_connector.py +747 -0
- contextsynapse/connectors/registry.py +116 -0
- contextsynapse/connectors/scheduler.py +87 -0
- contextsynapse/connectors/universal.py +360 -0
- contextsynapse/context/__init__.py +96 -0
- contextsynapse/context/__main__.py +4 -0
- contextsynapse/context/acl.py +125 -0
- contextsynapse/context/agent_card.py +201 -0
- contextsynapse/context/agent_memory.py +814 -0
- contextsynapse/context/agents.py +472 -0
- contextsynapse/context/agents_redis.py +391 -0
- contextsynapse/context/assembled.py +204 -0
- contextsynapse/context/attribution.py +113 -0
- contextsynapse/context/blob.py +184 -0
- contextsynapse/context/boundaries.py +187 -0
- contextsynapse/context/boundary.py +507 -0
- contextsynapse/context/bundle.py +155 -0
- contextsynapse/context/cli.py +519 -0
- contextsynapse/context/collaboration.py +165 -0
- contextsynapse/context/compiler.py +604 -0
- contextsynapse/context/composite.py +536 -0
- contextsynapse/context/compression.py +147 -0
- contextsynapse/context/context_manager.py +888 -0
- contextsynapse/context/context_schema.py +90 -0
- contextsynapse/context/context_schema.yaml +105 -0
- contextsynapse/context/context_units.py +891 -0
- contextsynapse/context/conversation.py +488 -0
- contextsynapse/context/dedup.py +207 -0
- contextsynapse/context/document_processor.py +698 -0
- contextsynapse/context/embedding_hooks.py +338 -0
- contextsynapse/context/execution_schema.py +274 -0
- contextsynapse/context/fan_out.py +138 -0
- contextsynapse/context/frozen.py +80 -0
- contextsynapse/context/gravity.py +227 -0
- contextsynapse/context/hub.py +1401 -0
- contextsynapse/context/ingest.py +391 -0
- contextsynapse/context/injection.py +226 -0
- contextsynapse/context/interaction_graphifier.py +562 -0
- contextsynapse/context/layers.py +574 -0
- contextsynapse/context/projection.py +1195 -0
- contextsynapse/context/promotion.py +634 -0
- contextsynapse/context/proof.py +131 -0
- contextsynapse/context/propagation.py +577 -0
- contextsynapse/context/pubsub.py +135 -0
- contextsynapse/context/quality.py +1054 -0
- contextsynapse/context/quality_tracker.py +270 -0
- contextsynapse/context/scoping.py +370 -0
- contextsynapse/context/seed.py +992 -0
- contextsynapse/context/session.py +1142 -0
- contextsynapse/context/session_graph.py +593 -0
- contextsynapse/context/session_memory.py +309 -0
- contextsynapse/context/session_resolver.py +111 -0
- contextsynapse/context/skill_learning.py +237 -0
- contextsynapse/context/smart_budget.py +129 -0
- contextsynapse/context/source_policy.py +242 -0
- contextsynapse/context/store_factory.py +53 -0
- contextsynapse/context/sync.py +648 -0
- contextsynapse/context/token.py +236 -0
- contextsynapse/context/vector_integration.py +297 -0
- contextsynapse/context/webhooks.py +194 -0
- contextsynapse/context/working_memory.py +224 -0
- contextsynapse/core/__init__.py +18 -0
- contextsynapse/core/archiver.py +237 -0
- contextsynapse/core/change_stream.py +282 -0
- contextsynapse/core/checkpoint.py +310 -0
- contextsynapse/core/cloud_storage.py +387 -0
- contextsynapse/core/context_state.py +651 -0
- contextsynapse/core/cron.py +324 -0
- contextsynapse/core/datasource.py +643 -0
- contextsynapse/core/db.py +289 -0
- contextsynapse/core/federation.py +283 -0
- contextsynapse/core/graph_coordinator.py +201 -0
- contextsynapse/core/graph_intelligence.py +388 -0
- contextsynapse/core/graph_structures.py +106 -0
- contextsynapse/core/graph_sync.py +157 -0
- contextsynapse/core/hybrid_graph_storage.py +2998 -0
- contextsynapse/core/lifecycle.py +691 -0
- contextsynapse/core/multiworker.py +378 -0
- contextsynapse/core/pruning.py +312 -0
- contextsynapse/core/redis_registry.py +524 -0
- contextsynapse/core/registry.py +1043 -0
- contextsynapse/core/registry_factory.py +51 -0
- contextsynapse/core/registry_metadata.py +294 -0
- contextsynapse/core/replay.py +354 -0
- contextsynapse/core/replication.py +339 -0
- contextsynapse/core/time_travel.py +260 -0
- contextsynapse/core/write_behind.py +152 -0
- contextsynapse/core/write_context.py +39 -0
- contextsynapse/db/__init__.py +1 -0
- contextsynapse/db/postgres.py +267 -0
- contextsynapse/db/profile.py +236 -0
- contextsynapse/db/rules.py +328 -0
- contextsynapse/demo/__init__.py +0 -0
- contextsynapse/demo/report.py +147 -0
- contextsynapse/demo/runner.py +520 -0
- contextsynapse/demo/scenario.py +165 -0
- contextsynapse/demo/session_demo.py +280 -0
- contextsynapse/demo/session_report.py +143 -0
- contextsynapse/demo/universal_runner.py +257 -0
- contextsynapse/engine/__init__.py +15 -0
- contextsynapse/engine/core.py +555 -0
- contextsynapse/extraction/__init__.py +69 -0
- contextsynapse/extraction/cli.py +274 -0
- contextsynapse/extraction/config.py +107 -0
- contextsynapse/extraction/examples/example_usage.py +118 -0
- contextsynapse/extraction/examples/sample_manifest.json +53 -0
- contextsynapse/extraction/examples/sample_normalized.json +185 -0
- contextsynapse/extraction/extract_engine.py +440 -0
- contextsynapse/extraction/extractors/__init__.py +37 -0
- contextsynapse/extraction/extractors/chat_export_extractor.py +189 -0
- contextsynapse/extraction/extractors/csv_extractor.py +83 -0
- contextsynapse/extraction/extractors/docx_extractor.py +129 -0
- contextsynapse/extraction/extractors/excel_extractor.py +152 -0
- contextsynapse/extraction/extractors/html_extractor.py +146 -0
- contextsynapse/extraction/extractors/pdf_extractor.py +635 -0
- contextsynapse/extraction/extractors/pdf_extractor_layoutparser.py +408 -0
- contextsynapse/extraction/extractors/register_extractors.py +63 -0
- contextsynapse/extraction/extractors/text_extractor.py +65 -0
- contextsynapse/extraction/extractors/txt_extractor.py +120 -0
- contextsynapse/extraction/extractors/website_extractor.py +246 -0
- contextsynapse/extraction/fact_extractor.py +236 -0
- contextsynapse/extraction/file_manager.py +353 -0
- contextsynapse/extraction/hierarchy.py +239 -0
- contextsynapse/extraction/id_generator.py +450 -0
- contextsynapse/extraction/layout_detector.py +409 -0
- contextsynapse/extraction/llm_entity_extractor.py +301 -0
- contextsynapse/extraction/normalized_store.py +480 -0
- contextsynapse/extraction/normalizer.py +594 -0
- contextsynapse/extraction/parquet_writer.py +598 -0
- contextsynapse/extraction/post_processors/__init__.py +40 -0
- contextsynapse/extraction/post_processors/column_reorganizer.py +507 -0
- contextsynapse/extraction/ray_normalization.py +233 -0
- contextsynapse/extraction/ray_runner.py +207 -0
- contextsynapse/extraction/redis_schema_registry.py +222 -0
- contextsynapse/extraction/registry.py +204 -0
- contextsynapse/extraction/schema_loader.py +663 -0
- contextsynapse/extraction/schema_registry.py +230 -0
- contextsynapse/extraction/semantic_normalizer.py +245 -0
- contextsynapse/extraction/version_manager.py +314 -0
- contextsynapse/gateway/__init__.py +24 -0
- contextsynapse/gateway/action_emitter.py +212 -0
- contextsynapse/gateway/cost_tracker.py +508 -0
- contextsynapse/gateway/gateway.py +377 -0
- contextsynapse/gateway/policy.py +303 -0
- contextsynapse/governance/__init__.py +13 -0
- contextsynapse/governance/layer.py +513 -0
- contextsynapse/ingestion/__init__.py +15 -0
- contextsynapse/ingestion/amplifier.py +288 -0
- contextsynapse/ingestion/async_ingest.py +463 -0
- contextsynapse/ingestion/chunker.py +303 -0
- contextsynapse/ingestion/chunking.py +58 -0
- contextsynapse/ingestion/cleaner.py +190 -0
- contextsynapse/ingestion/cleanse.py +252 -0
- contextsynapse/ingestion/connectors/__init__.py +38 -0
- contextsynapse/ingestion/connectors/base.py +300 -0
- contextsynapse/ingestion/connectors/chatgpt_memory.py +247 -0
- contextsynapse/ingestion/connectors/claude_memory.py +274 -0
- contextsynapse/ingestion/connectors/sap_ingestor.py +208 -0
- contextsynapse/ingestion/content_detector.py +80 -0
- contextsynapse/ingestion/content_monitor.py +502 -0
- contextsynapse/ingestion/dedup.py +106 -0
- contextsynapse/ingestion/feed.py +640 -0
- contextsynapse/ingestion/filters.py +461 -0
- contextsynapse/ingestion/graph_builder.py +705 -0
- contextsynapse/ingestion/input_classifier.py +307 -0
- contextsynapse/ingestion/job_manager.py +571 -0
- contextsynapse/ingestion/llm_extractor.py +610 -0
- contextsynapse/ingestion/orchestrator.py +116 -0
- contextsynapse/ingestion/parsers/__init__.py +19 -0
- contextsynapse/ingestion/parsers/graph_parser_registry.py +103 -0
- contextsynapse/ingestion/parsers/graphml_parser.py +141 -0
- contextsynapse/ingestion/parsers/jsonld_parser.py +171 -0
- contextsynapse/ingestion/parsers/rdf_parser.py +150 -0
- contextsynapse/ingestion/pipeline_context.py +228 -0
- contextsynapse/ingestion/pipeline_store.py +1195 -0
- contextsynapse/ingestion/queue.py +133 -0
- contextsynapse/ingestion/reprocess.py +179 -0
- contextsynapse/ingestion/scenario_router.py +224 -0
- contextsynapse/ingestion/schema_extractor.py +759 -0
- contextsynapse/ingestion/schema_validator.py +274 -0
- contextsynapse/ingestion/sdlc_ingest.py +87 -0
- contextsynapse/ingestion/smart_ingest.py +2451 -0
- contextsynapse/ingestion/source_connector.py +171 -0
- contextsynapse/ingestion/stage_executor.py +2095 -0
- contextsynapse/ingestion/stages/__init__.py +9 -0
- contextsynapse/ingestion/stages/tabular_to_graph.py +289 -0
- contextsynapse/ingestion/strategies.py +386 -0
- contextsynapse/ingestion/universal/__init__.py +4 -0
- contextsynapse/ingestion/universal/_operator_registry.py +59 -0
- contextsynapse/ingestion/universal/ingest_content.py +32 -0
- contextsynapse/ingestion/universal/operators/__init__.py +1 -0
- contextsynapse/ingestion/universal/operators/base.py +19 -0
- contextsynapse/ingestion/universal/operators/build_edges.py +75 -0
- contextsynapse/ingestion/universal/operators/cluster_topics.py +397 -0
- contextsynapse/ingestion/universal/operators/deduplicate.py +45 -0
- contextsynapse/ingestion/universal/operators/detect_signals.py +90 -0
- contextsynapse/ingestion/universal/operators/embed.py +80 -0
- contextsynapse/ingestion/universal/operators/extract_entities.py +253 -0
- contextsynapse/ingestion/universal/operators/extract_preferences.py +117 -0
- contextsynapse/ingestion/universal/operators/filter_content.py +69 -0
- contextsynapse/ingestion/universal/operators/index_bm25.py +52 -0
- contextsynapse/ingestion/universal/operators/infer_domains.py +69 -0
- contextsynapse/ingestion/universal/operators/link_cross_reference.py +210 -0
- contextsynapse/ingestion/universal/operators/parse_records.py +66 -0
- contextsynapse/ingestion/universal/operators/resolve_entities.py +261 -0
- contextsynapse/ingestion/universal/operators/scanners/__init__.py +16 -0
- contextsynapse/ingestion/universal/operators/scanners/edges.py +173 -0
- contextsynapse/ingestion/universal/operators/scanners/git.py +187 -0
- contextsynapse/ingestion/universal/operators/scanners/github.py +135 -0
- contextsynapse/ingestion/universal/operators/scanners/github_api.py +509 -0
- contextsynapse/ingestion/universal/operators/scanners/helpers.py +67 -0
- contextsynapse/ingestion/universal/operators/scanners/jira.py +65 -0
- contextsynapse/ingestion/universal/operators/scanners/llm_enrichment.py +82 -0
- contextsynapse/ingestion/universal/operators/scanners/repo.py +149 -0
- contextsynapse/ingestion/universal/operators/sdlc_scan.py +213 -0
- contextsynapse/ingestion/universal/operators/spec_scanner.py +149 -0
- contextsynapse/ingestion/universal/operators/store_documents.py +72 -0
- contextsynapse/ingestion/universal/operators/synthesize_cu.py +180 -0
- contextsynapse/ingestion/universal/operators/validate_gate.py +65 -0
- contextsynapse/ingestion/universal/parsers/__init__.py +5 -0
- contextsynapse/ingestion/universal/parsers/record_parser.py +203 -0
- contextsynapse/ingestion/universal/parsers/turn_parser.py +329 -0
- contextsynapse/ingestion/universal/shared_signals.py +161 -0
- contextsynapse/ingestion/universal/stage_executor.py +219 -0
- contextsynapse/ingestion/web_crawler.py +265 -0
- contextsynapse/intelligence/__init__.py +53 -0
- contextsynapse/intelligence/alerts.py +198 -0
- contextsynapse/intelligence/collector.py +655 -0
- contextsynapse/intelligence/comparison.py +198 -0
- contextsynapse/intelligence/config.py +56 -0
- contextsynapse/intelligence/conflict_detector.py +182 -0
- contextsynapse/intelligence/context_gaps.py +192 -0
- contextsynapse/intelligence/context_radar.py +135 -0
- contextsynapse/intelligence/correlation_engine.py +416 -0
- contextsynapse/intelligence/correlation_template.py +120 -0
- contextsynapse/intelligence/dedup.py +296 -0
- contextsynapse/intelligence/event_bus.py +91 -0
- contextsynapse/intelligence/feedback_loop.py +101 -0
- contextsynapse/intelligence/freshness.py +307 -0
- contextsynapse/intelligence/fundamental_feed.py +551 -0
- contextsynapse/intelligence/geo_data.py +170 -0
- contextsynapse/intelligence/geo_reference.py +112 -0
- contextsynapse/intelligence/graph_fusion.py +465 -0
- contextsynapse/intelligence/heuristics.py +176 -0
- contextsynapse/intelligence/impact_tracker.py +336 -0
- contextsynapse/intelligence/macro_indicators.py +319 -0
- contextsynapse/intelligence/ocr.py +104 -0
- contextsynapse/intelligence/persistence.py +267 -0
- contextsynapse/intelligence/pipeline_health.py +130 -0
- contextsynapse/intelligence/price_feed.py +221 -0
- contextsynapse/intelligence/rate_limiter.py +130 -0
- contextsynapse/intelligence/reactive.py +411 -0
- contextsynapse/intelligence/runtime_context.py +390 -0
- contextsynapse/intelligence/sentiment_decay.py +426 -0
- contextsynapse/intelligence/session.py +277 -0
- contextsynapse/intelligence/signal_hierarchy.py +438 -0
- contextsynapse/intelligence/signals.py +185 -0
- contextsynapse/intelligence/source_watcher.py +132 -0
- contextsynapse/intelligence/table_enricher.py +256 -0
- contextsynapse/intelligence/tagger.py +319 -0
- contextsynapse/intelligence/timeseries.py +209 -0
- contextsynapse/intelligence/tracker.py +318 -0
- contextsynapse/intelligence/watchdog.py +392 -0
- contextsynapse/intelligence/watchdog_manager.py +263 -0
- contextsynapse/intelligence/webhook_receiver.py +165 -0
- contextsynapse/intelligence/weight_learner.py +234 -0
- contextsynapse/llm/__init__.py +8 -0
- contextsynapse/llm/client.py +348 -0
- contextsynapse/llm/rate_limiter.py +166 -0
- contextsynapse/logging_config.py +2 -0
- contextsynapse/marketplace/__init__.py +7 -0
- contextsynapse/marketplace/registry.py +173 -0
- contextsynapse/mcp/__init__.py +11 -0
- contextsynapse/mcp/__main__.py +4 -0
- contextsynapse/mcp/auth_middleware.py +212 -0
- contextsynapse/mcp/connection_pool.py +128 -0
- contextsynapse/mcp/server.py +986 -0
- contextsynapse/mcp/session_context.py +67 -0
- contextsynapse/memory/__init__.py +34 -0
- contextsynapse/memory/engine.py +475 -0
- contextsynapse/memory/temporal.py +327 -0
- contextsynapse/metadata/__init__.py +12 -0
- contextsynapse/metadata/metadata_db.py +272 -0
- contextsynapse/metadata/metadata_query_tool.py +300 -0
- contextsynapse/metadata/metadata_tracker.py +425 -0
- contextsynapse/models/__init__.py +27 -0
- contextsynapse/models/embedding_service.py +360 -0
- contextsynapse/models/model_registry.py +228 -0
- contextsynapse/multiworker.py +2 -0
- contextsynapse/pipelines/__init__.py +1 -0
- contextsynapse/pipelines/executor.py +851 -0
- contextsynapse/pipelines/filters.py +189 -0
- contextsynapse/pipelines/models.py +176 -0
- contextsynapse/pipelines/scheduler.py +268 -0
- contextsynapse/playground/__init__.py +1 -0
- contextsynapse/playground/templates.py +85 -0
- contextsynapse/plugins/__init__.py +43 -0
- contextsynapse/plugins/api.py +66 -0
- contextsynapse/plugins/base.py +350 -0
- contextsynapse/plugins/domain_interface.py +508 -0
- contextsynapse/plugins/domain_loader.py +241 -0
- contextsynapse/plugins/loader.py +148 -0
- contextsynapse/plugins/registry.py +265 -0
- contextsynapse/project/__init__.py +11 -0
- contextsynapse/project/code_context.py +807 -0
- contextsynapse/project/connectors/__init__.py +4 -0
- contextsynapse/project/connectors/github_connector.py +113 -0
- contextsynapse/project/connectors/jira_connector.py +160 -0
- contextsynapse/project/graph.py +1023 -0
- contextsynapse/project/project_context.py +545 -0
- contextsynapse/project/scanner_registry.py +61 -0
- contextsynapse/project/schema_manager.py +322 -0
- contextsynapse/project/schemas/knowledge.yaml +59 -0
- contextsynapse/project/schemas/sdlc.yaml +369 -0
- contextsynapse/project/sdlc_schema.py +172 -0
- contextsynapse/project/spec_parser.py +85 -0
- contextsynapse/project/task_lock.py +181 -0
- contextsynapse/project/task_stream.py +203 -0
- contextsynapse/project/team_tools.py +537 -0
- contextsynapse/py.typed +0 -0
- contextsynapse/realtime/__init__.py +6 -0
- contextsynapse/realtime/collaboration.py +147 -0
- contextsynapse/rules/__init__.py +10 -0
- contextsynapse/rules/engine.py +116 -0
- contextsynapse/rules/evaluators/__init__.py +6 -0
- contextsynapse/rules/evaluators/aggregate.py +205 -0
- contextsynapse/rules/evaluators/custom.py +170 -0
- contextsynapse/rules/evaluators/pre_action.py +125 -0
- contextsynapse/rules/evaluators/temporal.py +129 -0
- contextsynapse/rules/loader.py +52 -0
- contextsynapse/rules/models.py +124 -0
- contextsynapse/rules/universal.py +393 -0
- contextsynapse/scheduler.py +229 -0
- contextsynapse/schema/__init__.py +42 -0
- contextsynapse/schema/compiler.py +252 -0
- contextsynapse/schema/composer.py +158 -0
- contextsynapse/schema/dedup.py +73 -0
- contextsynapse/schema/derivation.py +172 -0
- contextsynapse/schema/sdl.py +326 -0
- contextsynapse/schema/validation_gate.py +124 -0
- contextsynapse/sdk/__init__.py +63 -0
- contextsynapse/sdk/agent.py +214 -0
- contextsynapse/sdk/client.py +269 -0
- contextsynapse/sdk/exceptions.py +23 -0
- contextsynapse/sdk/models.py +78 -0
- contextsynapse/sdk/session.py +408 -0
- contextsynapse/sdk/tracked.py +218 -0
- contextsynapse/sdk/worker.py +266 -0
- contextsynapse/sdk/workspace_client.py +98 -0
- contextsynapse/sdk/ws.py +41 -0
- contextsynapse/search/__init__.py +20 -0
- contextsynapse/search/embedding_cache.py +140 -0
- contextsynapse/search/enhanced_search.py +152 -0
- contextsynapse/search/fulltext.py +130 -0
- contextsynapse/search/graph_search.py +634 -0
- contextsynapse/search/lmdb_index.py +1218 -0
- contextsynapse/search/nl_to_aiql.py +199 -0
- contextsynapse/search/rag.py +1346 -0
- contextsynapse/search/rag_cache.py +167 -0
- contextsynapse/search/reasoning_chain.py +231 -0
- contextsynapse/search/redis_search.py +207 -0
- contextsynapse/search/retrieval_quality.py +314 -0
- contextsynapse/search/semantic_query.py +455 -0
- contextsynapse/search/text_resolver.py +159 -0
- contextsynapse/search/whoosh_search.py +373 -0
- contextsynapse/security/__init__.py +17 -0
- contextsynapse/security/audit.py +297 -0
- contextsynapse/security/audit_logger.py +97 -0
- contextsynapse/security/audit_trail.py +270 -0
- contextsynapse/security/auth_unified.py +284 -0
- contextsynapse/security/auto_tagger.py +230 -0
- contextsynapse/security/data_security.py +218 -0
- contextsynapse/security/encryption.py +207 -0
- contextsynapse/security/identity.py +447 -0
- contextsynapse/security/jwt_identity.py +59 -0
- contextsynapse/security/middleware.py +336 -0
- contextsynapse/security/pii.py +233 -0
- contextsynapse/security/rbac.py +96 -0
- contextsynapse/security/rls.py +205 -0
- contextsynapse/security/sanitize.py +233 -0
- contextsynapse/security/scoped_encryption.py +131 -0
- contextsynapse/security/tenant.py +401 -0
- contextsynapse/shield/__init__.py +21 -0
- contextsynapse/shield/anomaly.py +56 -0
- contextsynapse/shield/permissions.py +99 -0
- contextsynapse/shield/profile.py +103 -0
- contextsynapse/shield/shield.py +108 -0
- contextsynapse/shield/trust_engine.py +69 -0
- contextsynapse/skills/__init__.py +14 -0
- contextsynapse/skills/engine.py +233 -0
- contextsynapse/skills/loader.py +59 -0
- contextsynapse/skills/models.py +139 -0
- contextsynapse/storage/__init__.py +7 -0
- contextsynapse/storage/cache.py +208 -0
- contextsynapse/storage/columnar_store_v2.py +178 -0
- contextsynapse/storage/csr_graph_storage.py +587 -0
- contextsynapse/storage/csr_store.py +388 -0
- contextsynapse/storage/document_store.py +491 -0
- contextsynapse/storage/hnsw_index.py +294 -0
- contextsynapse/storage/hybrid_store_v2.py +278 -0
- contextsynapse/storage/lmdb_graph_storage.py +366 -0
- contextsynapse/storage/migration.py +279 -0
- contextsynapse/storage/namespace_store.py +346 -0
- contextsynapse/storage/pure_graph_storage.py +185 -0
- contextsynapse/storage/redis_graph_adapter.py +186 -0
- contextsynapse/storage/redis_graph_storage.py +375 -0
- contextsynapse/storage/router/__init__.py +29 -0
- contextsynapse/storage/router/duckdb_store.py +398 -0
- contextsynapse/storage/router/factory.py +83 -0
- contextsynapse/storage/router/interface.py +125 -0
- contextsynapse/storage/router/postgres_store.py +339 -0
- contextsynapse/storage/router/resolver.py +192 -0
- contextsynapse/storage/router/router.py +300 -0
- contextsynapse/storage/single_file_storage.py +391 -0
- contextsynapse/storage/storage_manager.py +214 -0
- contextsynapse/storage/storage_query_optimizer.py +314 -0
- contextsynapse/storage/wal.py +359 -0
- contextsynapse/temporal/__init__.py +0 -0
- contextsynapse/temporal/storage.py +106 -0
- contextsynapse/tests/__init__.py +11 -0
- contextsynapse/tests/regression/__init__.py +1 -0
- contextsynapse/tests/regression/add_metadata.py +395 -0
- contextsynapse/tests/regression/add_skip_flags.py +85 -0
- contextsynapse/tests/regression/cases/test_basic_create_node.yaml +20 -0
- contextsynapse/tests/regression/cases/test_basic_select.yaml +21 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail_filtered.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_blocks_range.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_get_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_latest_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_length.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_merkle_root.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_create_edge.yaml +28 -0
- contextsynapse/tests/regression/cases/test_delete_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_delete_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_evaluation_mrr.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_ndcg.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_precision.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_recall.yaml +40 -0
- contextsynapse/tests/regression/cases/test_group_by_basic.yaml +56 -0
- contextsynapse/tests/regression/cases/test_match_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_match_node_with_order_limit.yaml +31 -0
- contextsynapse/tests/regression/cases/test_namespace_switch.yaml +18 -0
- contextsynapse/tests/regression/cases/test_pipeline_cascaded.yaml +61 -0
- contextsynapse/tests/regression/cases/test_pipeline_fixed_size_chunking.yaml +48 -0
- contextsynapse/tests/regression/cases/test_pipeline_gpt4_chunking.yaml +54 -0
- contextsynapse/tests/regression/cases/test_pipeline_hybrid_indexing.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_large_embedding.yaml +52 -0
- contextsynapse/tests/regression/cases/test_pipeline_paragraph_chunking.yaml +49 -0
- contextsynapse/tests/regression/cases/test_pipeline_recursive_chunking.yaml +47 -0
- contextsynapse/tests/regression/cases/test_pipeline_run_pipeline.yaml +56 -0
- contextsynapse/tests/regression/cases/test_pipeline_section_based_chunking.yaml +53 -0
- contextsynapse/tests/regression/cases/test_pipeline_semantic_chunking.yaml +51 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_entity_extraction.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_relationships.yaml +62 -0
- contextsynapse/tests/regression/cases/test_rag_evaluation.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_bfs_with_eval.yaml +48 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_dfs_with_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_multi_hop_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_path.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_basic.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_bfs.yaml +52 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_dfs.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_multi_hop.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_shortest_path.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_weighted.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_shortest_path_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_traversal.yaml +30 -0
- contextsynapse/tests/regression/cases/test_rag_graph_weighted_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_search.yaml +33 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_with_prompt_embedding.yaml +44 -0
- contextsynapse/tests/regression/cases/test_rag_keyword_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_multi_hop_graph.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_path_based.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_profile_based.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_vector_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_graph.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_hybrid.yaml +31 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_chunking.yaml +57 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_detection.yaml +33 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_function.yaml +30 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_with_model.yaml +31 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_fixed_size.yaml +44 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_recursive.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_section.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_semantic.yaml +44 -0
- contextsynapse/tests/regression/cases/test_time_travel_as_of.yaml +35 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_commit.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_timestamp.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_for_system_time_all.yaml +37 -0
- contextsynapse/tests/regression/cases/test_time_travel_versions_between.yaml +39 -0
- contextsynapse/tests/regression/cases/test_traverse_basic.yaml +31 -0
- contextsynapse/tests/regression/cases/test_traverse_max_depth.yaml +34 -0
- contextsynapse/tests/regression/cases/test_update_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/evaluation_metrics.py +227 -0
- contextsynapse/tests/regression/regression_runner.py +1081 -0
- contextsynapse/tests/regression/setup_regression_data.py +212 -0
- contextsynapse/tests/regression/test_basic_only.py +84 -0
- contextsynapse/tests/regression/test_namespace_creation.py +29 -0
- contextsynapse/tests/regression/test_single_fix.py +61 -0
- contextsynapse/tests/regression/test_single_query.py +20 -0
- contextsynapse/tests/regression/validate_test_syntax.py +179 -0
- contextsynapse/tests/test_advanced_features.py +347 -0
- contextsynapse/tests/test_aiql_summary.py +283 -0
- contextsynapse/tests/test_all_aiql_queries.py +948 -0
- contextsynapse/tests/test_all_create_queries.py +213 -0
- contextsynapse/tests/test_api_vs_native.py +280 -0
- contextsynapse/tests/test_chunking_dedup.py +199 -0
- contextsynapse/tests/test_connectors.py +171 -0
- contextsynapse/tests/test_edges_and_match.py +202 -0
- contextsynapse/tests/test_extraction_pipeline_integration.py +581 -0
- contextsynapse/tests/test_name_property.py +224 -0
- contextsynapse/tests/test_node_edge_metadata.py +250 -0
- contextsynapse/tests/test_playground_conn.py +114 -0
- contextsynapse/tests/test_query_cache.py +213 -0
- contextsynapse/tests/test_source_policy_auth.py +348 -0
- contextsynapse/tests/test_temporal_and_domain.py +224 -0
- contextsynapse/tests/test_time_travel_e2e.py +571 -0
- contextsynapse/tests/test_traversal_and_aggregation.py +275 -0
- contextsynapse/tools/__init__.py +36 -0
- contextsynapse/tools/a2a_tools.py +106 -0
- contextsynapse/tools/cognition_tools.py +118 -0
- contextsynapse/tools/context.py +905 -0
- contextsynapse/tools/formatting.py +92 -0
- contextsynapse/tools/gateway_tools.py +186 -0
- contextsynapse/tools/graph.py +1700 -0
- contextsynapse/tools/intelligence.py +235 -0
- contextsynapse/tools/memory.py +118 -0
- contextsynapse/tools/registry.py +373 -0
- contextsynapse/tools/shield_tools.py +159 -0
- contextsynapse/tools/tasks.py +341 -0
- contextsynapse/tools/workspace_tools.py +124 -0
- contextsynapse/utils/__init__.py +15 -0
- contextsynapse/utils/config_loader.py +198 -0
- contextsynapse/utils/env_loader.py +156 -0
- contextsynapse/utils/file_utils.py +288 -0
- contextsynapse/utils/logging_optimizer.py +119 -0
- contextsynapse/vector/__init__.py +30 -0
- contextsynapse/vector/chroma_vector_store.py +152 -0
- contextsynapse/vector/custom_vector_store.py +537 -0
- contextsynapse/vector/faiss_vector_store.py +296 -0
- contextsynapse/vector/qdrant_vector_store.py +163 -0
- contextsynapse/vector/vector_db.py +953 -0
- contextsynapse/vector/vector_db_manager.py +197 -0
- contextsynapse/workspace/__init__.py +27 -0
- contextsynapse/workspace/base.py +94 -0
- contextsynapse/workspace/git.py +228 -0
- contextsynapse/workspace/github.py +78 -0
- contextsynapse/workspace/local.py +73 -0
- contextsynapse/workspace/tools.py +222 -0
- contextsynapse-1.0.0.dist-info/METADATA +847 -0
- contextsynapse-1.0.0.dist-info/RECORD +776 -0
- contextsynapse-1.0.0.dist-info/WHEEL +5 -0
- contextsynapse-1.0.0.dist-info/entry_points.txt +4 -0
- contextsynapse-1.0.0.dist-info/licenses/LICENSE +200 -0
- contextsynapse-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pipeline Router
|
|
3
|
+
===============
|
|
4
|
+
REST endpoints for managing and executing stored pipelines.
|
|
5
|
+
|
|
6
|
+
GET /dashboard/pipelines — List pipeline templates
|
|
7
|
+
GET /dashboard/pipelines/{id} — Get pipeline details + stats
|
|
8
|
+
POST /dashboard/pipelines — Create custom pipeline
|
|
9
|
+
PUT /dashboard/pipelines/{id} — Update custom pipeline
|
|
10
|
+
DELETE /dashboard/pipelines/{id} — Delete custom pipeline
|
|
11
|
+
POST /dashboard/pipelines/{id}/run — Execute a pipeline
|
|
12
|
+
GET /dashboard/pipelines/runs — List pipeline runs (all)
|
|
13
|
+
GET /dashboard/pipelines/runs/{run_id} — Get run details + accuracy
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
|
|
31
|
+
from ..ingestion.pipeline_store import (
|
|
32
|
+
PipelineStore,
|
|
33
|
+
PipelineRun,
|
|
34
|
+
AccuracyScore,
|
|
35
|
+
StageLog,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Request models
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
class CreatePipelineRequest(BaseModel):
|
|
46
|
+
name: str = Field(..., min_length=1, max_length=200)
|
|
47
|
+
description: str = ""
|
|
48
|
+
stages: List[Dict[str, Any]] = Field(default_factory=list)
|
|
49
|
+
aiql_template: str = ""
|
|
50
|
+
default_params: Optional[Dict[str, Any]] = None
|
|
51
|
+
governance: Optional[Dict[str, Any]] = None
|
|
52
|
+
tags: Optional[List[str]] = None
|
|
53
|
+
category: str = "ingestion"
|
|
54
|
+
schema_name: str = ""
|
|
55
|
+
parser: str = "" # turn | record | paragraph
|
|
56
|
+
filters: Optional[Dict[str, Any]] = None
|
|
57
|
+
graph_config: Optional[Dict[str, Any]] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UpdatePipelineRequest(BaseModel):
|
|
61
|
+
name: Optional[str] = None
|
|
62
|
+
description: Optional[str] = None
|
|
63
|
+
stages: Optional[List[Dict[str, Any]]] = None
|
|
64
|
+
aiql_template: Optional[str] = None
|
|
65
|
+
default_params: Optional[Dict[str, Any]] = None
|
|
66
|
+
governance: Optional[Dict[str, Any]] = None
|
|
67
|
+
tags: Optional[List[str]] = None
|
|
68
|
+
category: Optional[str] = None
|
|
69
|
+
enabled: Optional[bool] = None
|
|
70
|
+
schema_name: Optional[str] = None
|
|
71
|
+
parser: Optional[str] = None
|
|
72
|
+
filters: Optional[Dict[str, Any]] = None
|
|
73
|
+
graph_config: Optional[Dict[str, Any]] = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RunPipelineRequest(BaseModel):
|
|
77
|
+
graph: str = Field(..., min_length=1)
|
|
78
|
+
text: Optional[str] = None
|
|
79
|
+
url: Optional[str] = None
|
|
80
|
+
params: Optional[Dict[str, Any]] = None
|
|
81
|
+
governance_tags: Optional[List[str]] = None
|
|
82
|
+
data_classification: str = "internal"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StartRunRequest(BaseModel):
|
|
86
|
+
"""Start a new step-wise pipeline run."""
|
|
87
|
+
graph: str = Field(..., min_length=1)
|
|
88
|
+
pipeline_id: Optional[str] = None # use builtin:auto if omitted
|
|
89
|
+
intent: str = "graph_rag" # graph_rag, build_graph, search_only
|
|
90
|
+
mode: str = "step_by_step" # step_by_step or run_all
|
|
91
|
+
text: Optional[str] = None
|
|
92
|
+
url: Optional[str] = None
|
|
93
|
+
params: Optional[Dict[str, Any]] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class EditContextRequest(BaseModel):
|
|
97
|
+
"""User edits to intermediate pipeline data."""
|
|
98
|
+
nodes: Optional[List[Dict[str, Any]]] = None
|
|
99
|
+
edges: Optional[List[Dict[str, Any]]] = None
|
|
100
|
+
chunks: Optional[List[Dict[str, Any]]] = None
|
|
101
|
+
tables: Optional[List[Dict[str, Any]]] = None
|
|
102
|
+
classification: Optional[Dict[str, Any]] = None
|
|
103
|
+
stages: Optional[List[str]] = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UploadSchemaRequest(BaseModel):
|
|
107
|
+
"""Upload a YAML schema for schema-first ingestion."""
|
|
108
|
+
graph: str = Field(..., min_length=1)
|
|
109
|
+
schema_yaml: str = Field(..., min_length=10)
|
|
110
|
+
name: Optional[str] = None
|
|
111
|
+
strict: bool = False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Router factory
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def create_pipeline_router(
|
|
119
|
+
user_registry,
|
|
120
|
+
tenant_registry,
|
|
121
|
+
graph_registry,
|
|
122
|
+
usage_meter=None,
|
|
123
|
+
) -> APIRouter:
|
|
124
|
+
"""Create the /dashboard/pipelines router."""
|
|
125
|
+
|
|
126
|
+
from .auth import UserAuth
|
|
127
|
+
|
|
128
|
+
router = APIRouter(tags=["pipelines"])
|
|
129
|
+
user_auth = UserAuth(user_registry)
|
|
130
|
+
store = PipelineStore()
|
|
131
|
+
|
|
132
|
+
def _get_tenant(user):
|
|
133
|
+
tenant_id = user_registry.get_primary_tenant_id(user.user_id)
|
|
134
|
+
if not tenant_id:
|
|
135
|
+
raise HTTPException(status_code=404, detail="No workspace found")
|
|
136
|
+
tenant = tenant_registry.get(tenant_id)
|
|
137
|
+
if not tenant:
|
|
138
|
+
raise HTTPException(status_code=404, detail="Workspace not found")
|
|
139
|
+
return tenant
|
|
140
|
+
|
|
141
|
+
def _scope_graph(tenant, name: str) -> str:
|
|
142
|
+
return f"{tenant.tenant_id}:{name}"
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# GET /dashboard/pipelines — list templates
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
@router.get("/dashboard/pipelines")
|
|
148
|
+
async def list_pipelines(user=Depends(user_auth)):
|
|
149
|
+
tenant = _get_tenant(user)
|
|
150
|
+
pipelines = store.list_pipelines(tenant.tenant_id)
|
|
151
|
+
# Attach stats to each pipeline
|
|
152
|
+
for p in pipelines:
|
|
153
|
+
p["stats"] = store.get_pipeline_stats(p["id"], tenant.tenant_id)
|
|
154
|
+
return {"pipelines": pipelines}
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# GET /dashboard/pipelines/stages — stage explanations
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
@router.get("/dashboard/pipelines/stages")
|
|
160
|
+
async def list_stage_explanations(user=Depends(user_auth)):
|
|
161
|
+
"""Return human-readable explanations for each pipeline stage type."""
|
|
162
|
+
from ..ingestion.pipeline_store import STAGE_EXPLANATIONS
|
|
163
|
+
return {"stages": STAGE_EXPLANATIONS}
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# GET /dashboard/pipelines/runs — list all runs
|
|
167
|
+
# NOTE: Must be registered BEFORE /dashboard/pipelines/{pipeline_id}
|
|
168
|
+
# to avoid the wildcard matching "runs" as a pipeline_id.
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
@router.get("/dashboard/pipelines/runs")
|
|
171
|
+
async def list_runs(
|
|
172
|
+
graph: Optional[str] = None,
|
|
173
|
+
pipeline_id: Optional[str] = None,
|
|
174
|
+
status: Optional[str] = None,
|
|
175
|
+
limit: int = 50,
|
|
176
|
+
user=Depends(user_auth),
|
|
177
|
+
):
|
|
178
|
+
tenant = _get_tenant(user)
|
|
179
|
+
runs = store.list_runs(
|
|
180
|
+
tenant.tenant_id,
|
|
181
|
+
pipeline_id=pipeline_id,
|
|
182
|
+
graph=graph,
|
|
183
|
+
status=status,
|
|
184
|
+
limit=limit,
|
|
185
|
+
)
|
|
186
|
+
return {"runs": runs}
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# GET /dashboard/pipelines/runs/{run_id} — get run details
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
@router.get("/dashboard/pipelines/runs/{run_id}")
|
|
192
|
+
async def get_run(run_id: str, user=Depends(user_auth)):
|
|
193
|
+
run = store.get_run(run_id)
|
|
194
|
+
if not run:
|
|
195
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
196
|
+
return run
|
|
197
|
+
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
# GET /dashboard/pipelines/{id} — get pipeline + stats
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
@router.get("/dashboard/pipelines/{pipeline_id}")
|
|
202
|
+
async def get_pipeline(pipeline_id: str, user=Depends(user_auth)):
|
|
203
|
+
tenant = _get_tenant(user)
|
|
204
|
+
pipeline = store.get_pipeline(pipeline_id)
|
|
205
|
+
if not pipeline:
|
|
206
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
207
|
+
pipeline["stats"] = store.get_pipeline_stats(pipeline_id, tenant.tenant_id)
|
|
208
|
+
pipeline["recent_runs"] = store.list_runs(tenant.tenant_id, pipeline_id=pipeline_id, limit=10)
|
|
209
|
+
return pipeline
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# POST /dashboard/pipelines — create custom pipeline
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
@router.post("/dashboard/pipelines")
|
|
215
|
+
async def create_pipeline(req: CreatePipelineRequest, user=Depends(user_auth)):
|
|
216
|
+
tenant = _get_tenant(user)
|
|
217
|
+
pipeline = store.create_pipeline(
|
|
218
|
+
tenant_id=tenant.tenant_id,
|
|
219
|
+
name=req.name,
|
|
220
|
+
description=req.description,
|
|
221
|
+
stages=req.stages,
|
|
222
|
+
aiql_template=req.aiql_template,
|
|
223
|
+
default_params=req.default_params,
|
|
224
|
+
governance=req.governance,
|
|
225
|
+
tags=req.tags,
|
|
226
|
+
category=req.category,
|
|
227
|
+
created_by=user.user_id,
|
|
228
|
+
schema_name=req.schema_name,
|
|
229
|
+
parser=req.parser,
|
|
230
|
+
filters=req.filters,
|
|
231
|
+
graph_config=req.graph_config,
|
|
232
|
+
)
|
|
233
|
+
return {"pipeline": pipeline, "message": "Pipeline created"}
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# PUT /dashboard/pipelines/{id} — update custom pipeline
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
@router.put("/dashboard/pipelines/{pipeline_id}")
|
|
239
|
+
async def update_pipeline(pipeline_id: str, req: UpdatePipelineRequest, user=Depends(user_auth)):
|
|
240
|
+
updates = {k: v for k, v in req.model_dump().items() if v is not None}
|
|
241
|
+
result = store.update_pipeline(pipeline_id, **updates)
|
|
242
|
+
if not result:
|
|
243
|
+
raise HTTPException(status_code=400, detail="Cannot update (builtin or not found)")
|
|
244
|
+
return {"pipeline": result, "message": "Pipeline updated"}
|
|
245
|
+
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
# DELETE /dashboard/pipelines/{id}
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
@router.delete("/dashboard/pipelines/{pipeline_id}")
|
|
250
|
+
async def delete_pipeline(pipeline_id: str, user=Depends(user_auth)):
|
|
251
|
+
if not store.delete_pipeline(pipeline_id):
|
|
252
|
+
raise HTTPException(status_code=400, detail="Cannot delete (builtin or not found)")
|
|
253
|
+
return {"message": "Pipeline deleted"}
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
# POST /dashboard/pipelines/{id}/run — execute pipeline
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
@router.post("/dashboard/pipelines/{pipeline_id}/run")
|
|
259
|
+
async def run_pipeline(pipeline_id: str, req: RunPipelineRequest, user=Depends(user_auth)):
|
|
260
|
+
tenant = _get_tenant(user)
|
|
261
|
+
pipeline = store.get_pipeline(pipeline_id)
|
|
262
|
+
if not pipeline:
|
|
263
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
264
|
+
if not pipeline.get("enabled", True):
|
|
265
|
+
raise HTTPException(status_code=400, detail="Pipeline is disabled")
|
|
266
|
+
|
|
267
|
+
# Smart pipeline shortcut: route to new 7-stage pipeline
|
|
268
|
+
smart_name = pipeline.get("_smart_pipeline")
|
|
269
|
+
if smart_name:
|
|
270
|
+
scoped = _scope_graph(tenant, req.graph)
|
|
271
|
+
try:
|
|
272
|
+
from ..ingestion.smart_ingest import ingest_url as _smart_url, ingest_text as _smart_text
|
|
273
|
+
from ..core.registry import GraphRegistry
|
|
274
|
+
reg = GraphRegistry()
|
|
275
|
+
db = reg.get_graph(scoped, load_if_missing=True) or reg.create_graph(scoped)
|
|
276
|
+
if req.url:
|
|
277
|
+
result = _smart_url(req.url, db, pipeline=smart_name)
|
|
278
|
+
elif req.text:
|
|
279
|
+
result = _smart_text(req.text, db, title="Ingested text", pipeline=smart_name)
|
|
280
|
+
else:
|
|
281
|
+
raise HTTPException(status_code=400, detail="URL or text required")
|
|
282
|
+
return {
|
|
283
|
+
"status": "completed",
|
|
284
|
+
"pipeline_id": pipeline_id,
|
|
285
|
+
"result": {
|
|
286
|
+
"document_id": result.document_id,
|
|
287
|
+
"passages": len(result.passage_ids),
|
|
288
|
+
"entities": len(result.entity_ids),
|
|
289
|
+
"facts": len(result.fact_ids),
|
|
290
|
+
"links": len(result.link_ids),
|
|
291
|
+
"edges": result.edge_count,
|
|
292
|
+
"errors": result.errors,
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
except HTTPException:
|
|
296
|
+
raise
|
|
297
|
+
except Exception as e:
|
|
298
|
+
raise HTTPException(status_code=500, detail=f"Smart pipeline error: {e}")
|
|
299
|
+
|
|
300
|
+
# Determine source
|
|
301
|
+
if req.text:
|
|
302
|
+
source_text = req.text
|
|
303
|
+
source_label = f"text:{len(req.text)} chars"
|
|
304
|
+
source_type = "text"
|
|
305
|
+
elif req.url:
|
|
306
|
+
# Fetch URL
|
|
307
|
+
try:
|
|
308
|
+
import httpx
|
|
309
|
+
resp = httpx.get(req.url, timeout=30, follow_redirects=True)
|
|
310
|
+
resp.raise_for_status()
|
|
311
|
+
raw_html = resp.text
|
|
312
|
+
source_label = req.url
|
|
313
|
+
source_type = "url"
|
|
314
|
+
# Extract article content using trafilatura
|
|
315
|
+
source_text = None
|
|
316
|
+
if "<html" in raw_html.lower() or "<body" in raw_html.lower():
|
|
317
|
+
try:
|
|
318
|
+
import trafilatura
|
|
319
|
+
source_text = trafilatura.extract(raw_html, include_links=False, include_tables=True)
|
|
320
|
+
except ImportError:
|
|
321
|
+
pass # optional
|
|
322
|
+
if not source_text:
|
|
323
|
+
import re as _re
|
|
324
|
+
source_text = _re.sub(r"<script[^>]*>.*?</script>", "", raw_html, flags=_re.DOTALL | _re.IGNORECASE)
|
|
325
|
+
source_text = _re.sub(r"<style[^>]*>.*?</style>", "", source_text, flags=_re.DOTALL | _re.IGNORECASE)
|
|
326
|
+
source_text = _re.sub(r"<[^>]+>", " ", source_text)
|
|
327
|
+
source_text = _re.sub(r"\s+", " ", source_text).strip()
|
|
328
|
+
else:
|
|
329
|
+
source_text = raw_html.strip()
|
|
330
|
+
if not source_text:
|
|
331
|
+
raise ValueError("No text content extracted from URL")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
raise HTTPException(status_code=400, detail=f"Failed to fetch URL: {e}")
|
|
334
|
+
else:
|
|
335
|
+
raise HTTPException(status_code=400, detail="Provide 'text' or 'url'")
|
|
336
|
+
|
|
337
|
+
scoped_graph = _scope_graph(tenant, req.graph)
|
|
338
|
+
|
|
339
|
+
# Duplicate check
|
|
340
|
+
source_hash = hashlib.sha256(source_text.encode()).hexdigest()
|
|
341
|
+
dup = store.find_duplicate(graph=req.graph, source_hash=source_hash, tenant_id=tenant.tenant_id)
|
|
342
|
+
if dup:
|
|
343
|
+
raise HTTPException(
|
|
344
|
+
status_code=409,
|
|
345
|
+
detail=f"Duplicate content: identical source was already ingested into graph '{req.graph}' "
|
|
346
|
+
f"(run {dup['run_id'][:12]}, {dup['status']}).",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Create run record
|
|
350
|
+
run = store.create_run(
|
|
351
|
+
pipeline_id=pipeline_id,
|
|
352
|
+
tenant_id=tenant.tenant_id,
|
|
353
|
+
user_id=user.user_id,
|
|
354
|
+
graph=req.graph,
|
|
355
|
+
source=source_label,
|
|
356
|
+
source_type=source_type,
|
|
357
|
+
source_hash=source_hash,
|
|
358
|
+
params=req.params,
|
|
359
|
+
governance_tags=req.governance_tags,
|
|
360
|
+
data_classification=req.data_classification,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Execute via async job queue or fallback to thread
|
|
364
|
+
import asyncio
|
|
365
|
+
try:
|
|
366
|
+
from ..jobs.queue import job_queue
|
|
367
|
+
try:
|
|
368
|
+
loop = asyncio.get_running_loop()
|
|
369
|
+
loop.create_task(_enqueue_pipeline(
|
|
370
|
+
job_queue, store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used
|
|
371
|
+
))
|
|
372
|
+
except RuntimeError:
|
|
373
|
+
# Fallback: run in thread if no event loop
|
|
374
|
+
import threading
|
|
375
|
+
threading.Thread(
|
|
376
|
+
target=_execute_pipeline,
|
|
377
|
+
args=(store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used),
|
|
378
|
+
daemon=True,
|
|
379
|
+
).start()
|
|
380
|
+
except ImportError:
|
|
381
|
+
# Job queue module not available, run in thread
|
|
382
|
+
import threading
|
|
383
|
+
threading.Thread(
|
|
384
|
+
target=_execute_pipeline,
|
|
385
|
+
args=(store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used),
|
|
386
|
+
daemon=True,
|
|
387
|
+
).start()
|
|
388
|
+
|
|
389
|
+
if usage_meter:
|
|
390
|
+
try:
|
|
391
|
+
usage_meter.record(tenant.tenant_id, "pipeline_run")
|
|
392
|
+
except Exception:
|
|
393
|
+
pass # optional
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"run_id": run.run_id,
|
|
397
|
+
"pipeline_id": pipeline_id,
|
|
398
|
+
"pipeline_name": pipeline["name"],
|
|
399
|
+
"graph": req.graph,
|
|
400
|
+
"status": "running",
|
|
401
|
+
"stages": [s["name"] for s in pipeline.get("stages", [])],
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ------------------------------------------------------------------
|
|
405
|
+
# POST /dashboard/pipelines/{id}/run/file — execute pipeline with file upload
|
|
406
|
+
# ------------------------------------------------------------------
|
|
407
|
+
@router.post("/dashboard/pipelines/{pipeline_id}/run/file")
|
|
408
|
+
async def run_pipeline_file(
|
|
409
|
+
pipeline_id: str,
|
|
410
|
+
file: UploadFile = File(...),
|
|
411
|
+
graph: str = Form(...),
|
|
412
|
+
data_classification: str = Form("internal"),
|
|
413
|
+
params: Optional[str] = Form(None),
|
|
414
|
+
governance_tags: Optional[str] = Form(None),
|
|
415
|
+
user=Depends(user_auth),
|
|
416
|
+
):
|
|
417
|
+
tenant = _get_tenant(user)
|
|
418
|
+
pipeline = store.get_pipeline(pipeline_id)
|
|
419
|
+
if not pipeline:
|
|
420
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
421
|
+
if not pipeline.get("enabled", True):
|
|
422
|
+
raise HTTPException(status_code=400, detail="Pipeline is disabled")
|
|
423
|
+
|
|
424
|
+
# Read file content — support binary files via temp file
|
|
425
|
+
content_bytes = await file.read()
|
|
426
|
+
source_hash = hashlib.sha256(content_bytes).hexdigest()
|
|
427
|
+
|
|
428
|
+
# Reject duplicate file
|
|
429
|
+
dup = store.find_duplicate(graph=graph, source_hash=source_hash, tenant_id=tenant.tenant_id)
|
|
430
|
+
if dup:
|
|
431
|
+
raise HTTPException(
|
|
432
|
+
status_code=409,
|
|
433
|
+
detail=f"Duplicate file: '{file.filename}' has identical content to "
|
|
434
|
+
f"'{dup['source']}' (run {dup['run_id'][:12]}, {dup['status']}). "
|
|
435
|
+
f"The same file was already ingested into graph '{graph}'.",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
import os, tempfile as _tf
|
|
439
|
+
from pathlib import Path as _P
|
|
440
|
+
_ext = _P(file.filename).suffix.lower() if file.filename else ""
|
|
441
|
+
_binary_exts = {".xlsx", ".xls", ".pdf", ".docx", ".doc", ".pptx"}
|
|
442
|
+
is_binary = _ext in _binary_exts
|
|
443
|
+
|
|
444
|
+
if is_binary:
|
|
445
|
+
# Save to temp file for extraction pipeline
|
|
446
|
+
fd, tmp_path = _tf.mkstemp(suffix=_ext, prefix="pipeline_upload_")
|
|
447
|
+
os.close(fd)
|
|
448
|
+
with open(tmp_path, "wb") as _f:
|
|
449
|
+
_f.write(content_bytes)
|
|
450
|
+
source_text = None
|
|
451
|
+
else:
|
|
452
|
+
tmp_path = None
|
|
453
|
+
try:
|
|
454
|
+
source_text = content_bytes.decode("utf-8")
|
|
455
|
+
except UnicodeDecodeError:
|
|
456
|
+
raise HTTPException(status_code=400, detail="File must be text-based (UTF-8)")
|
|
457
|
+
|
|
458
|
+
parsed_params = {}
|
|
459
|
+
if params:
|
|
460
|
+
try:
|
|
461
|
+
parsed_params = json.loads(params)
|
|
462
|
+
except Exception:
|
|
463
|
+
pass # optional
|
|
464
|
+
|
|
465
|
+
parsed_tags = []
|
|
466
|
+
if governance_tags:
|
|
467
|
+
try:
|
|
468
|
+
parsed_tags = json.loads(governance_tags)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass # optional
|
|
471
|
+
|
|
472
|
+
scoped_graph = _scope_graph(tenant, graph)
|
|
473
|
+
|
|
474
|
+
run = store.create_run(
|
|
475
|
+
pipeline_id=pipeline_id,
|
|
476
|
+
tenant_id=tenant.tenant_id,
|
|
477
|
+
user_id=user.user_id,
|
|
478
|
+
graph=graph,
|
|
479
|
+
source=file.filename or "uploaded_file",
|
|
480
|
+
source_type="file",
|
|
481
|
+
source_hash=source_hash,
|
|
482
|
+
params=parsed_params,
|
|
483
|
+
governance_tags=parsed_tags,
|
|
484
|
+
data_classification=data_classification,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Execute via async job queue or fallback to thread
|
|
488
|
+
import asyncio
|
|
489
|
+
try:
|
|
490
|
+
from ..jobs.queue import job_queue
|
|
491
|
+
try:
|
|
492
|
+
loop = asyncio.get_running_loop()
|
|
493
|
+
loop.create_task(_enqueue_pipeline(
|
|
494
|
+
job_queue, store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used
|
|
495
|
+
))
|
|
496
|
+
except RuntimeError:
|
|
497
|
+
import threading
|
|
498
|
+
threading.Thread(
|
|
499
|
+
target=_execute_pipeline,
|
|
500
|
+
args=(store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used),
|
|
501
|
+
daemon=True,
|
|
502
|
+
).start()
|
|
503
|
+
except ImportError:
|
|
504
|
+
import threading
|
|
505
|
+
threading.Thread(
|
|
506
|
+
target=_execute_pipeline,
|
|
507
|
+
args=(store, graph_registry, run.run_id, pipeline, scoped_graph, source_text, run.params_used),
|
|
508
|
+
daemon=True,
|
|
509
|
+
).start()
|
|
510
|
+
|
|
511
|
+
if usage_meter:
|
|
512
|
+
try:
|
|
513
|
+
usage_meter.record(tenant.tenant_id, "pipeline_run")
|
|
514
|
+
except Exception:
|
|
515
|
+
pass # optional
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
"run_id": run.run_id,
|
|
519
|
+
"pipeline_id": pipeline_id,
|
|
520
|
+
"pipeline_name": pipeline["name"],
|
|
521
|
+
"graph": graph,
|
|
522
|
+
"filename": file.filename,
|
|
523
|
+
"status": "running",
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# ------------------------------------------------------------------
|
|
527
|
+
# Step-wise Pipeline Execution Endpoints
|
|
528
|
+
# ------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
# In-memory store for active pipeline run contexts
|
|
531
|
+
_run_contexts: Dict[str, Any] = {}
|
|
532
|
+
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
# Async background helpers
|
|
535
|
+
# ------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
def _make_sub_step_callback():
|
|
538
|
+
"""Create a thread-safe callback that emits sub-step events."""
|
|
539
|
+
try:
|
|
540
|
+
from .events import event_bus
|
|
541
|
+
loop = asyncio.get_event_loop()
|
|
542
|
+
|
|
543
|
+
def _on_sub_step(run_id, stage, message, progress):
|
|
544
|
+
loop.call_soon_threadsafe(
|
|
545
|
+
event_bus.emit, "pipeline_sub_step",
|
|
546
|
+
{"run_id": run_id, "stage": stage, "message": message, "progress": progress},
|
|
547
|
+
)
|
|
548
|
+
return _on_sub_step
|
|
549
|
+
except Exception:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
def _emit_stage_event(ctx):
|
|
553
|
+
"""Emit a stage-level status update via event_bus."""
|
|
554
|
+
try:
|
|
555
|
+
from .events import event_bus
|
|
556
|
+
event_bus.emit("pipeline_stage_update", {
|
|
557
|
+
"run_id": ctx.run_id,
|
|
558
|
+
"status": ctx.status,
|
|
559
|
+
"current_stage": ctx.current_stage,
|
|
560
|
+
"stages": ctx.stages,
|
|
561
|
+
"completed_stages": [sr["stage_name"] for sr in ctx.stage_results],
|
|
562
|
+
"remaining_stages": ctx.remaining_stages,
|
|
563
|
+
"stage_results": ctx.stage_results,
|
|
564
|
+
"stage_output": ctx.stage_results[-1] if ctx.stage_results else None,
|
|
565
|
+
"sub_steps": ctx.sub_steps,
|
|
566
|
+
"nodes_count": len(ctx.nodes),
|
|
567
|
+
"edges_count": len(ctx.edges),
|
|
568
|
+
"chunks_count": len(ctx.chunks),
|
|
569
|
+
"classification": ctx.classification,
|
|
570
|
+
})
|
|
571
|
+
except Exception:
|
|
572
|
+
pass # optional
|
|
573
|
+
|
|
574
|
+
def _resolve_remaining_stages(ctx):
|
|
575
|
+
"""Determine remaining stages after CLASSIFY.
|
|
576
|
+
|
|
577
|
+
If the user selected a specific pipeline template, use its stage
|
|
578
|
+
sequence (skipping PARSE_FILE/CLASSIFY which already ran).
|
|
579
|
+
Otherwise fall back to the ScenarioRouter.
|
|
580
|
+
"""
|
|
581
|
+
pipeline_id = ctx.pipeline_id or "builtin:auto"
|
|
582
|
+
|
|
583
|
+
# If a specific template was selected, use its stages
|
|
584
|
+
if pipeline_id and pipeline_id != "builtin:auto":
|
|
585
|
+
pipeline = store.get_pipeline(pipeline_id)
|
|
586
|
+
if pipeline and pipeline.get("stages"):
|
|
587
|
+
# Extract stage types, skip the ones already executed
|
|
588
|
+
template_stages = [
|
|
589
|
+
s["type"] for s in pipeline["stages"]
|
|
590
|
+
if s.get("type") not in ("PARSE_FILE", "CLASSIFY")
|
|
591
|
+
]
|
|
592
|
+
if template_stages:
|
|
593
|
+
logger.info(
|
|
594
|
+
"Using template stages for %s: %s", pipeline_id, template_stages,
|
|
595
|
+
)
|
|
596
|
+
return template_stages
|
|
597
|
+
|
|
598
|
+
# Fallback: use the scenario router
|
|
599
|
+
if not ctx.classification:
|
|
600
|
+
return []
|
|
601
|
+
|
|
602
|
+
from ..ingestion.pipeline_context import ClassificationResult
|
|
603
|
+
from ..ingestion.scenario_router import ScenarioRouter
|
|
604
|
+
classification = ClassificationResult.from_dict(ctx.classification)
|
|
605
|
+
router_obj = ScenarioRouter()
|
|
606
|
+
return router_obj.route(classification, ctx.intent)
|
|
607
|
+
|
|
608
|
+
def _filter_stages_by_config(stages, ctx):
|
|
609
|
+
"""Remove stages that require LLM/embeddings if those aren't configured."""
|
|
610
|
+
llm = ctx.pipeline_params.get("llm_model")
|
|
611
|
+
emb = ctx.pipeline_params.get("embedding_model")
|
|
612
|
+
filtered = stages
|
|
613
|
+
if not llm:
|
|
614
|
+
filtered = [s for s in filtered if s not in ("EXTRACT", "EXTRACT_FACTS", "ENHANCE_GRAPH")]
|
|
615
|
+
if not emb:
|
|
616
|
+
filtered = [s for s in filtered if s not in ("EMBED", "STORE_VECTORS")]
|
|
617
|
+
# INDEX_BM25 depends on facts which need LLM
|
|
618
|
+
if not llm:
|
|
619
|
+
filtered = [s for s in filtered if s != "INDEX_BM25"]
|
|
620
|
+
return filtered
|
|
621
|
+
|
|
622
|
+
def _sync_to_db(ctx):
|
|
623
|
+
"""Sync current PipelineContext state to SQLite for persistence."""
|
|
624
|
+
try:
|
|
625
|
+
store.update_run_status(ctx.run_id, ctx.status)
|
|
626
|
+
store.sync_stages_log(ctx.run_id, ctx.stage_results)
|
|
627
|
+
store.update_run_counts(
|
|
628
|
+
ctx.run_id,
|
|
629
|
+
nodes=len(ctx.nodes),
|
|
630
|
+
edges=len(ctx.edges),
|
|
631
|
+
entities=len([n for n in ctx.nodes if n.get("properties", {}).get("extraction_method") == "llm_extraction"]),
|
|
632
|
+
)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.warning("Failed to sync run %s to DB: %s", ctx.run_id, e)
|
|
635
|
+
|
|
636
|
+
async def _cleanup_after_delay(run_id, delay=300):
|
|
637
|
+
"""Remove completed run from in-memory store after TTL."""
|
|
638
|
+
await asyncio.sleep(delay)
|
|
639
|
+
_run_contexts.pop(run_id, None)
|
|
640
|
+
|
|
641
|
+
async def _run_next_background(ctx, executor):
|
|
642
|
+
"""Run one stage in a thread and emit status update when done."""
|
|
643
|
+
try:
|
|
644
|
+
ctx.status = "running"
|
|
645
|
+
store.update_run_status(ctx.run_id, "running")
|
|
646
|
+
_emit_stage_event(ctx)
|
|
647
|
+
await asyncio.to_thread(executor.execute_next, ctx)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.error("Background stage failed: %s", e, exc_info=True)
|
|
650
|
+
ctx.status = "failed"
|
|
651
|
+
ctx.record_sub_step(f"Error: {e}")
|
|
652
|
+
_sync_to_db(ctx)
|
|
653
|
+
_emit_stage_event(ctx)
|
|
654
|
+
# Keep in memory for 5 min after terminal state
|
|
655
|
+
if ctx.status in ("completed", "failed"):
|
|
656
|
+
asyncio.create_task(_cleanup_after_delay(ctx.run_id))
|
|
657
|
+
|
|
658
|
+
async def _run_all_background(ctx, executor):
|
|
659
|
+
"""Run all stages in a thread, routing after CLASSIFY."""
|
|
660
|
+
try:
|
|
661
|
+
ctx.status = "running"
|
|
662
|
+
store.update_run_status(ctx.run_id, "running")
|
|
663
|
+
_emit_stage_event(ctx)
|
|
664
|
+
# Parse + classify first
|
|
665
|
+
await asyncio.to_thread(executor.execute_all, ctx)
|
|
666
|
+
_sync_to_db(ctx) # sync after initial stages
|
|
667
|
+
|
|
668
|
+
if ctx.status != "failed" and ctx.classification:
|
|
669
|
+
remaining = _resolve_remaining_stages(ctx)
|
|
670
|
+
remaining = _filter_stages_by_config(remaining, ctx)
|
|
671
|
+
ctx.stages = remaining
|
|
672
|
+
ctx.current_stage_index = 0
|
|
673
|
+
ctx.status = "running"
|
|
674
|
+
_emit_stage_event(ctx)
|
|
675
|
+
await asyncio.to_thread(executor.execute_all, ctx)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
logger.error("Background run-all failed: %s", e, exc_info=True)
|
|
678
|
+
ctx.status = "failed"
|
|
679
|
+
ctx.record_sub_step(f"Error: {e}")
|
|
680
|
+
_sync_to_db(ctx)
|
|
681
|
+
_emit_stage_event(ctx)
|
|
682
|
+
# Keep in memory for 5 min after terminal state
|
|
683
|
+
if ctx.status in ("completed", "failed"):
|
|
684
|
+
asyncio.create_task(_cleanup_after_delay(ctx.run_id))
|
|
685
|
+
|
|
686
|
+
@router.post("/dashboard/pipelines/runs/start")
|
|
687
|
+
async def start_pipeline_run(
|
|
688
|
+
graph: str = Form(..., min_length=1),
|
|
689
|
+
pipeline_id: Optional[str] = Form(None),
|
|
690
|
+
intent: str = Form("graph_rag"),
|
|
691
|
+
mode: str = Form("step_by_step"),
|
|
692
|
+
text: Optional[str] = Form(None),
|
|
693
|
+
url: Optional[str] = Form(None),
|
|
694
|
+
params: Optional[str] = Form(None),
|
|
695
|
+
file: Optional[UploadFile] = File(None),
|
|
696
|
+
user=Depends(user_auth),
|
|
697
|
+
):
|
|
698
|
+
"""Start a new pipeline run (step-by-step or run-all)."""
|
|
699
|
+
tenant = _get_tenant(user)
|
|
700
|
+
scoped = _scope_graph(tenant, graph)
|
|
701
|
+
|
|
702
|
+
from ..ingestion.pipeline_context import PipelineContext
|
|
703
|
+
from ..ingestion.stage_executor import StageExecutor
|
|
704
|
+
from ..ingestion.scenario_router import ScenarioRouter, STAGE_PARSE_FILE, STAGE_CLASSIFY
|
|
705
|
+
|
|
706
|
+
parsed_params = json.loads(params) if params else {}
|
|
707
|
+
|
|
708
|
+
# Determine source
|
|
709
|
+
source_file = None
|
|
710
|
+
source_text = text
|
|
711
|
+
source_hash = ""
|
|
712
|
+
|
|
713
|
+
if file and file.filename:
|
|
714
|
+
import os, tempfile as _tf
|
|
715
|
+
from pathlib import Path as _Path
|
|
716
|
+
content = await file.read()
|
|
717
|
+
source_hash = hashlib.sha256(content).hexdigest()
|
|
718
|
+
|
|
719
|
+
# Reject duplicate file (same content already ingested into this graph)
|
|
720
|
+
dup = store.find_duplicate(graph=graph, source_hash=source_hash, tenant_id=tenant.tenant_id)
|
|
721
|
+
if dup:
|
|
722
|
+
raise HTTPException(
|
|
723
|
+
status_code=409,
|
|
724
|
+
detail=f"Duplicate file: '{file.filename}' has identical content to "
|
|
725
|
+
f"'{dup['source']}' (run {dup['run_id'][:12]}, {dup['status']}). "
|
|
726
|
+
f"The same file was already ingested into graph '{graph}'.",
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
ext = _Path(file.filename).suffix.lower() if file.filename else ""
|
|
730
|
+
fd, tmp = _tf.mkstemp(suffix=ext, prefix="run_upload_")
|
|
731
|
+
os.close(fd)
|
|
732
|
+
with open(tmp, "wb") as f:
|
|
733
|
+
f.write(content)
|
|
734
|
+
source_file = tmp
|
|
735
|
+
elif source_text:
|
|
736
|
+
source_hash = hashlib.sha256(source_text.encode()).hexdigest()
|
|
737
|
+
dup = store.find_duplicate(graph=graph, source_hash=source_hash, tenant_id=tenant.tenant_id)
|
|
738
|
+
if dup:
|
|
739
|
+
raise HTTPException(
|
|
740
|
+
status_code=409,
|
|
741
|
+
detail=f"Duplicate content: identical text was already ingested into graph '{graph}' "
|
|
742
|
+
f"(run {dup['run_id'][:12]}, {dup['status']}).",
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
ctx = PipelineContext(
|
|
746
|
+
graph_namespace=scoped,
|
|
747
|
+
stages=[STAGE_PARSE_FILE, STAGE_CLASSIFY],
|
|
748
|
+
source_text=source_text,
|
|
749
|
+
source_bytes_path=source_file,
|
|
750
|
+
source_filename=file.filename if file else None,
|
|
751
|
+
source_hash=source_hash,
|
|
752
|
+
intent=intent,
|
|
753
|
+
mode=mode,
|
|
754
|
+
pipeline_params=parsed_params,
|
|
755
|
+
pipeline_id=pipeline_id or "builtin:auto",
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
executor = StageExecutor(
|
|
759
|
+
graph_registry=graph_registry,
|
|
760
|
+
on_sub_step=_make_sub_step_callback(),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
_run_contexts[ctx.run_id] = {"ctx": ctx, "executor": executor}
|
|
764
|
+
|
|
765
|
+
# Launch execution in background — return run_id immediately
|
|
766
|
+
if mode == "run_all":
|
|
767
|
+
asyncio.create_task(_run_all_background(ctx, executor))
|
|
768
|
+
else:
|
|
769
|
+
asyncio.create_task(_run_next_background(ctx, executor))
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
"run_id": ctx.run_id,
|
|
773
|
+
"status": ctx.status,
|
|
774
|
+
"stages": ctx.stages,
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
@router.get("/dashboard/pipelines/runs/{run_id}/context")
|
|
778
|
+
async def get_run_context(run_id: str, user=Depends(user_auth)):
|
|
779
|
+
"""Get the current pipeline context for a step-wise run."""
|
|
780
|
+
entry = _run_contexts.get(run_id)
|
|
781
|
+
if entry:
|
|
782
|
+
ctx = entry["ctx"]
|
|
783
|
+
return {
|
|
784
|
+
"run_id": ctx.run_id,
|
|
785
|
+
"status": ctx.status,
|
|
786
|
+
"mode": ctx.mode,
|
|
787
|
+
"current_stage": ctx.current_stage,
|
|
788
|
+
"current_stage_index": ctx.current_stage_index,
|
|
789
|
+
"stages": ctx.stages,
|
|
790
|
+
"remaining_stages": ctx.remaining_stages,
|
|
791
|
+
"stage_results": ctx.stage_results,
|
|
792
|
+
"classification": ctx.classification,
|
|
793
|
+
"nodes_count": len(ctx.nodes),
|
|
794
|
+
"edges_count": len(ctx.edges),
|
|
795
|
+
"chunks_count": len(ctx.chunks),
|
|
796
|
+
"nodes_preview": ctx.nodes[:20],
|
|
797
|
+
"edges_preview": ctx.edges[:20],
|
|
798
|
+
"intent": ctx.intent,
|
|
799
|
+
"sub_steps": ctx.sub_steps,
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
# Fallback: read from SQLite (run completed or server restarted)
|
|
803
|
+
run = store.get_run(run_id)
|
|
804
|
+
if not run:
|
|
805
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
806
|
+
stages_log = run.get("stages") or []
|
|
807
|
+
return {
|
|
808
|
+
"run_id": run_id,
|
|
809
|
+
"status": run.get("status", "unknown"),
|
|
810
|
+
"mode": run.get("mode", "run_all"),
|
|
811
|
+
"current_stage": None,
|
|
812
|
+
"current_stage_index": len(stages_log),
|
|
813
|
+
"stages": [s.get("stage_name", "") for s in stages_log] if stages_log else [],
|
|
814
|
+
"remaining_stages": [],
|
|
815
|
+
"stage_results": stages_log,
|
|
816
|
+
"classification": None,
|
|
817
|
+
"nodes_count": run.get("nodes_created", 0),
|
|
818
|
+
"edges_count": run.get("edges_created", 0),
|
|
819
|
+
"chunks_count": 0,
|
|
820
|
+
"nodes_preview": [],
|
|
821
|
+
"edges_preview": [],
|
|
822
|
+
"intent": run.get("intent", "graph_rag"),
|
|
823
|
+
"sub_steps": [],
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
@router.patch("/dashboard/pipelines/runs/{run_id}/context")
|
|
827
|
+
async def edit_run_context(run_id: str, req: EditContextRequest, user=Depends(user_auth)):
|
|
828
|
+
"""Edit intermediate data in a paused pipeline run."""
|
|
829
|
+
entry = _run_contexts.get(run_id)
|
|
830
|
+
if not entry:
|
|
831
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
832
|
+
ctx = entry["ctx"]
|
|
833
|
+
if ctx.status not in ("paused", "pending"):
|
|
834
|
+
raise HTTPException(status_code=400, detail=f"Run is {ctx.status}, not editable")
|
|
835
|
+
|
|
836
|
+
overrides = {k: v for k, v in req.model_dump().items() if v is not None}
|
|
837
|
+
ctx.apply_overrides(overrides)
|
|
838
|
+
|
|
839
|
+
# If classification was changed, re-route stages
|
|
840
|
+
if "classification" in overrides:
|
|
841
|
+
from ..ingestion.pipeline_context import ClassificationResult
|
|
842
|
+
from ..ingestion.scenario_router import ScenarioRouter
|
|
843
|
+
classification = ClassificationResult.from_dict(ctx.classification)
|
|
844
|
+
router_obj = ScenarioRouter()
|
|
845
|
+
new_stages = router_obj.route(classification, ctx.intent)
|
|
846
|
+
ctx.stages = new_stages
|
|
847
|
+
ctx.current_stage_index = 0
|
|
848
|
+
|
|
849
|
+
return {"message": "Context updated", "stages": ctx.stages}
|
|
850
|
+
|
|
851
|
+
@router.post("/dashboard/pipelines/runs/{run_id}/continue")
|
|
852
|
+
async def continue_run(run_id: str, user=Depends(user_auth)):
|
|
853
|
+
"""Execute the next stage of a paused pipeline run (async)."""
|
|
854
|
+
entry = _run_contexts.get(run_id)
|
|
855
|
+
if not entry:
|
|
856
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
857
|
+
ctx = entry["ctx"]
|
|
858
|
+
executor = entry["executor"]
|
|
859
|
+
|
|
860
|
+
if ctx.is_done:
|
|
861
|
+
return {"run_id": run_id, "status": "completed", "message": "All stages done"}
|
|
862
|
+
|
|
863
|
+
if ctx.status == "running":
|
|
864
|
+
return {"run_id": run_id, "status": "running", "message": "Already running"}
|
|
865
|
+
|
|
866
|
+
# After CLASSIFY, route remaining stages
|
|
867
|
+
if ctx.current_stage is None and ctx.classification and not ctx.is_done:
|
|
868
|
+
remaining = _resolve_remaining_stages(ctx)
|
|
869
|
+
remaining = _filter_stages_by_config(remaining, ctx)
|
|
870
|
+
ctx.stages = remaining
|
|
871
|
+
ctx.current_stage_index = 0
|
|
872
|
+
|
|
873
|
+
# Launch in background, return immediately
|
|
874
|
+
asyncio.create_task(_run_next_background(ctx, executor))
|
|
875
|
+
|
|
876
|
+
return {"run_id": run_id, "status": "running", "current_stage": ctx.current_stage}
|
|
877
|
+
|
|
878
|
+
@router.post("/dashboard/pipelines/runs/{run_id}/run-all")
|
|
879
|
+
async def run_all_remaining(run_id: str, user=Depends(user_auth)):
|
|
880
|
+
"""Execute all remaining stages without pausing (async)."""
|
|
881
|
+
entry = _run_contexts.get(run_id)
|
|
882
|
+
if not entry:
|
|
883
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
884
|
+
ctx = entry["ctx"]
|
|
885
|
+
executor = entry["executor"]
|
|
886
|
+
|
|
887
|
+
if ctx.is_done:
|
|
888
|
+
return {"run_id": run_id, "status": "completed"}
|
|
889
|
+
|
|
890
|
+
if ctx.status == "running":
|
|
891
|
+
return {"run_id": run_id, "status": "running", "message": "Already running"}
|
|
892
|
+
|
|
893
|
+
# Route if needed
|
|
894
|
+
if ctx.current_stage is None and ctx.classification:
|
|
895
|
+
remaining = _resolve_remaining_stages(ctx)
|
|
896
|
+
remaining = _filter_stages_by_config(remaining, ctx)
|
|
897
|
+
ctx.stages = remaining
|
|
898
|
+
ctx.current_stage_index = 0
|
|
899
|
+
|
|
900
|
+
# Launch in background, return immediately
|
|
901
|
+
asyncio.create_task(_run_all_background(ctx, executor))
|
|
902
|
+
|
|
903
|
+
return {"run_id": run_id, "status": "running"}
|
|
904
|
+
|
|
905
|
+
@router.post("/dashboard/pipelines/runs/{run_id}/skip")
|
|
906
|
+
async def skip_stage(run_id: str, user=Depends(user_auth)):
|
|
907
|
+
"""Skip the current stage and advance."""
|
|
908
|
+
entry = _run_contexts.get(run_id)
|
|
909
|
+
if not entry:
|
|
910
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
911
|
+
ctx = entry["ctx"]
|
|
912
|
+
if ctx.is_done:
|
|
913
|
+
return {"run_id": run_id, "status": "completed"}
|
|
914
|
+
|
|
915
|
+
skipped = ctx.current_stage
|
|
916
|
+
ctx.skip_stage()
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
"run_id": run_id,
|
|
920
|
+
"skipped": skipped,
|
|
921
|
+
"status": ctx.status,
|
|
922
|
+
"current_stage": ctx.current_stage,
|
|
923
|
+
"remaining_stages": ctx.remaining_stages,
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
@router.delete("/dashboard/pipelines/runs/{run_id}/cancel")
|
|
927
|
+
async def cancel_run(run_id: str, user=Depends(user_auth)):
|
|
928
|
+
"""Cancel an active pipeline run."""
|
|
929
|
+
entry = _run_contexts.pop(run_id, None)
|
|
930
|
+
if not entry:
|
|
931
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
932
|
+
ctx = entry["ctx"]
|
|
933
|
+
ctx.status = "cancelled"
|
|
934
|
+
|
|
935
|
+
# Clean up temp file
|
|
936
|
+
if ctx.source_bytes_path:
|
|
937
|
+
import os
|
|
938
|
+
try:
|
|
939
|
+
if os.path.exists(ctx.source_bytes_path):
|
|
940
|
+
os.unlink(ctx.source_bytes_path)
|
|
941
|
+
except Exception:
|
|
942
|
+
pass # optional
|
|
943
|
+
|
|
944
|
+
return {"run_id": run_id, "status": "cancelled"}
|
|
945
|
+
|
|
946
|
+
# ------------------------------------------------------------------
|
|
947
|
+
# POST /dashboard/pipelines/runs/batch — multi-file ingestion
|
|
948
|
+
# ------------------------------------------------------------------
|
|
949
|
+
@router.post("/dashboard/pipelines/runs/batch")
|
|
950
|
+
async def batch_pipeline_run(
|
|
951
|
+
files: List[UploadFile] = File(...),
|
|
952
|
+
graph: str = Form(...),
|
|
953
|
+
intent: str = Form("graph_rag"),
|
|
954
|
+
mode: str = Form("run_all"),
|
|
955
|
+
pipeline_id: Optional[str] = Form(None),
|
|
956
|
+
params: Optional[str] = Form(None),
|
|
957
|
+
user=Depends(user_auth),
|
|
958
|
+
):
|
|
959
|
+
"""
|
|
960
|
+
Multi-file batch ingestion.
|
|
961
|
+
|
|
962
|
+
Each file is classified independently and routed through the best
|
|
963
|
+
pipeline. Results are merged into the same target graph.
|
|
964
|
+
"""
|
|
965
|
+
tenant = _get_tenant(user)
|
|
966
|
+
scoped = _scope_graph(tenant, graph)
|
|
967
|
+
|
|
968
|
+
from ..ingestion.pipeline_context import PipelineContext
|
|
969
|
+
from ..ingestion.stage_executor import StageExecutor
|
|
970
|
+
from ..ingestion.scenario_router import ScenarioRouter, STAGE_PARSE_FILE, STAGE_CLASSIFY
|
|
971
|
+
from pathlib import Path as _P
|
|
972
|
+
|
|
973
|
+
parsed_params = {}
|
|
974
|
+
if params:
|
|
975
|
+
try:
|
|
976
|
+
parsed_params = json.loads(params)
|
|
977
|
+
except Exception:
|
|
978
|
+
pass # optional
|
|
979
|
+
|
|
980
|
+
executor = StageExecutor(graph_registry=graph_registry)
|
|
981
|
+
router_obj = ScenarioRouter()
|
|
982
|
+
results = []
|
|
983
|
+
|
|
984
|
+
for upload in files:
|
|
985
|
+
import os, tempfile as _tf
|
|
986
|
+
content = await upload.read()
|
|
987
|
+
ext = _P(upload.filename).suffix.lower() if upload.filename else ""
|
|
988
|
+
_binary_exts = {".xlsx", ".xls", ".pdf", ".docx", ".doc", ".pptx"}
|
|
989
|
+
|
|
990
|
+
if ext in _binary_exts:
|
|
991
|
+
fd, tmp = _tf.mkstemp(suffix=ext, prefix="batch_")
|
|
992
|
+
os.close(fd)
|
|
993
|
+
with open(tmp, "wb") as f:
|
|
994
|
+
f.write(content)
|
|
995
|
+
source_file = tmp
|
|
996
|
+
source_text = None
|
|
997
|
+
else:
|
|
998
|
+
source_file = None
|
|
999
|
+
try:
|
|
1000
|
+
source_text = content.decode("utf-8")
|
|
1001
|
+
except UnicodeDecodeError:
|
|
1002
|
+
results.append({
|
|
1003
|
+
"filename": upload.filename,
|
|
1004
|
+
"status": "failed",
|
|
1005
|
+
"error": "Not a valid UTF-8 text file",
|
|
1006
|
+
})
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
ctx = PipelineContext(
|
|
1010
|
+
graph_namespace=scoped,
|
|
1011
|
+
stages=[STAGE_PARSE_FILE, STAGE_CLASSIFY],
|
|
1012
|
+
source_text=source_text,
|
|
1013
|
+
source_bytes_path=source_file,
|
|
1014
|
+
source_filename=upload.filename,
|
|
1015
|
+
intent=intent,
|
|
1016
|
+
mode="run_all",
|
|
1017
|
+
pipeline_params=parsed_params,
|
|
1018
|
+
pipeline_id=pipeline_id or "builtin:auto",
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
try:
|
|
1022
|
+
# Parse + classify
|
|
1023
|
+
executor.execute_all(ctx)
|
|
1024
|
+
|
|
1025
|
+
# Route remaining stages
|
|
1026
|
+
if ctx.status != "failed" and ctx.classification:
|
|
1027
|
+
from ..ingestion.pipeline_context import ClassificationResult
|
|
1028
|
+
classification = ClassificationResult.from_dict(ctx.classification)
|
|
1029
|
+
remaining = router_obj.route(classification, intent)
|
|
1030
|
+
|
|
1031
|
+
if not parsed_params.get("llm_model"):
|
|
1032
|
+
remaining = [s for s in remaining if s not in ("EXTRACT", "ENHANCE_GRAPH")]
|
|
1033
|
+
if not parsed_params.get("embedding_model"):
|
|
1034
|
+
remaining = [s for s in remaining if s != "EMBED"]
|
|
1035
|
+
|
|
1036
|
+
ctx.stages = remaining
|
|
1037
|
+
ctx.current_stage_index = 0
|
|
1038
|
+
ctx.status = "running"
|
|
1039
|
+
executor.execute_all(ctx)
|
|
1040
|
+
|
|
1041
|
+
results.append({
|
|
1042
|
+
"filename": upload.filename,
|
|
1043
|
+
"run_id": ctx.run_id,
|
|
1044
|
+
"status": ctx.status,
|
|
1045
|
+
"classification": ctx.classification,
|
|
1046
|
+
"nodes": len(ctx.nodes),
|
|
1047
|
+
"edges": len(ctx.edges),
|
|
1048
|
+
"stages_completed": [sr["stage_name"] for sr in ctx.stage_results],
|
|
1049
|
+
})
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
logger.error("Batch file %s failed: %s", upload.filename, e)
|
|
1052
|
+
results.append({
|
|
1053
|
+
"filename": upload.filename,
|
|
1054
|
+
"status": "failed",
|
|
1055
|
+
"error": str(e),
|
|
1056
|
+
})
|
|
1057
|
+
finally:
|
|
1058
|
+
# Clean up temp file
|
|
1059
|
+
if source_file:
|
|
1060
|
+
try:
|
|
1061
|
+
os.unlink(source_file)
|
|
1062
|
+
except Exception:
|
|
1063
|
+
pass # optional
|
|
1064
|
+
|
|
1065
|
+
total_nodes = sum(r.get("nodes", 0) for r in results)
|
|
1066
|
+
total_edges = sum(r.get("edges", 0) for r in results)
|
|
1067
|
+
succeeded = sum(1 for r in results if r.get("status") == "completed")
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
"graph": graph,
|
|
1071
|
+
"files_total": len(files),
|
|
1072
|
+
"files_succeeded": succeeded,
|
|
1073
|
+
"files_failed": len(files) - succeeded,
|
|
1074
|
+
"total_nodes": total_nodes,
|
|
1075
|
+
"total_edges": total_edges,
|
|
1076
|
+
"results": results,
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
# ------------------------------------------------------------------
|
|
1080
|
+
# Schema Management
|
|
1081
|
+
# ------------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
# In-memory schema store (per-graph)
|
|
1084
|
+
_graph_schemas: Dict[str, Dict[str, Any]] = {}
|
|
1085
|
+
|
|
1086
|
+
@router.post("/dashboard/pipelines/schemas")
|
|
1087
|
+
async def upload_schema(req: UploadSchemaRequest, user=Depends(user_auth)):
|
|
1088
|
+
"""Upload a YAML schema for schema-first ingestion."""
|
|
1089
|
+
tenant = _get_tenant(user)
|
|
1090
|
+
|
|
1091
|
+
try:
|
|
1092
|
+
import yaml
|
|
1093
|
+
data = yaml.safe_load(req.schema_yaml)
|
|
1094
|
+
if not isinstance(data, dict):
|
|
1095
|
+
raise HTTPException(status_code=400, detail="Invalid YAML: expected a mapping")
|
|
1096
|
+
except Exception as e:
|
|
1097
|
+
raise HTTPException(status_code=400, detail=f"YAML parse error: {e}")
|
|
1098
|
+
|
|
1099
|
+
# Validate schema structure
|
|
1100
|
+
try:
|
|
1101
|
+
import tempfile, os
|
|
1102
|
+
fd, tmp = tempfile.mkstemp(suffix=".yaml", prefix="schema_")
|
|
1103
|
+
os.close(fd)
|
|
1104
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
1105
|
+
f.write(req.schema_yaml)
|
|
1106
|
+
try:
|
|
1107
|
+
from ..schema.schema_parser import SchemaParser
|
|
1108
|
+
parser = SchemaParser(tmp)
|
|
1109
|
+
schema = parser.parse()
|
|
1110
|
+
finally:
|
|
1111
|
+
os.unlink(tmp)
|
|
1112
|
+
except Exception as e:
|
|
1113
|
+
raise HTTPException(status_code=400, detail=f"Schema validation error: {e}")
|
|
1114
|
+
|
|
1115
|
+
scoped = _scope_graph(tenant, req.graph)
|
|
1116
|
+
schema_entry = {
|
|
1117
|
+
"graph": req.graph,
|
|
1118
|
+
"scoped_graph": scoped,
|
|
1119
|
+
"name": req.name or data.get("NAMESPACE", req.graph),
|
|
1120
|
+
"strict": req.strict,
|
|
1121
|
+
"schema_yaml": req.schema_yaml,
|
|
1122
|
+
"node_types": list(schema.node_types.keys()),
|
|
1123
|
+
"edge_types": list(schema.edge_types.keys()),
|
|
1124
|
+
"version": schema.version,
|
|
1125
|
+
}
|
|
1126
|
+
_graph_schemas[scoped] = schema_entry
|
|
1127
|
+
|
|
1128
|
+
# Generate extraction hints
|
|
1129
|
+
from ..ingestion.schema_validator import SchemaValidator
|
|
1130
|
+
validator = SchemaValidator(schema)
|
|
1131
|
+
hints = validator.get_extraction_hints()
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
"message": "Schema uploaded",
|
|
1135
|
+
"schema": schema_entry,
|
|
1136
|
+
"extraction_hints": hints,
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
@router.get("/dashboard/pipelines/schemas/{graph_name}")
|
|
1140
|
+
async def get_schema(graph_name: str, user=Depends(user_auth)):
|
|
1141
|
+
"""Get the schema for a graph."""
|
|
1142
|
+
tenant = _get_tenant(user)
|
|
1143
|
+
scoped = _scope_graph(tenant, graph_name)
|
|
1144
|
+
entry = _graph_schemas.get(scoped)
|
|
1145
|
+
if not entry:
|
|
1146
|
+
raise HTTPException(status_code=404, detail="No schema found for this graph")
|
|
1147
|
+
return {"schema": entry}
|
|
1148
|
+
|
|
1149
|
+
@router.delete("/dashboard/pipelines/schemas/{graph_name}")
|
|
1150
|
+
async def delete_schema(graph_name: str, user=Depends(user_auth)):
|
|
1151
|
+
"""Remove the schema for a graph."""
|
|
1152
|
+
tenant = _get_tenant(user)
|
|
1153
|
+
scoped = _scope_graph(tenant, graph_name)
|
|
1154
|
+
if scoped not in _graph_schemas:
|
|
1155
|
+
raise HTTPException(status_code=404, detail="No schema found")
|
|
1156
|
+
del _graph_schemas[scoped]
|
|
1157
|
+
return {"message": "Schema removed"}
|
|
1158
|
+
|
|
1159
|
+
return router
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
# ---------------------------------------------------------------------------
|
|
1163
|
+
# Pipeline Executor — delegates to AIQL executor
|
|
1164
|
+
# ---------------------------------------------------------------------------
|
|
1165
|
+
|
|
1166
|
+
def _execute_pipeline(
|
|
1167
|
+
store: PipelineStore,
|
|
1168
|
+
graph_registry,
|
|
1169
|
+
run_id: str,
|
|
1170
|
+
pipeline: Dict,
|
|
1171
|
+
scoped_graph: str,
|
|
1172
|
+
text: str,
|
|
1173
|
+
params: Dict,
|
|
1174
|
+
):
|
|
1175
|
+
"""
|
|
1176
|
+
Execute pipeline stages via the AIQL executor.
|
|
1177
|
+
|
|
1178
|
+
Writes text as a normalized JSON document to disk, then dispatches each
|
|
1179
|
+
stage to the executor's internal stage methods — identical to AIQL
|
|
1180
|
+
PIPELINE execution.
|
|
1181
|
+
"""
|
|
1182
|
+
import os
|
|
1183
|
+
import tempfile
|
|
1184
|
+
from pathlib import Path
|
|
1185
|
+
from ..aiql.engine import AIQLExecutor
|
|
1186
|
+
|
|
1187
|
+
store.update_run_status(run_id, "running")
|
|
1188
|
+
accuracy = AccuracyScore()
|
|
1189
|
+
normalized_file = None # cleanup later
|
|
1190
|
+
|
|
1191
|
+
try:
|
|
1192
|
+
# Create executor wired to the graph registry
|
|
1193
|
+
logger.info(f"[pipeline] Starting run {run_id} on graph={scoped_graph}")
|
|
1194
|
+
executor = AIQLExecutor(graph_registry=graph_registry)
|
|
1195
|
+
|
|
1196
|
+
# Ensure graph exists (use registry directly — scoped names contain ':')
|
|
1197
|
+
db = graph_registry.get_graph(scoped_graph, load_if_missing=True)
|
|
1198
|
+
if not db:
|
|
1199
|
+
db = graph_registry.create_graph(scoped_graph)
|
|
1200
|
+
executor.active_namespace = scoped_graph
|
|
1201
|
+
|
|
1202
|
+
namespace = scoped_graph
|
|
1203
|
+
if not db:
|
|
1204
|
+
raise RuntimeError(f"Could not access graph '{scoped_graph}'")
|
|
1205
|
+
|
|
1206
|
+
# Count nodes/edges before to calculate deltas
|
|
1207
|
+
nodes_before = len(db.get_all_nodes())
|
|
1208
|
+
edges_before = len(db.get_all_edges())
|
|
1209
|
+
|
|
1210
|
+
# ---------------------------------------------------------------
|
|
1211
|
+
# Write text as a normalized JSON file that the executor expects
|
|
1212
|
+
# ---------------------------------------------------------------
|
|
1213
|
+
document_id = f"pipeline_doc_{run_id[:12]}"
|
|
1214
|
+
safe_ns = namespace.replace(":", "__")
|
|
1215
|
+
norm_dir = Path("contextcore_data") / "namespaces" / safe_ns / "documents" / document_id / "normalized"
|
|
1216
|
+
norm_dir.mkdir(parents=True, exist_ok=True)
|
|
1217
|
+
normalized_file = norm_dir / "normalized_v1.json"
|
|
1218
|
+
|
|
1219
|
+
# Build pages (group paragraphs into ~2000-char pages)
|
|
1220
|
+
pages = _split_into_pages(text)
|
|
1221
|
+
normalized_doc = {
|
|
1222
|
+
"document_id": document_id,
|
|
1223
|
+
"version": "v1",
|
|
1224
|
+
"content": {
|
|
1225
|
+
"pages": [
|
|
1226
|
+
{
|
|
1227
|
+
"page_no": i + 1,
|
|
1228
|
+
"flat_text": page_text,
|
|
1229
|
+
"text": page_text,
|
|
1230
|
+
"elements": [{"type": "text", "content": page_text}],
|
|
1231
|
+
}
|
|
1232
|
+
for i, page_text in enumerate(pages)
|
|
1233
|
+
],
|
|
1234
|
+
},
|
|
1235
|
+
}
|
|
1236
|
+
normalized_file.write_text(json.dumps(normalized_doc, ensure_ascii=False), encoding="utf-8")
|
|
1237
|
+
logger.info(f"[pipeline] Wrote normalized doc ({len(pages)} pages) to {normalized_file}")
|
|
1238
|
+
|
|
1239
|
+
# ---------------------------------------------------------------
|
|
1240
|
+
# Create Document node in the graph
|
|
1241
|
+
# ---------------------------------------------------------------
|
|
1242
|
+
from ..core.graph_structures import GraphNode, GraphEdge
|
|
1243
|
+
|
|
1244
|
+
doc_node_id = f"doc_{document_id}"
|
|
1245
|
+
# Fetch run record to get the actual source URL/label
|
|
1246
|
+
run_record = store.get_run(run_id)
|
|
1247
|
+
run_source = run_record.get("source", "pipeline") if run_record else "pipeline"
|
|
1248
|
+
run_source_type = run_record.get("source_type", "unknown") if run_record else "unknown"
|
|
1249
|
+
# Use actual source (URL or filename) as the source field, store origin as "pipeline"
|
|
1250
|
+
doc_node = GraphNode(
|
|
1251
|
+
id=doc_node_id,
|
|
1252
|
+
label="Document",
|
|
1253
|
+
properties={
|
|
1254
|
+
"name": document_id,
|
|
1255
|
+
"source": run_source,
|
|
1256
|
+
"source_type": run_source_type,
|
|
1257
|
+
"pipeline": pipeline.get("name", "unknown"),
|
|
1258
|
+
"pipeline_run_id": run_id,
|
|
1259
|
+
"text_length": len(text),
|
|
1260
|
+
"pages": len(pages),
|
|
1261
|
+
},
|
|
1262
|
+
)
|
|
1263
|
+
db.add_node(doc_node)
|
|
1264
|
+
|
|
1265
|
+
# ---------------------------------------------------------------
|
|
1266
|
+
# Initialize executor's stage_data for this namespace
|
|
1267
|
+
# ---------------------------------------------------------------
|
|
1268
|
+
if not hasattr(executor, '_stage_data'):
|
|
1269
|
+
executor._stage_data = {}
|
|
1270
|
+
executor._stage_data.setdefault(namespace, {})
|
|
1271
|
+
stage_data = executor._stage_data[namespace]
|
|
1272
|
+
|
|
1273
|
+
stage_data['extract'] = {
|
|
1274
|
+
'status': 'success',
|
|
1275
|
+
'document_id': document_id,
|
|
1276
|
+
'normalized_file': str(normalized_file),
|
|
1277
|
+
'normalized_version': 'v1',
|
|
1278
|
+
'normalized_doc': normalized_doc,
|
|
1279
|
+
'pages_count': len(pages),
|
|
1280
|
+
}
|
|
1281
|
+
stage_data['connect'] = {
|
|
1282
|
+
'status': 'completed',
|
|
1283
|
+
'document_nodes': [doc_node_id],
|
|
1284
|
+
'nodes_created': 1,
|
|
1285
|
+
'edges_created': 0,
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
# ---------------------------------------------------------------
|
|
1289
|
+
# Execute stages
|
|
1290
|
+
# ---------------------------------------------------------------
|
|
1291
|
+
stages = pipeline.get("stages", [])
|
|
1292
|
+
|
|
1293
|
+
for stage_def in stages:
|
|
1294
|
+
stage_name = stage_def["name"]
|
|
1295
|
+
stage_type = stage_def["type"]
|
|
1296
|
+
stage_config = stage_def.get("config", {})
|
|
1297
|
+
|
|
1298
|
+
store.update_stage(run_id, stage_name, "running")
|
|
1299
|
+
|
|
1300
|
+
try:
|
|
1301
|
+
# Build the stage dict that the executor expects
|
|
1302
|
+
aiql_stage = {
|
|
1303
|
+
'type': stage_type,
|
|
1304
|
+
'stage_type': stage_type,
|
|
1305
|
+
'step_name': stage_name,
|
|
1306
|
+
**stage_config,
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
# Add model params from pipeline config / run params
|
|
1310
|
+
if stage_type == "EMBED":
|
|
1311
|
+
aiql_stage.setdefault('parameters', {})
|
|
1312
|
+
aiql_stage['parameters']['model'] = params.get('embedding_model', 'text-embedding-3-small')
|
|
1313
|
+
elif stage_type in ("EXTRACT_ENTITIES", "EXTRACT_ENTITIES_AND_RELATIONSHIPS"):
|
|
1314
|
+
aiql_stage['using_llm'] = params.get('llm_model', 'gpt-4o-mini')
|
|
1315
|
+
if stage_type == "CHUNK":
|
|
1316
|
+
aiql_stage['chunk_method'] = params.get(
|
|
1317
|
+
'chunk_method', stage_config.get('method', 'paragraph'))
|
|
1318
|
+
aiql_stage.setdefault('parameters', {})
|
|
1319
|
+
aiql_stage['parameters']['chunk_size'] = params.get(
|
|
1320
|
+
'chunk_size', stage_config.get('max_size', 2000))
|
|
1321
|
+
|
|
1322
|
+
# Dispatch to executor's stage methods
|
|
1323
|
+
namespace_graph = db
|
|
1324
|
+
source_col = 'raw_docs'
|
|
1325
|
+
target_col = 'processed_docs'
|
|
1326
|
+
|
|
1327
|
+
if stage_type == "CHUNK":
|
|
1328
|
+
result = executor._execute_chunk_stage(
|
|
1329
|
+
aiql_stage, stage_data, namespace,
|
|
1330
|
+
source_col, target_col, namespace_graph,
|
|
1331
|
+
)
|
|
1332
|
+
count = result.get('chunks_created', 0) if isinstance(result, dict) else 0
|
|
1333
|
+
# Fallback: count Chunk nodes in graph
|
|
1334
|
+
if count == 0:
|
|
1335
|
+
try:
|
|
1336
|
+
count = sum(1 for n in db.get_all_nodes() if n.label == "Chunk")
|
|
1337
|
+
except Exception:
|
|
1338
|
+
pass # optional
|
|
1339
|
+
accuracy.chunk_count = count
|
|
1340
|
+
accuracy.chunk_quality = 0.8
|
|
1341
|
+
store.update_stage(run_id, stage_name, "completed", items_created=count)
|
|
1342
|
+
|
|
1343
|
+
elif stage_type == "EMBED":
|
|
1344
|
+
result = executor._execute_embed_stage(
|
|
1345
|
+
aiql_stage, stage_data, namespace,
|
|
1346
|
+
source_col, target_col, namespace_graph,
|
|
1347
|
+
)
|
|
1348
|
+
count = result.get('chunks_embedded', 0) if isinstance(result, dict) else 0
|
|
1349
|
+
accuracy.embedded_count = count
|
|
1350
|
+
accuracy.embedding_coverage = 1.0 if count > 0 else 0.0
|
|
1351
|
+
store.update_stage(run_id, stage_name, "completed", items_created=count)
|
|
1352
|
+
|
|
1353
|
+
elif stage_type == "EXTRACT_ENTITIES":
|
|
1354
|
+
result = executor._execute_extract_entities_stage(
|
|
1355
|
+
aiql_stage, stage_data, namespace,
|
|
1356
|
+
source_col, target_col, namespace_graph,
|
|
1357
|
+
)
|
|
1358
|
+
count = result.get('entities_extracted', 0) if isinstance(result, dict) else 0
|
|
1359
|
+
accuracy.entity_count = count
|
|
1360
|
+
accuracy.entity_confidence_avg = 0.8 if count > 0 else 0.0
|
|
1361
|
+
store.update_stage(run_id, stage_name, "completed", items_created=count)
|
|
1362
|
+
|
|
1363
|
+
elif stage_type == "EXTRACT_ENTITIES_AND_RELATIONSHIPS":
|
|
1364
|
+
result = executor._execute_extract_entities_and_relationships_stage(
|
|
1365
|
+
aiql_stage, stage_data, namespace,
|
|
1366
|
+
source_col, target_col,
|
|
1367
|
+
)
|
|
1368
|
+
ent = result.get('entities_created', 0) if isinstance(result, dict) else 0
|
|
1369
|
+
rel = result.get('relationships_created', 0) if isinstance(result, dict) else 0
|
|
1370
|
+
accuracy.entity_count = ent
|
|
1371
|
+
accuracy.relationship_count = rel
|
|
1372
|
+
accuracy.entity_confidence_avg = 0.8 if ent > 0 else 0.0
|
|
1373
|
+
accuracy.relationship_coverage = 0.7 if rel > 0 else 0.0
|
|
1374
|
+
store.update_stage(run_id, stage_name, "completed", items_created=ent + rel)
|
|
1375
|
+
|
|
1376
|
+
elif stage_type == "ENHANCE_GRAPH":
|
|
1377
|
+
result = executor._execute_enhance_graph_stage(
|
|
1378
|
+
aiql_stage, stage_data, namespace,
|
|
1379
|
+
source_col, target_col,
|
|
1380
|
+
)
|
|
1381
|
+
count = result.get('edges_created', 0) if isinstance(result, dict) else 0
|
|
1382
|
+
store.update_stage(run_id, stage_name, "completed", items_created=count)
|
|
1383
|
+
|
|
1384
|
+
elif stage_type == "PERSIST":
|
|
1385
|
+
try:
|
|
1386
|
+
graph_registry.save_graph(scoped_graph)
|
|
1387
|
+
except Exception:
|
|
1388
|
+
pass # optional
|
|
1389
|
+
store.update_stage(run_id, stage_name, "completed")
|
|
1390
|
+
|
|
1391
|
+
else:
|
|
1392
|
+
store.update_stage(run_id, stage_name, "skipped",
|
|
1393
|
+
metadata={"reason": f"Unknown stage type: {stage_type}"})
|
|
1394
|
+
|
|
1395
|
+
except Exception as stage_err:
|
|
1396
|
+
logger.exception(f"[pipeline] stage {stage_name} failed for run {run_id}")
|
|
1397
|
+
store.update_stage(run_id, stage_name, "failed", error=str(stage_err))
|
|
1398
|
+
# Continue to next stage (graceful degradation)
|
|
1399
|
+
|
|
1400
|
+
# Compute final counts from graph delta
|
|
1401
|
+
nodes_after = len(db.get_all_nodes())
|
|
1402
|
+
edges_after = len(db.get_all_edges())
|
|
1403
|
+
total_nodes = nodes_after - nodes_before
|
|
1404
|
+
total_edges = edges_after - edges_before
|
|
1405
|
+
|
|
1406
|
+
accuracy.compute_overall()
|
|
1407
|
+
store.update_run_counts(run_id, max(total_nodes, 0), max(total_edges, 0), accuracy.entity_count)
|
|
1408
|
+
store.set_accuracy(run_id, accuracy)
|
|
1409
|
+
store.update_run_status(run_id, "completed")
|
|
1410
|
+
|
|
1411
|
+
except Exception as exc:
|
|
1412
|
+
logger.exception(f"[pipeline] run {run_id} failed")
|
|
1413
|
+
store.update_run_status(run_id, "failed", error=str(exc))
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _split_into_pages(text: str, page_size: int = 2000) -> List[str]:
|
|
1417
|
+
"""Split text into page-sized sections for the normalized document."""
|
|
1418
|
+
paragraphs = text.split("\n\n")
|
|
1419
|
+
pages: List[str] = []
|
|
1420
|
+
current = ""
|
|
1421
|
+
for para in paragraphs:
|
|
1422
|
+
if len(current) + len(para) + 2 > page_size and current:
|
|
1423
|
+
pages.append(current)
|
|
1424
|
+
current = para
|
|
1425
|
+
else:
|
|
1426
|
+
current = f"{current}\n\n{para}" if current else para
|
|
1427
|
+
if current.strip():
|
|
1428
|
+
pages.append(current)
|
|
1429
|
+
return pages if pages else [text]
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
async def _enqueue_pipeline(job_queue, store, graph_registry, run_id, pipeline, scoped_graph, text, params):
|
|
1433
|
+
"""Enqueue a pipeline run into the async job queue."""
|
|
1434
|
+
await job_queue.enqueue(
|
|
1435
|
+
_execute_pipeline,
|
|
1436
|
+
store, graph_registry, run_id, pipeline, scoped_graph, text, params,
|
|
1437
|
+
)
|