amfs-http-server 0.2.0__tar.gz → 0.3.0__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.2.0 → amfs_http_server-0.3.0}/PKG-INFO +1 -1
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/pyproject.toml +1 -1
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/auth.py +7 -6
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/models.py +1 -0
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/server.py +276 -5
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/.gitignore +0 -0
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/__init__.py +0 -0
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/pro_proxy.py +0 -0
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/sse.py +0 -0
- {amfs_http_server-0.2.0 → amfs_http_server-0.3.0}/src/amfs_http/tenant_middleware.py +0 -0
|
@@ -26,6 +26,10 @@ def _check_db_key(api_key: str) -> bool:
|
|
|
26
26
|
|
|
27
27
|
Keys are stored as SHA-256 hex digests, so we hash the incoming raw key
|
|
28
28
|
before comparing.
|
|
29
|
+
|
|
30
|
+
Uses SECURITY DEFINER functions to bypass RLS — this connection does not
|
|
31
|
+
set amfs.current_account_id, so direct table queries would return nothing
|
|
32
|
+
when FORCE ROW LEVEL SECURITY is enabled.
|
|
29
33
|
"""
|
|
30
34
|
try:
|
|
31
35
|
import psycopg
|
|
@@ -39,16 +43,13 @@ def _check_db_key(api_key: str) -> bool:
|
|
|
39
43
|
|
|
40
44
|
with psycopg.connect(dsn, row_factory=dict_row) as conn:
|
|
41
45
|
row = conn.execute(
|
|
42
|
-
"SELECT
|
|
43
|
-
"WHERE key_hash = %s AND active = TRUE "
|
|
44
|
-
"AND (expires_at IS NULL OR expires_at > NOW()) "
|
|
45
|
-
"LIMIT 1",
|
|
46
|
+
"SELECT * FROM amfs_authenticate_api_key(%s)",
|
|
46
47
|
(key_hash,),
|
|
47
48
|
).fetchone()
|
|
48
49
|
if row:
|
|
49
50
|
conn.execute(
|
|
50
|
-
"
|
|
51
|
-
(row["
|
|
51
|
+
"SELECT amfs_touch_api_key_usage(%s)",
|
|
52
|
+
(row["key_id"],),
|
|
52
53
|
)
|
|
53
54
|
return row is not None
|
|
54
55
|
except Exception:
|
|
@@ -33,6 +33,7 @@ from amfs.config import load_config_or_default
|
|
|
33
33
|
from pydantic import BaseModel, Field
|
|
34
34
|
from amfs_core.models import (
|
|
35
35
|
AMFSConfig,
|
|
36
|
+
DecisionTrace,
|
|
36
37
|
Event,
|
|
37
38
|
EventType,
|
|
38
39
|
GraphEdge,
|
|
@@ -400,10 +401,11 @@ async def write_entry(
|
|
|
400
401
|
async def list_entries(
|
|
401
402
|
entity_path: str | None = Query(None),
|
|
402
403
|
branch: str = Query("main"),
|
|
404
|
+
include_superseded: bool = Query(False),
|
|
403
405
|
_auth: str | None = Depends(verify_api_key),
|
|
404
406
|
) -> dict[str, Any]:
|
|
405
407
|
mem = _get_memory()
|
|
406
|
-
entries = mem.list(entity_path, branch=branch)
|
|
408
|
+
entries = mem.list(entity_path, branch=branch, include_superseded=include_superseded)
|
|
407
409
|
return {"entries": [_entry_to_response(e) for e in entries]}
|
|
408
410
|
|
|
409
411
|
|
|
@@ -429,6 +431,7 @@ async def search_entries(
|
|
|
429
431
|
pattern_ref=req.pattern_ref,
|
|
430
432
|
sort_by=req.sort_by,
|
|
431
433
|
limit=req.limit,
|
|
434
|
+
depth=req.depth,
|
|
432
435
|
)
|
|
433
436
|
try:
|
|
434
437
|
results = mem._adapter.search(sq, branch=branch)
|
|
@@ -462,6 +465,176 @@ async def get_stats(
|
|
|
462
465
|
return json.loads(json.dumps(stats.model_dump(mode="json"), default=str))
|
|
463
466
|
|
|
464
467
|
|
|
468
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
469
|
+
# Integrity verification
|
|
470
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@app.post("/api/v1/verify")
|
|
474
|
+
async def verify_integrity(
|
|
475
|
+
body: dict[str, Any] = {},
|
|
476
|
+
_auth: str | None = Depends(verify_api_key),
|
|
477
|
+
) -> dict[str, Any]:
|
|
478
|
+
mem = _get_memory()
|
|
479
|
+
entity_path = body.get("entity_path")
|
|
480
|
+
return mem.verify(entity_path)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
484
|
+
# Atomic commits
|
|
485
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@app.post("/api/v1/commits")
|
|
489
|
+
async def create_commit(
|
|
490
|
+
body: dict[str, Any],
|
|
491
|
+
_auth: str | None = Depends(verify_api_key),
|
|
492
|
+
) -> dict[str, Any]:
|
|
493
|
+
mem = _get_memory()
|
|
494
|
+
writes = body.get("writes", [])
|
|
495
|
+
message = body.get("message", "")
|
|
496
|
+
with mem.transaction(message) as tx:
|
|
497
|
+
for w in writes:
|
|
498
|
+
tx.write(w["entity_path"], w["key"], w.get("value"))
|
|
499
|
+
return {
|
|
500
|
+
"commit_id": tx.commit.id if tx.commit else None,
|
|
501
|
+
"message": message,
|
|
502
|
+
"entries_written": len(writes),
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@app.get("/api/v1/commits")
|
|
507
|
+
async def list_commits(
|
|
508
|
+
limit: int = Query(50, ge=1, le=500),
|
|
509
|
+
_auth: str | None = Depends(verify_api_key),
|
|
510
|
+
) -> dict[str, Any]:
|
|
511
|
+
mem = _get_memory()
|
|
512
|
+
commits = mem.commit_log(limit=limit)
|
|
513
|
+
return {
|
|
514
|
+
"commits": [json.loads(json.dumps(c.model_dump(mode="json"), default=str)) for c in commits],
|
|
515
|
+
"count": len(commits),
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@app.get("/api/v1/commits/{commit_id}")
|
|
520
|
+
async def get_commit(
|
|
521
|
+
commit_id: str,
|
|
522
|
+
_auth: str | None = Depends(verify_api_key),
|
|
523
|
+
) -> dict[str, Any]:
|
|
524
|
+
mem = _get_memory()
|
|
525
|
+
commit = mem.get_commit(commit_id)
|
|
526
|
+
if commit is None:
|
|
527
|
+
from fastapi import HTTPException
|
|
528
|
+
raise HTTPException(status_code=404, detail="Commit not found")
|
|
529
|
+
return json.loads(json.dumps(commit.model_dump(mode="json"), default=str))
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@app.post("/api/v1/merge-base")
|
|
533
|
+
async def merge_base(
|
|
534
|
+
body: dict[str, Any],
|
|
535
|
+
_auth: str | None = Depends(verify_api_key),
|
|
536
|
+
) -> dict[str, Any]:
|
|
537
|
+
mem = _get_memory()
|
|
538
|
+
ancestor = mem.common_ancestor(body["commit_a"], body["commit_b"])
|
|
539
|
+
return {
|
|
540
|
+
"ancestor_commit_id": ancestor,
|
|
541
|
+
"commit_a": body["commit_a"],
|
|
542
|
+
"commit_b": body["commit_b"],
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
547
|
+
# Agent binding
|
|
548
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@app.put("/api/v1/agents/{agent_id}/profile")
|
|
552
|
+
async def update_agent_profile(
|
|
553
|
+
agent_id: str,
|
|
554
|
+
body: dict[str, Any],
|
|
555
|
+
_auth: str | None = Depends(verify_api_key),
|
|
556
|
+
) -> dict[str, Any]:
|
|
557
|
+
from amfs_core.models import AgentProfile
|
|
558
|
+
|
|
559
|
+
mem = _get_memory()
|
|
560
|
+
profile = AgentProfile.model_validate(body)
|
|
561
|
+
agent = mem._adapter.update_agent_profile(agent_id, profile)
|
|
562
|
+
return json.loads(json.dumps(agent.model_dump(mode="json"), default=str))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@app.put("/api/v1/agents/{agent_id}/capabilities")
|
|
566
|
+
async def update_agent_capabilities(
|
|
567
|
+
agent_id: str,
|
|
568
|
+
body: dict[str, Any],
|
|
569
|
+
_auth: str | None = Depends(verify_api_key),
|
|
570
|
+
) -> dict[str, Any]:
|
|
571
|
+
from amfs_core.models import AgentCapability
|
|
572
|
+
|
|
573
|
+
mem = _get_memory()
|
|
574
|
+
capabilities = [AgentCapability.model_validate(c) for c in body.get("capabilities", [])]
|
|
575
|
+
agent = mem._adapter.update_agent_capabilities(agent_id, capabilities)
|
|
576
|
+
return json.loads(json.dumps(agent.model_dump(mode="json"), default=str))
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@app.put("/api/v1/agents/{agent_id}/contracts")
|
|
580
|
+
async def update_agent_contracts(
|
|
581
|
+
agent_id: str,
|
|
582
|
+
body: dict[str, Any],
|
|
583
|
+
_auth: str | None = Depends(verify_api_key),
|
|
584
|
+
) -> dict[str, Any]:
|
|
585
|
+
from amfs_core.models import MemoryContract
|
|
586
|
+
|
|
587
|
+
mem = _get_memory()
|
|
588
|
+
contracts = [MemoryContract.model_validate(c) for c in body.get("contracts", [])]
|
|
589
|
+
agent = mem._adapter.update_agent_contracts(agent_id, contracts)
|
|
590
|
+
return json.loads(json.dumps(agent.model_dump(mode="json"), default=str))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@app.get("/api/v1/agents/discover")
|
|
594
|
+
async def discover_agents(
|
|
595
|
+
capability: str | None = Query(None),
|
|
596
|
+
entity_path: str | None = Query(None),
|
|
597
|
+
_auth: str | None = Depends(verify_api_key),
|
|
598
|
+
) -> dict[str, Any]:
|
|
599
|
+
mem = _get_memory()
|
|
600
|
+
agents = mem.discover_agents(capability=capability, entity_path=entity_path)
|
|
601
|
+
return {
|
|
602
|
+
"agents": [json.loads(json.dumps(a.model_dump(mode="json"), default=str)) for a in agents],
|
|
603
|
+
"count": len(agents),
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
608
|
+
# Diff & patch
|
|
609
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@app.post("/api/v1/diff")
|
|
613
|
+
async def compute_diff(
|
|
614
|
+
body: dict[str, Any],
|
|
615
|
+
_auth: str | None = Depends(verify_api_key),
|
|
616
|
+
) -> dict[str, Any]:
|
|
617
|
+
mem = _get_memory()
|
|
618
|
+
return mem.diff(
|
|
619
|
+
body["entity_path"],
|
|
620
|
+
body["key"],
|
|
621
|
+
body.get("old_version"),
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@app.post("/api/v1/patches")
|
|
626
|
+
async def create_patch(
|
|
627
|
+
body: dict[str, Any],
|
|
628
|
+
_auth: str | None = Depends(verify_api_key),
|
|
629
|
+
) -> dict[str, Any]:
|
|
630
|
+
mem = _get_memory()
|
|
631
|
+
return mem.create_patch(
|
|
632
|
+
body["entity_path"],
|
|
633
|
+
body["key"],
|
|
634
|
+
body.get("source_version"),
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
465
638
|
# ──────────────────────────────────────────────────────────────────────
|
|
466
639
|
# History
|
|
467
640
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -541,6 +714,8 @@ def _auto_seal_trace(mem: AgentMemory) -> str | None:
|
|
|
541
714
|
if store is None:
|
|
542
715
|
return None
|
|
543
716
|
try:
|
|
717
|
+
from uuid import UUID as _UUID
|
|
718
|
+
|
|
544
719
|
now = datetime.now(timezone.utc)
|
|
545
720
|
causal = [
|
|
546
721
|
TraceEntry(
|
|
@@ -569,7 +744,20 @@ def _auto_seal_trace(mem: AgentMemory) -> str | None:
|
|
|
569
744
|
seq = _seal_sequence.get(session_id, 0)
|
|
570
745
|
parent_hash = store.get_latest_hash(session_id)
|
|
571
746
|
|
|
747
|
+
trace_id = _UUID(oss_trace.id) if oss_trace.id else None
|
|
748
|
+
|
|
749
|
+
account_id = None
|
|
750
|
+
try:
|
|
751
|
+
from amfs_postgres.tenant_context import get_request_tenant_account_id
|
|
752
|
+
tid = get_request_tenant_account_id()
|
|
753
|
+
if tid:
|
|
754
|
+
account_id = _UUID(tid)
|
|
755
|
+
except (ImportError, ValueError):
|
|
756
|
+
pass
|
|
757
|
+
|
|
572
758
|
imm = ImmutableDecisionTrace(
|
|
759
|
+
**({"id": trace_id} if trace_id else {}),
|
|
760
|
+
**({"account_id": account_id} if account_id else {}),
|
|
573
761
|
agent_id=mem.agent_id,
|
|
574
762
|
session_id=session_id,
|
|
575
763
|
sequence_number=seq,
|
|
@@ -578,7 +766,7 @@ def _auto_seal_trace(mem: AgentMemory) -> str | None:
|
|
|
578
766
|
decision_summary=getattr(oss_trace, "decision_summary", None),
|
|
579
767
|
causal_entries=causal,
|
|
580
768
|
external_contexts=contexts,
|
|
581
|
-
created_at=
|
|
769
|
+
created_at=now,
|
|
582
770
|
)
|
|
583
771
|
sealed = seal(
|
|
584
772
|
imm,
|
|
@@ -710,6 +898,19 @@ async def list_traces(
|
|
|
710
898
|
return {"traces": [t.model_dump(mode="json") for t in traces]}
|
|
711
899
|
|
|
712
900
|
|
|
901
|
+
@app.post("/api/v1/traces")
|
|
902
|
+
async def save_trace(
|
|
903
|
+
req: Request,
|
|
904
|
+
_auth: str | None = Depends(verify_api_key),
|
|
905
|
+
) -> dict[str, Any]:
|
|
906
|
+
"""Persist a decision trace (used by HttpAdapter.save_trace)."""
|
|
907
|
+
body = await req.json()
|
|
908
|
+
trace = DecisionTrace.model_validate(body)
|
|
909
|
+
mem = _get_memory()
|
|
910
|
+
saved = mem._adapter.save_trace(trace)
|
|
911
|
+
return saved.model_dump(mode="json")
|
|
912
|
+
|
|
913
|
+
|
|
713
914
|
@app.get("/api/v1/traces/{trace_id}")
|
|
714
915
|
async def get_trace(
|
|
715
916
|
trace_id: str,
|
|
@@ -928,11 +1129,37 @@ async def list_agents(
|
|
|
928
1129
|
"entries_written": 0,
|
|
929
1130
|
"entities_touched": set(),
|
|
930
1131
|
"last_active": e.provenance.written_at,
|
|
1132
|
+
"first_seen": e.provenance.written_at,
|
|
931
1133
|
}
|
|
932
1134
|
agent_data[aid]["entries_written"] += 1
|
|
933
1135
|
agent_data[aid]["entities_touched"].add(e.entity_path)
|
|
934
1136
|
if e.provenance.written_at > agent_data[aid]["last_active"]:
|
|
935
1137
|
agent_data[aid]["last_active"] = e.provenance.written_at
|
|
1138
|
+
if e.provenance.written_at and (
|
|
1139
|
+
agent_data[aid]["first_seen"] is None
|
|
1140
|
+
or e.provenance.written_at < agent_data[aid]["first_seen"]
|
|
1141
|
+
):
|
|
1142
|
+
agent_data[aid]["first_seen"] = e.provenance.written_at
|
|
1143
|
+
|
|
1144
|
+
agent_registration: dict[str, dict[str, Any]] = {}
|
|
1145
|
+
known_agent_ids = list(agent_data.keys())
|
|
1146
|
+
if known_agent_ids:
|
|
1147
|
+
try:
|
|
1148
|
+
from amfs_postgres.adapter import PostgresAdapter
|
|
1149
|
+
adapter = mem._adapter
|
|
1150
|
+
if isinstance(adapter, PostgresAdapter):
|
|
1151
|
+
with adapter._pool.connection() as conn:
|
|
1152
|
+
with conn.cursor() as cur:
|
|
1153
|
+
placeholders = ", ".join(["%s"] * len(known_agent_ids))
|
|
1154
|
+
cur.execute(
|
|
1155
|
+
f"SELECT agent_id, created_at FROM amfs_agents "
|
|
1156
|
+
f"WHERE namespace = %s AND agent_id IN ({placeholders})",
|
|
1157
|
+
[adapter._namespace, *known_agent_ids],
|
|
1158
|
+
)
|
|
1159
|
+
for row in cur.fetchall():
|
|
1160
|
+
agent_registration[row["agent_id"]] = {"created_at": row["created_at"]}
|
|
1161
|
+
except (ImportError, Exception):
|
|
1162
|
+
pass
|
|
936
1163
|
|
|
937
1164
|
agent_descriptions: dict[str, dict[str, Any]] = {}
|
|
938
1165
|
try:
|
|
@@ -949,11 +1176,14 @@ async def list_agents(
|
|
|
949
1176
|
agents = []
|
|
950
1177
|
for ad in sorted(agent_data.values(), key=lambda x: x["entries_written"], reverse=True):
|
|
951
1178
|
desc_info = agent_descriptions.get(ad["agent_id"], {})
|
|
1179
|
+
reg = agent_registration.get(ad["agent_id"], {})
|
|
1180
|
+
created = reg.get("created_at") or ad.get("first_seen")
|
|
952
1181
|
agents.append({
|
|
953
1182
|
"agentId": ad["agent_id"],
|
|
954
1183
|
"entriesWritten": ad["entries_written"],
|
|
955
1184
|
"entitiesTouched": len(ad["entities_touched"]),
|
|
956
1185
|
"lastActive": ad["last_active"].isoformat() if ad["last_active"] else None,
|
|
1186
|
+
"createdAt": created.isoformat() if created else None,
|
|
957
1187
|
"description": desc_info.get("description", ""),
|
|
958
1188
|
"platform": desc_info.get("platform", ""),
|
|
959
1189
|
})
|
|
@@ -987,6 +1217,7 @@ async def agent_memory_graph(
|
|
|
987
1217
|
"writtenAt": e.provenance.written_at.isoformat(),
|
|
988
1218
|
})
|
|
989
1219
|
|
|
1220
|
+
# Build read counts from both traces (causal_entries) and timeline events.
|
|
990
1221
|
read_entities: dict[str, dict[str, int]] = {}
|
|
991
1222
|
for t in traces:
|
|
992
1223
|
for ce in t.causal_entries:
|
|
@@ -996,6 +1227,26 @@ async def agent_memory_graph(
|
|
|
996
1227
|
read_entities[ep] = {}
|
|
997
1228
|
read_entities[ep][key] = read_entities[ep].get(key, 0) + 1
|
|
998
1229
|
|
|
1230
|
+
# Supplement with READ events from the timeline (persisted independently).
|
|
1231
|
+
total_read_events = 0
|
|
1232
|
+
try:
|
|
1233
|
+
read_events = mem._adapter.list_events(
|
|
1234
|
+
agent_id, mem.namespace, event_type="read", limit=10000,
|
|
1235
|
+
)
|
|
1236
|
+
cross_read_events = mem._adapter.list_events(
|
|
1237
|
+
agent_id, mem.namespace, event_type="cross_agent_read", limit=10000,
|
|
1238
|
+
)
|
|
1239
|
+
for ev in read_events + cross_read_events:
|
|
1240
|
+
ep = ev.details.get("entity_path", "")
|
|
1241
|
+
key = ev.details.get("key", "")
|
|
1242
|
+
if ep and key:
|
|
1243
|
+
if ep not in read_entities:
|
|
1244
|
+
read_entities[ep] = {}
|
|
1245
|
+
read_entities[ep][key] = read_entities[ep].get(key, 0) + 1
|
|
1246
|
+
total_read_events = len(read_events) + len(cross_read_events)
|
|
1247
|
+
except Exception:
|
|
1248
|
+
pass
|
|
1249
|
+
|
|
999
1250
|
entry_authors: dict[str, str] = {}
|
|
1000
1251
|
for e in entries:
|
|
1001
1252
|
entry_authors[f"{e.entity_path}/{e.key}"] = e.provenance.agent_id
|
|
@@ -1026,6 +1277,7 @@ async def agent_memory_graph(
|
|
|
1026
1277
|
"nodes": nodes,
|
|
1027
1278
|
"traceCount": len(traces),
|
|
1028
1279
|
"totalWritten": len(written_by_agent),
|
|
1280
|
+
"totalReads": total_read_events,
|
|
1029
1281
|
"crossAgentReads": cross_agent_reads,
|
|
1030
1282
|
}
|
|
1031
1283
|
|
|
@@ -2874,9 +3126,10 @@ async def cortex_status(
|
|
|
2874
3126
|
@app.get("/api/v1/cortex/digests")
|
|
2875
3127
|
async def list_cortex_digests(
|
|
2876
3128
|
digest_type: str | None = Query(None),
|
|
3129
|
+
scope: str | None = Query(None),
|
|
2877
3130
|
_auth: str | None = Depends(verify_api_key),
|
|
2878
3131
|
) -> dict[str, Any]:
|
|
2879
|
-
"""List
|
|
3132
|
+
"""List compiled digests, optionally filtered by type and scope."""
|
|
2880
3133
|
try:
|
|
2881
3134
|
from amfs_postgres.adapter import PostgresAdapter
|
|
2882
3135
|
from amfs_core.models import DigestType
|
|
@@ -2885,13 +3138,18 @@ async def list_cortex_digests(
|
|
|
2885
3138
|
adapter = mem._adapter
|
|
2886
3139
|
if isinstance(adapter, PostgresAdapter):
|
|
2887
3140
|
dt = DigestType(digest_type) if digest_type else None
|
|
2888
|
-
|
|
3141
|
+
namespace = getattr(adapter, "_namespace", "default")
|
|
3142
|
+
digests = adapter.list_digests(digest_type=dt, namespace=namespace)
|
|
3143
|
+
if scope:
|
|
3144
|
+
digests = [d for d in digests if d.scope == scope]
|
|
2889
3145
|
return {
|
|
2890
3146
|
"digests": [d.model_dump(mode="json") for d in digests],
|
|
2891
3147
|
"total": len(digests),
|
|
2892
3148
|
}
|
|
2893
|
-
except (ImportError, ValueError
|
|
3149
|
+
except (ImportError, ValueError):
|
|
2894
3150
|
pass
|
|
3151
|
+
except Exception:
|
|
3152
|
+
logger.exception("Failed to list cortex digests")
|
|
2895
3153
|
|
|
2896
3154
|
return {"digests": [], "total": 0}
|
|
2897
3155
|
|
|
@@ -2914,6 +3172,17 @@ async def cortex_activity(
|
|
|
2914
3172
|
return {"events": [], "total": 0, "throughput": [], "stats": None}
|
|
2915
3173
|
|
|
2916
3174
|
|
|
3175
|
+
@app.post("/api/v1/cortex/recompile")
|
|
3176
|
+
async def cortex_recompile(
|
|
3177
|
+
_auth: str | None = Depends(verify_api_key),
|
|
3178
|
+
) -> dict[str, Any]:
|
|
3179
|
+
"""Trigger a full digest recompilation."""
|
|
3180
|
+
if not _cortex_worker:
|
|
3181
|
+
raise HTTPException(status_code=503, detail="Cortex worker not running")
|
|
3182
|
+
count = _cortex_worker._compiler.recompile_all()
|
|
3183
|
+
return {"recompiled": count}
|
|
3184
|
+
|
|
3185
|
+
|
|
2917
3186
|
@app.post("/api/v1/webhooks/{connector_name}")
|
|
2918
3187
|
async def ingest_webhook(
|
|
2919
3188
|
connector_name: str,
|
|
@@ -3122,6 +3391,8 @@ def main() -> None:
|
|
|
3122
3391
|
logger.info("Embedded Cortex worker started")
|
|
3123
3392
|
except ImportError:
|
|
3124
3393
|
logger.warning("--with-cortex requires amfs-cortex package")
|
|
3394
|
+
except Exception:
|
|
3395
|
+
logger.exception("Failed to start embedded Cortex worker — server will run without Cortex")
|
|
3125
3396
|
else:
|
|
3126
3397
|
logger.warning("--with-cortex requires AMFS_POSTGRES_DSN")
|
|
3127
3398
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|