amfs-adapter-postgres 0.2.41__tar.gz → 0.2.42__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 (29) hide show
  1. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/PKG-INFO +1 -1
  2. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/pyproject.toml +1 -1
  3. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/adapter.py +123 -0
  4. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/.gitignore +0 -0
  5. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/__init__.py +0 -0
  6. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/_fts.py +0 -0
  7. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/async_adapter.py +0 -0
  8. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/evidence_sql.py +0 -0
  9. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/knn.py +0 -0
  10. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/001_initial.sql +0 -0
  11. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
  12. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
  13. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
  14. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
  15. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
  16. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
  17. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
  18. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
  19. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
  20. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
  21. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
  22. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
  23. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/012_evidence_from_model.sql +0 -0
  24. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/migrations/013_outcome_verified_by_backfill.sql +0 -0
  25. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/retention.py +0 -0
  26. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/schema.sql +0 -0
  27. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/tenant_context.py +0 -0
  28. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/tenant_gucs.py +0 -0
  29. {amfs_adapter_postgres-0.2.41 → amfs_adapter_postgres-0.2.42}/src/amfs_postgres/trace_partitions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: amfs-adapter-postgres
3
- Version: 0.2.41
3
+ Version: 0.2.42
4
4
  Summary: AMFS Postgres adapter with back-propagation triggers
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "amfs-adapter-postgres"
3
- version = "0.2.41"
3
+ version = "0.2.42"
4
4
  description = "AMFS Postgres adapter with back-propagation triggers"
5
5
  requires-python = ">=3.11"
6
6
  license = "Apache-2.0"
