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.
- context_bridge/__init__.py +5 -0
- context_bridge/__main__.py +21 -0
- context_bridge/api/__init__.py +1 -0
- context_bridge/api/access.py +67 -0
- context_bridge/api/app.py +187 -0
- context_bridge/api/deps.py +142 -0
- context_bridge/api/metrics.py +58 -0
- context_bridge/api/routes/__init__.py +1 -0
- context_bridge/api/routes/conflicts.py +51 -0
- context_bridge/api/routes/graph.py +61 -0
- context_bridge/api/routes/health.py +49 -0
- context_bridge/api/routes/insights.py +66 -0
- context_bridge/api/routes/learning.py +95 -0
- context_bridge/api/routes/lessons.py +88 -0
- context_bridge/api/routes/maintenance.py +76 -0
- context_bridge/api/routes/memory.py +221 -0
- context_bridge/api/routes/quality.py +23 -0
- context_bridge/api/routes/sessions.py +68 -0
- context_bridge/api/schemas.py +370 -0
- context_bridge/api/security.py +137 -0
- context_bridge/api/tracing.py +42 -0
- context_bridge/benchmark.py +120 -0
- context_bridge/config.py +159 -0
- context_bridge/core/__init__.py +1 -0
- context_bridge/core/chunking/__init__.py +18 -0
- context_bridge/core/chunking/base.py +63 -0
- context_bridge/core/chunking/recursive.py +58 -0
- context_bridge/core/chunking/semantic.py +73 -0
- context_bridge/core/embeddings/__init__.py +46 -0
- context_bridge/core/embeddings/base.py +43 -0
- context_bridge/core/embeddings/cohere_embedder.py +59 -0
- context_bridge/core/embeddings/fastembed_embedder.py +63 -0
- context_bridge/core/embeddings/hashing.py +80 -0
- context_bridge/core/embeddings/openai_embedder.py +63 -0
- context_bridge/core/events.py +72 -0
- context_bridge/core/graph/__init__.py +12 -0
- context_bridge/core/graph/extractor.py +86 -0
- context_bridge/core/graph/resolver.py +22 -0
- context_bridge/core/memory/__init__.py +8 -0
- context_bridge/core/memory/consolidation.py +38 -0
- context_bridge/core/memory/contradiction.py +104 -0
- context_bridge/core/memory/manager.py +1104 -0
- context_bridge/core/memory/policy.py +40 -0
- context_bridge/core/memory/redaction.py +47 -0
- context_bridge/core/memory/salience.py +123 -0
- context_bridge/core/memory/summarizer.py +174 -0
- context_bridge/core/models.py +139 -0
- context_bridge/core/retrieval/__init__.py +8 -0
- context_bridge/core/retrieval/budget.py +73 -0
- context_bridge/core/retrieval/mmr.py +69 -0
- context_bridge/core/retrieval/reranker.py +96 -0
- context_bridge/core/retrieval/retriever.py +175 -0
- context_bridge/core/tracing.py +32 -0
- context_bridge/core/vectorstore/__init__.py +23 -0
- context_bridge/core/vectorstore/base.py +63 -0
- context_bridge/core/vectorstore/qdrant_store.py +333 -0
- context_bridge/core/working/__init__.py +22 -0
- context_bridge/core/working/base.py +26 -0
- context_bridge/core/working/memory_store.py +33 -0
- context_bridge/core/working/redis_store.py +34 -0
- context_bridge/db/__init__.py +51 -0
- context_bridge/db/models.py +298 -0
- context_bridge/db/repository.py +633 -0
- context_bridge/db/session.py +37 -0
- context_bridge/py.typed +0 -0
- context_bridge/sdk/__init__.py +7 -0
- context_bridge/sdk/client.py +341 -0
- context_bridge/tokenizer.py +52 -0
- context_bridge_memory-0.1.0.dist-info/METADATA +485 -0
- context_bridge_memory-0.1.0.dist-info/RECORD +74 -0
- context_bridge_memory-0.1.0.dist-info/WHEEL +4 -0
- context_bridge_memory-0.1.0.dist-info/entry_points.txt +2 -0
- context_bridge_memory-0.1.0.dist-info/licenses/LICENSE +201 -0
- context_bridge_memory-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Insight endpoints: namespace health panel and belief timeline (memory diff)."""
|
|
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
|
+
BeliefTimelineResponse,
|
|
10
|
+
HealthPanelResponse,
|
|
11
|
+
ImportResponse,
|
|
12
|
+
NamespaceExport,
|
|
13
|
+
)
|
|
14
|
+
from context_bridge.api.security import authorize
|
|
15
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
16
|
+
|
|
17
|
+
router = APIRouter(tags=["insights"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/namespaces/{namespace}/health", response_model=HealthPanelResponse)
|
|
21
|
+
def namespace_health(
|
|
22
|
+
namespace: str,
|
|
23
|
+
request: Request,
|
|
24
|
+
manager: MemoryManager = Depends(get_manager),
|
|
25
|
+
) -> HealthPanelResponse:
|
|
26
|
+
"""A single pulse-check: volume, trust distribution, conflicts, lessons, quality."""
|
|
27
|
+
authorize(request, namespace, "read")
|
|
28
|
+
return HealthPanelResponse(**manager.namespace_health(namespace=namespace))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/namespaces/{namespace}/beliefs", response_model=BeliefTimelineResponse)
|
|
32
|
+
def belief_timeline(
|
|
33
|
+
namespace: str,
|
|
34
|
+
request: Request,
|
|
35
|
+
query: str = Query(..., min_length=1),
|
|
36
|
+
limit: int = Query(default=50, ge=1, le=200),
|
|
37
|
+
manager: MemoryManager = Depends(get_manager),
|
|
38
|
+
) -> BeliefTimelineResponse:
|
|
39
|
+
"""Trace how belief about a topic evolved over time (a memory diff)."""
|
|
40
|
+
authorize(request, namespace, "read")
|
|
41
|
+
events = manager.belief_timeline(query=query, namespace=namespace, limit=limit)
|
|
42
|
+
return BeliefTimelineResponse(query=query, events=events)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/namespaces/{namespace}/export", response_model=NamespaceExport)
|
|
46
|
+
def export_namespace(
|
|
47
|
+
namespace: str,
|
|
48
|
+
request: Request,
|
|
49
|
+
manager: MemoryManager = Depends(get_manager),
|
|
50
|
+
) -> NamespaceExport:
|
|
51
|
+
"""Serialize a namespace's memories, lessons and procedures for backup/transfer."""
|
|
52
|
+
authorize(request, namespace, "read")
|
|
53
|
+
return NamespaceExport(**manager.export_namespace(namespace=namespace))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/namespaces/{namespace}/import", response_model=ImportResponse)
|
|
57
|
+
def import_namespace(
|
|
58
|
+
namespace: str,
|
|
59
|
+
body: NamespaceExport,
|
|
60
|
+
request: Request,
|
|
61
|
+
manager: MemoryManager = Depends(get_manager),
|
|
62
|
+
) -> ImportResponse:
|
|
63
|
+
"""Recreate memories, lessons and procedures from an exported document."""
|
|
64
|
+
authorize(request, namespace, "write")
|
|
65
|
+
result = manager.import_namespace(namespace=namespace, payload=body.model_dump())
|
|
66
|
+
return ImportResponse(**result)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Collective-learning endpoints: agent reputation, outcomes, procedures."""
|
|
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 (
|
|
9
|
+
AgentsResponse,
|
|
10
|
+
OutcomeRequest,
|
|
11
|
+
OutcomeResponse,
|
|
12
|
+
ProcedureCreate,
|
|
13
|
+
ProcedureCreated,
|
|
14
|
+
ProcedureOutcomeRequest,
|
|
15
|
+
ProceduresResponse,
|
|
16
|
+
)
|
|
17
|
+
from context_bridge.api.security import authorize
|
|
18
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
19
|
+
|
|
20
|
+
router = APIRouter(tags=["learning"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/agents", response_model=AgentsResponse)
|
|
24
|
+
def agent_leaderboard(
|
|
25
|
+
request: Request,
|
|
26
|
+
namespace: str = Query(default="default"),
|
|
27
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
28
|
+
manager: MemoryManager = Depends(get_manager),
|
|
29
|
+
) -> AgentsResponse:
|
|
30
|
+
"""Rank agents by accumulated reputation in a namespace."""
|
|
31
|
+
authorize(request, namespace, "read")
|
|
32
|
+
return AgentsResponse(agents=manager.agent_leaderboard(namespace=namespace, limit=limit))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("/outcomes", response_model=OutcomeResponse)
|
|
36
|
+
def record_outcome(
|
|
37
|
+
req: OutcomeRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
38
|
+
) -> OutcomeResponse:
|
|
39
|
+
"""Credit a session's memories and agents by its task outcome."""
|
|
40
|
+
authorize(request, req.namespace, "write")
|
|
41
|
+
result = manager.record_outcome(
|
|
42
|
+
session_id=req.session_id,
|
|
43
|
+
namespace=req.namespace,
|
|
44
|
+
success=req.success,
|
|
45
|
+
weight=req.weight,
|
|
46
|
+
lesson=req.lesson,
|
|
47
|
+
lesson_trigger=req.lesson_trigger,
|
|
48
|
+
severity=req.severity,
|
|
49
|
+
)
|
|
50
|
+
return OutcomeResponse(**result)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post("/procedures", response_model=ProcedureCreated)
|
|
54
|
+
def create_procedure(
|
|
55
|
+
req: ProcedureCreate, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
56
|
+
) -> ProcedureCreated:
|
|
57
|
+
"""Store a reusable playbook (procedural memory)."""
|
|
58
|
+
authorize(request, req.namespace, "write")
|
|
59
|
+
proc_id = manager.create_procedure(
|
|
60
|
+
namespace=req.namespace,
|
|
61
|
+
title=req.title,
|
|
62
|
+
steps=req.steps,
|
|
63
|
+
tags=req.tags,
|
|
64
|
+
created_by=req.created_by,
|
|
65
|
+
)
|
|
66
|
+
return ProcedureCreated(id=proc_id or "")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.get("/procedures", response_model=ProceduresResponse)
|
|
70
|
+
def list_procedures(
|
|
71
|
+
request: Request,
|
|
72
|
+
namespace: str = Query(default="default"),
|
|
73
|
+
query: str | None = Query(default=None),
|
|
74
|
+
limit: int = Query(default=50, ge=1, le=200),
|
|
75
|
+
manager: MemoryManager = Depends(get_manager),
|
|
76
|
+
) -> ProceduresResponse:
|
|
77
|
+
"""List playbooks for a namespace, best-performing first."""
|
|
78
|
+
authorize(request, namespace, "read")
|
|
79
|
+
return ProceduresResponse(
|
|
80
|
+
procedures=manager.list_procedures(namespace=namespace, query=query, limit=limit)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.post("/procedures/{procedure_id}/outcome", status_code=status.HTTP_204_NO_CONTENT)
|
|
85
|
+
def procedure_outcome(
|
|
86
|
+
procedure_id: str,
|
|
87
|
+
req: ProcedureOutcomeRequest,
|
|
88
|
+
request: Request,
|
|
89
|
+
namespace: str = Query(default="default"),
|
|
90
|
+
manager: MemoryManager = Depends(get_manager),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Record whether using a playbook succeeded, so good ones rise."""
|
|
93
|
+
authorize(request, namespace, "write")
|
|
94
|
+
if not manager.record_procedure_outcome(procedure_id, success=req.success):
|
|
95
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="procedure not found")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Failure-memory endpoints: capture, list and apply lessons from mistakes."""
|
|
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 (
|
|
9
|
+
LessonCreated,
|
|
10
|
+
LessonRequest,
|
|
11
|
+
LessonsDistilled,
|
|
12
|
+
LessonsResponse,
|
|
13
|
+
PreflightRequest,
|
|
14
|
+
PreflightResponse,
|
|
15
|
+
)
|
|
16
|
+
from context_bridge.api.security import authorize
|
|
17
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
18
|
+
|
|
19
|
+
router = APIRouter(tags=["lessons"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.post("/lessons", response_model=LessonCreated)
|
|
23
|
+
def record_lesson(
|
|
24
|
+
req: LessonRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
25
|
+
) -> LessonCreated:
|
|
26
|
+
"""Capture a lesson so a past mistake can be flagged before it recurs."""
|
|
27
|
+
authorize(request, req.namespace, "write")
|
|
28
|
+
lesson_id = manager.record_lesson(
|
|
29
|
+
namespace=req.namespace,
|
|
30
|
+
trigger=req.trigger,
|
|
31
|
+
guidance=req.guidance,
|
|
32
|
+
severity=req.severity,
|
|
33
|
+
created_by=req.created_by,
|
|
34
|
+
session_id=req.session_id,
|
|
35
|
+
)
|
|
36
|
+
return LessonCreated(id=lesson_id or "")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/lessons", response_model=LessonsResponse)
|
|
40
|
+
def list_lessons(
|
|
41
|
+
request: Request,
|
|
42
|
+
namespace: str = Query(default="default"),
|
|
43
|
+
limit: int = Query(default=100, ge=1, le=500),
|
|
44
|
+
manager: MemoryManager = Depends(get_manager),
|
|
45
|
+
) -> LessonsResponse:
|
|
46
|
+
"""List a namespace's lessons, most-proven first."""
|
|
47
|
+
authorize(request, namespace, "read")
|
|
48
|
+
return LessonsResponse(lessons=manager.list_lessons(namespace=namespace, limit=limit))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/lessons/distill", response_model=LessonsDistilled)
|
|
52
|
+
def distill_lessons(
|
|
53
|
+
request: Request,
|
|
54
|
+
namespace: str = Query(default="default"),
|
|
55
|
+
manager: MemoryManager = Depends(get_manager),
|
|
56
|
+
) -> LessonsDistilled:
|
|
57
|
+
"""Cluster memories implicated in failures into auto-drafted lessons."""
|
|
58
|
+
authorize(request, namespace, "write")
|
|
59
|
+
settings = request.app.state.settings
|
|
60
|
+
result = manager.distill_lessons(
|
|
61
|
+
namespace=namespace,
|
|
62
|
+
min_cluster=settings.lesson_distill_min_cluster,
|
|
63
|
+
similarity=settings.lesson_distill_similarity,
|
|
64
|
+
)
|
|
65
|
+
return LessonsDistilled(**result)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/lessons/{lesson_id}/confirm", status_code=status.HTTP_204_NO_CONTENT)
|
|
69
|
+
def confirm_lesson(
|
|
70
|
+
lesson_id: str,
|
|
71
|
+
request: Request,
|
|
72
|
+
namespace: str = Query(default="default"),
|
|
73
|
+
manager: MemoryManager = Depends(get_manager),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Record that a surfaced lesson actually helped, so it ranks higher."""
|
|
76
|
+
authorize(request, namespace, "write")
|
|
77
|
+
if not manager.confirm_lesson(lesson_id):
|
|
78
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="lesson not found")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/preflight", response_model=PreflightResponse)
|
|
82
|
+
def preflight(
|
|
83
|
+
req: PreflightRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
84
|
+
) -> PreflightResponse:
|
|
85
|
+
"""Brief an agent on what the collective knows before it starts a task."""
|
|
86
|
+
authorize(request, req.namespace, "read")
|
|
87
|
+
result = manager.preflight(task=req.task, namespace=req.namespace, limit=req.limit)
|
|
88
|
+
return PreflightResponse(**result)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Maintenance endpoints (TTL sweep and other housekeeping)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Query, Request
|
|
6
|
+
|
|
7
|
+
from context_bridge.api import metrics
|
|
8
|
+
from context_bridge.api.deps import get_manager
|
|
9
|
+
from context_bridge.api.schemas import (
|
|
10
|
+
ConsolidateResponse,
|
|
11
|
+
MaintenanceRunResponse,
|
|
12
|
+
SweepResponse,
|
|
13
|
+
)
|
|
14
|
+
from context_bridge.api.security import authorize
|
|
15
|
+
from context_bridge.config import Settings
|
|
16
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/maintenance", tags=["maintenance"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run_maintenance(manager: MemoryManager, settings: Settings) -> dict:
|
|
22
|
+
"""Run one full housekeeping cycle using the configured policy."""
|
|
23
|
+
return manager.run_maintenance(
|
|
24
|
+
auto_resolve=settings.maintenance_auto_resolve,
|
|
25
|
+
consolidate=settings.maintenance_consolidate,
|
|
26
|
+
distill_lessons=settings.maintenance_distill_lessons,
|
|
27
|
+
min_gap=settings.auto_resolve_min_gap,
|
|
28
|
+
consolidation_min_cluster=settings.consolidation_min_cluster,
|
|
29
|
+
consolidation_similarity=settings.consolidation_similarity,
|
|
30
|
+
lesson_distill_min_cluster=settings.lesson_distill_min_cluster,
|
|
31
|
+
lesson_distill_similarity=settings.lesson_distill_similarity,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("/sweep", response_model=SweepResponse)
|
|
36
|
+
def sweep_expired(request: Request, manager: MemoryManager = Depends(get_manager)) -> SweepResponse:
|
|
37
|
+
"""Physically delete TTL-expired memories from the semantic store.
|
|
38
|
+
|
|
39
|
+
A global maintenance action: requires write access across all namespaces.
|
|
40
|
+
"""
|
|
41
|
+
authorize(request, "*", "write")
|
|
42
|
+
deleted = manager.sweep_expired()
|
|
43
|
+
metrics.SWEEP_DELETED.inc(deleted)
|
|
44
|
+
return SweepResponse(deleted=deleted)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/consolidate", response_model=ConsolidateResponse)
|
|
48
|
+
def consolidate(
|
|
49
|
+
request: Request,
|
|
50
|
+
namespace: str = Query(default="default"),
|
|
51
|
+
manager: MemoryManager = Depends(get_manager),
|
|
52
|
+
) -> ConsolidateResponse:
|
|
53
|
+
"""Cluster a namespace's memories and write back synthesized insights."""
|
|
54
|
+
authorize(request, namespace, "write")
|
|
55
|
+
settings = request.app.state.settings
|
|
56
|
+
result = manager.consolidate(
|
|
57
|
+
namespace=namespace,
|
|
58
|
+
min_cluster=settings.consolidation_min_cluster,
|
|
59
|
+
similarity=settings.consolidation_similarity,
|
|
60
|
+
)
|
|
61
|
+
return ConsolidateResponse(**result)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.post("/run", response_model=MaintenanceRunResponse)
|
|
65
|
+
def run_maintenance(
|
|
66
|
+
request: Request, manager: MemoryManager = Depends(get_manager)
|
|
67
|
+
) -> MaintenanceRunResponse:
|
|
68
|
+
"""Run one full housekeeping cycle: sweep + per-namespace upkeep.
|
|
69
|
+
|
|
70
|
+
A global maintenance action: requires write access across all namespaces.
|
|
71
|
+
"""
|
|
72
|
+
authorize(request, "*", "write")
|
|
73
|
+
result = _run_maintenance(manager, request.app.state.settings)
|
|
74
|
+
metrics.SWEEP_DELETED.inc(result["swept"])
|
|
75
|
+
metrics.MAINTENANCE_RUNS.inc()
|
|
76
|
+
return MaintenanceRunResponse(**result)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Memory read/write endpoints — the core agent-facing contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
|
|
11
|
+
from context_bridge.api import metrics
|
|
12
|
+
from context_bridge.api.deps import get_manager
|
|
13
|
+
from context_bridge.api.schemas import (
|
|
14
|
+
ChunkOut,
|
|
15
|
+
FeedbackRequest,
|
|
16
|
+
ForgetResponse,
|
|
17
|
+
ListResponse,
|
|
18
|
+
QueryRequest,
|
|
19
|
+
QueryResponse,
|
|
20
|
+
SummarizeRequest,
|
|
21
|
+
SummarizeResponse,
|
|
22
|
+
WriteBatchRequest,
|
|
23
|
+
WriteBatchResponse,
|
|
24
|
+
WriteRequest,
|
|
25
|
+
WriteResponse,
|
|
26
|
+
)
|
|
27
|
+
from context_bridge.api.security import authorize
|
|
28
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
29
|
+
|
|
30
|
+
router = APIRouter(prefix="/memory", tags=["memory"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _do_write(manager: MemoryManager, req: WriteRequest) -> WriteResponse:
|
|
34
|
+
result = manager.write(
|
|
35
|
+
content=req.content,
|
|
36
|
+
agent_id=req.agent_id,
|
|
37
|
+
session_id=req.session_id,
|
|
38
|
+
task_id=req.task_id,
|
|
39
|
+
namespace=req.namespace,
|
|
40
|
+
tags=req.tags,
|
|
41
|
+
confidence=req.confidence,
|
|
42
|
+
metadata=req.metadata,
|
|
43
|
+
source=req.source,
|
|
44
|
+
ttl_seconds=req.ttl_seconds,
|
|
45
|
+
dedup=req.dedup,
|
|
46
|
+
summarize_before_store=req.summarize_before_store,
|
|
47
|
+
)
|
|
48
|
+
metrics.WRITES.inc()
|
|
49
|
+
metrics.CHUNKS_STORED.inc(result.stored)
|
|
50
|
+
metrics.CHUNKS_DEDUPED.inc(result.deduped)
|
|
51
|
+
return WriteResponse(
|
|
52
|
+
ids=result.ids, stored=result.stored, deduped=result.deduped, skipped=result.skipped
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/write", response_model=WriteResponse)
|
|
57
|
+
def write_memory(
|
|
58
|
+
req: WriteRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
59
|
+
) -> WriteResponse:
|
|
60
|
+
authorize(request, req.namespace, "write")
|
|
61
|
+
return _do_write(manager, req)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.post("/write_batch", response_model=WriteBatchResponse)
|
|
65
|
+
def write_memory_batch(
|
|
66
|
+
req: WriteBatchRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
67
|
+
) -> WriteBatchResponse:
|
|
68
|
+
for item in req.items:
|
|
69
|
+
authorize(request, item.namespace, "write")
|
|
70
|
+
return WriteBatchResponse(results=[_do_write(manager, item) for item in req.items])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.post("/query", response_model=QueryResponse)
|
|
74
|
+
def query_memory(
|
|
75
|
+
req: QueryRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
76
|
+
) -> QueryResponse:
|
|
77
|
+
authorize(request, req.namespace, "read")
|
|
78
|
+
assembled = manager.query(
|
|
79
|
+
query=req.query,
|
|
80
|
+
namespace=req.namespace,
|
|
81
|
+
agent_id=req.agent_id,
|
|
82
|
+
session_id=req.session_id,
|
|
83
|
+
top_k=req.top_k,
|
|
84
|
+
token_budget=req.token_budget,
|
|
85
|
+
filters=req.filters,
|
|
86
|
+
rerank=req.rerank,
|
|
87
|
+
expand_parents=req.expand_parents,
|
|
88
|
+
include_dates=req.include_dates,
|
|
89
|
+
since=req.since,
|
|
90
|
+
until=req.until,
|
|
91
|
+
with_lessons=req.with_lessons,
|
|
92
|
+
)
|
|
93
|
+
metrics.QUERIES.inc()
|
|
94
|
+
metrics.QUERY_TOKENS.observe(assembled.tokens_used)
|
|
95
|
+
metrics.QUERY_CHUNKS.observe(len(assembled.chunks))
|
|
96
|
+
return QueryResponse.from_assembled(assembled)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _sse(event: str, data: dict) -> str:
|
|
100
|
+
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@router.post("/query/stream")
|
|
104
|
+
def query_memory_stream(
|
|
105
|
+
req: QueryRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
106
|
+
) -> StreamingResponse:
|
|
107
|
+
"""Recall as a Server-Sent Events stream: one event per chunk, then 'done'.
|
|
108
|
+
|
|
109
|
+
Lets an agent render or forward context progressively instead of waiting
|
|
110
|
+
for the whole assembled block.
|
|
111
|
+
"""
|
|
112
|
+
authorize(request, req.namespace, "read")
|
|
113
|
+
assembled = manager.query(
|
|
114
|
+
query=req.query,
|
|
115
|
+
namespace=req.namespace,
|
|
116
|
+
agent_id=req.agent_id,
|
|
117
|
+
session_id=req.session_id,
|
|
118
|
+
top_k=req.top_k,
|
|
119
|
+
token_budget=req.token_budget,
|
|
120
|
+
filters=req.filters,
|
|
121
|
+
rerank=req.rerank,
|
|
122
|
+
expand_parents=req.expand_parents,
|
|
123
|
+
)
|
|
124
|
+
metrics.QUERIES.inc()
|
|
125
|
+
metrics.QUERY_TOKENS.observe(assembled.tokens_used)
|
|
126
|
+
metrics.QUERY_CHUNKS.observe(len(assembled.chunks))
|
|
127
|
+
|
|
128
|
+
def generate() -> Iterator[str]:
|
|
129
|
+
for chunk in assembled.chunks:
|
|
130
|
+
yield _sse(
|
|
131
|
+
"chunk",
|
|
132
|
+
{
|
|
133
|
+
"id": chunk.id,
|
|
134
|
+
"content": chunk.content,
|
|
135
|
+
"score": chunk.score,
|
|
136
|
+
"agent_id": chunk.provenance.agent_id,
|
|
137
|
+
"source": chunk.provenance.source,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
yield _sse(
|
|
141
|
+
"done",
|
|
142
|
+
{
|
|
143
|
+
"tokens_used": assembled.tokens_used,
|
|
144
|
+
"num_chunks": len(assembled.chunks),
|
|
145
|
+
"sources": assembled.sources,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.get("", response_model=ListResponse)
|
|
153
|
+
def list_memory(
|
|
154
|
+
request: Request,
|
|
155
|
+
namespace: str = Query(default="default"),
|
|
156
|
+
limit: int = Query(default=50, ge=1, le=500),
|
|
157
|
+
cursor: str | None = Query(default=None),
|
|
158
|
+
manager: MemoryManager = Depends(get_manager),
|
|
159
|
+
) -> ListResponse:
|
|
160
|
+
authorize(request, namespace, "read")
|
|
161
|
+
chunks, next_cursor = manager.list_records(namespace=namespace, limit=limit, cursor=cursor)
|
|
162
|
+
return ListResponse(chunks=[ChunkOut.from_chunk(c) for c in chunks], next_cursor=next_cursor)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router.delete("", response_model=ForgetResponse)
|
|
166
|
+
def forget_memory(
|
|
167
|
+
request: Request,
|
|
168
|
+
namespace: str | None = Query(default=None),
|
|
169
|
+
session_id: str | None = Query(default=None),
|
|
170
|
+
manager: MemoryManager = Depends(get_manager),
|
|
171
|
+
) -> ForgetResponse:
|
|
172
|
+
"""Erase all memory for a namespace and/or session (right-to-be-forgotten)."""
|
|
173
|
+
if not namespace and not session_id:
|
|
174
|
+
raise HTTPException(
|
|
175
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
176
|
+
detail="provide a namespace and/or session_id",
|
|
177
|
+
)
|
|
178
|
+
authorize(request, namespace or "*", "write")
|
|
179
|
+
return ForgetResponse(**manager.forget(namespace=namespace, session_id=session_id))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.get("/{record_id}", response_model=ChunkOut)
|
|
183
|
+
def get_memory(record_id: str, manager: MemoryManager = Depends(get_manager)) -> ChunkOut:
|
|
184
|
+
chunk = manager.get(record_id)
|
|
185
|
+
if chunk is None:
|
|
186
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="record not found")
|
|
187
|
+
return ChunkOut.from_chunk(chunk)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.delete("/{record_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
191
|
+
def delete_memory(record_id: str, manager: MemoryManager = Depends(get_manager)) -> None:
|
|
192
|
+
manager.delete([record_id])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.post("/feedback", status_code=status.HTTP_204_NO_CONTENT)
|
|
196
|
+
def submit_feedback(
|
|
197
|
+
req: FeedbackRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Signal whether a recalled memory was useful; re-ranks future recall."""
|
|
200
|
+
authorize(request, req.namespace, "write")
|
|
201
|
+
manager.record_feedback(
|
|
202
|
+
memory_id=req.memory_id,
|
|
203
|
+
namespace=req.namespace,
|
|
204
|
+
useful=req.useful,
|
|
205
|
+
weight=req.weight,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.post("/summarize", response_model=SummarizeResponse)
|
|
210
|
+
def summarize_session(
|
|
211
|
+
req: SummarizeRequest, request: Request, manager: MemoryManager = Depends(get_manager)
|
|
212
|
+
) -> SummarizeResponse:
|
|
213
|
+
authorize(request, req.namespace, "write")
|
|
214
|
+
result = manager.summarize_session(
|
|
215
|
+
session_id=req.session_id,
|
|
216
|
+
namespace=req.namespace,
|
|
217
|
+
agent_id=req.agent_id,
|
|
218
|
+
max_sentences=req.max_sentences,
|
|
219
|
+
store_summary=req.store_summary,
|
|
220
|
+
)
|
|
221
|
+
return SummarizeResponse(summary=result["summary"], chunk_ids=result["chunk_ids"])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Collaboration-quality endpoint: a single metric for how well agents share."""
|
|
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 QualityResponse
|
|
9
|
+
from context_bridge.api.security import authorize
|
|
10
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
11
|
+
|
|
12
|
+
router = APIRouter(tags=["quality"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/quality", response_model=QualityResponse)
|
|
16
|
+
def collaboration_quality(
|
|
17
|
+
request: Request,
|
|
18
|
+
namespace: str = Query(default="default"),
|
|
19
|
+
manager: MemoryManager = Depends(get_manager),
|
|
20
|
+
) -> QualityResponse:
|
|
21
|
+
"""Return a composite 0-100 score of collaboration health for a namespace."""
|
|
22
|
+
authorize(request, namespace, "read")
|
|
23
|
+
return QualityResponse(**manager.collaboration_quality(namespace=namespace))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Session-level views over episodic / provenance memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Query, Request, status
|
|
6
|
+
|
|
7
|
+
from context_bridge.api.deps import get_manager
|
|
8
|
+
from context_bridge.api.schemas import (
|
|
9
|
+
DistillRequest,
|
|
10
|
+
DistillResponse,
|
|
11
|
+
TimelineResponse,
|
|
12
|
+
TurnRequest,
|
|
13
|
+
TurnsResponse,
|
|
14
|
+
)
|
|
15
|
+
from context_bridge.api.security import authorize
|
|
16
|
+
from context_bridge.core.memory.manager import MemoryManager
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/sessions", tags=["sessions"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/{session_id}/timeline", response_model=TimelineResponse)
|
|
22
|
+
def session_timeline(
|
|
23
|
+
session_id: str,
|
|
24
|
+
limit: int = Query(default=100, ge=1, le=1000),
|
|
25
|
+
manager: MemoryManager = Depends(get_manager),
|
|
26
|
+
) -> TimelineResponse:
|
|
27
|
+
episodes = manager.timeline(session_id, limit=limit)
|
|
28
|
+
return TimelineResponse(session_id=session_id, episodes=episodes)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/{session_id}/turns", status_code=status.HTTP_204_NO_CONTENT)
|
|
32
|
+
def add_turn(
|
|
33
|
+
session_id: str,
|
|
34
|
+
req: TurnRequest,
|
|
35
|
+
manager: MemoryManager = Depends(get_manager),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Append an ephemeral conversational turn to working memory (cheap, TTL'd)."""
|
|
38
|
+
manager.remember_turn(
|
|
39
|
+
session_id, {"kind": req.kind, "agent_id": req.agent_id, "content": req.content}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/{session_id}/turns", response_model=TurnsResponse)
|
|
44
|
+
def recent_turns(
|
|
45
|
+
session_id: str,
|
|
46
|
+
limit: int = Query(default=20, ge=1, le=200),
|
|
47
|
+
manager: MemoryManager = Depends(get_manager),
|
|
48
|
+
) -> TurnsResponse:
|
|
49
|
+
return TurnsResponse(session_id=session_id, turns=manager.recent_turns(session_id, limit=limit))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/{session_id}/distill", response_model=DistillResponse)
|
|
53
|
+
def distill_session(
|
|
54
|
+
session_id: str,
|
|
55
|
+
req: DistillRequest,
|
|
56
|
+
request: Request,
|
|
57
|
+
manager: MemoryManager = Depends(get_manager),
|
|
58
|
+
) -> DistillResponse:
|
|
59
|
+
"""Promote the session's most salient turns into durable, cross-session memory."""
|
|
60
|
+
authorize(request, req.namespace, "write")
|
|
61
|
+
result = manager.distill_session(
|
|
62
|
+
session_id=session_id,
|
|
63
|
+
namespace=req.namespace,
|
|
64
|
+
agent_id=req.agent_id,
|
|
65
|
+
max_promote=req.max_promote,
|
|
66
|
+
min_score=req.min_score,
|
|
67
|
+
)
|
|
68
|
+
return DistillResponse(**result)
|