amfs-adapter-postgres 0.2.22__tar.gz → 0.2.23__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.
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/PKG-INFO +2 -2
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/pyproject.toml +2 -2
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/adapter.py +148 -48
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/async_adapter.py +99 -67
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/.gitignore +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/__init__.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/_fts.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/knn.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/001_initial.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/retention.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/schema.sql +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/tenant_context.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/tenant_gucs.py +0 -0
- {amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/trace_partitions.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: amfs-adapter-postgres
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.23
|
|
4
4
|
Summary: AMFS Postgres adapter with back-propagation triggers
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: amfs-core>=0.3.
|
|
7
|
+
Requires-Dist: amfs-core>=0.3.37
|
|
8
8
|
Requires-Dist: psycopg-pool>=3.1
|
|
9
9
|
Requires-Dist: psycopg[binary]<4,>=3.1
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "amfs-adapter-postgres"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.23"
|
|
4
4
|
description = "AMFS Postgres adapter with back-propagation triggers"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
dependencies = [
|
|
8
8
|
# 0.3.14 adds the capture fields this adapter now persists and selects back,
|
|
9
9
|
# tool_calls among them.
|
|
10
|
-
"amfs-core>=0.3.
|
|
10
|
+
"amfs-core>=0.3.37",
|
|
11
11
|
"psycopg[binary]>=3.1,<4",
|
|
12
12
|
"psycopg-pool>=3.1",
|
|
13
13
|
]
|
|
@@ -34,6 +34,7 @@ from amfs_core.exclusions import (
|
|
|
34
34
|
AGENT_ID_NOT_EXCLUDED_SQL,
|
|
35
35
|
ENTITY_PATH_NOT_EXCLUDED_SQL,
|
|
36
36
|
)
|
|
37
|
+
from amfs_core.actions import recorded_environment
|
|
37
38
|
from amfs_core.evidence import cited_entries, outcome_model
|
|
38
39
|
from amfs_core.models import (
|
|
39
40
|
OUTCOME_MULTIPLIERS,
|
|
@@ -110,6 +111,33 @@ logger = logging.getLogger(__name__)
|
|
|
110
111
|
# collapse, so '@%%/%%' matches exactly what '@%/%' does.
|
|
111
112
|
_EXCLUDE_SHARED_PATHS = "entity_path NOT LIKE '@%%/%%'"
|
|
112
113
|
|
|
114
|
+
# Stands in for the branch condition in a WHERE list until a connection is open
|
|
115
|
+
# and ``PostgresAdapter._branch_scope`` can resolve the branch's parent. Compared
|
|
116
|
+
# by identity, so it can never collide with a real SQL fragment.
|
|
117
|
+
_BRANCH_SCOPE = "<branch scope>"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def branch_scope_sql(branch: str, parent: str | None) -> tuple[str, list[Any]]:
|
|
121
|
+
"""The ``WHERE`` fragment reading *branch* as an overlay on *parent*, with
|
|
122
|
+
its parameters in order: the branch's own live rows, plus the parent's live
|
|
123
|
+
row for every ``(entity_path, key)`` the branch has not touched. With no
|
|
124
|
+
parent (``main``, or a branch the adapter has no record of) it is the plain
|
|
125
|
+
branch filter. Shared by both adapters; see ``PostgresAdapter._branch_scope``
|
|
126
|
+
for why the parent is read live rather than as of the branch point.
|
|
127
|
+
"""
|
|
128
|
+
if parent is None:
|
|
129
|
+
return "branch = %s", [branch]
|
|
130
|
+
return (
|
|
131
|
+
"(branch = %s OR (branch = %s AND NOT EXISTS ("
|
|
132
|
+
"SELECT 1 FROM amfs_memory_entries b"
|
|
133
|
+
" WHERE b.namespace = amfs_memory_entries.namespace"
|
|
134
|
+
" AND b.branch = %s"
|
|
135
|
+
" AND b.entity_path = amfs_memory_entries.entity_path"
|
|
136
|
+
" AND b.key = amfs_memory_entries.key"
|
|
137
|
+
" AND b.superseded_at IS NULL)))",
|
|
138
|
+
[branch, parent, branch],
|
|
139
|
+
)
|
|
140
|
+
|
|
113
141
|
# Benchmark and system rows, kept out of the aggregates that describe how much
|
|
114
142
|
# memory an account has. The predicates come from amfs_core.exclusions so that
|
|
115
143
|
# this and the Python aggregate cannot answer differently; see that module.
|
|
@@ -163,15 +191,25 @@ def list_conditions(
|
|
|
163
191
|
branch: str,
|
|
164
192
|
scope: SqlScope | None,
|
|
165
193
|
agent_id: str | None,
|
|
194
|
+
branch_scope: tuple[str, list[Any]] | None = None,
|
|
166
195
|
) -> tuple[list[str], list[Any]]:
|
|
167
196
|
"""The WHERE behind ``list()`` and ``count_entries()`` on both adapters.
|
|
168
197
|
|
|
169
198
|
One definition so a page and the ``total`` it reports cannot disagree
|
|
170
199
|
about which rows exist, and so the shared-namespace guard is applied to
|
|
171
200
|
the unscoped read in one place (``tests/test_shared_path_scoping.py``).
|
|
201
|
+
|
|
202
|
+
*branch_scope* replaces the plain ``branch = %s`` predicate with the
|
|
203
|
+
overlay fragment from :func:`branch_scope_sql` (a branch's own live rows
|
|
204
|
+
plus the parent's for every key it has not touched) — the adapters pass
|
|
205
|
+
it once they hold a cursor to look the parent up on.
|
|
172
206
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
207
|
+
if branch_scope is not None:
|
|
208
|
+
branch_sql, branch_params = branch_scope
|
|
209
|
+
else:
|
|
210
|
+
branch_sql, branch_params = "branch = %s", [branch]
|
|
211
|
+
conditions = ["namespace = %s", branch_sql]
|
|
212
|
+
params: list[Any] = [namespace, *branch_params]
|
|
175
213
|
|
|
176
214
|
if entity_path is not None:
|
|
177
215
|
conditions.append("entity_path = %s")
|
|
@@ -723,6 +761,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
723
761
|
self._has_validators_col = False
|
|
724
762
|
self._has_action_cols = False
|
|
725
763
|
self._has_outcome_embedding_col = False
|
|
764
|
+
self._has_outcome_env_col = False
|
|
726
765
|
self._has_task_text_col = False
|
|
727
766
|
self._pgvector_version: tuple[int, ...] | None = None
|
|
728
767
|
# When an embedder is provided, embeddings are computed at write time and
|
|
@@ -1225,6 +1264,14 @@ class PostgresAdapter(AdapterABC):
|
|
|
1225
1264
|
cur.execute(_OUTCOME_EVIDENCE_SQL)
|
|
1226
1265
|
# Action-level learning (trigger v4): see migration 009.
|
|
1227
1266
|
cur.execute(_ACTION_PRIORS_SQL)
|
|
1267
|
+
# The environment an outcome was recorded in (model / agent_version /
|
|
1268
|
+
# runtime / platform, see amfs_core.models.ENVIRONMENT_KEYS), so priors
|
|
1269
|
+
# can down-weight what won under another runtime. Derived at commit
|
|
1270
|
+
# from the record's session metadata, which already travels with it.
|
|
1271
|
+
cur.execute("""
|
|
1272
|
+
ALTER TABLE amfs_outcomes
|
|
1273
|
+
ADD COLUMN IF NOT EXISTS environment JSONB
|
|
1274
|
+
""")
|
|
1228
1275
|
# Query-conditioned evidence: see migration 010.
|
|
1229
1276
|
cur.execute(_LOCAL_EVIDENCE_SQL)
|
|
1230
1277
|
# The task text on outcomes: see migration 011.
|
|
@@ -1675,18 +1722,51 @@ class PostgresAdapter(AdapterABC):
|
|
|
1675
1722
|
"""
|
|
1676
1723
|
SELECT column_name FROM information_schema.columns
|
|
1677
1724
|
WHERE table_name = 'amfs_outcomes'
|
|
1678
|
-
AND column_name IN (
|
|
1725
|
+
AND column_name IN (
|
|
1726
|
+
'actions_taken', 'task_embedding', 'environment', 'task_text'
|
|
1727
|
+
)
|
|
1679
1728
|
""",
|
|
1680
1729
|
)
|
|
1681
1730
|
found = {row["column_name"] for row in cur.fetchall()}
|
|
1682
1731
|
self._has_action_cols = "actions_taken" in found
|
|
1683
1732
|
self._has_outcome_embedding_col = "task_embedding" in found
|
|
1733
|
+
self._has_outcome_env_col = "environment" in found
|
|
1684
1734
|
self._has_task_text_col = "task_text" in found
|
|
1685
1735
|
|
|
1686
1736
|
# ------------------------------------------------------------------
|
|
1687
1737
|
# read
|
|
1688
1738
|
# ------------------------------------------------------------------
|
|
1689
1739
|
|
|
1740
|
+
def _parent_branch(self, cur: Any, branch: str) -> str | None:
|
|
1741
|
+
"""The parent a non-main *branch* overlays, or ``None`` for ``main``
|
|
1742
|
+
and for a branch the adapter has no record of."""
|
|
1743
|
+
if branch == "main":
|
|
1744
|
+
return None
|
|
1745
|
+
row = cur.execute(
|
|
1746
|
+
"SELECT parent_branch FROM amfs_branches WHERE namespace = %s AND name = %s",
|
|
1747
|
+
(self._namespace, branch),
|
|
1748
|
+
).fetchone()
|
|
1749
|
+
if not row:
|
|
1750
|
+
return None
|
|
1751
|
+
return str(row["parent_branch"] or "main")
|
|
1752
|
+
|
|
1753
|
+
def _branch_scope(self, cur: Any, branch: str) -> tuple[str, list[Any]]:
|
|
1754
|
+
"""The ``WHERE`` fragment that reads *branch* as an overlay on its parent.
|
|
1755
|
+
|
|
1756
|
+
A branch is a delta: its own live rows, and for every ``(entity_path,
|
|
1757
|
+
key)`` it has not touched, the parent's **live** row. Live, not the
|
|
1758
|
+
parent as of ``branched_at`` — a session pointed at a repair branch or
|
|
1759
|
+
a canary must see the same memory a session on ``main`` sees, differing
|
|
1760
|
+
only in the entries the branch changed; a snapshot would drift from
|
|
1761
|
+
``main`` for as long as the branch lives and confound the comparison.
|
|
1762
|
+
``main`` itself, and a branch with no record, read their own rows only.
|
|
1763
|
+
|
|
1764
|
+
The fragment is :func:`branch_scope_sql`; the async adapter builds the
|
|
1765
|
+
same one from its own parent lookup, so the two read paths cannot
|
|
1766
|
+
disagree about what a branch sees.
|
|
1767
|
+
"""
|
|
1768
|
+
return branch_scope_sql(branch, self._parent_branch(cur, branch))
|
|
1769
|
+
|
|
1690
1770
|
def read(
|
|
1691
1771
|
self,
|
|
1692
1772
|
entity_path: str,
|
|
@@ -1709,23 +1789,20 @@ class PostgresAdapter(AdapterABC):
|
|
|
1709
1789
|
)
|
|
1710
1790
|
row = cur.fetchone()
|
|
1711
1791
|
|
|
1712
|
-
if row is None
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
if branch_info:
|
|
1792
|
+
if row is None:
|
|
1793
|
+
# Not on the branch: the parent's live row, per the overlay
|
|
1794
|
+
# semantics in ``_branch_scope``.
|
|
1795
|
+
parent = self._parent_branch(cur, branch)
|
|
1796
|
+
if parent is not None:
|
|
1718
1797
|
cur.execute(
|
|
1719
1798
|
"""
|
|
1720
1799
|
SELECT * FROM amfs_memory_entries
|
|
1721
1800
|
WHERE namespace = %s AND branch = %s
|
|
1722
1801
|
AND entity_path = %s AND key = %s
|
|
1723
1802
|
AND superseded_at IS NULL
|
|
1724
|
-
AND written_at <= %s
|
|
1725
1803
|
ORDER BY version DESC LIMIT 1
|
|
1726
1804
|
""",
|
|
1727
|
-
(self._namespace,
|
|
1728
|
-
entity_path, key, branch_info["branched_at"]),
|
|
1805
|
+
(self._namespace, parent, entity_path, key),
|
|
1729
1806
|
)
|
|
1730
1807
|
row = cur.fetchone()
|
|
1731
1808
|
|
|
@@ -2176,24 +2253,28 @@ class PostgresAdapter(AdapterABC):
|
|
|
2176
2253
|
- *limit*/*offset* page the result in the query. Filtering happens
|
|
2177
2254
|
before paging by construction, so a caller can never page past rows
|
|
2178
2255
|
its scope hides.
|
|
2179
|
-
"""
|
|
2180
|
-
conditions, params = list_conditions(
|
|
2181
|
-
self._namespace,
|
|
2182
|
-
entity_path,
|
|
2183
|
-
include_superseded=include_superseded,
|
|
2184
|
-
branch=branch,
|
|
2185
|
-
scope=scope,
|
|
2186
|
-
agent_id=agent_id,
|
|
2187
|
-
)
|
|
2188
|
-
where = " AND ".join(conditions)
|
|
2189
|
-
query = (
|
|
2190
|
-
f"SELECT {entry_select(self._has_is_artifact_col, self._has_validators_col)} "
|
|
2191
|
-
f"FROM amfs_memory_entries WHERE {where} ORDER BY {_list_order_sql(order_by)}"
|
|
2192
|
-
)
|
|
2193
|
-
query, params = _paginate_sql(query, params, limit=limit, offset=offset)
|
|
2194
2256
|
|
|
2257
|
+
*branch* is read as an overlay on its parent (``_branch_scope``): a
|
|
2258
|
+
repair branch or a canary lists the same memory ``main`` does, apart
|
|
2259
|
+
from the entries it changed.
|
|
2260
|
+
"""
|
|
2195
2261
|
with self._pool.connection() as conn:
|
|
2196
2262
|
with conn.cursor() as cur:
|
|
2263
|
+
conditions, params = list_conditions(
|
|
2264
|
+
self._namespace,
|
|
2265
|
+
entity_path,
|
|
2266
|
+
include_superseded=include_superseded,
|
|
2267
|
+
branch=branch,
|
|
2268
|
+
scope=scope,
|
|
2269
|
+
agent_id=agent_id,
|
|
2270
|
+
branch_scope=self._branch_scope(cur, branch),
|
|
2271
|
+
)
|
|
2272
|
+
where = " AND ".join(conditions)
|
|
2273
|
+
query = (
|
|
2274
|
+
f"SELECT {entry_select(self._has_is_artifact_col, self._has_validators_col)} "
|
|
2275
|
+
f"FROM amfs_memory_entries WHERE {where} ORDER BY {_list_order_sql(order_by)}"
|
|
2276
|
+
)
|
|
2277
|
+
query, params = _paginate_sql(query, params, limit=limit, offset=offset)
|
|
2197
2278
|
cur.execute(query, params)
|
|
2198
2279
|
rows = cur.fetchall()
|
|
2199
2280
|
|
|
@@ -2286,18 +2367,20 @@ class PostgresAdapter(AdapterABC):
|
|
|
2286
2367
|
"""``COUNT(*)`` over exactly the rows :meth:`list` would return.
|
|
2287
2368
|
|
|
2288
2369
|
The ``total`` a paged listing reports, computed without fetching a row.
|
|
2370
|
+
Same branch overlay as :meth:`list`, so the two cannot disagree.
|
|
2289
2371
|
"""
|
|
2290
|
-
conditions, params = list_conditions(
|
|
2291
|
-
self._namespace,
|
|
2292
|
-
entity_path,
|
|
2293
|
-
include_superseded=include_superseded,
|
|
2294
|
-
branch=branch,
|
|
2295
|
-
scope=scope,
|
|
2296
|
-
agent_id=agent_id,
|
|
2297
|
-
)
|
|
2298
|
-
where = " AND ".join(conditions)
|
|
2299
2372
|
with self._pool.connection() as conn:
|
|
2300
2373
|
with conn.cursor() as cur:
|
|
2374
|
+
conditions, params = list_conditions(
|
|
2375
|
+
self._namespace,
|
|
2376
|
+
entity_path,
|
|
2377
|
+
include_superseded=include_superseded,
|
|
2378
|
+
branch=branch,
|
|
2379
|
+
scope=scope,
|
|
2380
|
+
agent_id=agent_id,
|
|
2381
|
+
branch_scope=self._branch_scope(cur, branch),
|
|
2382
|
+
)
|
|
2383
|
+
where = " AND ".join(conditions)
|
|
2301
2384
|
cur.execute(f"SELECT COUNT(*) AS n FROM amfs_memory_entries WHERE {where}", params)
|
|
2302
2385
|
row = cur.fetchone()
|
|
2303
2386
|
return int(row["n"]) if row else 0
|
|
@@ -2349,8 +2432,10 @@ class PostgresAdapter(AdapterABC):
|
|
|
2349
2432
|
col_ready = getattr(self, "_has_is_artifact_col", False)
|
|
2350
2433
|
tsq_sql, tsq_params = or_tsquery(query.query) if use_fts else ("", [])
|
|
2351
2434
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2435
|
+
# The branch scope is resolved on the connection below (it may need the
|
|
2436
|
+
# branch's parent); a placeholder keeps its position in the WHERE.
|
|
2437
|
+
conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL"]
|
|
2438
|
+
params: list[Any] = [self._namespace]
|
|
2354
2439
|
|
|
2355
2440
|
if query.depth < 3:
|
|
2356
2441
|
conditions.append("tier <= %s")
|
|
@@ -2422,18 +2507,23 @@ class PostgresAdapter(AdapterABC):
|
|
|
2422
2507
|
elif not col_ready:
|
|
2423
2508
|
fetch_limit = min(max(fetch_limit, query.limit * 3), 1000)
|
|
2424
2509
|
|
|
2425
|
-
where = " AND ".join(conditions)
|
|
2426
|
-
sql = f"""
|
|
2427
|
-
SELECT {entry_select(col_ready, self._has_validators_col)} FROM amfs_memory_entries
|
|
2428
|
-
WHERE {where}
|
|
2429
|
-
ORDER BY {order}
|
|
2430
|
-
LIMIT %s
|
|
2431
|
-
"""
|
|
2432
2510
|
params.append(fetch_limit)
|
|
2433
2511
|
|
|
2434
2512
|
with self._pool.connection() as conn:
|
|
2435
2513
|
with conn.cursor() as cur:
|
|
2436
|
-
|
|
2514
|
+
scope_sql, scope_params = self._branch_scope(cur, branch)
|
|
2515
|
+
where = " AND ".join(
|
|
2516
|
+
scope_sql if c is _BRANCH_SCOPE else c for c in conditions
|
|
2517
|
+
)
|
|
2518
|
+
sql = f"""
|
|
2519
|
+
SELECT {entry_select(col_ready, self._has_validators_col)} FROM amfs_memory_entries
|
|
2520
|
+
WHERE {where}
|
|
2521
|
+
ORDER BY {order}
|
|
2522
|
+
LIMIT %s
|
|
2523
|
+
"""
|
|
2524
|
+
# The scope's params sit where its placeholder sits: right
|
|
2525
|
+
# after the namespace.
|
|
2526
|
+
cur.execute(sql, [params[0], *scope_params, *params[1:]])
|
|
2437
2527
|
rows = cur.fetchall()
|
|
2438
2528
|
|
|
2439
2529
|
entries = [self._row_to_entry(r) for r in rows]
|
|
@@ -3705,6 +3795,12 @@ class PostgresAdapter(AdapterABC):
|
|
|
3705
3795
|
list(record.entity_paths or []),
|
|
3706
3796
|
record.situation,
|
|
3707
3797
|
]
|
|
3798
|
+
if getattr(self, "_has_outcome_env_col", False):
|
|
3799
|
+
env = recorded_environment({"session_metadata": record.session_metadata or {}})
|
|
3800
|
+
if env:
|
|
3801
|
+
columns.append("environment")
|
|
3802
|
+
placeholders.append("%s::jsonb")
|
|
3803
|
+
params.append(json.dumps(env))
|
|
3708
3804
|
task_embedding = self._outcome_embedding(record)
|
|
3709
3805
|
if task_embedding is not None:
|
|
3710
3806
|
columns.append("task_embedding")
|
|
@@ -3907,9 +4003,10 @@ class PostgresAdapter(AdapterABC):
|
|
|
3907
4003
|
if since is not None:
|
|
3908
4004
|
conditions.append("committed_at >= %s")
|
|
3909
4005
|
params.append(since)
|
|
4006
|
+
env_col = ", environment" if getattr(self, "_has_outcome_env_col", False) else ""
|
|
3910
4007
|
sql = f"""
|
|
3911
4008
|
SELECT outcome_ref, outcome_type, committed_at, agent_id, actions_taken, situation,
|
|
3912
|
-
1 - (task_embedding <=> %s::vector) AS similarity
|
|
4009
|
+
1 - (task_embedding <=> %s::vector) AS similarity{env_col}
|
|
3913
4010
|
FROM amfs_outcomes
|
|
3914
4011
|
WHERE {" AND ".join(conditions)}
|
|
3915
4012
|
ORDER BY task_embedding <=> %s::vector
|
|
@@ -3931,6 +4028,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
3931
4028
|
"agent_id": row["agent_id"],
|
|
3932
4029
|
"situation": row.get("situation"),
|
|
3933
4030
|
"actions_taken": _jsonb(row.get("actions_taken"), []),
|
|
4031
|
+
"environment": _jsonb(row.get("environment"), {}) or {},
|
|
3934
4032
|
"similarity": sim,
|
|
3935
4033
|
})
|
|
3936
4034
|
return out
|
|
@@ -4137,8 +4235,9 @@ class PostgresAdapter(AdapterABC):
|
|
|
4137
4235
|
if since is not None:
|
|
4138
4236
|
conditions.append("committed_at >= %s")
|
|
4139
4237
|
params.append(since)
|
|
4238
|
+
env_col = ", environment" if getattr(self, "_has_outcome_env_col", False) else ""
|
|
4140
4239
|
sql = f"""
|
|
4141
|
-
SELECT outcome_ref, outcome_type, committed_at, agent_id, actions_taken, situation
|
|
4240
|
+
SELECT outcome_ref, outcome_type, committed_at, agent_id, actions_taken, situation{env_col}
|
|
4142
4241
|
FROM amfs_outcomes
|
|
4143
4242
|
WHERE {" AND ".join(conditions)}
|
|
4144
4243
|
ORDER BY committed_at DESC
|
|
@@ -4155,6 +4254,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
4155
4254
|
"agent_id": row["agent_id"],
|
|
4156
4255
|
"situation": row.get("situation"),
|
|
4157
4256
|
"actions_taken": _jsonb(row.get("actions_taken"), []),
|
|
4257
|
+
"environment": _jsonb(row.get("environment"), {}) or {},
|
|
4158
4258
|
"similarity": 1.0,
|
|
4159
4259
|
} for row in rows]
|
|
4160
4260
|
|
{amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/async_adapter.py
RENAMED
|
@@ -38,6 +38,7 @@ from psycopg_pool import AsyncConnectionPool
|
|
|
38
38
|
|
|
39
39
|
from amfs_postgres._fts import or_tsquery
|
|
40
40
|
from amfs_postgres.adapter import (
|
|
41
|
+
_BRANCH_SCOPE,
|
|
41
42
|
_EVIDENCE_COLUMNS,
|
|
42
43
|
_EVIDENCE_SELECT,
|
|
43
44
|
_EXCLUDE_SHARED_PATHS,
|
|
@@ -47,6 +48,7 @@ from amfs_postgres.adapter import (
|
|
|
47
48
|
_inherit_from_row,
|
|
48
49
|
_list_order_sql,
|
|
49
50
|
_paginate_sql,
|
|
51
|
+
branch_scope_sql,
|
|
50
52
|
connection_options,
|
|
51
53
|
entry_select,
|
|
52
54
|
list_conditions,
|
|
@@ -257,6 +259,30 @@ class AsyncPostgresAdapter:
|
|
|
257
259
|
except ImportError:
|
|
258
260
|
return None
|
|
259
261
|
|
|
262
|
+
# ── branches as overlays ────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async def _parent_branch(self, cur: Any, branch: str) -> str | None:
|
|
265
|
+
"""The parent a non-main *branch* overlays, or ``None`` for ``main``
|
|
266
|
+
and for a branch the adapter has no record of. The async twin of
|
|
267
|
+
``PostgresAdapter._parent_branch``."""
|
|
268
|
+
if branch == "main":
|
|
269
|
+
return None
|
|
270
|
+
await cur.execute(
|
|
271
|
+
"SELECT parent_branch FROM amfs_branches WHERE namespace = %s AND name = %s",
|
|
272
|
+
(self._namespace, branch),
|
|
273
|
+
)
|
|
274
|
+
row = await cur.fetchone()
|
|
275
|
+
if not row:
|
|
276
|
+
return None
|
|
277
|
+
return str(row["parent_branch"] or "main")
|
|
278
|
+
|
|
279
|
+
async def _branch_scope(self, cur: Any, branch: str) -> tuple[str, list[Any]]:
|
|
280
|
+
"""The ``WHERE`` fragment reading *branch* as an overlay on its live
|
|
281
|
+
parent — the same fragment the sync adapter builds, from the same
|
|
282
|
+
helper, so ``/search``, ``/retrieve`` and ``/entries`` (which run here)
|
|
283
|
+
see exactly what a sync ``list`` or ``search`` on the branch sees."""
|
|
284
|
+
return branch_scope_sql(branch, await self._parent_branch(cur, branch))
|
|
285
|
+
|
|
260
286
|
# ──────────────────────────────────────────────────────────────
|
|
261
287
|
# 1. read
|
|
262
288
|
# ──────────────────────────────────────────────────────────────
|
|
@@ -283,29 +309,21 @@ class AsyncPostgresAdapter:
|
|
|
283
309
|
)
|
|
284
310
|
row = await cur.fetchone()
|
|
285
311
|
|
|
286
|
-
if row is None
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
if branch_info:
|
|
312
|
+
if row is None:
|
|
313
|
+
# Not on the branch: the parent's *live* row. A branch is a
|
|
314
|
+
# delta over its parent, not a snapshot of it — see
|
|
315
|
+
# ``PostgresAdapter._branch_scope``.
|
|
316
|
+
parent = await self._parent_branch(cur, branch)
|
|
317
|
+
if parent is not None:
|
|
293
318
|
await cur.execute(
|
|
294
319
|
"""
|
|
295
320
|
SELECT * FROM amfs_memory_entries
|
|
296
321
|
WHERE namespace = %s AND branch = %s
|
|
297
322
|
AND entity_path = %s AND key = %s
|
|
298
323
|
AND superseded_at IS NULL
|
|
299
|
-
AND written_at <= %s
|
|
300
324
|
ORDER BY version DESC LIMIT 1
|
|
301
325
|
""",
|
|
302
|
-
(
|
|
303
|
-
self._namespace,
|
|
304
|
-
branch_info["parent_branch"],
|
|
305
|
-
entity_path,
|
|
306
|
-
key,
|
|
307
|
-
branch_info["branched_at"],
|
|
308
|
-
),
|
|
326
|
+
(self._namespace, parent, entity_path, key),
|
|
309
327
|
)
|
|
310
328
|
row = await cur.fetchone()
|
|
311
329
|
|
|
@@ -471,25 +489,26 @@ class AsyncPostgresAdapter:
|
|
|
471
489
|
|
|
472
490
|
The async twin of :meth:`PostgresAdapter.list`; see there for what the
|
|
473
491
|
keyword arguments beyond the ABC's mean. The WHERE comes from the same
|
|
474
|
-
``list_conditions`` so the two adapters cannot drift
|
|
492
|
+
``list_conditions`` so the two adapters cannot drift, and *branch* is
|
|
493
|
+
read through the same overlay on its parent.
|
|
475
494
|
"""
|
|
476
|
-
conditions, params = list_conditions(
|
|
477
|
-
self._namespace,
|
|
478
|
-
entity_path,
|
|
479
|
-
include_superseded=include_superseded,
|
|
480
|
-
branch=branch,
|
|
481
|
-
scope=scope,
|
|
482
|
-
agent_id=agent_id,
|
|
483
|
-
)
|
|
484
|
-
where = " AND ".join(conditions)
|
|
485
|
-
query = (
|
|
486
|
-
f"SELECT {entry_select(self._has_is_artifact_col)} FROM amfs_memory_entries "
|
|
487
|
-
f"WHERE {where} ORDER BY {_list_order_sql(order_by)}"
|
|
488
|
-
)
|
|
489
|
-
query, params = _paginate_sql(query, params, limit=limit, offset=offset)
|
|
490
|
-
|
|
491
495
|
async with self._pool.connection() as conn:
|
|
492
496
|
async with conn.cursor() as cur:
|
|
497
|
+
conditions, params = list_conditions(
|
|
498
|
+
self._namespace,
|
|
499
|
+
entity_path,
|
|
500
|
+
include_superseded=include_superseded,
|
|
501
|
+
branch=branch,
|
|
502
|
+
scope=scope,
|
|
503
|
+
agent_id=agent_id,
|
|
504
|
+
branch_scope=await self._branch_scope(cur, branch),
|
|
505
|
+
)
|
|
506
|
+
where = " AND ".join(conditions)
|
|
507
|
+
query = (
|
|
508
|
+
f"SELECT {entry_select(self._has_is_artifact_col)} FROM amfs_memory_entries "
|
|
509
|
+
f"WHERE {where} ORDER BY {_list_order_sql(order_by)}"
|
|
510
|
+
)
|
|
511
|
+
query, params = _paginate_sql(query, params, limit=limit, offset=offset)
|
|
493
512
|
await cur.execute(query, params)
|
|
494
513
|
rows = await cur.fetchall()
|
|
495
514
|
|
|
@@ -504,18 +523,20 @@ class AsyncPostgresAdapter:
|
|
|
504
523
|
scope: SqlScope | None = None,
|
|
505
524
|
agent_id: str | None = None,
|
|
506
525
|
) -> int:
|
|
507
|
-
"""``COUNT(*)`` over exactly the rows :meth:`list` would return
|
|
508
|
-
|
|
509
|
-
self._namespace,
|
|
510
|
-
entity_path,
|
|
511
|
-
include_superseded=include_superseded,
|
|
512
|
-
branch=branch,
|
|
513
|
-
scope=scope,
|
|
514
|
-
agent_id=agent_id,
|
|
515
|
-
)
|
|
516
|
-
where = " AND ".join(conditions)
|
|
526
|
+
"""``COUNT(*)`` over exactly the rows :meth:`list` would return —
|
|
527
|
+
the same branch overlay included, so a page and its ``total`` agree."""
|
|
517
528
|
async with self._pool.connection() as conn:
|
|
518
529
|
async with conn.cursor() as cur:
|
|
530
|
+
conditions, params = list_conditions(
|
|
531
|
+
self._namespace,
|
|
532
|
+
entity_path,
|
|
533
|
+
include_superseded=include_superseded,
|
|
534
|
+
branch=branch,
|
|
535
|
+
scope=scope,
|
|
536
|
+
agent_id=agent_id,
|
|
537
|
+
branch_scope=await self._branch_scope(cur, branch),
|
|
538
|
+
)
|
|
539
|
+
where = " AND ".join(conditions)
|
|
519
540
|
await cur.execute(
|
|
520
541
|
f"SELECT COUNT(*) AS n FROM amfs_memory_entries WHERE {where}", params
|
|
521
542
|
)
|
|
@@ -531,8 +552,10 @@ class AsyncPostgresAdapter:
|
|
|
531
552
|
col_ready = self._has_is_artifact_col
|
|
532
553
|
tsq_sql, tsq_params = or_tsquery(query.query) if use_fts else ("", [])
|
|
533
554
|
|
|
534
|
-
|
|
535
|
-
params
|
|
555
|
+
# ``_BRANCH_SCOPE`` is swapped for the real branch condition once a
|
|
556
|
+
# connection is open; its params are spliced in after the namespace.
|
|
557
|
+
conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL"]
|
|
558
|
+
params: list[Any] = [self._namespace]
|
|
536
559
|
|
|
537
560
|
if query.depth < 3:
|
|
538
561
|
conditions.append("tier <= %s")
|
|
@@ -597,18 +620,21 @@ class AsyncPostgresAdapter:
|
|
|
597
620
|
elif not col_ready:
|
|
598
621
|
fetch_limit = min(query.limit * 3, 1000)
|
|
599
622
|
|
|
600
|
-
where = " AND ".join(conditions)
|
|
601
|
-
sql = f"""
|
|
602
|
-
SELECT {entry_select(col_ready)} FROM amfs_memory_entries
|
|
603
|
-
WHERE {where}
|
|
604
|
-
ORDER BY {order}
|
|
605
|
-
LIMIT %s
|
|
606
|
-
"""
|
|
607
623
|
params.append(fetch_limit)
|
|
608
624
|
|
|
609
625
|
async with self._pool.connection() as conn:
|
|
610
626
|
async with conn.cursor() as cur:
|
|
611
|
-
await
|
|
627
|
+
scope_sql, scope_params = await self._branch_scope(cur, branch)
|
|
628
|
+
where = " AND ".join(
|
|
629
|
+
scope_sql if c is _BRANCH_SCOPE else c for c in conditions
|
|
630
|
+
)
|
|
631
|
+
sql = f"""
|
|
632
|
+
SELECT {entry_select(col_ready)} FROM amfs_memory_entries
|
|
633
|
+
WHERE {where}
|
|
634
|
+
ORDER BY {order}
|
|
635
|
+
LIMIT %s
|
|
636
|
+
"""
|
|
637
|
+
await cur.execute(sql, [params[0], *scope_params, *params[1:]])
|
|
612
638
|
rows = await cur.fetchall()
|
|
613
639
|
|
|
614
640
|
entries = [self._row_to_entry(r) for r in rows]
|
|
@@ -654,13 +680,15 @@ class AsyncPostgresAdapter:
|
|
|
654
680
|
query_vec = await asyncio.to_thread(embedder.embed, query.text)
|
|
655
681
|
vec_str = f"[{','.join(str(v) for v in query_vec)}]"
|
|
656
682
|
|
|
683
|
+
# The branch condition is resolved once a connection is open; its
|
|
684
|
+
# params follow the namespace, after the leading query vector.
|
|
657
685
|
conditions = [
|
|
658
686
|
"namespace = %s",
|
|
659
|
-
|
|
687
|
+
_BRANCH_SCOPE,
|
|
660
688
|
"superseded_at IS NULL",
|
|
661
689
|
"embedding IS NOT NULL",
|
|
662
690
|
]
|
|
663
|
-
params: list[Any] = [self._namespace
|
|
691
|
+
params: list[Any] = [self._namespace]
|
|
664
692
|
if query.entity_path is not None:
|
|
665
693
|
conditions.append("entity_path = %s")
|
|
666
694
|
params.append(query.entity_path)
|
|
@@ -673,21 +701,25 @@ class AsyncPostgresAdapter:
|
|
|
673
701
|
conditions.append("confidence <= %s")
|
|
674
702
|
params.append(query.max_confidence)
|
|
675
703
|
|
|
676
|
-
where = " AND ".join(conditions)
|
|
677
|
-
# Projected, not ``*``: the ranked rows do not need their own 1536-dim
|
|
678
|
-
# vectors shipped back only to be dropped by ``_row_to_entry`` — that
|
|
679
|
-
# was ~6 KB per candidate, per query variant.
|
|
680
|
-
sql = f"""
|
|
681
|
-
SELECT {entry_select(self._has_is_artifact_col)},
|
|
682
|
-
1 - (embedding <=> %s::vector) AS similarity
|
|
683
|
-
FROM amfs_memory_entries
|
|
684
|
-
WHERE {where}
|
|
685
|
-
ORDER BY embedding <=> %s::vector
|
|
686
|
-
LIMIT %s
|
|
687
|
-
"""
|
|
688
|
-
sql_params = [vec_str] + params + [vec_str, query.limit]
|
|
689
|
-
|
|
690
704
|
async with self._pool.connection() as conn:
|
|
705
|
+
async with conn.cursor() as cur:
|
|
706
|
+
scope_sql, scope_params = await self._branch_scope(cur, branch)
|
|
707
|
+
where = " AND ".join(
|
|
708
|
+
scope_sql if c is _BRANCH_SCOPE else c for c in conditions
|
|
709
|
+
)
|
|
710
|
+
# Projected, not ``*``: the ranked rows do not need their own 1536-dim
|
|
711
|
+
# vectors shipped back only to be dropped by ``_row_to_entry`` — that
|
|
712
|
+
# was ~6 KB per candidate, per query variant.
|
|
713
|
+
sql = f"""
|
|
714
|
+
SELECT {entry_select(self._has_is_artifact_col)},
|
|
715
|
+
1 - (embedding <=> %s::vector) AS similarity
|
|
716
|
+
FROM amfs_memory_entries
|
|
717
|
+
WHERE {where}
|
|
718
|
+
ORDER BY embedding <=> %s::vector
|
|
719
|
+
LIMIT %s
|
|
720
|
+
"""
|
|
721
|
+
sql_params = [vec_str, params[0], *scope_params, *params[1:], vec_str, query.limit]
|
|
722
|
+
|
|
691
723
|
rows: list[dict[str, Any]] | None = None
|
|
692
724
|
if use_hnsw_scan(entity_path=query.entity_path, version=self._pgvector_version):
|
|
693
725
|
# Account-wide: put this one statement on the HNSW index (see
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/retention.py
RENAMED
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/tenant_context.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/tenant_gucs.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.22 → amfs_adapter_postgres-0.2.23}/src/amfs_postgres/trace_partitions.py
RENAMED
|
File without changes
|