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.
Files changed (776) hide show
  1. contextsynapse/__init__.py +107 -0
  2. contextsynapse/__main__.py +9 -0
  3. contextsynapse/a2a/__init__.py +28 -0
  4. contextsynapse/a2a/client.py +120 -0
  5. contextsynapse/a2a/discovery.py +128 -0
  6. contextsynapse/a2a/handlers.py +164 -0
  7. contextsynapse/a2a/models.py +246 -0
  8. contextsynapse/a2a/streaming.py +92 -0
  9. contextsynapse/a2a/task_manager.py +275 -0
  10. contextsynapse/adapters/__init__.py +98 -0
  11. contextsynapse/adapters/_base.py +298 -0
  12. contextsynapse/adapters/autogen/__init__.py +4 -0
  13. contextsynapse/adapters/autogen/tools.py +34 -0
  14. contextsynapse/adapters/crewai/__init__.py +4 -0
  15. contextsynapse/adapters/crewai/tools.py +63 -0
  16. contextsynapse/adapters/langchain/__init__.py +19 -0
  17. contextsynapse/adapters/langchain/message_history.py +17 -0
  18. contextsynapse/adapters/langchain/retriever.py +158 -0
  19. contextsynapse/adapters/langchain/tools.py +43 -0
  20. contextsynapse/adapters/langgraph/__init__.py +21 -0
  21. contextsynapse/adapters/langgraph/checkpoint.py +223 -0
  22. contextsynapse/adapters/langgraph/context_tools.py +341 -0
  23. contextsynapse/adapters/langgraph/message_history.py +138 -0
  24. contextsynapse/adapters/langgraph/tools.py +43 -0
  25. contextsynapse/adapters/llamaindex/__init__.py +4 -0
  26. contextsynapse/adapters/llamaindex/tools.py +64 -0
  27. contextsynapse/adapters/openai/__init__.py +24 -0
  28. contextsynapse/adapters/openai/tools.py +206 -0
  29. contextsynapse/adapters/pydantic_ai/__init__.py +4 -0
  30. contextsynapse/adapters/pydantic_ai/tools.py +36 -0
  31. contextsynapse/adapters/swarm/__init__.py +4 -0
  32. contextsynapse/adapters/swarm/tools.py +44 -0
  33. contextsynapse/agents/__init__.py +6 -0
  34. contextsynapse/agents/auto_capture.py +80 -0
  35. contextsynapse/agents/dispatcher.py +299 -0
  36. contextsynapse/agents/notifications.py +112 -0
  37. contextsynapse/aiql/__init__.py +71 -0
  38. contextsynapse/aiql/cache.py +341 -0
  39. contextsynapse/aiql/compiler/__init__.py +62 -0
  40. contextsynapse/aiql/compiler/optimizer.py +126 -0
  41. contextsynapse/aiql/compiler/planner.py +366 -0
  42. contextsynapse/aiql/compiler/statistics.py +235 -0
  43. contextsynapse/aiql/compiler/validator.py +425 -0
  44. contextsynapse/aiql/engine/__init__.py +11 -0
  45. contextsynapse/aiql/engine/chunk_storage.py +2584 -0
  46. contextsynapse/aiql/engine/executor.py +7737 -0
  47. contextsynapse/aiql/engine/extraction_integration.py +442 -0
  48. contextsynapse/aiql/engine/graph_builder.py +405 -0
  49. contextsynapse/aiql/engine/node_creation_from_normalized.py +691 -0
  50. contextsynapse/aiql/engine/performance_monitor.py +289 -0
  51. contextsynapse/aiql/engine/postprocessing.py +365 -0
  52. contextsynapse/aiql/engine/ray_stage_executors.py +918 -0
  53. contextsynapse/aiql/engine/redis_cache.py +106 -0
  54. contextsynapse/aiql/engine/streaming_json.py +366 -0
  55. contextsynapse/aiql/engine/streaming_parquet.py +568 -0
  56. contextsynapse/aiql/engine/streaming_ray_chunking.py +419 -0
  57. contextsynapse/aiql/grammar/__init__.py +14 -0
  58. contextsynapse/aiql/grammar/aiql_grammar.py +1822 -0
  59. contextsynapse/aiql/grammar/base.py +71 -0
  60. contextsynapse/aiql/parser/__init__.py +46 -0
  61. contextsynapse/aiql/parser/aiql_parser.py +11330 -0
  62. contextsynapse/aiql/parser/base_parser.py +47 -0
  63. contextsynapse/api/__init__.py +5 -0
  64. contextsynapse/api/a2a_router.py +233 -0
  65. contextsynapse/api/admin_router.py +140 -0
  66. contextsynapse/api/agent_worker_router.py +978 -0
  67. contextsynapse/api/algorithms_router.py +317 -0
  68. contextsynapse/api/api.py +4144 -0
  69. contextsynapse/api/audit.py +204 -0
  70. contextsynapse/api/auth.py +441 -0
  71. contextsynapse/api/auth_router.py +537 -0
  72. contextsynapse/api/billing.py +261 -0
  73. contextsynapse/api/billing_router.py +275 -0
  74. contextsynapse/api/boundary_router.py +452 -0
  75. contextsynapse/api/cognition_router.py +116 -0
  76. contextsynapse/api/connector_router.py +237 -0
  77. contextsynapse/api/context_router.py +1619 -0
  78. contextsynapse/api/dashboard_router.py +11367 -0
  79. contextsynapse/api/errors.py +183 -0
  80. contextsynapse/api/events.py +88 -0
  81. contextsynapse/api/experiment_bootstrap.py +321 -0
  82. contextsynapse/api/experiment_orchestrator.py +1078 -0
  83. contextsynapse/api/experiment_router.py +2089 -0
  84. contextsynapse/api/feature_gate.py +89 -0
  85. contextsynapse/api/federation_router.py +108 -0
  86. contextsynapse/api/integrations.py +567 -0
  87. contextsynapse/api/integrations_router.py +461 -0
  88. contextsynapse/api/intelligence_router.py +1028 -0
  89. contextsynapse/api/metering.py +155 -0
  90. contextsynapse/api/metrics.py +112 -0
  91. contextsynapse/api/models.py +38 -0
  92. contextsynapse/api/monitoring_router.py +174 -0
  93. contextsynapse/api/pipeline_router.py +1437 -0
  94. contextsynapse/api/playground_conn.py +116 -0
  95. contextsynapse/api/projects_router.py +131 -0
  96. contextsynapse/api/queries_router.py +282 -0
  97. contextsynapse/api/quota.py +95 -0
  98. contextsynapse/api/rate_limit.py +64 -0
  99. contextsynapse/api/search_router.py +261 -0
  100. contextsynapse/api/shield_router.py +447 -0
  101. contextsynapse/api/tenant_guard.py +103 -0
  102. contextsynapse/api/tenant_quotas.py +231 -0
  103. contextsynapse/api/tenants.py +305 -0
  104. contextsynapse/api/users.py +646 -0
  105. contextsynapse/api/v1.py +167 -0
  106. contextsynapse/api/vertical_builder_router.py +387 -0
  107. contextsynapse/benchmark/__init__.py +15 -0
  108. contextsynapse/benchmark/__main__.py +64 -0
  109. contextsynapse/benchmark/agents.py +338 -0
  110. contextsynapse/benchmark/live_ab.py +338 -0
  111. contextsynapse/benchmark/reporter.py +149 -0
  112. contextsynapse/benchmark/runner.py +264 -0
  113. contextsynapse/benchmark/savings.py +266 -0
  114. contextsynapse/benchmark/token_counter.py +212 -0
  115. contextsynapse/benchmark/workloads.py +289 -0
  116. contextsynapse/buffer/__init__.py +154 -0
  117. contextsynapse/buffer/base.py +257 -0
  118. contextsynapse/buffer/duckdb_buffer.py +246 -0
  119. contextsynapse/buffer/file_buffer.py +229 -0
  120. contextsynapse/buffer/lmdb_buffer.py +286 -0
  121. contextsynapse/buffer/local_buffer.py +154 -0
  122. contextsynapse/buffer/redis_buffer.py +211 -0
  123. contextsynapse/buffer/sqlite_buffer.py +266 -0
  124. contextsynapse/cache.py +2 -0
  125. contextsynapse/cli/__init__.py +40 -0
  126. contextsynapse/cli/cli.py +475 -0
  127. contextsynapse/cli/config_cli.py +177 -0
  128. contextsynapse/cli/scaffold.py +465 -0
  129. contextsynapse/cognition/__init__.py +31 -0
  130. contextsynapse/cognition/derivation.py +55 -0
  131. contextsynapse/cognition/events.py +135 -0
  132. contextsynapse/cognition/feedback.py +52 -0
  133. contextsynapse/cognition/hallucination.py +90 -0
  134. contextsynapse/cognition/invalidation.py +70 -0
  135. contextsynapse/cognition/read_tracker.py +50 -0
  136. contextsynapse/collaboration.py +2 -0
  137. contextsynapse/comparison.py +2 -0
  138. contextsynapse/config/__init__.py +10 -0
  139. contextsynapse/config/connectors/amfi_nav.yaml +18 -0
  140. contextsynapse/config/connectors/auto_sector.yaml +20 -0
  141. contextsynapse/config/connectors/banking_sector.yaml +20 -0
  142. contextsynapse/config/connectors/energy_sector.yaml +20 -0
  143. contextsynapse/config/connectors/fmcg_sector.yaml +20 -0
  144. contextsynapse/config/connectors/nsdl_fpi.yaml +9 -0
  145. contextsynapse/config/connectors/nse_price.yaml +61 -0
  146. contextsynapse/config/connectors/pharma_sector.yaml +20 -0
  147. contextsynapse/config/connectors/rbi_dbie.yaml +12 -0
  148. contextsynapse/config/context_presets/ai_research.json +25 -0
  149. contextsynapse/config/context_presets/chatgpt_conversations.json +19 -0
  150. contextsynapse/config/context_presets/claude_conversations.json +19 -0
  151. contextsynapse/config/context_presets/legal_compliance.json +21 -0
  152. contextsynapse/config/context_presets/meeting_notes.json +21 -0
  153. contextsynapse/config/context_presets/slack_engineering.json +21 -0
  154. contextsynapse/config/context_presets/tech_climate.json +24 -0
  155. contextsynapse/config/context_presets/toi_politics.json +25 -0
  156. contextsynapse/config/database_config.py +453 -0
  157. contextsynapse/config/deployment.py +221 -0
  158. contextsynapse/config/gpt_action_schema.json +164 -0
  159. contextsynapse/config/indicators/india_macro.yaml +213 -0
  160. contextsynapse/config/logging_config.py +88 -0
  161. contextsynapse/config/mode_config.py +223 -0
  162. contextsynapse/config/schemas/api_spec.yaml +48 -0
  163. contextsynapse/config/schemas/architecture_doc.yaml +51 -0
  164. contextsynapse/config/schemas/chat_history.yaml +90 -0
  165. contextsynapse/config/schemas/chatgpt_conversation.yaml +101 -0
  166. contextsynapse/config/schemas/claude_conversation.yaml +148 -0
  167. contextsynapse/config/schemas/contract.yaml +48 -0
  168. contextsynapse/config/schemas/conversation.yaml +167 -0
  169. contextsynapse/config/schemas/database.yaml +74 -0
  170. contextsynapse/config/schemas/decision.yaml +72 -0
  171. contextsynapse/config/schemas/ecommerce.yaml +56 -0
  172. contextsynapse/config/schemas/education.yaml +42 -0
  173. contextsynapse/config/schemas/healthcare.yaml +133 -0
  174. contextsynapse/config/schemas/hr_document.yaml +48 -0
  175. contextsynapse/config/schemas/invoice.yaml +40 -0
  176. contextsynapse/config/schemas/knowledge_base.yaml +75 -0
  177. contextsynapse/config/schemas/legal.yaml +42 -0
  178. contextsynapse/config/schemas/marketing_content.yaml +48 -0
  179. contextsynapse/config/schemas/meeting_notes.yaml +51 -0
  180. contextsynapse/config/schemas/news_article.yaml +61 -0
  181. contextsynapse/config/schemas/real_estate.yaml +49 -0
  182. contextsynapse/config/schemas/requirements_doc.yaml +48 -0
  183. contextsynapse/config/schemas/research_paper.yaml +56 -0
  184. contextsynapse/config/schemas/resume.yaml +49 -0
  185. contextsynapse/config/schemas/retail.yaml +42 -0
  186. contextsynapse/config/schemas/rule_meta.yaml +173 -0
  187. contextsynapse/config/schemas/rules.yaml +72 -0
  188. contextsynapse/config/schemas/screener_meta.yaml +236 -0
  189. contextsynapse/config/schemas/sdlc.yaml +103 -0
  190. contextsynapse/config/schemas/session_graph.yaml +172 -0
  191. contextsynapse/config/schemas/support_ticket.yaml +48 -0
  192. contextsynapse/config/schemas/web.yaml +64 -0
  193. contextsynapse/config/sensors/earnings_calendar.yaml +52 -0
  194. contextsynapse/config/sensors/global_risk.yaml +53 -0
  195. contextsynapse/config/sensors/government_policy.yaml +54 -0
  196. contextsynapse/config/sensors/supply_chain.yaml +50 -0
  197. contextsynapse/config/sensors/templates/behavioral.yaml +56 -0
  198. contextsynapse/config/sensors/templates/equity_sentiment.yaml +48 -0
  199. contextsynapse/config/sensors/templates/global_macro.yaml +47 -0
  200. contextsynapse/config/sensors/templates/market_data.yaml +118 -0
  201. contextsynapse/config/sensors/templates/sector_technical.yaml +47 -0
  202. contextsynapse/config/sensors/weather.yaml +52 -0
  203. contextsynapse/config/storage_strategy.py +202 -0
  204. contextsynapse/config/templates/competitive_intel.yaml +39 -0
  205. contextsynapse/config/templates/geopolitical_risk.yaml +46 -0
  206. contextsynapse/config/templates/market_analysis.yaml +58 -0
  207. contextsynapse/connectors/__init__.py +15 -0
  208. contextsynapse/connectors/api_router.py +121 -0
  209. contextsynapse/connectors/base.py +357 -0
  210. contextsynapse/connectors/financial/audio.py +170 -0
  211. contextsynapse/connectors/financial/youtube.py +272 -0
  212. contextsynapse/connectors/github.py +164 -0
  213. contextsynapse/connectors/jira.py +120 -0
  214. contextsynapse/connectors/kafka_connector.py +124 -0
  215. contextsynapse/connectors/pipeline_connector.py +747 -0
  216. contextsynapse/connectors/registry.py +116 -0
  217. contextsynapse/connectors/scheduler.py +87 -0
  218. contextsynapse/connectors/universal.py +360 -0
  219. contextsynapse/context/__init__.py +96 -0
  220. contextsynapse/context/__main__.py +4 -0
  221. contextsynapse/context/acl.py +125 -0
  222. contextsynapse/context/agent_card.py +201 -0
  223. contextsynapse/context/agent_memory.py +814 -0
  224. contextsynapse/context/agents.py +472 -0
  225. contextsynapse/context/agents_redis.py +391 -0
  226. contextsynapse/context/assembled.py +204 -0
  227. contextsynapse/context/attribution.py +113 -0
  228. contextsynapse/context/blob.py +184 -0
  229. contextsynapse/context/boundaries.py +187 -0
  230. contextsynapse/context/boundary.py +507 -0
  231. contextsynapse/context/bundle.py +155 -0
  232. contextsynapse/context/cli.py +519 -0
  233. contextsynapse/context/collaboration.py +165 -0
  234. contextsynapse/context/compiler.py +604 -0
  235. contextsynapse/context/composite.py +536 -0
  236. contextsynapse/context/compression.py +147 -0
  237. contextsynapse/context/context_manager.py +888 -0
  238. contextsynapse/context/context_schema.py +90 -0
  239. contextsynapse/context/context_schema.yaml +105 -0
  240. contextsynapse/context/context_units.py +891 -0
  241. contextsynapse/context/conversation.py +488 -0
  242. contextsynapse/context/dedup.py +207 -0
  243. contextsynapse/context/document_processor.py +698 -0
  244. contextsynapse/context/embedding_hooks.py +338 -0
  245. contextsynapse/context/execution_schema.py +274 -0
  246. contextsynapse/context/fan_out.py +138 -0
  247. contextsynapse/context/frozen.py +80 -0
  248. contextsynapse/context/gravity.py +227 -0
  249. contextsynapse/context/hub.py +1401 -0
  250. contextsynapse/context/ingest.py +391 -0
  251. contextsynapse/context/injection.py +226 -0
  252. contextsynapse/context/interaction_graphifier.py +562 -0
  253. contextsynapse/context/layers.py +574 -0
  254. contextsynapse/context/projection.py +1195 -0
  255. contextsynapse/context/promotion.py +634 -0
  256. contextsynapse/context/proof.py +131 -0
  257. contextsynapse/context/propagation.py +577 -0
  258. contextsynapse/context/pubsub.py +135 -0
  259. contextsynapse/context/quality.py +1054 -0
  260. contextsynapse/context/quality_tracker.py +270 -0
  261. contextsynapse/context/scoping.py +370 -0
  262. contextsynapse/context/seed.py +992 -0
  263. contextsynapse/context/session.py +1142 -0
  264. contextsynapse/context/session_graph.py +593 -0
  265. contextsynapse/context/session_memory.py +309 -0
  266. contextsynapse/context/session_resolver.py +111 -0
  267. contextsynapse/context/skill_learning.py +237 -0
  268. contextsynapse/context/smart_budget.py +129 -0
  269. contextsynapse/context/source_policy.py +242 -0
  270. contextsynapse/context/store_factory.py +53 -0
  271. contextsynapse/context/sync.py +648 -0
  272. contextsynapse/context/token.py +236 -0
  273. contextsynapse/context/vector_integration.py +297 -0
  274. contextsynapse/context/webhooks.py +194 -0
  275. contextsynapse/context/working_memory.py +224 -0
  276. contextsynapse/core/__init__.py +18 -0
  277. contextsynapse/core/archiver.py +237 -0
  278. contextsynapse/core/change_stream.py +282 -0
  279. contextsynapse/core/checkpoint.py +310 -0
  280. contextsynapse/core/cloud_storage.py +387 -0
  281. contextsynapse/core/context_state.py +651 -0
  282. contextsynapse/core/cron.py +324 -0
  283. contextsynapse/core/datasource.py +643 -0
  284. contextsynapse/core/db.py +289 -0
  285. contextsynapse/core/federation.py +283 -0
  286. contextsynapse/core/graph_coordinator.py +201 -0
  287. contextsynapse/core/graph_intelligence.py +388 -0
  288. contextsynapse/core/graph_structures.py +106 -0
  289. contextsynapse/core/graph_sync.py +157 -0
  290. contextsynapse/core/hybrid_graph_storage.py +2998 -0
  291. contextsynapse/core/lifecycle.py +691 -0
  292. contextsynapse/core/multiworker.py +378 -0
  293. contextsynapse/core/pruning.py +312 -0
  294. contextsynapse/core/redis_registry.py +524 -0
  295. contextsynapse/core/registry.py +1043 -0
  296. contextsynapse/core/registry_factory.py +51 -0
  297. contextsynapse/core/registry_metadata.py +294 -0
  298. contextsynapse/core/replay.py +354 -0
  299. contextsynapse/core/replication.py +339 -0
  300. contextsynapse/core/time_travel.py +260 -0
  301. contextsynapse/core/write_behind.py +152 -0
  302. contextsynapse/core/write_context.py +39 -0
  303. contextsynapse/db/__init__.py +1 -0
  304. contextsynapse/db/postgres.py +267 -0
  305. contextsynapse/db/profile.py +236 -0
  306. contextsynapse/db/rules.py +328 -0
  307. contextsynapse/demo/__init__.py +0 -0
  308. contextsynapse/demo/report.py +147 -0
  309. contextsynapse/demo/runner.py +520 -0
  310. contextsynapse/demo/scenario.py +165 -0
  311. contextsynapse/demo/session_demo.py +280 -0
  312. contextsynapse/demo/session_report.py +143 -0
  313. contextsynapse/demo/universal_runner.py +257 -0
  314. contextsynapse/engine/__init__.py +15 -0
  315. contextsynapse/engine/core.py +555 -0
  316. contextsynapse/extraction/__init__.py +69 -0
  317. contextsynapse/extraction/cli.py +274 -0
  318. contextsynapse/extraction/config.py +107 -0
  319. contextsynapse/extraction/examples/example_usage.py +118 -0
  320. contextsynapse/extraction/examples/sample_manifest.json +53 -0
  321. contextsynapse/extraction/examples/sample_normalized.json +185 -0
  322. contextsynapse/extraction/extract_engine.py +440 -0
  323. contextsynapse/extraction/extractors/__init__.py +37 -0
  324. contextsynapse/extraction/extractors/chat_export_extractor.py +189 -0
  325. contextsynapse/extraction/extractors/csv_extractor.py +83 -0
  326. contextsynapse/extraction/extractors/docx_extractor.py +129 -0
  327. contextsynapse/extraction/extractors/excel_extractor.py +152 -0
  328. contextsynapse/extraction/extractors/html_extractor.py +146 -0
  329. contextsynapse/extraction/extractors/pdf_extractor.py +635 -0
  330. contextsynapse/extraction/extractors/pdf_extractor_layoutparser.py +408 -0
  331. contextsynapse/extraction/extractors/register_extractors.py +63 -0
  332. contextsynapse/extraction/extractors/text_extractor.py +65 -0
  333. contextsynapse/extraction/extractors/txt_extractor.py +120 -0
  334. contextsynapse/extraction/extractors/website_extractor.py +246 -0
  335. contextsynapse/extraction/fact_extractor.py +236 -0
  336. contextsynapse/extraction/file_manager.py +353 -0
  337. contextsynapse/extraction/hierarchy.py +239 -0
  338. contextsynapse/extraction/id_generator.py +450 -0
  339. contextsynapse/extraction/layout_detector.py +409 -0
  340. contextsynapse/extraction/llm_entity_extractor.py +301 -0
  341. contextsynapse/extraction/normalized_store.py +480 -0
  342. contextsynapse/extraction/normalizer.py +594 -0
  343. contextsynapse/extraction/parquet_writer.py +598 -0
  344. contextsynapse/extraction/post_processors/__init__.py +40 -0
  345. contextsynapse/extraction/post_processors/column_reorganizer.py +507 -0
  346. contextsynapse/extraction/ray_normalization.py +233 -0
  347. contextsynapse/extraction/ray_runner.py +207 -0
  348. contextsynapse/extraction/redis_schema_registry.py +222 -0
  349. contextsynapse/extraction/registry.py +204 -0
  350. contextsynapse/extraction/schema_loader.py +663 -0
  351. contextsynapse/extraction/schema_registry.py +230 -0
  352. contextsynapse/extraction/semantic_normalizer.py +245 -0
  353. contextsynapse/extraction/version_manager.py +314 -0
  354. contextsynapse/gateway/__init__.py +24 -0
  355. contextsynapse/gateway/action_emitter.py +212 -0
  356. contextsynapse/gateway/cost_tracker.py +508 -0
  357. contextsynapse/gateway/gateway.py +377 -0
  358. contextsynapse/gateway/policy.py +303 -0
  359. contextsynapse/governance/__init__.py +13 -0
  360. contextsynapse/governance/layer.py +513 -0
  361. contextsynapse/ingestion/__init__.py +15 -0
  362. contextsynapse/ingestion/amplifier.py +288 -0
  363. contextsynapse/ingestion/async_ingest.py +463 -0
  364. contextsynapse/ingestion/chunker.py +303 -0
  365. contextsynapse/ingestion/chunking.py +58 -0
  366. contextsynapse/ingestion/cleaner.py +190 -0
  367. contextsynapse/ingestion/cleanse.py +252 -0
  368. contextsynapse/ingestion/connectors/__init__.py +38 -0
  369. contextsynapse/ingestion/connectors/base.py +300 -0
  370. contextsynapse/ingestion/connectors/chatgpt_memory.py +247 -0
  371. contextsynapse/ingestion/connectors/claude_memory.py +274 -0
  372. contextsynapse/ingestion/connectors/sap_ingestor.py +208 -0
  373. contextsynapse/ingestion/content_detector.py +80 -0
  374. contextsynapse/ingestion/content_monitor.py +502 -0
  375. contextsynapse/ingestion/dedup.py +106 -0
  376. contextsynapse/ingestion/feed.py +640 -0
  377. contextsynapse/ingestion/filters.py +461 -0
  378. contextsynapse/ingestion/graph_builder.py +705 -0
  379. contextsynapse/ingestion/input_classifier.py +307 -0
  380. contextsynapse/ingestion/job_manager.py +571 -0
  381. contextsynapse/ingestion/llm_extractor.py +610 -0
  382. contextsynapse/ingestion/orchestrator.py +116 -0
  383. contextsynapse/ingestion/parsers/__init__.py +19 -0
  384. contextsynapse/ingestion/parsers/graph_parser_registry.py +103 -0
  385. contextsynapse/ingestion/parsers/graphml_parser.py +141 -0
  386. contextsynapse/ingestion/parsers/jsonld_parser.py +171 -0
  387. contextsynapse/ingestion/parsers/rdf_parser.py +150 -0
  388. contextsynapse/ingestion/pipeline_context.py +228 -0
  389. contextsynapse/ingestion/pipeline_store.py +1195 -0
  390. contextsynapse/ingestion/queue.py +133 -0
  391. contextsynapse/ingestion/reprocess.py +179 -0
  392. contextsynapse/ingestion/scenario_router.py +224 -0
  393. contextsynapse/ingestion/schema_extractor.py +759 -0
  394. contextsynapse/ingestion/schema_validator.py +274 -0
  395. contextsynapse/ingestion/sdlc_ingest.py +87 -0
  396. contextsynapse/ingestion/smart_ingest.py +2451 -0
  397. contextsynapse/ingestion/source_connector.py +171 -0
  398. contextsynapse/ingestion/stage_executor.py +2095 -0
  399. contextsynapse/ingestion/stages/__init__.py +9 -0
  400. contextsynapse/ingestion/stages/tabular_to_graph.py +289 -0
  401. contextsynapse/ingestion/strategies.py +386 -0
  402. contextsynapse/ingestion/universal/__init__.py +4 -0
  403. contextsynapse/ingestion/universal/_operator_registry.py +59 -0
  404. contextsynapse/ingestion/universal/ingest_content.py +32 -0
  405. contextsynapse/ingestion/universal/operators/__init__.py +1 -0
  406. contextsynapse/ingestion/universal/operators/base.py +19 -0
  407. contextsynapse/ingestion/universal/operators/build_edges.py +75 -0
  408. contextsynapse/ingestion/universal/operators/cluster_topics.py +397 -0
  409. contextsynapse/ingestion/universal/operators/deduplicate.py +45 -0
  410. contextsynapse/ingestion/universal/operators/detect_signals.py +90 -0
  411. contextsynapse/ingestion/universal/operators/embed.py +80 -0
  412. contextsynapse/ingestion/universal/operators/extract_entities.py +253 -0
  413. contextsynapse/ingestion/universal/operators/extract_preferences.py +117 -0
  414. contextsynapse/ingestion/universal/operators/filter_content.py +69 -0
  415. contextsynapse/ingestion/universal/operators/index_bm25.py +52 -0
  416. contextsynapse/ingestion/universal/operators/infer_domains.py +69 -0
  417. contextsynapse/ingestion/universal/operators/link_cross_reference.py +210 -0
  418. contextsynapse/ingestion/universal/operators/parse_records.py +66 -0
  419. contextsynapse/ingestion/universal/operators/resolve_entities.py +261 -0
  420. contextsynapse/ingestion/universal/operators/scanners/__init__.py +16 -0
  421. contextsynapse/ingestion/universal/operators/scanners/edges.py +173 -0
  422. contextsynapse/ingestion/universal/operators/scanners/git.py +187 -0
  423. contextsynapse/ingestion/universal/operators/scanners/github.py +135 -0
  424. contextsynapse/ingestion/universal/operators/scanners/github_api.py +509 -0
  425. contextsynapse/ingestion/universal/operators/scanners/helpers.py +67 -0
  426. contextsynapse/ingestion/universal/operators/scanners/jira.py +65 -0
  427. contextsynapse/ingestion/universal/operators/scanners/llm_enrichment.py +82 -0
  428. contextsynapse/ingestion/universal/operators/scanners/repo.py +149 -0
  429. contextsynapse/ingestion/universal/operators/sdlc_scan.py +213 -0
  430. contextsynapse/ingestion/universal/operators/spec_scanner.py +149 -0
  431. contextsynapse/ingestion/universal/operators/store_documents.py +72 -0
  432. contextsynapse/ingestion/universal/operators/synthesize_cu.py +180 -0
  433. contextsynapse/ingestion/universal/operators/validate_gate.py +65 -0
  434. contextsynapse/ingestion/universal/parsers/__init__.py +5 -0
  435. contextsynapse/ingestion/universal/parsers/record_parser.py +203 -0
  436. contextsynapse/ingestion/universal/parsers/turn_parser.py +329 -0
  437. contextsynapse/ingestion/universal/shared_signals.py +161 -0
  438. contextsynapse/ingestion/universal/stage_executor.py +219 -0
  439. contextsynapse/ingestion/web_crawler.py +265 -0
  440. contextsynapse/intelligence/__init__.py +53 -0
  441. contextsynapse/intelligence/alerts.py +198 -0
  442. contextsynapse/intelligence/collector.py +655 -0
  443. contextsynapse/intelligence/comparison.py +198 -0
  444. contextsynapse/intelligence/config.py +56 -0
  445. contextsynapse/intelligence/conflict_detector.py +182 -0
  446. contextsynapse/intelligence/context_gaps.py +192 -0
  447. contextsynapse/intelligence/context_radar.py +135 -0
  448. contextsynapse/intelligence/correlation_engine.py +416 -0
  449. contextsynapse/intelligence/correlation_template.py +120 -0
  450. contextsynapse/intelligence/dedup.py +296 -0
  451. contextsynapse/intelligence/event_bus.py +91 -0
  452. contextsynapse/intelligence/feedback_loop.py +101 -0
  453. contextsynapse/intelligence/freshness.py +307 -0
  454. contextsynapse/intelligence/fundamental_feed.py +551 -0
  455. contextsynapse/intelligence/geo_data.py +170 -0
  456. contextsynapse/intelligence/geo_reference.py +112 -0
  457. contextsynapse/intelligence/graph_fusion.py +465 -0
  458. contextsynapse/intelligence/heuristics.py +176 -0
  459. contextsynapse/intelligence/impact_tracker.py +336 -0
  460. contextsynapse/intelligence/macro_indicators.py +319 -0
  461. contextsynapse/intelligence/ocr.py +104 -0
  462. contextsynapse/intelligence/persistence.py +267 -0
  463. contextsynapse/intelligence/pipeline_health.py +130 -0
  464. contextsynapse/intelligence/price_feed.py +221 -0
  465. contextsynapse/intelligence/rate_limiter.py +130 -0
  466. contextsynapse/intelligence/reactive.py +411 -0
  467. contextsynapse/intelligence/runtime_context.py +390 -0
  468. contextsynapse/intelligence/sentiment_decay.py +426 -0
  469. contextsynapse/intelligence/session.py +277 -0
  470. contextsynapse/intelligence/signal_hierarchy.py +438 -0
  471. contextsynapse/intelligence/signals.py +185 -0
  472. contextsynapse/intelligence/source_watcher.py +132 -0
  473. contextsynapse/intelligence/table_enricher.py +256 -0
  474. contextsynapse/intelligence/tagger.py +319 -0
  475. contextsynapse/intelligence/timeseries.py +209 -0
  476. contextsynapse/intelligence/tracker.py +318 -0
  477. contextsynapse/intelligence/watchdog.py +392 -0
  478. contextsynapse/intelligence/watchdog_manager.py +263 -0
  479. contextsynapse/intelligence/webhook_receiver.py +165 -0
  480. contextsynapse/intelligence/weight_learner.py +234 -0
  481. contextsynapse/llm/__init__.py +8 -0
  482. contextsynapse/llm/client.py +348 -0
  483. contextsynapse/llm/rate_limiter.py +166 -0
  484. contextsynapse/logging_config.py +2 -0
  485. contextsynapse/marketplace/__init__.py +7 -0
  486. contextsynapse/marketplace/registry.py +173 -0
  487. contextsynapse/mcp/__init__.py +11 -0
  488. contextsynapse/mcp/__main__.py +4 -0
  489. contextsynapse/mcp/auth_middleware.py +212 -0
  490. contextsynapse/mcp/connection_pool.py +128 -0
  491. contextsynapse/mcp/server.py +986 -0
  492. contextsynapse/mcp/session_context.py +67 -0
  493. contextsynapse/memory/__init__.py +34 -0
  494. contextsynapse/memory/engine.py +475 -0
  495. contextsynapse/memory/temporal.py +327 -0
  496. contextsynapse/metadata/__init__.py +12 -0
  497. contextsynapse/metadata/metadata_db.py +272 -0
  498. contextsynapse/metadata/metadata_query_tool.py +300 -0
  499. contextsynapse/metadata/metadata_tracker.py +425 -0
  500. contextsynapse/models/__init__.py +27 -0
  501. contextsynapse/models/embedding_service.py +360 -0
  502. contextsynapse/models/model_registry.py +228 -0
  503. contextsynapse/multiworker.py +2 -0
  504. contextsynapse/pipelines/__init__.py +1 -0
  505. contextsynapse/pipelines/executor.py +851 -0
  506. contextsynapse/pipelines/filters.py +189 -0
  507. contextsynapse/pipelines/models.py +176 -0
  508. contextsynapse/pipelines/scheduler.py +268 -0
  509. contextsynapse/playground/__init__.py +1 -0
  510. contextsynapse/playground/templates.py +85 -0
  511. contextsynapse/plugins/__init__.py +43 -0
  512. contextsynapse/plugins/api.py +66 -0
  513. contextsynapse/plugins/base.py +350 -0
  514. contextsynapse/plugins/domain_interface.py +508 -0
  515. contextsynapse/plugins/domain_loader.py +241 -0
  516. contextsynapse/plugins/loader.py +148 -0
  517. contextsynapse/plugins/registry.py +265 -0
  518. contextsynapse/project/__init__.py +11 -0
  519. contextsynapse/project/code_context.py +807 -0
  520. contextsynapse/project/connectors/__init__.py +4 -0
  521. contextsynapse/project/connectors/github_connector.py +113 -0
  522. contextsynapse/project/connectors/jira_connector.py +160 -0
  523. contextsynapse/project/graph.py +1023 -0
  524. contextsynapse/project/project_context.py +545 -0
  525. contextsynapse/project/scanner_registry.py +61 -0
  526. contextsynapse/project/schema_manager.py +322 -0
  527. contextsynapse/project/schemas/knowledge.yaml +59 -0
  528. contextsynapse/project/schemas/sdlc.yaml +369 -0
  529. contextsynapse/project/sdlc_schema.py +172 -0
  530. contextsynapse/project/spec_parser.py +85 -0
  531. contextsynapse/project/task_lock.py +181 -0
  532. contextsynapse/project/task_stream.py +203 -0
  533. contextsynapse/project/team_tools.py +537 -0
  534. contextsynapse/py.typed +0 -0
  535. contextsynapse/realtime/__init__.py +6 -0
  536. contextsynapse/realtime/collaboration.py +147 -0
  537. contextsynapse/rules/__init__.py +10 -0
  538. contextsynapse/rules/engine.py +116 -0
  539. contextsynapse/rules/evaluators/__init__.py +6 -0
  540. contextsynapse/rules/evaluators/aggregate.py +205 -0
  541. contextsynapse/rules/evaluators/custom.py +170 -0
  542. contextsynapse/rules/evaluators/pre_action.py +125 -0
  543. contextsynapse/rules/evaluators/temporal.py +129 -0
  544. contextsynapse/rules/loader.py +52 -0
  545. contextsynapse/rules/models.py +124 -0
  546. contextsynapse/rules/universal.py +393 -0
  547. contextsynapse/scheduler.py +229 -0
  548. contextsynapse/schema/__init__.py +42 -0
  549. contextsynapse/schema/compiler.py +252 -0
  550. contextsynapse/schema/composer.py +158 -0
  551. contextsynapse/schema/dedup.py +73 -0
  552. contextsynapse/schema/derivation.py +172 -0
  553. contextsynapse/schema/sdl.py +326 -0
  554. contextsynapse/schema/validation_gate.py +124 -0
  555. contextsynapse/sdk/__init__.py +63 -0
  556. contextsynapse/sdk/agent.py +214 -0
  557. contextsynapse/sdk/client.py +269 -0
  558. contextsynapse/sdk/exceptions.py +23 -0
  559. contextsynapse/sdk/models.py +78 -0
  560. contextsynapse/sdk/session.py +408 -0
  561. contextsynapse/sdk/tracked.py +218 -0
  562. contextsynapse/sdk/worker.py +266 -0
  563. contextsynapse/sdk/workspace_client.py +98 -0
  564. contextsynapse/sdk/ws.py +41 -0
  565. contextsynapse/search/__init__.py +20 -0
  566. contextsynapse/search/embedding_cache.py +140 -0
  567. contextsynapse/search/enhanced_search.py +152 -0
  568. contextsynapse/search/fulltext.py +130 -0
  569. contextsynapse/search/graph_search.py +634 -0
  570. contextsynapse/search/lmdb_index.py +1218 -0
  571. contextsynapse/search/nl_to_aiql.py +199 -0
  572. contextsynapse/search/rag.py +1346 -0
  573. contextsynapse/search/rag_cache.py +167 -0
  574. contextsynapse/search/reasoning_chain.py +231 -0
  575. contextsynapse/search/redis_search.py +207 -0
  576. contextsynapse/search/retrieval_quality.py +314 -0
  577. contextsynapse/search/semantic_query.py +455 -0
  578. contextsynapse/search/text_resolver.py +159 -0
  579. contextsynapse/search/whoosh_search.py +373 -0
  580. contextsynapse/security/__init__.py +17 -0
  581. contextsynapse/security/audit.py +297 -0
  582. contextsynapse/security/audit_logger.py +97 -0
  583. contextsynapse/security/audit_trail.py +270 -0
  584. contextsynapse/security/auth_unified.py +284 -0
  585. contextsynapse/security/auto_tagger.py +230 -0
  586. contextsynapse/security/data_security.py +218 -0
  587. contextsynapse/security/encryption.py +207 -0
  588. contextsynapse/security/identity.py +447 -0
  589. contextsynapse/security/jwt_identity.py +59 -0
  590. contextsynapse/security/middleware.py +336 -0
  591. contextsynapse/security/pii.py +233 -0
  592. contextsynapse/security/rbac.py +96 -0
  593. contextsynapse/security/rls.py +205 -0
  594. contextsynapse/security/sanitize.py +233 -0
  595. contextsynapse/security/scoped_encryption.py +131 -0
  596. contextsynapse/security/tenant.py +401 -0
  597. contextsynapse/shield/__init__.py +21 -0
  598. contextsynapse/shield/anomaly.py +56 -0
  599. contextsynapse/shield/permissions.py +99 -0
  600. contextsynapse/shield/profile.py +103 -0
  601. contextsynapse/shield/shield.py +108 -0
  602. contextsynapse/shield/trust_engine.py +69 -0
  603. contextsynapse/skills/__init__.py +14 -0
  604. contextsynapse/skills/engine.py +233 -0
  605. contextsynapse/skills/loader.py +59 -0
  606. contextsynapse/skills/models.py +139 -0
  607. contextsynapse/storage/__init__.py +7 -0
  608. contextsynapse/storage/cache.py +208 -0
  609. contextsynapse/storage/columnar_store_v2.py +178 -0
  610. contextsynapse/storage/csr_graph_storage.py +587 -0
  611. contextsynapse/storage/csr_store.py +388 -0
  612. contextsynapse/storage/document_store.py +491 -0
  613. contextsynapse/storage/hnsw_index.py +294 -0
  614. contextsynapse/storage/hybrid_store_v2.py +278 -0
  615. contextsynapse/storage/lmdb_graph_storage.py +366 -0
  616. contextsynapse/storage/migration.py +279 -0
  617. contextsynapse/storage/namespace_store.py +346 -0
  618. contextsynapse/storage/pure_graph_storage.py +185 -0
  619. contextsynapse/storage/redis_graph_adapter.py +186 -0
  620. contextsynapse/storage/redis_graph_storage.py +375 -0
  621. contextsynapse/storage/router/__init__.py +29 -0
  622. contextsynapse/storage/router/duckdb_store.py +398 -0
  623. contextsynapse/storage/router/factory.py +83 -0
  624. contextsynapse/storage/router/interface.py +125 -0
  625. contextsynapse/storage/router/postgres_store.py +339 -0
  626. contextsynapse/storage/router/resolver.py +192 -0
  627. contextsynapse/storage/router/router.py +300 -0
  628. contextsynapse/storage/single_file_storage.py +391 -0
  629. contextsynapse/storage/storage_manager.py +214 -0
  630. contextsynapse/storage/storage_query_optimizer.py +314 -0
  631. contextsynapse/storage/wal.py +359 -0
  632. contextsynapse/temporal/__init__.py +0 -0
  633. contextsynapse/temporal/storage.py +106 -0
  634. contextsynapse/tests/__init__.py +11 -0
  635. contextsynapse/tests/regression/__init__.py +1 -0
  636. contextsynapse/tests/regression/add_metadata.py +395 -0
  637. contextsynapse/tests/regression/add_skip_flags.py +85 -0
  638. contextsynapse/tests/regression/cases/test_basic_create_node.yaml +20 -0
  639. contextsynapse/tests/regression/cases/test_basic_select.yaml +21 -0
  640. contextsynapse/tests/regression/cases/test_blockchain_audit_trail.yaml +37 -0
  641. contextsynapse/tests/regression/cases/test_blockchain_audit_trail_filtered.yaml +39 -0
  642. contextsynapse/tests/regression/cases/test_blockchain_blocks_range.yaml +39 -0
  643. contextsynapse/tests/regression/cases/test_blockchain_get_block.yaml +35 -0
  644. contextsynapse/tests/regression/cases/test_blockchain_latest_block.yaml +35 -0
  645. contextsynapse/tests/regression/cases/test_blockchain_length.yaml +37 -0
  646. contextsynapse/tests/regression/cases/test_blockchain_merkle_root.yaml +37 -0
  647. contextsynapse/tests/regression/cases/test_blockchain_verify.yaml +35 -0
  648. contextsynapse/tests/regression/cases/test_blockchain_verify_block.yaml +35 -0
  649. contextsynapse/tests/regression/cases/test_create_edge.yaml +28 -0
  650. contextsynapse/tests/regression/cases/test_delete_edge_basic.yaml +29 -0
  651. contextsynapse/tests/regression/cases/test_delete_node_basic.yaml +29 -0
  652. contextsynapse/tests/regression/cases/test_evaluation_mrr.yaml +40 -0
  653. contextsynapse/tests/regression/cases/test_evaluation_ndcg.yaml +40 -0
  654. contextsynapse/tests/regression/cases/test_evaluation_precision.yaml +40 -0
  655. contextsynapse/tests/regression/cases/test_evaluation_recall.yaml +40 -0
  656. contextsynapse/tests/regression/cases/test_group_by_basic.yaml +56 -0
  657. contextsynapse/tests/regression/cases/test_match_node_basic.yaml +29 -0
  658. contextsynapse/tests/regression/cases/test_match_node_with_order_limit.yaml +31 -0
  659. contextsynapse/tests/regression/cases/test_namespace_switch.yaml +18 -0
  660. contextsynapse/tests/regression/cases/test_pipeline_cascaded.yaml +61 -0
  661. contextsynapse/tests/regression/cases/test_pipeline_fixed_size_chunking.yaml +48 -0
  662. contextsynapse/tests/regression/cases/test_pipeline_gpt4_chunking.yaml +54 -0
  663. contextsynapse/tests/regression/cases/test_pipeline_hybrid_indexing.yaml +57 -0
  664. contextsynapse/tests/regression/cases/test_pipeline_large_embedding.yaml +52 -0
  665. contextsynapse/tests/regression/cases/test_pipeline_paragraph_chunking.yaml +49 -0
  666. contextsynapse/tests/regression/cases/test_pipeline_recursive_chunking.yaml +47 -0
  667. contextsynapse/tests/regression/cases/test_pipeline_run_pipeline.yaml +56 -0
  668. contextsynapse/tests/regression/cases/test_pipeline_section_based_chunking.yaml +53 -0
  669. contextsynapse/tests/regression/cases/test_pipeline_semantic_chunking.yaml +51 -0
  670. contextsynapse/tests/regression/cases/test_pipeline_with_entity_extraction.yaml +57 -0
  671. contextsynapse/tests/regression/cases/test_pipeline_with_relationships.yaml +62 -0
  672. contextsynapse/tests/regression/cases/test_rag_evaluation.yaml +38 -0
  673. contextsynapse/tests/regression/cases/test_rag_graph_bfs_with_eval.yaml +48 -0
  674. contextsynapse/tests/regression/cases/test_rag_graph_centrality.yaml +40 -0
  675. contextsynapse/tests/regression/cases/test_rag_graph_centrality_eval.yaml +35 -0
  676. contextsynapse/tests/regression/cases/test_rag_graph_community.yaml +40 -0
  677. contextsynapse/tests/regression/cases/test_rag_graph_community_eval.yaml +35 -0
  678. contextsynapse/tests/regression/cases/test_rag_graph_dfs_with_eval.yaml +35 -0
  679. contextsynapse/tests/regression/cases/test_rag_graph_multi_hop_eval.yaml +35 -0
  680. contextsynapse/tests/regression/cases/test_rag_graph_path.yaml +40 -0
  681. contextsynapse/tests/regression/cases/test_rag_graph_search_basic.yaml +38 -0
  682. contextsynapse/tests/regression/cases/test_rag_graph_search_bfs.yaml +52 -0
  683. contextsynapse/tests/regression/cases/test_rag_graph_search_dfs.yaml +41 -0
  684. contextsynapse/tests/regression/cases/test_rag_graph_search_multi_hop.yaml +43 -0
  685. contextsynapse/tests/regression/cases/test_rag_graph_search_shortest_path.yaml +41 -0
  686. contextsynapse/tests/regression/cases/test_rag_graph_search_weighted.yaml +43 -0
  687. contextsynapse/tests/regression/cases/test_rag_graph_shortest_path_eval.yaml +35 -0
  688. contextsynapse/tests/regression/cases/test_rag_graph_traversal.yaml +30 -0
  689. contextsynapse/tests/regression/cases/test_rag_graph_weighted_eval.yaml +35 -0
  690. contextsynapse/tests/regression/cases/test_rag_hybrid_search.yaml +33 -0
  691. contextsynapse/tests/regression/cases/test_rag_hybrid_with_prompt_embedding.yaml +44 -0
  692. contextsynapse/tests/regression/cases/test_rag_keyword_search.yaml +32 -0
  693. contextsynapse/tests/regression/cases/test_rag_multi_hop_graph.yaml +32 -0
  694. contextsynapse/tests/regression/cases/test_rag_path_based.yaml +34 -0
  695. contextsynapse/tests/regression/cases/test_rag_profile_based.yaml +32 -0
  696. contextsynapse/tests/regression/cases/test_rag_vector_search.yaml +32 -0
  697. contextsynapse/tests/regression/cases/test_rag_weighted_graph.yaml +34 -0
  698. contextsynapse/tests/regression/cases/test_rag_weighted_hybrid.yaml +31 -0
  699. contextsynapse/tests/regression/cases/test_semantic_boundary_chunking.yaml +57 -0
  700. contextsynapse/tests/regression/cases/test_semantic_boundary_detection.yaml +33 -0
  701. contextsynapse/tests/regression/cases/test_semantic_hash_function.yaml +30 -0
  702. contextsynapse/tests/regression/cases/test_semantic_hash_with_model.yaml +31 -0
  703. contextsynapse/tests/regression/cases/test_tcs_ingestion_fixed_size.yaml +44 -0
  704. contextsynapse/tests/regression/cases/test_tcs_ingestion_recursive.yaml +46 -0
  705. contextsynapse/tests/regression/cases/test_tcs_ingestion_section.yaml +46 -0
  706. contextsynapse/tests/regression/cases/test_tcs_ingestion_semantic.yaml +44 -0
  707. contextsynapse/tests/regression/cases/test_time_travel_as_of.yaml +35 -0
  708. contextsynapse/tests/regression/cases/test_time_travel_at_commit.yaml +33 -0
  709. contextsynapse/tests/regression/cases/test_time_travel_at_timestamp.yaml +33 -0
  710. contextsynapse/tests/regression/cases/test_time_travel_for_system_time_all.yaml +37 -0
  711. contextsynapse/tests/regression/cases/test_time_travel_versions_between.yaml +39 -0
  712. contextsynapse/tests/regression/cases/test_traverse_basic.yaml +31 -0
  713. contextsynapse/tests/regression/cases/test_traverse_max_depth.yaml +34 -0
  714. contextsynapse/tests/regression/cases/test_update_edge_basic.yaml +29 -0
  715. contextsynapse/tests/regression/evaluation_metrics.py +227 -0
  716. contextsynapse/tests/regression/regression_runner.py +1081 -0
  717. contextsynapse/tests/regression/setup_regression_data.py +212 -0
  718. contextsynapse/tests/regression/test_basic_only.py +84 -0
  719. contextsynapse/tests/regression/test_namespace_creation.py +29 -0
  720. contextsynapse/tests/regression/test_single_fix.py +61 -0
  721. contextsynapse/tests/regression/test_single_query.py +20 -0
  722. contextsynapse/tests/regression/validate_test_syntax.py +179 -0
  723. contextsynapse/tests/test_advanced_features.py +347 -0
  724. contextsynapse/tests/test_aiql_summary.py +283 -0
  725. contextsynapse/tests/test_all_aiql_queries.py +948 -0
  726. contextsynapse/tests/test_all_create_queries.py +213 -0
  727. contextsynapse/tests/test_api_vs_native.py +280 -0
  728. contextsynapse/tests/test_chunking_dedup.py +199 -0
  729. contextsynapse/tests/test_connectors.py +171 -0
  730. contextsynapse/tests/test_edges_and_match.py +202 -0
  731. contextsynapse/tests/test_extraction_pipeline_integration.py +581 -0
  732. contextsynapse/tests/test_name_property.py +224 -0
  733. contextsynapse/tests/test_node_edge_metadata.py +250 -0
  734. contextsynapse/tests/test_playground_conn.py +114 -0
  735. contextsynapse/tests/test_query_cache.py +213 -0
  736. contextsynapse/tests/test_source_policy_auth.py +348 -0
  737. contextsynapse/tests/test_temporal_and_domain.py +224 -0
  738. contextsynapse/tests/test_time_travel_e2e.py +571 -0
  739. contextsynapse/tests/test_traversal_and_aggregation.py +275 -0
  740. contextsynapse/tools/__init__.py +36 -0
  741. contextsynapse/tools/a2a_tools.py +106 -0
  742. contextsynapse/tools/cognition_tools.py +118 -0
  743. contextsynapse/tools/context.py +905 -0
  744. contextsynapse/tools/formatting.py +92 -0
  745. contextsynapse/tools/gateway_tools.py +186 -0
  746. contextsynapse/tools/graph.py +1700 -0
  747. contextsynapse/tools/intelligence.py +235 -0
  748. contextsynapse/tools/memory.py +118 -0
  749. contextsynapse/tools/registry.py +373 -0
  750. contextsynapse/tools/shield_tools.py +159 -0
  751. contextsynapse/tools/tasks.py +341 -0
  752. contextsynapse/tools/workspace_tools.py +124 -0
  753. contextsynapse/utils/__init__.py +15 -0
  754. contextsynapse/utils/config_loader.py +198 -0
  755. contextsynapse/utils/env_loader.py +156 -0
  756. contextsynapse/utils/file_utils.py +288 -0
  757. contextsynapse/utils/logging_optimizer.py +119 -0
  758. contextsynapse/vector/__init__.py +30 -0
  759. contextsynapse/vector/chroma_vector_store.py +152 -0
  760. contextsynapse/vector/custom_vector_store.py +537 -0
  761. contextsynapse/vector/faiss_vector_store.py +296 -0
  762. contextsynapse/vector/qdrant_vector_store.py +163 -0
  763. contextsynapse/vector/vector_db.py +953 -0
  764. contextsynapse/vector/vector_db_manager.py +197 -0
  765. contextsynapse/workspace/__init__.py +27 -0
  766. contextsynapse/workspace/base.py +94 -0
  767. contextsynapse/workspace/git.py +228 -0
  768. contextsynapse/workspace/github.py +78 -0
  769. contextsynapse/workspace/local.py +73 -0
  770. contextsynapse/workspace/tools.py +222 -0
  771. contextsynapse-1.0.0.dist-info/METADATA +847 -0
  772. contextsynapse-1.0.0.dist-info/RECORD +776 -0
  773. contextsynapse-1.0.0.dist-info/WHEEL +5 -0
  774. contextsynapse-1.0.0.dist-info/entry_points.txt +4 -0
  775. contextsynapse-1.0.0.dist-info/licenses/LICENSE +200 -0
  776. 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
+ )