amfs-adapter-postgres 0.2.20__tar.gz → 0.2.22__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.
Files changed (26) hide show
  1. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/PKG-INFO +2 -2
  2. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/pyproject.toml +2 -2
  3. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/adapter.py +122 -26
  4. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/.gitignore +0 -0
  5. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/__init__.py +0 -0
  6. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/_fts.py +0 -0
  7. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/async_adapter.py +0 -0
  8. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/knn.py +0 -0
  9. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/001_initial.sql +0 -0
  10. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
  11. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
  12. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
  13. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
  14. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
  15. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
  16. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
  17. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
  18. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
  19. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
  20. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
  21. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
  22. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/retention.py +0 -0
  23. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/schema.sql +0 -0
  24. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/tenant_context.py +0 -0
  25. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/src/amfs_postgres/tenant_gucs.py +0 -0
  26. {amfs_adapter_postgres-0.2.20 → amfs_adapter_postgres-0.2.22}/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.20
3
+ Version: 0.2.22
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.32
7
+ Requires-Dist: amfs-core>=0.3.34
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.20"
3
+ version = "0.2.22"
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.32",
10
+ "amfs-core>=0.3.34",
11
11
  "psycopg[binary]>=3.1,<4",
12
12
  "psycopg-pool>=3.1",
13
13
  ]
@@ -1212,6 +1212,12 @@ class PostgresAdapter(AdapterABC):
1212
1212
  ON amfs_memory_entries (namespace, entity_path)
1213
1213
  WHERE tier <= 2 AND superseded_at IS NULL
1214
1214
  """)
1215
+ # TTL sweep (list_expired): the few rows carrying a ttl_at, by expiry.
1216
+ cur.execute("""
1217
+ CREATE INDEX IF NOT EXISTS idx_entries_ttl
1218
+ ON amfs_memory_entries (ttl_at)
1219
+ WHERE ttl_at IS NOT NULL AND superseded_at IS NULL
1220
+ """)
1215
1221
  # Outcome propagation (trigger v3): evidence columns on entries,
1216
1222
  # attempts/final_action_index on outcomes, and the propagate function
1217
1223
  # with its helpers. Applied from the migration file rather than a copy
@@ -2193,6 +2199,34 @@ class PostgresAdapter(AdapterABC):
2193
2199
 
2194
2200
  return [self._row_to_entry(r) for r in rows]
2195
2201
 
2202
+ def list_expired(
2203
+ self, *, now: datetime | None = None, branch: str = "main", limit: int = 1000
2204
+ ) -> list[MemoryEntry]:
2205
+ """Current entries whose ``ttl_at`` has passed, oldest expiry first.
2206
+
2207
+ What a TTL sweep needs: the rows to archive, found through the
2208
+ partial index on ``ttl_at`` (``idx_entries_ttl``) rather than by
2209
+ listing the tenant and checking each entry's clock in Python. Same
2210
+ row set as ``list()`` — current versions, this branch, shared paths
2211
+ excluded — narrowed to the expired ones. *limit* bounds one sweep;
2212
+ the next sweep picks up the rest.
2213
+ """
2214
+ conditions, params = list_conditions(
2215
+ self._namespace, None, include_superseded=False, branch=branch, scope=None, agent_id=None,
2216
+ )
2217
+ conditions.append("ttl_at IS NOT NULL AND ttl_at <= %s")
2218
+ params.append(now or datetime.now(timezone.utc))
2219
+ query = (
2220
+ f"SELECT {entry_select(self._has_is_artifact_col, self._has_validators_col)} "
2221
+ f"FROM amfs_memory_entries WHERE {' AND '.join(conditions)} ORDER BY ttl_at LIMIT %s"
2222
+ )
2223
+ params.append(limit)
2224
+ with self._pool.connection() as conn:
2225
+ with conn.cursor() as cur:
2226
+ cur.execute(query, params)
2227
+ rows = cur.fetchall()
2228
+ return [self._row_to_entry(r) for r in rows]
2229
+
2196
2230
  def list_scopes(self, *, branch: str = "main") -> tuple[set[str], set[str]]:
2197
2231
  """The distinct ``(entity paths, agent ids)`` behind the current entries.
2198
2232
 
@@ -3933,18 +3967,36 @@ class PostgresAdapter(AdapterABC):
3933
3967
  *,
3934
3968
  per_key: int = 50,
3935
3969
  min_similarity: float = 0.0,
3970
+ tau: float | None = None,
3971
+ min_weight: float | None = None,
3972
+ recent: int = 5,
3936
3973
  ) -> dict[str, dict[str, Any]]:
