amfs-http-server 0.3.51__tar.gz → 0.3.52__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.52}/PKG-INFO +1 -1
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/pyproject.toml +1 -1
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/server.py +191 -2
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/.gitignore +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/__init__.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/auth.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/models.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/openrouter_proxy.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/pro_provider.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/pro_proxy.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/sse.py +0 -0
- {amfs_http_server-0.3.51 → amfs_http_server-0.3.52}/src/amfs_http/tenant_middleware.py +0 -0
|
@@ -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,103 @@ 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), or that a lexical read on the
|
|
983
|
+
branch returns where there are no vectors. Empty on ``main``, with no
|
|
984
|
+
scope, on an adapter without branches, when the branch changed nothing
|
|
985
|
+
under the scope, or when the query is about none of it. Never raises:
|
|
986
|
+
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
|
+
out = [
|
|
1052
|
+
t for t in found.values()
|
|
1053
|
+
if getattr(t[0], "discredited_at", None) is None
|
|
1054
|
+
and (include_artifacts or not is_artifact(t[0]))
|
|
1055
|
+
]
|
|
1056
|
+
if out and vis is not None and vis.should_filter():
|
|
1057
|
+
allowed = {e.entry_key for e in vis.filter_entries([t[0] for t in out])}
|
|
1058
|
+
out = [t for t in out if t[0].entry_key in allowed]
|
|
1059
|
+
return out
|
|
1060
|
+
|
|
1061
|
+
|
|
965
1062
|
async def _discredited_below_gate(
|
|
966
1063
|
entity_path: str,
|
|
967
1064
|
*,
|
|
@@ -3325,6 +3422,94 @@ async def retrieve_entries(
|
|
|
3325
3422
|
kept.append((entry, score, bd))
|
|
3326
3423
|
scored = kept
|
|
3327
3424
|
|
|
3425
|
+
# 9b. The branch under test. A read routed to a repair branch is the
|
|
3426
|
+
# canary arm of a fix: the branch exists to put that fix in front of
|
|
3427
|
+
# the agent, and the entries it changed are the treatment. Ranked
|
|
3428
|
+
# like any other entry they do not arrive. The repair loop writes a
|
|
3429
|
+
# correction at 60% of the confidence of the rule it corrects — under
|
|
3430
|
+
# the caller's gate as often as not — onto a scope where outcome
|
|
3431
|
+
# learning has already written dozens of validated near-duplicates
|
|
3432
|
+
# of the stale rule, and confidence is a ranking term. Measured on
|
|
3433
|
+
# clbench (2026-09-30): 18 canaries, 14-197 branch reads each, zero
|
|
3434
|
+
# reads of the fix's own entry. Both arms read the same rules, the
|
|
3435
|
+
# canary compared two untreated populations, and 0 vs 0 recurrence
|
|
3436
|
+
# said nothing about the fix.
|
|
3437
|
+
#
|
|
3438
|
+
# So the branch's changed entries that are *about this query* lead
|
|
3439
|
+
# the list, in their own order, marked ``_pinned`` so the client can
|
|
3440
|
+
# see why. "About this query" is the semantic pool on the branch with
|
|
3441
|
+
# no confidence gate (the fix's confidence is the repair loop's
|
|
3442
|
+
# business, not a reason to hide the treatment from its own arm), or
|
|
3443
|
+
# a lexical hit where there are no vectors; an entry the query is not
|
|
3444
|
+
# about stays where it is — a fix to the CI rule is not pinned onto a
|
|
3445
|
+
# support ticket. Discredited entries are not pinned: a fix the record
|
|
3446
|
+
# has already failed is the avoid list's, not the head's; nor is a
|
|
3447
|
+
# procedure whose preconditions contradict the run's environment —
|
|
3448
|
+
# it is named in ``_meta.not_applicable`` like any other. Main reads
|
|
3449
|
+
# are untouched.
|
|
3450
|
+
pinned = await _branch_treatment(
|
|
3451
|
+
branch=branch,
|
|
3452
|
+
entity_path=req.entity_path,
|
|
3453
|
+
topical=topical,
|
|
3454
|
+
query_vector=query_vectors.get(topical),
|
|
3455
|
+
embedder=embedder,
|
|
3456
|
+
pool=pool,
|
|
3457
|
+
vis=vis,
|
|
3458
|
+
include_artifacts=req.include_artifacts,
|
|
3459
|
+
is_artifact=_is_artifact,
|
|
3460
|
+
)
|
|
3461
|
+
if pinned:
|
|
3462
|
+
if environment:
|
|
3463
|
+
from amfs_core.models import preconditions_status as _preconditions_status
|
|
3464
|
+
|
|
3465
|
+
# Named once: a pinned procedure above the caller's gate was a
|
|
3466
|
+
# candidate too, and step 9 has already recorded it.
|
|
3467
|
+
named = {(row["entity_path"], row["key"]) for row in not_applicable}
|
|
3468
|
+
applicable: list[tuple[MemoryEntry, float, float]] = []
|
|
3469
|
+
for entry, sim, keyword in pinned:
|
|
3470
|
+
if _is_procedure(entry):
|
|
3471
|
+
status, detail = _preconditions_status(entry.value, environment)
|
|
3472
|
+
if status == "not_applicable":
|
|
3473
|
+
if (entry.entity_path, entry.key) not in named:
|
|
3474
|
+
not_applicable.append({
|
|
3475
|
+
"entity_path": entry.entity_path, "key": entry.key, "why": detail,
|
|
3476
|
+
})
|
|
3477
|
+
continue
|
|
3478
|
+
applicable.append((entry, sim, keyword))
|
|
3479
|
+
pinned = applicable
|
|
3480
|
+
if pinned:
|
|
3481
|
+
pinned_scored: list[tuple[MemoryEntry, float, dict[str, Any]]] = []
|
|
3482
|
+
for entry, sim, keyword in pinned:
|
|
3483
|
+
written = getattr(entry.provenance, "written_at", None)
|
|
3484
|
+
if written is not None:
|
|
3485
|
+
if written.tzinfo is None:
|
|
3486
|
+
written = written.replace(tzinfo=UTC)
|
|
3487
|
+
recency = 0.5 ** (max(0.0, (now - written).total_seconds() / 86400.0) / half_life)
|
|
3488
|
+
else:
|
|
3489
|
+
recency = 0.0
|
|
3490
|
+
artifact = _is_artifact(entry)
|
|
3491
|
+
procedure = _is_procedure(entry)
|
|
3492
|
+
evidence = _evidence_signal(entry)
|
|
3493
|
+
bd = {
|
|
3494
|
+
"semantic": sim,
|
|
3495
|
+
"relevance": req.semantic_weight * sim + keyword_weight * keyword,
|
|
3496
|
+
"recency": recency,
|
|
3497
|
+
"confidence": float(entry.confidence),
|
|
3498
|
+
"keyword": keyword,
|
|
3499
|
+
"evidence": evidence,
|
|
3500
|
+
"evidence_status": entry.evidence_status,
|
|
3501
|
+
"is_artifact": artifact,
|
|
3502
|
+
"is_procedure": procedure,
|
|
3503
|
+
"pinned": branch,
|
|
3504
|
+
}
|
|
3505
|
+
conf = float(entry.confidence)
|
|
3506
|
+
pinned_scored.append((
|
|
3507
|
+
entry, _composite(sim, recency, conf, keyword, artifact, evidence, procedure), bd,
|
|
3508
|
+
))
|
|
3509
|
+
pinned_scored.sort(key=lambda t: t[1], reverse=True)
|
|
3510
|
+
pinned_keys = {e.entry_key for e, _, _ in pinned_scored}
|
|
3511
|
+
scored = pinned_scored + [t for t in scored if t[0].entry_key not in pinned_keys]
|
|
3512
|
+
|
|
3328
3513
|
# 10. Reuse accounting. Semantic retrieval is the primary way memories
|
|
3329
3514
|
# (e.g. browser-extension clips) get surfaced and used, but it
|
|
3330
3515
|
# historically incremented no counter — so value metrics (rework
|
|
@@ -3620,6 +3805,10 @@ async def retrieve_entries(
|
|
|
3620
3805
|
# agent reads the top-level field.
|
|
3621
3806
|
data["evidence_status"] = breakdown.get("evidence_status")
|
|
3622
3807
|
data["_rescued"] = True
|
|
3808
|
+
if breakdown.get("pinned"):
|
|
3809
|
+
# Changed on the branch this read was routed to (step 9b): the
|
|
3810
|
+
# treatment of the canary arm, ahead of the ranking on purpose.
|
|
3811
|
+
data["_pinned"] = True
|
|
3623
3812
|
if req.compact:
|
|
3624
3813
|
data["_breakdown"] = {"evidence_status": breakdown.get("evidence_status")}
|
|
3625
3814
|
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
|