citadeldb-ms-agent-framework 2.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.
- citadeldb_ms_agent_framework/__init__.py +14 -0
- citadeldb_ms_agent_framework/history.py +164 -0
- citadeldb_ms_agent_framework/memory.py +163 -0
- citadeldb_ms_agent_framework-2.0.0.dist-info/METADATA +100 -0
- citadeldb_ms_agent_framework-2.0.0.dist-info/RECORD +6 -0
- citadeldb_ms_agent_framework-2.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Microsoft Agent Framework storage backed by Citadel, encrypted at rest."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from .history import CitadelHistoryProvider
|
|
6
|
+
from .memory import CitadelContextProvider
|
|
7
|
+
|
|
8
|
+
__all__ = ["CitadelContextProvider", "CitadelHistoryProvider", "__version__"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("citadeldb-ms-agent-framework")
|
|
13
|
+
except PackageNotFoundError: # running from a source tree, never installed
|
|
14
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""HistoryProvider over an encrypted Citadel region."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any, ClassVar, Sequence
|
|
6
|
+
|
|
7
|
+
import citadeldb
|
|
8
|
+
from agent_framework import HistoryProvider, Message
|
|
9
|
+
|
|
10
|
+
KIND = "message"
|
|
11
|
+
DEFAULT_PATH = "agent_history.cdl"
|
|
12
|
+
DEFAULT_REGION = "history"
|
|
13
|
+
PAGE = 10_000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _page(mem: Any, region: str, session_id: str) -> list[Any]:
|
|
17
|
+
"""Page to the end: one fetch is bounded, and a partial erase must not look whole."""
|
|
18
|
+
out: list[Any] = []
|
|
19
|
+
after = None
|
|
20
|
+
while True:
|
|
21
|
+
got = mem.fetch(region, KIND, payload_filter={"sid": session_id}, limit=PAGE,
|
|
22
|
+
after_id=after)
|
|
23
|
+
out.extend(got)
|
|
24
|
+
if len(got) < PAGE:
|
|
25
|
+
return out
|
|
26
|
+
after = got[-1].id
|
|
27
|
+
# session_id is optional in the protocol; keep unattributed history together.
|
|
28
|
+
DEFAULT_SESSION = "default"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# A Database is pinned to its opening thread, so workers take Memory, not self.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _text_of(message: Message) -> str:
|
|
35
|
+
"""An atom needs text to embed; a textless message falls back to role."""
|
|
36
|
+
return message.text or str(message.role)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load(mem: Any, region: str, session_id: str) -> list[Message]:
|
|
40
|
+
hits = _page(mem, region, session_id)
|
|
41
|
+
return [Message.from_dict(h.payload["msg"]) for h in hits]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _append(mem: Any, region: str, session_id: str, messages: Sequence[Message]) -> None:
|
|
45
|
+
atoms = [
|
|
46
|
+
{
|
|
47
|
+
"kind": KIND,
|
|
48
|
+
"text": _text_of(m),
|
|
49
|
+
"payload": {"sid": session_id, "msg": m.to_dict()},
|
|
50
|
+
}
|
|
51
|
+
for m in messages
|
|
52
|
+
]
|
|
53
|
+
if atoms:
|
|
54
|
+
# One batch draws a contiguous id range, so list order survives.
|
|
55
|
+
mem.remember_batch(region, atoms)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _forget(mem: Any, region: str, session_id: str) -> int:
|
|
59
|
+
hits = _page(mem, region, session_id)
|
|
60
|
+
if not hits:
|
|
61
|
+
return 0
|
|
62
|
+
return mem.forget(region, [h.id for h in hits]).erased_count
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _recall(
|
|
66
|
+
mem: Any, region: str, session_id: str, query: str, limit: int
|
|
67
|
+
) -> list[Message]:
|
|
68
|
+
hits = mem.recall(
|
|
69
|
+
region,
|
|
70
|
+
text=query,
|
|
71
|
+
k=limit,
|
|
72
|
+
kinds=[KIND],
|
|
73
|
+
options=citadeldb.RecallOptions(payload_filter={"sid": session_id}),
|
|
74
|
+
)
|
|
75
|
+
return [Message.from_dict(h.payload["msg"]) for h in hits]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CitadelHistoryProvider(HistoryProvider):
|
|
79
|
+
"""A `HistoryProvider` backed by one encrypted Citadel region."""
|
|
80
|
+
|
|
81
|
+
DEFAULT_SOURCE_ID: ClassVar[str] = "citadel_history"
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
path: str = DEFAULT_PATH,
|
|
86
|
+
key: str = "",
|
|
87
|
+
*,
|
|
88
|
+
source_id: str = DEFAULT_SOURCE_ID,
|
|
89
|
+
region: str = DEFAULT_REGION,
|
|
90
|
+
embedder: Any | None = None,
|
|
91
|
+
load_messages: bool = True,
|
|
92
|
+
store_inputs: bool = True,
|
|
93
|
+
store_context_messages: bool = False,
|
|
94
|
+
store_context_from: set[str] | None = None,
|
|
95
|
+
store_outputs: bool = True,
|
|
96
|
+
) -> None:
|
|
97
|
+
super().__init__(
|
|
98
|
+
source_id=source_id,
|
|
99
|
+
load_messages=load_messages,
|
|
100
|
+
store_inputs=store_inputs,
|
|
101
|
+
store_context_messages=store_context_messages,
|
|
102
|
+
store_context_from=store_context_from,
|
|
103
|
+
store_outputs=store_outputs,
|
|
104
|
+
)
|
|
105
|
+
if not key:
|
|
106
|
+
raise ValueError("a passphrase is required: transcripts are the payload")
|
|
107
|
+
try:
|
|
108
|
+
self._db = citadeldb.connect(path, key=key, region_keys=True)
|
|
109
|
+
except citadeldb.OperationalError as e:
|
|
110
|
+
if "locked" not in str(e):
|
|
111
|
+
raise
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
f"{path} is open in another process. Citadel is embedded, so one "
|
|
114
|
+
f"process owns the file."
|
|
115
|
+
) from e
|
|
116
|
+
self._mem = self._db.memory()
|
|
117
|
+
self._region = region
|
|
118
|
+
# Idempotent for a region of the same width, so a dim clash raises here.
|
|
119
|
+
self._mem.create_encrypted_region(
|
|
120
|
+
region, embedder or citadeldb.MockEmbedder(dim=64)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ---- the abstract surface --------------------------------------------
|
|
124
|
+
# The bindings are sync, so a worker thread keeps the event loop free.
|
|
125
|
+
|
|
126
|
+
async def get_messages(
|
|
127
|
+
self, session_id: str | None, *, state: dict[str, Any] | None = None, **kwargs: Any
|
|
128
|
+
) -> list[Message]:
|
|
129
|
+
return await asyncio.to_thread(
|
|
130
|
+
_load, self._mem, self._region, session_id or DEFAULT_SESSION
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def save_messages(
|
|
134
|
+
self,
|
|
135
|
+
session_id: str | None,
|
|
136
|
+
messages: Sequence[Message],
|
|
137
|
+
*,
|
|
138
|
+
state: dict[str, Any] | None = None,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> None:
|
|
141
|
+
# Appends rather than replaces: history is a transcript, not a set.
|
|
142
|
+
await asyncio.to_thread(
|
|
143
|
+
_append,
|
|
144
|
+
self._mem,
|
|
145
|
+
self._region,
|
|
146
|
+
session_id or DEFAULT_SESSION,
|
|
147
|
+
list(messages),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# ---- beyond the protocol ---------------------------------------------
|
|
151
|
+
|
|
152
|
+
async def search(
|
|
153
|
+
self, session_id: str | None, query: str, *, limit: int = 5
|
|
154
|
+
) -> list[Message]:
|
|
155
|
+
"""Messages from one session ranked by hybrid recall, best first."""
|
|
156
|
+
return await asyncio.to_thread(
|
|
157
|
+
_recall, self._mem, self._region, session_id or DEFAULT_SESSION, query, limit
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def forget(self, session_id: str | None) -> int:
|
|
161
|
+
"""Destroy one session's messages, returning the number erased."""
|
|
162
|
+
return await asyncio.to_thread(
|
|
163
|
+
_forget, self._mem, self._region, session_id or DEFAULT_SESSION
|
|
164
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""ContextProvider over an encrypted Citadel region."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any, ClassVar, Sequence
|
|
6
|
+
|
|
7
|
+
import citadeldb
|
|
8
|
+
from agent_framework import ContextProvider, Message
|
|
9
|
+
|
|
10
|
+
KIND = "memory"
|
|
11
|
+
DEFAULT_PATH = "agent_memory.cdl"
|
|
12
|
+
DEFAULT_REGION = "memories"
|
|
13
|
+
PAGE = 10_000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _page(mem: Any, region: str, criterion: dict[str, Any]) -> list[Any]:
|
|
17
|
+
"""Page to the end: one fetch is bounded, and a partial erase must not look whole."""
|
|
18
|
+
out: list[Any] = []
|
|
19
|
+
after = None
|
|
20
|
+
while True:
|
|
21
|
+
got = mem.fetch(region, KIND, payload_filter=criterion, limit=PAGE, after_id=after)
|
|
22
|
+
out.extend(got)
|
|
23
|
+
if len(got) < PAGE:
|
|
24
|
+
return out
|
|
25
|
+
after = got[-1].id
|
|
26
|
+
# Roles worth remembering; tool traffic is transcript detail, not knowledge.
|
|
27
|
+
_REMEMBERED_ROLES = ("user", "assistant", "system")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# A Database is pinned to its opening thread, so workers take Memory, not self.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _role_of(message: Message) -> str:
|
|
34
|
+
role = message.role
|
|
35
|
+
return getattr(role, "value", None) or str(role)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _remember(mem: Any, region: str, scope: str, texts: Sequence[str]) -> None:
|
|
39
|
+
atoms = [
|
|
40
|
+
{"kind": KIND, "text": t, "payload": {"scope": scope, "text": t}} for t in texts
|
|
41
|
+
]
|
|
42
|
+
if atoms:
|
|
43
|
+
mem.remember_batch(region, atoms)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _recall(mem: Any, region: str, scope: str, query: str, limit: int) -> list[str]:
|
|
47
|
+
"""Distinct memories for `scope`, best first.
|
|
48
|
+
|
|
49
|
+
after_run stores every turn verbatim, so a fact the user restates is stored
|
|
50
|
+
once per turn. Those copies are one memory to the model, and asking the
|
|
51
|
+
engine for `limit` rows would spend the whole budget on them - 30 repeats of
|
|
52
|
+
one fact deliver one line and hide everything else in the scope. Widen until
|
|
53
|
+
`limit` distinct texts are found or the scope runs out.
|
|
54
|
+
"""
|
|
55
|
+
options = citadeldb.RecallOptions(payload_filter={"scope": scope})
|
|
56
|
+
k = max(limit, 32)
|
|
57
|
+
while True:
|
|
58
|
+
hits = mem.recall(region, text=query, k=k, kinds=[KIND], options=options)
|
|
59
|
+
out: list[str] = []
|
|
60
|
+
seen: set[str] = set()
|
|
61
|
+
for h in hits:
|
|
62
|
+
text = h.payload["text"]
|
|
63
|
+
if text in seen:
|
|
64
|
+
continue
|
|
65
|
+
seen.add(text)
|
|
66
|
+
out.append(text)
|
|
67
|
+
if len(out) >= limit or len(hits) < k:
|
|
68
|
+
return out[:limit]
|
|
69
|
+
k *= 2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _forget(mem: Any, region: str, scope: str) -> int:
|
|
73
|
+
hits = _page(mem, region, {"scope": scope})
|
|
74
|
+
if not hits:
|
|
75
|
+
return 0
|
|
76
|
+
return mem.forget(region, [h.id for h in hits]).erased_count
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CitadelContextProvider(ContextProvider):
|
|
80
|
+
"""A `ContextProvider` backed by one encrypted Citadel region."""
|
|
81
|
+
|
|
82
|
+
DEFAULT_SOURCE_ID: ClassVar[str] = "citadel_memory"
|
|
83
|
+
DEFAULT_CONTEXT_PROMPT: ClassVar[str] = (
|
|
84
|
+
"## Memories\nConsider the following memories from earlier conversations:"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
path: str = DEFAULT_PATH,
|
|
90
|
+
key: str = "",
|
|
91
|
+
*,
|
|
92
|
+
source_id: str = DEFAULT_SOURCE_ID,
|
|
93
|
+
scope: str = "default",
|
|
94
|
+
region: str = DEFAULT_REGION,
|
|
95
|
+
embedder: Any | None = None,
|
|
96
|
+
limit: int = 5,
|
|
97
|
+
context_prompt: str = DEFAULT_CONTEXT_PROMPT,
|
|
98
|
+
) -> None:
|
|
99
|
+
super().__init__(source_id)
|
|
100
|
+
if not key:
|
|
101
|
+
raise ValueError("a passphrase is required: memories are the payload")
|
|
102
|
+
self.scope = scope
|
|
103
|
+
self.limit = limit
|
|
104
|
+
self.context_prompt = context_prompt
|
|
105
|
+
try:
|
|
106
|
+
self._db = citadeldb.connect(path, key=key, region_keys=True)
|
|
107
|
+
except citadeldb.OperationalError as e:
|
|
108
|
+
if "locked" not in str(e):
|
|
109
|
+
raise
|
|
110
|
+
raise RuntimeError(
|
|
111
|
+
f"{path} is open in another process. Citadel is embedded, so one "
|
|
112
|
+
f"process owns the file."
|
|
113
|
+
) from e
|
|
114
|
+
self._mem = self._db.memory()
|
|
115
|
+
self._region = region
|
|
116
|
+
# Idempotent for a region of the same width, so a dim clash raises here.
|
|
117
|
+
self._mem.create_encrypted_region(
|
|
118
|
+
region, embedder or citadeldb.MockEmbedder(dim=64)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ---- the pipeline hooks ----------------------------------------------
|
|
122
|
+
|
|
123
|
+
async def before_run(
|
|
124
|
+
self, *, agent: Any, session: Any, context: Any, state: dict[str, Any]
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Recall what is relevant to this turn and add it to the context."""
|
|
127
|
+
query = "\n".join(
|
|
128
|
+
m.text for m in context.input_messages if m and m.text and m.text.strip()
|
|
129
|
+
)
|
|
130
|
+
if not query:
|
|
131
|
+
return
|
|
132
|
+
memories = await asyncio.to_thread(
|
|
133
|
+
_recall, self._mem, self._region, self.scope, query, self.limit
|
|
134
|
+
)
|
|
135
|
+
if not memories:
|
|
136
|
+
return
|
|
137
|
+
context.extend_messages(
|
|
138
|
+
self.source_id,
|
|
139
|
+
[Message("user", [f"{self.context_prompt}\n" + "\n".join(memories)])],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def after_run(
|
|
143
|
+
self, *, agent: Any, session: Any, context: Any, state: dict[str, Any]
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Remember this turn, inputs and response alike."""
|
|
146
|
+
turn: list[Message] = list(context.input_messages)
|
|
147
|
+
if context.response and context.response.messages:
|
|
148
|
+
turn.extend(context.response.messages)
|
|
149
|
+
texts = [
|
|
150
|
+
m.text
|
|
151
|
+
for m in turn
|
|
152
|
+
if m and m.text and m.text.strip() and _role_of(m) in _REMEMBERED_ROLES
|
|
153
|
+
]
|
|
154
|
+
if texts:
|
|
155
|
+
await asyncio.to_thread(
|
|
156
|
+
_remember, self._mem, self._region, self.scope, texts
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ---- beyond the pipeline ---------------------------------------------
|
|
160
|
+
|
|
161
|
+
async def forget(self) -> int:
|
|
162
|
+
"""Destroy this scope's memories, returning the number erased."""
|
|
163
|
+
return await asyncio.to_thread(_forget, self._mem, self._region, self.scope)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: citadeldb-ms-agent-framework
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Microsoft Agent Framework chat history backed by Citadel: encrypted at rest, with deletes that destroy the key
|
|
5
|
+
Project-URL: Homepage, https://citadeldb.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/yp3y5akh0v/citadel
|
|
7
|
+
Author: Yuriy Peysakhov
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: agent-framework,agents,autogen,chat-history,encryption,memory,microsoft-agent-framework
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Database
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: agent-framework-core<2,>=1.13
|
|
17
|
+
Requires-Dist: citadeldb<3,>=2.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# citadeldb-ms-agent-framework
|
|
24
|
+
|
|
25
|
+
[Microsoft Agent Framework](https://github.com/microsoft/agent-framework) storage backed
|
|
26
|
+
by [Citadel](https://citadeldb.dev). Encrypted at rest, embedded in your process, and
|
|
27
|
+
deletes that destroy the key, not just the row.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pip install citadeldb-ms-agent-framework
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Two providers for two jobs, matching how the framework's own Redis integration is split:
|
|
34
|
+
|
|
35
|
+
| Class | Implements | Use when |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `CitadelHistoryProvider` | `HistoryProvider` | a session must recover its complete transcript |
|
|
38
|
+
| `CitadelContextProvider` | `ContextProvider` | an agent should recall relevant facts across sessions |
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from agent_framework import Agent
|
|
42
|
+
from citadeldb_ms_agent_framework import CitadelContextProvider, CitadelHistoryProvider
|
|
43
|
+
|
|
44
|
+
agent = Agent(
|
|
45
|
+
client=chat_client, # any agent_framework chat client
|
|
46
|
+
context_providers=[
|
|
47
|
+
CitadelHistoryProvider("agent.cdl", key="your-passphrase"),
|
|
48
|
+
CitadelContextProvider("agent.cdl", key="your-passphrase", scope="user-123"),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both can share one encrypted file: a path already open on this thread, under the same
|
|
54
|
+
passphrase, is shared. Construct them on the same thread.
|
|
55
|
+
|
|
56
|
+
These are the framework's own extension points, with the file encrypted and a key per
|
|
57
|
+
message. The built-in `FileHistoryProvider` writes plaintext JSONL or MessagePack.
|
|
58
|
+
|
|
59
|
+
## Deletes destroy the key
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
history = CitadelHistoryProvider("agent.cdl", key="your-passphrase")
|
|
63
|
+
memory = CitadelContextProvider("agent.cdl", key="your-passphrase", scope="user-123")
|
|
64
|
+
|
|
65
|
+
await history.forget("session-42") # returns the number erased
|
|
66
|
+
await memory.forget() # this provider's whole scope
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Clearing a conversation destroys each message's own key and then deletes its row, so any
|
|
70
|
+
ciphertext surviving elsewhere stays unreadable.
|
|
71
|
+
|
|
72
|
+
## History provider
|
|
73
|
+
|
|
74
|
+
Implements `get_messages` and `save_messages`; the base class's `before_run`/`after_run`
|
|
75
|
+
handle loading and storing according to its configuration flags, so an audit-only or
|
|
76
|
+
evaluation-only provider works as documented:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
CitadelHistoryProvider("agent.cdl", key="your-passphrase", load_messages=False) # stores, never loads
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Messages round-trip through the framework's own serialization, so roles, author names,
|
|
83
|
+
multi-part contents and `additional_properties` all survive.
|
|
84
|
+
|
|
85
|
+
## Context provider
|
|
86
|
+
|
|
87
|
+
Recalls with Citadel's hybrid search: vector distance, keyword rank and recency, fused
|
|
88
|
+
into one score. The default `MockEmbedder` is lexical; pass `embedder=` a real one to
|
|
89
|
+
match across wording.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
memory = CitadelContextProvider("agent.cdl", key="your-passphrase", scope="user-123", limit=5)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Memories are scoped rather than session-bound, so a later conversation can recall an
|
|
96
|
+
earlier one. `scope` is the boundary an erasure request applies to.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
Apache-2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
citadeldb_ms_agent_framework/__init__.py,sha256=BKdeYcX4OYg0f4oihIjpPKZs4wo_xBYAw39Y4vlj55c,478
|
|
2
|
+
citadeldb_ms_agent_framework/history.py,sha256=9XvFQ70kJ9_-SiSo0pDdt7_y7VZHLGRlvUuluFO73C0,5464
|
|
3
|
+
citadeldb_ms_agent_framework/memory.py,sha256=KQKvsvIlrz0IsHAkIc1JvMq4NDk8WlSD3UuTYemPnWk,5770
|
|
4
|
+
citadeldb_ms_agent_framework-2.0.0.dist-info/METADATA,sha256=YDDb7Id6acv1jwJRN6_lYybTNwl-i8WdZ3rZ2IR3qFo,3801
|
|
5
|
+
citadeldb_ms_agent_framework-2.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
6
|
+
citadeldb_ms_agent_framework-2.0.0.dist-info/RECORD,,
|