kyp-mem 0.3.0 → 0.4.1
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/kyp_mem/__init__.py +1 -1
- package/kyp_mem/hooks.py +38 -14
- package/kyp_mem/server.py +179 -1
- package/kyp_mem/static/index.html +1197 -220
- package/kyp_mem/ui.py +111 -0
- package/kyp_mem/vault.py +15 -0
- package/kyp_mem/vector.py +43 -0
- package/package.json +1 -1
- package/pyproject.toml +2 -1
package/kyp_mem/ui.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""KYP-MEM web UI — interactive interface for browsing the vault."""
|
|
2
2
|
|
|
3
3
|
import webbrowser
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from fastapi import FastAPI, Request
|
|
6
7
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
@@ -119,6 +120,116 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
119
120
|
vault.write_note(path, content, tags, props)
|
|
120
121
|
return JSONResponse({"ok": True, "path": path})
|
|
121
122
|
|
|
123
|
+
@app.post("/api/sessions/create")
|
|
124
|
+
async def create_session(request: Request):
|
|
125
|
+
body = await request.json()
|
|
126
|
+
project = body.get("project", "").strip()
|
|
127
|
+
summary = body.get("summary", "").strip()
|
|
128
|
+
if not project:
|
|
129
|
+
return JSONResponse({"error": "Project name required"}, 400)
|
|
130
|
+
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
131
|
+
content = (
|
|
132
|
+
f"# Session {session_id}\n\n"
|
|
133
|
+
f"**Project:** {project}\n\n"
|
|
134
|
+
f"## Summary\n{summary}\n\n"
|
|
135
|
+
f"## INVESTIGATED\n\n\n"
|
|
136
|
+
f"## LEARNED\n\n\n"
|
|
137
|
+
f"## COMPLETED\n\n\n"
|
|
138
|
+
f"## NEXT STEPS\n\n"
|
|
139
|
+
)
|
|
140
|
+
tags = ["session", "manual", project.lower().replace(" ", "-")]
|
|
141
|
+
path = f"{project}/Sessions/{session_id}.md"
|
|
142
|
+
vault.write_note(path, content, tags, {})
|
|
143
|
+
return JSONResponse({"ok": True, "path": path})
|
|
144
|
+
|
|
145
|
+
@app.get("/api/sessions/search")
|
|
146
|
+
def search_sessions(q: str = "", project: str = ""):
|
|
147
|
+
from .vector import get_session_memory
|
|
148
|
+
mem = get_session_memory()
|
|
149
|
+
if not mem or not q:
|
|
150
|
+
return JSONResponse([])
|
|
151
|
+
results = mem.search_sessions(q, project=project or None, n_results=10)
|
|
152
|
+
if not results or not results.get("ids") or not results["ids"][0]:
|
|
153
|
+
return JSONResponse([])
|
|
154
|
+
items = []
|
|
155
|
+
for i, path in enumerate(results["ids"][0]):
|
|
156
|
+
doc = results["documents"][0][i]
|
|
157
|
+
dist = results["distances"][0][i]
|
|
158
|
+
note = vault.index.notes.get(path)
|
|
159
|
+
items.append({
|
|
160
|
+
"path": path,
|
|
161
|
+
"title": note.title if note else path,
|
|
162
|
+
"distance": dist,
|
|
163
|
+
"snippet": doc[:300],
|
|
164
|
+
})
|
|
165
|
+
return JSONResponse(items)
|
|
166
|
+
|
|
167
|
+
@app.get("/api/sessions")
|
|
168
|
+
def list_sessions(project: str = ""):
|
|
169
|
+
sessions = {}
|
|
170
|
+
for path, note in vault.index.notes.items():
|
|
171
|
+
if "/Sessions/" not in path and not path.startswith("Sessions/"):
|
|
172
|
+
continue
|
|
173
|
+
parts = path.split("/")
|
|
174
|
+
idx = parts.index("Sessions") if "Sessions" in parts else -1
|
|
175
|
+
proj = "/".join(parts[:idx]) if idx > 0 else "(root)"
|
|
176
|
+
if project and proj.lower() != project.lower():
|
|
177
|
+
continue
|
|
178
|
+
if proj not in sessions:
|
|
179
|
+
sessions[proj] = []
|
|
180
|
+
sessions[proj].append({
|
|
181
|
+
"path": path,
|
|
182
|
+
"title": note.title,
|
|
183
|
+
"tags": note.tags,
|
|
184
|
+
"created": note.created,
|
|
185
|
+
"updated": note.updated,
|
|
186
|
+
})
|
|
187
|
+
for proj in sessions:
|
|
188
|
+
sessions[proj].sort(key=lambda s: s["path"], reverse=True)
|
|
189
|
+
return JSONResponse(sessions)
|
|
190
|
+
|
|
191
|
+
@app.get("/api/projects")
|
|
192
|
+
def list_projects():
|
|
193
|
+
projects = set()
|
|
194
|
+
for path in vault.index.notes:
|
|
195
|
+
parts = path.split("/")
|
|
196
|
+
if len(parts) > 1:
|
|
197
|
+
projects.add(parts[0])
|
|
198
|
+
result = []
|
|
199
|
+
for proj in sorted(projects):
|
|
200
|
+
session_count = sum(1 for p in vault.index.notes if p.startswith(f"{proj}/Sessions/"))
|
|
201
|
+
result.append({"name": proj, "session_count": session_count})
|
|
202
|
+
return JSONResponse(result)
|
|
203
|
+
|
|
204
|
+
@app.delete("/api/note/{path:path}")
|
|
205
|
+
def delete_note(path: str):
|
|
206
|
+
if vault.delete(path):
|
|
207
|
+
return JSONResponse({"ok": True})
|
|
208
|
+
return JSONResponse({"error": "Not found"}, 404)
|
|
209
|
+
|
|
210
|
+
@app.post("/api/projects/create")
|
|
211
|
+
async def create_project(request: Request):
|
|
212
|
+
body = await request.json()
|
|
213
|
+
name = body.get("name", "").strip()
|
|
214
|
+
overview = body.get("overview", "").strip()
|
|
215
|
+
if not name:
|
|
216
|
+
return JSONResponse({"error": "Project name required"}, 400)
|
|
217
|
+
path = f"{name}/Knowledge.md"
|
|
218
|
+
if vault.read(path):
|
|
219
|
+
return JSONResponse({"error": "Project already exists"}, 409)
|
|
220
|
+
content = (
|
|
221
|
+
f"# {name}\n\n"
|
|
222
|
+
f"## Overview\n{overview or '(Project description, goals, tech stack)'}\n\n"
|
|
223
|
+
f"## Architecture\n(System design, key components, data flow)\n\n"
|
|
224
|
+
f"## Bugs\n### Known\n\n### Fixed\n\n\n"
|
|
225
|
+
f"## Improvements\n### Planned\n\n### Completed\n\n\n"
|
|
226
|
+
f"## Key Decisions\n(Important architectural or design decisions)\n\n"
|
|
227
|
+
f"## Notes\n(Miscellaneous project knowledge)\n"
|
|
228
|
+
)
|
|
229
|
+
tags = ["project", "knowledge", name.lower().replace(" ", "-")]
|
|
230
|
+
vault.write_note(path, content, tags, {})
|
|
231
|
+
return JSONResponse({"ok": True, "path": path})
|
|
232
|
+
|
|
122
233
|
@app.post("/api/reload")
|
|
123
234
|
def reload():
|
|
124
235
|
vault._load_all()
|
package/kyp_mem/vault.py
CHANGED
|
@@ -200,12 +200,23 @@ class Index:
|
|
|
200
200
|
return results[:15]
|
|
201
201
|
|
|
202
202
|
|
|
203
|
+
from .vector import init_vector_db, get_session_memory
|
|
204
|
+
|
|
203
205
|
class Vault:
|
|
204
206
|
def __init__(self, vault_path: str):
|
|
205
207
|
self.root = Path(vault_path).expanduser().resolve()
|
|
206
208
|
self.root.mkdir(parents=True, exist_ok=True)
|
|
207
209
|
self.index = Index()
|
|
210
|
+
init_vector_db(str(self.root))
|
|
208
211
|
self._load_all()
|
|
212
|
+
self._sync_vector_db()
|
|
213
|
+
|
|
214
|
+
def _sync_vector_db(self):
|
|
215
|
+
mem = get_session_memory()
|
|
216
|
+
for path, note in self.index.notes.items():
|
|
217
|
+
if "/Sessions/" in path or path.startswith("Sessions/"):
|
|
218
|
+
folder = note.folder
|
|
219
|
+
mem.upsert_session(path, folder, note.content)
|
|
209
220
|
|
|
210
221
|
def _load_all(self):
|
|
211
222
|
notes = {}
|
|
@@ -250,6 +261,8 @@ class Vault:
|
|
|
250
261
|
|
|
251
262
|
full.write_text(serialize_note(note), encoding="utf-8")
|
|
252
263
|
self._load_all()
|
|
264
|
+
if "/Sessions/" in path or path.startswith("Sessions/"):
|
|
265
|
+
get_session_memory().upsert_session(path, note.folder, note.content)
|
|
253
266
|
|
|
254
267
|
def delete(self, path: str) -> bool:
|
|
255
268
|
full = self.root / path
|
|
@@ -261,6 +274,8 @@ class Vault:
|
|
|
261
274
|
parent.rmdir()
|
|
262
275
|
parent = parent.parent
|
|
263
276
|
self._load_all()
|
|
277
|
+
if "/Sessions/" in path or path.startswith("Sessions/"):
|
|
278
|
+
get_session_memory().delete_session(path)
|
|
264
279
|
return True
|
|
265
280
|
return False
|
|
266
281
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chromadb
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class SessionMemory:
|
|
5
|
+
def __init__(self, vault_path: str):
|
|
6
|
+
self.db_path = Path(vault_path).parent / "chroma"
|
|
7
|
+
self.db_path.mkdir(parents=True, exist_ok=True)
|
|
8
|
+
self.client = chromadb.PersistentClient(path=str(self.db_path))
|
|
9
|
+
self.collection = self.client.get_or_create_collection(name="sessions")
|
|
10
|
+
|
|
11
|
+
def upsert_session(self, path: str, project: str, content: str):
|
|
12
|
+
self.collection.upsert(
|
|
13
|
+
documents=[content],
|
|
14
|
+
metadatas=[{"project": project}],
|
|
15
|
+
ids=[path]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def delete_session(self, path: str):
|
|
19
|
+
try:
|
|
20
|
+
self.collection.delete(ids=[path])
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def search_sessions(self, query: str, project: str = None, n_results: int = 5):
|
|
25
|
+
where = {"project": project} if project else None
|
|
26
|
+
try:
|
|
27
|
+
results = self.collection.query(
|
|
28
|
+
query_texts=[query],
|
|
29
|
+
n_results=n_results,
|
|
30
|
+
where=where
|
|
31
|
+
)
|
|
32
|
+
return results
|
|
33
|
+
except Exception:
|
|
34
|
+
return {"ids": [], "documents": [], "metadatas": [], "distances": []}
|
|
35
|
+
|
|
36
|
+
session_memory = None
|
|
37
|
+
|
|
38
|
+
def init_vector_db(vault_path: str):
|
|
39
|
+
global session_memory
|
|
40
|
+
session_memory = SessionMemory(vault_path)
|
|
41
|
+
|
|
42
|
+
def get_session_memory():
|
|
43
|
+
return session_memory
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kyp-mem"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
"pyyaml>=6.0",
|
|
31
31
|
"fastapi>=0.100.0",
|
|
32
32
|
"uvicorn>=0.20.0",
|
|
33
|
+
"chromadb>=0.4.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|