yourmemory 1.4.77__tar.gz → 1.4.78__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.
- {yourmemory-1.4.77 → yourmemory-1.4.78}/PKG-INFO +1 -1
- {yourmemory-1.4.77 → yourmemory-1.4.78}/pyproject.toml +1 -1
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/app.py +2 -1
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/migrate.py +73 -0
- yourmemory-1.4.78/src/routes/pools.py +236 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/retrieve.py +29 -4
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/PKG-INFO +1 -1
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/SOURCES.txt +1 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/LICENSE +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/README.md +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/memory_mcp.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/setup.cfg +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/__init__.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/connection.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/duckdb_schema.sql +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/schema.sql +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/sqlite_schema.sql +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/__init__.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/backend.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/graph_store.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/neo4j_backend.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/networkx_backend.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/svo_extract.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/__init__.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_observe.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_recall.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_recall.sh +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_server.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_store.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_user.sh +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/jobs/decay_job.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/__init__.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/agents.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/audit.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/compact.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/dsar.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/graph_viz.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/memories.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/proxy.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/ui.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/__init__.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/agent_registry.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/api_keys.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/audit.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/compaction.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/decay.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/embed.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/extract.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/extract_fallback.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/resolve.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/resolve_fallback.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/retrieve.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/session.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/temporal.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/utils.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/tests/test_features.py +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/dependency_links.txt +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/entry_points.txt +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/requires.txt +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/top_level.txt +0 -0
- {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory_run.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yourmemory"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.78"
|
|
8
8
|
description = "Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -13,7 +13,7 @@ from collections import defaultdict, deque
|
|
|
13
13
|
from threading import Lock
|
|
14
14
|
from fastapi import Request
|
|
15
15
|
from fastapi.responses import JSONResponse
|
|
16
|
-
from src.routes import memories, retrieve, agents, ui, graph_viz, proxy, audit, dsar, compact
|
|
16
|
+
from src.routes import memories, retrieve, agents, ui, graph_viz, proxy, audit, dsar, compact, pools
|
|
17
17
|
from src.jobs.decay_job import run as run_decay
|
|
18
18
|
from src.services.audit import prune_expired as prune_audit
|
|
19
19
|
from src.db.migrate import migrate
|
|
@@ -52,6 +52,7 @@ app.include_router(proxy.router)
|
|
|
52
52
|
app.include_router(audit.router)
|
|
53
53
|
app.include_router(dsar.router)
|
|
54
54
|
app.include_router(compact.router)
|
|
55
|
+
app.include_router(pools.router)
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
# ── Rate limiting (abuse prevention) ────────────────────────────────────────────
|
|
@@ -220,6 +220,76 @@ def _create_archive_table(conn, backend: str) -> None:
|
|
|
220
220
|
""")
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
def _create_pool_tables(conn, backend: str) -> None:
|
|
224
|
+
"""Shared memory pools (team / institutional memory). A pool's memories live in the
|
|
225
|
+
`memories` table under a namespaced user_id ('pool:<id>'), so they reuse all existing
|
|
226
|
+
machinery (embedding, dedup, recall, decay, compaction, audit). These two tables hold
|
|
227
|
+
the pool registry and role-based membership. Idempotent across backends."""
|
|
228
|
+
if backend == "postgres":
|
|
229
|
+
cur = conn.cursor()
|
|
230
|
+
cur.execute("""
|
|
231
|
+
CREATE TABLE IF NOT EXISTS pools (
|
|
232
|
+
pool_id TEXT PRIMARY KEY,
|
|
233
|
+
name TEXT,
|
|
234
|
+
owner TEXT,
|
|
235
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
236
|
+
);
|
|
237
|
+
CREATE TABLE IF NOT EXISTS pool_members (
|
|
238
|
+
pool_id TEXT NOT NULL,
|
|
239
|
+
member_id TEXT NOT NULL,
|
|
240
|
+
role TEXT NOT NULL DEFAULT 'reader',
|
|
241
|
+
can_read BOOLEAN NOT NULL DEFAULT TRUE,
|
|
242
|
+
can_write BOOLEAN NOT NULL DEFAULT FALSE,
|
|
243
|
+
added_at TIMESTAMPTZ DEFAULT NOW(),
|
|
244
|
+
PRIMARY KEY (pool_id, member_id)
|
|
245
|
+
);
|
|
246
|
+
CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id);
|
|
247
|
+
""")
|
|
248
|
+
conn.commit()
|
|
249
|
+
cur.close()
|
|
250
|
+
elif backend == "duckdb":
|
|
251
|
+
try:
|
|
252
|
+
conn.execute("""
|
|
253
|
+
CREATE TABLE IF NOT EXISTS pools (
|
|
254
|
+
pool_id VARCHAR PRIMARY KEY,
|
|
255
|
+
name VARCHAR,
|
|
256
|
+
owner VARCHAR,
|
|
257
|
+
created_at TIMESTAMP DEFAULT now()
|
|
258
|
+
)""")
|
|
259
|
+
conn.execute("""
|
|
260
|
+
CREATE TABLE IF NOT EXISTS pool_members (
|
|
261
|
+
pool_id VARCHAR NOT NULL,
|
|
262
|
+
member_id VARCHAR NOT NULL,
|
|
263
|
+
role VARCHAR NOT NULL DEFAULT 'reader',
|
|
264
|
+
can_read BOOLEAN NOT NULL DEFAULT TRUE,
|
|
265
|
+
can_write BOOLEAN NOT NULL DEFAULT FALSE,
|
|
266
|
+
added_at TIMESTAMP DEFAULT now(),
|
|
267
|
+
PRIMARY KEY (pool_id, member_id)
|
|
268
|
+
)""")
|
|
269
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id)")
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
print(f"pool tables (duckdb) skipped: {exc}", file=sys.stderr)
|
|
272
|
+
else: # sqlite
|
|
273
|
+
conn.executescript("""
|
|
274
|
+
CREATE TABLE IF NOT EXISTS pools (
|
|
275
|
+
pool_id TEXT PRIMARY KEY,
|
|
276
|
+
name TEXT,
|
|
277
|
+
owner TEXT,
|
|
278
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
279
|
+
);
|
|
280
|
+
CREATE TABLE IF NOT EXISTS pool_members (
|
|
281
|
+
pool_id TEXT NOT NULL,
|
|
282
|
+
member_id TEXT NOT NULL,
|
|
283
|
+
role TEXT NOT NULL DEFAULT 'reader',
|
|
284
|
+
can_read INTEGER NOT NULL DEFAULT 1,
|
|
285
|
+
can_write INTEGER NOT NULL DEFAULT 0,
|
|
286
|
+
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
287
|
+
PRIMARY KEY (pool_id, member_id)
|
|
288
|
+
);
|
|
289
|
+
CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id);
|
|
290
|
+
""")
|
|
291
|
+
|
|
292
|
+
|
|
223
293
|
def migrate():
|
|
224
294
|
backend = get_backend()
|
|
225
295
|
|
|
@@ -262,6 +332,9 @@ def migrate():
|
|
|
262
332
|
# ── Archive of originals compressed into summaries (memory compaction) ──
|
|
263
333
|
_create_archive_table(conn, backend)
|
|
264
334
|
|
|
335
|
+
# ── Shared memory pools (team / institutional memory) ──
|
|
336
|
+
_create_pool_tables(conn, backend)
|
|
337
|
+
|
|
265
338
|
# ── Post-schema FTS setup ─────────────────────────────────────────────
|
|
266
339
|
if backend == "sqlite":
|
|
267
340
|
# Backfill any rows that existed before the FTS table was created.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared memory pools — team / institutional memory.
|
|
3
|
+
|
|
4
|
+
A pool is a shared namespace many agents contribute to and everyone with access can
|
|
5
|
+
query. Individual agents still write their own personal memories; the pool is the
|
|
6
|
+
team-level layer on top. Pool memories are stored in the `memories` table under a
|
|
7
|
+
namespaced user_id (`pool:<id>`), so they reuse embedding, dedup, recall, decay,
|
|
8
|
+
compaction, and audit for free.
|
|
9
|
+
|
|
10
|
+
POST /pools create a pool
|
|
11
|
+
GET /pools list pools
|
|
12
|
+
POST /pools/{id}/members add a member with a role (reader/contributor/admin)
|
|
13
|
+
GET /pools/{id}/members list members
|
|
14
|
+
POST /pools/{id}/memories write a memory into the pool (writer role required)
|
|
15
|
+
GET /pools/{id}/memories list pool memories (reader role required)
|
|
16
|
+
POST /pools/{id}/retrieve semantic search within the pool
|
|
17
|
+
|
|
18
|
+
Role → permissions: reader = read · contributor = read+write · admin = read+write+manage.
|
|
19
|
+
|
|
20
|
+
> NOTE: in the default local deployment the caller identity (`memberId`) is supplied by
|
|
21
|
+
> the client and access checks are advisory — real enforcement arrives with hosted auth.
|
|
22
|
+
> The pool model, membership, and union recall are the durable substrate that hosted
|
|
23
|
+
> RBAC will enforce.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
from typing import Optional, List
|
|
29
|
+
|
|
30
|
+
from src.db.connection import get_backend, get_conn, duckdb_rows
|
|
31
|
+
from src.services.audit import log_event
|
|
32
|
+
|
|
33
|
+
router = APIRouter()
|
|
34
|
+
|
|
35
|
+
POOL_NS = "pool:" # memories namespace prefix
|
|
36
|
+
_ROLE_PERMS = {
|
|
37
|
+
"reader": (True, False),
|
|
38
|
+
"contributor": (True, True),
|
|
39
|
+
"admin": (True, True),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ns(pool_id: str) -> str:
|
|
44
|
+
return f"{POOL_NS}{pool_id.strip().lower()}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ph(backend: str) -> str:
|
|
48
|
+
return "%s" if backend == "postgres" else "?"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _member(conn, backend: str, pool_id: str, member_id: str) -> Optional[dict]:
|
|
52
|
+
p = _ph(backend)
|
|
53
|
+
sql = f"SELECT role, can_read, can_write FROM pool_members WHERE pool_id = {p} AND member_id = {p}"
|
|
54
|
+
args = (pool_id, member_id)
|
|
55
|
+
if backend == "duckdb":
|
|
56
|
+
rows = duckdb_rows(conn.execute(sql, list(args)))
|
|
57
|
+
return rows[0] if rows else None
|
|
58
|
+
cur = conn.cursor(); cur.execute(sql, args)
|
|
59
|
+
row = cur.fetchone(); cur.close()
|
|
60
|
+
if not row:
|
|
61
|
+
return None
|
|
62
|
+
return {"role": row[0], "can_read": bool(row[1]), "can_write": bool(row[2])}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _require(pool_id: str, member_id: str, need: str):
|
|
66
|
+
"""Advisory access check. need = 'read' | 'write'. Returns the membership dict."""
|
|
67
|
+
if not member_id:
|
|
68
|
+
raise HTTPException(403, "memberId required to access a pool.")
|
|
69
|
+
backend = get_backend(); conn = get_conn()
|
|
70
|
+
try:
|
|
71
|
+
m = _member(conn, backend, pool_id.strip().lower(), member_id.strip().lower())
|
|
72
|
+
finally:
|
|
73
|
+
conn.close()
|
|
74
|
+
if not m:
|
|
75
|
+
raise HTTPException(403, f"'{member_id}' is not a member of pool '{pool_id}'.")
|
|
76
|
+
if need == "read" and not m["can_read"]:
|
|
77
|
+
raise HTTPException(403, "Member lacks read access to this pool.")
|
|
78
|
+
if need == "write" and not m["can_write"]:
|
|
79
|
+
raise HTTPException(403, "Member lacks write access to this pool.")
|
|
80
|
+
return m
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── Create / list pools ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
class CreatePoolRequest(BaseModel):
|
|
86
|
+
pool_id: str
|
|
87
|
+
name: Optional[str] = ""
|
|
88
|
+
owner: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.post("/pools")
|
|
92
|
+
def create_pool(req: CreatePoolRequest):
|
|
93
|
+
pool_id = req.pool_id.strip().lower()
|
|
94
|
+
owner = req.owner.strip().lower()
|
|
95
|
+
if not pool_id or not owner:
|
|
96
|
+
raise HTTPException(422, "pool_id and owner are required.")
|
|
97
|
+
backend = get_backend(); conn = get_conn(); p = _ph(backend)
|
|
98
|
+
try:
|
|
99
|
+
# Create the pool and make the owner an admin member.
|
|
100
|
+
if backend == "duckdb":
|
|
101
|
+
conn.execute(f"INSERT INTO pools (pool_id, name, owner) VALUES (?,?,?) ON CONFLICT (pool_id) DO NOTHING",
|
|
102
|
+
[pool_id, req.name, owner])
|
|
103
|
+
conn.execute("INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
|
|
104
|
+
"VALUES (?,?,?,?,?) ON CONFLICT (pool_id, member_id) DO UPDATE SET role=excluded.role",
|
|
105
|
+
[pool_id, owner, "admin", True, True])
|
|
106
|
+
else:
|
|
107
|
+
cur = conn.cursor()
|
|
108
|
+
cur.execute(f"INSERT INTO pools (pool_id, name, owner) VALUES ({p},{p},{p}) ON CONFLICT (pool_id) DO NOTHING",
|
|
109
|
+
(pool_id, req.name, owner))
|
|
110
|
+
cur.execute(f"INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
|
|
111
|
+
f"VALUES ({p},{p},{p},{p},{p}) ON CONFLICT (pool_id, member_id) DO UPDATE SET role=excluded.role",
|
|
112
|
+
(pool_id, owner, "admin", True, True))
|
|
113
|
+
conn.commit(); cur.close()
|
|
114
|
+
finally:
|
|
115
|
+
conn.close()
|
|
116
|
+
log_event("write", "pool_create", owner, detail={"pool_id": pool_id})
|
|
117
|
+
return {"pool_id": pool_id, "name": req.name, "owner": owner, "role": "admin"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.get("/pools")
|
|
121
|
+
def list_pools(memberId: Optional[str] = Query(None, description="Filter to pools this member belongs to")):
|
|
122
|
+
backend = get_backend(); conn = get_conn(); p = _ph(backend)
|
|
123
|
+
try:
|
|
124
|
+
if memberId:
|
|
125
|
+
mid = memberId.strip().lower()
|
|
126
|
+
sql = (f"SELECT p.pool_id, p.name, p.owner, m.role FROM pools p "
|
|
127
|
+
f"JOIN pool_members m ON p.pool_id = m.pool_id WHERE m.member_id = {p} ORDER BY p.pool_id")
|
|
128
|
+
args = (mid,)
|
|
129
|
+
else:
|
|
130
|
+
sql = "SELECT pool_id, name, owner, '' AS role FROM pools ORDER BY pool_id"
|
|
131
|
+
args = ()
|
|
132
|
+
if backend == "duckdb":
|
|
133
|
+
rows = duckdb_rows(conn.execute(sql, list(args)))
|
|
134
|
+
else:
|
|
135
|
+
cur = conn.cursor(); cur.execute(sql, args)
|
|
136
|
+
cols = [d[0] for d in cur.description]
|
|
137
|
+
rows = [dict(zip(cols, r)) for r in cur.fetchall()]; cur.close()
|
|
138
|
+
finally:
|
|
139
|
+
conn.close()
|
|
140
|
+
return {"count": len(rows), "pools": rows}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── Membership ──────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
class AddMemberRequest(BaseModel):
|
|
146
|
+
member_id: str
|
|
147
|
+
role: str = "reader" # reader | contributor | admin
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.post("/pools/{pool_id}/members")
|
|
151
|
+
def add_member(pool_id: str, req: AddMemberRequest):
|
|
152
|
+
pool_id = pool_id.strip().lower()
|
|
153
|
+
member_id = req.member_id.strip().lower()
|
|
154
|
+
role = req.role.strip().lower()
|
|
155
|
+
if role not in _ROLE_PERMS:
|
|
156
|
+
raise HTTPException(422, f"role must be one of {list(_ROLE_PERMS)}")
|
|
157
|
+
can_read, can_write = _ROLE_PERMS[role]
|
|
158
|
+
backend = get_backend(); conn = get_conn(); p = _ph(backend)
|
|
159
|
+
try:
|
|
160
|
+
if backend == "duckdb":
|
|
161
|
+
conn.execute("INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
|
|
162
|
+
"VALUES (?,?,?,?,?) ON CONFLICT (pool_id, member_id) DO UPDATE SET "
|
|
163
|
+
"role=excluded.role, can_read=excluded.can_read, can_write=excluded.can_write",
|
|
164
|
+
[pool_id, member_id, role, can_read, can_write])
|
|
165
|
+
else:
|
|
166
|
+
cur = conn.cursor()
|
|
167
|
+
cur.execute(f"INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
|
|
168
|
+
f"VALUES ({p},{p},{p},{p},{p}) ON CONFLICT (pool_id, member_id) DO UPDATE SET "
|
|
169
|
+
f"role=excluded.role, can_read=excluded.can_read, can_write=excluded.can_write",
|
|
170
|
+
(pool_id, member_id, role, can_read, can_write))
|
|
171
|
+
conn.commit(); cur.close()
|
|
172
|
+
finally:
|
|
173
|
+
conn.close()
|
|
174
|
+
return {"pool_id": pool_id, "member_id": member_id, "role": role,
|
|
175
|
+
"can_read": can_read, "can_write": can_write}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@router.get("/pools/{pool_id}/members")
|
|
179
|
+
def list_members(pool_id: str):
|
|
180
|
+
pool_id = pool_id.strip().lower()
|
|
181
|
+
backend = get_backend(); conn = get_conn(); p = _ph(backend)
|
|
182
|
+
sql = f"SELECT member_id, role, can_read, can_write FROM pool_members WHERE pool_id = {p} ORDER BY member_id"
|
|
183
|
+
try:
|
|
184
|
+
if backend == "duckdb":
|
|
185
|
+
rows = duckdb_rows(conn.execute(sql, [pool_id]))
|
|
186
|
+
else:
|
|
187
|
+
cur = conn.cursor(); cur.execute(sql, (pool_id,))
|
|
188
|
+
cols = [d[0] for d in cur.description]
|
|
189
|
+
rows = [dict(zip(cols, r)) for r in cur.fetchall()]; cur.close()
|
|
190
|
+
finally:
|
|
191
|
+
conn.close()
|
|
192
|
+
return {"pool_id": pool_id, "count": len(rows), "members": rows}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ── Pool memories: write / list / retrieve (reuse the memory engine) ────────────
|
|
196
|
+
|
|
197
|
+
class PoolMemoryRequest(BaseModel):
|
|
198
|
+
memberId: str
|
|
199
|
+
content: str
|
|
200
|
+
importance: float = 0.5
|
|
201
|
+
category: Optional[str] = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.post("/pools/{pool_id}/memories")
|
|
205
|
+
def write_pool_memory(pool_id: str, req: PoolMemoryRequest):
|
|
206
|
+
_require(pool_id, req.memberId, "write")
|
|
207
|
+
from src.routes.memories import add_memory, MemoryRequest
|
|
208
|
+
result = add_memory(MemoryRequest(
|
|
209
|
+
userId=_ns(pool_id), content=req.content, importance=req.importance))
|
|
210
|
+
log_event("write", "pool_store", req.memberId.strip().lower(),
|
|
211
|
+
detail={"pool_id": pool_id.strip().lower(), "id": result.get("id")})
|
|
212
|
+
return {"pool_id": pool_id.strip().lower(), **result}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@router.get("/pools/{pool_id}/memories")
|
|
216
|
+
def list_pool_memories(pool_id: str, memberId: str = Query(...), limit: int = Query(50, ge=1, le=500)):
|
|
217
|
+
_require(pool_id, memberId, "read")
|
|
218
|
+
from src.routes.memories import list_memories
|
|
219
|
+
return list_memories(userId=_ns(pool_id), limit=limit)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class PoolRetrieveRequest(BaseModel):
|
|
223
|
+
memberId: str
|
|
224
|
+
query: str
|
|
225
|
+
topK: int = 6
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@router.post("/pools/{pool_id}/retrieve")
|
|
229
|
+
def retrieve_pool(pool_id: str, req: PoolRetrieveRequest):
|
|
230
|
+
_require(pool_id, req.memberId, "read")
|
|
231
|
+
from src.services.retrieve import retrieve as _retrieve
|
|
232
|
+
result = _retrieve(_ns(pool_id), req.query, req.topK)
|
|
233
|
+
log_event("read", "pool_retrieve", req.memberId.strip().lower(),
|
|
234
|
+
detail={"pool_id": pool_id.strip().lower(),
|
|
235
|
+
"count": len(result.get("memories", []))})
|
|
236
|
+
return result
|
|
@@ -21,20 +21,45 @@ class RetrieveRequest(BaseModel):
|
|
|
21
21
|
scope: Optional[List[str]] = None # hard filter by context_paths scope
|
|
22
22
|
currentPath: Optional[str] = None # spatial boost: current file/dir path
|
|
23
23
|
noGraph: bool = False # ablation: skip graph expansion
|
|
24
|
+
pools: Optional[List[str]] = None # also search these shared pools (if a member)
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@router.post("/retrieve")
|
|
27
28
|
def retrieve_memories(req: RetrieveRequest):
|
|
28
29
|
user_id = req.userId.strip().lower()
|
|
29
30
|
|
|
30
|
-
# ── Recall throttling
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# ── Recall throttling (bypassed when pools are requested — results differ) ──
|
|
32
|
+
if not req.pools:
|
|
33
|
+
cached = recall_cached(user_id, req.query)
|
|
34
|
+
if cached is not None:
|
|
35
|
+
return cached
|
|
34
36
|
|
|
35
37
|
result = retrieve(user_id, req.query, req.topK, current_path=req.currentPath, no_graph=req.noGraph,
|
|
36
38
|
score_threshold=req.scoreThreshold, scope=req.scope, expand_k=req.expandK)
|
|
37
39
|
|
|
40
|
+
# ── Union with shared pools the caller may read (institutional memory) ──
|
|
41
|
+
if req.pools:
|
|
42
|
+
try:
|
|
43
|
+
from src.routes.pools import _ns, _member
|
|
44
|
+
from src.db.connection import get_backend as _gb, get_conn as _gc
|
|
45
|
+
backend = _gb(); conn = _gc()
|
|
46
|
+
try:
|
|
47
|
+
readable = [p.strip().lower() for p in req.pools
|
|
48
|
+
if (lambda m: m and m["can_read"])(_member(conn, backend, p.strip().lower(), user_id))]
|
|
49
|
+
finally:
|
|
50
|
+
conn.close()
|
|
51
|
+
merged = list(result.get("memories", []))
|
|
52
|
+
for pid in readable:
|
|
53
|
+
pr = retrieve(_ns(pid), req.query, req.topK)
|
|
54
|
+
for mm in pr.get("memories", []):
|
|
55
|
+
mm["pool"] = pid
|
|
56
|
+
merged.append(mm)
|
|
57
|
+
merged.sort(key=lambda m: m.get("score", 0), reverse=True)
|
|
58
|
+
result["memories"] = merged[:req.topK]
|
|
59
|
+
result["memoriesFound"] = len(result["memories"])
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
38
63
|
# ── Session wrap-up tracking ───────────────────────────────────────────
|
|
39
64
|
session_track(user_id, [m["id"] for m in result.get("memories", [])])
|
|
40
65
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|