context-bridge-memory 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. context_bridge/__init__.py +5 -0
  2. context_bridge/__main__.py +21 -0
  3. context_bridge/api/__init__.py +1 -0
  4. context_bridge/api/access.py +67 -0
  5. context_bridge/api/app.py +187 -0
  6. context_bridge/api/deps.py +142 -0
  7. context_bridge/api/metrics.py +58 -0
  8. context_bridge/api/routes/__init__.py +1 -0
  9. context_bridge/api/routes/conflicts.py +51 -0
  10. context_bridge/api/routes/graph.py +61 -0
  11. context_bridge/api/routes/health.py +49 -0
  12. context_bridge/api/routes/insights.py +66 -0
  13. context_bridge/api/routes/learning.py +95 -0
  14. context_bridge/api/routes/lessons.py +88 -0
  15. context_bridge/api/routes/maintenance.py +76 -0
  16. context_bridge/api/routes/memory.py +221 -0
  17. context_bridge/api/routes/quality.py +23 -0
  18. context_bridge/api/routes/sessions.py +68 -0
  19. context_bridge/api/schemas.py +370 -0
  20. context_bridge/api/security.py +137 -0
  21. context_bridge/api/tracing.py +42 -0
  22. context_bridge/benchmark.py +120 -0
  23. context_bridge/config.py +159 -0
  24. context_bridge/core/__init__.py +1 -0
  25. context_bridge/core/chunking/__init__.py +18 -0
  26. context_bridge/core/chunking/base.py +63 -0
  27. context_bridge/core/chunking/recursive.py +58 -0
  28. context_bridge/core/chunking/semantic.py +73 -0
  29. context_bridge/core/embeddings/__init__.py +46 -0
  30. context_bridge/core/embeddings/base.py +43 -0
  31. context_bridge/core/embeddings/cohere_embedder.py +59 -0
  32. context_bridge/core/embeddings/fastembed_embedder.py +63 -0
  33. context_bridge/core/embeddings/hashing.py +80 -0
  34. context_bridge/core/embeddings/openai_embedder.py +63 -0
  35. context_bridge/core/events.py +72 -0
  36. context_bridge/core/graph/__init__.py +12 -0
  37. context_bridge/core/graph/extractor.py +86 -0
  38. context_bridge/core/graph/resolver.py +22 -0
  39. context_bridge/core/memory/__init__.py +8 -0
  40. context_bridge/core/memory/consolidation.py +38 -0
  41. context_bridge/core/memory/contradiction.py +104 -0
  42. context_bridge/core/memory/manager.py +1104 -0
  43. context_bridge/core/memory/policy.py +40 -0
  44. context_bridge/core/memory/redaction.py +47 -0
  45. context_bridge/core/memory/salience.py +123 -0
  46. context_bridge/core/memory/summarizer.py +174 -0
  47. context_bridge/core/models.py +139 -0
  48. context_bridge/core/retrieval/__init__.py +8 -0
  49. context_bridge/core/retrieval/budget.py +73 -0
  50. context_bridge/core/retrieval/mmr.py +69 -0
  51. context_bridge/core/retrieval/reranker.py +96 -0
  52. context_bridge/core/retrieval/retriever.py +175 -0
  53. context_bridge/core/tracing.py +32 -0
  54. context_bridge/core/vectorstore/__init__.py +23 -0
  55. context_bridge/core/vectorstore/base.py +63 -0
  56. context_bridge/core/vectorstore/qdrant_store.py +333 -0
  57. context_bridge/core/working/__init__.py +22 -0
  58. context_bridge/core/working/base.py +26 -0
  59. context_bridge/core/working/memory_store.py +33 -0
  60. context_bridge/core/working/redis_store.py +34 -0
  61. context_bridge/db/__init__.py +51 -0
  62. context_bridge/db/models.py +298 -0
  63. context_bridge/db/repository.py +633 -0
  64. context_bridge/db/session.py +37 -0
  65. context_bridge/py.typed +0 -0
  66. context_bridge/sdk/__init__.py +7 -0
  67. context_bridge/sdk/client.py +341 -0
  68. context_bridge/tokenizer.py +52 -0
  69. context_bridge_memory-0.1.0.dist-info/METADATA +485 -0
  70. context_bridge_memory-0.1.0.dist-info/RECORD +74 -0
  71. context_bridge_memory-0.1.0.dist-info/WHEEL +4 -0
  72. context_bridge_memory-0.1.0.dist-info/entry_points.txt +2 -0
  73. context_bridge_memory-0.1.0.dist-info/licenses/LICENSE +201 -0
  74. context_bridge_memory-0.1.0.dist-info/licenses/NOTICE +6 -0
