cilow 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.
- cilow/__init__.py +116 -0
- cilow/adapters/__init__.py +37 -0
- cilow/adapters/anthropic.py +64 -0
- cilow/adapters/langchain.py +96 -0
- cilow/adapters/memory.py +157 -0
- cilow/adapters/openai.py +74 -0
- cilow/adapters/remaining.py +65 -0
- cilow/client.py +1042 -0
- cilow/errors.py +33 -0
- cilow/types.py +1144 -0
- cilow-0.1.0.dist-info/METADATA +161 -0
- cilow-0.1.0.dist-info/RECORD +15 -0
- cilow-0.1.0.dist-info/WHEEL +5 -0
- cilow-0.1.0.dist-info/licenses/LICENSE +201 -0
- cilow-0.1.0.dist-info/top_level.txt +1 -0
cilow/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""cilow — Python client for Cilow, the verifiable context layer for AI agents.
|
|
2
|
+
|
|
3
|
+
Point it at a running ``cilow-serve`` and call the FOUR core verbs (the MCP core profile):
|
|
4
|
+
``ingest`` (the one write door: facts | text | bytes | url), ``context_pack`` (the product read:
|
|
5
|
+
injectible pack + citations with ``is_current`` + ``abstained`` / ``grounding`` / ``trace_id``),
|
|
6
|
+
``forget`` (withdraw), and ``explain`` (why a read surfaced what it surfaced, via ``trace_id``).
|
|
7
|
+
``remember`` / ``recall`` / ``answer`` / ``timeline`` / ``track_entity`` remain as full-profile /
|
|
8
|
+
legacy aliases. The headline feature is CALIBRATED ABSTENTION: reads return ``abstained=True``
|
|
9
|
+
when the engine does not know, instead of fabricating. Gate your agent on that flag.
|
|
10
|
+
|
|
11
|
+
from cilow import CilowClient
|
|
12
|
+
|
|
13
|
+
cilow = CilowClient(base_url="http://localhost:8080", token="demo")
|
|
14
|
+
cilow.ingest(facts=[{"subject": "Maya", "predicate": "employer", "object": "Acme"}])
|
|
15
|
+
r = cilow.context_pack("where does Maya work", anchor="Maya", attribute="employer")
|
|
16
|
+
print(r.reason if r.abstained else r.pack)
|
|
17
|
+
why = cilow.explain(r.trace_id) # cited claims + grounding vs the tenant threshold
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import CilowClient, ingest_source_id, now_micros
|
|
21
|
+
from .errors import CilowError
|
|
22
|
+
from .types import (
|
|
23
|
+
AnswerResponse,
|
|
24
|
+
Branch,
|
|
25
|
+
Citation,
|
|
26
|
+
CitationPart,
|
|
27
|
+
Claim,
|
|
28
|
+
ConsolidateResponse,
|
|
29
|
+
Fact,
|
|
30
|
+
FeedbackResponse,
|
|
31
|
+
ContextPackResponse,
|
|
32
|
+
ExplainCited,
|
|
33
|
+
ExplainReport,
|
|
34
|
+
ExplainResponse,
|
|
35
|
+
ForgetResponse,
|
|
36
|
+
ForkResponse,
|
|
37
|
+
IngestAnyResponse,
|
|
38
|
+
IngestResponse,
|
|
39
|
+
IngestAudioResponse,
|
|
40
|
+
IngestImageResponse,
|
|
41
|
+
IngestPdfResponse,
|
|
42
|
+
IngestStructuredResponse,
|
|
43
|
+
Outcome,
|
|
44
|
+
ReaderUsage,
|
|
45
|
+
ReasonCode,
|
|
46
|
+
TerminalLayer,
|
|
47
|
+
TERMINAL_LAYERS,
|
|
48
|
+
Evidence,
|
|
49
|
+
EvidenceStructure,
|
|
50
|
+
EvidenceProvenance,
|
|
51
|
+
Receipt,
|
|
52
|
+
RecallResponse,
|
|
53
|
+
RememberResponse,
|
|
54
|
+
RememberTextResponse,
|
|
55
|
+
Scope,
|
|
56
|
+
Source,
|
|
57
|
+
TimelineEntry,
|
|
58
|
+
TrackEntityResponse,
|
|
59
|
+
Workspace,
|
|
60
|
+
EvidenceRevision,
|
|
61
|
+
ContextProgram,
|
|
62
|
+
ContextProgramV2,
|
|
63
|
+
ContextTrace,
|
|
64
|
+
MemoryProposal,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__version__ = "0.1.0"
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"CilowClient",
|
|
71
|
+
"CilowError",
|
|
72
|
+
"now_micros",
|
|
73
|
+
"ingest_source_id",
|
|
74
|
+
"Scope",
|
|
75
|
+
"Fact",
|
|
76
|
+
"Source",
|
|
77
|
+
"RememberResponse",
|
|
78
|
+
"Claim",
|
|
79
|
+
"Receipt",
|
|
80
|
+
"RecallResponse",
|
|
81
|
+
"Citation",
|
|
82
|
+
"CitationPart",
|
|
83
|
+
"AnswerResponse",
|
|
84
|
+
"ReaderUsage",
|
|
85
|
+
"ReasonCode",
|
|
86
|
+
"TerminalLayer",
|
|
87
|
+
"TERMINAL_LAYERS",
|
|
88
|
+
"Evidence",
|
|
89
|
+
"EvidenceStructure",
|
|
90
|
+
"EvidenceProvenance",
|
|
91
|
+
"TimelineEntry",
|
|
92
|
+
"TrackEntityResponse",
|
|
93
|
+
"Outcome",
|
|
94
|
+
"FeedbackResponse",
|
|
95
|
+
"ConsolidateResponse",
|
|
96
|
+
"RememberTextResponse",
|
|
97
|
+
"IngestImageResponse",
|
|
98
|
+
"IngestAudioResponse",
|
|
99
|
+
"IngestPdfResponse",
|
|
100
|
+
"IngestStructuredResponse",
|
|
101
|
+
"IngestAnyResponse",
|
|
102
|
+
"ForgetResponse",
|
|
103
|
+
"ContextPackResponse",
|
|
104
|
+
"IngestResponse",
|
|
105
|
+
"ExplainResponse",
|
|
106
|
+
"ExplainReport",
|
|
107
|
+
"ExplainCited",
|
|
108
|
+
"Branch",
|
|
109
|
+
"ForkResponse",
|
|
110
|
+
"Workspace",
|
|
111
|
+
"EvidenceRevision",
|
|
112
|
+
"ContextProgram",
|
|
113
|
+
"ContextProgramV2",
|
|
114
|
+
"ContextTrace",
|
|
115
|
+
"MemoryProposal",
|
|
116
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""``cilow.adapters`` — framework bindings over the ONE :class:`CilowMemory` contract.
|
|
2
|
+
|
|
3
|
+
Every binding is a thin shim that maps a framework's memory/retriever/tool interface onto the five
|
|
4
|
+
contract verbs (``ingest`` / ``recall`` / ``answer`` / ``reset`` / ``health``) and re-exposes the
|
|
5
|
+
differentiator unchanged: ``abstained`` + ``reason_code`` + the ``grounded`` signal.
|
|
6
|
+
|
|
7
|
+
from cilow.adapters import CilowMemoryClient
|
|
8
|
+
from cilow.adapters import openai, anthropic, langchain
|
|
9
|
+
|
|
10
|
+
Branch on an adapter result's ``.is_calibrated_abstain`` / ``.is_infrastructure_abstain`` properties.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from . import anthropic, langchain, openai
|
|
14
|
+
from .memory import (
|
|
15
|
+
AnswerResult,
|
|
16
|
+
CilowMemory,
|
|
17
|
+
CilowMemoryClient,
|
|
18
|
+
IngestResult,
|
|
19
|
+
MemoryHit,
|
|
20
|
+
RecallResult,
|
|
21
|
+
)
|
|
22
|
+
from .remaining import REMAINING_ADAPTERS, make_autogen_memory, make_llamaindex_retriever
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CilowMemory",
|
|
26
|
+
"CilowMemoryClient",
|
|
27
|
+
"MemoryHit",
|
|
28
|
+
"RecallResult",
|
|
29
|
+
"AnswerResult",
|
|
30
|
+
"IngestResult",
|
|
31
|
+
"openai",
|
|
32
|
+
"anthropic",
|
|
33
|
+
"langchain",
|
|
34
|
+
"REMAINING_ADAPTERS",
|
|
35
|
+
"make_llamaindex_retriever",
|
|
36
|
+
"make_autogen_memory",
|
|
37
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Anthropic SDK binding over :class:`CilowMemory`, no ``anthropic`` dependency required.
|
|
2
|
+
|
|
3
|
+
Anthropic has no OpenAI-compatible endpoint, so the base_url swap does NOT apply (design doc R2.2).
|
|
4
|
+
This binding uses the explicit Messages tool-use path: register :data:`CILOW_RECALL_TOOL`, execute
|
|
5
|
+
calls with :func:`run_recall_tool` into a ``tool_result`` block, and/or pre-inject grounded context
|
|
6
|
+
with :func:`inject_memory`. Both surface the abstained/grounded branch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from .memory import CilowMemory, RecallResult
|
|
14
|
+
|
|
15
|
+
CILOW_RECALL_TOOL: Dict[str, Any] = {
|
|
16
|
+
"name": "cilow_recall",
|
|
17
|
+
"description": (
|
|
18
|
+
"Recall grounded facts from verifiable long-term memory. Returns memory-grounded results, "
|
|
19
|
+
"or a calibrated abstain when memory genuinely does not know — then DO NOT fabricate."
|
|
20
|
+
),
|
|
21
|
+
"input_schema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"query": {"type": "string", "description": "What to look up in memory."},
|
|
25
|
+
"anchor": {"type": "string", "description": "Optional exact entity hint."},
|
|
26
|
+
"attribute": {"type": "string", "description": "Optional exact predicate hint."},
|
|
27
|
+
},
|
|
28
|
+
"required": ["query"],
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_recall_tool(memory: CilowMemory, tool_use_id: str, tool_input: Dict[str, Any]) -> Tuple[Dict[str, Any], RecallResult]:
|
|
34
|
+
"""Execute one ``cilow_recall`` tool_use. Returns ``(tool_result_block, result)``. An INFRASTRUCTURE
|
|
35
|
+
abstain is marked ``is_error=True`` (retryable); a calibrated abstain is a normal result whose
|
|
36
|
+
content instructs the model not to fabricate."""
|
|
37
|
+
result = memory.recall(tool_input["query"], anchor=tool_input.get("anchor"), attribute=tool_input.get("attribute"))
|
|
38
|
+
block: Dict[str, Any] = {"type": "tool_result", "tool_use_id": tool_use_id}
|
|
39
|
+
if result.reason_code == "provider_unavailable":
|
|
40
|
+
block["is_error"] = True
|
|
41
|
+
block["content"] = f"MEMORY TEMPORARILY UNAVAILABLE: {result.reason or 'provider error'} — retry."
|
|
42
|
+
return block, result
|
|
43
|
+
if result.abstained or not result.grounded:
|
|
44
|
+
block["content"] = (
|
|
45
|
+
f"MEMORY ABSTAINED: {result.reason or 'memory does not know'}. "
|
|
46
|
+
"Do not fabricate; ask the user or say you don't know."
|
|
47
|
+
)
|
|
48
|
+
return block, result
|
|
49
|
+
lines = "\n".join(f"- {h.text}" for h in result.results)
|
|
50
|
+
block["content"] = f"MEMORY-GROUNDED FACTS:\n{lines}"
|
|
51
|
+
return block, result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inject_memory(memory: CilowMemory, query: str, *, anchor: Optional[str] = None, attribute: Optional[str] = None) -> Tuple[str, bool, RecallResult]:
|
|
55
|
+
"""Pre-inject grounded memory ahead of ``messages.create``: recall, and if grounded return a
|
|
56
|
+
``system`` string block to prepend plus the branch signal. On an abstain the system block is empty
|
|
57
|
+
and ``grounded`` is False — the caller decides whether to proceed or stop. Returns
|
|
58
|
+
``(system, grounded, result)``."""
|
|
59
|
+
result = memory.recall(query, anchor=anchor, attribute=attribute)
|
|
60
|
+
if not result.grounded:
|
|
61
|
+
return "", False, result
|
|
62
|
+
lines = "\n".join(f"- {h.text}" for h in result.results)
|
|
63
|
+
system = f"Answer using ONLY these memory-grounded facts. If they are insufficient, say so.\n{lines}"
|
|
64
|
+
return system, True, result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""LangChain binding over :class:`CilowMemory`, no ``langchain`` dependency required.
|
|
2
|
+
|
|
3
|
+
Two duck-typed classes that satisfy LangChain's structural contracts so they drop into any chain:
|
|
4
|
+
- :class:`CilowRetriever` mirrors ``BaseRetriever`` (``get_relevant_documents(query) -> list[Document]``).
|
|
5
|
+
- :class:`CilowChatMemory` mirrors ``BaseMemory`` (``memory_variables`` / ``load_memory_variables`` /
|
|
6
|
+
``save_context`` / ``clear``).
|
|
7
|
+
The abstention contract is preserved: an abstain yields ZERO documents (never a fabricated one), and
|
|
8
|
+
the grounded/abstain branch is readable via :meth:`CilowRetriever.recall_with_signal`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from .memory import CilowMemory, RecallResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Document:
|
|
21
|
+
"""Structural ``Document`` (LangChain ``langchain_core.documents``)."""
|
|
22
|
+
|
|
23
|
+
page_content: str
|
|
24
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CilowRetriever:
|
|
28
|
+
"""A LangChain-compatible retriever backed by Cilow recall."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, memory: CilowMemory, *, k: Optional[int] = None, anchor: Optional[str] = None,
|
|
31
|
+
attribute: Optional[str] = None) -> None:
|
|
32
|
+
self._m = memory
|
|
33
|
+
self._k = k
|
|
34
|
+
self._anchor = anchor
|
|
35
|
+
self._attribute = attribute
|
|
36
|
+
|
|
37
|
+
def get_relevant_documents(self, query: str) -> List[Document]:
|
|
38
|
+
"""BaseRetriever entry point. Returns memory-grounded docs, or ``[]`` on an abstain."""
|
|
39
|
+
r = self._m.recall(query, k=self._k, anchor=self._anchor, attribute=self._attribute)
|
|
40
|
+
if not r.grounded:
|
|
41
|
+
return []
|
|
42
|
+
return [
|
|
43
|
+
Document(page_content=h.text, metadata={"id": h.id, "entity": h.entity, "score": h.score, "grounded": True})
|
|
44
|
+
for h in r.results
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# LangChain 0.2+ internal hook.
|
|
48
|
+
def _get_relevant_documents(self, query: str) -> List[Document]:
|
|
49
|
+
return self.get_relevant_documents(query)
|
|
50
|
+
|
|
51
|
+
def recall_with_signal(self, query: str) -> RecallResult:
|
|
52
|
+
"""Escape hatch: the full :class:`RecallResult` to branch on ``abstained`` / ``reason_code``."""
|
|
53
|
+
return self._m.recall(query, k=self._k, anchor=self._anchor, attribute=self._attribute)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CilowChatMemory:
|
|
57
|
+
"""A LangChain-compatible ``BaseMemory``: recall on load, persist turns on save."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, memory: CilowMemory, *, memory_key: str = "history", input_key: Optional[str] = None,
|
|
60
|
+
output_key: Optional[str] = None, subject_hint: Optional[str] = None,
|
|
61
|
+
anchor: Optional[str] = None, attribute: Optional[str] = None) -> None:
|
|
62
|
+
self._m = memory
|
|
63
|
+
self._memory_key = memory_key
|
|
64
|
+
self._input_key = input_key
|
|
65
|
+
self._output_key = output_key
|
|
66
|
+
self._subject_hint = subject_hint
|
|
67
|
+
self._anchor = anchor
|
|
68
|
+
self._attribute = attribute
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def memory_variables(self) -> List[str]:
|
|
72
|
+
return [self._memory_key]
|
|
73
|
+
|
|
74
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
"""Load grounded history. On an abstain the variable is an empty string (honest, no fabrication)."""
|
|
76
|
+
query = self._pick(inputs, self._input_key)
|
|
77
|
+
if not query:
|
|
78
|
+
return {self._memory_key: ""}
|
|
79
|
+
r = self._m.recall(query, anchor=self._anchor, attribute=self._attribute)
|
|
80
|
+
return {self._memory_key: "\n".join(h.text for h in r.results) if r.grounded else ""}
|
|
81
|
+
|
|
82
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
83
|
+
"""Persist the turn into memory so future recalls can ground on it."""
|
|
84
|
+
text = "\n".join(t for t in (self._pick(inputs, self._input_key), self._pick(outputs, self._output_key)) if t)
|
|
85
|
+
if text:
|
|
86
|
+
self._m.ingest(text, subject_hint=self._subject_hint)
|
|
87
|
+
|
|
88
|
+
def clear(self) -> None:
|
|
89
|
+
self._m.reset()
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _pick(bag: Dict[str, Any], key: Optional[str]) -> str:
|
|
93
|
+
if key:
|
|
94
|
+
return str(bag.get(key, ""))
|
|
95
|
+
vals = list(bag.values())
|
|
96
|
+
return str(vals[0]) if vals else ""
|
cilow/adapters/memory.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""``CilowMemory`` — the ONE adapter contract every framework binding maps onto (Python mirror).
|
|
2
|
+
|
|
3
|
+
A tiny, framework-neutral surface over the shipped REST endpoints (``/v1/memories/*`` + the
|
|
4
|
+
``/v1/chat/completions`` proxy). Each framework adapter (OpenAI, Anthropic, LangChain, …) is a thin
|
|
5
|
+
shim onto these five verbs that re-exposes the differentiator unchanged: ``abstained`` +
|
|
6
|
+
``reason_code`` + the ``grounded`` signal. Transport is the existing :class:`~cilow.CilowClient` — the
|
|
7
|
+
bindings call the SDK, never raw HTTP.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Optional, Protocol, Union
|
|
14
|
+
|
|
15
|
+
from ..client import CilowClient
|
|
16
|
+
from ..types import ReasonCode
|
|
17
|
+
|
|
18
|
+
_CALIBRATED = ("below_threshold", "no_candidates")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class MemoryHit:
|
|
23
|
+
"""One recalled memory line — ``text`` is ready to inject; structured fields kept alongside."""
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
text: str
|
|
27
|
+
entity: str
|
|
28
|
+
value: str
|
|
29
|
+
score: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class _Abstainable:
|
|
34
|
+
abstained: bool = False
|
|
35
|
+
reason: Optional[str] = None
|
|
36
|
+
reason_code: Optional[ReasonCode] = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_infrastructure_abstain(self) -> bool:
|
|
40
|
+
"""Retryable INFRA abstain (the embedding provider failed) — memory may hold the answer."""
|
|
41
|
+
return self.abstained and self.reason_code == "provider_unavailable"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_calibrated_abstain(self) -> bool:
|
|
45
|
+
"""Memory genuinely lacks grounded context. Do NOT fabricate past it."""
|
|
46
|
+
return self.abstained and self.reason_code in _CALIBRATED
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def should_retry(self) -> bool:
|
|
50
|
+
return self.is_infrastructure_abstain
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class RecallResult(_Abstainable):
|
|
55
|
+
"""Result of :meth:`CilowMemory.recall`. ``grounded`` is the universal drop-in signal."""
|
|
56
|
+
|
|
57
|
+
results: List[MemoryHit] = field(default_factory=list)
|
|
58
|
+
grounded: bool = False
|
|
59
|
+
margin: Optional[float] = None
|
|
60
|
+
packed_tokens: Optional[int] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class AnswerResult(_Abstainable):
|
|
65
|
+
"""Result of :meth:`CilowMemory.answer` — a synthesized answer + the grounded/abstain signal."""
|
|
66
|
+
|
|
67
|
+
text: str = ""
|
|
68
|
+
grounded: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class IngestResult:
|
|
73
|
+
count: int
|
|
74
|
+
ids: List[str]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CilowMemory(Protocol):
|
|
78
|
+
"""The single contract. Five verbs; every framework binding targets exactly these.
|
|
79
|
+
|
|
80
|
+
``ingest`` → ``/v1/memories/add`` · ``recall`` → ``/search`` · ``answer`` → ``/search``
|
|
81
|
+
(mode ``"answer"``) · ``reset`` → ``/list`` + ``/delete`` · ``health`` → ``GET /health``.
|
|
82
|
+
``proxy_base_url()`` is the drop-in OpenAI-compatible base URL that emits the grounded header.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def ingest(self, items: Union[str, List[str]], *, subject_hint: Optional[str] = None) -> IngestResult: ...
|
|
86
|
+
def recall(self, query: str, *, k: Optional[int] = None, anchor: Optional[str] = None, attribute: Optional[str] = None) -> RecallResult: ...
|
|
87
|
+
def answer(self, query: str, *, anchor: Optional[str] = None, attribute: Optional[str] = None) -> AnswerResult: ...
|
|
88
|
+
def reset(self) -> int: ...
|
|
89
|
+
def health(self) -> bool: ...
|
|
90
|
+
def proxy_base_url(self) -> str: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CilowMemoryClient:
|
|
94
|
+
"""The concrete :class:`CilowMemory` over a :class:`~cilow.CilowClient`."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, client: CilowClient) -> None:
|
|
97
|
+
self._c = client
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def create(cls, base_url: str, token: str) -> "CilowMemoryClient":
|
|
101
|
+
return cls(CilowClient(base_url=base_url, token=token))
|
|
102
|
+
|
|
103
|
+
def ingest(self, items: Union[str, List[str]], *, subject_hint: Optional[str] = None) -> IngestResult:
|
|
104
|
+
the_list = [items] if isinstance(items, str) else list(items)
|
|
105
|
+
if len(the_list) == 1:
|
|
106
|
+
r = self._c.memories.add(the_list[0], subject_hint=subject_hint)
|
|
107
|
+
ids = [m["id"] for m in r.get("memories", [])]
|
|
108
|
+
return IngestResult(count=r.get("count", len(ids)), ids=ids)
|
|
109
|
+
r = self._c.memories.batch_add([{"content": c, "subject_hint": subject_hint} for c in the_list])
|
|
110
|
+
ids = [m["id"] for res in r.get("results", []) for m in res.get("memories", [])]
|
|
111
|
+
return IngestResult(count=len(ids), ids=ids)
|
|
112
|
+
|
|
113
|
+
def recall(self, query: str, *, k: Optional[int] = None, anchor: Optional[str] = None, attribute: Optional[str] = None) -> RecallResult:
|
|
114
|
+
r = self._c.memories.search(query, anchor=anchor, attribute=attribute)
|
|
115
|
+
raw = r.get("results", []) or []
|
|
116
|
+
if k is not None:
|
|
117
|
+
raw = raw[:k]
|
|
118
|
+
hits = [
|
|
119
|
+
MemoryHit(id=h.get("id", ""), text=h.get("text", ""), entity=h.get("entity", ""),
|
|
120
|
+
value=h.get("value", h.get("text", "")), score=h.get("score", 0.0))
|
|
121
|
+
for h in raw
|
|
122
|
+
]
|
|
123
|
+
abstained = bool(r.get("abstained", False))
|
|
124
|
+
return RecallResult(
|
|
125
|
+
abstained=abstained, reason=r.get("reason"), reason_code=r.get("reason_code"),
|
|
126
|
+
results=hits, grounded=(not abstained and len(hits) > 0),
|
|
127
|
+
margin=r.get("margin"), packed_tokens=r.get("packed_tokens"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def answer(self, query: str, *, anchor: Optional[str] = None, attribute: Optional[str] = None) -> AnswerResult:
|
|
131
|
+
r = self._c.memories.search(query, anchor=anchor, attribute=attribute, mode="answer")
|
|
132
|
+
abstained = bool(r.get("abstained", False))
|
|
133
|
+
text = r.get("context_text", "") or ""
|
|
134
|
+
return AnswerResult(
|
|
135
|
+
abstained=abstained, reason=r.get("reason"), reason_code=r.get("reason_code"),
|
|
136
|
+
text=text, grounded=(not abstained and bool(text)),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def reset(self) -> int:
|
|
140
|
+
deleted = 0
|
|
141
|
+
while True:
|
|
142
|
+
page = self._c.memories.list(active_only=True, limit=200)
|
|
143
|
+
mems = page.get("memories", [])
|
|
144
|
+
if not mems:
|
|
145
|
+
break
|
|
146
|
+
for m in mems:
|
|
147
|
+
res = self._c.memories.delete(m["id"])
|
|
148
|
+
deleted += res.get("deleted", 0)
|
|
149
|
+
if len(mems) < 200:
|
|
150
|
+
break
|
|
151
|
+
return deleted
|
|
152
|
+
|
|
153
|
+
def health(self) -> bool:
|
|
154
|
+
return self._c.health()
|
|
155
|
+
|
|
156
|
+
def proxy_base_url(self) -> str:
|
|
157
|
+
return f"{self._c.base_url}/v1"
|
cilow/adapters/openai.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""OpenAI SDK binding over :class:`CilowMemory`, no ``openai`` dependency required.
|
|
2
|
+
|
|
3
|
+
Two thin surfaces:
|
|
4
|
+
1. ``base_url`` swap — :func:`cilow_proxy_config` returns kwargs for ``OpenAI(...)`` pointing at the
|
|
5
|
+
Cilow proxy; recall→inject→forward is automatic and every response carries
|
|
6
|
+
``x-cilow-memory-grounded`` (read it with :func:`grounded_from_headers`).
|
|
7
|
+
2. memory tool-use — hand :data:`CILOW_RECALL_TOOL` to ``tools=[...]`` and execute calls with
|
|
8
|
+
:func:`run_recall_tool`; the tool result carries the abstained/grounded branch so the model never
|
|
9
|
+
fabricates past a calibrated abstain.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from .memory import CilowMemory, RecallResult
|
|
18
|
+
|
|
19
|
+
GROUNDED_HEADER = "x-cilow-memory-grounded"
|
|
20
|
+
|
|
21
|
+
CILOW_RECALL_TOOL: Dict[str, Any] = {
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": "cilow_recall",
|
|
25
|
+
"description": (
|
|
26
|
+
"Recall grounded facts from verifiable long-term memory. Returns memory-grounded results, "
|
|
27
|
+
"or a calibrated abstain when memory genuinely does not know — then DO NOT fabricate."
|
|
28
|
+
),
|
|
29
|
+
"parameters": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"query": {"type": "string", "description": "What to look up in memory."},
|
|
33
|
+
"anchor": {"type": "string", "description": "Optional exact entity hint."},
|
|
34
|
+
"attribute": {"type": "string", "description": "Optional exact predicate hint."},
|
|
35
|
+
},
|
|
36
|
+
"required": ["query"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cilow_proxy_config(memory: CilowMemory, token: str) -> Dict[str, str]:
|
|
43
|
+
"""Kwargs to spread into ``OpenAI(**cilow_proxy_config(memory, token))``."""
|
|
44
|
+
return {"base_url": memory.proxy_base_url(), "api_key": token}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def grounded_from_headers(headers: Any) -> Optional[bool]:
|
|
48
|
+
"""Read the grounded signal off an OpenAI raw response's headers (``.with_raw_response``), a mapping,
|
|
49
|
+
or anything with ``.get``. Returns ``None`` when the header is absent (non-proxy)."""
|
|
50
|
+
raw = None
|
|
51
|
+
if hasattr(headers, "get"):
|
|
52
|
+
raw = headers.get(GROUNDED_HEADER)
|
|
53
|
+
if raw is None and isinstance(headers, dict):
|
|
54
|
+
raw = headers.get(GROUNDED_HEADER.upper())
|
|
55
|
+
if raw is None:
|
|
56
|
+
return None
|
|
57
|
+
return str(raw).strip().lower() == "true"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_recall_tool(memory: CilowMemory, raw_args: Union[str, Dict[str, Any]]) -> Tuple[str, RecallResult]:
|
|
61
|
+
"""Execute one ``cilow_recall`` tool call. Returns ``(content, result)``: the ``content`` string to
|
|
62
|
+
feed back as the tool result, plus the raw :class:`RecallResult` so the caller can branch. On an
|
|
63
|
+
abstain the content is an explicit "memory does not know" so the model does not fabricate."""
|
|
64
|
+
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
|
65
|
+
result = memory.recall(args["query"], anchor=args.get("anchor"), attribute=args.get("attribute"))
|
|
66
|
+
if result.abstained or not result.grounded:
|
|
67
|
+
kind = "temporarily unavailable" if result.reason_code == "provider_unavailable" else "no grounded facts"
|
|
68
|
+
content = (
|
|
69
|
+
f"MEMORY ABSTAINED ({kind}): {result.reason or 'memory does not know'}. "
|
|
70
|
+
"Do not fabricate; ask the user or say you don't know."
|
|
71
|
+
)
|
|
72
|
+
return content, result
|
|
73
|
+
lines = "\n".join(f"- {h.text}" for h in result.results)
|
|
74
|
+
return f"MEMORY-GROUNDED FACTS:\n{lines}", result
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The remaining six framework bindings — same :class:`CilowMemory` contract, same thin-shim pattern as
|
|
2
|
+
``openai.py`` / ``anthropic.py`` / ``langchain.py``. Each row documents the framework's integration
|
|
3
|
+
point and the exact mapping onto the five verbs. Where the framework interface is a plain
|
|
4
|
+
function/protocol we ship a working stub; where it requires extending a framework base class we leave a
|
|
5
|
+
typed TODO so the shim lands the moment the dep is added.
|
|
6
|
+
|
|
7
|
+
THE INVARIANT (all nine): map the framework's memory/retriever/tool onto
|
|
8
|
+
``ingest`` / ``recall`` / ``answer`` / ``reset`` / ``health`` (+ ``proxy_base_url()`` for base_url swaps)
|
|
9
|
+
and re-expose ``abstained`` + ``reason_code`` + ``grounded`` unchanged. An abstain is always surfaced
|
|
10
|
+
so the caller can apply its fallback policy.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .memory import CilowMemory
|
|
18
|
+
|
|
19
|
+
# framework -> (integration point, mapping, status)
|
|
20
|
+
REMAINING_ADAPTERS: List[Dict[str, str]] = [
|
|
21
|
+
{"framework": "LlamaIndex", "integration_point": "BaseRetriever._retrieve(QueryBundle) -> list[NodeWithScore]",
|
|
22
|
+
"mapping": "recall -> NodeWithScore list; abstain => []. Near-identical to CilowRetriever.", "status": "stub"},
|
|
23
|
+
{"framework": "Vercel AI SDK", "integration_point": "wrapLanguageModel middleware",
|
|
24
|
+
"mapping": "proxy_base_url() as the provider base URL; surface grounded in stream metadata (R2.1 streaming).", "status": "todo"},
|
|
25
|
+
{"framework": "CrewAI", "integration_point": "Storage / memory backend",
|
|
26
|
+
"mapping": "save->ingest, search->recall; branch on abstained before use.", "status": "todo"},
|
|
27
|
+
{"framework": "AutoGen", "integration_point": "Memory protocol (add / query)",
|
|
28
|
+
"mapping": "add->ingest, query->recall; abstain => empty MemoryQueryResult.", "status": "stub"},
|
|
29
|
+
{"framework": "Composio", "integration_point": "connector / tool bundle",
|
|
30
|
+
"mapping": "the five CilowMemory verbs as Composio actions.", "status": "todo"},
|
|
31
|
+
{"framework": "MCP", "integration_point": "native /mcp recall/answer/remember (already shipped)",
|
|
32
|
+
"mapping": "no shim needed — add cilow-mcp to .mcp.json.", "status": "stub"},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_llamaindex_retriever(memory: CilowMemory, *, k: Optional[int] = None, anchor: Optional[str] = None):
|
|
37
|
+
"""LlamaIndex stub — an object whose ``retrieve`` returns NodeWithScore-shaped dicts (abstain => []).
|
|
38
|
+
When ``llama-index`` is added, extend ``BaseRetriever`` and delegate to this."""
|
|
39
|
+
|
|
40
|
+
class _Retriever:
|
|
41
|
+
def retrieve(self, query: str) -> List[Dict[str, Any]]:
|
|
42
|
+
r = memory.recall(query, k=k, anchor=anchor)
|
|
43
|
+
if not r.grounded:
|
|
44
|
+
return []
|
|
45
|
+
return [
|
|
46
|
+
{"node": {"text": h.text, "metadata": {"id": h.id, "entity": h.entity, "grounded": True}}, "score": h.score}
|
|
47
|
+
for h in r.results
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
return _Retriever()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_autogen_memory(memory: CilowMemory, *, subject_hint: Optional[str] = None):
|
|
54
|
+
"""AutoGen stub — the ``Memory`` protocol (``add`` / ``query``). ``query`` returns grounded content
|
|
55
|
+
strings; an abstain yields an empty list (never a fabricated memory)."""
|
|
56
|
+
|
|
57
|
+
class _Memory:
|
|
58
|
+
def add(self, content: str) -> None:
|
|
59
|
+
memory.ingest(content, subject_hint=subject_hint)
|
|
60
|
+
|
|
61
|
+
def query(self, query: str) -> Dict[str, Any]:
|
|
62
|
+
r = memory.recall(query)
|
|
63
|
+
return {"results": [h.text for h in r.results] if r.grounded else [], "grounded": r.grounded}
|
|
64
|
+
|
|
65
|
+
return _Memory()
|