@pentatonic-ai/ai-agent-sdk 0.9.5 → 0.10.0
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.
- package/README.md +3 -3
- package/bin/cli.js +1 -1
- package/bin/commands/config.js +1 -1
- package/dist/index.cjs +39 -72
- package/dist/index.js +36 -69
- package/package.json +10 -3
- package/packages/doctor/src/checks/local-memory.js +2 -2
- package/packages/memory/README.md +2 -2
- package/packages/memory/openclaw-plugin/README.md +2 -2
- package/packages/memory/openclaw-plugin/openclaw.plugin.json +1 -1
- package/packages/memory/package-lock.json +49 -33
- package/packages/memory/package.json +4 -1
- package/packages/memory/src/__tests__/engine.test.js +40 -5
- package/packages/memory/src/engine.js +38 -3
- package/packages/memory/src/server.js +2 -2
- package/packages/memory-engine-v2/.env.example +30 -0
- package/packages/memory-engine-v2/README.md +125 -0
- package/packages/memory-engine-v2/compat/Dockerfile +11 -0
- package/packages/memory-engine-v2/compat/requirements.txt +6 -0
- package/packages/memory-engine-v2/compat/server.py +1047 -0
- package/packages/memory-engine-v2/docker-compose.aws.yml +78 -0
- package/packages/memory-engine-v2/docker-compose.yml +206 -0
- package/packages/memory-engine-v2/extractor-async/Dockerfile +14 -0
- package/packages/memory-engine-v2/extractor-async/confidence.py +62 -0
- package/packages/memory-engine-v2/extractor-async/noise_filter.py +144 -0
- package/packages/memory-engine-v2/extractor-async/requirements.txt +2 -0
- package/packages/memory-engine-v2/extractor-async/test_confidence.py +76 -0
- package/packages/memory-engine-v2/extractor-async/test_noise_filter.py +177 -0
- package/packages/memory-engine-v2/extractor-async/worker.py +797 -0
- package/packages/memory-engine-v2/extractor-sync/Dockerfile +11 -0
- package/packages/memory-engine-v2/extractor-sync/requirements.txt +4 -0
- package/packages/memory-engine-v2/extractor-sync/server.py +424 -0
- package/packages/memory-engine-v2/org-model/migrations/001_init.sql +390 -0
- package/packages/memory-engine-v2/tests/e2e_smoke.py +356 -0
- package/packages/memory-engine-v2/tests/fixtures/generate_synthetic_corpus.py +758 -0
- package/packages/memory-engine/.env.example +0 -13
- package/packages/memory-engine/MIGRATION.md +0 -219
- package/packages/memory-engine/README.md +0 -145
- package/packages/memory-engine/bench/README.md +0 -99
- package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +0 -961
- package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +0 -937
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +0 -961
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +0 -883
- package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +0 -1115
- package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +0 -819
- package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +0 -1278
- package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +0 -1018
- package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +0 -1038
- package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +0 -937
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +0 -1115
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +0 -1115
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +0 -819
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +0 -542
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +0 -1278
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +0 -894
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +0 -1018
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +0 -680
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +0 -1038
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +0 -693
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +0 -961
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +0 -727
- package/packages/memory-engine/compat/Dockerfile +0 -22
- package/packages/memory-engine/compat/server.py +0 -1255
- package/packages/memory-engine/docker-compose.test.yml +0 -59
- package/packages/memory-engine/docker-compose.yml +0 -240
- package/packages/memory-engine/engine/README.md +0 -52
- package/packages/memory-engine/engine/l2-hybridrag-proxy.py +0 -1543
- package/packages/memory-engine/engine/l5-comms-layer.py +0 -663
- package/packages/memory-engine/engine/l6-document-store.py +0 -1018
- package/packages/memory-engine/engine/services/_shared/__init__.py +0 -1
- package/packages/memory-engine/engine/services/_shared/embed_provider.py +0 -468
- package/packages/memory-engine/engine/services/l2/Dockerfile +0 -50
- package/packages/memory-engine/engine/services/l2/init_databases.py +0 -81
- package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +0 -2721
- package/packages/memory-engine/engine/services/l5/Dockerfile +0 -11
- package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +0 -808
- package/packages/memory-engine/engine/services/l6/Dockerfile +0 -30
- package/packages/memory-engine/engine/services/l6/l6-document-store.py +0 -1221
- package/packages/memory-engine/engine/services/nv-embed/Dockerfile +0 -28
- package/packages/memory-engine/engine/services/nv-embed/server.py +0 -152
- package/packages/memory-engine/pme_memory/__init__.py +0 -0
- package/packages/memory-engine/pme_memory/__main__.py +0 -129
- package/packages/memory-engine/pme_memory/artifacts.py +0 -95
- package/packages/memory-engine/pme_memory/embed.py +0 -74
- package/packages/memory-engine/pme_memory/health.py +0 -36
- package/packages/memory-engine/pme_memory/hygiene.py +0 -159
- package/packages/memory-engine/pme_memory/indexer.py +0 -200
- package/packages/memory-engine/pme_memory/needs.py +0 -55
- package/packages/memory-engine/pme_memory/provenance.py +0 -80
- package/packages/memory-engine/pme_memory/scoring.py +0 -168
- package/packages/memory-engine/pme_memory/search.py +0 -52
- package/packages/memory-engine/pme_memory/store.py +0 -86
- package/packages/memory-engine/pme_memory/synthesis.py +0 -114
- package/packages/memory-engine/pyproject.toml +0 -65
- package/packages/memory-engine/scripts/kg-extractor.py +0 -557
- package/packages/memory-engine/scripts/kg-preflexor-v2.py +0 -738
- package/packages/memory-engine/scripts/wipe-legacy-l3-entities.py +0 -128
- package/packages/memory-engine/tests/e2e_arena.sh +0 -259
- package/packages/memory-engine/tests/embed_stub/Dockerfile +0 -13
- package/packages/memory-engine/tests/embed_stub/server.py +0 -80
- package/packages/memory-engine/tests/test_aggregate.py +0 -333
- package/packages/memory-engine/tests/test_api_contract.sh +0 -57
- package/packages/memory-engine/tests/test_arena_safety.py +0 -232
- package/packages/memory-engine/tests/test_channel_stat_reader.py +0 -437
- package/packages/memory-engine/tests/test_channel_stat_rollups.py +0 -308
- package/packages/memory-engine/tests/test_compat_nv_embed_probe.py +0 -48
- package/packages/memory-engine/tests/test_embed_provider.py +0 -492
- package/packages/memory-engine/tests/test_l2_qmd_vec_search.py +0 -280
- package/packages/memory-engine/tests/test_l3_arena_isolation.py +0 -412
- package/packages/memory-engine/tests/test_l6_module_load.py +0 -84
- package/packages/memory-engine/tests/test_people_list_reader.py +0 -432
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Wipe pre-arena-scoping :Entity nodes from the L3 Neo4j graph.
|
|
3
|
-
|
|
4
|
-
Run this AFTER the engine has been deployed with the arena-scoped
|
|
5
|
-
writer paths, not before. Sequence:
|
|
6
|
-
|
|
7
|
-
1. Deploy l2-hybridrag-proxy with arena-scoped MERGE patterns.
|
|
8
|
-
2. Verify new ingest is creating arena-tagged entities (run
|
|
9
|
-
--dry-run on this script first; it reports legacy vs new).
|
|
10
|
-
3. Run this script with --confirm to wipe legacy entities.
|
|
11
|
-
4. Future ingest re-extracts entities from existing :Chunk nodes
|
|
12
|
-
on-demand (search-side touches them; new stores recreate them
|
|
13
|
-
from scratch under the right arena).
|
|
14
|
-
|
|
15
|
-
Why wipe vs. backfill: pre-fix entities collapsed cross-tenant by name,
|
|
16
|
-
so their MENTIONS edges connect to chunks across multiple arenas.
|
|
17
|
-
Splitting them by mentions is doable but error-prone (edge cases for
|
|
18
|
-
many-arena entities, orphans, no-mention entities). The Hebbian weights
|
|
19
|
-
on those edges were also corrupted by cross-tenant traffic, so they
|
|
20
|
-
weren't worth saving. Chunks are preserved either way — re-extraction
|
|
21
|
-
is cheap.
|
|
22
|
-
|
|
23
|
-
Usage:
|
|
24
|
-
# report counts only
|
|
25
|
-
python wipe-legacy-l3-entities.py --neo4j-uri bolt://localhost:7687
|
|
26
|
-
|
|
27
|
-
# actually wipe
|
|
28
|
-
python wipe-legacy-l3-entities.py --neo4j-uri bolt://localhost:7687 --confirm
|
|
29
|
-
|
|
30
|
-
The script is idempotent — running it twice on a clean graph deletes
|
|
31
|
-
zero rows.
|
|
32
|
-
"""
|
|
33
|
-
from __future__ import annotations
|
|
34
|
-
|
|
35
|
-
import argparse
|
|
36
|
-
import os
|
|
37
|
-
import sys
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
from neo4j import GraphDatabase
|
|
41
|
-
except ImportError:
|
|
42
|
-
print("ERROR: neo4j driver not installed. `pip install neo4j` first.", file=sys.stderr)
|
|
43
|
-
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def main() -> int:
|
|
47
|
-
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
48
|
-
parser.add_argument(
|
|
49
|
-
"--neo4j-uri",
|
|
50
|
-
default=os.environ.get("NEO4J_URI", "bolt://localhost:7687"),
|
|
51
|
-
help="Neo4j bolt URI (default: NEO4J_URI env or bolt://localhost:7687)",
|
|
52
|
-
)
|
|
53
|
-
parser.add_argument(
|
|
54
|
-
"--neo4j-user",
|
|
55
|
-
default=os.environ.get("NEO4J_USER", "neo4j"),
|
|
56
|
-
)
|
|
57
|
-
parser.add_argument(
|
|
58
|
-
"--neo4j-password",
|
|
59
|
-
default=os.environ.get("NEO4J_PASSWORD"),
|
|
60
|
-
)
|
|
61
|
-
parser.add_argument(
|
|
62
|
-
"--confirm",
|
|
63
|
-
action="store_true",
|
|
64
|
-
help="Actually delete. Without this flag, runs in dry-run mode "
|
|
65
|
-
"(reports counts only).",
|
|
66
|
-
)
|
|
67
|
-
args = parser.parse_args()
|
|
68
|
-
|
|
69
|
-
if not args.neo4j_password:
|
|
70
|
-
print("ERROR: --neo4j-password or NEO4J_PASSWORD env required", file=sys.stderr)
|
|
71
|
-
return 2
|
|
72
|
-
|
|
73
|
-
driver = GraphDatabase.driver(
|
|
74
|
-
args.neo4j_uri, auth=(args.neo4j_user, args.neo4j_password),
|
|
75
|
-
)
|
|
76
|
-
try:
|
|
77
|
-
with driver.session() as session:
|
|
78
|
-
# Count legacy vs arena-scoped entities so the operator can
|
|
79
|
-
# eyeball whether the new writer path has actually started
|
|
80
|
-
# producing arena-scoped rows before deleting anything.
|
|
81
|
-
legacy = session.run(
|
|
82
|
-
"MATCH (e:Entity) WHERE e.arena IS NULL RETURN count(e) AS n"
|
|
83
|
-
).single()["n"]
|
|
84
|
-
scoped = session.run(
|
|
85
|
-
"MATCH (e:Entity) WHERE e.arena IS NOT NULL RETURN count(e) AS n"
|
|
86
|
-
).single()["n"]
|
|
87
|
-
print(f"L3 Entity nodes: {legacy} legacy (no arena) / {scoped} arena-scoped")
|
|
88
|
-
|
|
89
|
-
# Same breakdown for chunks — they should already all be
|
|
90
|
-
# arena-tagged but worth verifying before/after.
|
|
91
|
-
chunk_legacy = session.run(
|
|
92
|
-
"MATCH (c:Chunk) WHERE c.arena IS NULL RETURN count(c) AS n"
|
|
93
|
-
).single()["n"]
|
|
94
|
-
chunk_scoped = session.run(
|
|
95
|
-
"MATCH (c:Chunk) WHERE c.arena IS NOT NULL RETURN count(c) AS n"
|
|
96
|
-
).single()["n"]
|
|
97
|
-
print(f"L3 Chunk nodes: {chunk_legacy} legacy (no arena) / {chunk_scoped} arena-scoped")
|
|
98
|
-
|
|
99
|
-
if not args.confirm:
|
|
100
|
-
print("\nDry run — pass --confirm to actually delete legacy entities.")
|
|
101
|
-
return 0
|
|
102
|
-
|
|
103
|
-
if legacy == 0:
|
|
104
|
-
print("\nNothing to do — all entities already arena-scoped.")
|
|
105
|
-
return 0
|
|
106
|
-
|
|
107
|
-
print(f"\nWiping {legacy} legacy entities…")
|
|
108
|
-
result = session.run(
|
|
109
|
-
"MATCH (e:Entity) WHERE e.arena IS NULL DETACH DELETE e RETURN count(e) AS n"
|
|
110
|
-
).single()
|
|
111
|
-
deleted = result["n"]
|
|
112
|
-
print(f"Deleted {deleted} legacy entities.")
|
|
113
|
-
|
|
114
|
-
# Drop the old single-key entity index now that the data
|
|
115
|
-
# is gone — safe to do because the new writer path uses
|
|
116
|
-
# the compound (arena, name) index.
|
|
117
|
-
try:
|
|
118
|
-
session.run("DROP INDEX entity_name IF EXISTS")
|
|
119
|
-
print("Dropped legacy `entity_name` index.")
|
|
120
|
-
except Exception as e: # noqa: BLE001
|
|
121
|
-
print(f"Note: could not drop entity_name index ({e}); ok if absent.")
|
|
122
|
-
return 0
|
|
123
|
-
finally:
|
|
124
|
-
driver.close()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if __name__ == "__main__":
|
|
128
|
-
sys.exit(main())
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# e2e_arena.sh — multi-tenant store/retrieve smoke test against a live
|
|
3
|
-
# memory-engine stack. Exercises /store, arena-scoped /search, and
|
|
4
|
-
# /forget end-to-end across L0/L4/L5/L6 + the compat shim.
|
|
5
|
-
#
|
|
6
|
-
# Run after `docker compose -f docker-compose.yml -f docker-compose.test.yml \
|
|
7
|
-
# up -d --wait l3 l4 l5 l6 l2 compat embed-stub`.
|
|
8
|
-
set -eu
|
|
9
|
-
|
|
10
|
-
BASE="${BASE:-http://localhost:8099}"
|
|
11
|
-
WAIT_HEALTH_SECS="${WAIT_HEALTH_SECS:-180}"
|
|
12
|
-
PASS=0
|
|
13
|
-
FAIL=0
|
|
14
|
-
|
|
15
|
-
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
|
|
16
|
-
fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
|
|
17
|
-
|
|
18
|
-
# ---------------------------------------------------------------------------
|
|
19
|
-
# Wait for the compat shim to come up. Its /health aggregates layer
|
|
20
|
-
# health; we accept "ok" or "degraded" (l3 cosmetic 404 is known and
|
|
21
|
-
# doesn't block functional paths).
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
echo "=== waiting for $BASE/health (up to ${WAIT_HEALTH_SECS}s) ==="
|
|
25
|
-
deadline=$(( $(date +%s) + WAIT_HEALTH_SECS ))
|
|
26
|
-
while :; do
|
|
27
|
-
if H=$(curl -sf --max-time 5 "$BASE/health"); then
|
|
28
|
-
s=$(echo "$H" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("status",""))')
|
|
29
|
-
if [ "$s" = "ok" ] || [ "$s" = "degraded" ]; then
|
|
30
|
-
echo " health: $s"
|
|
31
|
-
break
|
|
32
|
-
fi
|
|
33
|
-
fi
|
|
34
|
-
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
35
|
-
echo " ❌ engine never became healthy"
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
sleep 3
|
|
39
|
-
done
|
|
40
|
-
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
# /store — two arenas, two distinct documents per arena.
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
echo ""
|
|
46
|
-
echo "=== /store ==="
|
|
47
|
-
post() {
|
|
48
|
-
curl -sf -X POST "$BASE/store" \
|
|
49
|
-
-H "Content-Type: application/json" \
|
|
50
|
-
-d "$1"
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
R1=$(post '{"content":"Alpha team owns project Atlas","metadata":{"arena":"e2e-tenant-a","kind":"note","probe":"e2e-arena"}}')
|
|
54
|
-
R2=$(post '{"content":"Alpha team owns project Borealis","metadata":{"arena":"e2e-tenant-b","kind":"note","probe":"e2e-arena"}}')
|
|
55
|
-
R3=$(post '{"content":"Bravo team owns project Cobalt","metadata":{"arena":"e2e-tenant-a","kind":"note","probe":"e2e-arena"}}')
|
|
56
|
-
R4=$(post '{"content":"Bravo team owns project Diamond","metadata":{"arena":"e2e-tenant-b","kind":"note","probe":"e2e-arena"}}')
|
|
57
|
-
|
|
58
|
-
[ -n "$R1" ] && [ -n "$R2" ] && [ -n "$R3" ] && [ -n "$R4" ] \
|
|
59
|
-
&& ok "stored 4 docs across 2 arenas" \
|
|
60
|
-
|| fail "store"
|
|
61
|
-
|
|
62
|
-
# Indexing is async on some layers — give the stack a brief settle.
|
|
63
|
-
sleep 4
|
|
64
|
-
|
|
65
|
-
# ---------------------------------------------------------------------------
|
|
66
|
-
# /search — arena scoping. tenant-a should never see Borealis/Diamond,
|
|
67
|
-
# tenant-b should never see Atlas/Cobalt.
|
|
68
|
-
# ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
echo ""
|
|
71
|
-
echo "=== /search arena=e2e-tenant-a ==="
|
|
72
|
-
SA=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
73
|
-
-d '{"query":"team project","limit":20,"arena":"e2e-tenant-a"}')
|
|
74
|
-
echo " hits: $(echo "$SA" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')"
|
|
75
|
-
|
|
76
|
-
leak_a=$(echo "$SA" | python3 -c '
|
|
77
|
-
import json,sys
|
|
78
|
-
data=json.load(sys.stdin).get("results",[])
|
|
79
|
-
bad=[r for r in data if "Borealis" in r.get("content","") or "Diamond" in r.get("content","")]
|
|
80
|
-
print(len(bad))')
|
|
81
|
-
[ "$leak_a" = "0" ] && ok "tenant-a: no Borealis/Diamond leakage" \
|
|
82
|
-
|| fail "tenant-a leaked $leak_a tenant-b docs"
|
|
83
|
-
|
|
84
|
-
found_atlas=$(echo "$SA" | python3 -c '
|
|
85
|
-
import json,sys
|
|
86
|
-
data=json.load(sys.stdin).get("results",[])
|
|
87
|
-
print("yes" if any("Atlas" in r.get("content","") for r in data) else "no")')
|
|
88
|
-
[ "$found_atlas" = "yes" ] && ok "tenant-a: Atlas recovered" \
|
|
89
|
-
|| fail "tenant-a missing Atlas"
|
|
90
|
-
|
|
91
|
-
echo ""
|
|
92
|
-
echo "=== /search arena=e2e-tenant-b ==="
|
|
93
|
-
SB=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
94
|
-
-d '{"query":"team project","limit":20,"arena":"e2e-tenant-b"}')
|
|
95
|
-
echo " hits: $(echo "$SB" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')"
|
|
96
|
-
|
|
97
|
-
leak_b=$(echo "$SB" | python3 -c '
|
|
98
|
-
import json,sys
|
|
99
|
-
data=json.load(sys.stdin).get("results",[])
|
|
100
|
-
bad=[r for r in data if "Atlas" in r.get("content","") or "Cobalt" in r.get("content","")]
|
|
101
|
-
print(len(bad))')
|
|
102
|
-
[ "$leak_b" = "0" ] && ok "tenant-b: no Atlas/Cobalt leakage" \
|
|
103
|
-
|| fail "tenant-b leaked $leak_b tenant-a docs"
|
|
104
|
-
|
|
105
|
-
found_borealis=$(echo "$SB" | python3 -c '
|
|
106
|
-
import json,sys
|
|
107
|
-
data=json.load(sys.stdin).get("results",[])
|
|
108
|
-
print("yes" if any("Borealis" in r.get("content","") for r in data) else "no")')
|
|
109
|
-
[ "$found_borealis" = "yes" ] && ok "tenant-b: Borealis recovered" \
|
|
110
|
-
|| fail "tenant-b missing Borealis"
|
|
111
|
-
|
|
112
|
-
# ---------------------------------------------------------------------------
|
|
113
|
-
# /search with metadata_filter — arena+probe combo should still scope.
|
|
114
|
-
# ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
echo ""
|
|
117
|
-
echo "=== /search metadata_filter probe=e2e-arena ==="
|
|
118
|
-
SF=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
119
|
-
-d '{"query":"team","limit":20,"arena":"e2e-tenant-a","metadata_filter":{"probe":"e2e-arena"}}')
|
|
120
|
-
all_match=$(echo "$SF" | python3 -c '
|
|
121
|
-
import json,sys
|
|
122
|
-
data=json.load(sys.stdin).get("results",[])
|
|
123
|
-
ok=all(r.get("metadata",{}).get("probe")=="e2e-arena" and r.get("metadata",{}).get("arena")=="e2e-tenant-a" for r in data)
|
|
124
|
-
print("yes" if ok and data else "no")')
|
|
125
|
-
[ "$all_match" = "yes" ] && ok "metadata_filter scopes to probe + arena" \
|
|
126
|
-
|| fail "metadata_filter let other rows through"
|
|
127
|
-
|
|
128
|
-
# ---------------------------------------------------------------------------
|
|
129
|
-
# User-scope vs tenant-wide arenas — proves the multi-arena search model.
|
|
130
|
-
#
|
|
131
|
-
# tenant-wide row arena=acme (visible to every user in acme)
|
|
132
|
-
# user-A's row arena=acme:user-a (only user-A retrieves it)
|
|
133
|
-
# user-B's row arena=acme:user-b (only user-B retrieves it)
|
|
134
|
-
#
|
|
135
|
-
# A user-scoped search sends arenas=[acme, acme:userX] so the user sees
|
|
136
|
-
# tenant-wide AND own user-scope, but never another user's user-scope.
|
|
137
|
-
# ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
echo ""
|
|
140
|
-
echo "=== user-scope vs tenant-wide ==="
|
|
141
|
-
post '{"content":"acme tenant-wide rules of engagement","metadata":{"arena":"acme","probe":"e2e-arena"}}' >/dev/null
|
|
142
|
-
post '{"content":"alice private note about Project Mercury","metadata":{"arena":"acme:alice","probe":"e2e-arena"}}' >/dev/null
|
|
143
|
-
post '{"content":"bob private note about Project Saturn","metadata":{"arena":"acme:bob","probe":"e2e-arena"}}' >/dev/null
|
|
144
|
-
sleep 3
|
|
145
|
-
|
|
146
|
-
# Search as alice: arenas=[acme, acme:alice] — should see tenant-wide + own
|
|
147
|
-
SAlice=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
148
|
-
-d '{"query":"Project rules note","limit":20,"arenas":["acme","acme:alice"]}')
|
|
149
|
-
|
|
150
|
-
alice_sees_tenant=$(echo "$SAlice" | python3 -c '
|
|
151
|
-
import json,sys
|
|
152
|
-
data=json.load(sys.stdin).get("results",[])
|
|
153
|
-
print("yes" if any("tenant-wide" in r.get("content","") for r in data) else "no")')
|
|
154
|
-
alice_sees_own=$(echo "$SAlice" | python3 -c '
|
|
155
|
-
import json,sys
|
|
156
|
-
data=json.load(sys.stdin).get("results",[])
|
|
157
|
-
print("yes" if any("Mercury" in r.get("content","") for r in data) else "no")')
|
|
158
|
-
alice_leak_bob=$(echo "$SAlice" | python3 -c '
|
|
159
|
-
import json,sys
|
|
160
|
-
data=json.load(sys.stdin).get("results",[])
|
|
161
|
-
print(sum(1 for r in data if "Saturn" in r.get("content","")))')
|
|
162
|
-
|
|
163
|
-
[ "$alice_sees_tenant" = "yes" ] && ok "alice: tenant-wide visible" \
|
|
164
|
-
|| fail "alice: missing tenant-wide row"
|
|
165
|
-
[ "$alice_sees_own" = "yes" ] && ok "alice: own user-scope visible" \
|
|
166
|
-
|| fail "alice: missing own user-scope row"
|
|
167
|
-
[ "$alice_leak_bob" = "0" ] && ok "alice: no leakage of bob's user-scope" \
|
|
168
|
-
|| fail "alice leaked $alice_leak_bob bob rows (cross-user!)"
|
|
169
|
-
|
|
170
|
-
# Search as bob: arenas=[acme, acme:bob] — should see tenant-wide + own
|
|
171
|
-
SBob=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
172
|
-
-d '{"query":"Project rules note","limit":20,"arenas":["acme","acme:bob"]}')
|
|
173
|
-
|
|
174
|
-
bob_sees_own=$(echo "$SBob" | python3 -c '
|
|
175
|
-
import json,sys
|
|
176
|
-
data=json.load(sys.stdin).get("results",[])
|
|
177
|
-
print("yes" if any("Saturn" in r.get("content","") for r in data) else "no")')
|
|
178
|
-
bob_leak_alice=$(echo "$SBob" | python3 -c '
|
|
179
|
-
import json,sys
|
|
180
|
-
data=json.load(sys.stdin).get("results",[])
|
|
181
|
-
print(sum(1 for r in data if "Mercury" in r.get("content","")))')
|
|
182
|
-
|
|
183
|
-
[ "$bob_sees_own" = "yes" ] && ok "bob: own user-scope visible" \
|
|
184
|
-
|| fail "bob: missing own user-scope row"
|
|
185
|
-
[ "$bob_leak_alice" = "0" ] && ok "bob: no leakage of alice's user-scope" \
|
|
186
|
-
|| fail "bob leaked $bob_leak_alice alice rows (cross-user!)"
|
|
187
|
-
|
|
188
|
-
# ---------------------------------------------------------------------------
|
|
189
|
-
# Same content across two arenas — proves the arena-aware id derivation.
|
|
190
|
-
# Pre-v0.7.8, identical content collapsed to one row in L4/L5/L6 because
|
|
191
|
-
# the id was sha1(content); the second tenant's write overwrote the first.
|
|
192
|
-
# ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
echo ""
|
|
195
|
-
echo "=== same-content-across-arenas ==="
|
|
196
|
-
post '{"content":"shared phrase about Eclipse","metadata":{"arena":"e2e-tenant-x","probe":"e2e-arena"}}' >/dev/null
|
|
197
|
-
post '{"content":"shared phrase about Eclipse","metadata":{"arena":"e2e-tenant-y","probe":"e2e-arena"}}' >/dev/null
|
|
198
|
-
sleep 3
|
|
199
|
-
|
|
200
|
-
SX=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
201
|
-
-d '{"query":"Eclipse","limit":10,"arena":"e2e-tenant-x"}')
|
|
202
|
-
SY=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
203
|
-
-d '{"query":"Eclipse","limit":10,"arena":"e2e-tenant-y"}')
|
|
204
|
-
|
|
205
|
-
x_has_phrase=$(echo "$SX" | python3 -c '
|
|
206
|
-
import json,sys
|
|
207
|
-
data=json.load(sys.stdin).get("results",[])
|
|
208
|
-
print("yes" if any("Eclipse" in r.get("content","") for r in data) else "no")')
|
|
209
|
-
y_has_phrase=$(echo "$SY" | python3 -c '
|
|
210
|
-
import json,sys
|
|
211
|
-
data=json.load(sys.stdin).get("results",[])
|
|
212
|
-
print("yes" if any("Eclipse" in r.get("content","") for r in data) else "no")')
|
|
213
|
-
|
|
214
|
-
[ "$x_has_phrase" = "yes" ] && ok "tenant-x: shared phrase preserved" \
|
|
215
|
-
|| fail "tenant-x lost the shared phrase (id collision?)"
|
|
216
|
-
[ "$y_has_phrase" = "yes" ] && ok "tenant-y: shared phrase preserved" \
|
|
217
|
-
|| fail "tenant-y lost the shared phrase (id collision?)"
|
|
218
|
-
|
|
219
|
-
# ---------------------------------------------------------------------------
|
|
220
|
-
# /forget — tenant-scoped delete by arena.
|
|
221
|
-
#
|
|
222
|
-
# Pre-arena, /forget would unconditionally trigger a global wipe of
|
|
223
|
-
# L0+L4+L3 inside the shim, regardless of what filters were passed. The
|
|
224
|
-
# smoke test happily asserted "deleted >= 1" because the shim returned
|
|
225
|
-
# the count of every row globally, even rows the caller didn't ask to
|
|
226
|
-
# remove. That was the right thing for a bench reset, the wrong thing
|
|
227
|
-
# for any real tenant operation.
|
|
228
|
-
#
|
|
229
|
-
# Now /forget honours the arena in the metadata filter:
|
|
230
|
-
# - {metadata_contains: {arena: <tenant>}} → tenant-scoped wipe
|
|
231
|
-
# - {metadata_contains: {<other>: <val>}} → L6 targeted only
|
|
232
|
-
# - {} (empty body) → bench reset (gated
|
|
233
|
-
# by GLOBAL_WIPE in the
|
|
234
|
-
# internal endpoint)
|
|
235
|
-
#
|
|
236
|
-
# We assert here that wiping arena=e2e-tenant-a actually removes ≥1 row,
|
|
237
|
-
# AND that arena=e2e-tenant-b's rows survived — proves real tenant
|
|
238
|
-
# isolation rather than just "shim returned non-zero".
|
|
239
|
-
# ---------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
echo ""
|
|
242
|
-
echo "=== /forget arena=e2e-tenant-a ==="
|
|
243
|
-
F=$(curl -sf -X POST "$BASE/forget" -H "Content-Type: application/json" \
|
|
244
|
-
-d '{"metadata_contains":{"arena":"e2e-tenant-a"}}')
|
|
245
|
-
deleted=$(echo "$F" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("deleted",0))')
|
|
246
|
-
echo " deleted: $deleted"
|
|
247
|
-
[ "$deleted" -ge "1" ] && ok "/forget removed tenant-a rows" || fail "/forget tenant-a wipe"
|
|
248
|
-
|
|
249
|
-
# tenant-b should still have its rows.
|
|
250
|
-
B=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
|
|
251
|
-
-d '{"arena":"e2e-tenant-b","query":"Borealis","limit":4}')
|
|
252
|
-
b_hits=$(echo "$B" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')
|
|
253
|
-
[ "$b_hits" -ge "1" ] && ok "/forget left tenant-b intact" || fail "/forget leaked into tenant-b"
|
|
254
|
-
|
|
255
|
-
echo ""
|
|
256
|
-
echo "=== Result ==="
|
|
257
|
-
echo " PASS: $PASS"
|
|
258
|
-
echo " FAIL: $FAIL"
|
|
259
|
-
exit $FAIL
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
FROM python:3.12-slim
|
|
2
|
-
|
|
3
|
-
WORKDIR /app
|
|
4
|
-
|
|
5
|
-
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" pydantic
|
|
6
|
-
|
|
7
|
-
COPY server.py /app/server.py
|
|
8
|
-
|
|
9
|
-
ENV EMBED_DIM=4096
|
|
10
|
-
|
|
11
|
-
EXPOSE 8041
|
|
12
|
-
|
|
13
|
-
CMD ["python", "-m", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8041"]
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""Deterministic embedding stub for hermetic CI runs.
|
|
2
|
-
|
|
3
|
-
Returns a fixed-dim vector per input string, derived from a hash so the
|
|
4
|
-
same text always maps to the same vector. Cosine similarity between two
|
|
5
|
-
embeddings equals 1.0 only for identical input strings, and decreases
|
|
6
|
-
roughly with edit distance — enough to exercise the engine's vector
|
|
7
|
-
search paths in CI without an actual embedding model.
|
|
8
|
-
|
|
9
|
-
Speaks both shapes the engine uses:
|
|
10
|
-
POST /v1/embeddings { input, model } -> { data:[{embedding:[...] }] }
|
|
11
|
-
POST /v1/embed { input, model } -> { embeddings:[[...]] }
|
|
12
|
-
|
|
13
|
-
Run:
|
|
14
|
-
EMBED_DIM=4096 uvicorn server:app --host 0.0.0.0 --port 8041
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import hashlib
|
|
19
|
-
import math
|
|
20
|
-
import os
|
|
21
|
-
from typing import Any
|
|
22
|
-
|
|
23
|
-
from fastapi import FastAPI
|
|
24
|
-
from pydantic import BaseModel
|
|
25
|
-
|
|
26
|
-
EMBED_DIM = int(os.environ.get("EMBED_DIM", "4096"))
|
|
27
|
-
|
|
28
|
-
app = FastAPI(title="embed-stub")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class EmbedRequest(BaseModel):
|
|
32
|
-
input: Any
|
|
33
|
-
model: str | None = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _vector_for(text: str) -> list[float]:
|
|
37
|
-
"""Deterministic vector: hash the text, expand to EMBED_DIM, L2-normalise."""
|
|
38
|
-
text = text or ""
|
|
39
|
-
seed = hashlib.sha256(text.encode("utf-8")).digest()
|
|
40
|
-
raw: list[int] = []
|
|
41
|
-
counter = 0
|
|
42
|
-
while len(raw) < EMBED_DIM:
|
|
43
|
-
chunk = hashlib.sha256(seed + counter.to_bytes(4, "big")).digest()
|
|
44
|
-
raw.extend(chunk)
|
|
45
|
-
counter += 1
|
|
46
|
-
floats = [(b - 127.5) / 127.5 for b in raw[:EMBED_DIM]]
|
|
47
|
-
norm = math.sqrt(sum(x * x for x in floats)) or 1.0
|
|
48
|
-
return [x / norm for x in floats]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _normalise_inputs(inp: Any) -> list[str]:
|
|
52
|
-
if isinstance(inp, str):
|
|
53
|
-
return [inp]
|
|
54
|
-
if isinstance(inp, list):
|
|
55
|
-
return [str(x) for x in inp]
|
|
56
|
-
return [str(inp)]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@app.get("/health")
|
|
60
|
-
def health() -> dict:
|
|
61
|
-
return {"status": "ok", "dim": EMBED_DIM}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@app.post("/v1/embeddings")
|
|
65
|
-
def openai_embeddings(req: EmbedRequest) -> dict:
|
|
66
|
-
texts = _normalise_inputs(req.input)
|
|
67
|
-
return {
|
|
68
|
-
"object": "list",
|
|
69
|
-
"data": [
|
|
70
|
-
{"object": "embedding", "index": i, "embedding": _vector_for(t)}
|
|
71
|
-
for i, t in enumerate(texts)
|
|
72
|
-
],
|
|
73
|
-
"model": req.model or "embed-stub",
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@app.post("/v1/embed")
|
|
78
|
-
def lambda_gateway_embed(req: EmbedRequest) -> dict:
|
|
79
|
-
texts = _normalise_inputs(req.input)
|
|
80
|
-
return {"embeddings": [_vector_for(t) for t in texts]}
|