@@ -0,0 +1,5 @@
1
+ """Context Bridge — shared neural memory middleware for multi-agent systems."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,21 @@
1
+ """Console entry point: run the API server with uvicorn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from context_bridge.config import get_settings
6
+
7
+
8
+ def main() -> None:
9
+ import uvicorn
10
+
11
+ settings = get_settings()
12
+ uvicorn.run(
13
+ "context_bridge.api.app:app",
14
+ host=settings.api_host,
15
+ port=settings.api_port,
16
+ log_level=settings.log_level,
17
+ )
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
@@ -0,0 +1 @@
1
+ """HTTP API layer (FastAPI)."""
@@ -0,0 +1,67 @@
1
+ """Role-based access control for namespaces.
2
+
3
+ Each API key resolves to a :class:`Rule`: a set of namespace glob patterns plus
4
+ the operations (``read`` / ``write``) it may perform. This is what makes the
5
+ shared pool safe for multi-tenant use — a key scoped to ``team-a*`` can never
6
+ read or write ``team-b`` memories.
7
+
8
+ Two configuration shapes feed it (rich overrides simple):
9
+
10
+ * ``API_KEY_NAMESPACES`` — ``{"key": ["ns-a", "ns-b"]}`` (implies read + write).
11
+ * ``API_KEY_POLICIES`` — ``{"key": {"namespaces": ["team-a*"],
12
+ "permissions": ["read"]}}``.
13
+
14
+ With no API keys configured, access is open.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from fnmatch import fnmatch
21
+
22
+ READ = "read"
23
+ WRITE = "write"
24
+ _ALL = ("*",)
25
+ _RW = frozenset({READ, WRITE})
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Rule:
30
+ namespaces: tuple[str, ...]
31
+ permissions: frozenset[str]
32
+
33
+
34
+ class AccessControl:
35
+ """Resolves and enforces per-key namespace/operation rules."""
36
+
37
+ def __init__(self, rules: dict[str, Rule], *, open_access: bool) -> None:
38
+ self.rules = rules
39
+ self.open_access = open_access
40
+
41
+ @classmethod
42
+ def build(cls, settings) -> AccessControl:
43
+ keys = settings.api_key_set()
44
+ rules: dict[str, Rule] = {}
45
+
46
+ for key, namespaces in settings.api_key_namespace_map().items():
47
+ rules[key] = Rule(tuple(namespaces) or _ALL, _RW)
48
+
49
+ for key, policy in settings.api_key_policies_map().items():
50
+ rules[key] = Rule(
51
+ tuple(policy.get("namespaces") or _ALL),
52
+ frozenset(policy.get("permissions") or [READ, WRITE]),
53
+ )
54
+
55
+ # Any configured key without an explicit rule gets full access.
56
+ for key in keys:
57
+ rules.setdefault(key, Rule(_ALL, _RW))
58
+
59
+ return cls(rules, open_access=not keys)
60
+
61
+ def allows(self, key: str | None, namespace: str, operation: str) -> bool:
62
+ if self.open_access:
63
+ return True
64
+ rule = self.rules.get(key or "")
65
+ if rule is None or operation not in rule.permissions:
66
+ return False
67
+ return any(fnmatch(namespace, pattern) for pattern in rule.namespaces)
@@ -0,0 +1,187 @@
1
+ """FastAPI application factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import logging
8
+ import time
9
+ import uuid
10
+ from collections.abc import AsyncIterator, Awaitable, Callable
11
+ from contextlib import asynccontextmanager
12
+
13
+ from fastapi import Depends, FastAPI, Request, Response
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.responses import JSONResponse
16
+
17
+ from context_bridge import __version__
18
+ from context_bridge.api import metrics
19
+ from context_bridge.api.access import AccessControl
20
+ from context_bridge.api.deps import build_container
21
+ from context_bridge.api.routes import (
22
+ conflicts,
23
+ graph,
24
+ health,
25
+ insights,
26
+ learning,
27
+ lessons,
28
+ maintenance,
29
+ memory,
30
+ quality,
31
+ sessions,
32
+ )
33
+ from context_bridge.api.security import api_key_guard, build_rate_limiter, rate_limit_guard
34
+ from context_bridge.api.tracing import setup_tracing
35
+ from context_bridge.config import Settings, get_settings
36
+
37
+ logger = logging.getLogger("context_bridge")
38
+
39
+ API_V1 = "/v1"
40
+ _REQUEST_ID_HEADER = "X-Request-ID"
41
+
42
+
43
+ async def _sweep_loop(app: FastAPI, interval: int) -> None:
44
+ """Periodically purge TTL-expired memories until cancelled."""
45
+ while True:
46
+ await asyncio.sleep(interval)
47
+ try:
48
+ deleted = await asyncio.to_thread(app.state.container.manager.sweep_expired)
49
+ if deleted:
50
+ logger.info("ttl sweep removed %d expired memories", deleted)
51
+ except Exception: # pragma: no cover - defensive background task
52
+ logger.exception("ttl sweep failed")
53
+
54
+
55
+ async def _maintenance_loop(app: FastAPI, interval: int) -> None:
56
+ """Periodically run a full housekeeping cycle until cancelled."""
57
+ from context_bridge.api.routes.maintenance import _run_maintenance
58
+
59
+ while True:
60
+ await asyncio.sleep(interval)
61
+ try:
62
+ settings = app.state.settings
63
+ result = await asyncio.to_thread(
64
+ _run_maintenance, app.state.container.manager, settings
65
+ )
66
+ logger.info("maintenance cycle: %s", result)
67
+ except Exception: # pragma: no cover - defensive background task
68
+ logger.exception("maintenance cycle failed")
69
+
70
+
71
+ @asynccontextmanager
72
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
73
+ """Build the component graph once at startup, tear down at shutdown."""
74
+ settings: Settings = app.state.settings
75
+ app.state.container = build_container(settings)
76
+
77
+ tasks: list[asyncio.Task] = []
78
+ if settings.sweep_interval_seconds > 0:
79
+ tasks.append(asyncio.create_task(_sweep_loop(app, settings.sweep_interval_seconds)))
80
+ if settings.maintenance_interval_seconds > 0:
81
+ tasks.append(
82
+ asyncio.create_task(_maintenance_loop(app, settings.maintenance_interval_seconds))
83
+ )
84
+
85
+ try:
86
+ yield
87
+ finally:
88
+ for task in tasks:
89
+ task.cancel()
90
+ with contextlib.suppress(asyncio.CancelledError):
91
+ await task
92
+ app.state.container = None
93
+
94
+
95
+ def create_app(settings: Settings | None = None) -> FastAPI:
96
+ """Create and configure the Context Bridge API application."""
97
+ settings = settings or get_settings()
98
+ app = FastAPI(
99
+ title="Context Bridge",
100
+ version=__version__,
101
+ summary="Shared neural memory middleware for multi-agent systems.",
102
+ lifespan=lifespan,
103
+ )
104
+ app.state.settings = settings
105
+ app.state.rate_limiter = build_rate_limiter(settings)
106
+ app.state.access = AccessControl.build(settings)
107
+
108
+ setup_tracing(app, settings)
109
+ _install_middleware(app, settings)
110
+ _install_error_handler(app)
111
+
112
+ @app.get("/", tags=["meta"])
113
+ def root() -> dict:
114
+ """Service banner with links to docs, health and metrics."""
115
+ return {
116
+ "name": "Context Bridge",
117
+ "description": "Shared neural memory middleware for multi-agent systems.",
118
+ "version": __version__,
119
+ "docs": "/docs",
120
+ "health": "/health",
121
+ "metrics": "/metrics" if settings.metrics_enabled else None,
122
+ "api": API_V1,
123
+ }
124
+
125
+ guarded = [Depends(api_key_guard), Depends(rate_limit_guard)]
126
+ app.include_router(health.router)
127
+ if settings.metrics_enabled:
128
+ app.include_router(metrics.router)
129
+ app.include_router(memory.router, prefix=API_V1, dependencies=guarded)
130
+ app.include_router(sessions.router, prefix=API_V1, dependencies=guarded)
131
+ app.include_router(maintenance.router, prefix=API_V1, dependencies=guarded)
132
+ app.include_router(conflicts.router, prefix=API_V1, dependencies=guarded)
133
+ app.include_router(graph.router, prefix=API_V1, dependencies=guarded)
134
+ app.include_router(learning.router, prefix=API_V1, dependencies=guarded)
135
+ app.include_router(lessons.router, prefix=API_V1, dependencies=guarded)
136
+ app.include_router(quality.router, prefix=API_V1, dependencies=guarded)
137
+ app.include_router(insights.router, prefix=API_V1, dependencies=guarded)
138
+ return app
139
+
140
+
141
+ def _install_middleware(app: FastAPI, settings: Settings) -> None:
142
+ origins = settings.cors_origin_list()
143
+ # Browsers reject (and it is unsafe to send) credentials with a "*" origin,
144
+ # so only enable credentialed CORS when explicit origins are configured.
145
+ allow_all = origins == ["*"]
146
+ app.add_middleware(
147
+ CORSMiddleware,
148
+ allow_origins=origins,
149
+ allow_credentials=not allow_all,
150
+ allow_methods=["*"],
151
+ allow_headers=["*"],
152
+ )
153
+
154
+ @app.middleware("http")
155
+ async def _observability(
156
+ request: Request, call_next: Callable[[Request], Awaitable[Response]]
157
+ ) -> Response:
158
+ request_id = request.headers.get(_REQUEST_ID_HEADER) or uuid.uuid4().hex
159
+ request.state.request_id = request_id
160
+ start = time.perf_counter()
161
+ response = await call_next(request)
162
+ elapsed = time.perf_counter() - start
163
+ response.headers[_REQUEST_ID_HEADER] = request_id
164
+ if settings.metrics_enabled:
165
+ route = request.scope.get("route")
166
+ endpoint = getattr(route, "path", request.url.path)
167
+ metrics.REQUEST_LATENCY.labels(
168
+ method=request.method, endpoint=endpoint, status=str(response.status_code)
169
+ ).observe(elapsed)
170
+ return response
171
+
172
+
173
+ def _install_error_handler(app: FastAPI) -> None:
174
+ @app.exception_handler(Exception)
175
+ async def _unhandled(request: Request, exc: Exception) -> JSONResponse:
176
+ request_id = getattr(request.state, "request_id", None)
177
+ logger.exception("unhandled error (request_id=%s)", request_id)
178
+ return JSONResponse(
179
+ status_code=500,
180
+ content={
181
+ "error": {"type": "internal_error", "message": "internal server error"},
182
+ "request_id": request_id,
183
+ },
184
+ )
185
+
186
+
187
+ app = create_app()
@@ -0,0 +1,142 @@
1
+ """Dependency wiring: build the component graph from settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from fastapi import Request
8
+
9
+ from context_bridge.config import Settings
10
+ from context_bridge.core.chunking import build_chunker
11
+ from context_bridge.core.embeddings import build_embedder
12
+ from context_bridge.core.embeddings.base import Embedder
13
+ from context_bridge.core.events import CompositeEmitter, EventEmitter, build_emitter
14
+ from context_bridge.core.graph.extractor import build_extractor
15
+ from context_bridge.core.memory.contradiction import build_detector
16
+ from context_bridge.core.memory.manager import CognitiveServices, MemoryManager
17
+ from context_bridge.core.memory.policy import WritePolicy
18
+ from context_bridge.core.memory.redaction import build_redactor
19
+ from context_bridge.core.memory.summarizer import build_summarizer
20
+ from context_bridge.core.retrieval import Retriever, build_reranker
21
+ from context_bridge.core.retrieval.retriever import RetrievalParams
22
+ from context_bridge.core.vectorstore import build_vector_store
23
+ from context_bridge.core.vectorstore.base import VectorStore
24
+ from context_bridge.core.working import build_working_memory
25
+ from context_bridge.db import (
26
+ AgentProfileRepository,
27
+ ConflictRepository,
28
+ Database,
29
+ EpisodeRepository,
30
+ FeedbackRepository,
31
+ GraphRepository,
32
+ LessonRepository,
33
+ ParentRepository,
34
+ ProcedureRepository,
35
+ )
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class Container:
40
+ """Holds the constructed singletons for the lifetime of the process."""
41
+
42
+ settings: Settings
43
+ embedder: Embedder
44
+ store: VectorStore
45
+ db: Database
46
+ manager: MemoryManager
47
+
48
+
49
+ def build_container(settings: Settings) -> Container:
50
+ """Construct every component from configuration and wire the manager."""
51
+ embedder = build_embedder(settings)
52
+ store = build_vector_store(
53
+ settings, dim=embedder.dense_dim, supports_sparse=embedder.supports_sparse
54
+ )
55
+ working = build_working_memory(settings)
56
+ db = Database(settings.database_url)
57
+ db.create_all()
58
+ episodes = EpisodeRepository(db)
59
+ parents = ParentRepository(db)
60
+ feedback = FeedbackRepository(db)
61
+ conflicts = ConflictRepository(db)
62
+ graph = GraphRepository(db)
63
+ agents = AgentProfileRepository(db)
64
+ procedures = ProcedureRepository(db)
65
+ lessons = LessonRepository(db)
66
+
67
+ reranker = build_reranker(settings)
68
+ retriever = Retriever(
69
+ embedder=embedder,
70
+ store=store,
71
+ reranker=reranker,
72
+ parent_lookup=parents.get_texts,
73
+ feedback_lookup=feedback.scores,
74
+ feedback_weight=settings.feedback_weight,
75
+ confidence_weight=settings.confidence_weight,
76
+ )
77
+
78
+ policy = WritePolicy(
79
+ dedup_threshold=settings.dedup_threshold,
80
+ min_confidence=settings.min_confidence,
81
+ )
82
+ defaults = RetrievalParams(
83
+ top_k=settings.default_top_k,
84
+ token_budget=settings.default_token_budget,
85
+ candidate_pool=settings.prefetch_limit,
86
+ mmr_lambda=settings.mmr_lambda,
87
+ )
88
+ cognitive = CognitiveServices(
89
+ redactor=build_redactor(settings),
90
+ detector=build_detector(settings),
91
+ extractor=build_extractor(settings),
92
+ feedback=feedback,
93
+ conflicts=conflicts,
94
+ graph=graph,
95
+ agents=agents,
96
+ procedures=procedures,
97
+ lessons=lessons,
98
+ graph_extraction=settings.graph_extraction,
99
+ contradiction_similarity=settings.contradiction_similarity,
100
+ lessons_enabled=settings.lessons_enabled,
101
+ lessons_top_k=settings.lessons_top_k,
102
+ lessons_min_score=settings.lessons_min_score,
103
+ belief_revision=settings.belief_revision,
104
+ conflict_loser_decay=settings.conflict_loser_decay,
105
+ )
106
+ manager = MemoryManager(
107
+ chunker=build_chunker(settings, embedder=embedder),
108
+ embedder=embedder,
109
+ store=store,
110
+ retriever=retriever,
111
+ working=working,
112
+ episodes=episodes,
113
+ parents=parents,
114
+ policy=policy,
115
+ defaults=defaults,
116
+ summarizer=build_summarizer(settings),
117
+ cognitive=cognitive,
118
+ events=_build_events(settings),
119
+ )
120
+ return Container(settings=settings, embedder=embedder, store=store, db=db, manager=manager)
121
+
122
+
123
+ def _build_events(settings: Settings) -> EventEmitter:
124
+ """Compose the event sinks: Prometheus metrics (if enabled) plus webhooks."""
125
+ from context_bridge.api.metrics import MetricsEmitter
126
+
127
+ sinks: list[EventEmitter] = []
128
+ if settings.metrics_enabled:
129
+ sinks.append(MetricsEmitter())
130
+ webhooks = build_emitter(settings.webhook_url_list(), timeout=settings.webhook_timeout_seconds)
131
+ sinks.append(webhooks)
132
+ return CompositeEmitter(sinks)
133
+
134
+
135
+ def get_container(request: Request) -> Container:
136
+ """FastAPI dependency: the process-wide container from app state."""
137
+ return request.app.state.container
138
+
139
+
140
+ def get_manager(request: Request) -> MemoryManager:
141
+ """FastAPI dependency: the shared :class:`MemoryManager`."""
142
+ return request.app.state.container.manager
@@ -0,0 +1,58 @@
1
+ """Prometheus metrics and the /metrics exposition endpoint.
2
+
3
+ Counters and histograms are module-level singletons (registered once against
4
+ the default registry) and are incremented from the route handlers — the core
5
+ domain layer stays free of any metrics dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from fastapi import APIRouter, Response
11
+ from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest
12
+
13
+ WRITES = Counter("cb_memory_writes_total", "Total memory write requests")
14
+ CHUNKS_STORED = Counter("cb_chunks_stored_total", "Chunks persisted to the store")
15
+ CHUNKS_DEDUPED = Counter("cb_chunks_deduped_total", "Chunks suppressed as duplicates")
16
+
17
+ QUERIES = Counter("cb_queries_total", "Total memory query requests")
18
+ QUERY_TOKENS = Histogram(
19
+ "cb_query_tokens_used",
20
+ "Tokens in the assembled context per query",
21
+ buckets=(0, 64, 128, 256, 512, 1024, 2048, 4096, 8192),
22
+ )
23
+ QUERY_CHUNKS = Histogram(
24
+ "cb_query_chunks_returned",
25
+ "Chunks returned per query",
26
+ buckets=(0, 1, 2, 4, 8, 16, 32),
27
+ )
28
+
29
+ SWEEP_DELETED = Counter("cb_sweep_deleted_total", "Expired memories removed by sweeps")
30
+ MAINTENANCE_RUNS = Counter("cb_maintenance_runs_total", "Maintenance cycles executed")
31
+
32
+ EVENTS = Counter("cb_events_total", "Notable memory events emitted", labelnames=("type",))
33
+
34
+ REQUEST_LATENCY = Histogram(
35
+ "cb_request_latency_seconds",
36
+ "HTTP request latency",
37
+ labelnames=("method", "endpoint", "status"),
38
+ )
39
+
40
+
41
+ class MetricsEmitter:
42
+ """An :class:`EventEmitter` that records each event as a Prometheus counter.
43
+
44
+ This keeps the core domain layer free of any metrics dependency: events are
45
+ emitted there, and the wiring layer composes this emitter to observe them.
46
+ """
47
+
48
+ def emit(self, event_type: str, namespace: str, data: dict) -> None:
49
+ EVENTS.labels(type=event_type).inc()
50
+
51
+
52
+ router = APIRouter(tags=["metrics"])
53
+
54
+
55
+ @router.get("/metrics")
56
+ def metrics() -> Response:
57
+ """Expose metrics in the Prometheus text exposition format."""
58
+ return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
@@ -0,0 +1 @@
1
+ """API route modules."""
@@ -0,0 +1,51 @@
1
+ """Truth-maintenance endpoints: inspect and resolve detected contradictions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
6
+
7
+ from context_bridge.api.deps import get_manager
8
+ from context_bridge.api.schemas import AutoResolveResponse, ConflictResolveRequest
9
+ from context_bridge.api.security import authorize
10
+ from context_bridge.core.memory.manager import MemoryManager
11
+
12
+ router = APIRouter(prefix="/conflicts", tags=["conflicts"])
13
+
14
+
15
+ @router.get("")
16
+ def list_conflicts(
17
+ request: Request,
18
+ namespace: str = Query(default="default"),
19
+ status_filter: str | None = Query(default=None, alias="status"),
20
+ manager: MemoryManager = Depends(get_manager),
21
+ ) -> dict:
22
+ authorize(request, namespace, "read")
23
+ return {"conflicts": manager.list_conflicts(namespace=namespace, status=status_filter)}
24
+
25
+
26
+ @router.post("/auto-resolve", response_model=AutoResolveResponse)
27
+ def auto_resolve(
28
+ request: Request,
29
+ namespace: str = Query(default="default"),
30
+ manager: MemoryManager = Depends(get_manager),
31
+ ) -> AutoResolveResponse:
32
+ """Auto-close contradictions where the evidence is decisive (belief revision)."""
33
+ authorize(request, namespace, "write")
34
+ settings = request.app.state.settings
35
+ result = manager.auto_resolve_conflicts(
36
+ namespace=namespace, min_gap=settings.auto_resolve_min_gap
37
+ )
38
+ return AutoResolveResponse(**result)
39
+
40
+
41
+ @router.post("/{conflict_id}/resolve", status_code=status.HTTP_204_NO_CONTENT)
42
+ def resolve_conflict(
43
+ conflict_id: str,
44
+ req: ConflictResolveRequest,
45
+ request: Request,
46
+ namespace: str = Query(default="default"),
47
+ manager: MemoryManager = Depends(get_manager),
48
+ ) -> None:
49
+ authorize(request, namespace, "write")
50
+ if not manager.resolve_conflict(conflict_id, winner_id=req.winner_id):
51
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="conflict not found")
@@ -0,0 +1,61 @@
1
+ """Knowledge-graph endpoints: traverse entities and relations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends, Query, Request
6
+
7
+ from context_bridge.api.deps import get_manager
8
+ from context_bridge.api.schemas import (
9
+ AliasRequest,
10
+ AliasResponse,
11
+ AlignRequest,
12
+ AlignResponse,
13
+ GraphEdgeOut,
14
+ GraphResponse,
15
+ )
16
+ from context_bridge.api.security import authorize
17
+ from context_bridge.core.memory.manager import MemoryManager
18
+
19
+ router = APIRouter(prefix="/graph", tags=["graph"])
20
+
21
+
22
+ @router.get("/neighbors", response_model=GraphResponse)
23
+ def neighbors(
24
+ request: Request,
25
+ entity: str = Query(..., min_length=1),
26
+ namespace: str = Query(default="default"),
27
+ hops: int = Query(default=1, ge=1, le=4),
28
+ manager: MemoryManager = Depends(get_manager),
29
+ ) -> GraphResponse:
30
+ """Return relations reachable from ``entity`` within ``hops`` edges."""
31
+ authorize(request, namespace, "read")
32
+ edges = manager.graph_neighbors(namespace=namespace, entity=entity, hops=hops)
33
+ return GraphResponse(entity=entity, edges=[GraphEdgeOut(**e) for e in edges])
34
+
35
+
36
+ @router.post("/aliases", response_model=AliasResponse)
37
+ def add_alias(
38
+ request: Request,
39
+ body: AliasRequest,
40
+ manager: MemoryManager = Depends(get_manager),
41
+ ) -> AliasResponse:
42
+ """Declare that ``alias`` refers to the same entity as ``canonical``."""
43
+ authorize(request, body.namespace, "write")
44
+ registered = manager.add_alias(
45
+ namespace=body.namespace, alias=body.alias, canonical=body.canonical
46
+ )
47
+ return AliasResponse(
48
+ registered=registered, aliases=manager.list_aliases(namespace=body.namespace)
49
+ )
50
+
51
+
52
+ @router.post("/align", response_model=AlignResponse)
53
+ def align(
54
+ request: Request,
55
+ body: AlignRequest,
56
+ manager: MemoryManager = Depends(get_manager),
57
+ ) -> AlignResponse:
58
+ """Auto-merge surface variants of the same entity onto one canonical name."""
59
+ authorize(request, body.namespace, "write")
60
+ result = manager.align_graph(namespace=body.namespace)
61
+ return AlignResponse(**result)
@@ -0,0 +1,49 @@
1
+ """Liveness and readiness endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends
6
+ from sqlalchemy import text
7
+
8
+ from context_bridge.api.deps import Container, get_container
9
+ from context_bridge.api.schemas import HealthResponse
10
+
11
+ router = APIRouter(tags=["health"])
12
+
13
+
14
+ @router.get("/health", response_model=HealthResponse)
15
+ def health() -> HealthResponse:
16
+ """Liveness check — the process is up and serving."""
17
+ return HealthResponse(status="ok", components={})
18
+
19
+
20
+ @router.get("/healthz", response_model=HealthResponse)
21
+ def readiness(container: Container = Depends(get_container)) -> HealthResponse:
22
+ """Readiness check — verify backing services are reachable."""
23
+ components: dict[str, str] = {}
24
+
25
+ try:
26
+ with container.db.session() as session:
27
+ session.execute(text("SELECT 1"))
28
+ components["database"] = "ok"
29
+ except Exception as exc: # pragma: no cover - depends on live infra
30
+ components["database"] = f"error: {exc}"
31
+
32
+ try:
33
+ container.store.get("readiness-probe")
34
+ components["vector_store"] = "ok"
35
+ except Exception as exc: # pragma: no cover - depends on live infra
36
+ components["vector_store"] = f"error: {exc}"
37
+
38
+ settings = container.settings
39
+ if "redis" in (settings.working_provider, settings.rate_limit_backend):
40
+ try: # pragma: no cover - depends on live infra
41
+ import redis
42
+
43
+ redis.Redis.from_url(settings.redis_url).ping()
44
+ components["redis"] = "ok"
45
+ except Exception as exc: # pragma: no cover - depends on live infra
46
+ components["redis"] = f"error: {exc}"
47
+
48
+ status = "ok" if all(v == "ok" for v in components.values()) else "degraded"
49
+ return HealthResponse(status=status, components=components)