3937
3974
  """The outcome record of each entry in ``entry_keys``, restricted to
3938
3975
  outcomes whose task was like the one embedded.
3939
3976
 
3940
- Returns ``entry_key -> {"success", "failure", "n", "best_similarity"}``
3941
- where ``success`` and ``failure`` are similarity-weighted counts: an
3942
- outcome on a task at similarity 0.9 counts 0.9, one at 0.5 counts 0.5.
3943
- Keys with no nearby outcome are absent. Terminal outcomes credit the
3944
- row's ``causal_entry_keys``; the failed attempts inside ``attempts``
3945
- credit their own keys with a failure, exactly as the propagation
3946
- trigger applies them to the pooled columns — so this is the pooled
3947
- record, conditioned on the task, and nothing more.
3977
+ Returns ``entry_key -> {"success", "failure", "n", "best_similarity",
3978
+ "recent"}``. ``success`` and ``failure`` are weighted counts, the
3979
+ weight of each outcome relative to the *nearest* one for that key:
3980
+ ``exp(-(best - sim) / tau)``, the form ``neighbourhood_weights`` gives
3981
+ the action priors and for the same reason — under a retrieval
3982
+ embedder every task on an entity clears any absolute floor, but
3983
+ within one query the nearest outcomes are the same kind of task and
3984
+ the rest trail by a gap. An outcome 0.05 behind the best counts a
3985
+ fifth, one 0.1 behind counts 4%, and rows under ``min_weight`` are
3986
+ dropped rather than counted toward ``n``. ``tau`` and ``min_weight``
3987
+ default to the priors' constants. ``recent`` is the newest
3988
+ in-neighbourhood outcomes for the key, newest first, each ``{success,
3989
+ similarity, committed_at}`` — the order the sums cannot carry, which
3990
+ ``locally_discredited`` reads. Keys with no nearby outcome are absent.
3991
+
3992
+ Terminal outcomes credit the row's ``causal_entry_keys``; the failed
3993
+ attempts inside ``attempts`` credit their own keys with a failure,
3994
+ exactly as the propagation trigger applies them to the pooled columns
3995
+ — so this is the pooled record, conditioned on the task, and nothing
3996
+ more. ``min_similarity`` is an absolute floor under the relative
3997
+ weighting: rows below it never enter, so a key whose only outcomes
3998
+ are on unrelated tasks has no local record rather than a full-weight
3999
+ one.
3948
4000
 
3949
4001
  Bounded on both sides: ``per_key`` nearest outcomes per entry, over
3950
4002
  the outcomes the GIN index on ``causal_entry_keys`` narrows to.
@@ -3952,18 +4004,28 @@ class PostgresAdapter(AdapterABC):
3952
4004
  """
3953
4005
  if not self._has_outcome_embedding_col or not embedding or not entry_keys:
3954
4006
  return {}
4007
+ from amfs_core.actions import PRIORS_NEIGHBOURHOOD_MIN_W, PRIORS_NEIGHBOURHOOD_TAU
4008
+
4009
+ tau_ = float(PRIORS_NEIGHBOURHOOD_TAU if tau is None else tau)
4010
+ min_w = float(PRIORS_NEIGHBOURHOOD_MIN_W if min_weight is None else min_weight)
3955
4011
  vec = f"[{','.join(str(v) for v in embedding)}]"
3956
4012
  keys = list(dict.fromkeys(entry_keys))
3957
4013
  # Each row is unnested to (key, is_failure) pairs: the terminal outcome
3958
4014
  # for every causal key, and a failure for every key inside a failed
3959
4015
  # attempt. Rows are narrowed first by key overlap (indexed), then
