community-agent-framework-postgres 0.1.1__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.
- agent_framework_community_postgres/__init__.py +77 -0
- agent_framework_community_postgres/__main__.py +3 -0
- agent_framework_community_postgres/_checkpoint_storage.py +218 -0
- agent_framework_community_postgres/_cli.py +97 -0
- agent_framework_community_postgres/_client.py +390 -0
- agent_framework_community_postgres/_entra.py +178 -0
- agent_framework_community_postgres/_framework.py +148 -0
- agent_framework_community_postgres/_history_provider.py +261 -0
- agent_framework_community_postgres/_json.py +34 -0
- agent_framework_community_postgres/_leases.py +196 -0
- agent_framework_community_postgres/_migrations.py +235 -0
- agent_framework_community_postgres/_persistence.py +184 -0
- agent_framework_community_postgres/_retention.py +159 -0
- agent_framework_community_postgres/_session_store.py +135 -0
- agent_framework_community_postgres/_store.py +66 -0
- agent_framework_community_postgres/_thread_snapshot_store.py +162 -0
- agent_framework_community_postgres/py.typed +0 -0
- community_agent_framework_postgres-0.1.1.dist-info/METADATA +367 -0
- community_agent_framework_postgres-0.1.1.dist-info/RECORD +21 -0
- community_agent_framework_postgres-0.1.1.dist-info/WHEEL +4 -0
- community_agent_framework_postgres-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: community-agent-framework-postgres
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: PostgreSQL storage for Microsoft Agent Framework: chat history, sessions, workflow checkpoints, AG-UI thread snapshots and leases.
|
|
5
|
+
Project-URL: Source, https://github.com/matias-io/community-agent-framework-postgres
|
|
6
|
+
Project-URL: Issues, https://github.com/matias-io/community-agent-framework-postgres/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/matias-io/community-agent-framework-postgres/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://github.com/matias-io/community-agent-framework-postgres/tree/main/docs
|
|
9
|
+
Author: Matias Suxo Salinas
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-framework,checkpoint,history-provider,microsoft-agent-framework,postgresql,psycopg,session-store
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: agent-framework-core<2,>=1.20.0
|
|
24
|
+
Requires-Dist: psycopg[binary,pool]<4,>=3.3.5
|
|
25
|
+
Provides-Extra: ag-ui
|
|
26
|
+
Requires-Dist: agent-framework-ag-ui<2,>=1.5.0; extra == 'ag-ui'
|
|
27
|
+
Provides-Extra: azure
|
|
28
|
+
Requires-Dist: aiohttp<4,>=3.9; extra == 'azure'
|
|
29
|
+
Requires-Dist: azure-identity<2,>=1.19; extra == 'azure'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# community-agent-framework-postgres
|
|
33
|
+
A community-maintained PostgreSQL persistence implementation for Microsoft Agent Framework (MAF), supporting agent chat history, sessions, workflow checkpoints, AG-UI snapshots, and a lease that serializes work across replicas.
|
|
34
|
+
|
|
35
|
+
The package provides `PostgresHistoryProvider` for chat history, `PostgresSessionStore` for `AgentSession` snapshots, `PostgresCheckpointStorage` for workflow checkpoints, `PostgresAGUIThreadSnapshotStore` for AG-UI thread snapshots and `PostgresLeases` for a lock across replicas. `PostgresPersistence` creates all of them over one connection pool and runs the schema migrations. This is a community package written by Matias Suxo Salinas. Microsoft does not maintain or support it.
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
|
|
39
|
+
Alpha. Public names and constructor arguments may change in a 0.x minor release, and the [changelog](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/CHANGELOG.md) lists every such change. Schema changes ship as new numbered migrations, so `migrate()` upgrades an existing database in place.
|
|
40
|
+
|
|
41
|
+
## Compatibility
|
|
42
|
+
|
|
43
|
+
| 0.1.1 | Declared | Tested |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `agent-framework-core` | `>=1.20.0,<2` | 1.20.0, 1.21.0 |
|
|
46
|
+
| `agent-framework-ag-ui` (`ag-ui` extra) | `>=1.5.0,<2` | 1.5.0, 1.5.1 |
|
|
47
|
+
| Python | `>=3.11` | 3.11, 3.12, 3.13, 3.14 |
|
|
48
|
+
| PostgreSQL | | 16, 17 |
|
|
49
|
+
| psycopg | `>=3.3.5,<4` | 3.3.6 |
|
|
50
|
+
|
|
51
|
+
pip and uv refuse a MAF version outside the declared range. A newer minor inside it installs. The first store or hub built in a process then logs one warning for each untested distribution, and a new major, which pip and uv install only when forced, is logged as outside the declared range. `compatibility()` returns the installed and tested versions for a diagnostics command. A newer minor is expected to work, because MAF keeps breaking changes for major versions. [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md) explains the support policy and the compatibility check.
|
|
52
|
+
|
|
53
|
+
MAF 1.19 and older are not supported. Their history dedupe drops any new message that matches one already stored, so a user who answers "yes" twice loses the second "yes". MAF 1.20 fixed that, so 1.20 is the floor.
|
|
54
|
+
|
|
55
|
+
0.1.1 is the first release on PyPI. 0.1.0 was never published.
|
|
56
|
+
|
|
57
|
+
## What it implements
|
|
58
|
+
|
|
59
|
+
| Class | MAF interface | Extra | Conditions |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `PostgresHistoryProvider` | `HistoryProvider` | none | Drops replayed messages with MAF's own `filter_new_messages` |
|
|
62
|
+
| `PostgresSessionStore` | `SessionStore` | none | MAF marks `SessionStore` experimental |
|
|
63
|
+
| `PostgresCheckpointStorage` | `CheckpointStorage` | none | Decodes only MAF's safe types and its own; `allowed_checkpoint_types` adds more |
|
|
64
|
+
| `PostgresAGUIThreadSnapshotStore` | `AGUIThreadSnapshotStore` | `ag-ui` | The AG-UI endpoint needs a `snapshot_scope_resolver` next to `snapshot_store` |
|
|
65
|
+
| `PostgresLeases` | none (this package's own) | none | A cross-replica lock; MAF hosting serializes runs per process only |
|
|
66
|
+
| `PostgresPersistence` | none (creates the stores above over one pool) | none | |
|
|
67
|
+
| Microsoft Entra ID sign-in (`credential=`) | none | `azure` | Azure Database for PostgreSQL with Entra authentication |
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv add community-agent-framework-postgres
|
|
73
|
+
uv add "community-agent-framework-postgres[ag-ui]" # adds PostgresAGUIThreadSnapshotStore
|
|
74
|
+
uv add "community-agent-framework-postgres[azure]" # adds Microsoft Entra ID sign-in
|
|
75
|
+
pip install community-agent-framework-postgres
|
|
76
|
+
pip install "community-agent-framework-postgres[ag-ui]"
|
|
77
|
+
pip install "community-agent-framework-postgres[azure]"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `ag-ui` extra installs `agent-framework-ag-ui`. Without it, every store except `PostgresAGUIThreadSnapshotStore` works. `from agent_framework_community_postgres import *` also needs the extra, because `__all__` lists that store. The `azure` extra installs `azure-identity`, and `aiohttp` for its async credentials, for [Microsoft Entra ID sign-in](#microsoft-entra-id-on-azure).
|
|
81
|
+
|
|
82
|
+
On Windows, async psycopg cannot run on the default `ProactorEventLoop`. Run a script's entry point on a selector loop:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
86
|
+
runner.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run uvicorn 0.36 or later with `--loop asyncio:SelectorEventLoop`. uvicorn imports that value as the loop factory. Its plain `--loop asyncio` picks `ProactorEventLoop` on Windows unless reload or workers are on. Use `127.0.0.1` rather than `localhost` in local connection strings. libpq tries `::1` first, and some Windows and WSL setups drop that traffic without an error.
|
|
90
|
+
|
|
91
|
+
## Quick start
|
|
92
|
+
|
|
93
|
+
This script runs against the database from `docker-compose.yml`. It needs the `ag-ui` extra.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
from agent_framework import AgentSession, Message
|
|
100
|
+
|
|
101
|
+
from agent_framework_community_postgres import PostgresPersistence
|
|
102
|
+
|
|
103
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def main() -> None:
|
|
107
|
+
async with PostgresPersistence(application_id="quickstart", connection_string=DSN) as hub:
|
|
108
|
+
await hub.migrate()
|
|
109
|
+
history = hub.history_provider()
|
|
110
|
+
await history.save_messages("session-1", [Message(role="user", contents=["hello"])])
|
|
111
|
+
print([m.text for m in await history.get_messages("session-1")])
|
|
112
|
+
await hub.session_store().set("session-1", AgentSession(session_id="session-1"))
|
|
113
|
+
print(await hub.checkpoint_storage(scope="session-1").list_checkpoint_ids(workflow_name="demo"))
|
|
114
|
+
print(await hub.thread_snapshot_store().get(scope="user-1", thread_id="thread-1"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
118
|
+
runner.run(main())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`compatibility()` returns the installed and tested MAF versions, for a startup check or a diagnostics command.
|
|
122
|
+
|
|
123
|
+
Every store the hub creates shares its pool, `application_id`, `schema` and `table_prefix`. The factories raise `TypeError` if you pass `application_id`, `client`, `credential`, `pool_kwargs`, `schema` or `table_prefix`. For another schema or prefix, create a separate hub or a standalone store. You can override `retention` per store. `hub.purge()` covers every store created by that hub, including one with its own `retention`, and needs no policy of its own. It purges the rows whose `expires_at` has passed, in the mode you pass or else the hub policy's mode. `hub.leases()` returns a `PostgresLeases` over the same pool. After `hub.close()`, every store it created raises `PostgresStorageError`, even when the hub was built over your own pool.
|
|
124
|
+
|
|
125
|
+
## Use with Agent Framework
|
|
126
|
+
|
|
127
|
+
Give an agent the history provider in `context_providers` and pass a session to `run`. The provider loads and saves that session's messages. This snippet needs a chat client. It uses `OpenAIChatClient` from `agent-framework-openai` with `OPENAI_API_KEY` set, but any MAF chat client works.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import asyncio
|
|
131
|
+
import sys
|
|
132
|
+
|
|
133
|
+
from agent_framework import Agent
|
|
134
|
+
from agent_framework.openai import OpenAIChatClient
|
|
135
|
+
|
|
136
|
+
from agent_framework_community_postgres import PostgresPersistence
|
|
137
|
+
|
|
138
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def main() -> None:
|
|
142
|
+
async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
|
|
143
|
+
await hub.migrate()
|
|
144
|
+
agent = Agent(OpenAIChatClient(model="gpt-4o"), "Answer briefly.", context_providers=[hub.history_provider()])
|
|
145
|
+
session = agent.create_session()
|
|
146
|
+
print((await agent.run("My name is Ada.", session=session)).text)
|
|
147
|
+
print((await agent.run("What is my name?", session=session)).text)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
151
|
+
runner.run(main())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Pass the checkpoint storage to `WorkflowBuilder`. The workflow then writes checkpoints you can resume from. [docs/checkpoints.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/checkpoints.md) has the full example.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import asyncio
|
|
158
|
+
import sys
|
|
159
|
+
|
|
160
|
+
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
|
161
|
+
|
|
162
|
+
from agent_framework_community_postgres import PostgresPersistence
|
|
163
|
+
|
|
164
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Shout(Executor):
|
|
168
|
+
@handler
|
|
169
|
+
async def run(self, text: str, ctx: WorkflowContext[str, str]) -> None:
|
|
170
|
+
await ctx.yield_output(text.upper())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def main() -> None:
|
|
174
|
+
async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
|
|
175
|
+
await hub.migrate()
|
|
176
|
+
storage = hub.checkpoint_storage(scope="conversation-1")
|
|
177
|
+
workflow = WorkflowBuilder(start_executor=Shout(id="shout"), checkpoint_storage=storage).build()
|
|
178
|
+
print((await workflow.run("hello")).get_outputs())
|
|
179
|
+
print(await storage.get_latest(workflow_name=workflow.name) is not None)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
183
|
+
runner.run(main())
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Save the `AgentSession` with `PostgresSessionStore` to continue a conversation in another process. An application loads the session at the start of a request and saves it after `agent.run` returns.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import asyncio
|
|
190
|
+
import sys
|
|
191
|
+
|
|
192
|
+
from agent_framework import AgentSession
|
|
193
|
+
|
|
194
|
+
from agent_framework_community_postgres import PostgresPersistence
|
|
195
|
+
|
|
196
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def main() -> None:
|
|
200
|
+
async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
|
|
201
|
+
await hub.migrate()
|
|
202
|
+
sessions = hub.session_store()
|
|
203
|
+
session = await sessions.get("user-1:chat-1") or AgentSession(session_id="chat-1")
|
|
204
|
+
session.state["visits"] = session.state.get("visits", 0) + 1
|
|
205
|
+
await sessions.set("user-1:chat-1", session)
|
|
206
|
+
print(session.session_id, session.state)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
210
|
+
runner.run(main())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
In an agent, create the new session with `agent.create_session(session_id=...)` instead of `AgentSession(...)`. See [docs/history.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/history.md) and [docs/sessions.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/sessions.md).
|
|
214
|
+
|
|
215
|
+
## Standalone use
|
|
216
|
+
|
|
217
|
+
Each store also works on its own. A standalone store does not migrate, so create the tables first with `hub.migrate()` or the CLI.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5433/agent_framework python -m agent_framework_community_postgres migrate
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
In PowerShell:
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
$env:POSTGRES_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"; python -m agent_framework_community_postgres migrate
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import asyncio
|
|
231
|
+
import sys
|
|
232
|
+
|
|
233
|
+
from agent_framework import Message
|
|
234
|
+
|
|
235
|
+
from agent_framework_community_postgres import PostgresHistoryProvider
|
|
236
|
+
|
|
237
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def main() -> None:
|
|
241
|
+
async with PostgresHistoryProvider(application_id="my-app", connection_string=DSN) as history:
|
|
242
|
+
await history.save_messages("session-1", [Message(role="user", contents=["hello"])])
|
|
243
|
+
print([m.text for m in await history.get_messages("session-1")])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
247
|
+
runner.run(main())
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Every store and the hub take the same connection arguments.
|
|
251
|
+
|
|
252
|
+
- `connection_string` or `client`. Pass exactly one. When both are absent, the connection string comes from `POSTGRES_CONNECTION_STRING`, read through MAF's `load_settings`. An explicit argument wins over the `.env` file named by `env_file_path`, which wins over the environment.
|
|
253
|
+
- `client` is a psycopg `AsyncConnection` or `AsyncConnectionPool`. A pool that is not open is opened on first use. The package never closes a client it did not create.
|
|
254
|
+
- `credential` signs the owned pool in to Azure Database for PostgreSQL with Microsoft Entra ID tokens. It needs a connection string and cannot be combined with `client`. With a credential, the pool defaults to `sslmode=verify-full` with `sslrootcert=system`. An `sslmode` or `sslrootcert` set in `pool_kwargs["kwargs"]`, the connection string, `PGSSLMODE` or `PGSSLROOTCERT` takes precedence over these defaults, and the store or hub raises `ValueError` when it is created if the `sslmode` in effect is `disable`, `allow` or `prefer`. An explicit `sslmode` also drops the `sslrootcert=system` default. On Windows, `system` finds no certificates in psycopg's binary wheel, so set `PGSSLROOTCERT` to a CA file; [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) says which. See [Microsoft Entra ID on Azure](#microsoft-entra-id-on-azure).
|
|
255
|
+
- A single `AsyncConnection` runs one call at a time. Every store and hub over it waits for the same lock, so a server should pass a pool. On a connection already inside your own transaction, writes and multi-statement calls become a savepoint, single-statement reads run directly inside your transaction, and transaction-scoped locks (the history save lock and the migration lock) are held until your transaction ends.
|
|
256
|
+
- A connection string makes the store own a pool with `min_size=1`, `max_size=10`, autocommit, libpq `connect_timeout=10` unless the string sets one, and a pool `timeout` of 10 seconds.
|
|
257
|
+
- The owned pool also sets TCP keepalives (`keepalives=1`, `keepalives_idle=30`, `keepalives_interval=10`, `keepalives_count=3`) and `tcp_user_timeout=30000` unless the connection string sets them, so a silently dead connection fails within about a minute. The pool sets no statement or lock timeout; `migrate()` sets its own lock timeout.
|
|
258
|
+
- `pool_kwargs` is merged over the owned pool's defaults, and the `kwargs` entry inside it over the connection defaults; for example `{"max_size": 50, "kwargs": {"prepare_threshold": None}}` for PgBouncer. A connection parameter in `kwargs` wins over the same parameter in the connection string. It cannot be combined with `client`.
|
|
259
|
+
- `schema="public"` must be a lowercase identifier that does not start with `pg_`. `table_prefix="af_"` allows lowercase letters, digits and underscores, up to 28 bytes.
|
|
260
|
+
|
|
261
|
+
When the database is down or rejects the login, or every pooled connection stays busy, each call raises `PostgresStorageError("No PostgreSQL connection became available within 10 seconds: ...")` after about 10 seconds. psycopg_pool raises the same timeout in both cases, so the message names both. Over your own pool, the message gives that pool's `timeout`. The owned pool keeps retrying in the background and recovers when the database returns. psycopg_pool logs the driver's reason on the `psycopg.pool` logger. If you need other timeouts, set them through `pool_kwargs` or pass your own pool. A malformed connection string raises `PostgresStorageError("Invalid connection string.")` and never repeats libpq's text, which can contain the password.
|
|
262
|
+
|
|
263
|
+
A driver failure raises `PostgresStorageError` with the driver exception chained as the cause. Its `sqlstate` is the server's error code when there was one. Its `retryable` is `True` for SQLSTATE classes `08` (connection exceptions), `40` (serialization failures and deadlocks), `53` (insufficient resources, such as too many connections), codes `57P0x` (operator intervention, such as an administrator shutdown) and `55P03` (a lock timeout), for a connection lost without a SQLSTATE, and for the pool timeout above. A fresh call may get past these.
|
|
264
|
+
|
|
265
|
+
After a database restart or failover, each stale connection in the owned pool fails one call before the pool replaces it. Behind PgBouncer in transaction mode older than 1.21, or without `max_prepared_statements`, psycopg's prepared statements fail, because psycopg prepares a query after its fifth run. For either case, pass your own pool. `check=AsyncConnectionPool.check_connection` tests each connection as the pool hands it out, at the cost of one round trip, and `prepare_threshold=None` turns prepared statements off.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
import asyncio
|
|
269
|
+
import sys
|
|
270
|
+
|
|
271
|
+
from psycopg_pool import AsyncConnectionPool
|
|
272
|
+
|
|
273
|
+
from agent_framework_community_postgres import PostgresPersistence
|
|
274
|
+
|
|
275
|
+
DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def main() -> None:
|
|
279
|
+
pool = AsyncConnectionPool(
|
|
280
|
+
DSN,
|
|
281
|
+
open=False,
|
|
282
|
+
kwargs={"autocommit": True, "prepare_threshold": None},
|
|
283
|
+
check=AsyncConnectionPool.check_connection,
|
|
284
|
+
)
|
|
285
|
+
async with pool, PostgresPersistence(application_id="my-app", client=pool) as hub:
|
|
286
|
+
await hub.migrate()
|
|
287
|
+
print(await hub.pending_migrations())
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
|
|
291
|
+
runner.run(main())
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Microsoft Entra ID on Azure
|
|
295
|
+
|
|
296
|
+
With the `azure` extra, pass an `azure-identity` credential as `credential=` and the owned pool signs in to Azure Database for PostgreSQL with a Microsoft Entra ID token for each new connection, instead of a password:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from azure.identity.aio import DefaultAzureCredential
|
|
300
|
+
|
|
301
|
+
DSN = "host=my-server.postgres.database.azure.com dbname=agent_framework user=ada@contoso.com"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def main() -> None:
|
|
305
|
+
async with (
|
|
306
|
+
DefaultAzureCredential() as credential,
|
|
307
|
+
PostgresPersistence(application_id="my-app", connection_string=DSN, credential=credential) as hub,
|
|
308
|
+
):
|
|
309
|
+
await hub.migrate()
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A failing credential raises `PostgresStorageError` naming its error on the first call. A managed identity or a service principal has no user name in its token, so set `user=` to its database role.
|
|
313
|
+
|
|
314
|
+
This is verified with a fake credential against PostgreSQL 17 locally, and the CI matrix runs the same tests on PostgreSQL 16 and 17. No real Azure server has been used yet. [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) covers the server setup, managed identities, token lifetime and the CLI.
|
|
315
|
+
|
|
316
|
+
## Schema
|
|
317
|
+
|
|
318
|
+
Tables are created in `schema` and named `{table_prefix}{name}`. With the defaults:
|
|
319
|
+
|
|
320
|
+
| Table | Key | Holds |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `af_history_messages` | `id`, read by application, tenant, agent, source and session | One `Message.to_dict()` per row |
|
|
323
|
+
| `af_sessions` | `application_id, session_id` | `AgentSession.to_dict()` |
|
|
324
|
+
| `af_checkpoints` | `application_id, tenant_id, scope, checkpoint_id` | The encoded `WorkflowCheckpoint` |
|
|
325
|
+
| `af_thread_snapshots` | `application_id, scope, thread_id` | The latest AG-UI thread snapshot |
|
|
326
|
+
| `af_leases` | `application_id, resource` | Lease owner, token and expiry |
|
|
327
|
+
| `af_migrations` | `version` | Applied migration versions |
|
|
328
|
+
|
|
329
|
+
## Retention
|
|
330
|
+
|
|
331
|
+
Retention is off unless you pass `RetentionPolicy(ttl=...)`. With a TTL, every write sets `expires_at` to now plus the TTL, and `purge()` or the `purge` CLI command tombstones or deletes the rows whose `expires_at` has passed. History messages, checkpoints and expired leases are always deleted. Leases take no policy. A history session is purged whole, once every one of its messages has expired, so an active conversation keeps its transcript. A purge works in batches of 1000 rows, one transaction per batch, and skips rows another purge has locked. `hub.purge()` covers every table, leases included. It returns a `PurgeReport`; `report.counts` is a read-only mapping, so use `dict(report.counts)` for a copy you can change. Until a purge runs, reads still return expired rows. See [docs/retention.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/retention.md).
|
|
332
|
+
|
|
333
|
+
## Private Agent Framework imports
|
|
334
|
+
|
|
335
|
+
The package uses three names MAF does not export. `filter_new_messages` deduplicates history, and `encode_checkpoint_value` and `decode_checkpoint_value` encode checkpoints. Microsoft's own Cosmos DB package imports all three, and its Redis package imports `filter_new_messages`. `_framework.py` is the only module that imports them, and `tests/unit/test_framework.py` fails when one of them moves. CI also runs the suite against the newest `agent-framework-core` the declared range allows, and a weekly canary runs it against the newest MAF release, so an upstream rename shows up there first. See [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md).
|
|
336
|
+
|
|
337
|
+
## Development
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
docker compose up -d --wait
|
|
341
|
+
export POSTGRES_TEST_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5433/agent_framework
|
|
342
|
+
# PowerShell: $env:POSTGRES_TEST_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
|
|
343
|
+
uv run pytest
|
|
344
|
+
uv run ruff check .
|
|
345
|
+
uv run ruff format --check .
|
|
346
|
+
uv run pyright
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Without `POSTGRES_TEST_CONNECTION_STRING`, the integration and conformance tests are skipped. See [CONTRIBUTING.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/CONTRIBUTING.md).
|
|
350
|
+
|
|
351
|
+
## Documentation
|
|
352
|
+
|
|
353
|
+
- [docs/history.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/history.md) covers `PostgresHistoryProvider`.
|
|
354
|
+
- [docs/sessions.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/sessions.md) covers `PostgresSessionStore`.
|
|
355
|
+
- [docs/checkpoints.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/checkpoints.md) covers `PostgresCheckpointStorage`.
|
|
356
|
+
- [docs/thread-snapshots.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/thread-snapshots.md) covers `PostgresAGUIThreadSnapshotStore`.
|
|
357
|
+
- [docs/leases.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/leases.md) covers `PostgresLeases`.
|
|
358
|
+
- [docs/retention.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/retention.md) covers `RetentionPolicy` and purging.
|
|
359
|
+
- [docs/migrations.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/migrations.md) covers `migrate()`, the CLI and SQL for a DBA.
|
|
360
|
+
- [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) covers Microsoft Entra ID sign-in on Azure Database for PostgreSQL.
|
|
361
|
+
- [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md) covers versions, private imports, JSONB limits and Windows.
|
|
362
|
+
|
|
363
|
+
Runnable scripts for each store are in [samples/](https://github.com/matias-io/community-agent-framework-postgres/tree/v0.1.1/samples).
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
MIT. See [LICENSE](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
agent_framework_community_postgres/__init__.py,sha256=1EXV3in5WdfdYqn0CX0r425KhUaqwWXLRytoEgEpWrE,2195
|
|
2
|
+
agent_framework_community_postgres/__main__.py,sha256=g-q3cJEP2vv6Ppov3S-YHp3sJsGGZH4v3gYvvwdFOxg,49
|
|
3
|
+
agent_framework_community_postgres/_checkpoint_storage.py,sha256=uKW_nXx_ClE9z6jxYpNnpuSuKtwCH24gErna029Zg7E,10640
|
|
4
|
+
agent_framework_community_postgres/_cli.py,sha256=xd8caEm2Ifab2z6Gfl09cvj3ufvdBgNPMyvTN-ko0k0,4378
|
|
5
|
+
agent_framework_community_postgres/_client.py,sha256=TK7DpPyWEPJuzfy_kVlKGM9AxGzXXAEbKbrZJmD9xp4,18575
|
|
6
|
+
agent_framework_community_postgres/_entra.py,sha256=s45zJHYSUibbR4IRM3cpjiQ69gO4aq3k-orQRcxZUd4,7872
|
|
7
|
+
agent_framework_community_postgres/_framework.py,sha256=kEZZ6nwXRh74v_d-wnGYuxsUOtPXPtfmyQy3zQ2kQjc,5825
|
|
8
|
+
agent_framework_community_postgres/_history_provider.py,sha256=OOtVr3axvX06bVOroIWtoeyLcFA95GrvGm6s2FcxWiw,12443
|
|
9
|
+
agent_framework_community_postgres/_json.py,sha256=PrycccJ2WIKh8J7XxgPJ958hcjuNcnhGA8DRnWB2zOk,1526
|
|
10
|
+
agent_framework_community_postgres/_leases.py,sha256=JisCXs40Sz5A8AcZ1-kTY5-g7z5rOJG0RFKhWYnFGyA,8660
|
|
11
|
+
agent_framework_community_postgres/_migrations.py,sha256=qW6qX0rg2lVIcV4HTWxg0DLRrup4JZ4hbru4cr5CVLE,11201
|
|
12
|
+
agent_framework_community_postgres/_persistence.py,sha256=AMCt_Tmaxu2VcHcc2FN4TgqylremtW82BNyDIVUZimM,7941
|
|
13
|
+
agent_framework_community_postgres/_retention.py,sha256=pscxWfErYFFnZ1eqVTMSc_zWVTb_HR4NZ1GfLzOcVhQ,5917
|
|
14
|
+
agent_framework_community_postgres/_session_store.py,sha256=PcGu0VnIeZt7zDPwqh-qWtd0v0yeIOZdjmHB45KIMVE,5907
|
|
15
|
+
agent_framework_community_postgres/_store.py,sha256=WdegHovFy4VDxz0H4CZEWAAAxBl4hqoHliHJAdFg-mo,1985
|
|
16
|
+
agent_framework_community_postgres/_thread_snapshot_store.py,sha256=Ze9WAeEyE7k8j5Q8Tk3Z42v8MNh7gcvs1B5pUbDUQkE,7502
|
|
17
|
+
agent_framework_community_postgres/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
community_agent_framework_postgres-0.1.1.dist-info/METADATA,sha256=wN1Xp4DYDd6AueYLItpm_YvMPOvSNTtq6S5X8hwluBs,24439
|
|
19
|
+
community_agent_framework_postgres-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
20
|
+
community_agent_framework_postgres-0.1.1.dist-info/licenses/LICENSE,sha256=o29n5hirCtF4mmDf_uNd_dSLl0gaxsQiFwwRtRZnRJ0,1076
|
|
21
|
+
community_agent_framework_postgres-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matias Suxo Salinas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|