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,1619 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context-as-a-Service API Router
|
|
3
|
+
================================
|
|
4
|
+
All ``/context/`` endpoints for multi-agent shared context.
|
|
5
|
+
|
|
6
|
+
Mount on the main FastAPI app with::
|
|
7
|
+
|
|
8
|
+
from contextsynapse.api.context_router import create_context_router
|
|
9
|
+
app.include_router(create_context_router(graph_registry))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import logging
|
|
16
|
+
import secrets
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from fastapi import (
|
|
20
|
+
APIRouter,
|
|
21
|
+
BackgroundTasks,
|
|
22
|
+
Depends,
|
|
23
|
+
HTTPException,
|
|
24
|
+
Query,
|
|
25
|
+
UploadFile,
|
|
26
|
+
File,
|
|
27
|
+
WebSocket,
|
|
28
|
+
WebSocketDisconnect,
|
|
29
|
+
)
|
|
30
|
+
from pydantic import BaseModel, Field
|
|
31
|
+
|
|
32
|
+
from .auth import AgentAuth
|
|
33
|
+
from ..context.agents import AgentIdentity, ProvenanceRecord
|
|
34
|
+
from ..context.store_factory import create_agent_registry
|
|
35
|
+
from ..context.session import ContextSessionManager
|
|
36
|
+
from ..context.hub import ContextHub, ContextItem, ContextRole
|
|
37
|
+
from ..context.blob import BlobStore
|
|
38
|
+
from ..context.ingest import UnifiedIngestor
|
|
39
|
+
from ..context.document_processor import DocumentProcessor
|
|
40
|
+
from ..context.pubsub import ContextEvent, PubSubHub, pubsub_hub
|
|
41
|
+
from ..context.vector_integration import SessionVectorStore
|
|
42
|
+
from ..context.dedup import ContextDedup
|
|
43
|
+
from ..context.scoping import ContextScoper, ScopingConfig
|
|
44
|
+
from ..context.conversation import ConversationStore
|
|
45
|
+
from ..context.embedding_hooks import EmbeddingHooks
|
|
46
|
+
from ..context.session_graph import SessionGraphBuilder
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ======================================================================
|
|
52
|
+
# Pydantic request/response models
|
|
53
|
+
# ======================================================================
|
|
54
|
+
|
|
55
|
+
class AgentRegisterRequest(BaseModel):
|
|
56
|
+
name: str
|
|
57
|
+
role: str = "agent"
|
|
58
|
+
platform: str = "app" # desktop | mobile | app | browser | embedded
|
|
59
|
+
capabilities: List[str] = Field(default_factory=lambda: ["read", "write"])
|
|
60
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
class AgentResponse(BaseModel):
|
|
63
|
+
agent_id: str
|
|
64
|
+
name: str
|
|
65
|
+
role: str
|
|
66
|
+
platform: str = "app"
|
|
67
|
+
capabilities: List[str]
|
|
68
|
+
status: str = "active"
|
|
69
|
+
created_at: str
|
|
70
|
+
last_seen: Optional[str] = None
|
|
71
|
+
metadata: Dict[str, Any]
|
|
72
|
+
|
|
73
|
+
class AgentRegisterResponse(AgentResponse):
|
|
74
|
+
api_key: str # returned only on registration
|
|
75
|
+
|
|
76
|
+
class SessionCreateRequest(BaseModel):
|
|
77
|
+
name: str
|
|
78
|
+
agent_id: Optional[str] = None
|
|
79
|
+
context_id: Optional[str] = None # auto-attach context (e.g. SDLC context)
|
|
80
|
+
goal: Optional[str] = None # session goal / objective
|
|
81
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
class SessionResponse(BaseModel):
|
|
84
|
+
session_id: str
|
|
85
|
+
name: str
|
|
86
|
+
created_at: str
|
|
87
|
+
updated_at: str
|
|
88
|
+
owner_agent_id: Optional[str]
|
|
89
|
+
graph_namespace: str
|
|
90
|
+
vector_collection: str
|
|
91
|
+
document_collection: str
|
|
92
|
+
config: Dict[str, Any]
|
|
93
|
+
status: str
|
|
94
|
+
|
|
95
|
+
class AccessRequest(BaseModel):
|
|
96
|
+
agent_id: str
|
|
97
|
+
level: str = "read" # read, write, admin
|
|
98
|
+
allowed_tags: List[str] = Field(default_factory=list) # extra tags this agent may see
|
|
99
|
+
|
|
100
|
+
class IngestRequest(BaseModel):
|
|
101
|
+
data: Any
|
|
102
|
+
agent_id: str
|
|
103
|
+
data_type: str = "auto"
|
|
104
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
105
|
+
source: Optional[str] = None
|
|
106
|
+
mime_type: Optional[str] = None
|
|
107
|
+
filename: Optional[str] = None
|
|
108
|
+
tags: List[str] = Field(default_factory=list)
|
|
109
|
+
sensitivity: Optional[str] = None # public|internal|confidential|restricted
|
|
110
|
+
auto_pii_scan: bool = True
|
|
111
|
+
|
|
112
|
+
class ContextItemRequest(BaseModel):
|
|
113
|
+
content: str
|
|
114
|
+
agent_id: str
|
|
115
|
+
role: str = "user"
|
|
116
|
+
label: Optional[str] = None
|
|
117
|
+
source: Optional[str] = None
|
|
118
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
119
|
+
tags: List[str] = Field(default_factory=list)
|
|
120
|
+
sensitivity: str = "public"
|
|
121
|
+
|
|
122
|
+
class DocumentProcessRequest(BaseModel):
|
|
123
|
+
file_path: str
|
|
124
|
+
agent_id: str
|
|
125
|
+
extract_tables: bool = True
|
|
126
|
+
extract_images: bool = True
|
|
127
|
+
chunk_size: Optional[int] = None
|
|
128
|
+
chunk_overlap: Optional[int] = None
|
|
129
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
130
|
+
|
|
131
|
+
class ContributeRequest(BaseModel):
|
|
132
|
+
content: str
|
|
133
|
+
agent_id: Optional[str] = None # auto-filled from auth if omitted
|
|
134
|
+
role: str = "generated" # generated, synthesis, decision
|
|
135
|
+
label: Optional[str] = None
|
|
136
|
+
source_ids: List[str] = Field(default_factory=list)
|
|
137
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
138
|
+
tags: List[str] = Field(default_factory=list)
|
|
139
|
+
sensitivity: str = "internal"
|
|
140
|
+
|
|
141
|
+
class QueryRequest(BaseModel):
|
|
142
|
+
query: str
|
|
143
|
+
agent_id: Optional[str] = None
|
|
144
|
+
limit: int = 50
|
|
145
|
+
|
|
146
|
+
class SemanticSearchRequest(BaseModel):
|
|
147
|
+
query: str
|
|
148
|
+
k: int = 10
|
|
149
|
+
agent_id: Optional[str] = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --- Phase 2 models ---
|
|
153
|
+
|
|
154
|
+
class ScopingConfigRequest(BaseModel):
|
|
155
|
+
max_tokens: int = 8000
|
|
156
|
+
strategy: str = "combined"
|
|
157
|
+
role_weights: Dict[str, float] = Field(default_factory=dict)
|
|
158
|
+
recency_decay: float = 0.95
|
|
159
|
+
reserve_system: bool = True
|
|
160
|
+
|
|
161
|
+
class ConversationCreateRequest(BaseModel):
|
|
162
|
+
session_id: str
|
|
163
|
+
title: Optional[str] = None
|
|
164
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
165
|
+
ttl_seconds: Optional[int] = None
|
|
166
|
+
|
|
167
|
+
class ConversationMessageRequest(BaseModel):
|
|
168
|
+
role: str # user | assistant | system | tool
|
|
169
|
+
content: str
|
|
170
|
+
agent_id: Optional[str] = None
|
|
171
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
172
|
+
|
|
173
|
+
class ConversationKVRequest(BaseModel):
|
|
174
|
+
key: str
|
|
175
|
+
value: Any
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ======================================================================
|
|
179
|
+
# Router factory
|
|
180
|
+
# ======================================================================
|
|
181
|
+
|
|
182
|
+
def create_context_router(graph_registry=None) -> APIRouter:
|
|
183
|
+
"""
|
|
184
|
+
Build and return the ``/context`` router.
|
|
185
|
+
|
|
186
|
+
Instantiates the shared singletons (AgentRegistry, SessionManager, etc.)
|
|
187
|
+
so they live for the lifetime of the FastAPI app.
|
|
188
|
+
"""
|
|
189
|
+
router = APIRouter(prefix="/context", tags=["Context Service"])
|
|
190
|
+
|
|
191
|
+
# Shared state
|
|
192
|
+
agent_registry = create_agent_registry()
|
|
193
|
+
session_manager = ContextSessionManager(graph_registry=graph_registry)
|
|
194
|
+
blob_store = BlobStore()
|
|
195
|
+
|
|
196
|
+
# Vector store — auto-embed on ingest, semantic search
|
|
197
|
+
vector_store = SessionVectorStore()
|
|
198
|
+
dedup = ContextDedup(vector_store=vector_store)
|
|
199
|
+
|
|
200
|
+
# Phase 2: embedding hooks, conversation store
|
|
201
|
+
embedding_hooks = EmbeddingHooks(vector_store=vector_store)
|
|
202
|
+
conversation_store = ConversationStore()
|
|
203
|
+
|
|
204
|
+
ingestor = UnifiedIngestor(
|
|
205
|
+
session_manager=session_manager,
|
|
206
|
+
agent_registry=agent_registry,
|
|
207
|
+
blob_store=blob_store,
|
|
208
|
+
embedding_hooks=embedding_hooks,
|
|
209
|
+
)
|
|
210
|
+
doc_processor = DocumentProcessor(
|
|
211
|
+
session_manager=session_manager,
|
|
212
|
+
agent_registry=agent_registry,
|
|
213
|
+
graph_registry=graph_registry,
|
|
214
|
+
blob_store=blob_store,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Per-session scoping configs
|
|
218
|
+
_scoping_configs: Dict[str, ScopingConfig] = {}
|
|
219
|
+
|
|
220
|
+
# Session graph builders (session_id -> SessionGraphBuilder)
|
|
221
|
+
_session_builders: Dict[str, SessionGraphBuilder] = {}
|
|
222
|
+
|
|
223
|
+
async def _get_or_create_builder(session_id: str) -> SessionGraphBuilder:
|
|
224
|
+
"""Lazily get or create a SessionGraphBuilder for a session."""
|
|
225
|
+
if session_id not in _session_builders:
|
|
226
|
+
session = session_manager.get_session(session_id)
|
|
227
|
+
if session:
|
|
228
|
+
builder = SessionGraphBuilder(session, graph_registry)
|
|
229
|
+
await builder.initialize()
|
|
230
|
+
_session_builders[session_id] = builder
|
|
231
|
+
return _session_builders.get(session_id)
|
|
232
|
+
|
|
233
|
+
# Auth dependency — inject into protected endpoints
|
|
234
|
+
from .auth import require_agent_capability
|
|
235
|
+
require_agent = AgentAuth(agent_registry)
|
|
236
|
+
|
|
237
|
+
# RBAC: capability-gated dependencies
|
|
238
|
+
require_read = require_agent_capability(require_agent, "read")
|
|
239
|
+
require_write = require_agent_capability(require_agent, "read", "write")
|
|
240
|
+
require_agent_admin = require_agent_capability(require_agent, "read", "write", "admin")
|
|
241
|
+
|
|
242
|
+
# Per-session ContextHub cache (session_id -> ContextHub)
|
|
243
|
+
_hubs: Dict[str, ContextHub] = {}
|
|
244
|
+
|
|
245
|
+
def _get_hub(session_id: str) -> ContextHub:
|
|
246
|
+
if session_id not in _hubs:
|
|
247
|
+
_hubs[session_id] = ContextHub()
|
|
248
|
+
return _hubs[session_id]
|
|
249
|
+
|
|
250
|
+
# ==================================================================
|
|
251
|
+
# Agent endpoints (registration is PUBLIC, others require auth)
|
|
252
|
+
# ==================================================================
|
|
253
|
+
|
|
254
|
+
@router.post("/agents", response_model=AgentRegisterResponse)
|
|
255
|
+
async def register_agent(req: AgentRegisterRequest):
|
|
256
|
+
agent, api_key = agent_registry.register(
|
|
257
|
+
name=req.name,
|
|
258
|
+
role=req.role,
|
|
259
|
+
platform=req.platform,
|
|
260
|
+
capabilities=req.capabilities,
|
|
261
|
+
metadata=req.metadata,
|
|
262
|
+
)
|
|
263
|
+
# Return composite key: "<agent_id>:<secret>"
|
|
264
|
+
# Clients use this as: Authorization: Bearer <agent_id>:<secret>
|
|
265
|
+
composite_key = f"{agent.agent_id}:{api_key}"
|
|
266
|
+
return AgentRegisterResponse(
|
|
267
|
+
**agent.to_dict(),
|
|
268
|
+
api_key=composite_key,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@router.get("/agents", response_model=List[AgentResponse])
|
|
272
|
+
async def list_agents(caller: AgentIdentity = Depends(require_read)):
|
|
273
|
+
agents = agent_registry.list_agents()
|
|
274
|
+
return [AgentResponse(**a.to_dict()) for a in agents]
|
|
275
|
+
|
|
276
|
+
@router.get("/agents/{agent_id}", response_model=AgentResponse)
|
|
277
|
+
async def get_agent(agent_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
278
|
+
agent = agent_registry.get(agent_id)
|
|
279
|
+
if not agent:
|
|
280
|
+
raise HTTPException(404, "Agent not found")
|
|
281
|
+
return AgentResponse(**agent.to_dict())
|
|
282
|
+
|
|
283
|
+
@router.delete("/agents/{agent_id}")
|
|
284
|
+
async def deregister_agent(agent_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
285
|
+
# Only allow self-deregistration or admin
|
|
286
|
+
if caller.agent_id != agent_id and caller.role != "admin":
|
|
287
|
+
raise HTTPException(403, "Can only deregister yourself unless admin")
|
|
288
|
+
ok = agent_registry.deregister(agent_id)
|
|
289
|
+
if not ok:
|
|
290
|
+
raise HTTPException(404, "Agent not found")
|
|
291
|
+
return {"success": True, "message": f"Agent {agent_id} deregistered"}
|
|
292
|
+
|
|
293
|
+
# ==================================================================
|
|
294
|
+
# Session endpoints
|
|
295
|
+
# ==================================================================
|
|
296
|
+
|
|
297
|
+
@router.post("/sessions", response_model=SessionResponse)
|
|
298
|
+
async def create_session(req: SessionCreateRequest, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
299
|
+
# Merge goal into config if provided
|
|
300
|
+
config = dict(req.config)
|
|
301
|
+
if req.goal:
|
|
302
|
+
config["goal"] = req.goal
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
session = session_manager.create_session(
|
|
306
|
+
name=req.name,
|
|
307
|
+
owner_agent_id=req.agent_id or caller.agent_id,
|
|
308
|
+
config=config,
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
raise HTTPException(400, str(e))
|
|
312
|
+
|
|
313
|
+
# Initialize session graph builder (unified knowledge + conversation layer)
|
|
314
|
+
try:
|
|
315
|
+
builder = SessionGraphBuilder(session, graph_registry)
|
|
316
|
+
await builder.initialize()
|
|
317
|
+
_session_builders[session.session_id] = builder
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.warning("Failed to initialize session graph builder: %s", e)
|
|
320
|
+
|
|
321
|
+
# Auto-attach context and create thread if context_id provided
|
|
322
|
+
if req.context_id:
|
|
323
|
+
try:
|
|
324
|
+
session_manager.attach_context(session.session_id, req.context_id)
|
|
325
|
+
|
|
326
|
+
# Create thread conversation
|
|
327
|
+
thread = conversation_store.create(
|
|
328
|
+
session_id=session.session_id,
|
|
329
|
+
title="Thread",
|
|
330
|
+
metadata={"type": "sdlc_thread", "context_id": req.context_id, "goal": req.goal or ""},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Store thread_id in session config
|
|
334
|
+
config["thread_id"] = thread.conversation_id
|
|
335
|
+
config["context_id"] = req.context_id
|
|
336
|
+
session_manager.update_session(session.session_id, config=config)
|
|
337
|
+
|
|
338
|
+
# Post initial system message with context stats
|
|
339
|
+
doc_count = 0
|
|
340
|
+
chunk_count = 0
|
|
341
|
+
ctx_name = req.context_id
|
|
342
|
+
try:
|
|
343
|
+
ns = session.graph_namespace
|
|
344
|
+
db = graph_registry.get_graph(ns, load_if_missing=True)
|
|
345
|
+
if db:
|
|
346
|
+
for n in db.get_all_nodes():
|
|
347
|
+
if n.label in ("Document", "Source"):
|
|
348
|
+
doc_count += 1
|
|
349
|
+
elif n.label in ("TextChunk", "Chunk", "Passage"):
|
|
350
|
+
chunk_count += 1
|
|
351
|
+
except Exception:
|
|
352
|
+
pass # optional
|
|
353
|
+
|
|
354
|
+
goal_text = f" Goal: {req.goal}" if req.goal else ""
|
|
355
|
+
conversation_store.append_message(
|
|
356
|
+
conversation_id=thread.conversation_id,
|
|
357
|
+
role="system",
|
|
358
|
+
content=(
|
|
359
|
+
f"Session started.{goal_text} "
|
|
360
|
+
f"Context '{ctx_name}' attached with {doc_count} documents, "
|
|
361
|
+
f"{chunk_count} chunks available via RAG."
|
|
362
|
+
),
|
|
363
|
+
agent_id=None,
|
|
364
|
+
metadata={"type": "system_event", "event": "session_started"},
|
|
365
|
+
)
|
|
366
|
+
logger.info("Auto-created thread %s for session %s", thread.conversation_id, session.session_id)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.warning("Failed to auto-create thread for session: %s", e)
|
|
369
|
+
|
|
370
|
+
return SessionResponse(**session.to_dict())
|
|
371
|
+
|
|
372
|
+
@router.get("/sessions", response_model=List[SessionResponse])
|
|
373
|
+
async def list_sessions(agent_id: Optional[str] = Query(None), caller: AgentIdentity = Depends(require_read)):
|
|
374
|
+
sessions = session_manager.list_sessions(agent_id=agent_id)
|
|
375
|
+
return [SessionResponse(**s.to_dict()) for s in sessions]
|
|
376
|
+
|
|
377
|
+
@router.get("/sessions/{session_id}", response_model=SessionResponse)
|
|
378
|
+
async def get_session(session_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
379
|
+
session = session_manager.get_session(session_id)
|
|
380
|
+
if not session:
|
|
381
|
+
raise HTTPException(404, "Session not found")
|
|
382
|
+
return SessionResponse(**session.to_dict())
|
|
383
|
+
|
|
384
|
+
@router.delete("/sessions/{session_id}")
|
|
385
|
+
async def delete_session(session_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
386
|
+
ok = session_manager.delete_session(session_id)
|
|
387
|
+
if not ok:
|
|
388
|
+
raise HTTPException(404, "Session not found")
|
|
389
|
+
_hubs.pop(session_id, None)
|
|
390
|
+
vector_store.delete_session(session_id)
|
|
391
|
+
return {"success": True, "message": "Session deleted"}
|
|
392
|
+
|
|
393
|
+
# --- access control ---
|
|
394
|
+
|
|
395
|
+
@router.post("/sessions/{session_id}/access")
|
|
396
|
+
async def grant_access(session_id: str, req: AccessRequest, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
397
|
+
session = session_manager.get_session(session_id)
|
|
398
|
+
if not session:
|
|
399
|
+
raise HTTPException(404, "Session not found")
|
|
400
|
+
session_manager.grant_access(
|
|
401
|
+
session_id, req.agent_id, req.level, allowed_tags=req.allowed_tags,
|
|
402
|
+
agent_registry=agent_registry,
|
|
403
|
+
)
|
|
404
|
+
return {
|
|
405
|
+
"success": True,
|
|
406
|
+
"agent_id": req.agent_id,
|
|
407
|
+
"level": req.level,
|
|
408
|
+
"allowed_tags": req.allowed_tags,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@router.delete("/sessions/{session_id}/access/{agent_id}")
|
|
412
|
+
async def revoke_access(session_id: str, agent_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
413
|
+
ok = session_manager.revoke_access(session_id, agent_id)
|
|
414
|
+
if not ok:
|
|
415
|
+
raise HTTPException(404, "Access entry not found")
|
|
416
|
+
return {"success": True}
|
|
417
|
+
|
|
418
|
+
@router.get("/sessions/{session_id}/access")
|
|
419
|
+
async def get_access_list(session_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
420
|
+
return session_manager.get_access_list(session_id)
|
|
421
|
+
|
|
422
|
+
# ==================================================================
|
|
423
|
+
# Join requests (approval gate)
|
|
424
|
+
# ==================================================================
|
|
425
|
+
|
|
426
|
+
@router.post("/sessions/{session_id}/join")
|
|
427
|
+
async def request_join(session_id: str, level: str = "read", reason: str = None, caller: AgentIdentity = Depends(require_read)):
|
|
428
|
+
"""Agent requests to join a session. Goes to pending queue until approved."""
|
|
429
|
+
session = session_manager.get_session(session_id)
|
|
430
|
+
if not session:
|
|
431
|
+
raise HTTPException(404, "Session not found")
|
|
432
|
+
result = session_manager.request_join(session_id, caller.agent_id, level, reason)
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
@router.get("/sessions/{session_id}/join-requests")
|
|
436
|
+
async def list_join_requests(session_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
437
|
+
"""List pending join requests (admin only)."""
|
|
438
|
+
requests = session_manager.get_pending_requests(session_id)
|
|
439
|
+
enriched = []
|
|
440
|
+
for req in requests:
|
|
441
|
+
entry = dict(req)
|
|
442
|
+
agent = agent_registry.get(req["agent_id"])
|
|
443
|
+
if agent:
|
|
444
|
+
entry["agent"] = agent.to_dict()
|
|
445
|
+
enriched.append(entry)
|
|
446
|
+
return {"requests": enriched, "total": len(enriched)}
|
|
447
|
+
|
|
448
|
+
@router.post("/sessions/{session_id}/join-requests/{request_id}/approve")
|
|
449
|
+
async def approve_join(session_id: str, request_id: str, level: str = None, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
450
|
+
"""Approve a pending join request."""
|
|
451
|
+
ok = session_manager.approve_join(request_id, caller.agent_id, level)
|
|
452
|
+
if not ok:
|
|
453
|
+
raise HTTPException(404, "Request not found or already reviewed")
|
|
454
|
+
return {"status": "approved", "request_id": request_id}
|
|
455
|
+
|
|
456
|
+
@router.post("/sessions/{session_id}/join-requests/{request_id}/deny")
|
|
457
|
+
async def deny_join(session_id: str, request_id: str, caller: AgentIdentity = Depends(require_agent_admin)):
|
|
458
|
+
"""Deny a pending join request."""
|
|
459
|
+
ok = session_manager.deny_join(request_id, caller.agent_id)
|
|
460
|
+
if not ok:
|
|
461
|
+
raise HTTPException(404, "Request not found or already reviewed")
|
|
462
|
+
return {"status": "denied", "request_id": request_id}
|
|
463
|
+
|
|
464
|
+
# ==================================================================
|
|
465
|
+
# Ingest endpoints
|
|
466
|
+
# ==================================================================
|
|
467
|
+
|
|
468
|
+
@router.post("/sessions/{session_id}/ingest")
|
|
469
|
+
async def ingest_data(session_id: str, req: IngestRequest, caller: AgentIdentity = Depends(require_write)):
|
|
470
|
+
from ..ingestion.amplifier import build_context_purpose
|
|
471
|
+
|
|
472
|
+
session = session_manager.get_session(session_id)
|
|
473
|
+
if not session:
|
|
474
|
+
raise HTTPException(404, "Session not found")
|
|
475
|
+
|
|
476
|
+
# Auto-detect context purpose from session metadata for amplifier scoring
|
|
477
|
+
ctx_meta = session.config.get("context_meta", {}) if hasattr(session, "config") and session.config else {}
|
|
478
|
+
context_purpose = build_context_purpose(
|
|
479
|
+
context_name=ctx_meta.get("name", session.name if hasattr(session, "name") else ""),
|
|
480
|
+
context_description=ctx_meta.get("description", ""),
|
|
481
|
+
tags=ctx_meta.get("tags", req.tags or []),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# --- Dedup check ---
|
|
485
|
+
text_repr = str(req.data) if not isinstance(req.data, str) else req.data
|
|
486
|
+
dup = dedup.check(session_id, text_repr)
|
|
487
|
+
if dup.is_duplicate:
|
|
488
|
+
return {
|
|
489
|
+
"success": False,
|
|
490
|
+
"duplicate": True,
|
|
491
|
+
"match_type": dup.match_type,
|
|
492
|
+
"matched_id": dup.matched_id,
|
|
493
|
+
"score": dup.score,
|
|
494
|
+
"message": f"Duplicate detected ({dup.match_type}, score={dup.score:.3f})",
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Inject context_purpose into metadata so it propagates downstream
|
|
498
|
+
enriched_metadata = {**(req.metadata or {})}
|
|
499
|
+
if context_purpose:
|
|
500
|
+
enriched_metadata["_context_purpose"] = context_purpose
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
result = ingestor.ingest(
|
|
504
|
+
session_id=session_id,
|
|
505
|
+
agent_id=req.agent_id,
|
|
506
|
+
data=req.data,
|
|
507
|
+
data_type=req.data_type,
|
|
508
|
+
metadata=enriched_metadata,
|
|
509
|
+
source=req.source,
|
|
510
|
+
mime_type=req.mime_type,
|
|
511
|
+
filename=req.filename,
|
|
512
|
+
tags=req.tags,
|
|
513
|
+
sensitivity=req.sensitivity,
|
|
514
|
+
auto_pii_scan=req.auto_pii_scan,
|
|
515
|
+
)
|
|
516
|
+
except ValueError as e:
|
|
517
|
+
raise HTTPException(400, str(e))
|
|
518
|
+
except Exception as e:
|
|
519
|
+
logger.error(f"Ingest error: {e}")
|
|
520
|
+
raise HTTPException(500, f"Ingestion failed: {e}")
|
|
521
|
+
|
|
522
|
+
# Auto-embed ingested text into vector store
|
|
523
|
+
embedded = False
|
|
524
|
+
if vector_store.available and result.get("node_ids"):
|
|
525
|
+
if text_repr and len(text_repr.strip()) > 0:
|
|
526
|
+
for nid in result["node_ids"]:
|
|
527
|
+
vector_store.add_text(
|
|
528
|
+
session_id=session_id,
|
|
529
|
+
text=text_repr,
|
|
530
|
+
node_id=nid,
|
|
531
|
+
metadata={"agent_id": req.agent_id, "source": req.source or "ingest"},
|
|
532
|
+
)
|
|
533
|
+
embedded = True
|
|
534
|
+
|
|
535
|
+
# Register content hash so future duplicates are caught
|
|
536
|
+
for nid in result.get("node_ids", []):
|
|
537
|
+
dedup.register(session_id, nid, text_repr)
|
|
538
|
+
|
|
539
|
+
# Publish event
|
|
540
|
+
await pubsub_hub.publish(ContextEvent(
|
|
541
|
+
event_type="context_added",
|
|
542
|
+
session_id=session_id,
|
|
543
|
+
agent_id=req.agent_id,
|
|
544
|
+
payload={"data_type": result["data_type"], "node_ids": result["node_ids"]},
|
|
545
|
+
))
|
|
546
|
+
|
|
547
|
+
return {"success": True, "embedded": embedded, **result}
|
|
548
|
+
|
|
549
|
+
# ==================================================================
|
|
550
|
+
# Document processing endpoints
|
|
551
|
+
# ==================================================================
|
|
552
|
+
|
|
553
|
+
@router.post("/sessions/{session_id}/documents")
|
|
554
|
+
async def process_document(session_id: str, req: DocumentProcessRequest, caller: AgentIdentity = Depends(require_write)):
|
|
555
|
+
"""Process a document (PDF, CSV, TXT, etc.) into the session's graph.
|
|
556
|
+
|
|
557
|
+
Extracts text, tables, images → creates Source, Document, Chunk, Table,
|
|
558
|
+
Image nodes with edges → permanent context.
|
|
559
|
+
"""
|
|
560
|
+
session = session_manager.get_session(session_id)
|
|
561
|
+
if not session:
|
|
562
|
+
raise HTTPException(404, "Session not found")
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
result = doc_processor.process(
|
|
566
|
+
session_id=session_id,
|
|
567
|
+
agent_id=req.agent_id,
|
|
568
|
+
file_path=req.file_path,
|
|
569
|
+
extract_tables=req.extract_tables,
|
|
570
|
+
extract_images=req.extract_images,
|
|
571
|
+
chunk_size=req.chunk_size,
|
|
572
|
+
chunk_overlap=req.chunk_overlap,
|
|
573
|
+
metadata=req.metadata,
|
|
574
|
+
)
|
|
575
|
+
except FileNotFoundError as e:
|
|
576
|
+
raise HTTPException(404, str(e))
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Document processing error: {e}")
|
|
579
|
+
raise HTTPException(500, f"Document processing failed: {e}")
|
|
580
|
+
|
|
581
|
+
await pubsub_hub.publish(ContextEvent(
|
|
582
|
+
event_type="context_added",
|
|
583
|
+
session_id=session_id,
|
|
584
|
+
agent_id=req.agent_id,
|
|
585
|
+
payload={
|
|
586
|
+
"type": "document",
|
|
587
|
+
"document_id": result["document_id"],
|
|
588
|
+
"chunk_count": result["chunk_count"],
|
|
589
|
+
"table_count": result["table_count"],
|
|
590
|
+
"image_count": result["image_count"],
|
|
591
|
+
},
|
|
592
|
+
))
|
|
593
|
+
|
|
594
|
+
return {"success": True, **result}
|
|
595
|
+
|
|
596
|
+
@router.post("/sessions/{session_id}/documents/upload")
|
|
597
|
+
async def upload_document(
|
|
598
|
+
session_id: str,
|
|
599
|
+
file: UploadFile = File(...),
|
|
600
|
+
agent_id: str = Query(...),
|
|
601
|
+
extract_tables: bool = Query(True),
|
|
602
|
+
extract_images: bool = Query(True),
|
|
603
|
+
caller: AgentIdentity = Depends(require_write),
|
|
604
|
+
):
|
|
605
|
+
"""Upload a document file and process it into the session's graph."""
|
|
606
|
+
session = session_manager.get_session(session_id)
|
|
607
|
+
if not session:
|
|
608
|
+
raise HTTPException(404, "Session not found")
|
|
609
|
+
|
|
610
|
+
raw_bytes = await file.read()
|
|
611
|
+
try:
|
|
612
|
+
result = doc_processor.process(
|
|
613
|
+
session_id=session_id,
|
|
614
|
+
agent_id=agent_id,
|
|
615
|
+
raw_bytes=raw_bytes,
|
|
616
|
+
filename=file.filename or "upload",
|
|
617
|
+
extract_tables=extract_tables,
|
|
618
|
+
extract_images=extract_images,
|
|
619
|
+
)
|
|
620
|
+
except Exception as e:
|
|
621
|
+
logger.error(f"Document upload processing error: {e}")
|
|
622
|
+
raise HTTPException(500, f"Document processing failed: {e}")
|
|
623
|
+
|
|
624
|
+
await pubsub_hub.publish(ContextEvent(
|
|
625
|
+
event_type="context_added",
|
|
626
|
+
session_id=session_id,
|
|
627
|
+
agent_id=agent_id,
|
|
628
|
+
payload={
|
|
629
|
+
"type": "document",
|
|
630
|
+
"document_id": result["document_id"],
|
|
631
|
+
"chunk_count": result["chunk_count"],
|
|
632
|
+
},
|
|
633
|
+
))
|
|
634
|
+
|
|
635
|
+
return {"success": True, **result}
|
|
636
|
+
|
|
637
|
+
# ==================================================================
|
|
638
|
+
# Context item endpoints
|
|
639
|
+
# ==================================================================
|
|
640
|
+
|
|
641
|
+
@router.post("/sessions/{session_id}/items")
|
|
642
|
+
async def add_context_item(session_id: str, req: ContextItemRequest, caller: AgentIdentity = Depends(require_write)):
|
|
643
|
+
session = session_manager.get_session(session_id)
|
|
644
|
+
if not session:
|
|
645
|
+
raise HTTPException(404, "Session not found")
|
|
646
|
+
|
|
647
|
+
hub = _get_hub(session_id)
|
|
648
|
+
hub.add_text(
|
|
649
|
+
text=req.content,
|
|
650
|
+
role=req.role,
|
|
651
|
+
label=req.label,
|
|
652
|
+
source=req.source,
|
|
653
|
+
metadata={**req.metadata, "agent_id": req.agent_id},
|
|
654
|
+
tags=req.tags,
|
|
655
|
+
sensitivity=req.sensitivity,
|
|
656
|
+
)
|
|
657
|
+
# Tag the last item with agent_id
|
|
658
|
+
hub._items[-1].agent_id = req.agent_id
|
|
659
|
+
|
|
660
|
+
await pubsub_hub.publish(ContextEvent(
|
|
661
|
+
event_type="context_added",
|
|
662
|
+
session_id=session_id,
|
|
663
|
+
agent_id=req.agent_id,
|
|
664
|
+
payload={"label": req.label, "role": req.role},
|
|
665
|
+
))
|
|
666
|
+
|
|
667
|
+
return {"success": True, "item_count": len(hub)}
|
|
668
|
+
|
|
669
|
+
@router.get("/sessions/{session_id}/items")
|
|
670
|
+
async def list_context_items(
|
|
671
|
+
session_id: str,
|
|
672
|
+
role: Optional[str] = Query(None),
|
|
673
|
+
agent_id: Optional[str] = Query(None),
|
|
674
|
+
source: Optional[str] = Query(None),
|
|
675
|
+
label: Optional[str] = Query(None),
|
|
676
|
+
tag: Optional[str] = Query(None, description="Filter by tag"),
|
|
677
|
+
sensitivity: Optional[str] = Query(None),
|
|
678
|
+
caller: AgentIdentity = Depends(require_read),
|
|
679
|
+
):
|
|
680
|
+
hub = _get_hub(session_id)
|
|
681
|
+
|
|
682
|
+
# First apply authorization filter based on caller's access
|
|
683
|
+
access_info = session_manager.get_agent_access(session_id, caller.agent_id)
|
|
684
|
+
if access_info:
|
|
685
|
+
access_level = access_info["access_level"]
|
|
686
|
+
allowed_tags = access_info["allowed_tags"]
|
|
687
|
+
else:
|
|
688
|
+
access_level = "read"
|
|
689
|
+
allowed_tags = []
|
|
690
|
+
|
|
691
|
+
visible_items = hub.filter_for_agent(access_level, allowed_tags)
|
|
692
|
+
|
|
693
|
+
# Then apply user-specified filters on visible items
|
|
694
|
+
filtered_hub = ContextHub()
|
|
695
|
+
filtered_hub._items = visible_items
|
|
696
|
+
items = filtered_hub.filter(
|
|
697
|
+
role=role, agent_id=agent_id, source=source, label=label,
|
|
698
|
+
tags=[tag] if tag else None, sensitivity=sensitivity,
|
|
699
|
+
)
|
|
700
|
+
return {"items": [i.to_dict() for i in items], "count": len(items)}
|
|
701
|
+
|
|
702
|
+
# ==================================================================
|
|
703
|
+
# Agent contributions (write outputs back into shared context)
|
|
704
|
+
# ==================================================================
|
|
705
|
+
|
|
706
|
+
@router.post("/sessions/{session_id}/contribute")
|
|
707
|
+
async def contribute(session_id: str, req: ContributeRequest, caller: AgentIdentity = Depends(require_write)):
|
|
708
|
+
"""
|
|
709
|
+
Agent writes its output (summary, analysis, decision) back into the
|
|
710
|
+
session. The output is:
|
|
711
|
+
|
|
712
|
+
1. Added to the in-memory ContextHub (available via /export)
|
|
713
|
+
2. Persisted as an ``Insight`` graph node (survives restarts)
|
|
714
|
+
3. Linked to source nodes via ``DERIVED_FROM`` edges (lineage)
|
|
715
|
+
4. Broadcast to subscribers via WebSocket
|
|
716
|
+
"""
|
|
717
|
+
session = session_manager.get_session(session_id)
|
|
718
|
+
if not session:
|
|
719
|
+
raise HTTPException(404, "Session not found")
|
|
720
|
+
|
|
721
|
+
# Verify agent has write access to this session
|
|
722
|
+
if session.owner_agent_id != caller.agent_id:
|
|
723
|
+
if not session_manager.check_access(session_id, caller.agent_id, "write"):
|
|
724
|
+
raise HTTPException(403, "No write access to this session")
|
|
725
|
+
|
|
726
|
+
# Auto-fill agent_id from authenticated caller if not provided
|
|
727
|
+
if not req.agent_id:
|
|
728
|
+
req.agent_id = caller.agent_id
|
|
729
|
+
|
|
730
|
+
# Dedup check on contributions too
|
|
731
|
+
dup = dedup.check(session_id, req.content)
|
|
732
|
+
if dup.is_duplicate:
|
|
733
|
+
return {
|
|
734
|
+
"success": False,
|
|
735
|
+
"duplicate": True,
|
|
736
|
+
"match_type": dup.match_type,
|
|
737
|
+
"matched_id": dup.matched_id,
|
|
738
|
+
"score": dup.score,
|
|
739
|
+
"message": f"Duplicate contribution ({dup.match_type}, score={dup.score:.3f})",
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
# 1. Add to hub
|
|
743
|
+
hub = _get_hub(session_id)
|
|
744
|
+
hub.contribute(
|
|
745
|
+
content=req.content,
|
|
746
|
+
agent_id=req.agent_id,
|
|
747
|
+
role=req.role,
|
|
748
|
+
label=req.label,
|
|
749
|
+
source_ids=req.source_ids,
|
|
750
|
+
metadata=req.metadata,
|
|
751
|
+
tags=req.tags,
|
|
752
|
+
sensitivity=req.sensitivity,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# 2. Persist as Insight node in graph
|
|
756
|
+
insight_node_id = None
|
|
757
|
+
if graph_registry:
|
|
758
|
+
graph = graph_registry.get_graph(session.graph_namespace, load_if_missing=True)
|
|
759
|
+
if graph:
|
|
760
|
+
import uuid as _uuid
|
|
761
|
+
from ..core.graph_structures import GraphNode, GraphEdge
|
|
762
|
+
|
|
763
|
+
insight_node_id = str(_uuid.uuid4())
|
|
764
|
+
node = GraphNode(
|
|
765
|
+
id=insight_node_id,
|
|
766
|
+
label="Insight",
|
|
767
|
+
properties={
|
|
768
|
+
"content": req.content,
|
|
769
|
+
"insight_type": req.role,
|
|
770
|
+
"derived_from": req.source_ids,
|
|
771
|
+
"_ctx_session_id": session.session_id,
|
|
772
|
+
"_ctx_agent_id": req.agent_id,
|
|
773
|
+
"_ctx_source": "contribute",
|
|
774
|
+
**req.metadata,
|
|
775
|
+
},
|
|
776
|
+
)
|
|
777
|
+
graph.add_node(node)
|
|
778
|
+
|
|
779
|
+
# 3. Create DERIVED_FROM edges to source nodes
|
|
780
|
+
for src_id in req.source_ids:
|
|
781
|
+
try:
|
|
782
|
+
edge = GraphEdge(
|
|
783
|
+
id=str(_uuid.uuid4()),
|
|
784
|
+
source=insight_node_id,
|
|
785
|
+
target=src_id,
|
|
786
|
+
label="DERIVED_FROM",
|
|
787
|
+
properties={"agent_id": req.agent_id},
|
|
788
|
+
)
|
|
789
|
+
graph.add_edge(edge)
|
|
790
|
+
except Exception:
|
|
791
|
+
pass # source node may not exist
|
|
792
|
+
|
|
793
|
+
# Save
|
|
794
|
+
graph_registry.save_graph(session.graph_namespace, create_checkpoint=False)
|
|
795
|
+
|
|
796
|
+
# Provenance
|
|
797
|
+
if insight_node_id:
|
|
798
|
+
agent_registry.record_provenance(ProvenanceRecord(
|
|
799
|
+
session_id=session_id,
|
|
800
|
+
agent_id=req.agent_id,
|
|
801
|
+
operation="contribute",
|
|
802
|
+
target_type="insight",
|
|
803
|
+
target_id=insight_node_id,
|
|
804
|
+
source_ids=req.source_ids,
|
|
805
|
+
metadata={"role": req.role},
|
|
806
|
+
))
|
|
807
|
+
|
|
808
|
+
# 4. Auto-embed the contribution into vector store
|
|
809
|
+
if vector_store.available and insight_node_id:
|
|
810
|
+
vector_store.add_text(
|
|
811
|
+
session_id=session_id,
|
|
812
|
+
text=req.content,
|
|
813
|
+
node_id=insight_node_id,
|
|
814
|
+
metadata={"agent_id": req.agent_id, "role": req.role, "source": "contribute"},
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# Register hash for dedup
|
|
818
|
+
if insight_node_id:
|
|
819
|
+
dedup.register(session_id, insight_node_id, req.content)
|
|
820
|
+
|
|
821
|
+
# 5. Broadcast
|
|
822
|
+
await pubsub_hub.publish(ContextEvent(
|
|
823
|
+
event_type="context_added",
|
|
824
|
+
session_id=session_id,
|
|
825
|
+
agent_id=req.agent_id,
|
|
826
|
+
payload={
|
|
827
|
+
"type": "insight",
|
|
828
|
+
"role": req.role,
|
|
829
|
+
"label": req.label,
|
|
830
|
+
"insight_node_id": insight_node_id,
|
|
831
|
+
"source_ids": req.source_ids,
|
|
832
|
+
},
|
|
833
|
+
))
|
|
834
|
+
|
|
835
|
+
session_manager.touch_session(session_id)
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
"success": True,
|
|
839
|
+
"insight_node_id": insight_node_id,
|
|
840
|
+
"item_count": len(hub),
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
# ==================================================================
|
|
844
|
+
# Export / query endpoints
|
|
845
|
+
# ==================================================================
|
|
846
|
+
|
|
847
|
+
@router.get("/sessions/{session_id}/export")
|
|
848
|
+
async def export_context(
|
|
849
|
+
session_id: str,
|
|
850
|
+
format: str = Query("messages", description="messages|prompt|markdown|dict"),
|
|
851
|
+
pii_mode: str = Query("block", description="block|mask|redact|allow"),
|
|
852
|
+
max_tokens: Optional[int] = Query(None, description="Token budget (applies scoping)"),
|
|
853
|
+
strategy: Optional[str] = Query(None, description="Scoping strategy: recency|relevance|role_priority|combined"),
|
|
854
|
+
caller: AgentIdentity = Depends(require_read),
|
|
855
|
+
):
|
|
856
|
+
"""Export context filtered by the calling agent's access level + tag grants."""
|
|
857
|
+
session = session_manager.get_session(session_id)
|
|
858
|
+
if not session:
|
|
859
|
+
raise HTTPException(404, "Session not found")
|
|
860
|
+
|
|
861
|
+
# Verify agent has access to this session
|
|
862
|
+
if session.owner_agent_id != caller.agent_id:
|
|
863
|
+
if not session_manager.check_access(session_id, caller.agent_id, "read"):
|
|
864
|
+
raise HTTPException(403, "No access to this session")
|
|
865
|
+
|
|
866
|
+
if pii_mode not in ("block", "mask", "redact", "allow"):
|
|
867
|
+
raise HTTPException(400, "pii_mode must be one of: block, mask, redact, allow")
|
|
868
|
+
|
|
869
|
+
hub = _get_hub(session_id)
|
|
870
|
+
|
|
871
|
+
# Also pull graph nodes into the hub if it's empty
|
|
872
|
+
if len(hub) == 0 and graph_registry:
|
|
873
|
+
graph = graph_registry.get_graph(session.graph_namespace, load_if_missing=True)
|
|
874
|
+
if graph:
|
|
875
|
+
try:
|
|
876
|
+
all_nodes = graph.get_all_nodes()
|
|
877
|
+
if all_nodes:
|
|
878
|
+
hub.add_nodes(all_nodes, label="Session Graph Nodes")
|
|
879
|
+
except Exception:
|
|
880
|
+
pass # optional
|
|
881
|
+
|
|
882
|
+
# --- Authorization-filtered export ---
|
|
883
|
+
access_info = session_manager.get_agent_access(session_id, caller.agent_id)
|
|
884
|
+
if access_info:
|
|
885
|
+
access_level = access_info["access_level"]
|
|
886
|
+
allowed_tags = access_info["allowed_tags"]
|
|
887
|
+
else:
|
|
888
|
+
access_level = "read"
|
|
889
|
+
allowed_tags = []
|
|
890
|
+
|
|
891
|
+
# Build a filtered hub containing only items this agent may see
|
|
892
|
+
visible_items = hub.filter_for_agent(access_level, allowed_tags, pii_mode=pii_mode)
|
|
893
|
+
filtered_hub = ContextHub(max_tokens=hub.max_tokens)
|
|
894
|
+
filtered_hub._items = visible_items
|
|
895
|
+
|
|
896
|
+
# Apply scoping if requested
|
|
897
|
+
budget = max_tokens
|
|
898
|
+
if budget or strategy or session_id in _scoping_configs:
|
|
899
|
+
cfg = _scoping_configs.get(session_id, ScopingConfig())
|
|
900
|
+
if strategy:
|
|
901
|
+
cfg.strategy = strategy
|
|
902
|
+
if budget:
|
|
903
|
+
cfg.max_tokens = budget
|
|
904
|
+
filtered_hub.set_scoping(cfg)
|
|
905
|
+
|
|
906
|
+
if format in ("messages", "prompt"):
|
|
907
|
+
return filtered_hub.export(format) if not budget else (
|
|
908
|
+
filtered_hub.to_messages(budget=budget) if format == "messages"
|
|
909
|
+
else filtered_hub.to_prompt(budget=budget)
|
|
910
|
+
)
|
|
911
|
+
return filtered_hub.export(format)
|
|
912
|
+
|
|
913
|
+
@router.post("/sessions/{session_id}/query")
|
|
914
|
+
async def query_session(session_id: str, req: QueryRequest, caller: AgentIdentity = Depends(require_read)):
|
|
915
|
+
"""Execute an AIQL query scoped to the session's graph namespace."""
|
|
916
|
+
session = session_manager.get_session(session_id)
|
|
917
|
+
if not session:
|
|
918
|
+
raise HTTPException(404, "Session not found")
|
|
919
|
+
|
|
920
|
+
# Verify agent has access to this session
|
|
921
|
+
if session.owner_agent_id != caller.agent_id:
|
|
922
|
+
if not session_manager.check_access(session_id, caller.agent_id, "read"):
|
|
923
|
+
raise HTTPException(403, "No access to this session")
|
|
924
|
+
|
|
925
|
+
if not graph_registry:
|
|
926
|
+
raise HTTPException(500, "Graph registry not available")
|
|
927
|
+
|
|
928
|
+
graph = graph_registry.get_graph(session.graph_namespace, load_if_missing=True)
|
|
929
|
+
if not graph:
|
|
930
|
+
# Create graph on first query if it doesn't exist yet
|
|
931
|
+
graph = graph_registry.create_graph(session.graph_namespace)
|
|
932
|
+
if not graph:
|
|
933
|
+
return {"nodes": [], "edges": [], "message": "Could not create graph"}
|
|
934
|
+
|
|
935
|
+
try:
|
|
936
|
+
from ..aiql.engine import AIQLExecutor
|
|
937
|
+
|
|
938
|
+
ex = AIQLExecutor(contextcore=graph, graph_registry=graph_registry)
|
|
939
|
+
ex.active_namespace = session.graph_namespace
|
|
940
|
+
result = ex.execute(req.query)
|
|
941
|
+
|
|
942
|
+
# Persist after write operations
|
|
943
|
+
query_upper = req.query.strip().upper()
|
|
944
|
+
if query_upper.startswith("CREATE") or query_upper.startswith("DELETE") or query_upper.startswith("UPDATE"):
|
|
945
|
+
try:
|
|
946
|
+
graph_registry.save_graph(session.graph_namespace, create_checkpoint=False)
|
|
947
|
+
except Exception:
|
|
948
|
+
pass # optional
|
|
949
|
+
|
|
950
|
+
return {"success": True, "data": result}
|
|
951
|
+
except Exception as e:
|
|
952
|
+
raise HTTPException(400, f"Query failed: {e}")
|
|
953
|
+
|
|
954
|
+
# ==================================================================
|
|
955
|
+
# Semantic search (vector DB)
|
|
956
|
+
# ==================================================================
|
|
957
|
+
|
|
958
|
+
@router.post("/sessions/{session_id}/search")
|
|
959
|
+
async def semantic_search(
|
|
960
|
+
session_id: str,
|
|
961
|
+
req: SemanticSearchRequest,
|
|
962
|
+
caller: AgentIdentity = Depends(require_read),
|
|
963
|
+
):
|
|
964
|
+
"""Semantic similarity search across a session's embedded context."""
|
|
965
|
+
session = session_manager.get_session(session_id)
|
|
966
|
+
if not session:
|
|
967
|
+
raise HTTPException(404, "Session not found")
|
|
968
|
+
|
|
969
|
+
if not vector_store.available:
|
|
970
|
+
raise HTTPException(503, "Vector search unavailable (no vector backend)")
|
|
971
|
+
|
|
972
|
+
results = vector_store.search(
|
|
973
|
+
session_id=session_id,
|
|
974
|
+
query=req.query,
|
|
975
|
+
k=req.k,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
return {"success": True, "results": results, "count": len(results)}
|
|
979
|
+
|
|
980
|
+
@router.get("/sessions/{session_id}/vectors/stats")
|
|
981
|
+
async def vector_stats(session_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
982
|
+
"""Get vector store statistics for a session."""
|
|
983
|
+
return vector_store.get_stats(session_id)
|
|
984
|
+
|
|
985
|
+
# ==================================================================
|
|
986
|
+
# Provenance
|
|
987
|
+
# ==================================================================
|
|
988
|
+
|
|
989
|
+
@router.get("/sessions/{session_id}/provenance")
|
|
990
|
+
async def get_provenance(
|
|
991
|
+
session_id: str,
|
|
992
|
+
agent_id: Optional[str] = Query(None),
|
|
993
|
+
limit: int = Query(100),
|
|
994
|
+
caller: AgentIdentity = Depends(require_read),
|
|
995
|
+
):
|
|
996
|
+
records = agent_registry.get_provenance(
|
|
997
|
+
session_id=session_id,
|
|
998
|
+
agent_id=agent_id,
|
|
999
|
+
limit=limit,
|
|
1000
|
+
)
|
|
1001
|
+
return {"provenance": records, "count": len(records)}
|
|
1002
|
+
|
|
1003
|
+
# ==================================================================
|
|
1004
|
+
# Blob endpoints
|
|
1005
|
+
# ==================================================================
|
|
1006
|
+
|
|
1007
|
+
@router.post("/sessions/{session_id}/blobs")
|
|
1008
|
+
async def upload_blob(
|
|
1009
|
+
session_id: str,
|
|
1010
|
+
file: UploadFile = File(...),
|
|
1011
|
+
agent_id: str = Query(...),
|
|
1012
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1013
|
+
):
|
|
1014
|
+
session = session_manager.get_session(session_id)
|
|
1015
|
+
if not session:
|
|
1016
|
+
raise HTTPException(404, "Session not found")
|
|
1017
|
+
|
|
1018
|
+
data = await file.read()
|
|
1019
|
+
mime = file.content_type or "application/octet-stream"
|
|
1020
|
+
|
|
1021
|
+
blob_info = blob_store.store(
|
|
1022
|
+
session_id=session_id,
|
|
1023
|
+
data=data,
|
|
1024
|
+
filename=file.filename or "upload",
|
|
1025
|
+
mime_type=mime,
|
|
1026
|
+
agent_id=agent_id,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
agent_registry.record_provenance(ProvenanceRecord(
|
|
1030
|
+
session_id=session_id,
|
|
1031
|
+
agent_id=agent_id,
|
|
1032
|
+
operation="ingest",
|
|
1033
|
+
target_type="blob",
|
|
1034
|
+
target_id=blob_info["blob_id"],
|
|
1035
|
+
))
|
|
1036
|
+
|
|
1037
|
+
await pubsub_hub.publish(ContextEvent(
|
|
1038
|
+
event_type="context_added",
|
|
1039
|
+
session_id=session_id,
|
|
1040
|
+
agent_id=agent_id,
|
|
1041
|
+
payload={"blob_id": blob_info["blob_id"], "filename": file.filename},
|
|
1042
|
+
))
|
|
1043
|
+
|
|
1044
|
+
return {"success": True, **blob_info}
|
|
1045
|
+
|
|
1046
|
+
@router.get("/sessions/{session_id}/blobs")
|
|
1047
|
+
async def list_blobs(session_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
1048
|
+
return blob_store.list_blobs(session_id)
|
|
1049
|
+
|
|
1050
|
+
@router.get("/sessions/{session_id}/blobs/{blob_id}")
|
|
1051
|
+
async def get_blob(session_id: str, blob_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
1052
|
+
result = blob_store.get(blob_id)
|
|
1053
|
+
if not result:
|
|
1054
|
+
raise HTTPException(404, "Blob not found")
|
|
1055
|
+
raw_bytes, meta = result
|
|
1056
|
+
from fastapi.responses import Response
|
|
1057
|
+
return Response(
|
|
1058
|
+
content=raw_bytes,
|
|
1059
|
+
media_type=meta.get("mime_type", "application/octet-stream"),
|
|
1060
|
+
headers={"Content-Disposition": f'attachment; filename="{meta.get("filename", "download")}"'},
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
@router.delete("/sessions/{session_id}/blobs/{blob_id}")
|
|
1064
|
+
async def delete_blob(session_id: str, blob_id: str, caller: AgentIdentity = Depends(require_write)):
|
|
1065
|
+
ok = blob_store.delete(blob_id)
|
|
1066
|
+
if not ok:
|
|
1067
|
+
raise HTTPException(404, "Blob not found")
|
|
1068
|
+
return {"success": True}
|
|
1069
|
+
|
|
1070
|
+
# ==================================================================
|
|
1071
|
+
# WebSocket — real-time context subscription
|
|
1072
|
+
# ==================================================================
|
|
1073
|
+
|
|
1074
|
+
@router.websocket("/sessions/{session_id}/ws")
|
|
1075
|
+
async def websocket_subscribe(websocket: WebSocket, session_id: str):
|
|
1076
|
+
await websocket.accept()
|
|
1077
|
+
|
|
1078
|
+
# Expect first message to be {"agent_id": "..."}
|
|
1079
|
+
try:
|
|
1080
|
+
init = await websocket.receive_json()
|
|
1081
|
+
agent_id = init.get("agent_id", "anonymous")
|
|
1082
|
+
except Exception:
|
|
1083
|
+
agent_id = "anonymous"
|
|
1084
|
+
|
|
1085
|
+
conn_id = secrets.token_hex(8)
|
|
1086
|
+
await pubsub_hub.subscribe(
|
|
1087
|
+
session_id=session_id,
|
|
1088
|
+
connection_id=conn_id,
|
|
1089
|
+
agent_id=agent_id,
|
|
1090
|
+
send_fn=websocket.send_text,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
try:
|
|
1094
|
+
while True:
|
|
1095
|
+
# Keep connection alive; clients can also send messages
|
|
1096
|
+
data = await websocket.receive_text()
|
|
1097
|
+
# Echo back or handle client-side events if needed
|
|
1098
|
+
except WebSocketDisconnect:
|
|
1099
|
+
await pubsub_hub.unsubscribe(conn_id)
|
|
1100
|
+
|
|
1101
|
+
# ==================================================================
|
|
1102
|
+
# Scoping config endpoints
|
|
1103
|
+
# ==================================================================
|
|
1104
|
+
|
|
1105
|
+
@router.put("/sessions/{session_id}/scoping")
|
|
1106
|
+
async def set_scoping_config(
|
|
1107
|
+
session_id: str, req: ScopingConfigRequest,
|
|
1108
|
+
caller: AgentIdentity = Depends(require_agent_admin),
|
|
1109
|
+
):
|
|
1110
|
+
"""Set token-budget scoping config for a session (requires admin)."""
|
|
1111
|
+
session = session_manager.get_session(session_id)
|
|
1112
|
+
if not session:
|
|
1113
|
+
raise HTTPException(404, "Session not found")
|
|
1114
|
+
cfg = ScopingConfig(
|
|
1115
|
+
max_tokens=req.max_tokens,
|
|
1116
|
+
strategy=req.strategy,
|
|
1117
|
+
role_weights=req.role_weights or dict(ScopingConfig().role_weights),
|
|
1118
|
+
recency_decay=req.recency_decay,
|
|
1119
|
+
reserve_system=req.reserve_system,
|
|
1120
|
+
)
|
|
1121
|
+
_scoping_configs[session_id] = cfg
|
|
1122
|
+
return {"success": True, "config": {
|
|
1123
|
+
"max_tokens": cfg.max_tokens,
|
|
1124
|
+
"strategy": cfg.strategy,
|
|
1125
|
+
"recency_decay": cfg.recency_decay,
|
|
1126
|
+
"reserve_system": cfg.reserve_system,
|
|
1127
|
+
}}
|
|
1128
|
+
|
|
1129
|
+
@router.get("/sessions/{session_id}/scoping")
|
|
1130
|
+
async def get_scoping_config(session_id: str, caller: AgentIdentity = Depends(require_read)):
|
|
1131
|
+
"""Get current scoping config for a session."""
|
|
1132
|
+
cfg = _scoping_configs.get(session_id)
|
|
1133
|
+
if not cfg:
|
|
1134
|
+
return {"configured": False, "config": None}
|
|
1135
|
+
return {"configured": True, "config": {
|
|
1136
|
+
"max_tokens": cfg.max_tokens,
|
|
1137
|
+
"strategy": cfg.strategy,
|
|
1138
|
+
"recency_decay": cfg.recency_decay,
|
|
1139
|
+
"reserve_system": cfg.reserve_system,
|
|
1140
|
+
}}
|
|
1141
|
+
|
|
1142
|
+
# ==================================================================
|
|
1143
|
+
# Conversation endpoints
|
|
1144
|
+
# ==================================================================
|
|
1145
|
+
|
|
1146
|
+
@router.post("/conversations")
|
|
1147
|
+
async def create_conversation(
|
|
1148
|
+
req: ConversationCreateRequest,
|
|
1149
|
+
caller: AgentIdentity = Depends(require_write),
|
|
1150
|
+
):
|
|
1151
|
+
"""Create a new conversation tied to a session."""
|
|
1152
|
+
session = session_manager.get_session(req.session_id)
|
|
1153
|
+
if not session:
|
|
1154
|
+
raise HTTPException(404, "Session not found")
|
|
1155
|
+
conv = conversation_store.create(
|
|
1156
|
+
session_id=req.session_id,
|
|
1157
|
+
title=req.title,
|
|
1158
|
+
metadata=req.metadata,
|
|
1159
|
+
ttl_seconds=req.ttl_seconds,
|
|
1160
|
+
)
|
|
1161
|
+
return {"success": True, **conv.to_dict()}
|
|
1162
|
+
|
|
1163
|
+
@router.get("/conversations")
|
|
1164
|
+
async def list_conversations(
|
|
1165
|
+
session_id: str = Query(...),
|
|
1166
|
+
status: str = Query("active"),
|
|
1167
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1168
|
+
):
|
|
1169
|
+
convs = conversation_store.list_conversations(session_id, status=status)
|
|
1170
|
+
return {"conversations": [c.to_dict() for c in convs], "count": len(convs)}
|
|
1171
|
+
|
|
1172
|
+
@router.get("/conversations/{conversation_id}")
|
|
1173
|
+
async def get_conversation(
|
|
1174
|
+
conversation_id: str,
|
|
1175
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1176
|
+
):
|
|
1177
|
+
conv = conversation_store.get(conversation_id)
|
|
1178
|
+
if not conv:
|
|
1179
|
+
raise HTTPException(404, "Conversation not found")
|
|
1180
|
+
return {
|
|
1181
|
+
**conv.to_dict(),
|
|
1182
|
+
"messages": [m.to_dict() for m in conv.messages],
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
@router.delete("/conversations/{conversation_id}")
|
|
1186
|
+
async def delete_conversation(
|
|
1187
|
+
conversation_id: str,
|
|
1188
|
+
caller: AgentIdentity = Depends(require_agent_admin),
|
|
1189
|
+
):
|
|
1190
|
+
ok = conversation_store.delete(conversation_id)
|
|
1191
|
+
if not ok:
|
|
1192
|
+
raise HTTPException(404, "Conversation not found")
|
|
1193
|
+
return {"success": True}
|
|
1194
|
+
|
|
1195
|
+
@router.post("/conversations/{conversation_id}/messages")
|
|
1196
|
+
async def append_conversation_message(
|
|
1197
|
+
conversation_id: str,
|
|
1198
|
+
req: ConversationMessageRequest,
|
|
1199
|
+
background_tasks: BackgroundTasks,
|
|
1200
|
+
caller: AgentIdentity = Depends(require_write),
|
|
1201
|
+
):
|
|
1202
|
+
"""Append a message to a conversation.
|
|
1203
|
+
|
|
1204
|
+
Also graphifies the interaction into the session's unified graph
|
|
1205
|
+
(fire-and-forget background task) so agents can later traverse
|
|
1206
|
+
from conversation turns to knowledge entities.
|
|
1207
|
+
"""
|
|
1208
|
+
conv = conversation_store.get(conversation_id, include_messages=False)
|
|
1209
|
+
if not conv:
|
|
1210
|
+
raise HTTPException(404, "Conversation not found")
|
|
1211
|
+
msg = conversation_store.append_message(
|
|
1212
|
+
conversation_id=conversation_id,
|
|
1213
|
+
role=req.role,
|
|
1214
|
+
content=req.content,
|
|
1215
|
+
agent_id=req.agent_id,
|
|
1216
|
+
metadata=req.metadata,
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
# Fire-and-forget: graphify interaction into session graph
|
|
1220
|
+
if hasattr(conv, "session_id") and conv.session_id:
|
|
1221
|
+
builder = await _get_or_create_builder(conv.session_id)
|
|
1222
|
+
if builder:
|
|
1223
|
+
background_tasks.add_task(
|
|
1224
|
+
builder.graphify_interaction,
|
|
1225
|
+
role=req.role,
|
|
1226
|
+
content=req.content,
|
|
1227
|
+
agent_id=req.agent_id or caller.agent_id,
|
|
1228
|
+
conversation_id=conversation_id,
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
return {"success": True, **msg.to_dict()}
|
|
1232
|
+
|
|
1233
|
+
@router.get("/conversations/{conversation_id}/messages")
|
|
1234
|
+
async def get_conversation_messages(
|
|
1235
|
+
conversation_id: str,
|
|
1236
|
+
limit: Optional[int] = Query(None),
|
|
1237
|
+
offset: int = Query(0),
|
|
1238
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1239
|
+
):
|
|
1240
|
+
messages = conversation_store.get_history(conversation_id, limit=limit, offset=offset)
|
|
1241
|
+
return {"messages": [m.to_dict() for m in messages], "count": len(messages)}
|
|
1242
|
+
|
|
1243
|
+
@router.put("/conversations/{conversation_id}/kv")
|
|
1244
|
+
async def set_conversation_kv(
|
|
1245
|
+
conversation_id: str,
|
|
1246
|
+
req: ConversationKVRequest,
|
|
1247
|
+
caller: AgentIdentity = Depends(require_write),
|
|
1248
|
+
):
|
|
1249
|
+
"""Set a key-value pair on a conversation."""
|
|
1250
|
+
conv = conversation_store.get(conversation_id, include_messages=False)
|
|
1251
|
+
if not conv:
|
|
1252
|
+
raise HTTPException(404, "Conversation not found")
|
|
1253
|
+
conversation_store.set_kv(conversation_id, req.key, req.value)
|
|
1254
|
+
return {"success": True, "key": req.key}
|
|
1255
|
+
|
|
1256
|
+
@router.get("/conversations/{conversation_id}/kv")
|
|
1257
|
+
async def get_conversation_kv_all(
|
|
1258
|
+
conversation_id: str,
|
|
1259
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1260
|
+
):
|
|
1261
|
+
"""Get all KV pairs for a conversation."""
|
|
1262
|
+
return conversation_store.get_all_kv(conversation_id)
|
|
1263
|
+
|
|
1264
|
+
@router.get("/conversations/{conversation_id}/kv/{key}")
|
|
1265
|
+
async def get_conversation_kv(
|
|
1266
|
+
conversation_id: str,
|
|
1267
|
+
key: str,
|
|
1268
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1269
|
+
):
|
|
1270
|
+
"""Get a single KV value."""
|
|
1271
|
+
value = conversation_store.get_kv(conversation_id, key)
|
|
1272
|
+
if value is None:
|
|
1273
|
+
raise HTTPException(404, f"Key '{key}' not found")
|
|
1274
|
+
return {"key": key, "value": value}
|
|
1275
|
+
|
|
1276
|
+
@router.delete("/conversations/{conversation_id}/kv/{key}")
|
|
1277
|
+
async def delete_conversation_kv(
|
|
1278
|
+
conversation_id: str,
|
|
1279
|
+
key: str,
|
|
1280
|
+
caller: AgentIdentity = Depends(require_write),
|
|
1281
|
+
):
|
|
1282
|
+
ok = conversation_store.delete_kv(conversation_id, key)
|
|
1283
|
+
if not ok:
|
|
1284
|
+
raise HTTPException(404, f"Key '{key}' not found")
|
|
1285
|
+
return {"success": True}
|
|
1286
|
+
|
|
1287
|
+
@router.get("/conversations/{conversation_id}/export")
|
|
1288
|
+
async def export_conversation(
|
|
1289
|
+
conversation_id: str,
|
|
1290
|
+
format: str = Query("messages", description="messages|prompt|markdown|dict"),
|
|
1291
|
+
last_n: Optional[int] = Query(None, description="Only export last N messages"),
|
|
1292
|
+
system_prompt: Optional[str] = Query(None),
|
|
1293
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1294
|
+
):
|
|
1295
|
+
"""Export a conversation as a ContextHub format."""
|
|
1296
|
+
conv = conversation_store.get(conversation_id, include_messages=False)
|
|
1297
|
+
if not conv:
|
|
1298
|
+
raise HTTPException(404, "Conversation not found")
|
|
1299
|
+
hub = conversation_store.to_hub(conversation_id, system_prompt=system_prompt, last_n=last_n)
|
|
1300
|
+
return hub.export(format)
|
|
1301
|
+
|
|
1302
|
+
# ==================================================================
|
|
1303
|
+
# Embedding capabilities
|
|
1304
|
+
# ==================================================================
|
|
1305
|
+
|
|
1306
|
+
@router.get("/capabilities")
|
|
1307
|
+
async def get_capabilities():
|
|
1308
|
+
"""Report available embedding/extraction capabilities."""
|
|
1309
|
+
return {
|
|
1310
|
+
**embedding_hooks.capabilities,
|
|
1311
|
+
"embedding_service": vector_store._embedding_service is not None,
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
# ==================================================================
|
|
1315
|
+
# Health / info
|
|
1316
|
+
# ==================================================================
|
|
1317
|
+
|
|
1318
|
+
@router.get("/health")
|
|
1319
|
+
async def context_health():
|
|
1320
|
+
# Lazy TTL cleanup
|
|
1321
|
+
try:
|
|
1322
|
+
conversation_store.cleanup_expired()
|
|
1323
|
+
except Exception:
|
|
1324
|
+
pass # optional
|
|
1325
|
+
|
|
1326
|
+
return {
|
|
1327
|
+
"status": "ok",
|
|
1328
|
+
"agents": len(agent_registry.list_agents()),
|
|
1329
|
+
"sessions": len(session_manager.list_sessions()),
|
|
1330
|
+
"vector_backend": vector_store._manager.backend_name if vector_store._manager else None,
|
|
1331
|
+
"vector_available": vector_store.available,
|
|
1332
|
+
"embedding_available": vector_store._embedding_service is not None,
|
|
1333
|
+
"embedding_capabilities": embedding_hooks.capabilities,
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
# ------------------------------------------------------------------
|
|
1337
|
+
# Agent Memory endpoints
|
|
1338
|
+
# ------------------------------------------------------------------
|
|
1339
|
+
|
|
1340
|
+
_agent_memory = None
|
|
1341
|
+
|
|
1342
|
+
def _get_memory():
|
|
1343
|
+
nonlocal _agent_memory
|
|
1344
|
+
if _agent_memory is None:
|
|
1345
|
+
if graph_registry:
|
|
1346
|
+
from ..context.agent_memory import AgentMemory
|
|
1347
|
+
_agent_memory = AgentMemory(graph_registry)
|
|
1348
|
+
else:
|
|
1349
|
+
raise HTTPException(status_code=503, detail="Graph registry not available")
|
|
1350
|
+
return _agent_memory
|
|
1351
|
+
|
|
1352
|
+
class RememberRequest(BaseModel):
|
|
1353
|
+
content: str = Field(..., min_length=1)
|
|
1354
|
+
tags: Optional[List[str]] = None
|
|
1355
|
+
confidence: float = 1.0
|
|
1356
|
+
source: Optional[str] = None
|
|
1357
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
1358
|
+
|
|
1359
|
+
class InteractionRequest(BaseModel):
|
|
1360
|
+
role: str = Field(..., min_length=1)
|
|
1361
|
+
content: str = Field(..., min_length=1)
|
|
1362
|
+
session_id: Optional[str] = None
|
|
1363
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
1364
|
+
|
|
1365
|
+
class BuildContextRequest(BaseModel):
|
|
1366
|
+
system_prompt: Optional[str] = None
|
|
1367
|
+
include_memories: bool = True
|
|
1368
|
+
include_recent_interactions: int = 10
|
|
1369
|
+
memory_tags: Optional[List[str]] = None
|
|
1370
|
+
memory_limit: int = 20
|
|
1371
|
+
max_tokens: int = 8000
|
|
1372
|
+
|
|
1373
|
+
@router.post("/context/agents/{agent_id}/memory")
|
|
1374
|
+
async def remember(agent_id: str, req: RememberRequest, agent=Depends(require_write)):
|
|
1375
|
+
"""Store a memory for an agent."""
|
|
1376
|
+
memory = _get_memory()
|
|
1377
|
+
mem_id = memory.remember(
|
|
1378
|
+
agent_id=agent_id,
|
|
1379
|
+
content=req.content,
|
|
1380
|
+
tags=req.tags,
|
|
1381
|
+
confidence=req.confidence,
|
|
1382
|
+
source=req.source,
|
|
1383
|
+
metadata=req.metadata,
|
|
1384
|
+
)
|
|
1385
|
+
return {"memory_id": mem_id, "agent_id": agent_id}
|
|
1386
|
+
|
|
1387
|
+
@router.get("/context/agents/{agent_id}/memory")
|
|
1388
|
+
async def recall(
|
|
1389
|
+
agent_id: str,
|
|
1390
|
+
query: Optional[str] = None,
|
|
1391
|
+
tags: Optional[str] = None,
|
|
1392
|
+
limit: int = 10,
|
|
1393
|
+
agent=Depends(require_read),
|
|
1394
|
+
):
|
|
1395
|
+
"""Recall memories for an agent."""
|
|
1396
|
+
memory = _get_memory()
|
|
1397
|
+
tag_list = tags.split(",") if tags else None
|
|
1398
|
+
memories = memory.recall(agent_id=agent_id, query=query, tags=tag_list, limit=limit)
|
|
1399
|
+
return {"agent_id": agent_id, "memories": memories, "count": len(memories)}
|
|
1400
|
+
|
|
1401
|
+
@router.delete("/context/agents/{agent_id}/memory/{memory_id}")
|
|
1402
|
+
async def forget(agent_id: str, memory_id: str, agent=Depends(require_write)):
|
|
1403
|
+
"""Remove a specific memory."""
|
|
1404
|
+
memory = _get_memory()
|
|
1405
|
+
ok = memory.forget(agent_id, memory_id)
|
|
1406
|
+
if not ok:
|
|
1407
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1408
|
+
return {"message": "Memory removed", "memory_id": memory_id}
|
|
1409
|
+
|
|
1410
|
+
@router.post("/context/agents/{agent_id}/sessions")
|
|
1411
|
+
async def start_session(agent_id: str, agent=Depends(require_write)):
|
|
1412
|
+
"""Start a new interaction session."""
|
|
1413
|
+
memory = _get_memory()
|
|
1414
|
+
session_id = memory.start_session(agent_id)
|
|
1415
|
+
return {"session_id": session_id, "agent_id": agent_id}
|
|
1416
|
+
|
|
1417
|
+
@router.post("/context/agents/{agent_id}/interactions")
|
|
1418
|
+
async def log_interaction(agent_id: str, req: InteractionRequest, agent=Depends(require_write)):
|
|
1419
|
+
"""Log an interaction turn."""
|
|
1420
|
+
memory = _get_memory()
|
|
1421
|
+
interaction_id = memory.log_interaction(
|
|
1422
|
+
agent_id=agent_id,
|
|
1423
|
+
role=req.role,
|
|
1424
|
+
content=req.content,
|
|
1425
|
+
session_id=req.session_id,
|
|
1426
|
+
metadata=req.metadata,
|
|
1427
|
+
)
|
|
1428
|
+
return {"interaction_id": interaction_id, "session_id": req.session_id}
|
|
1429
|
+
|
|
1430
|
+
@router.post("/context/agents/{agent_id}/build-context")
|
|
1431
|
+
async def build_agent_context(agent_id: str, req: BuildContextRequest, agent=Depends(require_write)):
|
|
1432
|
+
"""Build a ContextHub pre-loaded with agent memories and interactions."""
|
|
1433
|
+
memory = _get_memory()
|
|
1434
|
+
hub = memory.build_context(
|
|
1435
|
+
agent_id=agent_id,
|
|
1436
|
+
system_prompt=req.system_prompt,
|
|
1437
|
+
include_memories=req.include_memories,
|
|
1438
|
+
include_recent_interactions=req.include_recent_interactions,
|
|
1439
|
+
memory_tags=req.memory_tags,
|
|
1440
|
+
memory_limit=req.memory_limit,
|
|
1441
|
+
max_tokens=req.max_tokens,
|
|
1442
|
+
)
|
|
1443
|
+
return {
|
|
1444
|
+
"agent_id": agent_id,
|
|
1445
|
+
"messages": hub.to_messages(),
|
|
1446
|
+
"item_count": len(hub._items),
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
@router.get("/context/agents/{agent_id}/summary")
|
|
1450
|
+
async def get_agent_memory_summary(agent_id: str, agent=Depends(require_read)):
|
|
1451
|
+
"""Get summary of agent's memory state."""
|
|
1452
|
+
memory = _get_memory()
|
|
1453
|
+
return memory.get_agent_summary(agent_id)
|
|
1454
|
+
|
|
1455
|
+
@router.post("/context/agents/{agent_id}/share/{to_agent_id}")
|
|
1456
|
+
async def share_memory(
|
|
1457
|
+
agent_id: str, to_agent_id: str, memory_id: str = Query(...), agent=Depends(require_write)
|
|
1458
|
+
):
|
|
1459
|
+
"""Share a memory from one agent to another."""
|
|
1460
|
+
memory = _get_memory()
|
|
1461
|
+
ok = memory.share_memory(agent_id, to_agent_id, memory_id)
|
|
1462
|
+
if not ok:
|
|
1463
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1464
|
+
return {"message": "Memory shared", "from": agent_id, "to": to_agent_id}
|
|
1465
|
+
|
|
1466
|
+
# ==================================================================
|
|
1467
|
+
# Session Graph endpoints (unified knowledge + conversation layer)
|
|
1468
|
+
# ==================================================================
|
|
1469
|
+
|
|
1470
|
+
@router.get("/sessions/{session_id}/graph/schema")
|
|
1471
|
+
async def get_session_graph_schema(
|
|
1472
|
+
session_id: str,
|
|
1473
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1474
|
+
):
|
|
1475
|
+
"""Get the session graph schema definition."""
|
|
1476
|
+
builder = await _get_or_create_builder(session_id)
|
|
1477
|
+
if not builder:
|
|
1478
|
+
raise HTTPException(404, "Session not found")
|
|
1479
|
+
return builder.get_schema_dict()
|
|
1480
|
+
|
|
1481
|
+
@router.put("/sessions/{session_id}/graph/schema")
|
|
1482
|
+
async def update_session_graph_schema(
|
|
1483
|
+
session_id: str,
|
|
1484
|
+
req: Dict[str, Any],
|
|
1485
|
+
caller: AgentIdentity = Depends(require_agent_admin),
|
|
1486
|
+
):
|
|
1487
|
+
"""Update the session graph schema with new YAML content."""
|
|
1488
|
+
builder = await _get_or_create_builder(session_id)
|
|
1489
|
+
if not builder:
|
|
1490
|
+
raise HTTPException(404, "Session not found")
|
|
1491
|
+
yaml_content = req.get("yaml", "")
|
|
1492
|
+
if not yaml_content:
|
|
1493
|
+
raise HTTPException(400, "Missing 'yaml' field in request body")
|
|
1494
|
+
try:
|
|
1495
|
+
new_schema = await builder.update_schema(yaml_content)
|
|
1496
|
+
return {
|
|
1497
|
+
"success": True,
|
|
1498
|
+
"node_types": list(new_schema.node_types.keys()),
|
|
1499
|
+
"edge_types": list(new_schema.edge_types.keys()),
|
|
1500
|
+
}
|
|
1501
|
+
except Exception as e:
|
|
1502
|
+
raise HTTPException(400, f"Invalid schema: {e}")
|
|
1503
|
+
|
|
1504
|
+
@router.get("/sessions/{session_id}/graph/context")
|
|
1505
|
+
async def get_session_graph_context(
|
|
1506
|
+
session_id: str,
|
|
1507
|
+
system_prompt: str = Query("You are an AI assistant."),
|
|
1508
|
+
max_tokens: int = Query(8000),
|
|
1509
|
+
recent_turns: int = Query(20),
|
|
1510
|
+
format: str = Query("messages", description="messages|prompt|markdown|dict"),
|
|
1511
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1512
|
+
):
|
|
1513
|
+
"""Build graph-aware context from the unified session graph.
|
|
1514
|
+
|
|
1515
|
+
Follows REFERENCES edges from recent conversation turns to pull
|
|
1516
|
+
in contextually relevant knowledge nodes.
|
|
1517
|
+
"""
|
|
1518
|
+
builder = await _get_or_create_builder(session_id)
|
|
1519
|
+
if not builder:
|
|
1520
|
+
raise HTTPException(404, "Session not found")
|
|
1521
|
+
hub = builder.build_context(
|
|
1522
|
+
agent_id=caller.agent_id,
|
|
1523
|
+
system_prompt=system_prompt,
|
|
1524
|
+
max_tokens=max_tokens,
|
|
1525
|
+
recent_turns=recent_turns,
|
|
1526
|
+
)
|
|
1527
|
+
if format == "messages":
|
|
1528
|
+
return {"messages": hub.to_messages(), "item_count": len(hub.items())}
|
|
1529
|
+
elif format == "prompt":
|
|
1530
|
+
return {"prompt": hub.to_string(), "item_count": len(hub.items())}
|
|
1531
|
+
elif format == "markdown":
|
|
1532
|
+
return {"markdown": hub.to_markdown(), "item_count": len(hub.items())}
|
|
1533
|
+
else:
|
|
1534
|
+
return hub.to_dict()
|
|
1535
|
+
|
|
1536
|
+
@router.get("/sessions/{session_id}/graph/turns")
|
|
1537
|
+
async def get_session_graph_turns(
|
|
1538
|
+
session_id: str,
|
|
1539
|
+
limit: int = Query(20),
|
|
1540
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1541
|
+
):
|
|
1542
|
+
"""Get recent Turn nodes from the session graph."""
|
|
1543
|
+
builder = await _get_or_create_builder(session_id)
|
|
1544
|
+
if not builder:
|
|
1545
|
+
raise HTTPException(404, "Session not found")
|
|
1546
|
+
turns = builder.get_recent_turns(limit=limit)
|
|
1547
|
+
return {"turns": turns, "count": len(turns)}
|
|
1548
|
+
|
|
1549
|
+
@router.get("/sessions/{session_id}/graph/decisions")
|
|
1550
|
+
async def get_session_graph_decisions(
|
|
1551
|
+
session_id: str,
|
|
1552
|
+
include_done: bool = Query(False),
|
|
1553
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1554
|
+
):
|
|
1555
|
+
"""Get Decision nodes from the session graph."""
|
|
1556
|
+
builder = await _get_or_create_builder(session_id)
|
|
1557
|
+
if not builder:
|
|
1558
|
+
raise HTTPException(404, "Session not found")
|
|
1559
|
+
decisions = builder.get_decisions(include_done=include_done)
|
|
1560
|
+
return {"decisions": decisions, "count": len(decisions)}
|
|
1561
|
+
|
|
1562
|
+
@router.get("/sessions/{session_id}/graph/topics")
|
|
1563
|
+
async def get_session_graph_topics(
|
|
1564
|
+
session_id: str,
|
|
1565
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1566
|
+
):
|
|
1567
|
+
"""Get Topic nodes from the session graph, sorted by mention count."""
|
|
1568
|
+
builder = await _get_or_create_builder(session_id)
|
|
1569
|
+
if not builder:
|
|
1570
|
+
raise HTTPException(404, "Session not found")
|
|
1571
|
+
topics = builder.get_topics()
|
|
1572
|
+
return {"topics": topics, "count": len(topics)}
|
|
1573
|
+
|
|
1574
|
+
@router.get("/sessions/{session_id}/graph/stats")
|
|
1575
|
+
async def get_session_graph_stats(
|
|
1576
|
+
session_id: str,
|
|
1577
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1578
|
+
):
|
|
1579
|
+
"""Get summary statistics of the session graph."""
|
|
1580
|
+
builder = await _get_or_create_builder(session_id)
|
|
1581
|
+
if not builder:
|
|
1582
|
+
raise HTTPException(404, "Session not found")
|
|
1583
|
+
return builder.get_graph_stats()
|
|
1584
|
+
|
|
1585
|
+
# ==================================================================
|
|
1586
|
+
# Context manifest endpoint
|
|
1587
|
+
# ==================================================================
|
|
1588
|
+
|
|
1589
|
+
@router.get("/contexts/{context_id}/manifest")
|
|
1590
|
+
async def get_context_manifest(
|
|
1591
|
+
context_id: str,
|
|
1592
|
+
caller: AgentIdentity = Depends(require_read),
|
|
1593
|
+
):
|
|
1594
|
+
"""Get the pre-computed context quality manifest."""
|
|
1595
|
+
from contextsynapse.context.quality import get_manifest, ManifestBuilder, EntityIndex
|
|
1596
|
+
|
|
1597
|
+
redis_client = None
|
|
1598
|
+
try:
|
|
1599
|
+
import redis as _redis_mod
|
|
1600
|
+
import os
|
|
1601
|
+
url = os.environ.get("CONTEXTSYNAPSE_REDIS_URL") or os.environ.get("AICONTEXTDB_REDIS_URL", "redis://localhost:6379")
|
|
1602
|
+
redis_client = _redis_mod.from_url(url)
|
|
1603
|
+
except Exception:
|
|
1604
|
+
pass
|
|
1605
|
+
|
|
1606
|
+
manifest = get_manifest(context_id, redis_client)
|
|
1607
|
+
if manifest:
|
|
1608
|
+
return manifest
|
|
1609
|
+
|
|
1610
|
+
# Build on demand if not cached
|
|
1611
|
+
try:
|
|
1612
|
+
eidx = EntityIndex(redis_client=redis_client)
|
|
1613
|
+
builder = ManifestBuilder(entity_index=eidx, redis_client=redis_client)
|
|
1614
|
+
manifest = builder.build(context_id, context_id)
|
|
1615
|
+
return manifest
|
|
1616
|
+
except Exception as e:
|
|
1617
|
+
raise HTTPException(status_code=500, detail=f"Failed to build manifest: {e}")
|
|
1618
|
+
|
|
1619
|
+
return router
|