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,1043 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic graph creation, caching, and metadata management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import pickle
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, List, Any, Optional, Union
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from dataclasses import dataclass, asdict
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
import re as _re
|
|
17
|
+
|
|
18
|
+
from .hybrid_graph_storage import AIContextDB
|
|
19
|
+
|
|
20
|
+
# Ensure .env is loaded before any graph creation (critical for Redis backend)
|
|
21
|
+
try:
|
|
22
|
+
from ..utils.env_loader import initialize_env_loader
|
|
23
|
+
initialize_env_loader()
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _safe_dir_name(name: str) -> str:
|
|
31
|
+
"""Sanitize a graph name for use as a filesystem directory name.
|
|
32
|
+
|
|
33
|
+
Replaces all characters invalid on Windows (<>:"/\\|?*) with underscore.
|
|
34
|
+
"""
|
|
35
|
+
return _re.sub(r'[<>:"/\\|?*]', '_', name)
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class GraphMetadata:
|
|
39
|
+
"""Metadata for a graph instance."""
|
|
40
|
+
name: str
|
|
41
|
+
created_at: datetime
|
|
42
|
+
updated_at: datetime
|
|
43
|
+
num_nodes: int
|
|
44
|
+
num_edges: int
|
|
45
|
+
schema_file: Optional[str] = None
|
|
46
|
+
data_files: List[str] = None
|
|
47
|
+
embedding_model: Optional[str] = None
|
|
48
|
+
last_accessed: Optional[datetime] = None
|
|
49
|
+
access_count: int = 0
|
|
50
|
+
size_mb: float = 0.0
|
|
51
|
+
tags: List[str] = None
|
|
52
|
+
storage_backend: str = "disk" # "disk" or "redis"
|
|
53
|
+
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
if self.data_files is None:
|
|
56
|
+
self.data_files = []
|
|
57
|
+
if self.tags is None:
|
|
58
|
+
self.tags = []
|
|
59
|
+
|
|
60
|
+
class GraphRegistry:
|
|
61
|
+
"""
|
|
62
|
+
Registry for managing multiple graph instances with caching and metadata.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, storage_dir: str = None):
|
|
66
|
+
# Use namespace-centric structure by default
|
|
67
|
+
if storage_dir is None:
|
|
68
|
+
storage_dir = "contextcore_data"
|
|
69
|
+
self.storage_dir = Path(storage_dir)
|
|
70
|
+
self.storage_dir.mkdir(exist_ok=True)
|
|
71
|
+
|
|
72
|
+
self.graphs: Dict[str, AIContextDB] = {}
|
|
73
|
+
self.metadata: Dict[str, GraphMetadata] = {}
|
|
74
|
+
self._lock = threading.RLock()
|
|
75
|
+
self._metadata_mtime: float = 0.0 # track file mtime for multi-worker sync
|
|
76
|
+
self._graph_file_mtimes: Dict[str, float] = {} # track per-graph file mtime
|
|
77
|
+
self.cache_config = {
|
|
78
|
+
'max_graphs': int(os.environ.get('AICONTEXTDB_MAX_GRAPHS', '50')),
|
|
79
|
+
'max_size_mb': int(os.environ.get('AICONTEXTDB_MAX_GRAPH_SIZE_MB', '1000')),
|
|
80
|
+
'ttl_hours': 24
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Load existing metadata
|
|
84
|
+
self._load_metadata()
|
|
85
|
+
|
|
86
|
+
def create_graph(self, name: str, schema_file: Optional[str] = None,
|
|
87
|
+
config: Optional[Dict] = None,
|
|
88
|
+
schema_name: Optional[str] = None) -> AIContextDB:
|
|
89
|
+
"""
|
|
90
|
+
Create a new graph instance.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Name of the graph
|
|
94
|
+
schema_file: Optional schema file path
|
|
95
|
+
config: Optional configuration
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
AIContextDB instance
|
|
99
|
+
"""
|
|
100
|
+
with self._lock:
|
|
101
|
+
if name in self.graphs:
|
|
102
|
+
return self.graphs[name]
|
|
103
|
+
|
|
104
|
+
# Let AIContextDB pick its backend (Redis if available, else CSR)
|
|
105
|
+
graph = AIContextDB(name, config)
|
|
106
|
+
|
|
107
|
+
if schema_file:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
self.graphs[name] = graph
|
|
111
|
+
|
|
112
|
+
# Create ContextMeta root node — metadata anchor for this graph
|
|
113
|
+
from .graph_structures import GraphNode as _GN
|
|
114
|
+
meta_props = {
|
|
115
|
+
"name": name,
|
|
116
|
+
"description": "",
|
|
117
|
+
"purpose": "knowledge_base",
|
|
118
|
+
"schema_summary": "",
|
|
119
|
+
"node_count": 0,
|
|
120
|
+
"edge_count": 0,
|
|
121
|
+
"created_at": datetime.now().isoformat(),
|
|
122
|
+
"updated_at": datetime.now().isoformat(),
|
|
123
|
+
"owner": "",
|
|
124
|
+
"tags": [],
|
|
125
|
+
}
|
|
126
|
+
# Include schema in ContextMeta if provided
|
|
127
|
+
if config and config.get("schema"):
|
|
128
|
+
meta_props["schema"] = config["schema"]
|
|
129
|
+
if schema_name:
|
|
130
|
+
meta_props["schema_name"] = schema_name
|
|
131
|
+
graph.add_node(_GN(id="_context_meta", label="ContextMeta", properties=meta_props))
|
|
132
|
+
|
|
133
|
+
# Bind namespace to schema if provided
|
|
134
|
+
if schema_name:
|
|
135
|
+
try:
|
|
136
|
+
from contextsynapse.project.schema_manager import get_schema_manager
|
|
137
|
+
get_schema_manager().bind(name, schema_name)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning("Could not bind schema '%s' to namespace '%s': %s",
|
|
140
|
+
schema_name, name, e)
|
|
141
|
+
|
|
142
|
+
metadata = GraphMetadata(
|
|
143
|
+
name=name,
|
|
144
|
+
created_at=datetime.now(),
|
|
145
|
+
updated_at=datetime.now(),
|
|
146
|
+
num_nodes=0,
|
|
147
|
+
num_edges=0,
|
|
148
|
+
schema_file=schema_file,
|
|
149
|
+
storage_backend="redis" if getattr(graph, '_using_redis_backend', False) else "disk",
|
|
150
|
+
)
|
|
151
|
+
self.metadata[name] = metadata
|
|
152
|
+
self._save_metadata()
|
|
153
|
+
|
|
154
|
+
# Auto-evict LRU if over limit
|
|
155
|
+
self._auto_cleanup()
|
|
156
|
+
|
|
157
|
+
return graph
|
|
158
|
+
|
|
159
|
+
def drop_graph(self, name: str) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Drop a graph instance and its associated files.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Name of the graph to drop
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if successful, False if graph doesn't exist
|
|
168
|
+
"""
|
|
169
|
+
with self._lock:
|
|
170
|
+
if name not in self.graphs and name not in self.metadata:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Remove from memory
|
|
175
|
+
if name in self.graphs:
|
|
176
|
+
del self.graphs[name]
|
|
177
|
+
|
|
178
|
+
# Remove metadata
|
|
179
|
+
if name in self.metadata:
|
|
180
|
+
del self.metadata[name]
|
|
181
|
+
|
|
182
|
+
# Remove files from disk
|
|
183
|
+
graph_dir = self.storage_dir / name
|
|
184
|
+
if graph_dir.exists():
|
|
185
|
+
import shutil
|
|
186
|
+
shutil.rmtree(graph_dir)
|
|
187
|
+
|
|
188
|
+
# Save updated metadata
|
|
189
|
+
self._save_metadata()
|
|
190
|
+
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f"Error dropping graph '{name}': {e}")
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def get_graph(self, name: str, load_if_missing: bool = True) -> Optional[AIContextDB]:
|
|
198
|
+
"""
|
|
199
|
+
Get a graph instance by name.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
name: Name of the graph
|
|
203
|
+
load_if_missing: Whether to load from disk if not in memory
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
AIContextDB instance or None
|
|
207
|
+
"""
|
|
208
|
+
with self._lock:
|
|
209
|
+
self._refresh_metadata_if_stale()
|
|
210
|
+
|
|
211
|
+
if name in self.graphs:
|
|
212
|
+
graph = self.graphs[name]
|
|
213
|
+
# Skip disk reload for Redis-backed graphs — Redis is source of truth
|
|
214
|
+
if not getattr(graph, '_using_redis_backend', False):
|
|
215
|
+
if self._graph_file_is_newer(name):
|
|
216
|
+
logger.debug("[REGISTRY] Graph '%s' changed on disk — reloading", name)
|
|
217
|
+
reloaded = self._load_graph_from_disk(name)
|
|
218
|
+
if reloaded:
|
|
219
|
+
self.graphs[name] = reloaded
|
|
220
|
+
graph = reloaded
|
|
221
|
+
self._update_access_metadata(name)
|
|
222
|
+
return graph
|
|
223
|
+
|
|
224
|
+
if load_if_missing:
|
|
225
|
+
# For Redis-backed graphs: just create instance (connects to Redis, no disk load)
|
|
226
|
+
import os
|
|
227
|
+
if os.environ.get("CONTEXTSYNAPSE_REDIS_URL") or os.environ.get("AICONTEXTDB_REDIS_URL") and os.environ.get("CONTEXTSYNAPSE_GRAPH_BACKEND") or os.environ.get("AICONTEXTDB_GRAPH_BACKEND", "redis") == "redis":
|
|
228
|
+
graph = AIContextDB(name)
|
|
229
|
+
if getattr(graph, '_using_redis_backend', False):
|
|
230
|
+
self.graphs[name] = graph
|
|
231
|
+
self._auto_cleanup()
|
|
232
|
+
logger.debug("[REGISTRY] Graph '%s' created (Redis backend, no disk load)", name)
|
|
233
|
+
return graph
|
|
234
|
+
|
|
235
|
+
# Fallback: load from disk (CSR/LMDB)
|
|
236
|
+
if name in self.metadata:
|
|
237
|
+
loaded_graph = self._load_graph_from_disk(name)
|
|
238
|
+
if loaded_graph:
|
|
239
|
+
self.graphs[name] = loaded_graph
|
|
240
|
+
self._auto_cleanup()
|
|
241
|
+
return loaded_graph
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def list_graphs(self) -> List[Dict[str, Any]]:
|
|
246
|
+
"""List all available graphs with metadata."""
|
|
247
|
+
with self._lock:
|
|
248
|
+
self._refresh_metadata_if_stale()
|
|
249
|
+
graphs_info = []
|
|
250
|
+
for name, metadata in self.metadata.items():
|
|
251
|
+
info = asdict(metadata)
|
|
252
|
+
info['in_memory'] = name in self.graphs
|
|
253
|
+
info['on_disk'] = True # assume on disk if in metadata
|
|
254
|
+
info['last_accessed'] = metadata.last_accessed.isoformat() if metadata.last_accessed else None
|
|
255
|
+
info['created_at'] = metadata.created_at.isoformat()
|
|
256
|
+
info['updated_at'] = metadata.updated_at.isoformat()
|
|
257
|
+
graphs_info.append(info)
|
|
258
|
+
return sorted(graphs_info, key=lambda x: x.get('last_accessed') or x.get('created_at', ''), reverse=True)
|
|
259
|
+
|
|
260
|
+
def delete_graph(self, name: str, delete_files: bool = True) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Delete a graph instance.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
name: Name of the graph
|
|
266
|
+
delete_files: Whether to delete associated files
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
True if successful
|
|
270
|
+
"""
|
|
271
|
+
with self._lock:
|
|
272
|
+
try:
|
|
273
|
+
# Close WAL/buffer handles before deleting
|
|
274
|
+
if name in self.graphs:
|
|
275
|
+
graph = self.graphs[name]
|
|
276
|
+
if hasattr(graph, 'wal') and graph.wal:
|
|
277
|
+
try:
|
|
278
|
+
graph.wal.running = False
|
|
279
|
+
if graph.wal.wal_file:
|
|
280
|
+
graph.wal.wal_file.close()
|
|
281
|
+
graph.wal.wal_file = None
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
del self.graphs[name]
|
|
285
|
+
else:
|
|
286
|
+
# Graph not in memory — try to close any orphaned WAL file handles
|
|
287
|
+
# This handles the case where a graph was deleted but WAL is
|
|
288
|
+
# still locked by a stale file handle
|
|
289
|
+
safe_name = _safe_dir_name(name)
|
|
290
|
+
for wal_dir in [
|
|
291
|
+
self.storage_dir / "wal" / safe_name,
|
|
292
|
+
self.storage_dir / "wal" / name,
|
|
293
|
+
]:
|
|
294
|
+
if wal_dir.exists():
|
|
295
|
+
for wal_file in wal_dir.glob("*.log"):
|
|
296
|
+
try:
|
|
297
|
+
# Open and immediately close to release any stale handle
|
|
298
|
+
with open(wal_file, "a"):
|
|
299
|
+
pass
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
# Remove metadata
|
|
304
|
+
if name in self.metadata:
|
|
305
|
+
del self.metadata[name]
|
|
306
|
+
|
|
307
|
+
# Delete files if requested
|
|
308
|
+
if delete_files:
|
|
309
|
+
import shutil as _shutil
|
|
310
|
+
safe_name = _safe_dir_name(name)
|
|
311
|
+
# Clean namespace directories (relative to storage_dir)
|
|
312
|
+
for base in [
|
|
313
|
+
self.storage_dir / "namespaces" / safe_name,
|
|
314
|
+
self.storage_dir / "namespaces" / name,
|
|
315
|
+
self.storage_dir / safe_name,
|
|
316
|
+
]:
|
|
317
|
+
if base.exists():
|
|
318
|
+
_shutil.rmtree(base, ignore_errors=True)
|
|
319
|
+
# Clean WAL directory
|
|
320
|
+
for wal in [
|
|
321
|
+
self.storage_dir / "wal" / safe_name,
|
|
322
|
+
self.storage_dir / "wal" / name,
|
|
323
|
+
]:
|
|
324
|
+
if wal.exists():
|
|
325
|
+
try:
|
|
326
|
+
_shutil.rmtree(wal)
|
|
327
|
+
except PermissionError:
|
|
328
|
+
# WAL locked by another process — close handles and retry
|
|
329
|
+
for wf in wal.glob("*.log"):
|
|
330
|
+
try:
|
|
331
|
+
with open(wf, "a"):
|
|
332
|
+
pass
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
try:
|
|
336
|
+
_shutil.rmtree(wal)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.warning("WAL cleanup failed for %s: %s (will clear on restart)", name, e)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning("WAL cleanup failed for %s: %s", name, e)
|
|
341
|
+
|
|
342
|
+
# Save updated metadata
|
|
343
|
+
self._save_metadata()
|
|
344
|
+
|
|
345
|
+
# Cascade: delete any context/session that uses this graph namespace
|
|
346
|
+
# Use storage_dir-relative context.db so it works with custom paths
|
|
347
|
+
context_db = str(self.storage_dir / "context.db")
|
|
348
|
+
try:
|
|
349
|
+
import sqlite3 as _sqlite3
|
|
350
|
+
conn = _sqlite3.connect(context_db)
|
|
351
|
+
conn.row_factory = _sqlite3.Row
|
|
352
|
+
row = conn.execute(
|
|
353
|
+
"SELECT context_id FROM contexts WHERE graph_namespace = ?",
|
|
354
|
+
(name,),
|
|
355
|
+
).fetchone()
|
|
356
|
+
if row:
|
|
357
|
+
conn.execute(
|
|
358
|
+
"DELETE FROM contexts WHERE graph_namespace = ?",
|
|
359
|
+
(name,),
|
|
360
|
+
)
|
|
361
|
+
conn.commit()
|
|
362
|
+
# Remove from default graph
|
|
363
|
+
default = self.graphs.get("default")
|
|
364
|
+
if default and row["context_id"]:
|
|
365
|
+
try:
|
|
366
|
+
default.remove_node(row["context_id"])
|
|
367
|
+
except Exception:
|
|
368
|
+
pass # optional
|
|
369
|
+
conn.close()
|
|
370
|
+
except Exception:
|
|
371
|
+
pass # optional
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
import sqlite3 as _sqlite3
|
|
375
|
+
conn = _sqlite3.connect(context_db)
|
|
376
|
+
conn.execute(
|
|
377
|
+
"DELETE FROM context_sessions WHERE graph_namespace = ?",
|
|
378
|
+
(name,),
|
|
379
|
+
)
|
|
380
|
+
conn.commit()
|
|
381
|
+
conn.close()
|
|
382
|
+
except Exception:
|
|
383
|
+
pass # optional
|
|
384
|
+
|
|
385
|
+
return True
|
|
386
|
+
except Exception as e:
|
|
387
|
+
print(f"Error deleting graph '{name}': {e}")
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
def save_graph(self, name: str, create_checkpoint: bool = True) -> bool:
|
|
391
|
+
"""
|
|
392
|
+
Save a graph to disk - uses namespace-centric structure for PERSISTENT namespaces.
|
|
393
|
+
Optionally creates a checkpoint (Git-like versioning) for rollback capability.
|
|
394
|
+
|
|
395
|
+
This method is used by BOTH direct graph operations and API operations.
|
|
396
|
+
It ensures the same code path is used regardless of how the graph was created/modified.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
name: Name of the graph/namespace
|
|
400
|
+
create_checkpoint: Whether to create a checkpoint after saving (default: True)
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
True if successful
|
|
404
|
+
"""
|
|
405
|
+
if name not in self.graphs:
|
|
406
|
+
logger.error(f"[REGISTRY SAVE] Graph '{name}' not found in registry!")
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
graph = self.graphs[name]
|
|
411
|
+
if graph is None:
|
|
412
|
+
logger.error(f"[REGISTRY SAVE] Graph '{name}' is None in registry!")
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
# Skip disk save for Redis-backed graphs — Redis is the persistence layer
|
|
416
|
+
if getattr(graph, '_using_redis_backend', False):
|
|
417
|
+
logger.debug("[REGISTRY SAVE] Skipping disk save for Redis-backed graph '%s'", name)
|
|
418
|
+
if name in self.metadata:
|
|
419
|
+
self.metadata[name].storage_backend = "redis"
|
|
420
|
+
self._save_metadata()
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
# Single deterministic path — relative to storage_dir
|
|
424
|
+
safe_name = _safe_dir_name(name)
|
|
425
|
+
graph_file = self.storage_dir / "namespaces" / safe_name / "graph.json"
|
|
426
|
+
|
|
427
|
+
# Ensure directory exists
|
|
428
|
+
graph_file.parent.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
|
|
430
|
+
# NOTE: Aliases are now query-scoped (stored in executor, not in graph)
|
|
431
|
+
# No need to save aliases to disk
|
|
432
|
+
|
|
433
|
+
node_count = len(graph.node_index) if hasattr(graph, 'node_index') else 0
|
|
434
|
+
logger.info(f"[REGISTRY SAVE] Saving graph '{name}': {node_count} nodes")
|
|
435
|
+
|
|
436
|
+
# Save graph data
|
|
437
|
+
success = graph.save(str(graph_file))
|
|
438
|
+
|
|
439
|
+
if success:
|
|
440
|
+
# Create metadata entry if it doesn't exist yet (e.g. graph
|
|
441
|
+
# populated via ingestion without an explicit create_graph call)
|
|
442
|
+
if name not in self.metadata:
|
|
443
|
+
self.metadata[name] = GraphMetadata(
|
|
444
|
+
name=name,
|
|
445
|
+
created_at=datetime.now(),
|
|
446
|
+
updated_at=datetime.now(),
|
|
447
|
+
num_nodes=0,
|
|
448
|
+
num_edges=0,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Update metadata
|
|
452
|
+
metadata = self.metadata[name]
|
|
453
|
+
metadata.updated_at = datetime.now()
|
|
454
|
+
|
|
455
|
+
# Get node and edge counts — try multiple sources, take max
|
|
456
|
+
node_count = 0
|
|
457
|
+
edge_count = 0
|
|
458
|
+
if hasattr(graph, 'csr_storage') and graph.csr_storage:
|
|
459
|
+
node_count = max(node_count, graph.csr_storage.get_node_count() if hasattr(graph.csr_storage, 'get_node_count') else 0)
|
|
460
|
+
edge_count = max(edge_count, graph.csr_storage.get_edge_count() if hasattr(graph.csr_storage, 'get_edge_count') else 0)
|
|
461
|
+
if hasattr(graph, 'node_index') and graph.node_index:
|
|
462
|
+
node_count = max(node_count, len(graph.node_index))
|
|
463
|
+
if hasattr(graph, 'edge_store') and graph.edge_store:
|
|
464
|
+
edge_count = max(edge_count, len(graph.edge_store))
|
|
465
|
+
metadata.num_nodes = node_count
|
|
466
|
+
metadata.num_edges = edge_count
|
|
467
|
+
|
|
468
|
+
# Check actual file size (may be .h5 instead of .json)
|
|
469
|
+
actual_file = graph_file if graph_file.exists() else graph_file.with_suffix('.h5')
|
|
470
|
+
if actual_file.exists():
|
|
471
|
+
metadata.size_mb = actual_file.stat().st_size / (1024 * 1024)
|
|
472
|
+
|
|
473
|
+
# Track file mtime so this worker knows it wrote this version
|
|
474
|
+
# Check both .json and .h5 since save() may produce either format
|
|
475
|
+
try:
|
|
476
|
+
actual_file = graph_file if graph_file.exists() else graph_file.with_suffix('.h5')
|
|
477
|
+
if not actual_file.exists():
|
|
478
|
+
# Also check the parent dir for graph.h5
|
|
479
|
+
h5_path = graph_file.parent / "graph.h5"
|
|
480
|
+
if h5_path.exists():
|
|
481
|
+
actual_file = h5_path
|
|
482
|
+
self._graph_file_mtimes[name] = actual_file.stat().st_mtime
|
|
483
|
+
except Exception:
|
|
484
|
+
pass # optional
|
|
485
|
+
|
|
486
|
+
# Save metadata
|
|
487
|
+
self._save_metadata()
|
|
488
|
+
|
|
489
|
+
# Create checkpoint (Git-like versioning) if enabled
|
|
490
|
+
if create_checkpoint:
|
|
491
|
+
try:
|
|
492
|
+
from .checkpoint import checkpoint_manager
|
|
493
|
+
checkpoint_manager.create_checkpoint(
|
|
494
|
+
namespace=safe_name,
|
|
495
|
+
graph_file=str(graph_file),
|
|
496
|
+
message="Auto-save checkpoint"
|
|
497
|
+
)
|
|
498
|
+
except Exception as e:
|
|
499
|
+
# Don't fail save if checkpoint fails
|
|
500
|
+
logger.warning(f"Failed to create checkpoint: {e}")
|
|
501
|
+
|
|
502
|
+
return True
|
|
503
|
+
|
|
504
|
+
return False
|
|
505
|
+
except Exception as e:
|
|
506
|
+
print(f"Error saving graph '{name}': {e}")
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
def load_graph(self, name: str) -> bool:
|
|
510
|
+
"""
|
|
511
|
+
Load a graph from disk.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
name: Name of the graph
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
True if successful
|
|
518
|
+
"""
|
|
519
|
+
return self._load_graph_from_disk(name) is not None
|
|
520
|
+
|
|
521
|
+
def _get_namespace_path(self, name: str) -> Optional[Path]:
|
|
522
|
+
"""Get the path to a namespace's graph file (if it exists) without loading.
|
|
523
|
+
|
|
524
|
+
Single deterministic lookup — relative to storage_dir.
|
|
525
|
+
Priority: namespace-centric JSON → namespace-centric H5 → old flat-structure fallback.
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
safe_name = _safe_dir_name(name)
|
|
529
|
+
ns_dir = self.storage_dir / "namespaces" / safe_name
|
|
530
|
+
candidates = [
|
|
531
|
+
ns_dir / "graph.json",
|
|
532
|
+
ns_dir / "graph.h5",
|
|
533
|
+
# Old flat structure fallback
|
|
534
|
+
self.storage_dir / safe_name / "graph.json",
|
|
535
|
+
self.storage_dir / safe_name / "graph.h5",
|
|
536
|
+
]
|
|
537
|
+
for c in candidates:
|
|
538
|
+
if c.exists():
|
|
539
|
+
return c
|
|
540
|
+
return None
|
|
541
|
+
except Exception as e:
|
|
542
|
+
logger.debug(f"Error getting namespace path for '{name}': {e}")
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
def _namespace_exists_on_disk(self, name: str) -> bool:
|
|
546
|
+
"""Check if namespace exists on disk (fast check, no loading)."""
|
|
547
|
+
graph_file = self._get_namespace_path(name)
|
|
548
|
+
return graph_file is not None and graph_file.exists()
|
|
549
|
+
|
|
550
|
+
def _load_graph_from_disk(self, name: str) -> Optional[AIContextDB]:
|
|
551
|
+
"""Load a graph from disk - supports both old and namespace-centric structures."""
|
|
552
|
+
try:
|
|
553
|
+
graph_file = self._get_namespace_path(name)
|
|
554
|
+
|
|
555
|
+
if not graph_file or not graph_file.exists():
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
# Create graph instance with CSR storage enabled by default
|
|
559
|
+
graph = AIContextDB(name, storage_backend='csr')
|
|
560
|
+
|
|
561
|
+
# Load graph data
|
|
562
|
+
success = graph.load(str(graph_file))
|
|
563
|
+
|
|
564
|
+
if success:
|
|
565
|
+
node_count = len(graph.node_index) if hasattr(graph, 'node_index') else 0
|
|
566
|
+
logger.debug(f"[LOAD] Loaded graph '{name}': {node_count} nodes")
|
|
567
|
+
|
|
568
|
+
# Store in registry
|
|
569
|
+
self.graphs[name] = graph
|
|
570
|
+
|
|
571
|
+
# Track file mtime for multi-worker change detection
|
|
572
|
+
try:
|
|
573
|
+
self._graph_file_mtimes[name] = graph_file.stat().st_mtime
|
|
574
|
+
except Exception:
|
|
575
|
+
pass # optional
|
|
576
|
+
|
|
577
|
+
# Update access metadata
|
|
578
|
+
self._update_access_metadata(name)
|
|
579
|
+
|
|
580
|
+
return graph
|
|
581
|
+
|
|
582
|
+
return None
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.error(f"Error loading graph '{name}': {e}")
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
def get_graph_for_request(self, name: str) -> AIContextDB:
|
|
588
|
+
"""Get graph instance for a request — delegates to get_graph().
|
|
589
|
+
|
|
590
|
+
Creates and caches a transient instance if the graph isn't in metadata
|
|
591
|
+
(e.g. Redis-backed graphs created at runtime). The cache ensures all
|
|
592
|
+
callers within the same process share the same instance — critical for
|
|
593
|
+
multi-agent experiments where agents must see each other's writes.
|
|
594
|
+
"""
|
|
595
|
+
graph = self.get_graph(name, load_if_missing=True)
|
|
596
|
+
if graph is None:
|
|
597
|
+
# Graph doesn't exist in metadata or on disk — create a transient one.
|
|
598
|
+
# Let AIContextDB pick its backend (Redis if available, else CSR).
|
|
599
|
+
graph = AIContextDB(name)
|
|
600
|
+
# Cache it so subsequent calls return the SAME instance
|
|
601
|
+
with self._lock:
|
|
602
|
+
self.graphs[name] = graph
|
|
603
|
+
return graph
|
|
604
|
+
|
|
605
|
+
def _update_access_metadata(self, name: str):
|
|
606
|
+
"""Update access metadata for a graph."""
|
|
607
|
+
if name in self.metadata:
|
|
608
|
+
metadata = self.metadata[name]
|
|
609
|
+
metadata.last_accessed = datetime.now()
|
|
610
|
+
metadata.access_count += 1
|
|
611
|
+
|
|
612
|
+
def _load_metadata(self):
|
|
613
|
+
"""Load metadata from LMDB (fast) or disk JSON (fallback)."""
|
|
614
|
+
# Try LMDB first — microsecond reads
|
|
615
|
+
if self._load_metadata_lmdb():
|
|
616
|
+
# Prune stale entries — skip Redis-backed graphs (they have no disk file by design)
|
|
617
|
+
stale = [
|
|
618
|
+
n for n in self.metadata
|
|
619
|
+
if self.metadata[n].storage_backend != "redis"
|
|
620
|
+
and not self._namespace_exists_on_disk(n)
|
|
621
|
+
]
|
|
622
|
+
for n in stale:
|
|
623
|
+
logger.debug(f"[REGISTRY] Pruning stale metadata entry '{n}' (no file on disk)")
|
|
624
|
+
del self.metadata[n]
|
|
625
|
+
if stale:
|
|
626
|
+
logger.info(f"[REGISTRY] Pruned {len(stale)} stale metadata entries")
|
|
627
|
+
# Write back to LMDB so they don't reload next startup
|
|
628
|
+
self._save_metadata_lmdb()
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
# Fallback: JSON file
|
|
632
|
+
metadata_file = self.storage_dir / "metadata.json"
|
|
633
|
+
|
|
634
|
+
if metadata_file.exists():
|
|
635
|
+
try:
|
|
636
|
+
with open(metadata_file, 'r') as f:
|
|
637
|
+
data = json.load(f)
|
|
638
|
+
|
|
639
|
+
for name, meta_data in data.items():
|
|
640
|
+
meta_data['created_at'] = datetime.fromisoformat(meta_data['created_at'])
|
|
641
|
+
meta_data['updated_at'] = datetime.fromisoformat(meta_data['updated_at'])
|
|
642
|
+
if meta_data.get('last_accessed'):
|
|
643
|
+
meta_data['last_accessed'] = datetime.fromisoformat(meta_data['last_accessed'])
|
|
644
|
+
self.metadata[name] = GraphMetadata(**meta_data)
|
|
645
|
+
|
|
646
|
+
# Prune stale entries — if graph file doesn't exist on disk, remove
|
|
647
|
+
# (skip Redis-backed graphs — they have no disk file by design)
|
|
648
|
+
stale = [
|
|
649
|
+
n for n in self.metadata
|
|
650
|
+
if self.metadata[n].storage_backend != "redis"
|
|
651
|
+
and not self._namespace_exists_on_disk(n)
|
|
652
|
+
]
|
|
653
|
+
for n in stale:
|
|
654
|
+
logger.debug(f"[REGISTRY] Pruning stale metadata entry '{n}' (no file on disk)")
|
|
655
|
+
del self.metadata[n]
|
|
656
|
+
if stale:
|
|
657
|
+
logger.info(f"[REGISTRY] Pruned {len(stale)} stale metadata entries")
|
|
658
|
+
self._save_metadata()
|
|
659
|
+
|
|
660
|
+
# Track file mtime so we can detect changes from other workers
|
|
661
|
+
if metadata_file.exists():
|
|
662
|
+
self._metadata_mtime = metadata_file.stat().st_mtime
|
|
663
|
+
except Exception as e:
|
|
664
|
+
print(f"Error loading metadata: {e}")
|
|
665
|
+
|
|
666
|
+
def _graph_file_is_newer(self, name: str) -> bool:
|
|
667
|
+
"""Check if another worker has saved a newer version of this graph to disk."""
|
|
668
|
+
try:
|
|
669
|
+
safe_name = _safe_dir_name(name)
|
|
670
|
+
base = self.storage_dir / "namespaces" / safe_name
|
|
671
|
+
# Check both .json and .h5 formats
|
|
672
|
+
for ext in ("graph.h5", "graph.json"):
|
|
673
|
+
graph_file = base / ext
|
|
674
|
+
if graph_file.exists():
|
|
675
|
+
current_mtime = graph_file.stat().st_mtime
|
|
676
|
+
last_known = self._graph_file_mtimes.get(name, 0.0)
|
|
677
|
+
if current_mtime > last_known:
|
|
678
|
+
return True
|
|
679
|
+
return False
|
|
680
|
+
except Exception:
|
|
681
|
+
return False
|
|
682
|
+
|
|
683
|
+
def _refresh_metadata_if_stale(self):
|
|
684
|
+
"""Re-read metadata.json if another worker has written to it.
|
|
685
|
+
|
|
686
|
+
Called before read operations (list_graphs, get_graph) so that
|
|
687
|
+
multi-worker deployments stay consistent without Redis.
|
|
688
|
+
"""
|
|
689
|
+
metadata_file = self.storage_dir / "metadata.json"
|
|
690
|
+
try:
|
|
691
|
+
if metadata_file.exists():
|
|
692
|
+
current_mtime = metadata_file.stat().st_mtime
|
|
693
|
+
if current_mtime > self._metadata_mtime:
|
|
694
|
+
logger.debug("[REGISTRY] metadata.json changed on disk — reloading")
|
|
695
|
+
old_names = set(self.metadata.keys())
|
|
696
|
+
self._load_metadata()
|
|
697
|
+
new_names = set(self.metadata.keys())
|
|
698
|
+
# Evict in-memory graphs that were deleted by another worker
|
|
699
|
+
for removed in old_names - new_names:
|
|
700
|
+
self.graphs.pop(removed, None)
|
|
701
|
+
except Exception:
|
|
702
|
+
pass # optional
|
|
703
|
+
|
|
704
|
+
def _save_metadata(self):
|
|
705
|
+
"""Save metadata to disk + LMDB."""
|
|
706
|
+
metadata_file = self.storage_dir / "metadata.json"
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
data = {}
|
|
710
|
+
for name, metadata in self.metadata.items():
|
|
711
|
+
meta_dict = asdict(metadata)
|
|
712
|
+
meta_dict['created_at'] = metadata.created_at.isoformat()
|
|
713
|
+
meta_dict['updated_at'] = metadata.updated_at.isoformat()
|
|
714
|
+
if metadata.last_accessed:
|
|
715
|
+
meta_dict['last_accessed'] = metadata.last_accessed.isoformat()
|
|
716
|
+
data[name] = meta_dict
|
|
717
|
+
|
|
718
|
+
with open(metadata_file, 'w', encoding='utf-8') as f:
|
|
719
|
+
json.dump(data, f, indent=2)
|
|
720
|
+
|
|
721
|
+
# Update tracked mtime so we don't re-read our own write
|
|
722
|
+
self._metadata_mtime = metadata_file.stat().st_mtime
|
|
723
|
+
except Exception as e:
|
|
724
|
+
print(f"Error saving metadata: {e}")
|
|
725
|
+
|
|
726
|
+
# Also persist to LMDB for fast reads
|
|
727
|
+
try:
|
|
728
|
+
self._save_metadata_lmdb()
|
|
729
|
+
except Exception:
|
|
730
|
+
pass
|
|
731
|
+
|
|
732
|
+
def _get_lmdb_meta_env(self):
|
|
733
|
+
"""Get or create LMDB environment for metadata."""
|
|
734
|
+
if not hasattr(self, '_lmdb_meta_env') or self._lmdb_meta_env is None:
|
|
735
|
+
try:
|
|
736
|
+
import lmdb
|
|
737
|
+
lmdb_path = str(self.storage_dir / "metadata.lmdb")
|
|
738
|
+
Path(lmdb_path).mkdir(parents=True, exist_ok=True)
|
|
739
|
+
self._lmdb_meta_env = lmdb.open(lmdb_path, map_size=64 * 1024 * 1024)
|
|
740
|
+
except Exception:
|
|
741
|
+
self._lmdb_meta_env = None
|
|
742
|
+
return self._lmdb_meta_env
|
|
743
|
+
|
|
744
|
+
def _save_metadata_lmdb(self):
|
|
745
|
+
"""Persist all metadata to LMDB, deleting entries for removed graphs."""
|
|
746
|
+
env = self._get_lmdb_meta_env()
|
|
747
|
+
if not env:
|
|
748
|
+
return
|
|
749
|
+
with env.begin(write=True) as txn:
|
|
750
|
+
# Collect existing LMDB keys so we can delete orphaned ones
|
|
751
|
+
existing_keys = {key.decode() for key, _ in txn.cursor()}
|
|
752
|
+
for name, metadata in self.metadata.items():
|
|
753
|
+
meta_dict = asdict(metadata)
|
|
754
|
+
meta_dict['created_at'] = metadata.created_at.isoformat()
|
|
755
|
+
meta_dict['updated_at'] = metadata.updated_at.isoformat()
|
|
756
|
+
if metadata.last_accessed:
|
|
757
|
+
meta_dict['last_accessed'] = metadata.last_accessed.isoformat()
|
|
758
|
+
txn.put(name.encode(), json.dumps(meta_dict).encode())
|
|
759
|
+
existing_keys.discard(name)
|
|
760
|
+
# Delete LMDB entries for graphs that no longer exist
|
|
761
|
+
for orphaned_key in existing_keys:
|
|
762
|
+
txn.delete(orphaned_key.encode())
|
|
763
|
+
logger.debug("[REGISTRY] Deleted orphaned LMDB entry: %s", orphaned_key)
|
|
764
|
+
|
|
765
|
+
def _load_metadata_lmdb(self) -> bool:
|
|
766
|
+
"""Try loading metadata from LMDB. Returns True if successful."""
|
|
767
|
+
env = self._get_lmdb_meta_env()
|
|
768
|
+
if not env:
|
|
769
|
+
return False
|
|
770
|
+
try:
|
|
771
|
+
with env.begin() as txn:
|
|
772
|
+
cursor = txn.cursor()
|
|
773
|
+
count = 0
|
|
774
|
+
for key, val in cursor:
|
|
775
|
+
name = key.decode()
|
|
776
|
+
meta_data = json.loads(val.decode())
|
|
777
|
+
meta_data['created_at'] = datetime.fromisoformat(meta_data['created_at'])
|
|
778
|
+
meta_data['updated_at'] = datetime.fromisoformat(meta_data['updated_at'])
|
|
779
|
+
if meta_data.get('last_accessed'):
|
|
780
|
+
meta_data['last_accessed'] = datetime.fromisoformat(meta_data['last_accessed'])
|
|
781
|
+
self.metadata[name] = GraphMetadata(**meta_data)
|
|
782
|
+
count += 1
|
|
783
|
+
if count > 0:
|
|
784
|
+
logger.info("[REGISTRY] Loaded %d graph metadata from LMDB", count)
|
|
785
|
+
return True
|
|
786
|
+
except Exception as e:
|
|
787
|
+
logger.debug("[REGISTRY] LMDB metadata load failed: %s", e)
|
|
788
|
+
return False
|
|
789
|
+
|
|
790
|
+
def _auto_cleanup(self):
|
|
791
|
+
"""Evict LRU in-memory graphs when over max_graphs limit.
|
|
792
|
+
|
|
793
|
+
Called internally after loading/creating graphs. Assumes the caller
|
|
794
|
+
already holds ``self._lock``.
|
|
795
|
+
"""
|
|
796
|
+
max_graphs = self.cache_config['max_graphs']
|
|
797
|
+
if len(self.graphs) <= max_graphs:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
# Sort loaded graphs by last_accessed (oldest first)
|
|
801
|
+
sorted_graphs = sorted(
|
|
802
|
+
[(name, self.metadata[name].last_accessed or self.metadata[name].created_at)
|
|
803
|
+
for name in self.graphs
|
|
804
|
+
if name in self.metadata],
|
|
805
|
+
key=lambda x: x[1]
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
excess = len(self.graphs) - max_graphs
|
|
809
|
+
for name, _ in sorted_graphs[:excess]:
|
|
810
|
+
# Save to disk before evicting so in-memory changes aren't lost
|
|
811
|
+
try:
|
|
812
|
+
self.save_graph(name, create_checkpoint=False)
|
|
813
|
+
except Exception:
|
|
814
|
+
logger.warning(f"[REGISTRY] Failed to save graph '{name}' before eviction")
|
|
815
|
+
logger.info(f"[REGISTRY] Auto-evicting LRU graph '{name}' from memory")
|
|
816
|
+
del self.graphs[name]
|
|
817
|
+
|
|
818
|
+
def cleanup_cache(self):
|
|
819
|
+
"""Clean up cache based on configuration (public, acquires lock)."""
|
|
820
|
+
with self._lock:
|
|
821
|
+
# Remove graphs that haven't been accessed recently
|
|
822
|
+
cutoff_time = datetime.now() - timedelta(hours=self.cache_config['ttl_hours'])
|
|
823
|
+
|
|
824
|
+
graphs_to_remove = []
|
|
825
|
+
for name, metadata in self.metadata.items():
|
|
826
|
+
if (name in self.graphs and
|
|
827
|
+
metadata.last_accessed and
|
|
828
|
+
metadata.last_accessed < cutoff_time):
|
|
829
|
+
graphs_to_remove.append(name)
|
|
830
|
+
|
|
831
|
+
for name in graphs_to_remove:
|
|
832
|
+
if name in self.graphs:
|
|
833
|
+
del self.graphs[name]
|
|
834
|
+
|
|
835
|
+
# Delegate count/size eviction to _auto_cleanup
|
|
836
|
+
self._auto_cleanup()
|
|
837
|
+
|
|
838
|
+
# Check total size
|
|
839
|
+
total_size = sum(metadata.size_mb for metadata in self.metadata.values())
|
|
840
|
+
if total_size > self.cache_config['max_size_mb']:
|
|
841
|
+
sorted_by_size = sorted(
|
|
842
|
+
[(name, metadata.size_mb) for name, metadata in self.metadata.items()],
|
|
843
|
+
key=lambda x: x[1],
|
|
844
|
+
reverse=True
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
current_size = total_size
|
|
848
|
+
for name, size in sorted_by_size:
|
|
849
|
+
if current_size <= self.cache_config['max_size_mb']:
|
|
850
|
+
break
|
|
851
|
+
if name in self.graphs:
|
|
852
|
+
del self.graphs[name]
|
|
853
|
+
current_size -= size
|
|
854
|
+
|
|
855
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
856
|
+
"""Get registry statistics."""
|
|
857
|
+
return {
|
|
858
|
+
'total_graphs': len(self.metadata),
|
|
859
|
+
'loaded_graphs': len(self.graphs),
|
|
860
|
+
'total_nodes': sum(metadata.num_nodes for metadata in self.metadata.values()),
|
|
861
|
+
'total_edges': sum(metadata.num_edges for metadata in self.metadata.values()),
|
|
862
|
+
'total_size_mb': sum(metadata.size_mb for metadata in self.metadata.values()),
|
|
863
|
+
'cache_config': self.cache_config
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
def search_graphs(self, query: str, tags: List[str] = None) -> List[Dict[str, Any]]:
|
|
867
|
+
"""
|
|
868
|
+
Search graphs by name, tags, or metadata.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
query: Search query
|
|
872
|
+
tags: Optional tags to filter by
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
List of matching graph metadata
|
|
876
|
+
"""
|
|
877
|
+
results = []
|
|
878
|
+
query_lower = query.lower()
|
|
879
|
+
|
|
880
|
+
for name, metadata in self.metadata.items():
|
|
881
|
+
# Check name match
|
|
882
|
+
if query_lower in name.lower():
|
|
883
|
+
results.append(name)
|
|
884
|
+
continue
|
|
885
|
+
|
|
886
|
+
# Check tag match
|
|
887
|
+
if tags:
|
|
888
|
+
if any(tag in metadata.tags for tag in tags):
|
|
889
|
+
results.append(name)
|
|
890
|
+
continue
|
|
891
|
+
|
|
892
|
+
# Check metadata match
|
|
893
|
+
if (query_lower in str(metadata.num_nodes) or
|
|
894
|
+
query_lower in str(metadata.num_edges) or
|
|
895
|
+
query_lower in metadata.embedding_model.lower() if metadata.embedding_model else False):
|
|
896
|
+
results.append(name)
|
|
897
|
+
|
|
898
|
+
# Return full metadata for matches
|
|
899
|
+
return [asdict(self.metadata[name]) for name in results]
|
|
900
|
+
|
|
901
|
+
# ── Property Indexes ─────────────────────────────────────────────────
|
|
902
|
+
# Lightweight in-memory indexes built incrementally on add_node calls.
|
|
903
|
+
# Structure: {graph_name: {field: {value: set(node_ids)}}}
|
|
904
|
+
_INDEXED_FIELDS = ("node_type", "_sentiment", "stock_symbol", "portfolio_id", "source_type")
|
|
905
|
+
|
|
906
|
+
def _ensure_property_index(self, graph_name: str) -> Dict[str, Dict[str, set]]:
|
|
907
|
+
"""Get or create the property index dict for a graph."""
|
|
908
|
+
if not hasattr(self, '_property_indexes'):
|
|
909
|
+
self._property_indexes: Dict[str, Dict[str, Dict[str, set]]] = {}
|
|
910
|
+
if graph_name not in self._property_indexes:
|
|
911
|
+
self._property_indexes[graph_name] = {f: {} for f in self._INDEXED_FIELDS}
|
|
912
|
+
return self._property_indexes[graph_name]
|
|
913
|
+
|
|
914
|
+
def index_node(self, graph_name: str, node_id: str, label: str = "", properties: Dict = None):
|
|
915
|
+
"""Index a node's key properties for O(1) lookup.
|
|
916
|
+
|
|
917
|
+
Called incrementally when nodes are added — not a retroactive rebuild.
|
|
918
|
+
"""
|
|
919
|
+
idx = self._ensure_property_index(graph_name)
|
|
920
|
+
# Index by node_type / label
|
|
921
|
+
if label:
|
|
922
|
+
idx["node_type"].setdefault(label, set()).add(node_id)
|
|
923
|
+
if properties:
|
|
924
|
+
for field in self._INDEXED_FIELDS:
|
|
925
|
+
if field == "node_type":
|
|
926
|
+
continue # handled above via label
|
|
927
|
+
val = properties.get(field)
|
|
928
|
+
if val is not None:
|
|
929
|
+
idx[field].setdefault(str(val), set()).add(node_id)
|
|
930
|
+
|
|
931
|
+
def query_nodes(self, graph_name: str, node_type: str = None,
|
|
932
|
+
properties: Dict = None, limit: int = 100) -> List:
|
|
933
|
+
"""Query nodes with indexed lookup instead of full scan.
|
|
934
|
+
|
|
935
|
+
Uses indexes for: node_type, _sentiment, stock_symbol, portfolio_id, source_type.
|
|
936
|
+
Falls back to scan if no index exists for the requested fields.
|
|
937
|
+
"""
|
|
938
|
+
graph = self.get_graph(graph_name, load_if_missing=True)
|
|
939
|
+
if not graph:
|
|
940
|
+
return []
|
|
941
|
+
|
|
942
|
+
# Try indexed path first
|
|
943
|
+
idx = self._ensure_property_index(graph_name)
|
|
944
|
+
candidate_ids = None
|
|
945
|
+
|
|
946
|
+
# Narrow by node_type index
|
|
947
|
+
if node_type and "node_type" in idx and node_type in idx["node_type"]:
|
|
948
|
+
candidate_ids = set(idx["node_type"][node_type])
|
|
949
|
+
|
|
950
|
+
# Narrow by property indexes
|
|
951
|
+
if properties:
|
|
952
|
+
for field, val in properties.items():
|
|
953
|
+
if field in idx and str(val) in idx[field]:
|
|
954
|
+
matching = idx[field][str(val)]
|
|
955
|
+
if candidate_ids is None:
|
|
956
|
+
candidate_ids = set(matching)
|
|
957
|
+
else:
|
|
958
|
+
candidate_ids &= matching
|
|
959
|
+
|
|
960
|
+
# If we have indexed candidates, resolve to nodes
|
|
961
|
+
if candidate_ids is not None:
|
|
962
|
+
results = []
|
|
963
|
+
adapter = getattr(graph, 'csr_adapter', graph)
|
|
964
|
+
csr = getattr(adapter, 'csr_storage', None)
|
|
965
|
+
for nid in list(candidate_ids)[:limit]:
|
|
966
|
+
node = None
|
|
967
|
+
if csr and hasattr(csr, 'get_node'):
|
|
968
|
+
node = csr.get_node(nid)
|
|
969
|
+
elif hasattr(graph, 'node_index') and nid in graph.node_index:
|
|
970
|
+
node = graph.node_index[nid]
|
|
971
|
+
if node:
|
|
972
|
+
results.append(node)
|
|
973
|
+
return results
|
|
974
|
+
|
|
975
|
+
# Fallback: use get_nodes_by_type if only node_type is requested
|
|
976
|
+
if node_type and not properties:
|
|
977
|
+
return self.get_nodes_by_type(graph_name, node_type, limit=limit)
|
|
978
|
+
|
|
979
|
+
# Final fallback: full scan with filter
|
|
980
|
+
return self._scan_nodes(graph, node_type=node_type, properties=properties, limit=limit)
|
|
981
|
+
|
|
982
|
+
def get_nodes_by_type(self, graph_name: str, node_type: str, limit: int = 100) -> List:
|
|
983
|
+
"""Get nodes by type using CSR adapter's type storage if available,
|
|
984
|
+
falling back to filtered scan."""
|
|
985
|
+
graph = self.get_graph(graph_name, load_if_missing=True)
|
|
986
|
+
if not graph:
|
|
987
|
+
return []
|
|
988
|
+
|
|
989
|
+
# Try CSR adapter's typed storage
|
|
990
|
+
adapter = getattr(graph, 'csr_adapter', graph)
|
|
991
|
+
csr = getattr(adapter, 'csr_storage', None)
|
|
992
|
+
if csr and hasattr(csr, 'get_all_nodes'):
|
|
993
|
+
try:
|
|
994
|
+
nodes = csr.get_all_nodes(node_type=node_type)
|
|
995
|
+
return nodes[:limit]
|
|
996
|
+
except TypeError:
|
|
997
|
+
# get_all_nodes may not accept node_type kwarg
|
|
998
|
+
pass
|
|
999
|
+
|
|
1000
|
+
# Fallback: property index
|
|
1001
|
+
idx = self._ensure_property_index(graph_name)
|
|
1002
|
+
if node_type in idx.get("node_type", {}):
|
|
1003
|
+
ids = list(idx["node_type"][node_type])[:limit]
|
|
1004
|
+
results = []
|
|
1005
|
+
for nid in ids:
|
|
1006
|
+
if hasattr(graph, 'node_index') and nid in graph.node_index:
|
|
1007
|
+
results.append(graph.node_index[nid])
|
|
1008
|
+
elif csr and hasattr(csr, 'get_node'):
|
|
1009
|
+
node = csr.get_node(nid)
|
|
1010
|
+
if node:
|
|
1011
|
+
results.append(node)
|
|
1012
|
+
return results
|
|
1013
|
+
|
|
1014
|
+
# Final fallback: scan
|
|
1015
|
+
return self._scan_nodes(graph, node_type=node_type, limit=limit)
|
|
1016
|
+
|
|
1017
|
+
def _scan_nodes(self, graph, node_type: str = None,
|
|
1018
|
+
properties: Dict = None, limit: int = 100) -> List:
|
|
1019
|
+
"""Full scan with optional filters (last resort)."""
|
|
1020
|
+
results = []
|
|
1021
|
+
all_nodes = []
|
|
1022
|
+
if hasattr(graph, 'node_index') and graph.node_index:
|
|
1023
|
+
all_nodes = list(graph.node_index.values())
|
|
1024
|
+
else:
|
|
1025
|
+
adapter = getattr(graph, 'csr_adapter', graph)
|
|
1026
|
+
if hasattr(adapter, 'get_all_nodes'):
|
|
1027
|
+
all_nodes = adapter.get_all_nodes()
|
|
1028
|
+
|
|
1029
|
+
for node in all_nodes:
|
|
1030
|
+
if len(results) >= limit:
|
|
1031
|
+
break
|
|
1032
|
+
label = getattr(node, 'label', getattr(node, 'node_type', ''))
|
|
1033
|
+
if node_type and label != node_type:
|
|
1034
|
+
continue
|
|
1035
|
+
if properties:
|
|
1036
|
+
props = getattr(node, 'properties', {})
|
|
1037
|
+
if not all(props.get(k) == v for k, v in properties.items()):
|
|
1038
|
+
continue
|
|
1039
|
+
results.append(node)
|
|
1040
|
+
return results
|
|
1041
|
+
|
|
1042
|
+
# Global singleton instance for backward compatibility
|
|
1043
|
+
graph_registry = GraphRegistry()
|