amfs-http-server 0.3.42__tar.gz → 0.3.44__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.42 → amfs_http_server-0.3.44}/PKG-INFO +3 -3
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/pyproject.toml +3 -3
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/server.py +198 -172
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/.gitignore +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/__init__.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/auth.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/models.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/openrouter_proxy.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/pro_provider.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/pro_proxy.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/src/amfs_http/sse.py +0 -0
- {amfs_http_server-0.3.42 → amfs_http_server-0.3.44}/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.44
|
|
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.38
|
|
8
|
+
Requires-Dist: amfs>=0.5.29
|
|
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.44"
|
|
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.29",
|
|
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.38",
|
|
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
|
|
@@ -1610,7 +1610,7 @@ async def health_v1() -> dict[str, str]:
|
|
|
1610
1610
|
|
|
1611
1611
|
|
|
1612
1612
|
@app.get("/api/v1/auth/whoami")
|
|
1613
|
-
|
|
1613
|
+
def whoami(
|
|
1614
1614
|
request: Request,
|
|
1615
1615
|
_auth: str | None = Depends(verify_api_key),
|
|
1616
1616
|
) -> dict[str, Any]:
|
|
@@ -2304,7 +2304,7 @@ async def list_entity_summaries(
|
|
|
2304
2304
|
|
|
2305
2305
|
|
|
2306
2306
|
@app.post("/api/v1/aggregate")
|
|
2307
|
-
|
|
2307
|
+
def aggregate_entries_endpoint(
|
|
2308
2308
|
request: Request,
|
|
2309
2309
|
req: AggregateRequest,
|
|
2310
2310
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -3545,7 +3545,7 @@ async def get_stats(
|
|
|
3545
3545
|
|
|
3546
3546
|
|
|
3547
3547
|
@app.post("/api/v1/verify")
|
|
3548
|
-
|
|
3548
|
+
def verify_integrity(
|
|
3549
3549
|
request: Request,
|
|
3550
3550
|
body: dict[str, Any] = {},
|
|
3551
3551
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -3576,7 +3576,7 @@ _COMMIT_WRITE_OPTIONS = ("confidence", "memory_type", "pattern_refs", "shared")
|
|
|
3576
3576
|
|
|
3577
3577
|
|
|
3578
3578
|
@app.post("/api/v1/commits")
|
|
3579
|
-
|
|
3579
|
+
def create_commit(
|
|
3580
3580
|
body: dict[str, Any],
|
|
3581
3581
|
request: Request,
|
|
3582
3582
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -3640,56 +3640,52 @@ async def create_commit(
|
|
|
3640
3640
|
# from the moment this deploys. The body wins when both are present.
|
|
3641
3641
|
agent_id = body.get("agent_id") or request.headers.get("x-amfs-agent-id")
|
|
3642
3642
|
session_id = body.get("session_id")
|
|
3643
|
-
original_agent = mem._tagger.agent_id if agent_id else None
|
|
3644
|
-
original_session = mem._tagger.session_id if session_id else None
|
|
3645
3643
|
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
# otherwise fail on a name nothing has registered.
|
|
3654
|
-
mem._adapter.ensure_agent(agent_id, mem.namespace)
|
|
3655
|
-
except Exception: # noqa: BLE001
|
|
3656
|
-
logger.warning(
|
|
3657
|
-
"ensure_agent failed for %s — committing anyway", agent_id,
|
|
3658
|
-
exc_info=True,
|
|
3659
|
-
)
|
|
3660
|
-
_link_agent_owner_once(request, agent_id, mem.namespace)
|
|
3644
|
+
# A per-request handle carries the caller's identity (``AgentMemory.as_agent``).
|
|
3645
|
+
# Swapping the shared tagger and restoring it in a ``finally`` was atomic
|
|
3646
|
+
# only while this body ran on the event loop; on the threadpool two commits
|
|
3647
|
+
# would stamp each other's writes.
|
|
3648
|
+
handle = mem
|
|
3649
|
+
if agent_id or session_id:
|
|
3650
|
+
handle = mem.as_agent(agent_id or mem.agent_id)
|
|
3661
3651
|
if session_id:
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3652
|
+
handle._tagger.session_id = session_id
|
|
3653
|
+
|
|
3654
|
+
if agent_id:
|
|
3655
|
+
try:
|
|
3656
|
+
# The gateway ensures its agent when the session opens, so this
|
|
3657
|
+
# is normally a no-op. It is here for a client committing as an
|
|
3658
|
+
# agent this server has not seen, where the entry insert would
|
|
3659
|
+
# otherwise fail on a name nothing has registered.
|
|
3660
|
+
mem._adapter.ensure_agent(agent_id, mem.namespace)
|
|
3661
|
+
except Exception: # noqa: BLE001
|
|
3662
|
+
logger.warning(
|
|
3663
|
+
"ensure_agent failed for %s — committing anyway", agent_id,
|
|
3664
|
+
exc_info=True,
|
|
3665
|
+
)
|
|
3666
|
+
_link_agent_owner_once(request, agent_id, mem.namespace)
|
|
3667
|
+
|
|
3668
|
+
with handle.transaction(message) as tx:
|
|
3669
|
+
for w in writes:
|
|
3670
|
+
options = {k: w[k] for k in _COMMIT_WRITE_OPTIONS if k in w}
|
|
3671
|
+
# Arrives over the wire as a string, and the write path wants
|
|
3672
|
+
# the enum. An unknown one is refused rather than dropped:
|
|
3673
|
+
# writing the entry with a default type would put the wrong
|
|
3674
|
+
# metadata on the right memory, and nothing later can tell that
|
|
3675
|
+
# happened.
|
|
3676
|
+
if "memory_type" in options:
|
|
3677
|
+
raw = str(options["memory_type"])
|
|
3678
|
+
try:
|
|
3679
|
+
options["memory_type"] = MemoryType(raw)
|
|
3680
|
+
except ValueError as exc:
|
|
3681
|
+
raise HTTPException(
|
|
3682
|
+
status_code=422,
|
|
3683
|
+
detail=(
|
|
3684
|
+
f"Invalid memory_type {raw!r}. Valid: "
|
|
3685
|
+
+ ", ".join(m.value for m in MemoryType)
|
|
3686
|
+
),
|
|
3687
|
+
) from exc
|
|
3688
|
+
tx.write(w["entity_path"], w["key"], w.get("value"), **options)
|
|
3693
3689
|
|
|
3694
3690
|
commit = tx.commit
|
|
3695
3691
|
return {
|
|
@@ -3704,7 +3700,7 @@ async def create_commit(
|
|
|
3704
3700
|
|
|
3705
3701
|
|
|
3706
3702
|
@app.get("/api/v1/commits")
|
|
3707
|
-
|
|
3703
|
+
def list_commits(
|
|
3708
3704
|
request: Request,
|
|
3709
3705
|
limit: int = Query(50, ge=1, le=500),
|
|
3710
3706
|
branch: str | None = Query(None),
|
|
@@ -3752,7 +3748,7 @@ async def list_commits(
|
|
|
3752
3748
|
|
|
3753
3749
|
|
|
3754
3750
|
@app.get("/api/v1/commits/{commit_id}")
|
|
3755
|
-
|
|
3751
|
+
def get_commit(
|
|
3756
3752
|
request: Request,
|
|
3757
3753
|
commit_id: str,
|
|
3758
3754
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -3769,7 +3765,7 @@ async def get_commit(
|
|
|
3769
3765
|
|
|
3770
3766
|
|
|
3771
3767
|
@app.post("/api/v1/merge-base")
|
|
3772
|
-
|
|
3768
|
+
def merge_base(
|
|
3773
3769
|
body: dict[str, Any],
|
|
3774
3770
|
_auth: str | None = Depends(verify_api_key),
|
|
3775
3771
|
) -> dict[str, Any]:
|
|
@@ -3939,7 +3935,7 @@ def _infer_tags(
|
|
|
3939
3935
|
|
|
3940
3936
|
|
|
3941
3937
|
@app.put("/api/v1/agents/{agent_id:path}/profile")
|
|
3942
|
-
|
|
3938
|
+
def update_agent_profile(
|
|
3943
3939
|
request: Request,
|
|
3944
3940
|
agent_id: str,
|
|
3945
3941
|
body: dict[str, Any],
|
|
@@ -3960,7 +3956,7 @@ async def update_agent_profile(
|
|
|
3960
3956
|
|
|
3961
3957
|
|
|
3962
3958
|
@app.put("/api/v1/agents/{agent_id:path}/capabilities")
|
|
3963
|
-
|
|
3959
|
+
def update_agent_capabilities(
|
|
3964
3960
|
request: Request,
|
|
3965
3961
|
agent_id: str,
|
|
3966
3962
|
body: dict[str, Any],
|
|
@@ -3976,7 +3972,7 @@ async def update_agent_capabilities(
|
|
|
3976
3972
|
|
|
3977
3973
|
|
|
3978
3974
|
@app.put("/api/v1/agents/{agent_id:path}/contracts")
|
|
3979
|
-
|
|
3975
|
+
def update_agent_contracts(
|
|
3980
3976
|
request: Request,
|
|
3981
3977
|
agent_id: str,
|
|
3982
3978
|
body: dict[str, Any],
|
|
@@ -3992,7 +3988,7 @@ async def update_agent_contracts(
|
|
|
3992
3988
|
|
|
3993
3989
|
|
|
3994
3990
|
@app.get("/api/v1/agents/discover")
|
|
3995
|
-
|
|
3991
|
+
def discover_agents(
|
|
3996
3992
|
request: Request,
|
|
3997
3993
|
capability: str | None = Query(None),
|
|
3998
3994
|
entity_path: str | None = Query(None),
|
|
@@ -4027,7 +4023,7 @@ def _require_entry_visible(request: Request, entity_path: str, key: str) -> None
|
|
|
4027
4023
|
|
|
4028
4024
|
|
|
4029
4025
|
@app.post("/api/v1/diff")
|
|
4030
|
-
|
|
4026
|
+
def compute_diff(
|
|
4031
4027
|
request: Request,
|
|
4032
4028
|
body: dict[str, Any],
|
|
4033
4029
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -4042,7 +4038,7 @@ async def compute_diff(
|
|
|
4042
4038
|
|
|
4043
4039
|
|
|
4044
4040
|
@app.post("/api/v1/patches")
|
|
4045
|
-
|
|
4041
|
+
def create_patch(
|
|
4046
4042
|
request: Request,
|
|
4047
4043
|
body: dict[str, Any],
|
|
4048
4044
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -4787,7 +4783,7 @@ async def commit_outcome(
|
|
|
4787
4783
|
|
|
4788
4784
|
|
|
4789
4785
|
@app.get("/api/v1/outcomes")
|
|
4790
|
-
|
|
4786
|
+
def list_outcomes(
|
|
4791
4787
|
request: Request,
|
|
4792
4788
|
entity_path: str | None = Query(None),
|
|
4793
4789
|
since: str | None = Query(None),
|
|
@@ -4815,7 +4811,7 @@ async def list_outcomes(
|
|
|
4815
4811
|
|
|
4816
4812
|
|
|
4817
4813
|
@app.post("/api/v1/context")
|
|
4818
|
-
|
|
4814
|
+
def record_context(
|
|
4819
4815
|
req: ContextRequest,
|
|
4820
4816
|
_auth: str | None = Depends(verify_api_key),
|
|
4821
4817
|
) -> dict[str, Any]:
|
|
@@ -4825,7 +4821,7 @@ async def record_context(
|
|
|
4825
4821
|
|
|
4826
4822
|
|
|
4827
4823
|
@app.get("/api/v1/explain")
|
|
4828
|
-
|
|
4824
|
+
def explain(
|
|
4829
4825
|
request: Request,
|
|
4830
4826
|
outcome_ref: str | None = Query(None),
|
|
4831
4827
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -4997,7 +4993,7 @@ async def save_trace(
|
|
|
4997
4993
|
# NOTE: must be registered before /api/v1/traces/{trace_id} so "share-stats"
|
|
4998
4994
|
# isn't captured as a trace_id.
|
|
4999
4995
|
@app.get("/api/v1/traces/share-stats")
|
|
5000
|
-
|
|
4996
|
+
def get_share_stats(
|
|
5001
4997
|
request: Request,
|
|
5002
4998
|
since: datetime | None = Query(None),
|
|
5003
4999
|
pair_limit: int = Query(20, ge=1, le=100),
|
|
@@ -5034,7 +5030,7 @@ async def get_trace(
|
|
|
5034
5030
|
|
|
5035
5031
|
|
|
5036
5032
|
@app.post("/api/v1/traces/{trace_id}/explain")
|
|
5037
|
-
|
|
5033
|
+
def explain_trace(
|
|
5038
5034
|
request: Request,
|
|
5039
5035
|
trace_id: str,
|
|
5040
5036
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -5442,7 +5438,7 @@ async def list_agents(
|
|
|
5442
5438
|
|
|
5443
5439
|
|
|
5444
5440
|
@app.get("/api/v1/reuse")
|
|
5445
|
-
|
|
5441
|
+
def reuse_summary(
|
|
5446
5442
|
request: Request,
|
|
5447
5443
|
days: int = 7,
|
|
5448
5444
|
limit: int = 10,
|
|
@@ -5519,7 +5515,7 @@ async def reuse_summary(
|
|
|
5519
5515
|
|
|
5520
5516
|
|
|
5521
5517
|
@app.get("/api/v1/agents/{agent_id:path}/memory-graph")
|
|
5522
|
-
|
|
5518
|
+
def agent_memory_graph(
|
|
5523
5519
|
request: Request,
|
|
5524
5520
|
agent_id: str,
|
|
5525
5521
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -5690,7 +5686,7 @@ async def agent_cross_reads(
|
|
|
5690
5686
|
|
|
5691
5687
|
|
|
5692
5688
|
@app.get("/api/v1/agents/{agent_id:path}/recall/{entity_path:path}/{key}")
|
|
5693
|
-
|
|
5689
|
+
def agent_recall(
|
|
5694
5690
|
request: Request,
|
|
5695
5691
|
agent_id: str,
|
|
5696
5692
|
entity_path: str,
|
|
@@ -5706,13 +5702,10 @@ async def agent_recall(
|
|
|
5706
5702
|
if vis is not None and vis.should_filter() and not vis.is_agent_visible(agent_id):
|
|
5707
5703
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
5708
5704
|
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
entry = mem.recall(entity_path, key)
|
|
5714
|
-
finally:
|
|
5715
|
-
mem._tagger.agent_id = original_agent
|
|
5705
|
+
# Acting as the agent through a per-request handle, not by swapping the
|
|
5706
|
+
# shared tagger: these routes run on the threadpool now, and a swap there
|
|
5707
|
+
# is a race (``AgentMemory.as_agent``).
|
|
5708
|
+
entry = _get_memory().as_agent(agent_id).recall(entity_path, key)
|
|
5716
5709
|
if entry is None:
|
|
5717
5710
|
return {"status": "not_found", "agentId": agent_id,
|
|
5718
5711
|
"entityPath": entity_path, "key": key}
|
|
@@ -5720,7 +5713,7 @@ async def agent_recall(
|
|
|
5720
5713
|
|
|
5721
5714
|
|
|
5722
5715
|
@app.get("/api/v1/agents/{agent_id:path}/entries")
|
|
5723
|
-
|
|
5716
|
+
def agent_entries(
|
|
5724
5717
|
request: Request,
|
|
5725
5718
|
agent_id: str,
|
|
5726
5719
|
entity_path: str | None = Query(None),
|
|
@@ -5741,7 +5734,7 @@ async def agent_entries(
|
|
|
5741
5734
|
|
|
5742
5735
|
|
|
5743
5736
|
@app.get("/api/v1/agents/{agent_id:path}/read-from/{source_agent_id}/{entity_path:path}/{key}")
|
|
5744
|
-
|
|
5737
|
+
def agent_read_from(
|
|
5745
5738
|
request: Request,
|
|
5746
5739
|
agent_id: str,
|
|
5747
5740
|
source_agent_id: str,
|
|
@@ -5764,15 +5757,8 @@ async def agent_read_from(
|
|
|
5764
5757
|
# from another — and that edge is what the authority ranking is built on,
|
|
5765
5758
|
# so a cross-agent read over HTTP never counted for anything.
|
|
5766
5759
|
#
|
|
5767
|
-
#
|
|
5768
|
-
|
|
5769
|
-
# tagger left holding one caller's name would sign the next caller's reads.
|
|
5770
|
-
original_agent = mem._tagger.agent_id
|
|
5771
|
-
mem._tagger.agent_id = agent_id
|
|
5772
|
-
try:
|
|
5773
|
-
entry = mem.read_from(source_agent_id, entity_path, key)
|
|
5774
|
-
finally:
|
|
5775
|
-
mem._tagger.agent_id = original_agent
|
|
5760
|
+
# A per-request handle attributes the read to the caller (see agent_recall).
|
|
5761
|
+
entry = mem.as_agent(agent_id).read_from(source_agent_id, entity_path, key)
|
|
5776
5762
|
|
|
5777
5763
|
if entry is None:
|
|
5778
5764
|
return {"status": "not_found", "sourceAgentId": source_agent_id,
|
|
@@ -5973,7 +5959,7 @@ class LogEventRequest(BaseModel):
|
|
|
5973
5959
|
|
|
5974
5960
|
|
|
5975
5961
|
@app.post("/api/v1/timeline/events")
|
|
5976
|
-
|
|
5962
|
+
def log_timeline_event(
|
|
5977
5963
|
request: Request,
|
|
5978
5964
|
body: LogEventRequest,
|
|
5979
5965
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6024,7 +6010,7 @@ class UpsertGraphEdgeRequest(BaseModel):
|
|
|
6024
6010
|
|
|
6025
6011
|
|
|
6026
6012
|
@app.post("/api/v1/graph/edges")
|
|
6027
|
-
|
|
6013
|
+
def upsert_graph_edge_endpoint(
|
|
6028
6014
|
body: UpsertGraphEdgeRequest,
|
|
6029
6015
|
_auth: str | None = Depends(verify_api_key),
|
|
6030
6016
|
) -> dict[str, Any]:
|
|
@@ -6048,7 +6034,7 @@ async def upsert_graph_edge_endpoint(
|
|
|
6048
6034
|
|
|
6049
6035
|
|
|
6050
6036
|
@app.get("/api/v1/agents/enriched")
|
|
6051
|
-
|
|
6037
|
+
def list_agents_enriched(
|
|
6052
6038
|
request: Request,
|
|
6053
6039
|
histograms: bool = True,
|
|
6054
6040
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6078,7 +6064,7 @@ async def list_agents_enriched(
|
|
|
6078
6064
|
|
|
6079
6065
|
|
|
6080
6066
|
@app.get("/api/v1/agent-groups")
|
|
6081
|
-
|
|
6067
|
+
def list_agent_groups(
|
|
6082
6068
|
request: Request,
|
|
6083
6069
|
_auth: str | None = Depends(verify_api_key),
|
|
6084
6070
|
) -> dict[str, Any]:
|
|
@@ -6151,7 +6137,7 @@ async def update_agent_group_endpoint(
|
|
|
6151
6137
|
|
|
6152
6138
|
|
|
6153
6139
|
@app.delete("/api/v1/agent-groups/{group_id}")
|
|
6154
|
-
|
|
6140
|
+
def delete_agent_group_endpoint(
|
|
6155
6141
|
group_id: str,
|
|
6156
6142
|
_auth: str | None = Depends(verify_api_key),
|
|
6157
6143
|
) -> dict[str, Any]:
|
|
@@ -6226,7 +6212,7 @@ async def reorder_agent_groups_endpoint(
|
|
|
6226
6212
|
|
|
6227
6213
|
|
|
6228
6214
|
@app.get("/api/v1/agent-groups/suggestions")
|
|
6229
|
-
|
|
6215
|
+
def agent_group_suggestions(
|
|
6230
6216
|
request: Request,
|
|
6231
6217
|
_auth: str | None = Depends(verify_api_key),
|
|
6232
6218
|
) -> dict[str, Any]:
|
|
@@ -6263,7 +6249,7 @@ async def agent_group_suggestions(
|
|
|
6263
6249
|
|
|
6264
6250
|
|
|
6265
6251
|
@app.post("/api/v1/agent-groups/suggestions/{cluster_id}/accept", status_code=201)
|
|
6266
|
-
|
|
6252
|
+
def accept_cluster_suggestion(
|
|
6267
6253
|
cluster_id: str,
|
|
6268
6254
|
_auth: str | None = Depends(verify_api_key),
|
|
6269
6255
|
) -> dict[str, Any]:
|
|
@@ -6302,7 +6288,7 @@ async def accept_cluster_suggestion(
|
|
|
6302
6288
|
|
|
6303
6289
|
|
|
6304
6290
|
@app.post("/api/v1/agent-groups/suggestions/{cluster_id}/dismiss")
|
|
6305
|
-
|
|
6291
|
+
def dismiss_cluster_suggestion_endpoint(
|
|
6306
6292
|
cluster_id: str,
|
|
6307
6293
|
_auth: str | None = Depends(verify_api_key),
|
|
6308
6294
|
) -> dict[str, Any]:
|
|
@@ -6312,32 +6298,86 @@ async def dismiss_cluster_suggestion_endpoint(
|
|
|
6312
6298
|
return {"dismissed": True}
|
|
6313
6299
|
|
|
6314
6300
|
|
|
6301
|
+
# The cluster compile walks every agent in the namespace and is pure Python
|
|
6302
|
+
# CPU — minutes on an account with thousands of agents. The dashboard's Agents
|
|
6303
|
+
# page POSTs it on every mount, so without a guard two tabs or one reload queue
|
|
6304
|
+
# a second full compile behind the first while the process is pinned at one
|
|
6305
|
+
# core. One compile at a time per process, and a compile that finished within
|
|
6306
|
+
# the window is reused rather than repeated; the caller's ``ok`` is unchanged
|
|
6307
|
+
# because it only ever waited for the suggestions to be refreshed.
|
|
6308
|
+
_CLUSTER_RECOMPUTE_MIN_INTERVAL_S = float(
|
|
6309
|
+
os.environ.get("AMFS_CLUSTER_RECOMPUTE_MIN_INTERVAL_S", "300"),
|
|
6310
|
+
)
|
|
6311
|
+
_cluster_recompute_lock = threading.Lock()
|
|
6312
|
+
_cluster_recompute_last: dict[str, float] = {}
|
|
6313
|
+
|
|
6314
|
+
|
|
6315
|
+
def _recompute_window_key(request: Request, namespace: str) -> str:
|
|
6316
|
+
"""Who the reuse window belongs to.
|
|
6317
|
+
|
|
6318
|
+
A multi-tenant deployment runs every account under one namespace and keeps
|
|
6319
|
+
them apart with row-level security, so the namespace alone would let the
|
|
6320
|
+
first account's compile mark every other account on the instance as fresh.
|
|
6321
|
+
The account the compile will actually run under is the one on the RLS
|
|
6322
|
+
context — the same ContextVar the Postgres connection reads — so that is
|
|
6323
|
+
the key, whichever middleware branch authenticated the request.
|
|
6324
|
+
``request.state.account_id`` is the fallback for a deployment whose
|
|
6325
|
+
middleware sets the request state but not the RLS context; a single-tenant
|
|
6326
|
+
server has neither and the namespace is the whole story.
|
|
6327
|
+
"""
|
|
6328
|
+
account_id: Any = None
|
|
6329
|
+
try:
|
|
6330
|
+
from amfs_postgres.tenant_context import get_request_tenant_account_id
|
|
6331
|
+
|
|
6332
|
+
account_id = get_request_tenant_account_id()
|
|
6333
|
+
except ImportError:
|
|
6334
|
+
pass
|
|
6335
|
+
if not account_id:
|
|
6336
|
+
account_id = getattr(request.state, "account_id", None)
|
|
6337
|
+
return f"{account_id or ''}:{namespace}"
|
|
6338
|
+
|
|
6339
|
+
|
|
6315
6340
|
@app.post("/api/v1/agent-groups/recompute")
|
|
6316
|
-
|
|
6341
|
+
def recompute_clusters(
|
|
6342
|
+
request: Request,
|
|
6343
|
+
force: bool = False,
|
|
6317
6344
|
_auth: str | None = Depends(verify_api_key),
|
|
6318
6345
|
) -> dict[str, Any]:
|
|
6319
|
-
"""Trigger recomputation of agent clusters.
|
|
6346
|
+
"""Trigger recomputation of agent clusters.
|
|
6347
|
+
|
|
6348
|
+
Runs on the threadpool (plain ``def``): the compile is CPU-bound Python and
|
|
6349
|
+
used to freeze the event loop for its whole duration. ``force=true`` skips
|
|
6350
|
+
the reuse window but still waits its turn behind a compile in flight. The
|
|
6351
|
+
window is per tenant account; the lock is per process, because the compile
|
|
6352
|
+
is what pins the core.
|
|
6353
|
+
"""
|
|
6320
6354
|
try:
|
|
6321
6355
|
from amfs_cortex.compiler import DigestCompiler
|
|
6322
6356
|
from amfs_postgres.adapter import PostgresAdapter
|
|
6323
|
-
|
|
6324
|
-
mem = _get_memory()
|
|
6325
|
-
adapter = mem._adapter
|
|
6326
|
-
if not isinstance(adapter, PostgresAdapter):
|
|
6327
|
-
return JSONResponse(
|
|
6328
|
-
status_code=400,
|
|
6329
|
-
content={"error": "Cluster recomputation requires Postgres adapter"},
|
|
6330
|
-
)
|
|
6331
|
-
compiler = DigestCompiler(
|
|
6332
|
-
adapter=adapter,
|
|
6333
|
-
namespace=mem.namespace,
|
|
6334
|
-
)
|
|
6335
|
-
compiler.compile(f"cluster:account:{mem.namespace}")
|
|
6336
6357
|
except ImportError:
|
|
6337
6358
|
return JSONResponse(
|
|
6338
6359
|
status_code=400,
|
|
6339
6360
|
content={"error": "amfs-cortex package is required for recomputation"},
|
|
6340
6361
|
)
|
|
6362
|
+
mem = _get_memory()
|
|
6363
|
+
adapter = mem._adapter
|
|
6364
|
+
if not isinstance(adapter, PostgresAdapter):
|
|
6365
|
+
return JSONResponse(
|
|
6366
|
+
status_code=400,
|
|
6367
|
+
content={"error": "Cluster recomputation requires Postgres adapter"},
|
|
6368
|
+
)
|
|
6369
|
+
ns = mem.namespace
|
|
6370
|
+
window_key = _recompute_window_key(request, ns)
|
|
6371
|
+
with _cluster_recompute_lock:
|
|
6372
|
+
last = _cluster_recompute_last.get(window_key)
|
|
6373
|
+
if (
|
|
6374
|
+
not force
|
|
6375
|
+
and last is not None
|
|
6376
|
+
and time.monotonic() - last < _CLUSTER_RECOMPUTE_MIN_INTERVAL_S
|
|
6377
|
+
):
|
|
6378
|
+
return {"ok": True, "skipped": "recent"}
|
|
6379
|
+
DigestCompiler(adapter=adapter, namespace=ns).compile(f"cluster:account:{ns}")
|
|
6380
|
+
_cluster_recompute_last[window_key] = time.monotonic()
|
|
6341
6381
|
return {"ok": True}
|
|
6342
6382
|
|
|
6343
6383
|
|
|
@@ -6350,7 +6390,7 @@ SNAPSHOT_ENTITY = "_system/agent-snapshots"
|
|
|
6350
6390
|
|
|
6351
6391
|
|
|
6352
6392
|
@app.post("/api/v1/agents/{agent_id:path}/snapshots")
|
|
6353
|
-
|
|
6393
|
+
def create_snapshot(
|
|
6354
6394
|
request: Request,
|
|
6355
6395
|
agent_id: str,
|
|
6356
6396
|
req: CreateSnapshotRequest,
|
|
@@ -6370,17 +6410,12 @@ async def create_snapshot(
|
|
|
6370
6410
|
"data": req.snapshot_data,
|
|
6371
6411
|
}
|
|
6372
6412
|
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
snapshot_value,
|
|
6380
|
-
confidence=1.0,
|
|
6381
|
-
)
|
|
6382
|
-
finally:
|
|
6383
|
-
mem._tagger.agent_id = original_agent
|
|
6413
|
+
mem.as_agent(agent_id).write(
|
|
6414
|
+
SNAPSHOT_ENTITY,
|
|
6415
|
+
f"{agent_id}/{snapshot_id}",
|
|
6416
|
+
snapshot_value,
|
|
6417
|
+
confidence=1.0,
|
|
6418
|
+
)
|
|
6384
6419
|
|
|
6385
6420
|
try:
|
|
6386
6421
|
mem._adapter.log_event(Event(
|
|
@@ -6408,7 +6443,7 @@ async def create_snapshot(
|
|
|
6408
6443
|
|
|
6409
6444
|
|
|
6410
6445
|
@app.get("/api/v1/agents/{agent_id:path}/snapshots")
|
|
6411
|
-
|
|
6446
|
+
def list_snapshots(
|
|
6412
6447
|
request: Request,
|
|
6413
6448
|
agent_id: str,
|
|
6414
6449
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6436,7 +6471,7 @@ async def list_snapshots(
|
|
|
6436
6471
|
|
|
6437
6472
|
|
|
6438
6473
|
@app.get("/api/v1/agents/{agent_id:path}/snapshots/{snapshot_id}")
|
|
6439
|
-
|
|
6474
|
+
def get_snapshot(
|
|
6440
6475
|
request: Request,
|
|
6441
6476
|
agent_id: str,
|
|
6442
6477
|
snapshot_id: str,
|
|
@@ -6454,7 +6489,7 @@ async def get_snapshot(
|
|
|
6454
6489
|
|
|
6455
6490
|
|
|
6456
6491
|
@app.delete("/api/v1/agents/{agent_id:path}/snapshots/{snapshot_id}")
|
|
6457
|
-
|
|
6492
|
+
def delete_snapshot(
|
|
6458
6493
|
request: Request,
|
|
6459
6494
|
agent_id: str,
|
|
6460
6495
|
snapshot_id: str,
|
|
@@ -6472,7 +6507,7 @@ async def delete_snapshot(
|
|
|
6472
6507
|
|
|
6473
6508
|
|
|
6474
6509
|
@app.post("/api/v1/agents/{agent_id:path}/snapshots/{snapshot_id}/recover")
|
|
6475
|
-
|
|
6510
|
+
def recover_snapshot(
|
|
6476
6511
|
request: Request,
|
|
6477
6512
|
agent_id: str,
|
|
6478
6513
|
snapshot_id: str,
|
|
@@ -6527,7 +6562,7 @@ async def recover_snapshot(
|
|
|
6527
6562
|
|
|
6528
6563
|
|
|
6529
6564
|
@app.post("/api/v1/rollback")
|
|
6530
|
-
|
|
6565
|
+
def rollback(
|
|
6531
6566
|
request: Request,
|
|
6532
6567
|
body: dict[str, Any],
|
|
6533
6568
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6604,7 +6639,7 @@ async def rollback(
|
|
|
6604
6639
|
|
|
6605
6640
|
|
|
6606
6641
|
@app.get("/api/v1/agents/{agent_id:path}/branches")
|
|
6607
|
-
|
|
6642
|
+
def agent_branches(
|
|
6608
6643
|
request: Request,
|
|
6609
6644
|
agent_id: str,
|
|
6610
6645
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6629,7 +6664,7 @@ async def agent_branches(
|
|
|
6629
6664
|
|
|
6630
6665
|
|
|
6631
6666
|
@app.get("/api/v1/agents/{agent_id:path}/pull-requests")
|
|
6632
|
-
|
|
6667
|
+
def agent_pull_requests(
|
|
6633
6668
|
request: Request,
|
|
6634
6669
|
agent_id: str,
|
|
6635
6670
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -6947,7 +6982,7 @@ def list_audit_log(
|
|
|
6947
6982
|
|
|
6948
6983
|
|
|
6949
6984
|
@app.get("/api/v1/patterns")
|
|
6950
|
-
|
|
6985
|
+
def list_patterns(
|
|
6951
6986
|
request: Request,
|
|
6952
6987
|
entity_path: str | None = Query(None),
|
|
6953
6988
|
limit: int = Query(100),
|
|
@@ -7724,7 +7759,7 @@ def _filter_graph_edges(request: Request, edges: list) -> list:
|
|
|
7724
7759
|
|
|
7725
7760
|
|
|
7726
7761
|
@app.get("/api/v1/pro/graph/neighbors")
|
|
7727
|
-
|
|
7762
|
+
def graph_neighbors(
|
|
7728
7763
|
request: Request,
|
|
7729
7764
|
entity: str = Query(...),
|
|
7730
7765
|
relation: str | None = Query(None),
|
|
@@ -7841,7 +7876,7 @@ async def expertise_graph(
|
|
|
7841
7876
|
|
|
7842
7877
|
|
|
7843
7878
|
@app.post("/api/v1/pro/graph/backfill")
|
|
7844
|
-
|
|
7879
|
+
def graph_backfill(
|
|
7845
7880
|
_auth: str | None = Depends(verify_api_key),
|
|
7846
7881
|
) -> dict[str, Any]:
|
|
7847
7882
|
"""Backfill knowledge graph edges from existing entries and outcomes.
|
|
@@ -8050,7 +8085,7 @@ async def stream(
|
|
|
8050
8085
|
|
|
8051
8086
|
|
|
8052
8087
|
@app.get("/api/v1/admin/config")
|
|
8053
|
-
|
|
8088
|
+
def get_system_config(
|
|
8054
8089
|
_auth: str | None = Depends(verify_api_key),
|
|
8055
8090
|
) -> dict[str, Any]:
|
|
8056
8091
|
"""Return system configuration and active Pro module status."""
|
|
@@ -8086,7 +8121,7 @@ async def get_system_config(
|
|
|
8086
8121
|
|
|
8087
8122
|
|
|
8088
8123
|
@app.get("/api/v1/connectors")
|
|
8089
|
-
|
|
8124
|
+
def list_connectors(
|
|
8090
8125
|
_auth: str | None = Depends(verify_api_key),
|
|
8091
8126
|
) -> dict[str, Any]:
|
|
8092
8127
|
"""List available and installed connectors."""
|
|
@@ -8387,7 +8422,7 @@ async def get_briefing(
|
|
|
8387
8422
|
|
|
8388
8423
|
|
|
8389
8424
|
@app.get("/api/v1/cortex/status")
|
|
8390
|
-
|
|
8425
|
+
def cortex_status(
|
|
8391
8426
|
request: Request,
|
|
8392
8427
|
_auth: str | None = Depends(verify_api_key),
|
|
8393
8428
|
) -> dict[str, Any]:
|
|
@@ -8427,7 +8462,7 @@ async def cortex_status(
|
|
|
8427
8462
|
|
|
8428
8463
|
|
|
8429
8464
|
@app.get("/api/v1/cortex/digests")
|
|
8430
|
-
|
|
8465
|
+
def list_cortex_digests(
|
|
8431
8466
|
request: Request,
|
|
8432
8467
|
digest_type: str | None = Query(None),
|
|
8433
8468
|
scope: str | None = Query(None),
|
|
@@ -8464,7 +8499,7 @@ async def list_cortex_digests(
|
|
|
8464
8499
|
|
|
8465
8500
|
|
|
8466
8501
|
@app.get("/api/v1/cortex/activity")
|
|
8467
|
-
|
|
8502
|
+
def cortex_activity(
|
|
8468
8503
|
request: Request,
|
|
8469
8504
|
limit: int = Query(default=50, le=200),
|
|
8470
8505
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -8486,7 +8521,7 @@ async def cortex_activity(
|
|
|
8486
8521
|
|
|
8487
8522
|
|
|
8488
8523
|
@app.post("/api/v1/cortex/recompile")
|
|
8489
|
-
|
|
8524
|
+
def cortex_recompile(
|
|
8490
8525
|
_auth: str | None = Depends(verify_api_key),
|
|
8491
8526
|
) -> dict[str, Any]:
|
|
8492
8527
|
"""Trigger a full digest recompilation."""
|
|
@@ -8502,7 +8537,7 @@ async def cortex_recompile(
|
|
|
8502
8537
|
|
|
8503
8538
|
|
|
8504
8539
|
@app.post("/api/v1/cortex/consolidate")
|
|
8505
|
-
|
|
8540
|
+
def run_consolidation(
|
|
8506
8541
|
request: Request,
|
|
8507
8542
|
body: dict[str, Any] | None = None,
|
|
8508
8543
|
_auth: str | None = Depends(verify_api_key),
|
|
@@ -8539,7 +8574,7 @@ async def run_consolidation(
|
|
|
8539
8574
|
|
|
8540
8575
|
|
|
8541
8576
|
@app.get("/api/v1/cortex/consolidation/candidates")
|
|
8542
|
-
|
|
8577
|
+
def list_consolidation_candidates(
|
|
8543
8578
|
request: Request,
|
|
8544
8579
|
entity_path: str = Query(...),
|
|
8545
8580
|
branch: str = Query("main"),
|
|
@@ -8566,7 +8601,7 @@ async def list_consolidation_candidates(
|
|
|
8566
8601
|
|
|
8567
8602
|
|
|
8568
8603
|
@app.get("/api/v1/cortex/consolidation/proposals")
|
|
8569
|
-
|
|
8604
|
+
def list_consolidation_proposals(
|
|
8570
8605
|
request: Request,
|
|
8571
8606
|
entity_path: str | None = Query(None),
|
|
8572
8607
|
status: str | None = Query(None),
|
|
@@ -8656,7 +8691,7 @@ async def list_consolidation_proposals(
|
|
|
8656
8691
|
|
|
8657
8692
|
|
|
8658
8693
|
@app.get("/api/v1/cortex/consolidation/status")
|
|
8659
|
-
|
|
8694
|
+
def consolidation_status(
|
|
8660
8695
|
request: Request,
|
|
8661
8696
|
_auth: str | None = Depends(verify_api_key),
|
|
8662
8697
|
) -> dict[str, Any]:
|
|
@@ -8762,23 +8797,19 @@ async def ingest_webhook(
|
|
|
8762
8797
|
event_id=event_id,
|
|
8763
8798
|
)
|
|
8764
8799
|
|
|
8765
|
-
original_agent = mem._tagger.agent_id
|
|
8766
|
-
mem._tagger.agent_id = f"webhook/{connector_name}"
|
|
8767
8800
|
persisted = 0
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
finally:
|
|
8781
|
-
mem._tagger.agent_id = original_agent
|
|
8801
|
+
as_webhook = mem.as_agent(f"webhook/{connector_name}")
|
|
8802
|
+
for r in results:
|
|
8803
|
+
if r.success and r.action == "write":
|
|
8804
|
+
entry = as_webhook.write(
|
|
8805
|
+
r.entity_path,
|
|
8806
|
+
r.key,
|
|
8807
|
+
r.details,
|
|
8808
|
+
confidence=1.0,
|
|
8809
|
+
memory_type=MemoryType.EXPERIENCE,
|
|
8810
|
+
)
|
|
8811
|
+
_sse_manager.broadcast(entry)
|
|
8812
|
+
persisted += 1
|
|
8782
8813
|
|
|
8783
8814
|
return {
|
|
8784
8815
|
"results": [r.model_dump(mode="json") for r in results],
|
|
@@ -8788,7 +8819,7 @@ async def ingest_webhook(
|
|
|
8788
8819
|
|
|
8789
8820
|
|
|
8790
8821
|
@app.post("/api/v1/events")
|
|
8791
|
-
|
|
8822
|
+
def ingest_event(
|
|
8792
8823
|
body: EventRequest,
|
|
8793
8824
|
_auth: str | None = Depends(verify_api_key),
|
|
8794
8825
|
) -> dict[str, Any]:
|
|
@@ -8799,19 +8830,14 @@ async def ingest_event(
|
|
|
8799
8830
|
"""
|
|
8800
8831
|
mem = _get_memory()
|
|
8801
8832
|
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
memory_type=MemoryType.EXPERIENCE,
|
|
8811
|
-
)
|
|
8812
|
-
_sse_manager.broadcast(entry)
|
|
8813
|
-
finally:
|
|
8814
|
-
mem._tagger.agent_id = original_agent
|
|
8833
|
+
entry = mem.as_agent(f"external/{body.source}").write(
|
|
8834
|
+
body.entity_path,
|
|
8835
|
+
body.key,
|
|
8836
|
+
body.value,
|
|
8837
|
+
confidence=1.0,
|
|
8838
|
+
memory_type=MemoryType.EXPERIENCE,
|
|
8839
|
+
)
|
|
8840
|
+
_sse_manager.broadcast(entry)
|
|
8815
8841
|
|
|
8816
8842
|
return {
|
|
8817
8843
|
"status": "ok",
|
|
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
|