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,1195 @@
1
+ """
2
+ Contextual Graph Projection (CGP)
3
+ ==================================
4
+ Queryless context delivery for multi-agent systems.
5
+
6
+ Instead of agents searching for knowledge, the system projects a relevant
7
+ subgraph from the atomic context into the agent's view — automatically,
8
+ based on their live work state in the runtime graph.
9
+
10
+ Three components:
11
+ - IntentExtractor: reads runtime graph, produces weighted intent vector
12
+ - SubgraphProjector: queries atomic graph, returns relevant subgraph
13
+ - ProjectionAssembler: formats projection for agent context window
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Dict, List, Optional, Set
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _STOP_WORDS = frozenset({
26
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
27
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
28
+ "should", "may", "might", "can", "could", "of", "in", "to", "for",
29
+ "with", "on", "at", "by", "from", "as", "into", "through", "during",
30
+ "and", "or", "but", "not", "no", "if", "then", "than", "that", "this",
31
+ "it", "its", "he", "she", "his", "her", "we", "they", "them", "our",
32
+ "said", "also", "new", "one", "two", "who", "what", "when", "where",
33
+ "how", "all", "each", "every", "both", "few", "more", "most", "other",
34
+ "some", "such", "only", "own", "same", "so", "very", "just", "about",
35
+ "https", "http", "www", "com", "org", "html", "htm",
36
+ })
37
+
38
+ import time as _time
39
+
40
+
41
+ # ── Tier Definitions ────────────────────────────────────────────────
42
+
43
+ _TIER_DEFAULTS = {
44
+ "instant": {"max_tokens": 500, "max_time_ms": 50, "stages": {"cache"}},
45
+ "fast": {"max_tokens": 1500, "max_time_ms": 200, "stages": {"cache", "bm25"}},
46
+ "standard": {"max_tokens": 3000, "max_time_ms": 1000, "stages": {"cache", "bm25", "qdrant", "hops"}},
47
+ "deep": {"max_tokens": 8000, "max_time_ms": 5000, "stages": {"cache", "bm25", "qdrant", "hops", "signals", "extended"}},
48
+ }
49
+
50
+
51
+ class ProjectionBudget:
52
+ """Tracks token and time budgets for progressive projection pipeline."""
53
+
54
+ def __init__(self, max_tokens: int, max_time_ms: int, tier: str = "standard"):
55
+ self.max_tokens = max_tokens
56
+ self.max_time_ms = max_time_ms
57
+ self.tier = tier
58
+ self.tokens_used = 0
59
+ self._start = _time.monotonic()
60
+ self._stages = _TIER_DEFAULTS.get(tier, _TIER_DEFAULTS["standard"])["stages"]
61
+
62
+ @classmethod
63
+ def from_tier(cls, tier: str) -> "ProjectionBudget":
64
+ """Create a budget from tier defaults."""
65
+ defaults = _TIER_DEFAULTS.get(tier, _TIER_DEFAULTS["standard"])
66
+ return cls(
67
+ max_tokens=defaults["max_tokens"],
68
+ max_time_ms=defaults["max_time_ms"],
69
+ tier=tier,
70
+ )
71
+
72
+ def has_remaining(self) -> bool:
73
+ elapsed_ms = (_time.monotonic() - self._start) * 1000
74
+ return self.tokens_used < self.max_tokens and elapsed_ms < self.max_time_ms
75
+
76
+ @property
77
+ def remaining_tokens(self) -> int:
78
+ return self.max_tokens - self.tokens_used
79
+
80
+ def consume_tokens(self, n: int) -> None:
81
+ self.tokens_used += n
82
+
83
+ def exhausted(self) -> bool:
84
+ return not self.has_remaining()
85
+
86
+ def should_run_stage(self, stage: str) -> bool:
87
+ """Check if this tier includes the given pipeline stage."""
88
+ return stage in self._stages and self.has_remaining()
89
+
90
+
91
+ def _extract_keywords(text: str) -> Dict[str, float]:
92
+ """Extract weighted keywords from text. Returns {term: weight}."""
93
+ words = re.split(r"\W+", text.lower())
94
+ counts: Dict[str, int] = {}
95
+ for w in words:
96
+ if len(w) >= 3 and w not in _STOP_WORDS:
97
+ counts[w] = counts.get(w, 0) + 1
98
+ if not counts:
99
+ return {}
100
+ max_count = max(counts.values())
101
+ return {w: c / max_count for w, c in counts.items()}
102
+
103
+
104
+ def _merge_keywords(base: Dict[str, float], new: Dict[str, float], weight: float) -> Dict[str, float]:
105
+ """Merge new keywords into base with a source weight multiplier."""
106
+ merged = dict(base)
107
+ for k, v in new.items():
108
+ merged[k] = merged.get(k, 0.0) + v * weight
109
+ return merged
110
+
111
+
112
+ @dataclass
113
+ class IntentVector:
114
+ """Weighted intent extracted from an agent's runtime state."""
115
+ keywords: Dict[str, float] = field(default_factory=dict)
116
+ entity_ids: List[str] = field(default_factory=list)
117
+ edge_types: Set[str] = field(default_factory=set)
118
+ task_title: str = ""
119
+ agent_id: str = ""
120
+ confidence: float = 0.0
121
+
122
+
123
+ class IntentExtractor:
124
+ """Reads the runtime graph and produces an IntentVector."""
125
+
126
+ @staticmethod
127
+ def extract(
128
+ tasks: List[Dict],
129
+ runtime_db: Any = None,
130
+ agent_name: str = "",
131
+ agent_id: str = "",
132
+ ) -> IntentVector:
133
+ if not tasks:
134
+ return IntentVector(agent_id=agent_id)
135
+
136
+ task = tasks[0]
137
+ task_text = f"{task.get('title', '')} {task.get('description', '')}"
138
+ keywords = _extract_keywords(task_text)
139
+ confidence = 0.3
140
+
141
+ own_keywords: Dict[str, float] = {}
142
+ other_keywords: Dict[str, float] = {}
143
+
144
+ if runtime_db:
145
+ my_name = (agent_name or "").lower()
146
+ own_nodes = []
147
+ other_nodes = []
148
+
149
+ for label in ("Finding", "Insight", "AgentAction", "Decision", "Observation"):
150
+ try:
151
+ for n in runtime_db.get_all_nodes(label=label):
152
+ props = n.properties if hasattr(n, "properties") else {}
153
+ author = (
154
+ props.get("_agent_name", props.get("created_by", props.get("agent", ""))) or ""
155
+ ).lower()
156
+ text = props.get("name", props.get("description", props.get("content", ""))) or ""
157
+ if not text:
158
+ continue
159
+ if author == my_name:
160
+ own_nodes.append(text)
161
+ elif author and author != "system" and not author.startswith("pipeline"):
162
+ other_nodes.append(text)
163
+ except Exception:
164
+ pass
165
+
166
+ for text in own_nodes[:5]:
167
+ own_keywords = _merge_keywords(own_keywords, _extract_keywords(text), 0.8)
168
+ if own_keywords:
169
+ keywords = _merge_keywords(keywords, own_keywords, 1.0)
170
+ confidence = 0.6
171
+
172
+ for text in other_nodes[:3]:
173
+ other_keywords = _merge_keywords(other_keywords, _extract_keywords(text), 0.4)
174
+ if other_keywords:
175
+ keywords = _merge_keywords(keywords, other_keywords, 1.0)
176
+ confidence = 0.8
177
+
178
+ return IntentVector(
179
+ keywords=keywords,
180
+ task_title=task.get("title", ""),
181
+ agent_id=agent_id,
182
+ confidence=confidence,
183
+ )
184
+
185
+
186
+ # ── ProjectedSubgraph ────────────────────────────────────────────────
187
+
188
+ @dataclass
189
+ class ProjectedSubgraph:
190
+ """Result of projecting the atomic graph through an intent vector."""
191
+ nodes: List[Dict] = field(default_factory=list)
192
+ edges: List[Dict] = field(default_factory=list)
193
+ seed_count: int = 0
194
+ hop_depth: int = 0
195
+ token_estimate: int = 0
196
+ intent_keywords: List[str] = field(default_factory=list)
197
+
198
+
199
+ _INFRA_LABELS = frozenset({
200
+ "ContextMeta", "VectorIndex", "BM25Index", "Session", "ContextRef",
201
+ "Context", "Project", "ContextIntelligence", "CodeBase", "SystemStore",
202
+ "UserStore", "WebStore", "GeneratedStore", "MemoryStore", "ArtifactStore",
203
+ "ToolStore", "KnowledgeBase", "AgentThought", "AgentPresence",
204
+ "ExperimentRun", "PipelineRun",
205
+ })
206
+
207
+ _CHARS_PER_TOKEN = 4 # rough estimate for English text
208
+
209
+
210
+ def _estimate_tokens(text: str) -> int:
211
+ """Rough token estimate: ~4 chars per token for English."""
212
+ return max(1, len(text) // _CHARS_PER_TOKEN)
213
+
214
+
215
+ def _node_text(node) -> str:
216
+ """Extract searchable text from a node."""
217
+ props = node.properties if hasattr(node, "properties") else {}
218
+ return " ".join(str(props.get(f, "")) for f in
219
+ ("name", "statement", "content", "description", "topic", "claim",
220
+ "questions_answered", "next_clues")).lower()
221
+
222
+
223
+ def _build_idf(nodes: list, keywords: Dict[str, float]) -> Dict[str, float]:
224
+ """Compute IDF for keywords across the node corpus.
225
+
226
+ IDF = log(N / df) where df = number of nodes containing the term.
227
+ Words appearing in many nodes get downweighted.
228
+ """
229
+ import math
230
+ n = max(len(nodes), 1)
231
+ doc_freq: Dict[str, int] = {}
232
+ for kw in keywords:
233
+ doc_freq[kw] = 0
234
+ for node in nodes:
235
+ text = _node_text(node)
236
+ for kw in keywords:
237
+ if kw in text:
238
+ doc_freq[kw] += 1
239
+ return {kw: math.log(n / max(df, 1)) + 1.0 for kw, df in doc_freq.items()}
240
+
241
+
242
+ # Module-level IDF cache (recomputed when namespace changes)
243
+ _idf_cache: Dict[str, Dict[str, float]] = {} # namespace → {keyword → idf}
244
+ _idf_namespace: str = ""
245
+
246
+
247
+ def _node_keyword_score(node, keywords: Dict[str, float], idf: Optional[Dict[str, float]] = None) -> float:
248
+ """Score a node against intent keywords with IDF weighting.
249
+
250
+ IDF downweights words that appear in many nodes (generic terms like
251
+ 'module', 'service') and boosts words that appear in few (specific
252
+ terms like 'JWT', 'bcrypt').
253
+ """
254
+ text = _node_text(node)
255
+ hits = 0
256
+ score = 0.0
257
+ max_idf_hit = 0.0
258
+ for kw, weight in keywords.items():
259
+ if kw in text:
260
+ idf_boost = idf.get(kw, 1.0) if idf else 1.0
261
+ score += weight * idf_boost
262
+ hits += 1
263
+ if idf_boost > max_idf_hit:
264
+ max_idf_hit = idf_boost
265
+ # 2+ hits: always include (multiple keyword overlap = strong match)
266
+ # 1 hit: only include if the matching keyword is specific (high IDF)
267
+ # IDF > 2.0 means the word appears in <37% of nodes — it's distinctive
268
+ if hits == 0:
269
+ return 0.0
270
+ if hits == 1 and max_idf_hit < 2.0:
271
+ return 0.0
272
+ return score
273
+
274
+
275
+ def _node_to_dict(node) -> Dict:
276
+ """Convert a graph node to a projection dict. Keeps full content for budget-aware assembly."""
277
+ props = node.properties if hasattr(node, "properties") else {}
278
+ label = getattr(node, "node_type", None) or getattr(node, "label", None) or props.get("label", "?")
279
+ name = props.get("name", props.get("title", props.get("topic", "")))
280
+ # CUs have 'claim' as their main content; regular nodes use statement/content
281
+ content = props.get("claim", props.get("statement", props.get("content", props.get("description", "")))) or ""
282
+ # Don't repeat name in content if they're the same
283
+ if content == name:
284
+ content = ""
285
+ d = {
286
+ "id": node.id if hasattr(node, "id") else props.get("id", "?"),
287
+ "label": label,
288
+ "name": name,
289
+ "content": content,
290
+ "token_estimate": _estimate_tokens(f"{label} {name} {content}"),
291
+ }
292
+ # Preserve CU-specific fields for zoom-level rendering
293
+ if label == "ContextUnit":
294
+ d["confidence"] = props.get("confidence", 0.0)
295
+ d["questions_answered"] = props.get("questions_answered", [])
296
+ d["next_clues"] = props.get("next_clues", [])
297
+ d["evidence_ids"] = props.get("evidence_ids", [])
298
+ return d
299
+
300
+
301
+ class SubgraphProjector:
302
+ """Projects a relevant subgraph from the atomic graph based on intent."""
303
+
304
+ @staticmethod
305
+ def project(
306
+ intent: IntentVector,
307
+ atomic_db: Any = None,
308
+ namespace: str = "",
309
+ max_tokens: int = 2000,
310
+ budget: Optional["ProjectionBudget"] = None,
311
+ priority_labels: Optional[List[str]] = None,
312
+ ) -> ProjectedSubgraph:
313
+ if not intent.keywords or intent.confidence == 0.0 or atomic_db is None:
314
+ return ProjectedSubgraph()
315
+
316
+ if budget is None:
317
+ budget = ProjectionBudget(max_tokens=max_tokens, max_time_ms=10000, tier="deep")
318
+
319
+ adapter = getattr(atomic_db, "csr_adapter", None) or atomic_db
320
+ top_keywords = sorted(intent.keywords.items(), key=lambda x: -x[1])[:10]
321
+
322
+ seeds: List = []
323
+ seed_scores: Dict[str, float] = {}
324
+ idf: Optional[Dict[str, float]] = None
325
+ used_index = False
326
+
327
+ # Step 1a: LMDB BM25 — O(K) index lookup, not O(N) scan
328
+ if budget.should_run_stage("bm25"):
329
+ try:
330
+ import os
331
+ idx_path = f"contextcore_data/lmdb_index/{namespace}"
332
+ if os.path.exists(idx_path):
333
+ from ..search.lmdb_index import get_lmdb_index
334
+ lmdb_idx = get_lmdb_index(namespace)
335
+ idx_stats = lmdb_idx.stats()
336
+ if idx_stats.get("nodes", 0) > 0:
337
+ used_index = True
338
+ # Get IDF directly from index — O(K) per keyword
339
+ idf = lmdb_idx.get_idf(list(intent.keywords.keys()))
340
+ # BM25 search — O(K) postings lookup
341
+ query = " ".join(kw for kw, _ in top_keywords[:5])
342
+ lmdb_results = lmdb_idx.search(query, limit=50)
343
+ for r in lmdb_results:
344
+ nid = r.get("node_id", "") # LMDB returns "node_id", not "id"
345
+ if nid and r.get("label", "") not in _INFRA_LABELS:
346
+ node = adapter.get_node(nid) if hasattr(adapter, "get_node") else None
347
+ if node:
348
+ score = _node_keyword_score(node, intent.keywords, idf=idf)
349
+ if score > 0:
350
+ seeds.append(node)
351
+ seed_scores[nid] = score
352
+ except Exception as _e:
353
+ logger.warning("[CGP] LMDB BM25 seed retrieval failed for ns=%s: %s", namespace, _e)
354
+
355
+ # Step 1b: Qdrant vectors — O(K) ANN search, not O(N)
356
+ if budget.should_run_stage("qdrant"):
357
+ try:
358
+ from ..context.vector_integration import get_session_vector_store
359
+ svs = get_session_vector_store()
360
+ if svs.available and svs.has_vectors(namespace):
361
+ used_index = True
362
+ query_text = intent.task_title or " ".join(kw for kw, _ in top_keywords[:5])
363
+ vec_results = svs.search(namespace, query_text, k=30)
364
+ for r in vec_results:
365
+ nid = r.get("node_id", "")
366
+ if nid and nid not in seed_scores:
367
+ meta = r.get("metadata", {})
368
+ if meta.get("label", "") not in _INFRA_LABELS:
369
+ node = adapter.get_node(nid) if hasattr(adapter, "get_node") else None
370
+ if node:
371
+ vec_score = r.get("score", 0.5) * 2.0
372
+ seeds.append(node)
373
+ seed_scores[nid] = vec_score
374
+ except Exception as _e:
375
+ logger.warning("[CGP] Qdrant vector seed retrieval failed for ns=%s: %s", namespace, _e)
376
+
377
+ # Step 1c: Full scan fallback — ONLY when no index available (small/test graphs)
378
+ content_nodes: list = [] # defined here so step 1d can reference it
379
+ if "bm25" in budget._stages and (not used_index or len(seeds) < 3):
380
+ try:
381
+ all_nodes = list(adapter.get_all_nodes()) if hasattr(adapter, "get_all_nodes") else []
382
+ content_nodes = [n for n in all_nodes
383
+ if (getattr(n, "node_type", None) or getattr(n, "label", "")) not in _INFRA_LABELS]
384
+ # Compute IDF from full scan only if index didn't provide it
385
+ if not idf and content_nodes:
386
+ idf = _build_idf(content_nodes, intent.keywords)
387
+ for n in content_nodes:
388
+ nid = n.id if hasattr(n, "id") else ""
389
+ if nid in seed_scores:
390
+ continue
391
+ score = _node_keyword_score(n, intent.keywords, idf=idf)
392
+ if score > 0:
393
+ seeds.append(n)
394
+ seed_scores[nid] = score
395
+ except Exception:
396
+ pass
397
+
398
+ # Step 1d: ContextUnit retrieval — pre-assembled intelligence objects
399
+ # CUs are the richest nodes (bundled facts + claim + actors). Prioritize them.
400
+ cu_nodes = []
401
+ try:
402
+ for n in content_nodes:
403
+ nt = getattr(n, "node_type", None) or getattr(n, "label", "")
404
+ if nt != "ContextUnit":
405
+ continue
406
+ nid = n.id if hasattr(n, "id") else ""
407
+ if nid in seed_scores:
408
+ continue
409
+ score = _node_keyword_score(n, intent.keywords, idf=idf)
410
+ if score > 0:
411
+ cu_nodes.append(n)
412
+ seed_scores[nid] = score * 1.5 # boost CUs — they're pre-synthesized
413
+ except Exception:
414
+ pass
415
+
416
+ # CUs go first, then regular seeds
417
+ seeds = cu_nodes + seeds
418
+
419
+ if not seeds:
420
+ return ProjectedSubgraph(intent_keywords=[kw for kw, _ in top_keywords])
421
+
422
+ # Apply schema priority label boosts — domain-critical node types always surface
423
+ if priority_labels:
424
+ _priority_set = set(priority_labels)
425
+ for seed in seeds:
426
+ nid = seed.id if hasattr(seed, "id") else ""
427
+ label = getattr(seed, "node_type", None) or getattr(seed, "label", "")
428
+ if label in _priority_set and nid in seed_scores:
429
+ seed_scores[nid] *= 1.8 # strong boost for domain-priority types
430
+
431
+ # Apply feedback boosts — nodes that were useful in past projections rank higher
432
+ for seed in seeds:
433
+ nid = seed.id if hasattr(seed, "id") else ""
434
+ if nid in seed_scores:
435
+ seed_scores[nid] *= get_feedback_boost(namespace, nid)
436
+
437
+ # Sort seeds by score, add within budget (using actual token estimates)
438
+ seeds.sort(key=lambda n: seed_scores.get(n.id if hasattr(n, "id") else "", 0), reverse=True)
439
+
440
+ projected_nodes = []
441
+ projected_ids = set()
442
+ tokens_used = 0
443
+
444
+ for seed in seeds:
445
+ nd = _node_to_dict(seed)
446
+ if tokens_used + nd["token_estimate"] > budget.max_tokens:
447
+ break
448
+ projected_nodes.append(nd)
449
+ projected_ids.add(nd["id"])
450
+ tokens_used += nd["token_estimate"]
451
+ budget.consume_tokens(nd["token_estimate"])
452
+
453
+ seed_count = len(projected_nodes)
454
+ hop_depth = 0
455
+
456
+ # Step 2: Adaptive hop expansion
457
+ if budget.should_run_stage("hops") and budget.has_remaining() and budget.remaining_tokens > 200 and hasattr(adapter, "get_neighbors"):
458
+ hop_depth = 1
459
+ neighbors = []
460
+ for seed in seeds[:5]:
461
+ if budget.exhausted():
462
+ break
463
+ sid = seed.id if hasattr(seed, "id") else ""
464
+ try:
465
+ for neighbor in adapter.get_neighbors(sid):
466
+ nid = neighbor.id if hasattr(neighbor, "id") else ""
467
+ label = getattr(neighbor, "node_type", None) or getattr(neighbor, "label", "")
468
+ if nid not in projected_ids and label not in _INFRA_LABELS:
469
+ neighbors.append(neighbor)
470
+ projected_ids.add(nid)
471
+ except Exception:
472
+ pass
473
+
474
+ for neighbor in neighbors:
475
+ nd = _node_to_dict(neighbor)
476
+ if tokens_used + nd["token_estimate"] > budget.max_tokens:
477
+ break
478
+ projected_nodes.append(nd)
479
+ tokens_used += nd["token_estimate"]
480
+ budget.consume_tokens(nd["token_estimate"])
481
+
482
+ remaining = budget.remaining_tokens
483
+
484
+ if remaining > 500 and len(seeds) >= 2 and budget.has_remaining():
485
+ hop_depth = 2
486
+ hop2_neighbors = []
487
+ for neighbor in neighbors[:3]:
488
+ if budget.exhausted():
489
+ break
490
+ nid = neighbor.id if hasattr(neighbor, "id") else ""
491
+ try:
492
+ for n2 in adapter.get_neighbors(nid):
493
+ n2id = n2.id if hasattr(n2, "id") else ""
494
+ label = getattr(n2, "node_type", None) or getattr(n2, "label", "")
495
+ if n2id not in projected_ids and label not in _INFRA_LABELS:
496
+ hop2_neighbors.append(n2)
497
+ projected_ids.add(n2id)
498
+ except Exception:
499
+ pass
500
+ for n2 in hop2_neighbors:
501
+ nd = _node_to_dict(n2)
502
+ if tokens_used + nd["token_estimate"] > budget.max_tokens:
503
+ break
504
+ projected_nodes.append(nd)
505
+ tokens_used += nd["token_estimate"]
506
+ budget.consume_tokens(nd["token_estimate"])
507
+
508
+ # Step 3: Collect edges between projected nodes
509
+ # O(projected × degree) via adjacency, not O(E) full scan
510
+ projected_edges = []
511
+ try:
512
+ if hasattr(adapter, "get_neighbors"):
513
+ # Fast path: use adjacency dict — O(projected_nodes × avg_degree)
514
+ seen_edges = set()
515
+ for nid in projected_ids:
516
+ try:
517
+ for neighbor_id, edge in adapter.get_neighbors(nid):
518
+ if neighbor_id in projected_ids:
519
+ eid = getattr(edge, "id", None) or f"{nid}-{neighbor_id}"
520
+ if eid not in seen_edges:
521
+ seen_edges.add(eid)
522
+ projected_edges.append({
523
+ "source": nid,
524
+ "target": neighbor_id,
525
+ "label": getattr(edge, "label", "RELATED"),
526
+ })
527
+ except Exception:
528
+ pass
529
+ else:
530
+ # Fallback: full scan (only for adapters without get_neighbors)
531
+ all_edges = adapter.get_all_edges() if hasattr(adapter, "get_all_edges") else []
532
+ for e in all_edges:
533
+ src = str(getattr(e, "source", getattr(e, "from_id", "")))
534
+ tgt = str(getattr(e, "target", getattr(e, "to_id", "")))
535
+ if src in projected_ids and tgt in projected_ids:
536
+ projected_edges.append({
537
+ "source": src,
538
+ "target": tgt,
539
+ "label": getattr(e, "label", "RELATED"),
540
+ })
541
+ except Exception:
542
+ pass
543
+
544
+ return ProjectedSubgraph(
545
+ nodes=projected_nodes,
546
+ edges=projected_edges,
547
+ seed_count=seed_count,
548
+ hop_depth=hop_depth,
549
+ token_estimate=tokens_used,
550
+ intent_keywords=[kw for kw, _ in top_keywords],
551
+ )
552
+
553
+
554
+ class ProjectionAssembler:
555
+ """Formats a ProjectedSubgraph into agent-readable context."""
556
+
557
+ # Tier → CU zoom level mapping
558
+ TIER_ZOOM = {
559
+ "instant": "headline",
560
+ "fast": "headline",
561
+ "standard": "summary",
562
+ "deep": "full",
563
+ }
564
+
565
+ @staticmethod
566
+ def assemble(
567
+ subgraph: ProjectedSubgraph,
568
+ task_title: str = "",
569
+ total_nodes: int = 0,
570
+ cross_agent_signals: Optional[List[Dict]] = None,
571
+ tier: str = "standard",
572
+ cu_config: Any = None,
573
+ ) -> str:
574
+ if not subgraph.nodes:
575
+ kw_str = ", ".join(subgraph.intent_keywords[:5]) if subgraph.intent_keywords else "none"
576
+ return f"No relevant context found for task: \"{task_title}\" (keywords: {kw_str})"
577
+
578
+ lines = []
579
+
580
+ kw_display = ", ".join(subgraph.intent_keywords[:5])
581
+ lines.append(f"PROJECTED CONTEXT (for task: \"{task_title}\")")
582
+ lines.append(f"Based on: {kw_display} | {subgraph.seed_count} direct matches")
583
+ lines.append("")
584
+
585
+ direct = subgraph.nodes[:subgraph.seed_count]
586
+ connected = subgraph.nodes[subgraph.seed_count:]
587
+
588
+ # Resolve CU zoom level from tier
589
+ zoom = ProjectionAssembler.TIER_ZOOM.get(tier, "summary")
590
+
591
+ if direct:
592
+ lines.append("DIRECT MATCHES:")
593
+ for n in direct:
594
+ content = n.get("content", "")
595
+ if n.get("label") == "ContextUnit":
596
+ # ContextUnit: render at tier-appropriate zoom level
597
+ from ..context.context_units import format_cu_for_agent
598
+ cu_dict = {
599
+ "properties": {
600
+ "topic": n.get("name", ""),
601
+ "claim": content,
602
+ "confidence": n.get("confidence", 0.0),
603
+ "questions_answered": n.get("questions_answered", []),
604
+ "next_clues": n.get("next_clues", []),
605
+ },
606
+ "evidence_ids": n.get("evidence_ids", []),
607
+ }
608
+ formatted = format_cu_for_agent(cu_dict, zoom=zoom, cu_config=cu_config)
609
+ for fl in formatted.split("\n"):
610
+ lines.append(f" {fl}")
611
+ elif content and len(content) > 200:
612
+ # Passage-sized content: show full text
613
+ lines.append(f" [{n['label']}] {n['name']}")
614
+ lines.append(f" {content}")
615
+ elif content:
616
+ lines.append(f" [{n['label']}] {n['name']} — {content}")
617
+ else:
618
+ lines.append(f" [{n['label']}] {n['name']}")
619
+
620
+ if connected:
621
+ lines.append("")
622
+ lines.append("CONNECTED CONTEXT:")
623
+ for n in connected:
624
+ edge_label = ""
625
+ for e in subgraph.edges:
626
+ if e["target"] == n["id"] or e["source"] == n["id"]:
627
+ edge_label = f" -[{e['label']}]->"
628
+ break
629
+ content = n.get("content", "")
630
+ if content and len(content) > 200:
631
+ lines.append(f" [{n['label']}] {n['name']}{edge_label}")
632
+ lines.append(f" {content[:500]}")
633
+ elif content:
634
+ lines.append(f" [{n['label']}] {n['name']}{edge_label} — {content[:200]}")
635
+ else:
636
+ lines.append(f" [{n['label']}] {n['name']}{edge_label}")
637
+
638
+ if cross_agent_signals:
639
+ lines.append("")
640
+ lines.append("CROSS-AGENT SIGNALS:")
641
+ for sig in cross_agent_signals[:5]:
642
+ lines.append(f" [{sig.get('agent', '?')}] ({sig.get('label', '?')}) {sig.get('name', '')[:80]}")
643
+
644
+ pct = f"{len(subgraph.nodes) / total_nodes * 100:.1f}%" if total_nodes > 0 else "?"
645
+ lines.append("")
646
+ lines.append(
647
+ f"{len(subgraph.nodes)} nodes projected from {total_nodes} total ({pct}) "
648
+ f"| Budget: {subgraph.token_estimate:,} tokens"
649
+ )
650
+
651
+ return "\n".join(lines)
652
+
653
+
654
+ # ── Top-level orchestrator ───────────────────────────────────────────
655
+
656
+ def project_context(ctx: Any, max_tokens: int = 2000,
657
+ max_time_ms: int = 1000, tier: str = "standard",
658
+ schema: Any = None) -> Optional[str]:
659
+ """Top-level orchestrator: extract intent -> project subgraph -> assemble.
660
+
661
+ Called by orient() when the agent has tasks. Returns formatted projection
662
+ string, or None if no tasks / projection not applicable.
663
+ Accepts an optional ExtractionSchema whose context_unit config drives
664
+ domain-aware priority labels in the projection.
665
+ """
666
+ budget = ProjectionBudget(max_tokens=max_tokens, max_time_ms=max_time_ms, tier=tier)
667
+
668
+ tasks = []
669
+ try:
670
+ if ctx.project:
671
+ tasks = ctx.project.get_open_tasks(
672
+ agent_id=getattr(ctx, "agent_id", ""),
673
+ agent_name=getattr(ctx, "agent_name", ""),
674
+ )
675
+ except Exception:
676
+ pass
677
+
678
+ if not tasks:
679
+ return None
680
+
681
+ # Check cache first (60s TTL) — tier-aware
682
+ agent_id = getattr(ctx, "agent_id", "")
683
+ namespace = getattr(ctx.conn, "namespace", "") or getattr(ctx.conn, "_namespace", "")
684
+ if budget.should_run_stage("cache"):
685
+ cached = get_cached_projection(namespace, agent_id)
686
+ if cached:
687
+ return cached
688
+ if budget.tier == "instant":
689
+ return None
690
+
691
+ # 1. Extract intent from runtime graph
692
+ runtime_db = None
693
+ if hasattr(ctx, "runtime_conn") and ctx.runtime_conn:
694
+ runtime_db = getattr(ctx.runtime_conn, "db", None) or getattr(ctx.runtime_conn, "contextsynapse", None)
695
+
696
+ intent = IntentExtractor.extract(
697
+ tasks=tasks,
698
+ runtime_db=runtime_db,
699
+ agent_name=getattr(ctx, "agent_name", ""),
700
+ agent_id=getattr(ctx, "agent_id", ""),
701
+ )
702
+
703
+ if intent.confidence == 0.0:
704
+ return None
705
+
706
+ # 2. Project subgraph from atomic graph
707
+ atomic_db = getattr(ctx.conn, "db", None) or getattr(ctx.conn, "contextsynapse", None)
708
+ namespace = getattr(ctx.conn, "namespace", "") or getattr(ctx.conn, "_namespace", "")
709
+
710
+ # Extract priority labels from schema's context_unit config (if available)
711
+ _priority_labels = None
712
+ if schema is not None:
713
+ cu_cfg = getattr(schema, "context_unit", None)
714
+ if cu_cfg is not None:
715
+ _priority_labels = getattr(cu_cfg, "priority_labels", None)
716
+
717
+ subgraph = SubgraphProjector.project(
718
+ intent=intent,
719
+ atomic_db=atomic_db,
720
+ namespace=namespace,
721
+ budget=budget,
722
+ priority_labels=_priority_labels,
723
+ )
724
+
725
+ # 3. Collect cross-agent signals — scored by relevance to THIS agent's task
726
+ cross_signals = []
727
+ if runtime_db and ("signals" in budget._stages or "hops" in budget._stages):
728
+ my_name = (getattr(ctx, "agent_name", "") or "").lower()
729
+ candidates = []
730
+ for label in ("Finding", "Insight", "Decision"):
731
+ try:
732
+ for n in runtime_db.get_all_nodes(label=label):
733
+ props = n.properties if hasattr(n, "properties") else {}
734
+ author = (props.get("_agent_name", props.get("created_by", "")) or "").lower()
735
+ if author and author != my_name and author != "system":
736
+ name = (props.get("name", props.get("content", "")) or "")[:80]
737
+ if name:
738
+ # Score by keyword overlap with this agent's intent
739
+ sig_kws = _extract_keywords(name)
740
+ overlap = sum(1 for k in sig_kws if k in intent.keywords)
741
+ candidates.append({
742
+ "agent": author, "label": label,
743
+ "name": name, "_relevance": overlap,
744
+ })
745
+ except Exception:
746
+ pass
747
+ # Take top 5 most relevant signals (not all 99 in a 100-agent session)
748
+ candidates.sort(key=lambda x: x["_relevance"], reverse=True)
749
+ cross_signals = candidates[:5]
750
+
751
+ # 4. Count total nodes in atomic graph for stats
752
+ total_nodes = 0
753
+ try:
754
+ adapter = getattr(atomic_db, "csr_adapter", None) or atomic_db
755
+ if hasattr(adapter, "get_all_nodes"):
756
+ total_nodes = len(list(adapter.get_all_nodes()))
757
+ except Exception:
758
+ pass
759
+
760
+ # 5. Assemble
761
+ # Extract cu_config for zoom-level rendering
762
+ _cu_config = None
763
+ if schema is not None:
764
+ _cu_config = getattr(schema, "context_unit", None)
765
+
766
+ projection = ProjectionAssembler.assemble(
767
+ subgraph=subgraph,
768
+ task_title=tasks[0].get("title", ""),
769
+ total_nodes=total_nodes,
770
+ cross_agent_signals=cross_signals[:5],
771
+ tier=tier,
772
+ cu_config=_cu_config,
773
+ )
774
+
775
+ # 6. Prepend task list
776
+ task_lines = ["YOUR TASKS:"]
777
+ for t in tasks[:5]:
778
+ task_lines.append(f" [{t['priority'].upper()}] {t['title']} (id: {t['id']})")
779
+
780
+ # 7. Record what was projected (for feedback learning)
781
+ try:
782
+ _record_projection(
783
+ namespace=namespace,
784
+ agent_id=getattr(ctx, "agent_id", ""),
785
+ task_keywords=list(intent.keywords.keys())[:5],
786
+ projected_ids=[n["id"] for n in subgraph.nodes],
787
+ )
788
+ except Exception:
789
+ pass
790
+
791
+ # 8. Projection diff — show what changed since last orient
792
+ diff_section = ""
793
+ try:
794
+ prev_log = _projection_log.get(agent_id)
795
+ if prev_log and prev_log.get("projected_ids"):
796
+ prev_ids = prev_log["projected_ids"]
797
+ curr_ids = {n["id"] for n in subgraph.nodes}
798
+ new_ids = curr_ids - prev_ids
799
+ dropped_ids = prev_ids - curr_ids
800
+ if new_ids or dropped_ids:
801
+ diff_lines = []
802
+ if new_ids:
803
+ new_names = [n["name"] for n in subgraph.nodes if n["id"] in new_ids]
804
+ diff_lines.append(f" + {len(new_ids)} new: {', '.join(new_names[:3])}")
805
+ if dropped_ids:
806
+ diff_lines.append(f" - {len(dropped_ids)} no longer relevant")
807
+ diff_section = "\nCHANGED SINCE LAST ORIENT:\n" + "\n".join(diff_lines)
808
+ except Exception:
809
+ pass
810
+
811
+ # 9. Intent drift detection — are agent's actions diverging from task?
812
+ drift_section = ""
813
+ try:
814
+ if "hops" in budget._stages and intent.confidence >= 0.6: # only check when we have action signals
815
+ task_kws = set(_extract_keywords(tasks[0].get("title", "")).keys())
816
+ action_kws = set()
817
+ if runtime_db:
818
+ my_name = (getattr(ctx, "agent_name", "") or "").lower()
819
+ for label in ("Finding", "AgentAction"):
820
+ try:
821
+ for n in runtime_db.get_all_nodes(label=label):
822
+ props = n.properties if hasattr(n, "properties") else {}
823
+ author = (props.get("_agent_name", props.get("agent", "")) or "").lower()
824
+ if author == my_name:
825
+ text = props.get("name", props.get("description", "")) or ""
826
+ action_kws.update(_extract_keywords(text).keys())
827
+ except Exception:
828
+ pass
829
+
830
+ if action_kws and task_kws:
831
+ overlap = task_kws & action_kws
832
+ drift_kws = action_kws - task_kws - _STOP_WORDS
833
+ if len(drift_kws) > len(overlap) * 2 and len(drift_kws) > 3:
834
+ drift_sample = list(drift_kws)[:5]
835
+ drift_section = (
836
+ f"\nDRIFT DETECTED: Your recent work focuses on "
837
+ f"[{', '.join(drift_sample)}] which diverges from your task. "
838
+ f"Consider updating your task or refocusing."
839
+ )
840
+ except Exception:
841
+ pass
842
+
843
+ result = "\n".join(task_lines) + "\n\n" + projection
844
+ if diff_section:
845
+ result += diff_section
846
+ if drift_section:
847
+ result += drift_section
848
+
849
+ # Cache for 60s
850
+ try:
851
+ cache_projection(namespace, agent_id, result)
852
+ except Exception:
853
+ pass
854
+
855
+ return result
856
+
857
+
858
+ # ── Redis Helper ─────────────────────────────────────────────────────
859
+
860
+ def _get_redis():
861
+ """Get Redis client, or None if unavailable."""
862
+ try:
863
+ import os, redis
864
+ url = os.environ.get("CONTEXTSYNAPSE_REDIS_URL") or os.environ.get("AICONTEXTDB_REDIS_URL")
865
+ if url:
866
+ client = redis.Redis.from_url(url, decode_responses=True)
867
+ client.ping()
868
+ return client
869
+ except Exception:
870
+ pass
871
+ return None
872
+
873
+
874
+ # ── Projection Cache ─────────────────────────────────────────────────
875
+
876
+ _CACHE_TTL = 60 # seconds
877
+
878
+ # In-memory fallback cache — used when Redis is unavailable (e.g. test environments)
879
+ _mem_projection_cache: Dict[str, tuple] = {} # key -> (expiry_ts, result)
880
+
881
+ def _cache_key(namespace: str, agent_id: str) -> str:
882
+ return f"cgp:cache:{namespace}:{agent_id}"
883
+
884
+ def cache_projection(namespace: str, agent_id: str, result: str) -> None:
885
+ """Cache a projection result in Redis (60s TTL) and in-memory (always)."""
886
+ import time as _t
887
+ key = _cache_key(namespace, agent_id)
888
+ # Always populate in-memory cache — ensures warm calls are fast even without Redis
889
+ _mem_projection_cache[key] = (_t.monotonic() + _CACHE_TTL, result)
890
+ r = _get_redis()
891
+ if r:
892
+ try:
893
+ r.set(key, result, ex=_CACHE_TTL)
894
+ except Exception:
895
+ pass
896
+
897
+ def get_cached_projection(namespace: str, agent_id: str) -> Optional[str]:
898
+ """Get cached projection, or None if expired/missing."""
899
+ import time as _t
900
+ key = _cache_key(namespace, agent_id)
901
+ # Check in-memory first (zero-latency, always consistent within this process)
902
+ entry = _mem_projection_cache.get(key)
903
+ if entry is not None:
904
+ expiry, result = entry
905
+ if _t.monotonic() < expiry:
906
+ return result
907
+ del _mem_projection_cache[key]
908
+ # Fall back to Redis (shared across workers)
909
+ r = _get_redis()
910
+ if r:
911
+ try:
912
+ val = r.get(key)
913
+ if val is not None:
914
+ return val
915
+ except Exception:
916
+ pass
917
+ return None
918
+
919
+
920
+ def invalidate_projection_cache(namespace: str, agent_id: Optional[str] = None) -> int:
921
+ """Invalidate projection cache for a namespace.
922
+
923
+ If agent_id is given, clears only that agent's cache entry.
924
+ If agent_id is None, clears all agents' entries for this namespace.
925
+
926
+ Returns the number of entries cleared.
927
+ """
928
+ import logging as _log
929
+ _logger = _log.getLogger(__name__)
930
+ count = 0
931
+ prefix = _cache_key(namespace, "") # "cgp:cache:{namespace}:"
932
+
933
+ # Clear in-memory cache entries
934
+ to_remove = []
935
+ for key in list(_mem_projection_cache.keys()):
936
+ if agent_id is None:
937
+ if key.startswith(prefix):
938
+ to_remove.append(key)
939
+ else:
940
+ if key == _cache_key(namespace, agent_id):
941
+ to_remove.append(key)
942
+ for key in to_remove:
943
+ _mem_projection_cache.pop(key, None)
944
+ count += 1
945
+
946
+ # Clear Redis cache entries
947
+ r = _get_redis()
948
+ if r:
949
+ try:
950
+ if agent_id is None:
951
+ pattern = f"{prefix}*"
952
+ cursor = 0
953
+ while True:
954
+ cursor, keys = r.scan(cursor, match=pattern, count=100)
955
+ if keys:
956
+ r.delete(*keys)
957
+ count += len(keys)
958
+ if cursor == 0:
959
+ break
960
+ else:
961
+ key = _cache_key(namespace, agent_id)
962
+ deleted = r.delete(key)
963
+ count += deleted
964
+ except Exception as _e:
965
+ _logger.debug("Redis invalidation error: %s", _e)
966
+
967
+ return count
968
+
969
+
970
+ # ── Projection Feedback Loop (Redis-backed) ──────────────────────────
971
+
972
+ # In-memory fallback when Redis is unavailable
973
+ _projection_log: Dict[str, Dict] = {}
974
+ _feedback_scores: Dict[str, Dict[str, float]] = {}
975
+
976
+
977
+ def _record_projection(namespace: str, agent_id: str, task_keywords: List[str],
978
+ projected_ids: List[str]) -> None:
979
+ """Record what was projected to an agent (for learning from usage)."""
980
+ import time, json as _json
981
+ log_data = {
982
+ "projected_ids": list(projected_ids),
983
+ "task_keywords": task_keywords,
984
+ "timestamp": time.time(),
985
+ "namespace": namespace,
986
+ }
987
+
988
+ # Try Redis first
989
+ r = _get_redis()
990
+ if r:
991
+ try:
992
+ r.set(f"cgp:log:{agent_id}", _json.dumps(log_data), ex=3600) # 1h TTL
993
+ except Exception:
994
+ pass
995
+
996
+ # Always keep in-memory copy
997
+ log_data["projected_ids"] = set(projected_ids)
998
+ _projection_log[agent_id] = log_data
999
+
1000
+
1001
+ def record_usage(agent_id: str, referenced_node_id: str) -> None:
1002
+ """Called when an agent references a node in a Finding/Decision.
1003
+
1004
+ Positive signal: boost this node for future projections.
1005
+ """
1006
+ import json as _json
1007
+
1008
+ # Get projection log (Redis first, then memory)
1009
+ log = None
1010
+ r = _get_redis()
1011
+ if r:
1012
+ try:
1013
+ raw = r.get(f"cgp:log:{agent_id}")
1014
+ if raw:
1015
+ log = _json.loads(raw)
1016
+ log["projected_ids"] = set(log["projected_ids"])
1017
+ except Exception:
1018
+ pass
1019
+ if not log:
1020
+ log = _projection_log.get(agent_id)
1021
+ if not log:
1022
+ return
1023
+
1024
+ ns = log["namespace"]
1025
+ projected = log["projected_ids"]
1026
+
1027
+ if referenced_node_id in projected:
1028
+ # Boost in Redis (7-day TTL)
1029
+ if r:
1030
+ try:
1031
+ key = f"cgp:feedback:{ns}"
1032
+ current = float(r.hget(key, referenced_node_id) or 1.0)
1033
+ r.hset(key, referenced_node_id, str(current * 1.1))
1034
+ r.expire(key, 604800) # 7 days
1035
+ except Exception:
1036
+ pass
1037
+
1038
+ # Also update in-memory
1039
+ if ns not in _feedback_scores:
1040
+ _feedback_scores[ns] = {}
1041
+ _feedback_scores[ns][referenced_node_id] = (
1042
+ _feedback_scores[ns].get(referenced_node_id, 1.0) * 1.1
1043
+ )
1044
+
1045
+
1046
+ def get_feedback_boost(namespace: str, node_id: str) -> float:
1047
+ """Get the learned relevance boost. 1.0 = neutral, >1.0 = useful."""
1048
+ # Try Redis first
1049
+ r = _get_redis()
1050
+ if r:
1051
+ try:
1052
+ val = r.hget(f"cgp:feedback:{namespace}", node_id)
1053
+ if val:
1054
+ return float(val)
1055
+ except Exception:
1056
+ pass
1057
+ return _feedback_scores.get(namespace, {}).get(node_id, 1.0)
1058
+
1059
+
1060
+ def clear_feedback(namespace: str) -> None:
1061
+ """Clear feedback scores when a DIFFERENT atomic context is attached."""
1062
+ _feedback_scores.pop(namespace, None)
1063
+ r = _get_redis()
1064
+ if r:
1065
+ try:
1066
+ r.delete(f"cgp:feedback:{namespace}")
1067
+ except Exception:
1068
+ pass
1069
+ to_remove = [aid for aid, log in _projection_log.items() if log.get("namespace") == namespace]
1070
+ for aid in to_remove:
1071
+ _projection_log.pop(aid, None)
1072
+
1073
+
1074
+ # ── Re-query Tracking (evidence of projection quality) ───────────────────────
1075
+
1076
+ _ORIENT_TIME_TTL = 300 # 5 minutes — window within which an ask() counts as a re-query
1077
+ _REQUERY_TTL = 86400 # 24 hours — counter persists across sessions for daily stats
1078
+
1079
+ # In-memory fallbacks (used when Redis is unavailable)
1080
+ _orient_times: Dict[str, float] = {} # "{namespace}:{agent_id}" → unix timestamp
1081
+ _requery_counts: Dict[str, int] = {} # "{namespace}:{agent_id}" → count
1082
+
1083
+
1084
+ def _rq_key(namespace: str, agent_id: str, suffix: str) -> str:
1085
+ return f"cgp:rq:{suffix}:{namespace}:{agent_id}"
1086
+
1087
+
1088
+ def record_orient_time(namespace: str, agent_id: str) -> None:
1089
+ """Record that orient() was just called. Used to detect re-queries."""
1090
+ import time as _t
1091
+ ts = _t.time()
1092
+ mem_key = f"{namespace}:{agent_id}"
1093
+ _orient_times[mem_key] = ts
1094
+ r = _get_redis()
1095
+ if r:
1096
+ try:
1097
+ r.set(_rq_key(namespace, agent_id, "orient_ts"), str(ts), ex=_ORIENT_TIME_TTL)
1098
+ except Exception as _e:
1099
+ logger.debug("record_orient_time Redis error: %s", _e)
1100
+
1101
+
1102
+ def get_orient_time(namespace: str, agent_id: str) -> Optional[float]:
1103
+ """Return unix timestamp of last orient() for this agent, or None."""
1104
+ import time as _t
1105
+ mem_key = f"{namespace}:{agent_id}"
1106
+ ts = _orient_times.get(mem_key)
1107
+ if ts is not None and (_t.time() - ts) < _ORIENT_TIME_TTL:
1108
+ return ts
1109
+ r = _get_redis()
1110
+ if r:
1111
+ try:
1112
+ val = r.get(_rq_key(namespace, agent_id, "orient_ts"))
1113
+ if val is not None:
1114
+ return float(val)
1115
+ except Exception as _e:
1116
+ logger.debug("get_orient_time Redis error: %s", _e)
1117
+ return None
1118
+
1119
+
1120
+ def record_requery(namespace: str, agent_id: str) -> int:
1121
+ """Increment re-query counter if agent is asking within 5 min of orient().
1122
+
1123
+ Returns the new counter value (or current value if orient was not recent).
1124
+ """
1125
+ import time as _t
1126
+ orient_ts = get_orient_time(namespace, agent_id)
1127
+ if orient_ts is None or (_t.time() - orient_ts) > _ORIENT_TIME_TTL:
1128
+ return get_requery_count(namespace, agent_id)
1129
+
1130
+ mem_key = f"{namespace}:{agent_id}"
1131
+ _requery_counts[mem_key] = _requery_counts.get(mem_key, 0) + 1
1132
+ count = _requery_counts[mem_key]
1133
+ r = _get_redis()
1134
+ if r:
1135
+ try:
1136
+ rk = _rq_key(namespace, agent_id, "requery_count")
1137
+ count = r.incr(rk)
1138
+ r.expire(rk, _REQUERY_TTL)
1139
+ except Exception as _e:
1140
+ logger.debug("record_requery Redis error: %s", _e)
1141
+ return count
1142
+
1143
+
1144
+ def get_requery_count(namespace: str, agent_id: str) -> int:
1145
+ """Return the current re-query count for this agent."""
1146
+ mem_key = f"{namespace}:{agent_id}"
1147
+ r = _get_redis()
1148
+ if r:
1149
+ try:
1150
+ val = r.get(_rq_key(namespace, agent_id, "requery_count"))
1151
+ if val is not None:
1152
+ return int(val)
1153
+ except Exception as _e:
1154
+ logger.debug("get_requery_count Redis error: %s", _e)
1155
+ return _requery_counts.get(mem_key, 0)
1156
+
1157
+
1158
+ # ── Promotion Event Consumer ──────────────────────────────────────────────────
1159
+
1160
+ def get_recent_promotions(namespace: str, since_ts: float) -> list:
1161
+ """Return promotion events for a namespace newer than since_ts.
1162
+
1163
+ Each event is: {"node_id": str, "label": str, "title": str, "promoted_at": float}
1164
+ Returns empty list if no events or on any error.
1165
+ """
1166
+ import json as _json
1167
+ results = []
1168
+
1169
+ # Try Redis first (shared across workers)
1170
+ r = _get_redis()
1171
+ if r:
1172
+ try:
1173
+ rk = f"cgp:promotions:{namespace}"
1174
+ raw_events = r.lrange(rk, 0, 49)
1175
+ for raw in raw_events:
1176
+ try:
1177
+ evt = _json.loads(raw)
1178
+ if evt.get("promoted_at", 0) > since_ts:
1179
+ results.append(evt)
1180
+ except Exception as _e:
1181
+ logger.debug("get_recent_promotions JSON parse error: %s", _e)
1182
+ return results
1183
+ except Exception as _e:
1184
+ logger.debug("get_recent_promotions Redis error: %s", _e)
1185
+
1186
+ # In-memory fallback
1187
+ try:
1188
+ from ..context.promotion import _promotion_events
1189
+ for evt in _promotion_events.get(namespace, []):
1190
+ if evt.get("promoted_at", 0) > since_ts:
1191
+ results.append(evt)
1192
+ except Exception as _e:
1193
+ logger.debug("get_recent_promotions in-memory fallback error: %s", _e)
1194
+
1195
+ return results