orchid-storage-postgres 1.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.
- orchid_storage_postgres/__init__.py +48 -0
- orchid_storage_postgres/chat_storage.py +249 -0
- orchid_storage_postgres/migrations/__init__.py +44 -0
- orchid_storage_postgres/migrations/v001_initial_schema.py +48 -0
- orchid_storage_postgres/visibility.py +35 -0
- orchid_storage_postgres-1.0.0.dist-info/METADATA +74 -0
- orchid_storage_postgres-1.0.0.dist-info/RECORD +10 -0
- orchid_storage_postgres-1.0.0.dist-info/WHEEL +4 -0
- orchid_storage_postgres-1.0.0.dist-info/entry_points.txt +5 -0
- orchid_storage_postgres-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""PostgreSQL storage plugin for the Orchid AI framework.
|
|
2
|
+
|
|
3
|
+
Provides ``OrchidPostgresChatStorage``, a PostgreSQL visibility
|
|
4
|
+
fragment, and a PostgreSQL checkpointer. Auto-registers via
|
|
5
|
+
``importlib.metadata`` entry points.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
|
|
14
|
+
from .chat_storage import OrchidPostgresChatStorage
|
|
15
|
+
from .visibility import _build_postgres_filter
|
|
16
|
+
|
|
17
|
+
__all__ = ["OrchidPostgresChatStorage"]
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _build_postgres_checkpointer(dsn: str):
|
|
23
|
+
"""Build an async PostgreSQL checkpointer from a DSN."""
|
|
24
|
+
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
|
25
|
+
|
|
26
|
+
checkpointer = AsyncPostgresSaver.from_conn_string(dsn)
|
|
27
|
+
await checkpointer.setup()
|
|
28
|
+
logger.info("[orchid-storage-postgres] Checkpointer ready")
|
|
29
|
+
return checkpointer
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _register() -> None:
|
|
33
|
+
"""Entry-point callable — registers the postgres visibility fragment and checkpointer."""
|
|
34
|
+
try:
|
|
35
|
+
from orchid_ai.events.visibility import register_visibility_fragment
|
|
36
|
+
|
|
37
|
+
register_visibility_fragment("postgres", _build_postgres_filter)
|
|
38
|
+
logger.debug("[orchid-storage-postgres] Registered visibility fragment")
|
|
39
|
+
except ImportError:
|
|
40
|
+
logger.debug("[orchid-storage-postgres] Skipping visibility fragment (not in this orchid-ai version)")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from orchid_ai.checkpointing.factory import register_checkpointer
|
|
44
|
+
|
|
45
|
+
register_checkpointer("postgres", _build_postgres_checkpointer)
|
|
46
|
+
logger.debug("[orchid-storage-postgres] Registered checkpointer")
|
|
47
|
+
except ImportError:
|
|
48
|
+
logger.debug("[orchid-storage-postgres] Skipping checkpointer (not in this orchid-ai version)")
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL chat storage — production-grade :class:`OrchidChatStorage` backend.
|
|
3
|
+
|
|
4
|
+
Backed by ``asyncpg`` with connection pooling. Implements every
|
|
5
|
+
:class:`OrchidChatStorage` method using ``$1..$N`` placeholders.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from orchid_ai.persistence.base import OrchidChatStorage
|
|
17
|
+
from orchid_ai.persistence.models import OrchidChatMessage, OrchidChatSession, utcnow
|
|
18
|
+
|
|
19
|
+
from .migrations import PostgresMigrationRunner
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OrchidPostgresChatStorage(OrchidChatStorage):
|
|
25
|
+
"""Async PostgreSQL storage for chat sessions and messages.
|
|
26
|
+
|
|
27
|
+
Constructor accepts the DSN via ``dsn`` and an optional
|
|
28
|
+
``extra_migrations_package`` (dotted import path) so integrators
|
|
29
|
+
can append their own migrations after the framework's.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, dsn: str, extra_migrations_package: str | None = None):
|
|
33
|
+
self._dsn = dsn
|
|
34
|
+
self._pool: Any = None
|
|
35
|
+
self._migrator = PostgresMigrationRunner(
|
|
36
|
+
extra_migrations_package=extra_migrations_package,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# ── Lifecycle ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
async def init_db(self) -> None:
|
|
42
|
+
import asyncpg
|
|
43
|
+
|
|
44
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=1, max_size=10)
|
|
45
|
+
async with self._pool.acquire() as conn:
|
|
46
|
+
await self._migrator.run_up(conn)
|
|
47
|
+
logger.info("[OrchidChatStorage:postgres] Initialised")
|
|
48
|
+
|
|
49
|
+
async def close(self) -> None:
|
|
50
|
+
if self._pool:
|
|
51
|
+
await self._pool.close()
|
|
52
|
+
self._pool = None
|
|
53
|
+
|
|
54
|
+
async def _conn(self):
|
|
55
|
+
if self._pool is None:
|
|
56
|
+
raise RuntimeError("OrchidPostgresChatStorage: init_db() not called")
|
|
57
|
+
return await self._pool.acquire()
|
|
58
|
+
|
|
59
|
+
# ── Sessions ─────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async def create_chat(
|
|
62
|
+
self,
|
|
63
|
+
tenant_id: str,
|
|
64
|
+
user_id: str,
|
|
65
|
+
title: str = "",
|
|
66
|
+
) -> OrchidChatSession:
|
|
67
|
+
now = utcnow()
|
|
68
|
+
chat = OrchidChatSession(
|
|
69
|
+
id=str(uuid.uuid4()),
|
|
70
|
+
tenant_id=tenant_id,
|
|
71
|
+
user_id=user_id,
|
|
72
|
+
title=title or "New chat",
|
|
73
|
+
created_at=now,
|
|
74
|
+
updated_at=now,
|
|
75
|
+
)
|
|
76
|
+
async with self._pool.acquire() as conn:
|
|
77
|
+
await conn.execute(
|
|
78
|
+
"INSERT INTO chat_sessions (id, tenant_id, user_id, title, created_at, updated_at) "
|
|
79
|
+
"VALUES ($1, $2, $3, $4, $5, $6)",
|
|
80
|
+
chat.id,
|
|
81
|
+
chat.tenant_id,
|
|
82
|
+
chat.user_id,
|
|
83
|
+
chat.title,
|
|
84
|
+
now,
|
|
85
|
+
now,
|
|
86
|
+
)
|
|
87
|
+
return chat
|
|
88
|
+
|
|
89
|
+
async def list_chats(
|
|
90
|
+
self,
|
|
91
|
+
tenant_id: str,
|
|
92
|
+
user_id: str,
|
|
93
|
+
) -> list[OrchidChatSession]:
|
|
94
|
+
async with self._pool.acquire() as conn:
|
|
95
|
+
rows = await conn.fetch(
|
|
96
|
+
"SELECT * FROM chat_sessions WHERE tenant_id = $1 AND user_id = $2 ORDER BY updated_at DESC",
|
|
97
|
+
tenant_id,
|
|
98
|
+
user_id,
|
|
99
|
+
)
|
|
100
|
+
return [_row_to_session(r) for r in rows]
|
|
101
|
+
|
|
102
|
+
async def get_chat(self, chat_id: str) -> OrchidChatSession | None:
|
|
103
|
+
async with self._pool.acquire() as conn:
|
|
104
|
+
row = await conn.fetchrow(
|
|
105
|
+
"SELECT * FROM chat_sessions WHERE id = $1",
|
|
106
|
+
chat_id,
|
|
107
|
+
)
|
|
108
|
+
return _row_to_session(row) if row else None
|
|
109
|
+
|
|
110
|
+
async def delete_chat(self, chat_id: str) -> None:
|
|
111
|
+
async with self._pool.acquire() as conn:
|
|
112
|
+
await conn.execute("DELETE FROM chat_sessions WHERE id = $1", chat_id)
|
|
113
|
+
|
|
114
|
+
async def update_title(self, chat_id: str, title: str) -> None:
|
|
115
|
+
async with self._pool.acquire() as conn:
|
|
116
|
+
await conn.execute(
|
|
117
|
+
"UPDATE chat_sessions SET title = $1, updated_at = $2 WHERE id = $3",
|
|
118
|
+
title,
|
|
119
|
+
utcnow(),
|
|
120
|
+
chat_id,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def mark_shared(self, chat_id: str) -> None:
|
|
124
|
+
async with self._pool.acquire() as conn:
|
|
125
|
+
await conn.execute(
|
|
126
|
+
"UPDATE chat_sessions SET is_shared = TRUE, updated_at = $1 WHERE id = $2",
|
|
127
|
+
utcnow(),
|
|
128
|
+
chat_id,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ── Messages ─────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async def add_message(
|
|
134
|
+
self,
|
|
135
|
+
chat_id: str,
|
|
136
|
+
role: str,
|
|
137
|
+
content: str,
|
|
138
|
+
agents_used: list[str] | None = None,
|
|
139
|
+
metadata: dict | None = None,
|
|
140
|
+
) -> OrchidChatMessage:
|
|
141
|
+
now = utcnow()
|
|
142
|
+
msg = OrchidChatMessage(
|
|
143
|
+
id=str(uuid.uuid4()),
|
|
144
|
+
chat_id=chat_id,
|
|
145
|
+
role=role,
|
|
146
|
+
content=content,
|
|
147
|
+
agents_used=agents_used or [],
|
|
148
|
+
created_at=now,
|
|
149
|
+
metadata=metadata or {},
|
|
150
|
+
)
|
|
151
|
+
async with self._pool.acquire() as conn:
|
|
152
|
+
await conn.execute(
|
|
153
|
+
"INSERT INTO chat_messages (id, chat_id, role, content, agents_used, created_at, metadata) "
|
|
154
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
|
155
|
+
msg.id,
|
|
156
|
+
msg.chat_id,
|
|
157
|
+
msg.role,
|
|
158
|
+
msg.content,
|
|
159
|
+
json.dumps(msg.agents_used),
|
|
160
|
+
now,
|
|
161
|
+
json.dumps(msg.metadata),
|
|
162
|
+
)
|
|
163
|
+
await conn.execute(
|
|
164
|
+
"UPDATE chat_sessions SET updated_at = $1 WHERE id = $2",
|
|
165
|
+
now,
|
|
166
|
+
chat_id,
|
|
167
|
+
)
|
|
168
|
+
return msg
|
|
169
|
+
|
|
170
|
+
async def get_messages(
|
|
171
|
+
self,
|
|
172
|
+
chat_id: str,
|
|
173
|
+
limit: int = 50,
|
|
174
|
+
offset: int = 0,
|
|
175
|
+
) -> list[OrchidChatMessage]:
|
|
176
|
+
async with self._pool.acquire() as conn:
|
|
177
|
+
rows = await conn.fetch(
|
|
178
|
+
"SELECT * FROM chat_messages WHERE chat_id = $1 ORDER BY created_at ASC LIMIT $2 OFFSET $3",
|
|
179
|
+
chat_id,
|
|
180
|
+
limit,
|
|
181
|
+
offset,
|
|
182
|
+
)
|
|
183
|
+
return [_row_to_message(r) for r in rows]
|
|
184
|
+
|
|
185
|
+
# ── Conversation summaries ───────────────────────────────
|
|
186
|
+
|
|
187
|
+
async def get_conversation_summary(self, chat_id: str) -> str | None:
|
|
188
|
+
async with self._pool.acquire() as conn:
|
|
189
|
+
row = await conn.fetchrow(
|
|
190
|
+
"SELECT summary_text FROM conversation_summaries WHERE chat_id = $1",
|
|
191
|
+
chat_id,
|
|
192
|
+
)
|
|
193
|
+
return row["summary_text"] if row else None
|
|
194
|
+
|
|
195
|
+
async def save_conversation_summary(self, chat_id: str, summary: str, turn_number: int) -> None:
|
|
196
|
+
async with self._pool.acquire() as conn:
|
|
197
|
+
await conn.execute(
|
|
198
|
+
"INSERT INTO conversation_summaries (chat_id, summary_text, turn_number, updated_at) "
|
|
199
|
+
"VALUES ($1, $2, $3, $4) "
|
|
200
|
+
"ON CONFLICT (chat_id) DO UPDATE SET summary_text = $2, turn_number = $3, updated_at = $4",
|
|
201
|
+
chat_id,
|
|
202
|
+
summary,
|
|
203
|
+
turn_number,
|
|
204
|
+
utcnow(),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── Row mappers ──────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _parse_dt(val: Any) -> datetime:
|
|
212
|
+
if isinstance(val, datetime):
|
|
213
|
+
return val
|
|
214
|
+
if isinstance(val, str):
|
|
215
|
+
try:
|
|
216
|
+
return datetime.fromisoformat(val)
|
|
217
|
+
except (ValueError, TypeError):
|
|
218
|
+
return utcnow()
|
|
219
|
+
return utcnow()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _row_to_session(row: Any) -> OrchidChatSession:
|
|
223
|
+
return OrchidChatSession(
|
|
224
|
+
id=row["id"],
|
|
225
|
+
tenant_id=row["tenant_id"],
|
|
226
|
+
user_id=row["user_id"],
|
|
227
|
+
title=row["title"],
|
|
228
|
+
created_at=_parse_dt(row["created_at"]),
|
|
229
|
+
updated_at=_parse_dt(row["updated_at"]),
|
|
230
|
+
is_shared=bool(row["is_shared"]),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _row_to_message(row: Any) -> OrchidChatMessage:
|
|
235
|
+
agents_used = row["agents_used"]
|
|
236
|
+
if isinstance(agents_used, str):
|
|
237
|
+
agents_used = json.loads(agents_used)
|
|
238
|
+
meta = row["metadata"]
|
|
239
|
+
if isinstance(meta, str):
|
|
240
|
+
meta = json.loads(meta)
|
|
241
|
+
return OrchidChatMessage(
|
|
242
|
+
id=row["id"],
|
|
243
|
+
chat_id=row["chat_id"],
|
|
244
|
+
role=row["role"],
|
|
245
|
+
content=row["content"],
|
|
246
|
+
agents_used=agents_used or [],
|
|
247
|
+
created_at=_parse_dt(row["created_at"]),
|
|
248
|
+
metadata=meta or {},
|
|
249
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL migration runner.
|
|
3
|
+
|
|
4
|
+
The runner discovers migrations in ``orchid_storage_postgres.migrations``
|
|
5
|
+
and tracks applied versions in a ``_migrations`` table.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from orchid_ai.persistence.migrations.runner import OrchidMigrationRunner
|
|
13
|
+
|
|
14
|
+
MIGRATIONS_PACKAGE = "orchid_storage_postgres.migrations"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PostgresMigrationRunner(OrchidMigrationRunner):
|
|
18
|
+
"""PostgreSQL-specific migration tracking."""
|
|
19
|
+
|
|
20
|
+
dialect = "postgres"
|
|
21
|
+
migrations_package = MIGRATIONS_PACKAGE
|
|
22
|
+
|
|
23
|
+
async def ensure_migrations_table(self, conn: Any) -> None:
|
|
24
|
+
await conn.execute("""
|
|
25
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
26
|
+
version TEXT PRIMARY KEY,
|
|
27
|
+
description TEXT NOT NULL DEFAULT '',
|
|
28
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
29
|
+
)
|
|
30
|
+
""")
|
|
31
|
+
|
|
32
|
+
async def get_applied_versions(self, conn: Any) -> set[str]:
|
|
33
|
+
rows = await conn.fetch("SELECT version FROM _migrations")
|
|
34
|
+
return {r["version"] for r in rows}
|
|
35
|
+
|
|
36
|
+
async def record_version(self, conn: Any, version: str, description: str) -> None:
|
|
37
|
+
await conn.execute(
|
|
38
|
+
"INSERT INTO _migrations (version, description) VALUES ($1, $2)",
|
|
39
|
+
version,
|
|
40
|
+
description,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def remove_version(self, conn: Any, version: str) -> None:
|
|
44
|
+
await conn.execute("DELETE FROM _migrations WHERE version = $1", version)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration v001 — PostgreSQL initial schema.
|
|
3
|
+
|
|
4
|
+
Creates every framework-owned table in a single pass.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from orchid_ai.persistence.migrations._schema_ddl import PG_UP
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
VERSION = "001"
|
|
16
|
+
DESCRIPTION = "PostgreSQL initial schema (chat, MCP outbound, MCP inbound gateway, events)"
|
|
17
|
+
|
|
18
|
+
_PG_DOWN = [
|
|
19
|
+
"DROP TABLE IF EXISTS signal_sources CASCADE",
|
|
20
|
+
"DROP TABLE IF EXISTS job_runs CASCADE",
|
|
21
|
+
"DROP TABLE IF EXISTS schedules CASCADE",
|
|
22
|
+
"DROP TABLE IF EXISTS triggers CASCADE",
|
|
23
|
+
"DROP TABLE IF EXISTS signal_queue_dead_letter CASCADE",
|
|
24
|
+
"DROP TABLE IF EXISTS signal_queue CASCADE",
|
|
25
|
+
"DROP TABLE IF EXISTS signals CASCADE",
|
|
26
|
+
"DROP TABLE IF EXISTS mcp_gateway_tokens CASCADE",
|
|
27
|
+
"DROP TABLE IF EXISTS mcp_gateway_auth_codes CASCADE",
|
|
28
|
+
"DROP TABLE IF EXISTS mcp_gateway_clients CASCADE",
|
|
29
|
+
"DROP TABLE IF EXISTS mcp_client_registrations CASCADE",
|
|
30
|
+
"DROP TABLE IF EXISTS mcp_oauth_tokens CASCADE",
|
|
31
|
+
"DROP TABLE IF EXISTS agent_configs CASCADE",
|
|
32
|
+
"DROP TABLE IF EXISTS conversation_summaries CASCADE",
|
|
33
|
+
"DROP TABLE IF EXISTS chat_messages CASCADE",
|
|
34
|
+
"DROP TABLE IF EXISTS chat_sessions CASCADE",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def up(conn, *, dialect: str = "postgres") -> None:
|
|
39
|
+
"""Apply the PostgreSQL initial schema."""
|
|
40
|
+
for sql in PG_UP:
|
|
41
|
+
await conn.execute(sql)
|
|
42
|
+
logger.info("[orchid-storage-postgres] Migration v001 applied (%d statements)", len(PG_UP))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def down(conn, *, dialect: str = "postgres") -> None:
|
|
46
|
+
"""Roll back the PostgreSQL initial schema."""
|
|
47
|
+
for sql in _PG_DOWN:
|
|
48
|
+
await conn.execute(sql)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL run-visibility filter fragment.
|
|
3
|
+
|
|
4
|
+
Registers a ``build_run_filter_clause`` implementation for the ``postgres``
|
|
5
|
+
dialect that uses ``$1..$N`` positional parameters (asyncpg convention).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from orchid_ai.events.visibility import _Filter # noqa: PLC2701
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_postgres_filter(auth: Any) -> _Filter:
|
|
16
|
+
"""Return a ``WHERE`` fragment + positional bind params for PostgreSQL."""
|
|
17
|
+
tenant_key = getattr(auth, "tenant_key", "default")
|
|
18
|
+
user_id = getattr(auth, "user_id", "")
|
|
19
|
+
roles = getattr(auth, "roles", frozenset())
|
|
20
|
+
|
|
21
|
+
if "admin" in roles:
|
|
22
|
+
return _Filter(
|
|
23
|
+
where="tenant_key = $1",
|
|
24
|
+
params={"tenant_key": tenant_key},
|
|
25
|
+
)
|
|
26
|
+
return _Filter(
|
|
27
|
+
where=(
|
|
28
|
+
"tenant_key = $1 AND ("
|
|
29
|
+
"visibility = 'tenant' "
|
|
30
|
+
"OR (visibility IN ('actor', 'addressed') "
|
|
31
|
+
" AND visibility_user_id = $2)"
|
|
32
|
+
")"
|
|
33
|
+
),
|
|
34
|
+
params={"tenant_key": tenant_key, "user_id": user_id},
|
|
35
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orchid-storage-postgres
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: PostgreSQL storage backend plugin for the Orchid AI framework
|
|
5
|
+
Project-URL: Homepage, https://github.com/gadz82/orchid-storage-postgres
|
|
6
|
+
Project-URL: Issues, https://github.com/gadz82/orchid-storage-postgres/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,ai,asyncpg,postgresql,storage
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
19
|
+
Requires-Dist: langgraph-checkpoint-postgres>=2.0.0
|
|
20
|
+
Requires-Dist: orchid-ai>=1.8.2
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# orchid-storage-postgres
|
|
29
|
+
|
|
30
|
+
PostgreSQL storage backend plugin for the [Orchid AI](https://github.com/gadz82/orchid) framework.
|
|
31
|
+
|
|
32
|
+
## What it provides
|
|
33
|
+
|
|
34
|
+
- `OrchidPostgresChatStorage` — implements `OrchidChatStorage` backed by PostgreSQL (asyncpg)
|
|
35
|
+
- PostgreSQL visibility fragment for `build_run_filter_clause`
|
|
36
|
+
- PostgreSQL schema migration (v001)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install orchid-storage-postgres
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Reference in your `orchid.yml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
storage:
|
|
50
|
+
class: orchid_storage_postgres.chat_storage.OrchidPostgresChatStorage
|
|
51
|
+
dsn: postgresql://user:pass@localhost:5432/orchid
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or build it programmatically:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from orchid_storage_postgres import OrchidPostgresChatStorage
|
|
58
|
+
|
|
59
|
+
storage = OrchidPostgresChatStorage(dsn="postgresql://user:pass@localhost:5432/orchid")
|
|
60
|
+
await storage.init_db()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd orchid-storage-postgres
|
|
67
|
+
pip install -e ".[dev]"
|
|
68
|
+
pytest tests/ -x
|
|
69
|
+
ruff check orchid_storage_postgres/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
orchid_storage_postgres/__init__.py,sha256=PA1dsUPRdVPqiGOuyTX9H_hbaKgLEQUF7qZyKmXdxr8,1699
|
|
2
|
+
orchid_storage_postgres/chat_storage.py,sha256=egOTQIP8EKn57WgNVqxi-DBYkDC9hy62NrvZzAuV5T4,8600
|
|
3
|
+
orchid_storage_postgres/visibility.py,sha256=pUAXFOQD7U6yvB39aiITj8CfVzSx-zxcZZQfzKx1pys,1057
|
|
4
|
+
orchid_storage_postgres/migrations/__init__.py,sha256=vApfq2cIgx41bw7O7wK7hO5ZsH4p6lad2PAc9IAe-0U,1451
|
|
5
|
+
orchid_storage_postgres/migrations/v001_initial_schema.py,sha256=P2g18PASFTtmSjcJcHRc6r2NaGF2cxB4Nop-RVWbxmg,1649
|
|
6
|
+
orchid_storage_postgres-1.0.0.dist-info/METADATA,sha256=IcnFr17TZKEu6cULXgLORBqFHo0spEWL4gndKgIl29Q,2090
|
|
7
|
+
orchid_storage_postgres-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
orchid_storage_postgres-1.0.0.dist-info/entry_points.txt,sha256=bIUMwNTiu6N0pbG3oKQsKOukdyLpYPrQQx2re3ChlLM,144
|
|
9
|
+
orchid_storage_postgres-1.0.0.dist-info/licenses/LICENSE,sha256=1xiBMms-kdU_t3A21unskfGGda1M2uemoNTaRGgtV_s,1077
|
|
10
|
+
orchid_storage_postgres-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francesco Marchesini
|
|
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.
|