amfs-adapter-postgres 0.2.35__tar.gz → 0.2.40__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.35 → amfs_adapter_postgres-0.2.40}/PKG-INFO +2 -2
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/pyproject.toml +2 -2
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/adapter.py +180 -9
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/async_adapter.py +7 -2
- amfs_adapter_postgres-0.2.40/src/amfs_postgres/evidence_sql.py +468 -0
- amfs_adapter_postgres-0.2.40/src/amfs_postgres/migrations/012_evidence_from_model.sql +423 -0
- amfs_adapter_postgres-0.2.40/src/amfs_postgres/migrations/013_outcome_verified_by_backfill.sql +105 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/schema.sql +149 -17
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/.gitignore +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/__init__.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/_fts.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/knn.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/001_initial.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/retention.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/tenant_context.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/tenant_gucs.py +0 -0
- {amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/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.40
|
|
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.52
|
|
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.40"
|
|
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.52",
|
|
11
11
|
"psycopg[binary]>=3.1,<4",
|
|
12
12
|
"psycopg-pool>=3.1",
|
|
13
13
|
]
|
|
@@ -34,8 +34,8 @@ 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
|
|
38
|
-
from amfs_core.evidence import cited_entries, outcome_model
|
|
37
|
+
from amfs_core.actions import entity_paths_of, recorded_environment
|
|
38
|
+
from amfs_core.evidence import cited_entries, outcome_model, outcome_read_keys, outcome_verified_by
|
|
39
39
|
from amfs_core.models import (
|
|
40
40
|
OUTCOME_MULTIPLIERS,
|
|
41
41
|
Agent,
|
|
@@ -274,6 +274,20 @@ _LOCAL_EVIDENCE_SQL = (
|
|
|
274
274
|
_OUTCOME_TASK_TEXT_SQL = (
|
|
275
275
|
Path(__file__).parent / "migrations" / "011_outcome_task_text.sql"
|
|
276
276
|
).read_text(encoding="utf-8")
|
|
277
|
+
#: Migration 012 — the evidence functions rendered from ``amfs_core.evidence``
|
|
278
|
+
#: (``evidence_sql.render_evidence_sql``), replacing the hand-written bodies
|
|
279
|
+
#: of 008/009 with the same arithmetic read off the Python model. Applied
|
|
280
|
+
#: last so its CREATE OR REPLACE is what stands; folded into the fingerprint
|
|
281
|
+
#: so a regenerated file re-applies.
|
|
282
|
+
_EVIDENCE_FROM_MODEL_SQL = (
|
|
283
|
+
Path(__file__).parent / "migrations" / "012_evidence_from_model.sql"
|
|
284
|
+
).read_text(encoding="utf-8")
|
|
285
|
+
#: Migration 013 — ``verified_by`` copied from the sealed trace onto the
|
|
286
|
+
#: outcomes committed before 012 gave them the column. One pass, recorded in
|
|
287
|
+
#: ``amfs_backfills``; every later application returns at the first SELECT.
|
|
288
|
+
_VERIFIED_BY_BACKFILL_SQL = (
|
|
289
|
+
Path(__file__).parent / "migrations" / "013_outcome_verified_by_backfill.sql"
|
|
290
|
+
).read_text(encoding="utf-8")
|
|
277
291
|
#: Characters of ``task_input`` kept on the outcome row. Read for word
|
|
278
292
|
#: statistics — which of a query's words recur across this entity's tasks —
|
|
279
293
|
#: so the head of the request is all that is needed, and a pasted log or
|
|
@@ -620,6 +634,11 @@ _EVIDENCE_COLUMNS: tuple[str, ...] = (
|
|
|
620
634
|
"success_count", "failure_count", "evidence_success", "evidence_failure",
|
|
621
635
|
"last_outcome", "last_outcome_at", "discredited_at", "prior_confidence",
|
|
622
636
|
)
|
|
637
|
+
#: Credit by uplift (migration 012): maintained by the trigger for the keys
|
|
638
|
+
#: an outcome cites, read by ``amfs_core.evidence.uplift_credit``. Not
|
|
639
|
+
#: persisted on write — a rewritten claim starts from the uniform split and
|
|
640
|
+
#: earns its own measurement the next time a session cites it.
|
|
641
|
+
_UPLIFT_COLUMNS: tuple[str, ...] = ("uplift", "uplift_sessions")
|
|
623
642
|
_EVIDENCE_SELECT = ", outcome_count, " + ", ".join(_EVIDENCE_COLUMNS)
|
|
624
643
|
|
|
625
644
|
#: The columns ``_row_to_entry`` reads, named so that entry-returning SELECTs
|
|
@@ -630,8 +649,8 @@ _EVIDENCE_SELECT = ", outcome_count, " + ", ".join(_EVIDENCE_COLUMNS)
|
|
|
630
649
|
#: unscoped ``list()`` over a 20k-row namespace moved ~80 MB per call, and a
|
|
631
650
|
#: dashboard page that fans out to several such calls took 20-35 s while the
|
|
632
651
|
#: database sat at 1% CPU (2026-09-17). Every column here is created by
|
|
633
|
-
#: schema.sql or ensured by ``_apply_migrations`` (including
|
|
634
|
-
#: so the projection is safe on any bootstrapped database.
|
|
652
|
+
#: schema.sql or ensured by ``_apply_migrations`` (including migrations 008
|
|
653
|
+
#: and 012), so the projection is safe on any bootstrapped database.
|
|
635
654
|
_ENTRY_COLUMNS: tuple[str, ...] = (
|
|
636
655
|
"id",
|
|
637
656
|
"namespace",
|
|
@@ -658,6 +677,7 @@ _ENTRY_COLUMNS: tuple[str, ...] = (
|
|
|
658
677
|
"branch",
|
|
659
678
|
"is_artifact",
|
|
660
679
|
*_EVIDENCE_COLUMNS,
|
|
680
|
+
*_UPLIFT_COLUMNS,
|
|
661
681
|
)
|
|
662
682
|
ENTRY_SELECT = ", ".join(_ENTRY_COLUMNS)
|
|
663
683
|
#: For the window before ``_apply_migrations`` adds is_artifact; the decoder
|
|
@@ -804,6 +824,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
804
824
|
self._has_outcome_embedding_col = False
|
|
805
825
|
self._has_outcome_env_col = False
|
|
806
826
|
self._has_task_text_col = False
|
|
827
|
+
self._has_verified_by_col = False
|
|
807
828
|
self._pgvector_version: tuple[int, ...] | None = None
|
|
808
829
|
# When an embedder is provided, embeddings are computed at write time and
|
|
809
830
|
# persisted, so ANN retrieval (semantic_search / pgvector HNSW) works
|
|
@@ -949,7 +970,8 @@ class PostgresAdapter(AdapterABC):
|
|
|
949
970
|
return hashlib.sha256(
|
|
950
971
|
(
|
|
951
972
|
_SCHEMA_SQL + migrations + _OUTCOME_EVIDENCE_SQL + _ACTION_PRIORS_SQL
|
|
952
|
-
+ _LOCAL_EVIDENCE_SQL + _OUTCOME_TASK_TEXT_SQL
|
|
973
|
+
+ _LOCAL_EVIDENCE_SQL + _OUTCOME_TASK_TEXT_SQL + _EVIDENCE_FROM_MODEL_SQL
|
|
974
|
+
+ _VERIFIED_BY_BACKFILL_SQL
|
|
953
975
|
).encode("utf-8")
|
|
954
976
|
).hexdigest()
|
|
955
977
|
|
|
@@ -1317,6 +1339,12 @@ class PostgresAdapter(AdapterABC):
|
|
|
1317
1339
|
cur.execute(_LOCAL_EVIDENCE_SQL)
|
|
1318
1340
|
# The task text on outcomes: see migration 011.
|
|
1319
1341
|
cur.execute(_OUTCOME_TASK_TEXT_SQL)
|
|
1342
|
+
# The evidence functions from the Python model: see migration 012.
|
|
1343
|
+
cur.execute(_EVIDENCE_FROM_MODEL_SQL)
|
|
1344
|
+
# verified_by onto the outcomes that predate the column: see 013.
|
|
1345
|
+
# After 012 (the column) and after the traces gained session_metadata
|
|
1346
|
+
# above, which is where the value is copied from.
|
|
1347
|
+
cur.execute(_VERIFIED_BY_BACKFILL_SQL)
|
|
1320
1348
|
# task_embedding on outcomes follows the entries' embedding dimension,
|
|
1321
1349
|
# so it is derived here rather than declared in the migration.
|
|
1322
1350
|
cur.execute(
|
|
@@ -1776,7 +1804,8 @@ class PostgresAdapter(AdapterABC):
|
|
|
1776
1804
|
SELECT column_name FROM information_schema.columns
|
|
1777
1805
|
WHERE table_name = 'amfs_outcomes'
|
|
1778
1806
|
AND column_name IN (
|
|
1779
|
-
'actions_taken', 'task_embedding', 'environment', 'task_text'
|
|
1807
|
+
'actions_taken', 'task_embedding', 'environment', 'task_text',
|
|
1808
|
+
'verified_by'
|
|
1780
1809
|
)
|
|
1781
1810
|
""",
|
|
1782
1811
|
)
|
|
@@ -1785,6 +1814,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
1785
1814
|
self._has_outcome_embedding_col = "task_embedding" in found
|
|
1786
1815
|
self._has_outcome_env_col = "environment" in found
|
|
1787
1816
|
self._has_task_text_col = "task_text" in found
|
|
1817
|
+
self._has_verified_by_col = "verified_by" in found
|
|
1788
1818
|
|
|
1789
1819
|
# ------------------------------------------------------------------
|
|
1790
1820
|
# read
|
|
@@ -2659,8 +2689,13 @@ class PostgresAdapter(AdapterABC):
|
|
|
2659
2689
|
params: list[Any] = [self._namespace]
|
|
2660
2690
|
|
|
2661
2691
|
if query.entity_path is not None:
|
|
2662
|
-
|
|
2663
|
-
|
|
2692
|
+
if getattr(query, "include_descendants", False):
|
|
2693
|
+
clause, clause_params = descendants_sql("entity_path", query.entity_path)
|
|
2694
|
+
conditions.append(clause)
|
|
2695
|
+
params.extend(clause_params)
|
|
2696
|
+
else:
|
|
2697
|
+
conditions.append("entity_path = %s")
|
|
2698
|
+
params.append(query.entity_path)
|
|
2664
2699
|
else:
|
|
2665
2700
|
conditions.append(_EXCLUDE_SHARED_PATHS)
|
|
2666
2701
|
if query.min_confidence > 0:
|
|
@@ -2840,6 +2875,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
2840
2875
|
limit: int = 10,
|
|
2841
2876
|
visible_agents: Collection[str] | None = None,
|
|
2842
2877
|
agent: str | None = None,
|
|
2878
|
+
writer: str | None = None,
|
|
2843
2879
|
) -> dict[str, Any]:
|
|
2844
2880
|
"""What memory has been reused since a point in time, and by whom.
|
|
2845
2881
|
|
|
@@ -2871,6 +2907,13 @@ class PostgresAdapter(AdapterABC):
|
|
|
2871
2907
|
would be filtering a list already cut to ``limit`` by reuse volume, so a
|
|
2872
2908
|
quiet agent's handoffs would be missing — and indistinguishable from that
|
|
2873
2909
|
agent having none.
|
|
2910
|
+
|
|
2911
|
+
``writer`` is the same narrowing from the other side: the window as seen
|
|
2912
|
+
by one agent AS WRITER — the reads of memory it wrote. ``cross_surface``
|
|
2913
|
+
is then exactly how many times another agent took its memory, and
|
|
2914
|
+
``by_agent`` names who. It composes with ``agent`` and with the
|
|
2915
|
+
visibility scope the same way, and an agent the caller may not see
|
|
2916
|
+
answers nothing.
|
|
2874
2917
|
"""
|
|
2875
2918
|
empty: dict[str, Any] = {
|
|
2876
2919
|
"reuses": 0,
|
|
@@ -2896,6 +2939,11 @@ class PostgresAdapter(AdapterABC):
|
|
|
2896
2939
|
return empty
|
|
2897
2940
|
scope += " AND reused_by = %s"
|
|
2898
2941
|
scope_params = (*scope_params, agent)
|
|
2942
|
+
if writer:
|
|
2943
|
+
if visible_agents is not None and writer not in visible_agents:
|
|
2944
|
+
return empty
|
|
2945
|
+
scope += " AND written_by = %s"
|
|
2946
|
+
scope_params = (*scope_params, writer)
|
|
2899
2947
|
|
|
2900
2948
|
try:
|
|
2901
2949
|
# Every column is aliased and read by name: the pool's row factory is
|
|
@@ -3889,13 +3937,29 @@ class PostgresAdapter(AdapterABC):
|
|
|
3889
3937
|
json.dumps(dict(record.causal_entry_versions or {})),
|
|
3890
3938
|
]
|
|
3891
3939
|
if self._has_action_cols:
|
|
3940
|
+
# The entities this outcome is about. The SDK derives them
|
|
3941
|
+
# from the cited keys; a record built without them (an
|
|
3942
|
+
# older client, a direct adapter caller) gets the same
|
|
3943
|
+
# derivation here, because the trigger's uplift refresh
|
|
3944
|
+
# (migration 012) finds an entity's sessions by this column.
|
|
3945
|
+
entity_paths = list(record.entity_paths or []) or entity_paths_of(
|
|
3946
|
+
sorted(outcome_read_keys(record))
|
|
3947
|
+
)
|
|
3892
3948
|
columns += ["actions_taken", "entity_paths", "situation"]
|
|
3893
3949
|
placeholders += ["%s::jsonb", "%s", "%s"]
|
|
3894
3950
|
params += [
|
|
3895
3951
|
json.dumps(list(record.actions_taken or []), default=str),
|
|
3896
|
-
|
|
3952
|
+
entity_paths,
|
|
3897
3953
|
record.situation,
|
|
3898
3954
|
]
|
|
3955
|
+
# Who decided the outcome when it was not the agent (CI, a
|
|
3956
|
+
# monitor, a reviewer); the trigger weighs a verified outcome
|
|
3957
|
+
# by VERIFIED_EVIDENCE_WEIGHT (migration 012).
|
|
3958
|
+
verified_by = outcome_verified_by(record)
|
|
3959
|
+
if self._has_verified_by_col and verified_by:
|
|
3960
|
+
columns.append("verified_by")
|
|
3961
|
+
placeholders.append("%s")
|
|
3962
|
+
params.append(verified_by)
|
|
3899
3963
|
if getattr(self, "_has_outcome_env_col", False):
|
|
3900
3964
|
env = recorded_environment({"session_metadata": record.session_metadata or {}})
|
|
3901
3965
|
if env:
|
|
@@ -3972,6 +4036,111 @@ class PostgresAdapter(AdapterABC):
|
|
|
3972
4036
|
row = cur.fetchone()
|
|
3973
4037
|
return int(row["cnt"]) if row else 0
|
|
3974
4038
|
|
|
4039
|
+
def learning_series(
|
|
4040
|
+
self,
|
|
4041
|
+
*,
|
|
4042
|
+
since: datetime,
|
|
4043
|
+
agent: str | None = None,
|
|
4044
|
+
entity_path: str | None = None,
|
|
4045
|
+
limit: int = 5000,
|
|
4046
|
+
visible_agents: Collection[str] | None = None,
|
|
4047
|
+
) -> dict[str, Any]:
|
|
4048
|
+
"""The rows ``amfs_core.learning.learning_curve`` reads: the newest
|
|
4049
|
+
``limit`` outcomes in the window, returned oldest first, and the regime
|
|
4050
|
+
shifts among the entries they cited.
|
|
4051
|
+
|
|
4052
|
+
``sessions`` carries ``(at, outcome_type, agent_id, verified_by)`` per
|
|
4053
|
+
outcome. When the window holds more than ``limit`` outcomes the series
|
|
4054
|
+
keeps the most recent ones — the curve is about where the agent is
|
|
4055
|
+
now — and says so with ``truncated``; the shifts are then bounded by
|
|
4056
|
+
the oldest session kept, so every shift reported falls inside the
|
|
4057
|
+
sessions returned. ``shifts`` is one row per moment a cited entry was
|
|
4058
|
+
discredited — distinct ``(entity_path, key, discredited_at)`` across
|
|
4059
|
+
the entry's versions, since the step copies the timestamp forward
|
|
4060
|
+
until evidence lifts it. "Cited" counts the terminal causal keys and
|
|
4061
|
+
the failed attempts' keys alike, as the uplift refresh does.
|
|
4062
|
+
``visible_agents`` scopes everything to the agents the caller may see,
|
|
4063
|
+
and ``agent`` narrows on top of it, never instead of it; an agent
|
|
4064
|
+
outside the scope answers an empty series.
|
|
4065
|
+
"""
|
|
4066
|
+
empty: dict[str, Any] = {"sessions": [], "shifts": [], "truncated": False}
|
|
4067
|
+
if visible_agents is not None and not visible_agents:
|
|
4068
|
+
return empty
|
|
4069
|
+
conditions = ["namespace = %s", "committed_at >= %s"]
|
|
4070
|
+
params: list[Any] = [self._namespace, since]
|
|
4071
|
+
if visible_agents is not None:
|
|
4072
|
+
conditions.append("agent_id = ANY(%s)")
|
|
4073
|
+
params.append(list(visible_agents))
|
|
4074
|
+
if agent:
|
|
4075
|
+
if visible_agents is not None and agent not in visible_agents:
|
|
4076
|
+
return empty
|
|
4077
|
+
conditions.append("agent_id = %s")
|
|
4078
|
+
params.append(agent)
|
|
4079
|
+
if entity_path:
|
|
4080
|
+
if self._has_action_cols:
|
|
4081
|
+
conditions.append("entity_paths @> ARRAY[%s]::text[]")
|
|
4082
|
+
params.append(entity_path)
|
|
4083
|
+
else:
|
|
4084
|
+
conditions.append(
|
|
4085
|
+
"EXISTS (SELECT 1 FROM unnest(causal_entry_keys) AS ek WHERE ek LIKE %s)"
|
|
4086
|
+
)
|
|
4087
|
+
params.append(f"{entity_path}/%")
|
|
4088
|
+
where = " AND ".join(conditions)
|
|
4089
|
+
verified = "verified_by" if self._has_verified_by_col else "NULL::text AS verified_by"
|
|
4090
|
+
cap = max(1, int(limit))
|
|
4091
|
+
# Newest first under the cap, so a busy account's series ends at its
|
|
4092
|
+
# current sessions rather than stopping at the start of the window;
|
|
4093
|
+
# reversed below so the curve reads oldest to newest.
|
|
4094
|
+
newest = f"""SELECT id, committed_at, outcome_type, agent_id, causal_entry_keys, attempts
|
|
4095
|
+
FROM amfs_outcomes
|
|
4096
|
+
WHERE {where}
|
|
4097
|
+
ORDER BY committed_at DESC, id DESC
|
|
4098
|
+
LIMIT %s"""
|
|
4099
|
+
with self._pool.connection() as conn, conn.cursor() as cur:
|
|
4100
|
+
# One row past the cap tells "exactly the cap" from "more than
|
|
4101
|
+
# it"; a window holding exactly ``limit`` outcomes is complete.
|
|
4102
|
+
cur.execute(
|
|
4103
|
+
f"""WITH win AS ({newest})
|
|
4104
|
+
SELECT committed_at AS at, outcome_type, agent_id, {verified}
|
|
4105
|
+
FROM amfs_outcomes
|
|
4106
|
+
WHERE id IN (SELECT id FROM win)
|
|
4107
|
+
ORDER BY committed_at ASC, id ASC""",
|
|
4108
|
+
(*params, cap + 1),
|
|
4109
|
+
)
|
|
4110
|
+
sessions = [dict(row) for row in cur.fetchall()]
|
|
4111
|
+
if not sessions:
|
|
4112
|
+
return empty
|
|
4113
|
+
truncated = len(sessions) > cap
|
|
4114
|
+
if truncated:
|
|
4115
|
+
del sessions[0]
|
|
4116
|
+
# A shift before the oldest session kept has no "before" in this
|
|
4117
|
+
# series; bound the shifts by what the sessions actually cover.
|
|
4118
|
+
shift_since = max(since, sessions[0]["at"]) if truncated else since
|
|
4119
|
+
shift_conditions = ["e.namespace = %s", "e.discredited_at IS NOT NULL", "e.discredited_at >= %s"]
|
|
4120
|
+
shift_params: list[Any] = [self._namespace, shift_since]
|
|
4121
|
+
if entity_path:
|
|
4122
|
+
shift_conditions.append("e.entity_path = %s")
|
|
4123
|
+
shift_params.append(entity_path)
|
|
4124
|
+
cur.execute(
|
|
4125
|
+
f"""WITH win AS ({newest}),
|
|
4126
|
+
cited AS (
|
|
4127
|
+
SELECT DISTINCT x AS spec
|
|
4128
|
+
FROM win o, unnest(COALESCE(o.causal_entry_keys, '{{}}')) AS x
|
|
4129
|
+
UNION
|
|
4130
|
+
SELECT DISTINCT jsonb_array_elements_text(
|
|
4131
|
+
COALESCE(a->'causal_entry_keys', '[]'::jsonb))
|
|
4132
|
+
FROM win o, jsonb_array_elements(COALESCE(o.attempts, '[]'::jsonb)) AS a
|
|
4133
|
+
)
|
|
4134
|
+
SELECT DISTINCT e.entity_path, e.key, e.discredited_at AS at
|
|
4135
|
+
FROM amfs_memory_entries e
|
|
4136
|
+
JOIN cited c ON c.spec = e.entity_path || '/' || e.key
|
|
4137
|
+
WHERE {" AND ".join(shift_conditions)}
|
|
4138
|
+
ORDER BY e.discredited_at ASC""",
|
|
4139
|
+
(*params, cap, *shift_params),
|
|
4140
|
+
)
|
|
4141
|
+
shifts = [dict(row) for row in cur.fetchall()]
|
|
4142
|
+
return {"sessions": sessions, "shifts": shifts, "truncated": truncated}
|
|
4143
|
+
|
|
3975
4144
|
def list_outcomes(
|
|
3976
4145
|
self,
|
|
3977
4146
|
*,
|
|
@@ -4935,6 +5104,8 @@ class PostgresAdapter(AdapterABC):
|
|
|
4935
5104
|
last_outcome_at=row.get("last_outcome_at"),
|
|
4936
5105
|
discredited_at=row.get("discredited_at"),
|
|
4937
5106
|
validators=_validators_of(row),
|
|
5107
|
+
uplift=float(row["uplift"]) if row.get("uplift") is not None else None,
|
|
5108
|
+
uplift_sessions=int(row.get("uplift_sessions") or 0),
|
|
4938
5109
|
recall_count=row.get("recall_count", 0),
|
|
4939
5110
|
priority_score=float(row["priority_score"]) if row.get("priority_score") is not None else None,
|
|
4940
5111
|
tier=row.get("tier", 3),
|
{amfs_adapter_postgres-0.2.35 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/async_adapter.py
RENAMED
|
@@ -716,8 +716,13 @@ class AsyncPostgresAdapter:
|
|
|
716
716
|
]
|
|
717
717
|
params: list[Any] = [self._namespace]
|
|
718
718
|
if query.entity_path is not None:
|
|
719
|
-
|
|
720
|
-
|
|
719
|
+
if getattr(query, "include_descendants", False):
|
|
720
|
+
clause, clause_params = descendants_sql("entity_path", query.entity_path)
|
|
721
|
+
conditions.append(clause)
|
|
722
|
+
params.extend(clause_params)
|
|
723
|
+
else:
|
|
724
|
+
conditions.append("entity_path = %s")
|
|
725
|
+
params.append(query.entity_path)
|
|
721
726
|
else:
|
|
722
727
|
conditions.append(_EXCLUDE_SHARED_PATHS)
|
|
723
728
|
if query.min_confidence > 0:
|