astrocyte-postgres 0.13.1__tar.gz → 0.14.2__tar.gz
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.
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/.gitignore +2 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/PKG-INFO +14 -4
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/README.md +13 -3
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/astrocyte_postgres/__init__.py +6 -3
- astrocyte_postgres-0.14.2/astrocyte_postgres/conversation_store.py +269 -0
- astrocyte_postgres-0.14.2/astrocyte_postgres/document_store.py +368 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/astrocyte_postgres/mental_model_store.py +20 -4
- astrocyte_postgres-0.14.2/astrocyte_postgres/pageindex_store.py +1554 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/astrocyte_postgres/source_store.py +3 -5
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/astrocyte_postgres/store.py +74 -28
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/astrocyte_postgres/wiki_store.py +1 -0
- astrocyte_postgres-0.14.2/migrations/001_extension.sql +14 -0
- astrocyte_postgres-0.14.2/migrations/003_indexes.sql +31 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/009_entities_trigram_embedding.sql +10 -5
- astrocyte_postgres-0.14.2/migrations/015_tier2_recall.sql +156 -0
- astrocyte_postgres-0.14.2/migrations/016_drop_age.sql +76 -0
- astrocyte_postgres-0.14.2/migrations/017_pi_section_embedding.sql +28 -0
- astrocyte_postgres-0.14.2/migrations/018_wiki_page_embedding.sql +23 -0
- astrocyte_postgres-0.14.2/migrations/019_pi_section_event_date.sql +24 -0
- astrocyte_postgres-0.14.2/migrations/020_pi_facts.sql +54 -0
- astrocyte_postgres-0.14.2/migrations/021_pi_fact_assistant_statement.sql +25 -0
- astrocyte_postgres-0.14.2/migrations/022_mental_model_kind.sql +30 -0
- astrocyte_postgres-0.14.2/migrations/023_astrocyte_audit_log.sql +38 -0
- astrocyte_postgres-0.14.2/migrations/024_astrocyte_bank_disposition.sql +39 -0
- astrocyte_postgres-0.14.2/migrations/025_astrocyte_documents.sql +32 -0
- astrocyte_postgres-0.14.2/migrations/026_astrocyte_document_nodes.sql +49 -0
- astrocyte_postgres-0.14.2/migrations/027_astrocyte_conversations.sql +26 -0
- astrocyte_postgres-0.14.2/migrations/028_astrocyte_conversation_turns.sql +36 -0
- astrocyte_postgres-0.14.2/migrations/029_pi_facts_nullable_document_id.sql +41 -0
- astrocyte_postgres-0.14.2/migrations/030_document_node_page_range.sql +13 -0
- astrocyte_postgres-0.14.2/migrations/031_astrocyte_conversation_documents.sql +38 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/pyproject.toml +3 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/scripts/migrate.sh +18 -1
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_bm25_idf.py +37 -27
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_fts.py +56 -36
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_mental_model_store.py +4 -2
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_mental_model_tenant_isolation.py +2 -6
- astrocyte_postgres-0.14.2/tests/test_postgres_pageindex_store.py +277 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_source_aware_retain.py +36 -37
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_source_store.py +28 -22
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_source_tenant_isolation.py +13 -14
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_store.py +68 -48
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/uv.lock +38 -6
- astrocyte_postgres-0.13.1/migrations/001_extension.sql +0 -2
- astrocyte_postgres-0.13.1/migrations/003_indexes.sql +0 -11
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/002_astrocytes_vectors.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/004_memory_layer.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/005_banks_access.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/006_lifecycle_indexes.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/007_wiki_tables.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/008_entities_temporal.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/010_hybrid_recall_indexes.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/011_text_fts.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/012_mental_models.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/013_bm25_materialized_view.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/migrations/014_source_documents.sql +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/scripts/migrate-all-tenants.sh +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/scripts/migrate-wiki-piggyback-to-mental-models.sh +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/scripts/test-integration.sh +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/__init__.py +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/conftest.py +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_tenant_isolation.py +0 -0
- {astrocyte_postgres-0.13.1 → astrocyte_postgres-0.14.2}/tests/test_postgres_wiki_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astrocyte-postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.2
|
|
4
4
|
Summary: PostgreSQL adapter for Astrocyte (vector + document + wiki stores backed by pgvector and tsvector)
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
# astrocyte-postgres
|
|
18
18
|
|
|
19
|
-
**PostgreSQL
|
|
19
|
+
**PostgreSQL** implementation of the Astrocyte **`VectorStore`** and **`WikiStore`** SPIs ([`provider-spi.md`](../../docs/_plugins/provider-spi.md)). Uses **[pgvectorscale](https://github.com/timescale/pgvectorscale)** (DiskANN) for ANN indexing — pgvectorscale CASCADE-installs pgvector, so pgvector's `vector` type and `<=>` operator are available too. The shipped Docker image bakes pgvectorscale in; for self-host, use that image, Supabase, Neon, or Timescale Cloud.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -72,6 +72,16 @@ After migrations are applied, set **`bootstrap_schema: false`** in `vector_store
|
|
|
72
72
|
|
|
73
73
|
**Embedding width:** [`migrations/002_astrocyte_vectors.sql`](migrations/002_astrocyte_vectors.sql) creates `vector(${ASTROCYTE_EMBEDDING_DIMENSIONS:-128})`. That must match **`embedding_dimensions`** in config. For OpenAI `text-embedding-3-small`, run migrations with `ASTROCYTE_EMBEDDING_DIMENSIONS=1536`.
|
|
74
74
|
|
|
75
|
+
**ANN backend (DiskANN via pgvectorscale):** vector indexes are created with `USING diskann (embedding vector_cosine_ops) WITH (num_neighbors = 50)`. pgvectorscale CASCADE-installs pgvector, so application code using pgvector's `<=>` operator works unchanged. Astrocyte standardized on pgvectorscale in 2026-05 — the LongMemEval bench surfaced HNSW per-page write-lock drift (~1.0s → 2.0s/session as the index grew under concurrent retain), and DiskANN's graph layout serializes less aggressively. pgvectorscale is OSS under the PostgreSQL License with no feature gates.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
./scripts/migrate.sh
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Supported deployment targets:** the shipped `ghcr.io/astrocyteai/astrocyte-postgres` image (bakes pgvectorscale in), Supabase, Neon, Timescale Cloud, or any Postgres with `postgresql-16-pgvectorscale` from Timescale's apt repo. AWS RDS, GCP Cloud SQL, and Azure Database for PostgreSQL do **not** ship pgvectorscale and are not supported targets — run the shipped image on ECS / GCE / a VM instead.
|
|
82
|
+
|
|
83
|
+
The DiskANN index DDL is in [`migrations/003_indexes.sql`](migrations/003_indexes.sql) and [`migrations/009_entities_trigram_embedding.sql`](migrations/009_entities_trigram_embedding.sql); the `CREATE EXTENSION vectorscale CASCADE` lives in [`migrations/001_extension.sql`](migrations/001_extension.sql).
|
|
84
|
+
|
|
75
85
|
**Custom `table_name`:** The shipped SQL targets **`astrocyte_vectors`**. If you use another table name, copy and adjust the migration files accordingly.
|
|
76
86
|
|
|
77
87
|
The later migrations add the Hindsight-comparable Postgres substrate around vectors: bank metadata and access grants, lifecycle columns (`retained_at`, `forgotten_at`), durable wiki pages/revisions/provenance, canonical entity/link tables, and normalized temporal facts.
|
|
@@ -83,7 +93,7 @@ The later migrations add the Hindsight-comparable Postgres substrate around vect
|
|
|
83
93
|
| `dsn` | PostgreSQL connection URI (or set `DATABASE_URL` / `ASTROCYTE_PG_DSN`) |
|
|
84
94
|
| `table_name` | Table name (default `astrocyte_vectors`; alphanumeric + underscore only) |
|
|
85
95
|
| `embedding_dimensions` | Fixed `vector(N)` width; must match your embedding model and the **`vector(N)`** in SQL migrations (default **128**) |
|
|
86
|
-
| `bootstrap_schema` | If **`true`** (default), create extension / table / btree index on first use (dev-friendly; no
|
|
96
|
+
| `bootstrap_schema` | If **`true`** (default), create extension / table / btree index on first use (dev-friendly; no DiskANN). If **`false`**, assume **`migrate.sh`** already applied [`migrations/`](migrations/) (production). |
|
|
87
97
|
|
|
88
98
|
## How this fits `astrocyte_gateway`
|
|
89
99
|
|
|
@@ -128,6 +138,6 @@ cd astrocyte-services-py/astrocyte-gateway-py && uv sync --extra postgres
|
|
|
128
138
|
|
|
129
139
|
## Production notes
|
|
130
140
|
|
|
131
|
-
- **
|
|
141
|
+
- **DiskANN** parameters (`num_neighbors`) live in [`migrations/003_indexes.sql`](migrations/003_indexes.sql); tune with DBA guidance as load grows.
|
|
132
142
|
- **Embedding dimension** must match the **`LLMProvider.embed()`** output used by the pipeline.
|
|
133
143
|
- Use **secrets** for `dsn`, not committed YAML.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# astrocyte-postgres
|
|
2
2
|
|
|
3
|
-
**PostgreSQL
|
|
3
|
+
**PostgreSQL** implementation of the Astrocyte **`VectorStore`** and **`WikiStore`** SPIs ([`provider-spi.md`](../../docs/_plugins/provider-spi.md)). Uses **[pgvectorscale](https://github.com/timescale/pgvectorscale)** (DiskANN) for ANN indexing — pgvectorscale CASCADE-installs pgvector, so pgvector's `vector` type and `<=>` operator are available too. The shipped Docker image bakes pgvectorscale in; for self-host, use that image, Supabase, Neon, or Timescale Cloud.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -56,6 +56,16 @@ After migrations are applied, set **`bootstrap_schema: false`** in `vector_store
|
|
|
56
56
|
|
|
57
57
|
**Embedding width:** [`migrations/002_astrocyte_vectors.sql`](migrations/002_astrocyte_vectors.sql) creates `vector(${ASTROCYTE_EMBEDDING_DIMENSIONS:-128})`. That must match **`embedding_dimensions`** in config. For OpenAI `text-embedding-3-small`, run migrations with `ASTROCYTE_EMBEDDING_DIMENSIONS=1536`.
|
|
58
58
|
|
|
59
|
+
**ANN backend (DiskANN via pgvectorscale):** vector indexes are created with `USING diskann (embedding vector_cosine_ops) WITH (num_neighbors = 50)`. pgvectorscale CASCADE-installs pgvector, so application code using pgvector's `<=>` operator works unchanged. Astrocyte standardized on pgvectorscale in 2026-05 — the LongMemEval bench surfaced HNSW per-page write-lock drift (~1.0s → 2.0s/session as the index grew under concurrent retain), and DiskANN's graph layout serializes less aggressively. pgvectorscale is OSS under the PostgreSQL License with no feature gates.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
./scripts/migrate.sh
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Supported deployment targets:** the shipped `ghcr.io/astrocyteai/astrocyte-postgres` image (bakes pgvectorscale in), Supabase, Neon, Timescale Cloud, or any Postgres with `postgresql-16-pgvectorscale` from Timescale's apt repo. AWS RDS, GCP Cloud SQL, and Azure Database for PostgreSQL do **not** ship pgvectorscale and are not supported targets — run the shipped image on ECS / GCE / a VM instead.
|
|
66
|
+
|
|
67
|
+
The DiskANN index DDL is in [`migrations/003_indexes.sql`](migrations/003_indexes.sql) and [`migrations/009_entities_trigram_embedding.sql`](migrations/009_entities_trigram_embedding.sql); the `CREATE EXTENSION vectorscale CASCADE` lives in [`migrations/001_extension.sql`](migrations/001_extension.sql).
|
|
68
|
+
|
|
59
69
|
**Custom `table_name`:** The shipped SQL targets **`astrocyte_vectors`**. If you use another table name, copy and adjust the migration files accordingly.
|
|
60
70
|
|
|
61
71
|
The later migrations add the Hindsight-comparable Postgres substrate around vectors: bank metadata and access grants, lifecycle columns (`retained_at`, `forgotten_at`), durable wiki pages/revisions/provenance, canonical entity/link tables, and normalized temporal facts.
|
|
@@ -67,7 +77,7 @@ The later migrations add the Hindsight-comparable Postgres substrate around vect
|
|
|
67
77
|
| `dsn` | PostgreSQL connection URI (or set `DATABASE_URL` / `ASTROCYTE_PG_DSN`) |
|
|
68
78
|
| `table_name` | Table name (default `astrocyte_vectors`; alphanumeric + underscore only) |
|
|
69
79
|
| `embedding_dimensions` | Fixed `vector(N)` width; must match your embedding model and the **`vector(N)`** in SQL migrations (default **128**) |
|
|
70
|
-
| `bootstrap_schema` | If **`true`** (default), create extension / table / btree index on first use (dev-friendly; no
|
|
80
|
+
| `bootstrap_schema` | If **`true`** (default), create extension / table / btree index on first use (dev-friendly; no DiskANN). If **`false`**, assume **`migrate.sh`** already applied [`migrations/`](migrations/) (production). |
|
|
71
81
|
|
|
72
82
|
## How this fits `astrocyte_gateway`
|
|
73
83
|
|
|
@@ -112,6 +122,6 @@ cd astrocyte-services-py/astrocyte-gateway-py && uv sync --extra postgres
|
|
|
112
122
|
|
|
113
123
|
## Production notes
|
|
114
124
|
|
|
115
|
-
- **
|
|
125
|
+
- **DiskANN** parameters (`num_neighbors`) live in [`migrations/003_indexes.sql`](migrations/003_indexes.sql); tune with DBA guidance as load grows.
|
|
116
126
|
- **Embedding dimension** must match the **`LLMProvider.embed()`** output used by the pipeline.
|
|
117
127
|
- Use **secrets** for `dsn`, not committed YAML.
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
"""PostgreSQL adapter for Astrocyte
|
|
1
|
+
"""PostgreSQL adapter for Astrocyte storage and section recall.
|
|
2
2
|
|
|
3
3
|
Backed by pgvector (HNSW vector index) and tsvector (BM25 keyword search).
|
|
4
|
-
Provides VectorStore, DocumentStore, WikiStore, MentalModelStore,
|
|
5
|
-
SourceStore implementations against a single
|
|
4
|
+
Provides VectorStore, DocumentStore, WikiStore, MentalModelStore,
|
|
5
|
+
SourceStore, and (M9) PageIndexStore implementations against a single
|
|
6
|
+
Postgres database.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from astrocyte_postgres.mental_model_store import PostgresMentalModelStore
|
|
10
|
+
from astrocyte_postgres.pageindex_store import PostgresPageIndexStore
|
|
9
11
|
from astrocyte_postgres.source_store import PostgresSourceStore
|
|
10
12
|
from astrocyte_postgres.store import PostgresStore
|
|
11
13
|
from astrocyte_postgres.wiki_store import PostgresWikiStore
|
|
12
14
|
|
|
13
15
|
__all__ = [
|
|
14
16
|
"PostgresMentalModelStore",
|
|
17
|
+
"PostgresPageIndexStore",
|
|
15
18
|
"PostgresSourceStore",
|
|
16
19
|
"PostgresStore",
|
|
17
20
|
"PostgresWikiStore",
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""PostgresConversationStore — Postgres impl of ConversationStore.
|
|
2
|
+
|
|
3
|
+
Mirrors PostgresDocumentStore's pattern: psycopg async pool, optional
|
|
4
|
+
schema bootstrap, narrow upsert / read / delete surface. Schema lives
|
|
5
|
+
in migrations 027 (conversations) + 028 (conversation_turns).
|
|
6
|
+
|
|
7
|
+
Composability invariant: this store does NOT reference any Memory
|
|
8
|
+
Engine table. Conversation→Memory references go through opaque
|
|
9
|
+
metadata strings, not typed FKs.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import psycopg
|
|
21
|
+
from astrocyte.conversations.storage import ConversationStore
|
|
22
|
+
from astrocyte.conversations.types import Conversation, ConversationTurn
|
|
23
|
+
from psycopg_pool import AsyncConnectionPool
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PostgresConversationStore(ConversationStore):
|
|
29
|
+
"""Durable ConversationStore on Postgres (migrations 027 + 028)."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
dsn: str | None = None,
|
|
34
|
+
*,
|
|
35
|
+
bootstrap_schema: bool = True,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._dsn = dsn or os.environ.get("DATABASE_URL") or os.environ.get("ASTROCYTE_PG_DSN")
|
|
39
|
+
if not self._dsn:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"PostgresConversationStore requires `dsn` or DATABASE_URL / ASTROCYTE_PG_DSN",
|
|
42
|
+
)
|
|
43
|
+
self._bootstrap_schema = bool(bootstrap_schema)
|
|
44
|
+
self._pool: AsyncConnectionPool | None = None
|
|
45
|
+
self._pool_lock = asyncio.Lock()
|
|
46
|
+
self._schema_ready = False
|
|
47
|
+
self._schema_lock = asyncio.Lock()
|
|
48
|
+
|
|
49
|
+
# ── lifecycle ─────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async def _ensure_pool(self) -> AsyncConnectionPool:
|
|
52
|
+
async with self._pool_lock:
|
|
53
|
+
if self._pool is None:
|
|
54
|
+
|
|
55
|
+
async def configure(conn: psycopg.AsyncConnection) -> None:
|
|
56
|
+
await conn.execute('SET search_path = public, "$user"')
|
|
57
|
+
await conn.commit()
|
|
58
|
+
|
|
59
|
+
self._pool = AsyncConnectionPool(
|
|
60
|
+
conninfo=self._dsn,
|
|
61
|
+
configure=configure,
|
|
62
|
+
open=False,
|
|
63
|
+
min_size=1,
|
|
64
|
+
max_size=10,
|
|
65
|
+
kwargs={"connect_timeout": 10},
|
|
66
|
+
)
|
|
67
|
+
await self._pool.open()
|
|
68
|
+
return self._pool
|
|
69
|
+
|
|
70
|
+
async def close(self) -> None:
|
|
71
|
+
async with self._pool_lock:
|
|
72
|
+
if self._pool is not None:
|
|
73
|
+
await self._pool.close()
|
|
74
|
+
self._pool = None
|
|
75
|
+
|
|
76
|
+
async def _ensure_schema(self, pool: AsyncConnectionPool) -> None:
|
|
77
|
+
if not self._bootstrap_schema or self._schema_ready:
|
|
78
|
+
return
|
|
79
|
+
async with self._schema_lock:
|
|
80
|
+
if self._schema_ready:
|
|
81
|
+
return
|
|
82
|
+
async with pool.connection() as conn:
|
|
83
|
+
async with conn.cursor() as cur:
|
|
84
|
+
await cur.execute(_DDL_CONVERSATIONS)
|
|
85
|
+
await cur.execute(_DDL_CONVERSATION_TURNS)
|
|
86
|
+
await cur.execute(_INDEXES)
|
|
87
|
+
await conn.commit()
|
|
88
|
+
self._schema_ready = True
|
|
89
|
+
|
|
90
|
+
# ── save ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
async def save_conversation(self, conversation: Conversation) -> None:
|
|
93
|
+
pool = await self._ensure_pool()
|
|
94
|
+
await self._ensure_schema(pool)
|
|
95
|
+
async with pool.connection() as conn:
|
|
96
|
+
async with conn.cursor() as cur:
|
|
97
|
+
# Upsert the conversation row
|
|
98
|
+
await cur.execute(
|
|
99
|
+
"""
|
|
100
|
+
INSERT INTO astrocyte_conversations
|
|
101
|
+
(id, source_uri, title, metadata, created_at)
|
|
102
|
+
VALUES (%s, %s, %s, %s::jsonb, %s)
|
|
103
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
104
|
+
source_uri = EXCLUDED.source_uri,
|
|
105
|
+
title = EXCLUDED.title,
|
|
106
|
+
metadata = EXCLUDED.metadata
|
|
107
|
+
""",
|
|
108
|
+
(
|
|
109
|
+
conversation.id,
|
|
110
|
+
conversation.source_uri,
|
|
111
|
+
conversation.title,
|
|
112
|
+
json.dumps(conversation.metadata),
|
|
113
|
+
conversation.created_at,
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
# Replace any prior turns
|
|
117
|
+
await cur.execute(
|
|
118
|
+
"DELETE FROM astrocyte_conversation_turns WHERE conversation_id = %s",
|
|
119
|
+
(conversation.id,),
|
|
120
|
+
)
|
|
121
|
+
if conversation.turns:
|
|
122
|
+
rows = [
|
|
123
|
+
(
|
|
124
|
+
t.id,
|
|
125
|
+
conversation.id,
|
|
126
|
+
i,
|
|
127
|
+
t.role,
|
|
128
|
+
t.content,
|
|
129
|
+
t.timestamp,
|
|
130
|
+
json.dumps(t.metadata),
|
|
131
|
+
)
|
|
132
|
+
for i, t in enumerate(conversation.turns)
|
|
133
|
+
]
|
|
134
|
+
await cur.executemany(
|
|
135
|
+
"""
|
|
136
|
+
INSERT INTO astrocyte_conversation_turns
|
|
137
|
+
(id, conversation_id, turn_index, role, content, "timestamp", metadata)
|
|
138
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb)
|
|
139
|
+
""",
|
|
140
|
+
rows,
|
|
141
|
+
)
|
|
142
|
+
await conn.commit()
|
|
143
|
+
|
|
144
|
+
# ── reads ─────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
async def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
147
|
+
pool = await self._ensure_pool()
|
|
148
|
+
await self._ensure_schema(pool)
|
|
149
|
+
async with pool.connection() as conn:
|
|
150
|
+
async with conn.cursor() as cur:
|
|
151
|
+
await cur.execute(
|
|
152
|
+
"""
|
|
153
|
+
SELECT id, source_uri, title, metadata, created_at
|
|
154
|
+
FROM astrocyte_conversations
|
|
155
|
+
WHERE id = %s
|
|
156
|
+
""",
|
|
157
|
+
(conversation_id,),
|
|
158
|
+
)
|
|
159
|
+
row = await cur.fetchone()
|
|
160
|
+
if row is None:
|
|
161
|
+
return None
|
|
162
|
+
conv = Conversation(
|
|
163
|
+
id=str(row[0]),
|
|
164
|
+
turns=[],
|
|
165
|
+
source_uri=row[1] or "",
|
|
166
|
+
title=row[2] or "",
|
|
167
|
+
metadata=row[3] or {},
|
|
168
|
+
created_at=row[4],
|
|
169
|
+
)
|
|
170
|
+
await cur.execute(
|
|
171
|
+
"""
|
|
172
|
+
SELECT id, turn_index, role, content, "timestamp", metadata
|
|
173
|
+
FROM astrocyte_conversation_turns
|
|
174
|
+
WHERE conversation_id = %s
|
|
175
|
+
ORDER BY turn_index
|
|
176
|
+
""",
|
|
177
|
+
(conversation_id,),
|
|
178
|
+
)
|
|
179
|
+
turn_rows = await cur.fetchall()
|
|
180
|
+
conv.turns = [
|
|
181
|
+
ConversationTurn(
|
|
182
|
+
id=str(r[0]),
|
|
183
|
+
role=r[2] or "user",
|
|
184
|
+
content=r[3] or "",
|
|
185
|
+
timestamp=r[4],
|
|
186
|
+
metadata=r[5] or {},
|
|
187
|
+
)
|
|
188
|
+
for r in turn_rows
|
|
189
|
+
]
|
|
190
|
+
return conv
|
|
191
|
+
|
|
192
|
+
async def list_conversations(self, *, limit: int = 100) -> list[Conversation]:
|
|
193
|
+
pool = await self._ensure_pool()
|
|
194
|
+
await self._ensure_schema(pool)
|
|
195
|
+
async with pool.connection() as conn:
|
|
196
|
+
async with conn.cursor() as cur:
|
|
197
|
+
await cur.execute(
|
|
198
|
+
"""
|
|
199
|
+
SELECT id FROM astrocyte_conversations
|
|
200
|
+
ORDER BY created_at DESC LIMIT %s
|
|
201
|
+
""",
|
|
202
|
+
(limit,),
|
|
203
|
+
)
|
|
204
|
+
ids = [str(r[0]) for r in await cur.fetchall()]
|
|
205
|
+
# Eager-load turns. For huge conversations this is expensive;
|
|
206
|
+
# callers should pass small `limit` or add an `include_turns=False`
|
|
207
|
+
# variant (Phase 3+ if needed).
|
|
208
|
+
result: list[Conversation] = []
|
|
209
|
+
for cid in ids:
|
|
210
|
+
conv = await self.get_conversation(cid)
|
|
211
|
+
if conv is not None:
|
|
212
|
+
result.append(conv)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
# ── delete ────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
async def delete_conversation(self, conversation_id: str) -> None:
|
|
218
|
+
pool = await self._ensure_pool()
|
|
219
|
+
await self._ensure_schema(pool)
|
|
220
|
+
async with pool.connection() as conn:
|
|
221
|
+
async with conn.cursor() as cur:
|
|
222
|
+
# FK CASCADE handles the turns
|
|
223
|
+
await cur.execute(
|
|
224
|
+
"DELETE FROM astrocyte_conversations WHERE id = %s",
|
|
225
|
+
(conversation_id,),
|
|
226
|
+
)
|
|
227
|
+
await conn.commit()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ─── inline DDL (mirrors migrations 027+028) ──────────────────────────
|
|
231
|
+
|
|
232
|
+
_DDL_CONVERSATIONS = """
|
|
233
|
+
CREATE TABLE IF NOT EXISTS astrocyte_conversations (
|
|
234
|
+
id UUID PRIMARY KEY,
|
|
235
|
+
source_uri TEXT NOT NULL DEFAULT '',
|
|
236
|
+
title TEXT NOT NULL DEFAULT '',
|
|
237
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
238
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
239
|
+
);
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
_DDL_CONVERSATION_TURNS = """
|
|
243
|
+
CREATE TABLE IF NOT EXISTS astrocyte_conversation_turns (
|
|
244
|
+
id UUID PRIMARY KEY,
|
|
245
|
+
conversation_id UUID NOT NULL REFERENCES astrocyte_conversations(id) ON DELETE CASCADE,
|
|
246
|
+
turn_index INTEGER NOT NULL,
|
|
247
|
+
role TEXT NOT NULL,
|
|
248
|
+
content TEXT NOT NULL,
|
|
249
|
+
"timestamp" TIMESTAMPTZ,
|
|
250
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
251
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
252
|
+
UNIQUE (conversation_id, turn_index)
|
|
253
|
+
);
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
_INDEXES = """
|
|
257
|
+
CREATE INDEX IF NOT EXISTS astrocyte_conversations_source_idx
|
|
258
|
+
ON astrocyte_conversations (source_uri)
|
|
259
|
+
WHERE source_uri <> '';
|
|
260
|
+
CREATE INDEX IF NOT EXISTS astrocyte_conversations_created_at_idx
|
|
261
|
+
ON astrocyte_conversations (created_at DESC);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS astrocyte_conversation_turns_conv_idx
|
|
263
|
+
ON astrocyte_conversation_turns (conversation_id, turn_index);
|
|
264
|
+
CREATE INDEX IF NOT EXISTS astrocyte_conversation_turns_role_idx
|
|
265
|
+
ON astrocyte_conversation_turns (conversation_id, role);
|
|
266
|
+
CREATE INDEX IF NOT EXISTS astrocyte_conversation_turns_ts_idx
|
|
267
|
+
ON astrocyte_conversation_turns (conversation_id, "timestamp")
|
|
268
|
+
WHERE "timestamp" IS NOT NULL;
|
|
269
|
+
"""
|