contextsynapse 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contextsynapse/__init__.py +107 -0
- contextsynapse/__main__.py +9 -0
- contextsynapse/a2a/__init__.py +28 -0
- contextsynapse/a2a/client.py +120 -0
- contextsynapse/a2a/discovery.py +128 -0
- contextsynapse/a2a/handlers.py +164 -0
- contextsynapse/a2a/models.py +246 -0
- contextsynapse/a2a/streaming.py +92 -0
- contextsynapse/a2a/task_manager.py +275 -0
- contextsynapse/adapters/__init__.py +98 -0
- contextsynapse/adapters/_base.py +298 -0
- contextsynapse/adapters/autogen/__init__.py +4 -0
- contextsynapse/adapters/autogen/tools.py +34 -0
- contextsynapse/adapters/crewai/__init__.py +4 -0
- contextsynapse/adapters/crewai/tools.py +63 -0
- contextsynapse/adapters/langchain/__init__.py +19 -0
- contextsynapse/adapters/langchain/message_history.py +17 -0
- contextsynapse/adapters/langchain/retriever.py +158 -0
- contextsynapse/adapters/langchain/tools.py +43 -0
- contextsynapse/adapters/langgraph/__init__.py +21 -0
- contextsynapse/adapters/langgraph/checkpoint.py +223 -0
- contextsynapse/adapters/langgraph/context_tools.py +341 -0
- contextsynapse/adapters/langgraph/message_history.py +138 -0
- contextsynapse/adapters/langgraph/tools.py +43 -0
- contextsynapse/adapters/llamaindex/__init__.py +4 -0
- contextsynapse/adapters/llamaindex/tools.py +64 -0
- contextsynapse/adapters/openai/__init__.py +24 -0
- contextsynapse/adapters/openai/tools.py +206 -0
- contextsynapse/adapters/pydantic_ai/__init__.py +4 -0
- contextsynapse/adapters/pydantic_ai/tools.py +36 -0
- contextsynapse/adapters/swarm/__init__.py +4 -0
- contextsynapse/adapters/swarm/tools.py +44 -0
- contextsynapse/agents/__init__.py +6 -0
- contextsynapse/agents/auto_capture.py +80 -0
- contextsynapse/agents/dispatcher.py +299 -0
- contextsynapse/agents/notifications.py +112 -0
- contextsynapse/aiql/__init__.py +71 -0
- contextsynapse/aiql/cache.py +341 -0
- contextsynapse/aiql/compiler/__init__.py +62 -0
- contextsynapse/aiql/compiler/optimizer.py +126 -0
- contextsynapse/aiql/compiler/planner.py +366 -0
- contextsynapse/aiql/compiler/statistics.py +235 -0
- contextsynapse/aiql/compiler/validator.py +425 -0
- contextsynapse/aiql/engine/__init__.py +11 -0
- contextsynapse/aiql/engine/chunk_storage.py +2584 -0
- contextsynapse/aiql/engine/executor.py +7737 -0
- contextsynapse/aiql/engine/extraction_integration.py +442 -0
- contextsynapse/aiql/engine/graph_builder.py +405 -0
- contextsynapse/aiql/engine/node_creation_from_normalized.py +691 -0
- contextsynapse/aiql/engine/performance_monitor.py +289 -0
- contextsynapse/aiql/engine/postprocessing.py +365 -0
- contextsynapse/aiql/engine/ray_stage_executors.py +918 -0
- contextsynapse/aiql/engine/redis_cache.py +106 -0
- contextsynapse/aiql/engine/streaming_json.py +366 -0
- contextsynapse/aiql/engine/streaming_parquet.py +568 -0
- contextsynapse/aiql/engine/streaming_ray_chunking.py +419 -0
- contextsynapse/aiql/grammar/__init__.py +14 -0
- contextsynapse/aiql/grammar/aiql_grammar.py +1822 -0
- contextsynapse/aiql/grammar/base.py +71 -0
- contextsynapse/aiql/parser/__init__.py +46 -0
- contextsynapse/aiql/parser/aiql_parser.py +11330 -0
- contextsynapse/aiql/parser/base_parser.py +47 -0
- contextsynapse/api/__init__.py +5 -0
- contextsynapse/api/a2a_router.py +233 -0
- contextsynapse/api/admin_router.py +140 -0
- contextsynapse/api/agent_worker_router.py +978 -0
- contextsynapse/api/algorithms_router.py +317 -0
- contextsynapse/api/api.py +4144 -0
- contextsynapse/api/audit.py +204 -0
- contextsynapse/api/auth.py +441 -0
- contextsynapse/api/auth_router.py +537 -0
- contextsynapse/api/billing.py +261 -0
- contextsynapse/api/billing_router.py +275 -0
- contextsynapse/api/boundary_router.py +452 -0
- contextsynapse/api/cognition_router.py +116 -0
- contextsynapse/api/connector_router.py +237 -0
- contextsynapse/api/context_router.py +1619 -0
- contextsynapse/api/dashboard_router.py +11367 -0
- contextsynapse/api/errors.py +183 -0
- contextsynapse/api/events.py +88 -0
- contextsynapse/api/experiment_bootstrap.py +321 -0
- contextsynapse/api/experiment_orchestrator.py +1078 -0
- contextsynapse/api/experiment_router.py +2089 -0
- contextsynapse/api/feature_gate.py +89 -0
- contextsynapse/api/federation_router.py +108 -0
- contextsynapse/api/integrations.py +567 -0
- contextsynapse/api/integrations_router.py +461 -0
- contextsynapse/api/intelligence_router.py +1028 -0
- contextsynapse/api/metering.py +155 -0
- contextsynapse/api/metrics.py +112 -0
- contextsynapse/api/models.py +38 -0
- contextsynapse/api/monitoring_router.py +174 -0
- contextsynapse/api/pipeline_router.py +1437 -0
- contextsynapse/api/playground_conn.py +116 -0
- contextsynapse/api/projects_router.py +131 -0
- contextsynapse/api/queries_router.py +282 -0
- contextsynapse/api/quota.py +95 -0
- contextsynapse/api/rate_limit.py +64 -0
- contextsynapse/api/search_router.py +261 -0
- contextsynapse/api/shield_router.py +447 -0
- contextsynapse/api/tenant_guard.py +103 -0
- contextsynapse/api/tenant_quotas.py +231 -0
- contextsynapse/api/tenants.py +305 -0
- contextsynapse/api/users.py +646 -0
- contextsynapse/api/v1.py +167 -0
- contextsynapse/api/vertical_builder_router.py +387 -0
- contextsynapse/benchmark/__init__.py +15 -0
- contextsynapse/benchmark/__main__.py +64 -0
- contextsynapse/benchmark/agents.py +338 -0
- contextsynapse/benchmark/live_ab.py +338 -0
- contextsynapse/benchmark/reporter.py +149 -0
- contextsynapse/benchmark/runner.py +264 -0
- contextsynapse/benchmark/savings.py +266 -0
- contextsynapse/benchmark/token_counter.py +212 -0
- contextsynapse/benchmark/workloads.py +289 -0
- contextsynapse/buffer/__init__.py +154 -0
- contextsynapse/buffer/base.py +257 -0
- contextsynapse/buffer/duckdb_buffer.py +246 -0
- contextsynapse/buffer/file_buffer.py +229 -0
- contextsynapse/buffer/lmdb_buffer.py +286 -0
- contextsynapse/buffer/local_buffer.py +154 -0
- contextsynapse/buffer/redis_buffer.py +211 -0
- contextsynapse/buffer/sqlite_buffer.py +266 -0
- contextsynapse/cache.py +2 -0
- contextsynapse/cli/__init__.py +40 -0
- contextsynapse/cli/cli.py +475 -0
- contextsynapse/cli/config_cli.py +177 -0
- contextsynapse/cli/scaffold.py +465 -0
- contextsynapse/cognition/__init__.py +31 -0
- contextsynapse/cognition/derivation.py +55 -0
- contextsynapse/cognition/events.py +135 -0
- contextsynapse/cognition/feedback.py +52 -0
- contextsynapse/cognition/hallucination.py +90 -0
- contextsynapse/cognition/invalidation.py +70 -0
- contextsynapse/cognition/read_tracker.py +50 -0
- contextsynapse/collaboration.py +2 -0
- contextsynapse/comparison.py +2 -0
- contextsynapse/config/__init__.py +10 -0
- contextsynapse/config/connectors/amfi_nav.yaml +18 -0
- contextsynapse/config/connectors/auto_sector.yaml +20 -0
- contextsynapse/config/connectors/banking_sector.yaml +20 -0
- contextsynapse/config/connectors/energy_sector.yaml +20 -0
- contextsynapse/config/connectors/fmcg_sector.yaml +20 -0
- contextsynapse/config/connectors/nsdl_fpi.yaml +9 -0
- contextsynapse/config/connectors/nse_price.yaml +61 -0
- contextsynapse/config/connectors/pharma_sector.yaml +20 -0
- contextsynapse/config/connectors/rbi_dbie.yaml +12 -0
- contextsynapse/config/context_presets/ai_research.json +25 -0
- contextsynapse/config/context_presets/chatgpt_conversations.json +19 -0
- contextsynapse/config/context_presets/claude_conversations.json +19 -0
- contextsynapse/config/context_presets/legal_compliance.json +21 -0
- contextsynapse/config/context_presets/meeting_notes.json +21 -0
- contextsynapse/config/context_presets/slack_engineering.json +21 -0
- contextsynapse/config/context_presets/tech_climate.json +24 -0
- contextsynapse/config/context_presets/toi_politics.json +25 -0
- contextsynapse/config/database_config.py +453 -0
- contextsynapse/config/deployment.py +221 -0
- contextsynapse/config/gpt_action_schema.json +164 -0
- contextsynapse/config/indicators/india_macro.yaml +213 -0
- contextsynapse/config/logging_config.py +88 -0
- contextsynapse/config/mode_config.py +223 -0
- contextsynapse/config/schemas/api_spec.yaml +48 -0
- contextsynapse/config/schemas/architecture_doc.yaml +51 -0
- contextsynapse/config/schemas/chat_history.yaml +90 -0
- contextsynapse/config/schemas/chatgpt_conversation.yaml +101 -0
- contextsynapse/config/schemas/claude_conversation.yaml +148 -0
- contextsynapse/config/schemas/contract.yaml +48 -0
- contextsynapse/config/schemas/conversation.yaml +167 -0
- contextsynapse/config/schemas/database.yaml +74 -0
- contextsynapse/config/schemas/decision.yaml +72 -0
- contextsynapse/config/schemas/ecommerce.yaml +56 -0
- contextsynapse/config/schemas/education.yaml +42 -0
- contextsynapse/config/schemas/healthcare.yaml +133 -0
- contextsynapse/config/schemas/hr_document.yaml +48 -0
- contextsynapse/config/schemas/invoice.yaml +40 -0
- contextsynapse/config/schemas/knowledge_base.yaml +75 -0
- contextsynapse/config/schemas/legal.yaml +42 -0
- contextsynapse/config/schemas/marketing_content.yaml +48 -0
- contextsynapse/config/schemas/meeting_notes.yaml +51 -0
- contextsynapse/config/schemas/news_article.yaml +61 -0
- contextsynapse/config/schemas/real_estate.yaml +49 -0
- contextsynapse/config/schemas/requirements_doc.yaml +48 -0
- contextsynapse/config/schemas/research_paper.yaml +56 -0
- contextsynapse/config/schemas/resume.yaml +49 -0
- contextsynapse/config/schemas/retail.yaml +42 -0
- contextsynapse/config/schemas/rule_meta.yaml +173 -0
- contextsynapse/config/schemas/rules.yaml +72 -0
- contextsynapse/config/schemas/screener_meta.yaml +236 -0
- contextsynapse/config/schemas/sdlc.yaml +103 -0
- contextsynapse/config/schemas/session_graph.yaml +172 -0
- contextsynapse/config/schemas/support_ticket.yaml +48 -0
- contextsynapse/config/schemas/web.yaml +64 -0
- contextsynapse/config/sensors/earnings_calendar.yaml +52 -0
- contextsynapse/config/sensors/global_risk.yaml +53 -0
- contextsynapse/config/sensors/government_policy.yaml +54 -0
- contextsynapse/config/sensors/supply_chain.yaml +50 -0
- contextsynapse/config/sensors/templates/behavioral.yaml +56 -0
- contextsynapse/config/sensors/templates/equity_sentiment.yaml +48 -0
- contextsynapse/config/sensors/templates/global_macro.yaml +47 -0
- contextsynapse/config/sensors/templates/market_data.yaml +118 -0
- contextsynapse/config/sensors/templates/sector_technical.yaml +47 -0
- contextsynapse/config/sensors/weather.yaml +52 -0
- contextsynapse/config/storage_strategy.py +202 -0
- contextsynapse/config/templates/competitive_intel.yaml +39 -0
- contextsynapse/config/templates/geopolitical_risk.yaml +46 -0
- contextsynapse/config/templates/market_analysis.yaml +58 -0
- contextsynapse/connectors/__init__.py +15 -0
- contextsynapse/connectors/api_router.py +121 -0
- contextsynapse/connectors/base.py +357 -0
- contextsynapse/connectors/financial/audio.py +170 -0
- contextsynapse/connectors/financial/youtube.py +272 -0
- contextsynapse/connectors/github.py +164 -0
- contextsynapse/connectors/jira.py +120 -0
- contextsynapse/connectors/kafka_connector.py +124 -0
- contextsynapse/connectors/pipeline_connector.py +747 -0
- contextsynapse/connectors/registry.py +116 -0
- contextsynapse/connectors/scheduler.py +87 -0
- contextsynapse/connectors/universal.py +360 -0
- contextsynapse/context/__init__.py +96 -0
- contextsynapse/context/__main__.py +4 -0
- contextsynapse/context/acl.py +125 -0
- contextsynapse/context/agent_card.py +201 -0
- contextsynapse/context/agent_memory.py +814 -0
- contextsynapse/context/agents.py +472 -0
- contextsynapse/context/agents_redis.py +391 -0
- contextsynapse/context/assembled.py +204 -0
- contextsynapse/context/attribution.py +113 -0
- contextsynapse/context/blob.py +184 -0
- contextsynapse/context/boundaries.py +187 -0
- contextsynapse/context/boundary.py +507 -0
- contextsynapse/context/bundle.py +155 -0
- contextsynapse/context/cli.py +519 -0
- contextsynapse/context/collaboration.py +165 -0
- contextsynapse/context/compiler.py +604 -0
- contextsynapse/context/composite.py +536 -0
- contextsynapse/context/compression.py +147 -0
- contextsynapse/context/context_manager.py +888 -0
- contextsynapse/context/context_schema.py +90 -0
- contextsynapse/context/context_schema.yaml +105 -0
- contextsynapse/context/context_units.py +891 -0
- contextsynapse/context/conversation.py +488 -0
- contextsynapse/context/dedup.py +207 -0
- contextsynapse/context/document_processor.py +698 -0
- contextsynapse/context/embedding_hooks.py +338 -0
- contextsynapse/context/execution_schema.py +274 -0
- contextsynapse/context/fan_out.py +138 -0
- contextsynapse/context/frozen.py +80 -0
- contextsynapse/context/gravity.py +227 -0
- contextsynapse/context/hub.py +1401 -0
- contextsynapse/context/ingest.py +391 -0
- contextsynapse/context/injection.py +226 -0
- contextsynapse/context/interaction_graphifier.py +562 -0
- contextsynapse/context/layers.py +574 -0
- contextsynapse/context/projection.py +1195 -0
- contextsynapse/context/promotion.py +634 -0
- contextsynapse/context/proof.py +131 -0
- contextsynapse/context/propagation.py +577 -0
- contextsynapse/context/pubsub.py +135 -0
- contextsynapse/context/quality.py +1054 -0
- contextsynapse/context/quality_tracker.py +270 -0
- contextsynapse/context/scoping.py +370 -0
- contextsynapse/context/seed.py +992 -0
- contextsynapse/context/session.py +1142 -0
- contextsynapse/context/session_graph.py +593 -0
- contextsynapse/context/session_memory.py +309 -0
- contextsynapse/context/session_resolver.py +111 -0
- contextsynapse/context/skill_learning.py +237 -0
- contextsynapse/context/smart_budget.py +129 -0
- contextsynapse/context/source_policy.py +242 -0
- contextsynapse/context/store_factory.py +53 -0
- contextsynapse/context/sync.py +648 -0
- contextsynapse/context/token.py +236 -0
- contextsynapse/context/vector_integration.py +297 -0
- contextsynapse/context/webhooks.py +194 -0
- contextsynapse/context/working_memory.py +224 -0
- contextsynapse/core/__init__.py +18 -0
- contextsynapse/core/archiver.py +237 -0
- contextsynapse/core/change_stream.py +282 -0
- contextsynapse/core/checkpoint.py +310 -0
- contextsynapse/core/cloud_storage.py +387 -0
- contextsynapse/core/context_state.py +651 -0
- contextsynapse/core/cron.py +324 -0
- contextsynapse/core/datasource.py +643 -0
- contextsynapse/core/db.py +289 -0
- contextsynapse/core/federation.py +283 -0
- contextsynapse/core/graph_coordinator.py +201 -0
- contextsynapse/core/graph_intelligence.py +388 -0
- contextsynapse/core/graph_structures.py +106 -0
- contextsynapse/core/graph_sync.py +157 -0
- contextsynapse/core/hybrid_graph_storage.py +2998 -0
- contextsynapse/core/lifecycle.py +691 -0
- contextsynapse/core/multiworker.py +378 -0
- contextsynapse/core/pruning.py +312 -0
- contextsynapse/core/redis_registry.py +524 -0
- contextsynapse/core/registry.py +1043 -0
- contextsynapse/core/registry_factory.py +51 -0
- contextsynapse/core/registry_metadata.py +294 -0
- contextsynapse/core/replay.py +354 -0
- contextsynapse/core/replication.py +339 -0
- contextsynapse/core/time_travel.py +260 -0
- contextsynapse/core/write_behind.py +152 -0
- contextsynapse/core/write_context.py +39 -0
- contextsynapse/db/__init__.py +1 -0
- contextsynapse/db/postgres.py +267 -0
- contextsynapse/db/profile.py +236 -0
- contextsynapse/db/rules.py +328 -0
- contextsynapse/demo/__init__.py +0 -0
- contextsynapse/demo/report.py +147 -0
- contextsynapse/demo/runner.py +520 -0
- contextsynapse/demo/scenario.py +165 -0
- contextsynapse/demo/session_demo.py +280 -0
- contextsynapse/demo/session_report.py +143 -0
- contextsynapse/demo/universal_runner.py +257 -0
- contextsynapse/engine/__init__.py +15 -0
- contextsynapse/engine/core.py +555 -0
- contextsynapse/extraction/__init__.py +69 -0
- contextsynapse/extraction/cli.py +274 -0
- contextsynapse/extraction/config.py +107 -0
- contextsynapse/extraction/examples/example_usage.py +118 -0
- contextsynapse/extraction/examples/sample_manifest.json +53 -0
- contextsynapse/extraction/examples/sample_normalized.json +185 -0
- contextsynapse/extraction/extract_engine.py +440 -0
- contextsynapse/extraction/extractors/__init__.py +37 -0
- contextsynapse/extraction/extractors/chat_export_extractor.py +189 -0
- contextsynapse/extraction/extractors/csv_extractor.py +83 -0
- contextsynapse/extraction/extractors/docx_extractor.py +129 -0
- contextsynapse/extraction/extractors/excel_extractor.py +152 -0
- contextsynapse/extraction/extractors/html_extractor.py +146 -0
- contextsynapse/extraction/extractors/pdf_extractor.py +635 -0
- contextsynapse/extraction/extractors/pdf_extractor_layoutparser.py +408 -0
- contextsynapse/extraction/extractors/register_extractors.py +63 -0
- contextsynapse/extraction/extractors/text_extractor.py +65 -0
- contextsynapse/extraction/extractors/txt_extractor.py +120 -0
- contextsynapse/extraction/extractors/website_extractor.py +246 -0
- contextsynapse/extraction/fact_extractor.py +236 -0
- contextsynapse/extraction/file_manager.py +353 -0
- contextsynapse/extraction/hierarchy.py +239 -0
- contextsynapse/extraction/id_generator.py +450 -0
- contextsynapse/extraction/layout_detector.py +409 -0
- contextsynapse/extraction/llm_entity_extractor.py +301 -0
- contextsynapse/extraction/normalized_store.py +480 -0
- contextsynapse/extraction/normalizer.py +594 -0
- contextsynapse/extraction/parquet_writer.py +598 -0
- contextsynapse/extraction/post_processors/__init__.py +40 -0
- contextsynapse/extraction/post_processors/column_reorganizer.py +507 -0
- contextsynapse/extraction/ray_normalization.py +233 -0
- contextsynapse/extraction/ray_runner.py +207 -0
- contextsynapse/extraction/redis_schema_registry.py +222 -0
- contextsynapse/extraction/registry.py +204 -0
- contextsynapse/extraction/schema_loader.py +663 -0
- contextsynapse/extraction/schema_registry.py +230 -0
- contextsynapse/extraction/semantic_normalizer.py +245 -0
- contextsynapse/extraction/version_manager.py +314 -0
- contextsynapse/gateway/__init__.py +24 -0
- contextsynapse/gateway/action_emitter.py +212 -0
- contextsynapse/gateway/cost_tracker.py +508 -0
- contextsynapse/gateway/gateway.py +377 -0
- contextsynapse/gateway/policy.py +303 -0
- contextsynapse/governance/__init__.py +13 -0
- contextsynapse/governance/layer.py +513 -0
- contextsynapse/ingestion/__init__.py +15 -0
- contextsynapse/ingestion/amplifier.py +288 -0
- contextsynapse/ingestion/async_ingest.py +463 -0
- contextsynapse/ingestion/chunker.py +303 -0
- contextsynapse/ingestion/chunking.py +58 -0
- contextsynapse/ingestion/cleaner.py +190 -0
- contextsynapse/ingestion/cleanse.py +252 -0
- contextsynapse/ingestion/connectors/__init__.py +38 -0
- contextsynapse/ingestion/connectors/base.py +300 -0
- contextsynapse/ingestion/connectors/chatgpt_memory.py +247 -0
- contextsynapse/ingestion/connectors/claude_memory.py +274 -0
- contextsynapse/ingestion/connectors/sap_ingestor.py +208 -0
- contextsynapse/ingestion/content_detector.py +80 -0
- contextsynapse/ingestion/content_monitor.py +502 -0
- contextsynapse/ingestion/dedup.py +106 -0
- contextsynapse/ingestion/feed.py +640 -0
- contextsynapse/ingestion/filters.py +461 -0
- contextsynapse/ingestion/graph_builder.py +705 -0
- contextsynapse/ingestion/input_classifier.py +307 -0
- contextsynapse/ingestion/job_manager.py +571 -0
- contextsynapse/ingestion/llm_extractor.py +610 -0
- contextsynapse/ingestion/orchestrator.py +116 -0
- contextsynapse/ingestion/parsers/__init__.py +19 -0
- contextsynapse/ingestion/parsers/graph_parser_registry.py +103 -0
- contextsynapse/ingestion/parsers/graphml_parser.py +141 -0
- contextsynapse/ingestion/parsers/jsonld_parser.py +171 -0
- contextsynapse/ingestion/parsers/rdf_parser.py +150 -0
- contextsynapse/ingestion/pipeline_context.py +228 -0
- contextsynapse/ingestion/pipeline_store.py +1195 -0
- contextsynapse/ingestion/queue.py +133 -0
- contextsynapse/ingestion/reprocess.py +179 -0
- contextsynapse/ingestion/scenario_router.py +224 -0
- contextsynapse/ingestion/schema_extractor.py +759 -0
- contextsynapse/ingestion/schema_validator.py +274 -0
- contextsynapse/ingestion/sdlc_ingest.py +87 -0
- contextsynapse/ingestion/smart_ingest.py +2451 -0
- contextsynapse/ingestion/source_connector.py +171 -0
- contextsynapse/ingestion/stage_executor.py +2095 -0
- contextsynapse/ingestion/stages/__init__.py +9 -0
- contextsynapse/ingestion/stages/tabular_to_graph.py +289 -0
- contextsynapse/ingestion/strategies.py +386 -0
- contextsynapse/ingestion/universal/__init__.py +4 -0
- contextsynapse/ingestion/universal/_operator_registry.py +59 -0
- contextsynapse/ingestion/universal/ingest_content.py +32 -0
- contextsynapse/ingestion/universal/operators/__init__.py +1 -0
- contextsynapse/ingestion/universal/operators/base.py +19 -0
- contextsynapse/ingestion/universal/operators/build_edges.py +75 -0
- contextsynapse/ingestion/universal/operators/cluster_topics.py +397 -0
- contextsynapse/ingestion/universal/operators/deduplicate.py +45 -0
- contextsynapse/ingestion/universal/operators/detect_signals.py +90 -0
- contextsynapse/ingestion/universal/operators/embed.py +80 -0
- contextsynapse/ingestion/universal/operators/extract_entities.py +253 -0
- contextsynapse/ingestion/universal/operators/extract_preferences.py +117 -0
- contextsynapse/ingestion/universal/operators/filter_content.py +69 -0
- contextsynapse/ingestion/universal/operators/index_bm25.py +52 -0
- contextsynapse/ingestion/universal/operators/infer_domains.py +69 -0
- contextsynapse/ingestion/universal/operators/link_cross_reference.py +210 -0
- contextsynapse/ingestion/universal/operators/parse_records.py +66 -0
- contextsynapse/ingestion/universal/operators/resolve_entities.py +261 -0
- contextsynapse/ingestion/universal/operators/scanners/__init__.py +16 -0
- contextsynapse/ingestion/universal/operators/scanners/edges.py +173 -0
- contextsynapse/ingestion/universal/operators/scanners/git.py +187 -0
- contextsynapse/ingestion/universal/operators/scanners/github.py +135 -0
- contextsynapse/ingestion/universal/operators/scanners/github_api.py +509 -0
- contextsynapse/ingestion/universal/operators/scanners/helpers.py +67 -0
- contextsynapse/ingestion/universal/operators/scanners/jira.py +65 -0
- contextsynapse/ingestion/universal/operators/scanners/llm_enrichment.py +82 -0
- contextsynapse/ingestion/universal/operators/scanners/repo.py +149 -0
- contextsynapse/ingestion/universal/operators/sdlc_scan.py +213 -0
- contextsynapse/ingestion/universal/operators/spec_scanner.py +149 -0
- contextsynapse/ingestion/universal/operators/store_documents.py +72 -0
- contextsynapse/ingestion/universal/operators/synthesize_cu.py +180 -0
- contextsynapse/ingestion/universal/operators/validate_gate.py +65 -0
- contextsynapse/ingestion/universal/parsers/__init__.py +5 -0
- contextsynapse/ingestion/universal/parsers/record_parser.py +203 -0
- contextsynapse/ingestion/universal/parsers/turn_parser.py +329 -0
- contextsynapse/ingestion/universal/shared_signals.py +161 -0
- contextsynapse/ingestion/universal/stage_executor.py +219 -0
- contextsynapse/ingestion/web_crawler.py +265 -0
- contextsynapse/intelligence/__init__.py +53 -0
- contextsynapse/intelligence/alerts.py +198 -0
- contextsynapse/intelligence/collector.py +655 -0
- contextsynapse/intelligence/comparison.py +198 -0
- contextsynapse/intelligence/config.py +56 -0
- contextsynapse/intelligence/conflict_detector.py +182 -0
- contextsynapse/intelligence/context_gaps.py +192 -0
- contextsynapse/intelligence/context_radar.py +135 -0
- contextsynapse/intelligence/correlation_engine.py +416 -0
- contextsynapse/intelligence/correlation_template.py +120 -0
- contextsynapse/intelligence/dedup.py +296 -0
- contextsynapse/intelligence/event_bus.py +91 -0
- contextsynapse/intelligence/feedback_loop.py +101 -0
- contextsynapse/intelligence/freshness.py +307 -0
- contextsynapse/intelligence/fundamental_feed.py +551 -0
- contextsynapse/intelligence/geo_data.py +170 -0
- contextsynapse/intelligence/geo_reference.py +112 -0
- contextsynapse/intelligence/graph_fusion.py +465 -0
- contextsynapse/intelligence/heuristics.py +176 -0
- contextsynapse/intelligence/impact_tracker.py +336 -0
- contextsynapse/intelligence/macro_indicators.py +319 -0
- contextsynapse/intelligence/ocr.py +104 -0
- contextsynapse/intelligence/persistence.py +267 -0
- contextsynapse/intelligence/pipeline_health.py +130 -0
- contextsynapse/intelligence/price_feed.py +221 -0
- contextsynapse/intelligence/rate_limiter.py +130 -0
- contextsynapse/intelligence/reactive.py +411 -0
- contextsynapse/intelligence/runtime_context.py +390 -0
- contextsynapse/intelligence/sentiment_decay.py +426 -0
- contextsynapse/intelligence/session.py +277 -0
- contextsynapse/intelligence/signal_hierarchy.py +438 -0
- contextsynapse/intelligence/signals.py +185 -0
- contextsynapse/intelligence/source_watcher.py +132 -0
- contextsynapse/intelligence/table_enricher.py +256 -0
- contextsynapse/intelligence/tagger.py +319 -0
- contextsynapse/intelligence/timeseries.py +209 -0
- contextsynapse/intelligence/tracker.py +318 -0
- contextsynapse/intelligence/watchdog.py +392 -0
- contextsynapse/intelligence/watchdog_manager.py +263 -0
- contextsynapse/intelligence/webhook_receiver.py +165 -0
- contextsynapse/intelligence/weight_learner.py +234 -0
- contextsynapse/llm/__init__.py +8 -0
- contextsynapse/llm/client.py +348 -0
- contextsynapse/llm/rate_limiter.py +166 -0
- contextsynapse/logging_config.py +2 -0
- contextsynapse/marketplace/__init__.py +7 -0
- contextsynapse/marketplace/registry.py +173 -0
- contextsynapse/mcp/__init__.py +11 -0
- contextsynapse/mcp/__main__.py +4 -0
- contextsynapse/mcp/auth_middleware.py +212 -0
- contextsynapse/mcp/connection_pool.py +128 -0
- contextsynapse/mcp/server.py +986 -0
- contextsynapse/mcp/session_context.py +67 -0
- contextsynapse/memory/__init__.py +34 -0
- contextsynapse/memory/engine.py +475 -0
- contextsynapse/memory/temporal.py +327 -0
- contextsynapse/metadata/__init__.py +12 -0
- contextsynapse/metadata/metadata_db.py +272 -0
- contextsynapse/metadata/metadata_query_tool.py +300 -0
- contextsynapse/metadata/metadata_tracker.py +425 -0
- contextsynapse/models/__init__.py +27 -0
- contextsynapse/models/embedding_service.py +360 -0
- contextsynapse/models/model_registry.py +228 -0
- contextsynapse/multiworker.py +2 -0
- contextsynapse/pipelines/__init__.py +1 -0
- contextsynapse/pipelines/executor.py +851 -0
- contextsynapse/pipelines/filters.py +189 -0
- contextsynapse/pipelines/models.py +176 -0
- contextsynapse/pipelines/scheduler.py +268 -0
- contextsynapse/playground/__init__.py +1 -0
- contextsynapse/playground/templates.py +85 -0
- contextsynapse/plugins/__init__.py +43 -0
- contextsynapse/plugins/api.py +66 -0
- contextsynapse/plugins/base.py +350 -0
- contextsynapse/plugins/domain_interface.py +508 -0
- contextsynapse/plugins/domain_loader.py +241 -0
- contextsynapse/plugins/loader.py +148 -0
- contextsynapse/plugins/registry.py +265 -0
- contextsynapse/project/__init__.py +11 -0
- contextsynapse/project/code_context.py +807 -0
- contextsynapse/project/connectors/__init__.py +4 -0
- contextsynapse/project/connectors/github_connector.py +113 -0
- contextsynapse/project/connectors/jira_connector.py +160 -0
- contextsynapse/project/graph.py +1023 -0
- contextsynapse/project/project_context.py +545 -0
- contextsynapse/project/scanner_registry.py +61 -0
- contextsynapse/project/schema_manager.py +322 -0
- contextsynapse/project/schemas/knowledge.yaml +59 -0
- contextsynapse/project/schemas/sdlc.yaml +369 -0
- contextsynapse/project/sdlc_schema.py +172 -0
- contextsynapse/project/spec_parser.py +85 -0
- contextsynapse/project/task_lock.py +181 -0
- contextsynapse/project/task_stream.py +203 -0
- contextsynapse/project/team_tools.py +537 -0
- contextsynapse/py.typed +0 -0
- contextsynapse/realtime/__init__.py +6 -0
- contextsynapse/realtime/collaboration.py +147 -0
- contextsynapse/rules/__init__.py +10 -0
- contextsynapse/rules/engine.py +116 -0
- contextsynapse/rules/evaluators/__init__.py +6 -0
- contextsynapse/rules/evaluators/aggregate.py +205 -0
- contextsynapse/rules/evaluators/custom.py +170 -0
- contextsynapse/rules/evaluators/pre_action.py +125 -0
- contextsynapse/rules/evaluators/temporal.py +129 -0
- contextsynapse/rules/loader.py +52 -0
- contextsynapse/rules/models.py +124 -0
- contextsynapse/rules/universal.py +393 -0
- contextsynapse/scheduler.py +229 -0
- contextsynapse/schema/__init__.py +42 -0
- contextsynapse/schema/compiler.py +252 -0
- contextsynapse/schema/composer.py +158 -0
- contextsynapse/schema/dedup.py +73 -0
- contextsynapse/schema/derivation.py +172 -0
- contextsynapse/schema/sdl.py +326 -0
- contextsynapse/schema/validation_gate.py +124 -0
- contextsynapse/sdk/__init__.py +63 -0
- contextsynapse/sdk/agent.py +214 -0
- contextsynapse/sdk/client.py +269 -0
- contextsynapse/sdk/exceptions.py +23 -0
- contextsynapse/sdk/models.py +78 -0
- contextsynapse/sdk/session.py +408 -0
- contextsynapse/sdk/tracked.py +218 -0
- contextsynapse/sdk/worker.py +266 -0
- contextsynapse/sdk/workspace_client.py +98 -0
- contextsynapse/sdk/ws.py +41 -0
- contextsynapse/search/__init__.py +20 -0
- contextsynapse/search/embedding_cache.py +140 -0
- contextsynapse/search/enhanced_search.py +152 -0
- contextsynapse/search/fulltext.py +130 -0
- contextsynapse/search/graph_search.py +634 -0
- contextsynapse/search/lmdb_index.py +1218 -0
- contextsynapse/search/nl_to_aiql.py +199 -0
- contextsynapse/search/rag.py +1346 -0
- contextsynapse/search/rag_cache.py +167 -0
- contextsynapse/search/reasoning_chain.py +231 -0
- contextsynapse/search/redis_search.py +207 -0
- contextsynapse/search/retrieval_quality.py +314 -0
- contextsynapse/search/semantic_query.py +455 -0
- contextsynapse/search/text_resolver.py +159 -0
- contextsynapse/search/whoosh_search.py +373 -0
- contextsynapse/security/__init__.py +17 -0
- contextsynapse/security/audit.py +297 -0
- contextsynapse/security/audit_logger.py +97 -0
- contextsynapse/security/audit_trail.py +270 -0
- contextsynapse/security/auth_unified.py +284 -0
- contextsynapse/security/auto_tagger.py +230 -0
- contextsynapse/security/data_security.py +218 -0
- contextsynapse/security/encryption.py +207 -0
- contextsynapse/security/identity.py +447 -0
- contextsynapse/security/jwt_identity.py +59 -0
- contextsynapse/security/middleware.py +336 -0
- contextsynapse/security/pii.py +233 -0
- contextsynapse/security/rbac.py +96 -0
- contextsynapse/security/rls.py +205 -0
- contextsynapse/security/sanitize.py +233 -0
- contextsynapse/security/scoped_encryption.py +131 -0
- contextsynapse/security/tenant.py +401 -0
- contextsynapse/shield/__init__.py +21 -0
- contextsynapse/shield/anomaly.py +56 -0
- contextsynapse/shield/permissions.py +99 -0
- contextsynapse/shield/profile.py +103 -0
- contextsynapse/shield/shield.py +108 -0
- contextsynapse/shield/trust_engine.py +69 -0
- contextsynapse/skills/__init__.py +14 -0
- contextsynapse/skills/engine.py +233 -0
- contextsynapse/skills/loader.py +59 -0
- contextsynapse/skills/models.py +139 -0
- contextsynapse/storage/__init__.py +7 -0
- contextsynapse/storage/cache.py +208 -0
- contextsynapse/storage/columnar_store_v2.py +178 -0
- contextsynapse/storage/csr_graph_storage.py +587 -0
- contextsynapse/storage/csr_store.py +388 -0
- contextsynapse/storage/document_store.py +491 -0
- contextsynapse/storage/hnsw_index.py +294 -0
- contextsynapse/storage/hybrid_store_v2.py +278 -0
- contextsynapse/storage/lmdb_graph_storage.py +366 -0
- contextsynapse/storage/migration.py +279 -0
- contextsynapse/storage/namespace_store.py +346 -0
- contextsynapse/storage/pure_graph_storage.py +185 -0
- contextsynapse/storage/redis_graph_adapter.py +186 -0
- contextsynapse/storage/redis_graph_storage.py +375 -0
- contextsynapse/storage/router/__init__.py +29 -0
- contextsynapse/storage/router/duckdb_store.py +398 -0
- contextsynapse/storage/router/factory.py +83 -0
- contextsynapse/storage/router/interface.py +125 -0
- contextsynapse/storage/router/postgres_store.py +339 -0
- contextsynapse/storage/router/resolver.py +192 -0
- contextsynapse/storage/router/router.py +300 -0
- contextsynapse/storage/single_file_storage.py +391 -0
- contextsynapse/storage/storage_manager.py +214 -0
- contextsynapse/storage/storage_query_optimizer.py +314 -0
- contextsynapse/storage/wal.py +359 -0
- contextsynapse/temporal/__init__.py +0 -0
- contextsynapse/temporal/storage.py +106 -0
- contextsynapse/tests/__init__.py +11 -0
- contextsynapse/tests/regression/__init__.py +1 -0
- contextsynapse/tests/regression/add_metadata.py +395 -0
- contextsynapse/tests/regression/add_skip_flags.py +85 -0
- contextsynapse/tests/regression/cases/test_basic_create_node.yaml +20 -0
- contextsynapse/tests/regression/cases/test_basic_select.yaml +21 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail_filtered.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_blocks_range.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_get_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_latest_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_length.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_merkle_root.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_create_edge.yaml +28 -0
- contextsynapse/tests/regression/cases/test_delete_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_delete_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_evaluation_mrr.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_ndcg.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_precision.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_recall.yaml +40 -0
- contextsynapse/tests/regression/cases/test_group_by_basic.yaml +56 -0
- contextsynapse/tests/regression/cases/test_match_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_match_node_with_order_limit.yaml +31 -0
- contextsynapse/tests/regression/cases/test_namespace_switch.yaml +18 -0
- contextsynapse/tests/regression/cases/test_pipeline_cascaded.yaml +61 -0
- contextsynapse/tests/regression/cases/test_pipeline_fixed_size_chunking.yaml +48 -0
- contextsynapse/tests/regression/cases/test_pipeline_gpt4_chunking.yaml +54 -0
- contextsynapse/tests/regression/cases/test_pipeline_hybrid_indexing.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_large_embedding.yaml +52 -0
- contextsynapse/tests/regression/cases/test_pipeline_paragraph_chunking.yaml +49 -0
- contextsynapse/tests/regression/cases/test_pipeline_recursive_chunking.yaml +47 -0
- contextsynapse/tests/regression/cases/test_pipeline_run_pipeline.yaml +56 -0
- contextsynapse/tests/regression/cases/test_pipeline_section_based_chunking.yaml +53 -0
- contextsynapse/tests/regression/cases/test_pipeline_semantic_chunking.yaml +51 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_entity_extraction.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_relationships.yaml +62 -0
- contextsynapse/tests/regression/cases/test_rag_evaluation.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_bfs_with_eval.yaml +48 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_dfs_with_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_multi_hop_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_path.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_basic.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_bfs.yaml +52 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_dfs.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_multi_hop.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_shortest_path.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_weighted.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_shortest_path_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_traversal.yaml +30 -0
- contextsynapse/tests/regression/cases/test_rag_graph_weighted_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_search.yaml +33 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_with_prompt_embedding.yaml +44 -0
- contextsynapse/tests/regression/cases/test_rag_keyword_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_multi_hop_graph.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_path_based.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_profile_based.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_vector_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_graph.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_hybrid.yaml +31 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_chunking.yaml +57 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_detection.yaml +33 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_function.yaml +30 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_with_model.yaml +31 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_fixed_size.yaml +44 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_recursive.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_section.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_semantic.yaml +44 -0
- contextsynapse/tests/regression/cases/test_time_travel_as_of.yaml +35 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_commit.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_timestamp.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_for_system_time_all.yaml +37 -0
- contextsynapse/tests/regression/cases/test_time_travel_versions_between.yaml +39 -0
- contextsynapse/tests/regression/cases/test_traverse_basic.yaml +31 -0
- contextsynapse/tests/regression/cases/test_traverse_max_depth.yaml +34 -0
- contextsynapse/tests/regression/cases/test_update_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/evaluation_metrics.py +227 -0
- contextsynapse/tests/regression/regression_runner.py +1081 -0
- contextsynapse/tests/regression/setup_regression_data.py +212 -0
- contextsynapse/tests/regression/test_basic_only.py +84 -0
- contextsynapse/tests/regression/test_namespace_creation.py +29 -0
- contextsynapse/tests/regression/test_single_fix.py +61 -0
- contextsynapse/tests/regression/test_single_query.py +20 -0
- contextsynapse/tests/regression/validate_test_syntax.py +179 -0
- contextsynapse/tests/test_advanced_features.py +347 -0
- contextsynapse/tests/test_aiql_summary.py +283 -0
- contextsynapse/tests/test_all_aiql_queries.py +948 -0
- contextsynapse/tests/test_all_create_queries.py +213 -0
- contextsynapse/tests/test_api_vs_native.py +280 -0
- contextsynapse/tests/test_chunking_dedup.py +199 -0
- contextsynapse/tests/test_connectors.py +171 -0
- contextsynapse/tests/test_edges_and_match.py +202 -0
- contextsynapse/tests/test_extraction_pipeline_integration.py +581 -0
- contextsynapse/tests/test_name_property.py +224 -0
- contextsynapse/tests/test_node_edge_metadata.py +250 -0
- contextsynapse/tests/test_playground_conn.py +114 -0
- contextsynapse/tests/test_query_cache.py +213 -0
- contextsynapse/tests/test_source_policy_auth.py +348 -0
- contextsynapse/tests/test_temporal_and_domain.py +224 -0
- contextsynapse/tests/test_time_travel_e2e.py +571 -0
- contextsynapse/tests/test_traversal_and_aggregation.py +275 -0
- contextsynapse/tools/__init__.py +36 -0
- contextsynapse/tools/a2a_tools.py +106 -0
- contextsynapse/tools/cognition_tools.py +118 -0
- contextsynapse/tools/context.py +905 -0
- contextsynapse/tools/formatting.py +92 -0
- contextsynapse/tools/gateway_tools.py +186 -0
- contextsynapse/tools/graph.py +1700 -0
- contextsynapse/tools/intelligence.py +235 -0
- contextsynapse/tools/memory.py +118 -0
- contextsynapse/tools/registry.py +373 -0
- contextsynapse/tools/shield_tools.py +159 -0
- contextsynapse/tools/tasks.py +341 -0
- contextsynapse/tools/workspace_tools.py +124 -0
- contextsynapse/utils/__init__.py +15 -0
- contextsynapse/utils/config_loader.py +198 -0
- contextsynapse/utils/env_loader.py +156 -0
- contextsynapse/utils/file_utils.py +288 -0
- contextsynapse/utils/logging_optimizer.py +119 -0
- contextsynapse/vector/__init__.py +30 -0
- contextsynapse/vector/chroma_vector_store.py +152 -0
- contextsynapse/vector/custom_vector_store.py +537 -0
- contextsynapse/vector/faiss_vector_store.py +296 -0
- contextsynapse/vector/qdrant_vector_store.py +163 -0
- contextsynapse/vector/vector_db.py +953 -0
- contextsynapse/vector/vector_db_manager.py +197 -0
- contextsynapse/workspace/__init__.py +27 -0
- contextsynapse/workspace/base.py +94 -0
- contextsynapse/workspace/git.py +228 -0
- contextsynapse/workspace/github.py +78 -0
- contextsynapse/workspace/local.py +73 -0
- contextsynapse/workspace/tools.py +222 -0
- contextsynapse-1.0.0.dist-info/METADATA +847 -0
- contextsynapse-1.0.0.dist-info/RECORD +776 -0
- contextsynapse-1.0.0.dist-info/WHEEL +5 -0
- contextsynapse-1.0.0.dist-info/entry_points.txt +4 -0
- contextsynapse-1.0.0.dist-info/licenses/LICENSE +200 -0
- contextsynapse-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Sessions
|
|
3
|
+
================
|
|
4
|
+
Named, persistent workspaces where multiple agents can read/write shared
|
|
5
|
+
context backed by graph + vector + document stores.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import secrets
|
|
13
|
+
import sqlite3
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Prefix to avoid collisions with user-created namespaces
|
|
22
|
+
SESSION_NS_PREFIX = "ctx_"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ContextSession:
|
|
27
|
+
"""A shared context workspace."""
|
|
28
|
+
session_id: str
|
|
29
|
+
name: str
|
|
30
|
+
created_at: str
|
|
31
|
+
updated_at: str
|
|
32
|
+
owner_agent_id: Optional[str] = None
|
|
33
|
+
graph_namespace: str = ""
|
|
34
|
+
runtime_namespace: str = "" # separate graph for agent work (tasks, findings, actions)
|
|
35
|
+
vector_collection: str = ""
|
|
36
|
+
document_collection: str = ""
|
|
37
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
status: str = "active" # active, archived, deleted
|
|
39
|
+
region: str = "local" # region where session was created
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def security_config(self) -> Dict[str, Any]:
|
|
43
|
+
"""Get the runtime security configuration for this session.
|
|
44
|
+
|
|
45
|
+
Stored in config['security']. Controls how agents access data.
|
|
46
|
+
"""
|
|
47
|
+
defaults = {
|
|
48
|
+
"audit_trail": True,
|
|
49
|
+
"redaction_mode": "mask",
|
|
50
|
+
"agent_clearances": {}, # agent_id → {level, pii_access}
|
|
51
|
+
}
|
|
52
|
+
return {**defaults, **self.config.get("security", {})}
|
|
53
|
+
|
|
54
|
+
@security_config.setter
|
|
55
|
+
def security_config(self, value: Dict[str, Any]):
|
|
56
|
+
self.config["security"] = value
|
|
57
|
+
|
|
58
|
+
def get_agent_clearance(self, agent_id: str) -> Dict[str, Any]:
|
|
59
|
+
"""Get clearance for a specific agent in this session."""
|
|
60
|
+
clearances = self.security_config.get("agent_clearances", {})
|
|
61
|
+
return clearances.get(agent_id, {"level": "public", "pii_access": []})
|
|
62
|
+
|
|
63
|
+
def set_agent_clearance(self, agent_id: str, level: str = "public",
|
|
64
|
+
pii_access: Optional[List[str]] = None):
|
|
65
|
+
"""Set clearance for an agent in this session."""
|
|
66
|
+
sec = self.config.get("security", {})
|
|
67
|
+
clearances = sec.get("agent_clearances", {})
|
|
68
|
+
clearances[agent_id] = {"level": level, "pii_access": pii_access or []}
|
|
69
|
+
sec["agent_clearances"] = clearances
|
|
70
|
+
self.config["security"] = sec
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
return asdict(self)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ContextSessionManager:
|
|
77
|
+
"""
|
|
78
|
+
Manages context sessions and their backing stores.
|
|
79
|
+
|
|
80
|
+
Each session maps 1-to-1 to:
|
|
81
|
+
- A graph namespace (via GraphRegistry)
|
|
82
|
+
- A vector collection (name only — actual vector ops are done by caller)
|
|
83
|
+
- A document collection (name only)
|
|
84
|
+
|
|
85
|
+
Access control is stored in the same SQLite DB as the agent registry
|
|
86
|
+
(``contextcore_data/context.db``).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
_SCHEMA = """
|
|
90
|
+
CREATE TABLE IF NOT EXISTS context_sessions (
|
|
91
|
+
session_id TEXT PRIMARY KEY,
|
|
92
|
+
name TEXT NOT NULL UNIQUE,
|
|
93
|
+
created_at TEXT NOT NULL,
|
|
94
|
+
updated_at TEXT NOT NULL,
|
|
95
|
+
owner_agent_id TEXT,
|
|
96
|
+
graph_namespace TEXT NOT NULL,
|
|
97
|
+
runtime_namespace TEXT DEFAULT '',
|
|
98
|
+
vector_collection TEXT NOT NULL,
|
|
99
|
+
document_collection TEXT NOT NULL,
|
|
100
|
+
config TEXT DEFAULT '{}',
|
|
101
|
+
status TEXT DEFAULT 'active'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE TABLE IF NOT EXISTS session_access (
|
|
105
|
+
session_id TEXT NOT NULL,
|
|
106
|
+
agent_id TEXT NOT NULL,
|
|
107
|
+
access_level TEXT NOT NULL DEFAULT 'read',
|
|
108
|
+
allowed_tags TEXT DEFAULT '[]',
|
|
109
|
+
granted_at TEXT NOT NULL,
|
|
110
|
+
PRIMARY KEY (session_id, agent_id)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS session_contexts (
|
|
114
|
+
session_id TEXT NOT NULL,
|
|
115
|
+
context_id TEXT NOT NULL,
|
|
116
|
+
attached_at TEXT NOT NULL,
|
|
117
|
+
role TEXT DEFAULT 'input',
|
|
118
|
+
last_synced_at TEXT,
|
|
119
|
+
PRIMARY KEY (session_id, context_id)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON context_sessions(status);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_owner ON context_sessions(owner_agent_id);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_access_agent ON session_access(agent_id);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_session_contexts_ctx ON session_contexts(context_id);
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
_MIGRATIONS = [
|
|
129
|
+
# v1 → v2: add allowed_tags column to session_access
|
|
130
|
+
"ALTER TABLE session_access ADD COLUMN allowed_tags TEXT DEFAULT '[]'",
|
|
131
|
+
# v2 → v3: add region column for multi-region support
|
|
132
|
+
"ALTER TABLE context_sessions ADD COLUMN region TEXT DEFAULT 'local'",
|
|
133
|
+
# v3 → v4: runtime graph for agent work (tasks, findings, actions)
|
|
134
|
+
"ALTER TABLE context_sessions ADD COLUMN runtime_namespace TEXT DEFAULT ''",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
_POST_SCHEMA = """
|
|
138
|
+
CREATE TABLE IF NOT EXISTS session_join_requests (
|
|
139
|
+
request_id TEXT PRIMARY KEY,
|
|
140
|
+
session_id TEXT NOT NULL,
|
|
141
|
+
agent_id TEXT NOT NULL,
|
|
142
|
+
requested_level TEXT NOT NULL DEFAULT 'read',
|
|
143
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
144
|
+
reason TEXT,
|
|
145
|
+
reviewed_by TEXT,
|
|
146
|
+
reviewed_at TEXT,
|
|
147
|
+
created_at TEXT NOT NULL,
|
|
148
|
+
expires_at TEXT,
|
|
149
|
+
UNIQUE(session_id, agent_id, status)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_join_req_session ON session_join_requests(session_id);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_join_req_agent ON session_join_requests(agent_id);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_join_req_status ON session_join_requests(status);
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, db_path: str = "contextcore_data/context.db", graph_registry=None):
|
|
158
|
+
self._db_path = Path(db_path)
|
|
159
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
|
|
161
|
+
self._conn.row_factory = sqlite3.Row
|
|
162
|
+
self._conn.executescript(self._SCHEMA)
|
|
163
|
+
self._conn.executescript(self._POST_SCHEMA)
|
|
164
|
+
self._run_migrations()
|
|
165
|
+
self._graph_registry = graph_registry
|
|
166
|
+
|
|
167
|
+
def _run_migrations(self):
|
|
168
|
+
"""Apply schema migrations idempotently."""
|
|
169
|
+
for sql in self._MIGRATIONS:
|
|
170
|
+
try:
|
|
171
|
+
self._conn.execute(sql)
|
|
172
|
+
self._conn.commit()
|
|
173
|
+
except sqlite3.OperationalError:
|
|
174
|
+
pass # column/table already exists
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Session CRUD
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def create_session(
|
|
181
|
+
self,
|
|
182
|
+
name: str,
|
|
183
|
+
owner_agent_id: Optional[str] = None,
|
|
184
|
+
config: Optional[Dict[str, Any]] = None,
|
|
185
|
+
) -> ContextSession:
|
|
186
|
+
"""Create a new context session with backing stores."""
|
|
187
|
+
# Check for duplicate name (among active sessions)
|
|
188
|
+
existing = self._conn.execute(
|
|
189
|
+
"SELECT session_id FROM context_sessions WHERE name = ? AND status = 'active'",
|
|
190
|
+
(name,),
|
|
191
|
+
).fetchone()
|
|
192
|
+
if existing:
|
|
193
|
+
raise ValueError(f"A boundary with name '{name}' already exists")
|
|
194
|
+
|
|
195
|
+
session_id = secrets.token_hex(12)
|
|
196
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
197
|
+
# Sanitize name for use as filesystem path (Windows forbids : ? * etc.)
|
|
198
|
+
import re
|
|
199
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
|
|
200
|
+
# Include session_id prefix for unique namespace
|
|
201
|
+
ns = f"{SESSION_NS_PREFIX}{safe_name}_{session_id[:8]}"
|
|
202
|
+
rt_ns = f"{ns}_rt" # runtime graph for agent work (tasks, findings, actions)
|
|
203
|
+
|
|
204
|
+
from contextsynapse.core.write_context import REGION
|
|
205
|
+
session = ContextSession(
|
|
206
|
+
session_id=session_id,
|
|
207
|
+
name=name,
|
|
208
|
+
created_at=now,
|
|
209
|
+
updated_at=now,
|
|
210
|
+
owner_agent_id=owner_agent_id,
|
|
211
|
+
graph_namespace=ns,
|
|
212
|
+
runtime_namespace=rt_ns,
|
|
213
|
+
vector_collection=ns,
|
|
214
|
+
document_collection=ns,
|
|
215
|
+
config=config or {},
|
|
216
|
+
status="active",
|
|
217
|
+
region=REGION,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Graph namespace is reserved but NOT created until a context is attached
|
|
221
|
+
# or data is ingested. This keeps session creation lightweight.
|
|
222
|
+
|
|
223
|
+
# Persist session
|
|
224
|
+
self._conn.execute(
|
|
225
|
+
"INSERT INTO context_sessions "
|
|
226
|
+
"(session_id, name, created_at, updated_at, owner_agent_id, "
|
|
227
|
+
"graph_namespace, runtime_namespace, vector_collection, document_collection, config, status) "
|
|
228
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
229
|
+
(
|
|
230
|
+
session.session_id,
|
|
231
|
+
session.name,
|
|
232
|
+
session.created_at,
|
|
233
|
+
session.updated_at,
|
|
234
|
+
session.owner_agent_id,
|
|
235
|
+
session.graph_namespace,
|
|
236
|
+
session.runtime_namespace,
|
|
237
|
+
session.vector_collection,
|
|
238
|
+
session.document_collection,
|
|
239
|
+
json.dumps(session.config),
|
|
240
|
+
session.status,
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Owner gets admin access automatically
|
|
245
|
+
if owner_agent_id:
|
|
246
|
+
self._conn.execute(
|
|
247
|
+
"INSERT OR REPLACE INTO session_access (session_id, agent_id, access_level, granted_at) "
|
|
248
|
+
"VALUES (?, ?, 'admin', ?)",
|
|
249
|
+
(session_id, owner_agent_id, now),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self._conn.commit()
|
|
253
|
+
|
|
254
|
+
# Graph-first: add Session root node in BOTH the session's own graph AND default graph
|
|
255
|
+
if self._graph_registry:
|
|
256
|
+
from ..core.graph_structures import GraphNode
|
|
257
|
+
session_props = {
|
|
258
|
+
"name": session.name,
|
|
259
|
+
"graph_namespace": session.graph_namespace,
|
|
260
|
+
"owner_agent_id": session.owner_agent_id or "",
|
|
261
|
+
"status": session.status,
|
|
262
|
+
"created_at": session.created_at,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Root node in session's own graph
|
|
266
|
+
try:
|
|
267
|
+
session_graph = self._graph_registry.get_graph(session.graph_namespace)
|
|
268
|
+
if session_graph:
|
|
269
|
+
session_graph.add_node(GraphNode(
|
|
270
|
+
id=session.session_id, label="Session", properties=session_props,
|
|
271
|
+
), write_through=True)
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Reference node in default graph
|
|
276
|
+
try:
|
|
277
|
+
default_graph = self._graph_registry.get_graph("default") or self._graph_registry.create_graph("default")
|
|
278
|
+
default_graph.add_node(GraphNode(
|
|
279
|
+
id=session.session_id, label="Session", properties=session_props,
|
|
280
|
+
), write_through=True)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
return session
|
|
285
|
+
|
|
286
|
+
def rotate_session_id(self, old_session_id: str) -> Optional[str]:
|
|
287
|
+
"""Rotate session ID — generates new ID, old one stops working.
|
|
288
|
+
|
|
289
|
+
Updates all references: session_access, session_contexts, join_requests.
|
|
290
|
+
Returns the new session_id, or None if session not found.
|
|
291
|
+
"""
|
|
292
|
+
session = self.get_session(old_session_id)
|
|
293
|
+
if not session:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
new_id = secrets.token_hex(12)
|
|
297
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
298
|
+
|
|
299
|
+
# Update main session record
|
|
300
|
+
self._conn.execute(
|
|
301
|
+
"UPDATE context_sessions SET session_id = ?, updated_at = ? WHERE session_id = ?",
|
|
302
|
+
(new_id, now, old_session_id),
|
|
303
|
+
)
|
|
304
|
+
# Update access table
|
|
305
|
+
self._conn.execute(
|
|
306
|
+
"UPDATE session_access SET session_id = ? WHERE session_id = ?",
|
|
307
|
+
(new_id, old_session_id),
|
|
308
|
+
)
|
|
309
|
+
# Update session_contexts
|
|
310
|
+
self._conn.execute(
|
|
311
|
+
"UPDATE session_contexts SET session_id = ? WHERE session_id = ?",
|
|
312
|
+
(new_id, old_session_id),
|
|
313
|
+
)
|
|
314
|
+
# Update join requests
|
|
315
|
+
try:
|
|
316
|
+
self._conn.execute(
|
|
317
|
+
"UPDATE session_join_requests SET session_id = ? WHERE session_id = ?",
|
|
318
|
+
(new_id, old_session_id),
|
|
319
|
+
)
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
self._conn.commit()
|
|
323
|
+
|
|
324
|
+
# Update graph node if exists
|
|
325
|
+
if self._graph_registry:
|
|
326
|
+
try:
|
|
327
|
+
graph = self._graph_registry.get_graph(session.graph_namespace)
|
|
328
|
+
if graph:
|
|
329
|
+
old_node = graph.get_node(old_session_id)
|
|
330
|
+
if old_node:
|
|
331
|
+
from ..core.graph_structures import GraphNode
|
|
332
|
+
graph.add_node(GraphNode(
|
|
333
|
+
id=new_id, label="Session",
|
|
334
|
+
properties={**old_node.properties, "rotated_from": old_session_id, "rotated_at": now},
|
|
335
|
+
), write_through=True)
|
|
336
|
+
try:
|
|
337
|
+
graph.remove_node(old_session_id)
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
logger.info("Rotated session ID: %s → %s", old_session_id[:8], new_id[:8])
|
|
344
|
+
return new_id
|
|
345
|
+
|
|
346
|
+
def get_session(self, session_id: str) -> Optional[ContextSession]:
|
|
347
|
+
row = self._conn.execute(
|
|
348
|
+
"SELECT * FROM context_sessions WHERE session_id = ? AND status != 'deleted'",
|
|
349
|
+
(session_id,),
|
|
350
|
+
).fetchone()
|
|
351
|
+
return self._row_to_session(row) if row else None
|
|
352
|
+
|
|
353
|
+
def get_session_by_name(self, name: str) -> Optional[ContextSession]:
|
|
354
|
+
row = self._conn.execute(
|
|
355
|
+
"SELECT * FROM context_sessions WHERE name = ? AND status != 'deleted'",
|
|
356
|
+
(name,),
|
|
357
|
+
).fetchone()
|
|
358
|
+
return self._row_to_session(row) if row else None
|
|
359
|
+
|
|
360
|
+
def list_sessions(self, agent_id: Optional[str] = None, user_id: Optional[str] = None) -> List[ContextSession]:
|
|
361
|
+
"""List sessions, optionally filtered to those a user/agent has access to.
|
|
362
|
+
|
|
363
|
+
When *user_id* is provided the result includes sessions where the user
|
|
364
|
+
is the owner **or** has been granted access via ``session_access``.
|
|
365
|
+
"""
|
|
366
|
+
if user_id:
|
|
367
|
+
rows = self._conn.execute(
|
|
368
|
+
"SELECT DISTINCT cs.* FROM context_sessions cs "
|
|
369
|
+
"LEFT JOIN session_access sa ON cs.session_id = sa.session_id "
|
|
370
|
+
"WHERE cs.status != 'deleted' "
|
|
371
|
+
"AND (cs.owner_agent_id = ? OR sa.agent_id = ?) "
|
|
372
|
+
"ORDER BY cs.updated_at DESC",
|
|
373
|
+
(user_id, user_id),
|
|
374
|
+
).fetchall()
|
|
375
|
+
elif agent_id:
|
|
376
|
+
rows = self._conn.execute(
|
|
377
|
+
"SELECT cs.* FROM context_sessions cs "
|
|
378
|
+
"JOIN session_access sa ON cs.session_id = sa.session_id "
|
|
379
|
+
"WHERE sa.agent_id = ? AND cs.status != 'deleted' "
|
|
380
|
+
"ORDER BY cs.updated_at DESC",
|
|
381
|
+
(agent_id,),
|
|
382
|
+
).fetchall()
|
|
383
|
+
else:
|
|
384
|
+
rows = self._conn.execute(
|
|
385
|
+
"SELECT * FROM context_sessions WHERE status != 'deleted' ORDER BY updated_at DESC"
|
|
386
|
+
).fetchall()
|
|
387
|
+
return [self._row_to_session(r) for r in rows]
|
|
388
|
+
|
|
389
|
+
def update_session(self, session_id: str, config: Optional[Dict[str, Any]] = None) -> bool:
|
|
390
|
+
"""Update session fields (currently config)."""
|
|
391
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
392
|
+
if config is not None:
|
|
393
|
+
self._conn.execute(
|
|
394
|
+
"UPDATE context_sessions SET config = ?, updated_at = ? WHERE session_id = ?",
|
|
395
|
+
(json.dumps(config), now, session_id),
|
|
396
|
+
)
|
|
397
|
+
self._conn.commit()
|
|
398
|
+
return True
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
def delete_session(self, session_id: str) -> bool:
|
|
402
|
+
"""Soft-delete a session."""
|
|
403
|
+
# Final promotion sweep before deleting session
|
|
404
|
+
try:
|
|
405
|
+
session = self.get_session(session_id)
|
|
406
|
+
if session:
|
|
407
|
+
from .promotion import PromotionScorer, PromotionQueue, promote_node, PROMOTION_DEFAULTS
|
|
408
|
+
rt_ns = getattr(session, 'runtime_namespace', '') or f"{session.graph_namespace}_rt"
|
|
409
|
+
at_ns = session.graph_namespace
|
|
410
|
+
# Get graph registry from global
|
|
411
|
+
from ..core.registry import GraphRegistry
|
|
412
|
+
import os
|
|
413
|
+
reg = GraphRegistry(storage_dir=os.environ.get("CONTEXTSYNAPSE_STORAGE_DIR") or os.environ.get("AICONTEXTDB_STORAGE_DIR", "contextcore_data"))
|
|
414
|
+
rt_graph = reg.get_graph(rt_ns, load_if_missing=False)
|
|
415
|
+
if rt_graph:
|
|
416
|
+
scorer = PromotionScorer(PROMOTION_DEFAULTS)
|
|
417
|
+
for label in ("Finding", "Insight", "Observation", "Note"):
|
|
418
|
+
for n in rt_graph.get_all_nodes(label=label):
|
|
419
|
+
props = n.properties or {}
|
|
420
|
+
if props.get("_promotion_scored_at"):
|
|
421
|
+
continue
|
|
422
|
+
score = scorer.score(n, rt_graph)
|
|
423
|
+
if score.score >= 0.3:
|
|
424
|
+
try:
|
|
425
|
+
from ..adapters._base import AIContextDBConnection
|
|
426
|
+
at_conn = AIContextDBConnection(namespace=at_ns)
|
|
427
|
+
rt_conn = AIContextDBConnection(namespace=rt_ns)
|
|
428
|
+
promote_node(n.id, rt_conn, at_conn, promoted_by=props.get("_agent_name", "system"))
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
434
|
+
cur = self._conn.execute(
|
|
435
|
+
"UPDATE context_sessions SET status = 'deleted', updated_at = ? WHERE session_id = ?",
|
|
436
|
+
(now, session_id),
|
|
437
|
+
)
|
|
438
|
+
self._conn.commit()
|
|
439
|
+
return cur.rowcount > 0
|
|
440
|
+
|
|
441
|
+
def touch_session(self, session_id: str):
|
|
442
|
+
"""Update the updated_at timestamp."""
|
|
443
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
444
|
+
self._conn.execute(
|
|
445
|
+
"UPDATE context_sessions SET updated_at = ? WHERE session_id = ?",
|
|
446
|
+
(now, session_id),
|
|
447
|
+
)
|
|
448
|
+
self._conn.commit()
|
|
449
|
+
|
|
450
|
+
# ------------------------------------------------------------------
|
|
451
|
+
# Access control
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def grant_access(
|
|
455
|
+
self,
|
|
456
|
+
session_id: str,
|
|
457
|
+
agent_id: str,
|
|
458
|
+
level: str = "",
|
|
459
|
+
allowed_tags: Optional[List[str]] = None,
|
|
460
|
+
agent_registry=None,
|
|
461
|
+
) -> bool:
|
|
462
|
+
"""Grant access with optional tag-based scope.
|
|
463
|
+
|
|
464
|
+
If level is empty, it's auto-derived from the agent's role/capabilities:
|
|
465
|
+
- admin role or 'admin' capability → 'admin'
|
|
466
|
+
- 'write' capability → 'write'
|
|
467
|
+
- fallback → 'read'
|
|
468
|
+
"""
|
|
469
|
+
# Auto-derive level from agent registration
|
|
470
|
+
if not level and agent_registry:
|
|
471
|
+
try:
|
|
472
|
+
agent = agent_registry.get(agent_id)
|
|
473
|
+
if agent:
|
|
474
|
+
caps = agent.capabilities or []
|
|
475
|
+
if agent.role == "admin" or "admin" in caps:
|
|
476
|
+
level = "admin"
|
|
477
|
+
elif "write" in caps:
|
|
478
|
+
level = "write"
|
|
479
|
+
else:
|
|
480
|
+
level = "read"
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
if not level:
|
|
484
|
+
level = "read"
|
|
485
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
486
|
+
self._conn.execute(
|
|
487
|
+
"INSERT OR REPLACE INTO session_access "
|
|
488
|
+
"(session_id, agent_id, access_level, allowed_tags, granted_at) "
|
|
489
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
490
|
+
(session_id, agent_id, level, json.dumps(allowed_tags or []), now),
|
|
491
|
+
)
|
|
492
|
+
self._conn.commit()
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
def revoke_access(self, session_id: str, agent_id: str) -> bool:
|
|
496
|
+
cur = self._conn.execute(
|
|
497
|
+
"DELETE FROM session_access WHERE session_id = ? AND agent_id = ?",
|
|
498
|
+
(session_id, agent_id),
|
|
499
|
+
)
|
|
500
|
+
self._conn.commit()
|
|
501
|
+
return cur.rowcount > 0
|
|
502
|
+
|
|
503
|
+
def check_access(self, session_id: str, agent_id: str, required: str = "read") -> bool:
|
|
504
|
+
"""Check if agent has the required access level.
|
|
505
|
+
|
|
506
|
+
Levels: read < write < admin.
|
|
507
|
+
"""
|
|
508
|
+
row = self._conn.execute(
|
|
509
|
+
"SELECT access_level FROM session_access WHERE session_id = ? AND agent_id = ?",
|
|
510
|
+
(session_id, agent_id),
|
|
511
|
+
).fetchone()
|
|
512
|
+
if not row:
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
levels = {"read": 0, "write": 1, "admin": 2}
|
|
516
|
+
return levels.get(row["access_level"], -1) >= levels.get(required, 99)
|
|
517
|
+
|
|
518
|
+
def get_agent_access(self, session_id: str, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
519
|
+
"""Return the full access record for an agent on a session.
|
|
520
|
+
|
|
521
|
+
Returns ``{"access_level": str, "allowed_tags": List[str]}`` or None.
|
|
522
|
+
"""
|
|
523
|
+
row = self._conn.execute(
|
|
524
|
+
"SELECT access_level, allowed_tags FROM session_access "
|
|
525
|
+
"WHERE session_id = ? AND agent_id = ?",
|
|
526
|
+
(session_id, agent_id),
|
|
527
|
+
).fetchone()
|
|
528
|
+
if not row:
|
|
529
|
+
return None
|
|
530
|
+
return {
|
|
531
|
+
"access_level": row["access_level"],
|
|
532
|
+
"allowed_tags": json.loads(row["allowed_tags"]) if row["allowed_tags"] else [],
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def get_access_list(self, session_id: str) -> List[Dict[str, Any]]:
|
|
536
|
+
rows = self._conn.execute(
|
|
537
|
+
"SELECT agent_id, access_level, allowed_tags, granted_at FROM session_access WHERE session_id = ?",
|
|
538
|
+
(session_id,),
|
|
539
|
+
).fetchall()
|
|
540
|
+
result = []
|
|
541
|
+
for r in rows:
|
|
542
|
+
entry = dict(r)
|
|
543
|
+
entry["allowed_tags"] = json.loads(entry.get("allowed_tags") or "[]")
|
|
544
|
+
result.append(entry)
|
|
545
|
+
return result
|
|
546
|
+
|
|
547
|
+
# ------------------------------------------------------------------
|
|
548
|
+
# Join requests (approval gate)
|
|
549
|
+
# ------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
def request_join(
|
|
552
|
+
self,
|
|
553
|
+
session_id: str,
|
|
554
|
+
agent_id: str,
|
|
555
|
+
level: str = "read",
|
|
556
|
+
reason: Optional[str] = None,
|
|
557
|
+
expires_in_hours: int = 72,
|
|
558
|
+
) -> Dict[str, Any]:
|
|
559
|
+
"""Agent requests to join a session. Returns the request record.
|
|
560
|
+
|
|
561
|
+
The request stays pending until approved by the session owner or
|
|
562
|
+
an agent with admin access. If the requesting agent already has
|
|
563
|
+
access, this is a no-op that returns the existing access info.
|
|
564
|
+
"""
|
|
565
|
+
# Already has access?
|
|
566
|
+
if self.check_access(session_id, agent_id, "read"):
|
|
567
|
+
return {"status": "already_granted", "agent_id": agent_id, "session_id": session_id}
|
|
568
|
+
|
|
569
|
+
# Already has a pending request?
|
|
570
|
+
existing = self._conn.execute(
|
|
571
|
+
"SELECT * FROM session_join_requests "
|
|
572
|
+
"WHERE session_id = ? AND agent_id = ? AND status = 'pending'",
|
|
573
|
+
(session_id, agent_id),
|
|
574
|
+
).fetchone()
|
|
575
|
+
if existing:
|
|
576
|
+
return dict(existing)
|
|
577
|
+
|
|
578
|
+
request_id = secrets.token_hex(12)
|
|
579
|
+
now = datetime.now(timezone.utc)
|
|
580
|
+
expires_at = (
|
|
581
|
+
datetime(now.year, now.month, now.day, now.hour, now.minute, now.second, tzinfo=timezone.utc)
|
|
582
|
+
)
|
|
583
|
+
from datetime import timedelta
|
|
584
|
+
expires_at = (now + timedelta(hours=expires_in_hours)).isoformat()
|
|
585
|
+
|
|
586
|
+
self._conn.execute(
|
|
587
|
+
"INSERT INTO session_join_requests "
|
|
588
|
+
"(request_id, session_id, agent_id, requested_level, status, reason, created_at, expires_at) "
|
|
589
|
+
"VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)",
|
|
590
|
+
(request_id, session_id, agent_id, level, reason, now.isoformat(), expires_at),
|
|
591
|
+
)
|
|
592
|
+
self._conn.commit()
|
|
593
|
+
return {
|
|
594
|
+
"request_id": request_id,
|
|
595
|
+
"session_id": session_id,
|
|
596
|
+
"agent_id": agent_id,
|
|
597
|
+
"requested_level": level,
|
|
598
|
+
"status": "pending",
|
|
599
|
+
"reason": reason,
|
|
600
|
+
"created_at": now.isoformat(),
|
|
601
|
+
"expires_at": expires_at,
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
def approve_join(
|
|
605
|
+
self,
|
|
606
|
+
request_id: str,
|
|
607
|
+
reviewed_by: str,
|
|
608
|
+
level: Optional[str] = None,
|
|
609
|
+
allowed_tags: Optional[List[str]] = None,
|
|
610
|
+
) -> bool:
|
|
611
|
+
"""Approve a pending join request. Grants access automatically."""
|
|
612
|
+
row = self._conn.execute(
|
|
613
|
+
"SELECT * FROM session_join_requests WHERE request_id = ? AND status = 'pending'",
|
|
614
|
+
(request_id,),
|
|
615
|
+
).fetchone()
|
|
616
|
+
if not row:
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
620
|
+
self._conn.execute(
|
|
621
|
+
"UPDATE session_join_requests SET status = 'approved', reviewed_by = ?, reviewed_at = ? "
|
|
622
|
+
"WHERE request_id = ?",
|
|
623
|
+
(reviewed_by, now, request_id),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Grant actual access
|
|
627
|
+
access_level = level or row["requested_level"]
|
|
628
|
+
self.grant_access(row["session_id"], row["agent_id"], access_level, allowed_tags)
|
|
629
|
+
return True
|
|
630
|
+
|
|
631
|
+
def deny_join(self, request_id: str, reviewed_by: str) -> bool:
|
|
632
|
+
"""Deny a pending join request."""
|
|
633
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
634
|
+
cur = self._conn.execute(
|
|
635
|
+
"UPDATE session_join_requests SET status = 'denied', reviewed_by = ?, reviewed_at = ? "
|
|
636
|
+
"WHERE request_id = ? AND status = 'pending'",
|
|
637
|
+
(reviewed_by, now, request_id),
|
|
638
|
+
)
|
|
639
|
+
self._conn.commit()
|
|
640
|
+
return cur.rowcount > 0
|
|
641
|
+
|
|
642
|
+
def get_pending_requests(self, session_id: str) -> List[Dict[str, Any]]:
|
|
643
|
+
"""Get all pending join requests for a session."""
|
|
644
|
+
rows = self._conn.execute(
|
|
645
|
+
"SELECT * FROM session_join_requests WHERE session_id = ? AND status = 'pending' "
|
|
646
|
+
"ORDER BY created_at ASC",
|
|
647
|
+
(session_id,),
|
|
648
|
+
).fetchall()
|
|
649
|
+
return [dict(r) for r in rows]
|
|
650
|
+
|
|
651
|
+
def get_agent_requests(self, agent_id: str) -> List[Dict[str, Any]]:
|
|
652
|
+
"""Get all join requests by an agent."""
|
|
653
|
+
rows = self._conn.execute(
|
|
654
|
+
"SELECT * FROM session_join_requests WHERE agent_id = ? ORDER BY created_at DESC",
|
|
655
|
+
(agent_id,),
|
|
656
|
+
).fetchall()
|
|
657
|
+
return [dict(r) for r in rows]
|
|
658
|
+
|
|
659
|
+
def cleanup_expired_requests(self) -> int:
|
|
660
|
+
"""Remove expired pending requests. Returns count removed."""
|
|
661
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
662
|
+
cur = self._conn.execute(
|
|
663
|
+
"UPDATE session_join_requests SET status = 'expired' "
|
|
664
|
+
"WHERE status = 'pending' AND expires_at < ?",
|
|
665
|
+
(now,),
|
|
666
|
+
)
|
|
667
|
+
self._conn.commit()
|
|
668
|
+
return cur.rowcount
|
|
669
|
+
|
|
670
|
+
# ------------------------------------------------------------------
|
|
671
|
+
# Helpers
|
|
672
|
+
# ------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
def _row_to_session(self, row: sqlite3.Row) -> ContextSession:
|
|
675
|
+
# region column may not exist in old DBs before migration
|
|
676
|
+
try:
|
|
677
|
+
region = row["region"]
|
|
678
|
+
except (IndexError, KeyError):
|
|
679
|
+
region = "local"
|
|
680
|
+
# runtime_namespace added in v4 migration
|
|
681
|
+
try:
|
|
682
|
+
runtime_ns = row["runtime_namespace"] or ""
|
|
683
|
+
except (IndexError, KeyError):
|
|
684
|
+
runtime_ns = ""
|
|
685
|
+
# Backfill: derive runtime_namespace from graph_namespace for old sessions
|
|
686
|
+
if not runtime_ns:
|
|
687
|
+
runtime_ns = f"{row['graph_namespace']}_rt"
|
|
688
|
+
return ContextSession(
|
|
689
|
+
session_id=row["session_id"],
|
|
690
|
+
name=row["name"],
|
|
691
|
+
created_at=row["created_at"],
|
|
692
|
+
updated_at=row["updated_at"],
|
|
693
|
+
owner_agent_id=row["owner_agent_id"],
|
|
694
|
+
graph_namespace=row["graph_namespace"],
|
|
695
|
+
runtime_namespace=runtime_ns,
|
|
696
|
+
vector_collection=row["vector_collection"],
|
|
697
|
+
document_collection=row["document_collection"],
|
|
698
|
+
config=json.loads(row["config"]) if row["config"] else {},
|
|
699
|
+
status=row["status"],
|
|
700
|
+
region=region or "local",
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# ------------------------------------------------------------------
|
|
704
|
+
# Session ↔ Context linkage
|
|
705
|
+
# ------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
def attach_context(self, session_id: str, context_id: str, role: str = "input") -> bool:
|
|
708
|
+
"""Attach a context to a session — merges context graph into runtime graph."""
|
|
709
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
710
|
+
try:
|
|
711
|
+
self._conn.execute(
|
|
712
|
+
"INSERT OR REPLACE INTO session_contexts (session_id, context_id, attached_at, role) "
|
|
713
|
+
"VALUES (?, ?, ?, ?)",
|
|
714
|
+
(session_id, context_id, now, role),
|
|
715
|
+
)
|
|
716
|
+
self._conn.commit()
|
|
717
|
+
|
|
718
|
+
if self._graph_registry:
|
|
719
|
+
try:
|
|
720
|
+
session = self.get_session(session_id)
|
|
721
|
+
if not session:
|
|
722
|
+
return True
|
|
723
|
+
|
|
724
|
+
from ..core.graph_structures import GraphNode, GraphEdge
|
|
725
|
+
import uuid as _uuid
|
|
726
|
+
|
|
727
|
+
runtime_graph = self._graph_registry.get_graph(session.graph_namespace)
|
|
728
|
+
if not runtime_graph:
|
|
729
|
+
runtime_graph = self._graph_registry.create_graph(session.graph_namespace)
|
|
730
|
+
if not runtime_graph:
|
|
731
|
+
return True
|
|
732
|
+
|
|
733
|
+
# Ensure Session root node
|
|
734
|
+
session_root_id = session_id
|
|
735
|
+
existing_sessions = [n for n in runtime_graph.get_all_nodes()
|
|
736
|
+
if getattr(n, "label", "") == "Session"]
|
|
737
|
+
if existing_sessions:
|
|
738
|
+
session_root_id = existing_sessions[0].id
|
|
739
|
+
elif not runtime_graph.get_node(session_id):
|
|
740
|
+
runtime_graph.add_node(GraphNode(
|
|
741
|
+
id=session_id, label="Session",
|
|
742
|
+
properties={"name": session.name, "status": "active",
|
|
743
|
+
"created_at": session.created_at},
|
|
744
|
+
), write_through=True)
|
|
745
|
+
|
|
746
|
+
# Get context details
|
|
747
|
+
ctx_name = context_id[:12]
|
|
748
|
+
ctx_ns = ""
|
|
749
|
+
ctx_type = ""
|
|
750
|
+
ctx_desc = ""
|
|
751
|
+
ctx_items = 0
|
|
752
|
+
ctx_tokens = 0
|
|
753
|
+
ctx_tags: list = []
|
|
754
|
+
ctx_sensitivity = ""
|
|
755
|
+
try:
|
|
756
|
+
from .context_manager import ContextManager
|
|
757
|
+
cm = ContextManager(graph_registry=self._graph_registry)
|
|
758
|
+
ctx_obj = cm.get_context(context_id)
|
|
759
|
+
if ctx_obj:
|
|
760
|
+
ctx_name = ctx_obj.name
|
|
761
|
+
ctx_ns = ctx_obj.graph_namespace
|
|
762
|
+
ctx_type = ctx_obj.context_type or ""
|
|
763
|
+
ctx_desc = ctx_obj.description or ""
|
|
764
|
+
ctx_items = ctx_obj.item_count or 0
|
|
765
|
+
ctx_tokens = ctx_obj.estimated_tokens or 0
|
|
766
|
+
ctx_tags = ctx_obj.tags or []
|
|
767
|
+
ctx_sensitivity = ctx_obj.sensitivity or ""
|
|
768
|
+
except Exception:
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
# Add ContextRef node with full context details
|
|
772
|
+
runtime_graph.add_node(GraphNode(
|
|
773
|
+
id=context_id, label="ContextRef",
|
|
774
|
+
properties={
|
|
775
|
+
"name": ctx_name,
|
|
776
|
+
"context_id": context_id,
|
|
777
|
+
"graph_namespace": ctx_ns,
|
|
778
|
+
"context_type": ctx_type,
|
|
779
|
+
"description": ctx_desc,
|
|
780
|
+
"item_count": ctx_items,
|
|
781
|
+
"estimated_tokens": ctx_tokens,
|
|
782
|
+
"tags": ctx_tags,
|
|
783
|
+
"sensitivity": ctx_sensitivity,
|
|
784
|
+
"role": role,
|
|
785
|
+
"attached_at": now,
|
|
786
|
+
},
|
|
787
|
+
), write_through=True)
|
|
788
|
+
# Only add edge if it doesn't already exist (avoid duplicates on re-attach)
|
|
789
|
+
existing_edges = runtime_graph.get_all_edges()
|
|
790
|
+
has_edge = any(
|
|
791
|
+
e.source == session_root_id and e.target == context_id and e.label == "HAS_CONTEXT"
|
|
792
|
+
for e in existing_edges
|
|
793
|
+
)
|
|
794
|
+
if not has_edge:
|
|
795
|
+
runtime_graph.add_edge(GraphEdge(
|
|
796
|
+
id=str(_uuid.uuid4()), source=session_root_id, target=context_id,
|
|
797
|
+
label="HAS_CONTEXT", properties={"role": role},
|
|
798
|
+
))
|
|
799
|
+
|
|
800
|
+
# ── REFERENCE-ONLY: store graph namespace, don't copy data ──
|
|
801
|
+
# Agents search across all attached graphs via fan-out.
|
|
802
|
+
# This is instant — no node/edge copying needed.
|
|
803
|
+
if ctx_ns:
|
|
804
|
+
log = logging.getLogger(__name__)
|
|
805
|
+
log.info("[SESSION] Attached context '%s' (%s) to runtime '%s' (reference-only, no merge)",
|
|
806
|
+
ctx_name, ctx_ns, session.graph_namespace)
|
|
807
|
+
|
|
808
|
+
# Save — create a checkpoint BEFORE the merge so detach can
|
|
809
|
+
# restore to this exact state (like a git commit before a change)
|
|
810
|
+
try:
|
|
811
|
+
self._graph_registry.save_graph(
|
|
812
|
+
session.graph_namespace, create_checkpoint=True,
|
|
813
|
+
)
|
|
814
|
+
# Tag the checkpoint with the context being attached
|
|
815
|
+
try:
|
|
816
|
+
from ..core.checkpoint import checkpoint_manager
|
|
817
|
+
cps = checkpoint_manager.get_checkpoints(session.graph_namespace)
|
|
818
|
+
if cps:
|
|
819
|
+
cps[0].message = f"pre-attach:{context_id}"
|
|
820
|
+
checkpoint_manager._save_checkpoint_index(session.graph_namespace)
|
|
821
|
+
except Exception:
|
|
822
|
+
pass
|
|
823
|
+
except Exception:
|
|
824
|
+
pass
|
|
825
|
+
|
|
826
|
+
except Exception as e:
|
|
827
|
+
logging.getLogger(__name__).debug("Graph merge for attach_context failed: %s", e)
|
|
828
|
+
|
|
829
|
+
return True
|
|
830
|
+
except Exception:
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
def get_attached_contexts(self, session_id: str) -> List[Dict[str, Any]]:
|
|
834
|
+
"""Get all contexts attached to a session with their graph namespaces."""
|
|
835
|
+
rows = self._conn.execute(
|
|
836
|
+
"SELECT context_id, role, attached_at FROM session_contexts WHERE session_id = ?",
|
|
837
|
+
(session_id,),
|
|
838
|
+
).fetchall()
|
|
839
|
+
result = []
|
|
840
|
+
for row in rows:
|
|
841
|
+
ctx_id = row["context_id"]
|
|
842
|
+
entry = {"context_id": ctx_id, "role": row["role"], "attached_at": row["attached_at"]}
|
|
843
|
+
# Resolve full context details
|
|
844
|
+
try:
|
|
845
|
+
from .context_manager import ContextManager
|
|
846
|
+
cm = ContextManager(graph_registry=self._graph_registry)
|
|
847
|
+
ctx_obj = cm.get_context(ctx_id)
|
|
848
|
+
if ctx_obj:
|
|
849
|
+
entry["name"] = ctx_obj.name
|
|
850
|
+
entry["graph_namespace"] = ctx_obj.graph_namespace
|
|
851
|
+
entry["context_type"] = ctx_obj.context_type or ""
|
|
852
|
+
entry["description"] = ctx_obj.description or ""
|
|
853
|
+
entry["item_count"] = ctx_obj.item_count or 0
|
|
854
|
+
entry["estimated_tokens"] = ctx_obj.estimated_tokens or 0
|
|
855
|
+
entry["tags"] = ctx_obj.tags or []
|
|
856
|
+
entry["sensitivity"] = ctx_obj.sensitivity or ""
|
|
857
|
+
else:
|
|
858
|
+
entry["graph_namespace"] = ""
|
|
859
|
+
except Exception:
|
|
860
|
+
entry["graph_namespace"] = ""
|
|
861
|
+
result.append(entry)
|
|
862
|
+
return result
|
|
863
|
+
|
|
864
|
+
def detach_context(self, session_id: str, context_id: str) -> bool:
|
|
865
|
+
"""Detach a context from a session — removes merged nodes from runtime graph.
|
|
866
|
+
|
|
867
|
+
Handles versioned context IDs (e.g. ``abc123:1``) by trying both
|
|
868
|
+
the exact ID and the base ID (without the version suffix). After
|
|
869
|
+
removing the context's nodes the session graph falls back to its
|
|
870
|
+
base state — only native session nodes remain.
|
|
871
|
+
"""
|
|
872
|
+
log = logging.getLogger(__name__)
|
|
873
|
+
|
|
874
|
+
# Try exact match first, then base ID (strip version suffix like ":1")
|
|
875
|
+
cur = self._conn.execute(
|
|
876
|
+
"DELETE FROM session_contexts WHERE session_id = ? AND context_id = ?",
|
|
877
|
+
(session_id, context_id),
|
|
878
|
+
)
|
|
879
|
+
self._conn.commit()
|
|
880
|
+
removed = cur.rowcount > 0
|
|
881
|
+
|
|
882
|
+
# If exact match failed and ID has a version suffix, try the base ID
|
|
883
|
+
base_context_id = context_id
|
|
884
|
+
if not removed and ":" in context_id:
|
|
885
|
+
base_context_id = context_id.rsplit(":", 1)[0]
|
|
886
|
+
cur = self._conn.execute(
|
|
887
|
+
"DELETE FROM session_contexts WHERE session_id = ? AND context_id = ?",
|
|
888
|
+
(session_id, base_context_id),
|
|
889
|
+
)
|
|
890
|
+
self._conn.commit()
|
|
891
|
+
removed = cur.rowcount > 0
|
|
892
|
+
if removed:
|
|
893
|
+
log.info("[SESSION] Detached via base context_id '%s' (requested '%s')",
|
|
894
|
+
base_context_id, context_id)
|
|
895
|
+
|
|
896
|
+
# Also try matching by prefix if the stored ID has a version but the
|
|
897
|
+
# request doesn't (opposite direction)
|
|
898
|
+
if not removed:
|
|
899
|
+
cur = self._conn.execute(
|
|
900
|
+
"DELETE FROM session_contexts WHERE session_id = ? AND context_id LIKE ?",
|
|
901
|
+
(session_id, f"{context_id}%"),
|
|
902
|
+
)
|
|
903
|
+
self._conn.commit()
|
|
904
|
+
removed = cur.rowcount > 0
|
|
905
|
+
|
|
906
|
+
# Revert the graph to its pre-attach state — like `git checkout` to the
|
|
907
|
+
# checkpoint created before this context was merged.
|
|
908
|
+
if removed and self._graph_registry:
|
|
909
|
+
try:
|
|
910
|
+
session = self.get_session(session_id)
|
|
911
|
+
if session:
|
|
912
|
+
ns = session.graph_namespace
|
|
913
|
+
restored_from_checkpoint = False
|
|
914
|
+
|
|
915
|
+
# Strategy 1: Restore from the pre-attach checkpoint (clean revert)
|
|
916
|
+
try:
|
|
917
|
+
from ..core.checkpoint import checkpoint_manager
|
|
918
|
+
cps = checkpoint_manager.get_checkpoints(ns)
|
|
919
|
+
# Find the checkpoint tagged with this context attach
|
|
920
|
+
target_cp = None
|
|
921
|
+
for cp in cps:
|
|
922
|
+
msg = cp.message or ""
|
|
923
|
+
if msg == f"pre-attach:{context_id}" or msg == f"pre-attach:{base_context_id}":
|
|
924
|
+
target_cp = cp
|
|
925
|
+
break
|
|
926
|
+
|
|
927
|
+
if target_cp:
|
|
928
|
+
# Resolve the graph file path
|
|
929
|
+
ns_path = self._graph_registry._get_namespace_path(ns)
|
|
930
|
+
if ns_path:
|
|
931
|
+
result = checkpoint_manager.restore_checkpoint(
|
|
932
|
+
ns, target_cp.checkpoint_id, str(ns_path),
|
|
933
|
+
)
|
|
934
|
+
if result.get("success"):
|
|
935
|
+
# Reload the graph from the restored file
|
|
936
|
+
self._graph_registry.load_graph(ns)
|
|
937
|
+
restored_from_checkpoint = True
|
|
938
|
+
log.info(
|
|
939
|
+
"[SESSION] Restored graph '%s' from checkpoint '%s' "
|
|
940
|
+
"(pre-attach:%s) — clean revert like git checkout",
|
|
941
|
+
ns, target_cp.checkpoint_id, context_id,
|
|
942
|
+
)
|
|
943
|
+
except Exception as e:
|
|
944
|
+
log.debug("Checkpoint restore failed, falling back to node removal: %s", e)
|
|
945
|
+
|
|
946
|
+
# Strategy 2: Fallback — manually remove tagged nodes
|
|
947
|
+
if not restored_from_checkpoint:
|
|
948
|
+
runtime_graph = self._graph_registry.get_graph(ns)
|
|
949
|
+
if runtime_graph:
|
|
950
|
+
removed_nodes = 0
|
|
951
|
+
match_ids = {context_id, base_context_id}
|
|
952
|
+
|
|
953
|
+
to_remove = []
|
|
954
|
+
for node in runtime_graph.get_all_nodes():
|
|
955
|
+
props = node.properties if hasattr(node, "properties") else {}
|
|
956
|
+
source_ctx = props.get("_source_context", "")
|
|
957
|
+
if source_ctx in match_ids:
|
|
958
|
+
to_remove.append(node.id)
|
|
959
|
+
elif node.id in match_ids: # ContextRef node
|
|
960
|
+
to_remove.append(node.id)
|
|
961
|
+
elif getattr(node, "label", "") == "ContextRef":
|
|
962
|
+
if props.get("context_id", "") in match_ids:
|
|
963
|
+
to_remove.append(node.id)
|
|
964
|
+
|
|
965
|
+
for nid in to_remove:
|
|
966
|
+
try:
|
|
967
|
+
runtime_graph.delete_node(nid)
|
|
968
|
+
removed_nodes += 1
|
|
969
|
+
except Exception:
|
|
970
|
+
try:
|
|
971
|
+
from ..aiql import AIQLExecutor
|
|
972
|
+
ex = AIQLExecutor(contextcore=runtime_graph, graph_registry=self._graph_registry)
|
|
973
|
+
ex.active_namespace = ns
|
|
974
|
+
ex.execute(f'DELETE NODE "{nid}"')
|
|
975
|
+
removed_nodes += 1
|
|
976
|
+
except Exception:
|
|
977
|
+
pass
|
|
978
|
+
# Clean LMDB regardless of which delete path succeeded
|
|
979
|
+
try:
|
|
980
|
+
from ..search.lmdb_index import lmdb_delete_node
|
|
981
|
+
lmdb_delete_node(ns, nid)
|
|
982
|
+
except Exception:
|
|
983
|
+
pass
|
|
984
|
+
|
|
985
|
+
log.info("[SESSION] Detached context '%s' from '%s': removed %d nodes "
|
|
986
|
+
"(fallback — no checkpoint found)",
|
|
987
|
+
context_id, ns, removed_nodes)
|
|
988
|
+
|
|
989
|
+
# Save the reverted graph (with a new checkpoint)
|
|
990
|
+
try:
|
|
991
|
+
self._graph_registry.save_graph(ns, create_checkpoint=True)
|
|
992
|
+
except Exception:
|
|
993
|
+
pass
|
|
994
|
+
except Exception as e:
|
|
995
|
+
log.debug("Graph cleanup for detach_context failed: %s", e)
|
|
996
|
+
|
|
997
|
+
return removed
|
|
998
|
+
|
|
999
|
+
def get_session_context_ids(self, session_id: str) -> List[Dict[str, Any]]:
|
|
1000
|
+
"""Get all context attachments for a session."""
|
|
1001
|
+
rows = self._conn.execute(
|
|
1002
|
+
"SELECT context_id, role, attached_at FROM session_contexts WHERE session_id = ? ORDER BY attached_at",
|
|
1003
|
+
(session_id,),
|
|
1004
|
+
).fetchall()
|
|
1005
|
+
return [dict(r) for r in rows]
|
|
1006
|
+
|
|
1007
|
+
def get_context_session_ids(self, context_id: str) -> List[Dict[str, Any]]:
|
|
1008
|
+
"""Get all sessions that use a given context."""
|
|
1009
|
+
rows = self._conn.execute(
|
|
1010
|
+
"SELECT session_id, role, attached_at FROM session_contexts WHERE context_id = ? ORDER BY attached_at",
|
|
1011
|
+
(context_id,),
|
|
1012
|
+
).fetchall()
|
|
1013
|
+
return [dict(r) for r in rows]
|
|
1014
|
+
|
|
1015
|
+
def sync_context_to_boundaries(self, context_id: str, context_manager=None) -> int:
|
|
1016
|
+
"""Sync new nodes from a context to all boundaries that have it attached.
|
|
1017
|
+
|
|
1018
|
+
Called after ingestion or data addition. Only syncs nodes added since
|
|
1019
|
+
the last sync (delta sync, not full re-copy).
|
|
1020
|
+
|
|
1021
|
+
Returns number of nodes synced across all boundaries.
|
|
1022
|
+
"""
|
|
1023
|
+
if not self._graph_registry:
|
|
1024
|
+
return 0
|
|
1025
|
+
|
|
1026
|
+
# Find all sessions attached to this context
|
|
1027
|
+
sessions = self.get_context_session_ids(context_id)
|
|
1028
|
+
if not sessions:
|
|
1029
|
+
return 0
|
|
1030
|
+
|
|
1031
|
+
# Get context graph
|
|
1032
|
+
ctx_obj = None
|
|
1033
|
+
ctx_graph = None
|
|
1034
|
+
if context_manager:
|
|
1035
|
+
ctx_obj = context_manager.get_context(context_id)
|
|
1036
|
+
if ctx_obj:
|
|
1037
|
+
ctx_graph = self._graph_registry.get_graph(ctx_obj.graph_namespace, load_if_missing=True)
|
|
1038
|
+
|
|
1039
|
+
if not ctx_graph:
|
|
1040
|
+
return 0
|
|
1041
|
+
|
|
1042
|
+
from ..core.graph_structures import GraphNode, GraphEdge
|
|
1043
|
+
from ..context.boundaries import BOUNDARY_NODE_LABELS
|
|
1044
|
+
import uuid as _sync_uuid
|
|
1045
|
+
|
|
1046
|
+
# Structural labels to skip
|
|
1047
|
+
skip_labels = BOUNDARY_NODE_LABELS | {"Context", "KnowledgeBase", "CodeBase",
|
|
1048
|
+
"SystemStore", "UserStore", "WebStore",
|
|
1049
|
+
"GeneratedStore", "MemoryStore", "ArtifactStore", "ToolStore"}
|
|
1050
|
+
|
|
1051
|
+
total_synced = 0
|
|
1052
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
1053
|
+
|
|
1054
|
+
for att in sessions:
|
|
1055
|
+
session_id = att.get("session_id", "")
|
|
1056
|
+
session = self.get_session(session_id)
|
|
1057
|
+
if not session:
|
|
1058
|
+
continue
|
|
1059
|
+
|
|
1060
|
+
boundary_graph = self._graph_registry.get_graph(session.graph_namespace)
|
|
1061
|
+
if not boundary_graph:
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
# Get existing node IDs in boundary to avoid duplicates
|
|
1065
|
+
existing_ids = set()
|
|
1066
|
+
for n in boundary_graph.get_all_nodes():
|
|
1067
|
+
existing_ids.add(n.id if hasattr(n, "id") else "")
|
|
1068
|
+
|
|
1069
|
+
# Sync new nodes from context graph
|
|
1070
|
+
synced = 0
|
|
1071
|
+
for node in ctx_graph.get_all_nodes():
|
|
1072
|
+
label = node.label if hasattr(node, "label") else ""
|
|
1073
|
+
nid = node.id if hasattr(node, "id") else ""
|
|
1074
|
+
if label in skip_labels or nid in existing_ids:
|
|
1075
|
+
continue
|
|
1076
|
+
|
|
1077
|
+
props = node.properties if hasattr(node, "properties") else {}
|
|
1078
|
+
synced_props = dict(props)
|
|
1079
|
+
synced_props["_source_context"] = context_id
|
|
1080
|
+
synced_props["_source_context_name"] = ctx_obj.name if ctx_obj else ""
|
|
1081
|
+
synced_props["_synced_at"] = now
|
|
1082
|
+
|
|
1083
|
+
boundary_graph.add_node(GraphNode(
|
|
1084
|
+
id=nid, label=label, properties=synced_props,
|
|
1085
|
+
), write_through=True)
|
|
1086
|
+
|
|
1087
|
+
# Link to ContextRef if it exists
|
|
1088
|
+
if context_id in existing_ids:
|
|
1089
|
+
boundary_graph.add_edge(GraphEdge(
|
|
1090
|
+
id=str(_sync_uuid.uuid4()),
|
|
1091
|
+
source=context_id, target=nid,
|
|
1092
|
+
label="CONTAINS", properties={},
|
|
1093
|
+
))
|
|
1094
|
+
synced += 1
|
|
1095
|
+
|
|
1096
|
+
# Sync edges between synced nodes
|
|
1097
|
+
all_boundary_ids = existing_ids | {n.id for n in boundary_graph.get_all_nodes() if hasattr(n, "id")}
|
|
1098
|
+
for edge in ctx_graph.get_all_edges():
|
|
1099
|
+
src = edge.source if hasattr(edge, "source") else edge.get("source", "")
|
|
1100
|
+
tgt = edge.target if hasattr(edge, "target") else edge.get("target", "")
|
|
1101
|
+
if src in all_boundary_ids and tgt in all_boundary_ids:
|
|
1102
|
+
lbl = edge.label if hasattr(edge, "label") else edge.get("label", "")
|
|
1103
|
+
try:
|
|
1104
|
+
boundary_graph.add_edge(GraphEdge(
|
|
1105
|
+
id=str(_sync_uuid.uuid4()),
|
|
1106
|
+
source=src, target=tgt, label=lbl, properties={},
|
|
1107
|
+
))
|
|
1108
|
+
except Exception:
|
|
1109
|
+
pass
|
|
1110
|
+
|
|
1111
|
+
if synced > 0:
|
|
1112
|
+
try:
|
|
1113
|
+
self._graph_registry.save_graph(session.graph_namespace, create_checkpoint=False)
|
|
1114
|
+
except Exception:
|
|
1115
|
+
pass
|
|
1116
|
+
total_synced += synced
|
|
1117
|
+
|
|
1118
|
+
# Update last_synced_at
|
|
1119
|
+
try:
|
|
1120
|
+
self._conn.execute(
|
|
1121
|
+
"UPDATE session_contexts SET last_synced_at = ? WHERE session_id = ? AND context_id = ?",
|
|
1122
|
+
(now, session_id, context_id),
|
|
1123
|
+
)
|
|
1124
|
+
self._conn.commit()
|
|
1125
|
+
except Exception:
|
|
1126
|
+
pass
|
|
1127
|
+
|
|
1128
|
+
if total_synced > 0:
|
|
1129
|
+
logging.getLogger(__name__).info(
|
|
1130
|
+
"Reactive sync: %d nodes from context %s to %d boundaries",
|
|
1131
|
+
total_synced, context_id[:12], len(sessions),
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
return total_synced
|
|
1135
|
+
|
|
1136
|
+
def close(self):
|
|
1137
|
+
if self._conn:
|
|
1138
|
+
self._conn.close()
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
# Alias for simpler usage in demos and scripts
|
|
1142
|
+
SessionStore = ContextSessionManager
|