3960
- # ranked per key by task similarity and cut at per_key. ``n`` counts
3961
- # distinct outcomes, not credits: one outcome that names a key in a
3962
- # failed attempt and again as a causal key is one nearby task, and
3963
- # must not on its own clear LOCAL_EVIDENCE_MIN_N.
4016
+ # ranked per key by task similarity and cut at per_key, then weighted
4017
+ # against the key's best match and cut again at min_weight. ``n``
4018
+ # counts distinct outcomes, not credits: one outcome that names a key
4019
+ # in a failed attempt and again as a causal key is one nearby task,
4020
+ # and must not on its own clear LOCAL_EVIDENCE_MIN_N. ``recent`` is
4021
+ # aggregated newest first with the failure credit ahead of the success
4022
+ # credit of the same outcome, so the Python dedupe below keeps the
4023
+ # failure: the entry misled an attempt on that task, whatever else it
4024
+ # did afterwards.
3964
4025
  sql = """
3965
4026
  WITH near AS (
3966
4027
  SELECT o.id AS outcome_id, o.outcome_type, o.causal_entry_keys, o.attempts,
4028
+ o.committed_at,
3967
4029
  1 - (o.task_embedding <=> %s::vector) AS similarity
3968
4030
  FROM amfs_outcomes o
3969
4031
  WHERE o.namespace = %s
@@ -3983,11 +4045,11 @@ class PostgresAdapter(AdapterABC):
3983
4045
  credited AS (
3984
4046
  SELECT n.outcome_id, k AS entry_key,
3985
4047
  amfs_outcome_is_success(n.outcome_type) AS success,
3986
- n.similarity
4048
+ n.similarity, n.committed_at
3987
4049
  FROM near n, unnest(n.causal_entry_keys) AS k
3988
4050
  WHERE k = ANY(%s::text[])
3989
4051
  UNION ALL
3990
- SELECT n.outcome_id, ak AS entry_key, FALSE AS success, n.similarity
4052
+ SELECT n.outcome_id, ak AS entry_key, FALSE AS success, n.similarity, n.committed_at
3991
4053
  FROM near n,
3992
4054
  jsonb_array_elements(n.attempts) AS att,
3993
4055
  jsonb_array_elements_text(
@@ -3996,34 +4058,68 @@ class PostgresAdapter(AdapterABC):
3996
4058
  WHERE ak = ANY(%s::text[])
3997
4059
  ),
3998
4060
  ranked AS (
3999
- SELECT outcome_id, entry_key, success, similarity,
4000
- row_number() OVER (PARTITION BY entry_key ORDER BY similarity DESC) AS rn
4061
+ SELECT outcome_id, entry_key, success, similarity, committed_at,
4062
+ row_number() OVER (PARTITION BY entry_key ORDER BY similarity DESC) AS rn,
4063
+ MAX(similarity) OVER (PARTITION BY entry_key) AS best
4001
4064
  FROM credited
4002
4065
  WHERE similarity >= %s
4066
+ ),
4067
+ weighted AS (
4068
+ SELECT outcome_id, entry_key, success, similarity, committed_at, best,
4069
+ exp(-(best - similarity) / %s) AS w
4070
+ FROM ranked
4071
+ WHERE rn <= %s
4003
4072
  )
4004
4073
  SELECT entry_key,
4005
- SUM(CASE WHEN success THEN similarity ELSE 0 END) AS success,
4006
- SUM(CASE WHEN success THEN 0 ELSE similarity END) AS failure,
4074
+ SUM(CASE WHEN success THEN w ELSE 0 END) AS success,
4075
+ SUM(CASE WHEN success THEN 0 ELSE w END) AS failure,
4007
4076
  COUNT(DISTINCT outcome_id) AS n,
4008
- MAX(similarity) AS best_similarity
4009
- FROM ranked
4010
- WHERE rn <= %s
4077
+ MAX(similarity) AS best_similarity,
4078
+ (array_agg(
4079
+ jsonb_build_object(
4080
+ 'o', outcome_id, 's', success, 'sim', similarity,
4081
+ 'at', to_char(committed_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"+00:00"')
4082
+ )
4083
+ ORDER BY committed_at DESC, success ASC
4084
+ ))[1:%s] AS recent
4085
+ FROM weighted
4086
+ WHERE w >= %s
4011
4087
  GROUP BY entry_key
4012
4088
  """
4013
- params = [vec, self._namespace, keys, keys, keys, keys, float(min_similarity), int(per_key)]
4089
+ # ``recent`` is over-fetched (two credits per outcome at most) and
4090
+ # deduped per outcome below.
4091
+ params = [
4092
+ vec, self._namespace, keys, keys, keys, keys, float(min_similarity),
4093
+ tau_ if tau_ > 0 else 1e-9, int(per_key), int(max(1, recent)) * 2, min_w,
4094
+ ]
4014
4095
  with self._pool.connection() as conn:
4015
4096
  with conn.cursor() as cur:
4016
4097
  cur.execute(sql, params)
4017
4098
  rows = cur.fetchall()
4018
- return {
4019
- row["entry_key"]: {
4099
+ out: dict[str, dict[str, Any]] = {}
4100
+ for row in rows:
4101
+ seen: set[Any] = set()
4102
+ recent_rows: list[dict[str, Any]] = []
4103
+ for item in _jsonb(row.get("recent"), []) or []:
4104
+ item = _jsonb(item, None) # jsonb[] elements may arrive decoded or as text
4105
+ if not isinstance(item, dict) or item.get("o") in seen:
4106
+ continue
4107
+ seen.add(item.get("o"))
4108
+ recent_rows.append({
4109
+ "success": bool(item.get("s")),
4110
+ "similarity": float(item.get("sim") or 0.0),
4111
+ "committed_at": item.get("at"),
4112
+ })
4113
+ if len(recent_rows) >= max(1, recent):
4114
+ break
4115
+ out[row["entry_key"]] = {
4020
4116
  "success": float(row["success"] or 0.0),
4021
4117
  "failure": float(row["failure"] or 0.0),
4022
4118
  "n": int(row["n"] or 0),
4023
4119
  "best_similarity": float(row["best_similarity"] or 0.0),
4120
+ "recent": recent_rows,
4024
4121
  }
4025
- for row in rows
4026
- }
4122
+ return out
4027
4123
 
4028
4124
  def action_stats(
4029
4125
  self,