@@ -183,6 +183,45 @@ def _paginate_sql(
183
183
  return query, params
184
184
 
185
185
 
186
+ def priority_score_sql(now: datetime, config: Any | None = None) -> tuple[str, list[Any]]:
187
+ """``amfs_core.tiering.PriorityScorer.score`` as a SQL expression.
188
+
189
+ The tiers pages rank every current entry in the account by this score and
190
+ take the top 50 as Hot and the next 200 as Warm. Scoring in Python meant
191
+ loading the whole namespace first; on a 420K-entry account the two
192
+ requests the page makes together took an 8 GiB instance down. The
193
+ expression below is the same formula over the same columns, so the
194
+ database can order by it and hand back one page::
195
+
196
+ (alpha * importance + beta * recency) * freq_boost * time_decay
197
+
198
+ with ``importance = importance_score or confidence`` clamped to [0, 1],
199
+ ``recency = exp(-lambda * age_days)``, ``freq_boost = max(1, ln(1 +
200
+ recall_count))`` and ``time_decay = exp(-lambda * age_days / freq_boost)``.
201
+ ``importance_score`` of 0 falls through to confidence exactly as the
202
+ Python ``or`` does. Returns the expression and its parameters in placeholder
203
+ order: alpha, beta, lambda, *now*, lambda, *now*.
204
+ """
205
+ if config is None:
206
+ from amfs_core.models import TierConfig
207
+
208
+ config = TierConfig()
209
+ age_days = "GREATEST(0.0, EXTRACT(EPOCH FROM (%s - written_at)) / 86400.0)"
210
+ importance = (
211
+ "LEAST(1.0, GREATEST(0.0, COALESCE(NULLIF(importance_score, 0), confidence)::float8))"
212
+ )
213
+ freq_boost = "GREATEST(1.0, LN(1.0 + COALESCE(recall_count, 0)))"
214
+ expr = (
215
+ f"((%s * {importance} + %s * EXP(-%s * {age_days}))"
216
+ f" * {freq_boost}"
217
+ f" * EXP(-%s * {age_days} / {freq_boost}))"
218
+ )
219
+ # The age expression appears twice, each with its own ``now``.
220
+ lam = float(config.decay_lambda)
221
+ params: list[Any] = [float(config.alpha), float(config.beta), lam, now, lam, now]
222
+ return expr, params
223
+
224
+
186
225
  def list_conditions(
187
226
  namespace: str,
188
227
  entity_path: str | None,
@@ -2500,6 +2539,90 @@ class PostgresAdapter(AdapterABC):
2500
2539
  row = cur.fetchone()
2501
2540
  return int(row["n"]) if row else 0
2502
2541
 
2542
+ def priority_stats(
2543
+ self,
2544
+ *,
2545
+ scope: SqlScope | None = None,
2546
+ agent_id: str | None = None,
2547
+ now: datetime | None = None,
2548
+ branch: str = "main",
2549
+ ) -> tuple[int, float | None]:
2550
+ """``(count, mean priority score)`` over the rows :meth:`list` returns.
2551
+
2552
+ What the tiers distribution needs and nothing more: the count fixes
2553
+ the Hot/Warm/Archive split (the tiers are rank bands) and the mean is
2554
+ the page's "average score". Computed by the database with
2555
+ :func:`priority_score_sql`; no entry row leaves it.
2556
+ """
2557
+ now = now or datetime.now(timezone.utc)
2558
+ score, score_params = priority_score_sql(now)
2559
+ with self._pool.connection() as conn:
2560
+ with conn.cursor() as cur:
2561
+ conditions, params = list_conditions(
2562
+ self._namespace,
2563
+ None,
2564
+ include_superseded=False,
2565
+ branch=branch,
2566
+ scope=scope,
2567
+ agent_id=agent_id,
2568
+ branch_scope=self._branch_scope(cur, branch),
2569
+ )
2570
+ where = " AND ".join(conditions)
2571
+ cur.execute(
2572
+ f"SELECT COUNT(*) AS n, AVG({score}) AS mean "
2573
+ f"FROM amfs_memory_entries WHERE {where}",
2574
+ [*score_params, *params],
2575
+ )
2576
+ row = cur.fetchone()
2577
+ if not row or not row["n"]:
2578
+ return 0, None
2579
+ mean = row["mean"]
2580
+ return int(row["n"]), (float(mean) if mean is not None else None)
2581
+
2582
+ def list_by_priority(
2583
+ self,
2584
+ *,
2585
+ scope: SqlScope | None = None,
2586
+ agent_id: str | None = None,
2587
+ limit: int,
2588
+ offset: int = 0,
2589
+ now: datetime | None = None,
2590
+ branch: str = "main",
2591
+ ) -> list[MemoryEntry]:
2592
+ """One page of the rows :meth:`list` returns, highest priority first.
2593
+
2594
+ The order is :func:`priority_score_sql` descending with the stable
2595
+ ``written_at DESC, entity_path, key`` tiebreak, so a page boundary is
2596
+ deterministic. A tier is a band of this ranking: rows 0–49 are Hot,
2597
+ 50–249 Warm, the rest Archive — the caller picks *offset* and *limit*
2598
+ from the band and the database returns only that page.
2599
+ """
2600
+ now = now or datetime.now(timezone.utc)
2601
+ score, score_params = priority_score_sql(now)
2602
+ with self._pool.connection() as conn:
2603
+ with conn.cursor() as cur:
2604
+ conditions, params = list_conditions(
2605
+ self._namespace,
2606
+ None,
2607
+ include_superseded=False,
2608
+ branch=branch,
2609
+ scope=scope,
2610
+ agent_id=agent_id,
2611
+ branch_scope=self._branch_scope(cur, branch),
2612
+ )
2613
+ where = " AND ".join(conditions)
2614
+ query = (
2615
+ f"SELECT {entry_select(self._has_is_artifact_col, self._has_validators_col)} "
2616
+ f"FROM amfs_memory_entries WHERE {where} "
2617
+ f"ORDER BY {score} DESC, written_at DESC, entity_path, key"
2618
+ )
2619
+ query, all_params = _paginate_sql(
2620
+ query, [*params, *score_params], limit=limit, offset=offset
2621
+ )
2622
+ cur.execute(query, all_params)
2623
+ rows = cur.fetchall()
2624
+ return [self._row_to_entry(r) for r in rows]
2625
+
2503
2626
  def entry_authors(self, refs: list[tuple[str, str]]) -> dict[tuple[str, str], str]:
2504
2627
  """Who wrote each ``(entity_path, key)``, for the current version.
2505
2628