amfs-http-server 0.3.51__tar.gz → 0.3.53__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_http_server-0.3.51 → amfs_http_server-0.3.53}/PKG-INFO +3 -3
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/pyproject.toml +3 -3
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/server.py +209 -2
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/.gitignore +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/__init__.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/auth.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/models.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/openrouter_proxy.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/pro_provider.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/pro_proxy.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/sse.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.53}/src/amfs_http/tenant_middleware.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: amfs-http-server
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.53
|
|
4
4
|
Summary: AMFS HTTP/REST API server with SSE support
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: amfs-core>=0.3.
|
|
8
|
-
Requires-Dist: amfs>=0.5.
|
|
7
|
+
Requires-Dist: amfs-core>=0.3.44
|
|
8
|
+
Requires-Dist: amfs>=0.5.36
|
|
9
9
|
Requires-Dist: fastapi>=0.115
|
|
10
10
|
Requires-Dist: httpx>=0.27
|
|
11
11
|
Requires-Dist: sse-starlette>=2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "amfs-http-server"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.53"
|
|
4
4
|
description = "AMFS HTTP/REST API server with SSE support"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
# this tree is what test_dependency_floors forbids.
|
|
14
14
|
# 0.5.18 for AgentMemory.as_agent, which the agent detail page calls to list
|
|
15
15
|
# an agent's entries as that agent rather than as the server.
|
|
16
|
-
"amfs>=0.5.
|
|
16
|
+
"amfs>=0.5.36",
|
|
17
17
|
# 0.3.23 is real in the silent way: the write path calls
|
|
18
18
|
# adapter.scope_counts(), which is a default on AdapterABC from 0.3.23. On an
|
|
19
19
|
# older core the adapters that do not override it — filesystem, S3 — have no
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
# unexpected keyword, _scope_block swallows it, and the block disappears —
|
|
28
28
|
# while an adapter that ignored the argument instead would name another
|
|
29
29
|
# agent's private keys. Either way the floor is what makes the rule real.
|
|
30
|
-
"amfs-core>=0.3.
|
|
30
|
+
"amfs-core>=0.3.44",
|
|
31
31
|
"fastapi>=0.115",
|
|
32
32
|
# 0.37 is where uvicorn.run() grew timeout_worker_healthcheck, which
|
|
33
33
|
# main() passes unconditionally; on anything older that is a TypeError
|
|
@@ -30,7 +30,7 @@ import time
|
|
|
30
30
|
from concurrent.futures import ThreadPoolExecutor
|
|
31
31
|
from contextlib import asynccontextmanager
|
|
32
32
|
from datetime import UTC, datetime, timedelta, timezone
|
|
33
|
-
from collections.abc import Mapping, Sequence
|
|
33
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
34
34
|
from typing import Any
|
|
35
35
|
|
|
36
36
|
import uvicorn
|
|
@@ -50,7 +50,7 @@ from amfs_core.aggregates import (
|
|
|
50
50
|
)
|
|
51
51
|
from amfs_core.ranking import composite_score
|
|
52
52
|
from amfs_core.ranking import entry_text as _entry_text
|
|
53
|
-
from amfs_core.scope import SqlScope
|
|
53
|
+
from amfs_core.scope import SqlScope, covers
|
|
54
54
|
from amfs_core.ranking import keyword_coverage as _keyword_coverage
|
|
55
55
|
from amfs_core.reuse_value import REUSE_VALUE_HEADER, reuse_value_block
|
|
56
56
|
from amfs_core.capture import scan_captured_arguments, scan_captured_text
|
|
@@ -962,6 +962,112 @@ def _search_sync(adapter: Any, query: SearchQuery, branch: Any) -> list[MemoryEn
|
|
|
962
962
|
return adapter.search(query)
|
|
963
963
|
|
|
964
964
|
|
|
965
|
+
async def _branch_treatment(
|
|
966
|
+
*,
|
|
967
|
+
branch: str,
|
|
968
|
+
entity_path: str | None,
|
|
969
|
+
topical: str,
|
|
970
|
+
query_vector: list[float] | None,
|
|
971
|
+
embedder: Any,
|
|
972
|
+
pool: int,
|
|
973
|
+
vis: Any,
|
|
974
|
+
include_artifacts: bool,
|
|
975
|
+
is_artifact: Callable[[MemoryEntry], bool],
|
|
976
|
+
) -> list[tuple[MemoryEntry, float, float]]:
|
|
977
|
+
"""The entries *branch* changed under *entity_path* that are about *topical*.
|
|
978
|
+
|
|
979
|
+
Retrieve's step 9b: what a read routed to a repair branch exists to put in
|
|
980
|
+
front of the agent. Returns ``(entry, similarity, keyword)`` triples for
|
|
981
|
+
the branch's added or modified entries that the query's semantic pool on
|
|
982
|
+
the branch holds (no confidence gate, no evidence gate), or that a lexical
|
|
983
|
+
read on the branch returns where there are no vectors. Empty on ``main``,
|
|
984
|
+
with no scope, on an adapter without branches, when the branch changed
|
|
985
|
+
nothing under the scope, or when the query is about none of it. Never
|
|
986
|
+
raises: a failure here leaves the ranking as it was.
|
|
987
|
+
"""
|
|
988
|
+
if not isinstance(branch, str) or branch.strip() in ("", "main") or not entity_path:
|
|
989
|
+
return []
|
|
990
|
+
mem = _get_memory()
|
|
991
|
+
diff_fn = getattr(mem._adapter, "diff_branch", None)
|
|
992
|
+
if not callable(diff_fn):
|
|
993
|
+
return []
|
|
994
|
+
namespace = getattr(mem._adapter, "_namespace", None) or "default"
|
|
995
|
+
try:
|
|
996
|
+
diff = await _offload(_db_executor, diff_fn, branch, namespace=namespace)
|
|
997
|
+
except Exception: # noqa: BLE001 - best-effort
|
|
998
|
+
logger.debug("branch diff for retrieve failed", exc_info=True)
|
|
999
|
+
return []
|
|
1000
|
+
changed: set[tuple[str, str]] = {
|
|
1001
|
+
(d.entity_path, d.key)
|
|
1002
|
+
for d in diff or []
|
|
1003
|
+
if getattr(d, "diff_type", "") in ("added", "modified")
|
|
1004
|
+
and covers(entity_path, d.entity_path)
|
|
1005
|
+
and not _is_synthetic_key(d.key)
|
|
1006
|
+
and not _is_excluded_entity(d.entity_path)
|
|
1007
|
+
}
|
|
1008
|
+
if not changed:
|
|
1009
|
+
return []
|
|
1010
|
+
|
|
1011
|
+
# Which of them the query is about, read on the branch with no gate: the
|
|
1012
|
+
# semantic pool where vectors exist, the lexical channel where they do not.
|
|
1013
|
+
found: dict[tuple[str, str], tuple[MemoryEntry, float, float]] = {}
|
|
1014
|
+
if embedder is not None and _async_adapter is not None:
|
|
1015
|
+
try:
|
|
1016
|
+
pairs = await _async_adapter.semantic_search(
|
|
1017
|
+
SemanticQuery(
|
|
1018
|
+
text=topical, entity_path=entity_path, min_confidence=0.0,
|
|
1019
|
+
limit=pool, embedding=query_vector,
|
|
1020
|
+
),
|
|
1021
|
+
embedder,
|
|
1022
|
+
branch=branch,
|
|
1023
|
+
)
|
|
1024
|
+
except Exception: # noqa: BLE001
|
|
1025
|
+
logger.debug("branch semantic read for retrieve failed", exc_info=True)
|
|
1026
|
+
pairs = []
|
|
1027
|
+
floor = _retrieve_min_semantic()
|
|
1028
|
+
for entry, sim in pairs:
|
|
1029
|
+
ref = (entry.entity_path, entry.key)
|
|
1030
|
+
if ref in changed and float(sim) > 0.0 and float(sim) >= floor:
|
|
1031
|
+
found[ref] = (entry, float(sim), 0.0)
|
|
1032
|
+
lex = SearchQuery(
|
|
1033
|
+
query=topical, entity_path=entity_path, min_confidence=0.0, limit=pool,
|
|
1034
|
+
sort_by="confidence", depth=3, include_artifacts=include_artifacts,
|
|
1035
|
+
)
|
|
1036
|
+
try:
|
|
1037
|
+
if _async_adapter is not None:
|
|
1038
|
+
rows = await _async_adapter.search(lex, branch=branch)
|
|
1039
|
+
else:
|
|
1040
|
+
rows = await _offload(_db_executor, _search_sync, mem._adapter, lex, branch)
|
|
1041
|
+
except Exception: # noqa: BLE001
|
|
1042
|
+
logger.debug("branch lexical read for retrieve failed", exc_info=True)
|
|
1043
|
+
rows = []
|
|
1044
|
+
for entry in rows:
|
|
1045
|
+
ref = (entry.entity_path, entry.key)
|
|
1046
|
+
if ref not in changed:
|
|
1047
|
+
continue
|
|
1048
|
+
slot = found.get(ref)
|
|
1049
|
+
found[ref] = (entry, slot[1], 1.0) if slot else (entry, 0.0, 1.0)
|
|
1050
|
+
|
|
1051
|
+
# Discredited or not: the branch's own changes are pinned whatever their
|
|
1052
|
+
# evidence says. The pin exists so the arm under test is *treated*; the
|
|
1053
|
+
# canary is what decides whether the treatment is good, and it can only
|
|
1054
|
+
# decide if the arm read it. Dropping a discredited entry here handed the
|
|
1055
|
+
# verdict to whatever had discredited it first — on clbench (2026-09-30,
|
|
1056
|
+
# fleet 10) the loop's own replays, within minutes of the write, before
|
|
1057
|
+
# the canary's first session in two of six cases; the arm then read its
|
|
1058
|
+
# fix in 0 of 74 sessions and one canary *failed* on an arm that had never
|
|
1059
|
+
# seen the treatment. The row keeps its ``evidence_status``, so the agent
|
|
1060
|
+
# sees the record; it is not hidden from the arm on the record's account.
|
|
1061
|
+
out = [
|
|
1062
|
+
t for t in found.values()
|
|
1063
|
+
if include_artifacts or not is_artifact(t[0])
|
|
1064
|
+
]
|
|
1065
|
+
if out and vis is not None and vis.should_filter():
|
|
1066
|
+
allowed = {e.entry_key for e in vis.filter_entries([t[0] for t in out])}
|
|
1067
|
+
out = [t for t in out if t[0].entry_key in allowed]
|
|
1068
|
+
return out
|
|
1069
|
+
|
|
1070
|
+
|
|
965
1071
|
async def _discredited_below_gate(
|
|
966
1072
|
entity_path: str,
|
|
967
1073
|
*,
|
|
@@ -3325,6 +3431,103 @@ async def retrieve_entries(
|
|
|
3325
3431
|
kept.append((entry, score, bd))
|
|
3326
3432
|
scored = kept
|
|
3327
3433
|
|
|
3434
|
+
# 9b. The branch under test. A read routed to a repair branch is the
|
|
3435
|
+
# canary arm of a fix: the branch exists to put that fix in front of
|
|
3436
|
+
# the agent, and the entries it changed are the treatment. Ranked
|
|
3437
|
+
# like any other entry they do not arrive. The repair loop writes a
|
|
3438
|
+
# correction at 60% of the confidence of the rule it corrects — under
|
|
3439
|
+
# the caller's gate as often as not — onto a scope where outcome
|
|
3440
|
+
# learning has already written dozens of validated near-duplicates
|
|
3441
|
+
# of the stale rule, and confidence is a ranking term. Measured on
|
|
3442
|
+
# clbench (2026-09-30): 18 canaries, 14-197 branch reads each, zero
|
|
3443
|
+
# reads of the fix's own entry. Both arms read the same rules, the
|
|
3444
|
+
# canary compared two untreated populations, and 0 vs 0 recurrence
|
|
3445
|
+
# said nothing about the fix.
|
|
3446
|
+
#
|
|
3447
|
+
# So the branch's changed entries that are *about this query* lead
|
|
3448
|
+
# the list, in their own order, marked ``_pinned`` so the client can
|
|
3449
|
+
# see why. "About this query" is the semantic pool on the branch with
|
|
3450
|
+
# no confidence gate (the fix's confidence is the repair loop's
|
|
3451
|
+
# business, not a reason to hide the treatment from its own arm), or
|
|
3452
|
+
# a lexical hit where there are no vectors; an entry the query is not
|
|
3453
|
+
# about stays where it is — a fix to the CI rule is not pinned onto a
|
|
3454
|
+
# support ticket. A discredited entry is pinned all the same, with its
|
|
3455
|
+
# evidence on the row: the canary is the test of the fix, and it can
|
|
3456
|
+
# only test what the arm read. The first version of this step left
|
|
3457
|
+
# discredited entries to the avoid list; measured on clbench
|
|
3458
|
+
# (2026-09-30, fleet 10, six canaries), five corrections were
|
|
3459
|
+
# discredited within 1-24 minutes of being written — by the loop's
|
|
3460
|
+
# own replays, two before the canary's first session — and the arm
|
|
3461
|
+
# they were meant to treat read them in 0 of 74 sessions. What is not
|
|
3462
|
+
# pinned is a procedure whose preconditions contradict the run's
|
|
3463
|
+
# environment — it is named in ``_meta.not_applicable`` like any
|
|
3464
|
+
# other. Main reads are untouched.
|
|
3465
|
+
pinned = await _branch_treatment(
|
|
3466
|
+
branch=branch,
|
|
3467
|
+
entity_path=req.entity_path,
|
|
3468
|
+
topical=topical,
|
|
3469
|
+
query_vector=query_vectors.get(topical),
|
|
3470
|
+
embedder=embedder,
|
|
3471
|
+
pool=pool,
|
|
3472
|
+
vis=vis,
|
|
3473
|
+
include_artifacts=req.include_artifacts,
|
|
3474
|
+
is_artifact=_is_artifact,
|
|
3475
|
+
)
|
|
3476
|
+
if pinned:
|
|
3477
|
+
if environment:
|
|
3478
|
+
from amfs_core.models import preconditions_status as _preconditions_status
|
|
3479
|
+
|
|
3480
|
+
# Named once: a pinned procedure above the caller's gate was a
|
|
3481
|
+
# candidate too, and step 9 has already recorded it.
|
|
3482
|
+
named = {(row["entity_path"], row["key"]) for row in not_applicable}
|
|
3483
|
+
applicable: list[tuple[MemoryEntry, float, float]] = []
|
|
3484
|
+
for entry, sim, keyword in pinned:
|
|
3485
|
+
if _is_procedure(entry):
|
|
3486
|
+
status, detail = _preconditions_status(entry.value, environment)
|
|
3487
|
+
if status == "not_applicable":
|
|
3488
|
+
if (entry.entity_path, entry.key) not in named:
|
|
3489
|
+
not_applicable.append({
|
|
3490
|
+
"entity_path": entry.entity_path, "key": entry.key, "why": detail,
|
|
3491
|
+
})
|
|
3492
|
+
continue
|
|
3493
|
+
applicable.append((entry, sim, keyword))
|
|
3494
|
+
pinned = applicable
|
|
3495
|
+
if pinned:
|
|
3496
|
+
pinned_scored: list[tuple[MemoryEntry, float, dict[str, Any]]] = []
|
|
3497
|
+
for entry, sim, keyword in pinned:
|
|
3498
|
+
written = getattr(entry.provenance, "written_at", None)
|
|
3499
|
+
if written is not None:
|
|
3500
|
+
if written.tzinfo is None:
|
|
3501
|
+
written = written.replace(tzinfo=UTC)
|
|
3502
|
+
recency = 0.5 ** (max(0.0, (now - written).total_seconds() / 86400.0) / half_life)
|
|
3503
|
+
else:
|
|
3504
|
+
recency = 0.0
|
|
3505
|
+
artifact = _is_artifact(entry)
|
|
3506
|
+
procedure = _is_procedure(entry)
|
|
3507
|
+
evidence = _evidence_signal(entry)
|
|
3508
|
+
bd = {
|
|
3509
|
+
"semantic": sim,
|
|
3510
|
+
"relevance": req.semantic_weight * sim + keyword_weight * keyword,
|
|
3511
|
+
"recency": recency,
|
|
3512
|
+
"confidence": float(entry.confidence),
|
|
3513
|
+
"keyword": keyword,
|
|
3514
|
+
"evidence": evidence,
|
|
3515
|
+
"evidence_status": entry.evidence_status,
|
|
3516
|
+
"is_artifact": artifact,
|
|
3517
|
+
"is_procedure": procedure,
|
|
3518
|
+
"pinned": branch,
|
|
3519
|
+
}
|
|
3520
|
+
conf = float(entry.confidence)
|
|
3521
|
+
pinned_scored.append((
|
|
3522
|
+
entry, _composite(sim, recency, conf, keyword, artifact, evidence, procedure), bd,
|
|
3523
|
+
))
|
|
3524
|
+
pinned_scored.sort(key=lambda t: t[1], reverse=True)
|
|
3525
|
+
pinned_keys = {e.entry_key for e, _, _ in pinned_scored}
|
|
3526
|
+
scored = pinned_scored + [t for t in scored if t[0].entry_key not in pinned_keys]
|
|
3527
|
+
# Once, at the head: a pinned entry the record has discredited is not
|
|
3528
|
+
# also handed back in the avoid list. The row carries its evidence.
|
|
3529
|
+
avoided = [e for e in avoided if e.entry_key not in pinned_keys]
|
|
3530
|
+
|
|
3328
3531
|
# 10. Reuse accounting. Semantic retrieval is the primary way memories
|
|
3329
3532
|
# (e.g. browser-extension clips) get surfaced and used, but it
|
|
3330
3533
|
# historically incremented no counter — so value metrics (rework
|
|
@@ -3620,6 +3823,10 @@ async def retrieve_entries(
|
|
|
3620
3823
|
# agent reads the top-level field.
|
|
3621
3824
|
data["evidence_status"] = breakdown.get("evidence_status")
|
|
3622
3825
|
data["_rescued"] = True
|
|
3826
|
+
if breakdown.get("pinned"):
|
|
3827
|
+
# Changed on the branch this read was routed to (step 9b): the
|
|
3828
|
+
# treatment of the canary arm, ahead of the ranking on purpose.
|
|
3829
|
+
data["_pinned"] = True
|
|
3623
3830
|
if req.compact:
|
|
3624
3831
|
data["_breakdown"] = {"evidence_status": breakdown.get("evidence_status")}
|
|
3625
3832
|
else:
|
|
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
|