amfs-adapter-postgres 0.2.40__tar.gz → 0.2.41__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.40 → amfs_adapter_postgres-0.2.41}/PKG-INFO +2 -2
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/pyproject.toml +2 -2
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/adapter.py +277 -27
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/.gitignore +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/__init__.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/_fts.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/async_adapter.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/evidence_sql.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/knn.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/001_initial.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/012_evidence_from_model.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/migrations/013_outcome_verified_by_backfill.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/retention.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/schema.sql +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/tenant_context.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/tenant_gucs.py +0 -0
- {amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/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.41
|
|
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.53
|
|
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.41"
|
|
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.53",
|
|
11
11
|
"psycopg[binary]>=3.1,<4",
|
|
12
12
|
"psycopg-pool>=3.1",
|
|
13
13
|
]
|
|
@@ -477,6 +477,7 @@ from amfs_core.aggregates import ( # noqa: E402
|
|
|
477
477
|
CHARS_PER_TOKEN as _CHARS_PER_TOKEN,
|
|
478
478
|
RECALL_TOKENS_CEIL as _RECALL_TOKENS_CEIL,
|
|
479
479
|
RECALL_TOKENS_FLOOR as _RECALL_TOKENS_FLOOR,
|
|
480
|
+
entity_confidence_buckets,
|
|
480
481
|
)
|
|
481
482
|
|
|
482
483
|
# Guarded by recall_count, because an entry never recalled contributes zero
|
|
@@ -3434,17 +3435,40 @@ class PostgresAdapter(AdapterABC):
|
|
|
3434
3435
|
newest_entry_at=row["newest_entry_at"],
|
|
3435
3436
|
)
|
|
3436
3437
|
|
|
3438
|
+
#: ``sort`` values ``entity_summaries`` accepts, and the ORDER BY each is.
|
|
3439
|
+
_ENTITY_SUMMARY_SORTS: dict[str, str] = {
|
|
3440
|
+
"last_updated": "last_updated DESC NULLS LAST, entity_path",
|
|
3441
|
+
"entry_count": "entry_count DESC, entity_path",
|
|
3442
|
+
"total_recalls": "total_recalls DESC, entity_path",
|
|
3443
|
+
"avg_confidence": "avg_confidence DESC NULLS LAST, entity_path",
|
|
3444
|
+
"avg_confidence_asc": "avg_confidence ASC NULLS LAST, entity_path",
|
|
3445
|
+
"entity_path": "entity_path",
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3437
3448
|
def entity_summaries(
|
|
3438
3449
|
self,
|
|
3439
3450
|
*,
|
|
3440
3451
|
agent_ids: list[str] | None = None,
|
|
3441
3452
|
scope: SqlScope | None = None,
|
|
3453
|
+
q: str | None = None,
|
|
3454
|
+
agent: str | None = None,
|
|
3455
|
+
min_confidence: float | None = None,
|
|
3456
|
+
sort: str = "last_updated",
|
|
3457
|
+
limit: int | None = None,
|
|
3458
|
+
offset: int = 0,
|
|
3442
3459
|
) -> list[dict[str, Any]]:
|
|
3443
3460
|
"""Per-entity aggregates via GROUP BY — never loads entry values.
|
|
3444
3461
|
|
|
3445
3462
|
*scope* is a visibility predicate (room co-member rules and the like)
|
|
3446
3463
|
that ``agent_ids`` alone cannot express; with it, a per-user dashboard
|
|
3447
3464
|
gets this query instead of a Python pass over every entry.
|
|
3465
|
+
|
|
3466
|
+
*q* keeps the paths containing it (case-insensitive), *agent* the
|
|
3467
|
+
entities that agent has written to, *min_confidence* those whose mean
|
|
3468
|
+
confidence reaches it — the topics directory's filters, in SQL. With
|
|
3469
|
+
*limit* one page comes back and every row carries ``total``, the
|
|
3470
|
+
count before paging. Unpaged, the whole list is returned as before —
|
|
3471
|
+
1.6 MB on a 4,000-entity account, so a directory should page.
|
|
3448
3472
|
"""
|
|
3449
3473
|
conditions = ["namespace = %s", "branch = 'main'", "superseded_at IS NULL"]
|
|
3450
3474
|
params: list[Any] = [self._namespace]
|
|
@@ -3453,6 +3477,24 @@ class PostgresAdapter(AdapterABC):
|
|
|
3453
3477
|
params.append(list(agent_ids))
|
|
3454
3478
|
if scope is not None:
|
|
3455
3479
|
scope.apply(conditions, params)
|
|
3480
|
+
if q:
|
|
3481
|
+
pattern = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
|
3482
|
+
conditions.append("entity_path ILIKE %s")
|
|
3483
|
+
params.append(pattern)
|
|
3484
|
+
having: list[str] = []
|
|
3485
|
+
having_params: list[Any] = []
|
|
3486
|
+
if agent:
|
|
3487
|
+
having.append("BOOL_OR(agent_id = %s)")
|
|
3488
|
+
having_params.append(agent)
|
|
3489
|
+
if min_confidence is not None:
|
|
3490
|
+
having.append("AVG(confidence) >= %s")
|
|
3491
|
+
having_params.append(float(min_confidence))
|
|
3492
|
+
if sort not in self._ENTITY_SUMMARY_SORTS:
|
|
3493
|
+
raise ValueError(f"unknown sort {sort!r}")
|
|
3494
|
+
order_by = self._ENTITY_SUMMARY_SORTS[sort]
|
|
3495
|
+
page = ""
|
|
3496
|
+
if limit is not None:
|
|
3497
|
+
page = "LIMIT %s OFFSET %s"
|
|
3456
3498
|
# No entity_path argument to this one: it is unscoped by construction,
|
|
3457
3499
|
# so shared namespaces are always excluded. It groups by entity_path,
|
|
3458
3500
|
# which would otherwise list a shared namespace's topics as though
|
|
@@ -3478,8 +3520,21 @@ class PostgresAdapter(AdapterABC):
|
|
|
3478
3520
|
FROM amfs_memory_entries
|
|
3479
3521
|
WHERE {where}
|
|
3480
3522
|
GROUP BY entity_path
|
|
3481
|
-
|
|
3523
|
+
{("HAVING " + " AND ".join(having)) if having else ""}
|
|
3482
3524
|
"""
|
|
3525
|
+
params.extend(having_params)
|
|
3526
|
+
if limit is not None:
|
|
3527
|
+
# Paged: the count before paging rides on every row (one window
|
|
3528
|
+
# over the grouped rows, no second statement).
|
|
3529
|
+
params.extend([int(limit), max(0, int(offset))])
|
|
3530
|
+
sql = f"""
|
|
3531
|
+
SELECT x.*, COUNT(*) OVER () AS total
|
|
3532
|
+
FROM ({sql}) x
|
|
3533
|
+
ORDER BY {order_by}
|
|
3534
|
+
{page}
|
|
3535
|
+
"""
|
|
3536
|
+
else:
|
|
3537
|
+
sql = f"SELECT * FROM ({sql}) x ORDER BY {order_by}"
|
|
3483
3538
|
with self._pool.connection() as conn:
|
|
3484
3539
|
with conn.cursor() as cur:
|
|
3485
3540
|
# The checkout is one transaction (see _TenantRLSConnection),
|
|
@@ -3491,6 +3546,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
3491
3546
|
|
|
3492
3547
|
return [
|
|
3493
3548
|
{
|
|
3549
|
+
**({"total": int(r["total"])} if limit is not None else {}),
|
|
3494
3550
|
"entity_path": r["entity_path"],
|
|
3495
3551
|
"entry_count": r["entry_count"],
|
|
3496
3552
|
"avg_confidence": float(r["avg_confidence"] or 0),
|
|
@@ -3504,6 +3560,44 @@ class PostgresAdapter(AdapterABC):
|
|
|
3504
3560
|
for r in rows
|
|
3505
3561
|
]
|
|
3506
3562
|
|
|
3563
|
+
def consolidation_ready_entities(
|
|
3564
|
+
self,
|
|
3565
|
+
*,
|
|
3566
|
+
min_entries: int = 10,
|
|
3567
|
+
limit: int = 10,
|
|
3568
|
+
scope: SqlScope | None = None,
|
|
3569
|
+
) -> list[str]:
|
|
3570
|
+
"""Entity paths with at least *min_entries* live entries, largest first.
|
|
3571
|
+
|
|
3572
|
+
What the consolidation status card shows. Counted in SQL: the handler
|
|
3573
|
+
used to load every entry on the account into Python to count them per
|
|
3574
|
+
path — two to three minutes on 430K entries, for ten path names.
|
|
3575
|
+
"""
|
|
3576
|
+
conditions = ["namespace = %s", "branch = 'main'", "superseded_at IS NULL"]
|
|
3577
|
+
params: list[Any] = [self._namespace]
|
|
3578
|
+
if scope is not None:
|
|
3579
|
+
scope.apply(conditions, params)
|
|
3580
|
+
conditions.append(_EXCLUDE_SHARED_PATHS)
|
|
3581
|
+
conditions.append(_EXCLUDE_SYSTEM_ROWS)
|
|
3582
|
+
where = " AND ".join(conditions)
|
|
3583
|
+
params.extend([int(min_entries), int(limit)])
|
|
3584
|
+
with self._pool.connection() as conn:
|
|
3585
|
+
with conn.cursor() as cur:
|
|
3586
|
+
cur.execute(f"SET LOCAL work_mem = '{_AGGREGATE_WORK_MEM}'")
|
|
3587
|
+
cur.execute(
|
|
3588
|
+
f"""
|
|
3589
|
+
SELECT entity_path
|
|
3590
|
+
FROM amfs_memory_entries
|
|
3591
|
+
WHERE {where}
|
|
3592
|
+
GROUP BY entity_path
|
|
3593
|
+
HAVING COUNT(*) >= %s
|
|
3594
|
+
ORDER BY COUNT(*) DESC, entity_path
|
|
3595
|
+
LIMIT %s
|
|
3596
|
+
""",
|
|
3597
|
+
params,
|
|
3598
|
+
)
|
|
3599
|
+
return [r["entity_path"] for r in cur.fetchall()]
|
|
3600
|
+
|
|
3507
3601
|
def agent_summaries(
|
|
3508
3602
|
self,
|
|
3509
3603
|
*,
|
|
@@ -3634,6 +3728,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
3634
3728
|
*,
|
|
3635
3729
|
agent_ids: list[str] | None = None,
|
|
3636
3730
|
scope: SqlScope | None = None,
|
|
3731
|
+
breakdown: bool = True,
|
|
3637
3732
|
) -> dict[str, Any]:
|
|
3638
3733
|
"""Extended aggregate stats in SQL (recalls, weekly deltas, types).
|
|
3639
3734
|
|
|
@@ -3641,6 +3736,16 @@ class PostgresAdapter(AdapterABC):
|
|
|
3641
3736
|
to the entry aggregates only: the reuse-event query below is keyed by
|
|
3642
3737
|
account and reader agent, and an entry-visibility predicate has no
|
|
3643
3738
|
meaning over event rows.
|
|
3739
|
+
|
|
3740
|
+
``breakdown=False`` leaves out the per-agent and per-entity count maps
|
|
3741
|
+
(``agents``, ``entities``) and takes the two distinct counts from one
|
|
3742
|
+
grouped subquery each instead. On a 6,000-agent, 4,000-entity account
|
|
3743
|
+
the maps were 600 KB of a response whose reader wanted two integers.
|
|
3744
|
+
|
|
3745
|
+
``entity_confidence`` buckets the entities by their mean confidence
|
|
3746
|
+
(``healthy`` >= 0.9, ``moderate`` >= 0.7, ``low`` >= 0.4, ``critical``
|
|
3747
|
+
below) with the mean of those means as ``avg`` — the health page's
|
|
3748
|
+
donut, which used to need every entity summary to draw four counts.
|
|
3644
3749
|
"""
|
|
3645
3750
|
conditions = ["namespace = %s", "superseded_at IS NULL"]
|
|
3646
3751
|
params: list[Any] = [self._namespace]
|
|
@@ -3697,23 +3802,72 @@ class PostgresAdapter(AdapterABC):
|
|
|
3697
3802
|
)
|
|
3698
3803
|
row = cur.fetchone()
|
|
3699
3804
|
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3805
|
+
agent_rows: list[Any] = []
|
|
3806
|
+
entity_rows: list[Any] = []
|
|
3807
|
+
# Entities bucketed by mean confidence; one pass over the
|
|
3808
|
+
# per-entity groups either way.
|
|
3809
|
+
bucket_sql = """
|
|
3810
|
+
COUNT(*) AS total_entities,
|
|
3811
|
+
COALESCE(AVG(c), 0) AS entity_confidence_avg,
|
|
3812
|
+
COUNT(*) FILTER (WHERE c >= 0.9) AS entities_healthy,
|
|
3813
|
+
COUNT(*) FILTER (WHERE c >= 0.7 AND c < 0.9) AS entities_moderate,
|
|
3814
|
+
COUNT(*) FILTER (WHERE c >= 0.4 AND c < 0.7) AS entities_low,
|
|
3815
|
+
COUNT(*) FILTER (WHERE c < 0.4) AS entities_critical
|
|
3816
|
+
"""
|
|
3817
|
+
if breakdown:
|
|
3818
|
+
cur.execute(
|
|
3819
|
+
f"""
|
|
3820
|
+
SELECT agent_id, COUNT(*) AS cnt
|
|
3821
|
+
FROM amfs_memory_entries WHERE {where} GROUP BY agent_id
|
|
3822
|
+
""",
|
|
3823
|
+
params,
|
|
3824
|
+
)
|
|
3825
|
+
agent_rows = cur.fetchall()
|
|
3708
3826
|
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3827
|
+
cur.execute(
|
|
3828
|
+
f"""
|
|
3829
|
+
SELECT entity_path, COUNT(*) AS cnt,
|
|
3830
|
+
COALESCE(AVG(confidence), 0) AS c
|
|
3831
|
+
FROM amfs_memory_entries WHERE {where} GROUP BY entity_path
|
|
3832
|
+
""",
|
|
3833
|
+
params,
|
|
3834
|
+
)
|
|
3835
|
+
entity_rows = cur.fetchall()
|
|
3836
|
+
total_agents = len(agent_rows)
|
|
3837
|
+
total_entities = len(entity_rows)
|
|
3838
|
+
# The same edges the pure-Python aggregate uses.
|
|
3839
|
+
buckets = entity_confidence_buckets(r["c"] for r in entity_rows)
|
|
3840
|
+
else:
|
|
3841
|
+
# Grouped subqueries rather than COUNT(DISTINCT ...): the
|
|
3842
|
+
# distinct aggregate forces a sort of every row and keeps
|
|
3843
|
+
# the statement off the parallel plan (see above).
|
|
3844
|
+
cur.execute(
|
|
3845
|
+
f"""
|
|
3846
|
+
SELECT
|
|
3847
|
+
(SELECT COUNT(*) FROM (
|
|
3848
|
+
SELECT 1 FROM amfs_memory_entries WHERE {where}
|
|
3849
|
+
GROUP BY agent_id) a) AS total_agents,
|
|
3850
|
+
e.*
|
|
3851
|
+
FROM (
|
|
3852
|
+
SELECT {bucket_sql}
|
|
3853
|
+
FROM (
|
|
3854
|
+
SELECT COALESCE(AVG(confidence), 0) AS c
|
|
3855
|
+
FROM amfs_memory_entries WHERE {where}
|
|
3856
|
+
GROUP BY entity_path) g
|
|
3857
|
+
) e
|
|
3858
|
+
""",
|
|
3859
|
+
[*params, *params],
|
|
3860
|
+
)
|
|
3861
|
+
counts = cur.fetchone() or {}
|
|
3862
|
+
total_agents = int(counts.get("total_agents") or 0)
|
|
3863
|
+
total_entities = int(counts.get("total_entities") or 0)
|
|
3864
|
+
buckets = {
|
|
3865
|
+
"avg": float(counts.get("entity_confidence_avg") or 0),
|
|
3866
|
+
"healthy": int(counts.get("entities_healthy") or 0),
|
|
3867
|
+
"moderate": int(counts.get("entities_moderate") or 0),
|
|
3868
|
+
"low": int(counts.get("entities_low") or 0),
|
|
3869
|
+
"critical": int(counts.get("entities_critical") or 0),
|
|
3870
|
+
}
|
|
3717
3871
|
|
|
3718
3872
|
cur.execute(
|
|
3719
3873
|
f"""
|
|
@@ -3781,14 +3935,17 @@ class PostgresAdapter(AdapterABC):
|
|
|
3781
3935
|
# and the dashboard hides the trend line.
|
|
3782
3936
|
pass
|
|
3783
3937
|
|
|
3784
|
-
|
|
3938
|
+
out: dict[str, Any] = {
|
|
3785
3939
|
"recalls_this_week": recalls_this_week,
|
|
3786
3940
|
"recalls_last_week": recalls_last_week,
|
|
3787
3941
|
"total_entries": row["total_entries"] if row else 0,
|
|
3788
|
-
"total_entities":
|
|
3789
|
-
"total_agents":
|
|
3790
|
-
|
|
3791
|
-
|
|
3942
|
+
"total_entities": total_entities,
|
|
3943
|
+
"total_agents": total_agents,
|
|
3944
|
+
}
|
|
3945
|
+
if breakdown:
|
|
3946
|
+
out["agents"] = {r["agent_id"]: r["cnt"] for r in agent_rows}
|
|
3947
|
+
out["entities"] = {r["entity_path"]: r["cnt"] for r in entity_rows}
|
|
3948
|
+
out.update({
|
|
3792
3949
|
"confidence_avg": float(row["confidence_avg"] or 0) if row else 0.0,
|
|
3793
3950
|
"confidence_min": float(row["confidence_min"] or 0) if row else 0.0,
|
|
3794
3951
|
"confidence_max": float(row["confidence_max"] or 0) if row else 0.0,
|
|
@@ -3802,7 +3959,9 @@ class PostgresAdapter(AdapterABC):
|
|
|
3802
3959
|
"memory_type_counts": {
|
|
3803
3960
|
(r["memory_type"] or "fact"): r["cnt"] for r in type_rows
|
|
3804
3961
|
},
|
|
3805
|
-
|
|
3962
|
+
"entity_confidence": buckets,
|
|
3963
|
+
})
|
|
3964
|
+
return out
|
|
3806
3965
|
|
|
3807
3966
|
def share_stats(
|
|
3808
3967
|
self,
|
|
@@ -5194,11 +5353,18 @@ class PostgresAdapter(AdapterABC):
|
|
|
5194
5353
|
namespace: str = "default",
|
|
5195
5354
|
branch: str = "main",
|
|
5196
5355
|
*,
|
|
5356
|
+
scope: str | None = None,
|
|
5357
|
+
written_by: str | None = None,
|
|
5197
5358
|
relevant_to: list[str] | None = None,
|
|
5198
5359
|
limit: int | None = None,
|
|
5199
5360
|
) -> list[Digest]:
|
|
5200
|
-
"""List digests, optionally filtered by type and
|
|
5361
|
+
"""List digests, optionally filtered by type, branch and scope.
|
|
5201
5362
|
|
|
5363
|
+
``scope`` keeps the digests compiled for exactly that scope — an
|
|
5364
|
+
agent's brief, one entity — in SQL, so a page asking about one agent
|
|
5365
|
+
does not receive every brief on the account (5 MB on a large one) to
|
|
5366
|
+
pick its own out of. ``written_by`` keeps the digests whose scope is
|
|
5367
|
+
an entity path that agent has live entries under.
|
|
5202
5368
|
``relevant_to`` keeps only digests that name one of the given strings
|
|
5203
5369
|
— as their scope, or anywhere in their summary. The briefing scores a
|
|
5204
5370
|
digest only when the entity or agent it is asked about appears in one
|
|
@@ -5213,6 +5379,21 @@ class PostgresAdapter(AdapterABC):
|
|
|
5213
5379
|
if digest_type:
|
|
5214
5380
|
where.append("digest_type = %s")
|
|
5215
5381
|
params.append(digest_type.value)
|
|
5382
|
+
if scope:
|
|
5383
|
+
where.append("scope = %s")
|
|
5384
|
+
params.append(scope)
|
|
5385
|
+
if written_by:
|
|
5386
|
+
# The agent's entity paths, resolved in the database; RLS on the
|
|
5387
|
+
# entries table keeps the subquery to what the caller may see.
|
|
5388
|
+
# Same branch as the digests, and without benchmark or system
|
|
5389
|
+
# rows — the set ``entity_summaries`` would list for the agent,
|
|
5390
|
+
# so a digest shows up here only for a topic that page shows.
|
|
5391
|
+
where.append(
|
|
5392
|
+
"scope IN (SELECT DISTINCT entity_path FROM amfs_memory_entries"
|
|
5393
|
+
" WHERE namespace = %s AND branch = %s AND agent_id = %s"
|
|
5394
|
+
f" AND superseded_at IS NULL AND {_EXCLUDE_SYSTEM_ROWS})"
|
|
5395
|
+
)
|
|
5396
|
+
params.extend([namespace, branch, written_by])
|
|
5216
5397
|
if account_id:
|
|
5217
5398
|
where.append("account_id = %s")
|
|
5218
5399
|
params.append(account_id)
|
|
@@ -5761,7 +5942,37 @@ class PostgresAdapter(AdapterABC):
|
|
|
5761
5942
|
params,
|
|
5762
5943
|
)
|
|
5763
5944
|
|
|
5764
|
-
|
|
5945
|
+
#: ``sort`` values ``list_agents_enriched`` accepts, and the ORDER BY each is.
|
|
5946
|
+
_ENRICHED_AGENT_SORTS: dict[str, str] = {
|
|
5947
|
+
"last_active": "last_active DESC NULLS LAST, agent_id",
|
|
5948
|
+
"entries_written": "entries_written DESC, agent_id",
|
|
5949
|
+
"created_at": "created_at DESC NULLS LAST, agent_id",
|
|
5950
|
+
"agent_id": "agent_id",
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
def list_agents_enriched(
|
|
5954
|
+
self,
|
|
5955
|
+
namespace: str = "default",
|
|
5956
|
+
*,
|
|
5957
|
+
agent_ids: list[str] | None = None,
|
|
5958
|
+
q: str | None = None,
|
|
5959
|
+
platform: str | None = None,
|
|
5960
|
+
sort: str = "last_active",
|
|
5961
|
+
limit: int | None = None,
|
|
5962
|
+
offset: int = 0,
|
|
5963
|
+
) -> list[dict[str, Any]]:
|
|
5964
|
+
"""Enriched agent rows, optionally one page of them.
|
|
5965
|
+
|
|
5966
|
+
*agent_ids* narrows to the agents the caller may see, so a page is a
|
|
5967
|
+
page of visible agents. *q* is a case-insensitive substring match on
|
|
5968
|
+
the agent id, description, model, platform and primary repo;
|
|
5969
|
+
*platform* keeps one platform. With *limit* set, every row carries
|
|
5970
|
+
``total`` — the count before paging — and ``active_7d``, how many of
|
|
5971
|
+
those were active in the last seven days, so a directory can show
|
|
5972
|
+
"showing 50 of 6,356 · 412 active this week" without a second query.
|
|
5973
|
+
Unpaged, the whole roster is returned as before (3 MB on that account;
|
|
5974
|
+
the dashboard pages).
|
|
5975
|
+
"""
|
|
5765
5976
|
account_id = self._get_current_account_id()
|
|
5766
5977
|
if account_id:
|
|
5767
5978
|
account_filter = "AND a.account_id = %s"
|
|
@@ -5773,6 +5984,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
5773
5984
|
entry_account_filter = ""
|
|
5774
5985
|
graph_account_filter = ""
|
|
5775
5986
|
acct_params = []
|
|
5987
|
+
agent_ids_filter = "AND a.agent_id = ANY(%s)" if agent_ids is not None else ""
|
|
5776
5988
|
|
|
5777
5989
|
sql = f"""
|
|
5778
5990
|
WITH agent_entries AS (
|
|
@@ -5845,14 +6057,47 @@ class PostgresAdapter(AdapterABC):
|
|
|
5845
6057
|
LEFT JOIN agent_collab c ON c.agent_id = a.agent_id
|
|
5846
6058
|
LEFT JOIN amfs_agent_group_members gm
|
|
5847
6059
|
ON gm.agent_id = a.agent_id AND gm.namespace = a.namespace
|
|
5848
|
-
WHERE a.namespace = %s {account_filter}
|
|
5849
|
-
ORDER BY last_active DESC NULLS LAST
|
|
6060
|
+
WHERE a.namespace = %s {account_filter} {agent_ids_filter}
|
|
5850
6061
|
"""
|
|
5851
6062
|
params: list[Any] = [
|
|
5852
6063
|
namespace, *acct_params,
|
|
5853
6064
|
namespace, *acct_params,
|
|
5854
6065
|
namespace, *acct_params,
|
|
5855
6066
|
]
|
|
6067
|
+
if agent_ids is not None:
|
|
6068
|
+
params.append(list(agent_ids))
|
|
6069
|
+
if sort not in self._ENRICHED_AGENT_SORTS:
|
|
6070
|
+
raise ValueError(f"unknown sort {sort!r}")
|
|
6071
|
+
order_by = self._ENRICHED_AGENT_SORTS[sort]
|
|
6072
|
+
# The roster as a subquery so the search sees the computed columns
|
|
6073
|
+
# and the page count is taken over the rows the search kept.
|
|
6074
|
+
outer: list[str] = []
|
|
6075
|
+
if q:
|
|
6076
|
+
pattern = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
|
6077
|
+
outer.append(
|
|
6078
|
+
"(x.agent_id ILIKE %s OR x.description ILIKE %s OR x.model ILIKE %s"
|
|
6079
|
+
" OR x.platform ILIKE %s OR x.primary_repo ILIKE %s)"
|
|
6080
|
+
)
|
|
6081
|
+
params.extend([pattern] * 5)
|
|
6082
|
+
if platform:
|
|
6083
|
+
outer.append("x.platform = %s")
|
|
6084
|
+
params.append(platform)
|
|
6085
|
+
outer_where = ("WHERE " + " AND ".join(outer)) if outer else ""
|
|
6086
|
+
page = ""
|
|
6087
|
+
if limit is not None:
|
|
6088
|
+
page = "LIMIT %s OFFSET %s"
|
|
6089
|
+
params.extend([int(limit), max(0, int(offset))])
|
|
6090
|
+
sql = f"""
|
|
6091
|
+
SELECT x.*,
|
|
6092
|
+
COUNT(*) OVER () AS total,
|
|
6093
|
+
COUNT(*) FILTER (
|
|
6094
|
+
WHERE x.last_active >= NOW() - INTERVAL '7 days'
|
|
6095
|
+
) OVER () AS active_7d
|
|
6096
|
+
FROM ({sql}) x
|
|
6097
|
+
{outer_where}
|
|
6098
|
+
ORDER BY {order_by}
|
|
6099
|
+
{page}
|
|
6100
|
+
"""
|
|
5856
6101
|
|
|
5857
6102
|
with self._pool.connection() as conn:
|
|
5858
6103
|
with conn.cursor() as cur:
|
|
@@ -5865,6 +6110,11 @@ class PostgresAdapter(AdapterABC):
|
|
|
5865
6110
|
if isinstance(tags, str):
|
|
5866
6111
|
tags = json.loads(tags)
|
|
5867
6112
|
results.append({
|
|
6113
|
+
**(
|
|
6114
|
+
{"total": int(r["total"]), "active_7d": int(r["active_7d"] or 0)}
|
|
6115
|
+
if limit is not None
|
|
6116
|
+
else {}
|
|
6117
|
+
),
|
|
5868
6118
|
"agent_id": r["agent_id"],
|
|
5869
6119
|
"created_at": r["created_at"],
|
|
5870
6120
|
"entries_written": r["entries_written"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/async_adapter.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/evidence_sql.py
RENAMED
|
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.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/retention.py
RENAMED
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/tenant_context.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/tenant_gucs.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.40 → amfs_adapter_postgres-0.2.41}/src/amfs_postgres/trace_partitions.py
RENAMED
|
File without changes
|