memorytalk 0.4.0__py3-none-any.whl
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.
- memorytalk/__init__.py +0 -0
- memorytalk/__main__.py +4 -0
- memorytalk/adapters/__init__.py +4 -0
- memorytalk/adapters/base.py +33 -0
- memorytalk/adapters/claude_code.py +119 -0
- memorytalk/api/__init__.py +100 -0
- memorytalk/api/cards.py +20 -0
- memorytalk/api/links.py +20 -0
- memorytalk/api/log.py +33 -0
- memorytalk/api/rebuild.py +21 -0
- memorytalk/api/search.py +18 -0
- memorytalk/api/sessions.py +18 -0
- memorytalk/api/status.py +25 -0
- memorytalk/api/tags.py +30 -0
- memorytalk/api/view.py +33 -0
- memorytalk/cli/__init__.py +21 -0
- memorytalk/cli/_format.py +435 -0
- memorytalk/cli/_http.py +73 -0
- memorytalk/cli/_render.py +71 -0
- memorytalk/cli/_setup_helpers.py +157 -0
- memorytalk/cli/card.py +41 -0
- memorytalk/cli/link.py +45 -0
- memorytalk/cli/log.py +32 -0
- memorytalk/cli/rebuild.py +31 -0
- memorytalk/cli/search.py +40 -0
- memorytalk/cli/server.py +146 -0
- memorytalk/cli/setup.py +429 -0
- memorytalk/cli/sync.py +57 -0
- memorytalk/cli/tag.py +51 -0
- memorytalk/cli/view.py +32 -0
- memorytalk/config.py +150 -0
- memorytalk/provider/__init__.py +0 -0
- memorytalk/provider/embedding.py +165 -0
- memorytalk/provider/lancedb.py +182 -0
- memorytalk/provider/storage.py +89 -0
- memorytalk/repository/__init__.py +18 -0
- memorytalk/repository/cards.py +124 -0
- memorytalk/repository/links.py +101 -0
- memorytalk/repository/schema.py +77 -0
- memorytalk/repository/search_log.py +105 -0
- memorytalk/repository/sessions.py +240 -0
- memorytalk/repository/store.py +50 -0
- memorytalk/schemas/__init__.py +51 -0
- memorytalk/schemas/cards.py +21 -0
- memorytalk/schemas/links.py +20 -0
- memorytalk/schemas/log.py +22 -0
- memorytalk/schemas/rebuild.py +12 -0
- memorytalk/schemas/search.py +44 -0
- memorytalk/schemas/sessions.py +37 -0
- memorytalk/schemas/shared.py +48 -0
- memorytalk/schemas/status.py +17 -0
- memorytalk/schemas/tags.py +14 -0
- memorytalk/schemas/view.py +36 -0
- memorytalk/service/__init__.py +25 -0
- memorytalk/service/cards.py +264 -0
- memorytalk/service/events.py +34 -0
- memorytalk/service/links.py +128 -0
- memorytalk/service/rebuild.py +130 -0
- memorytalk/service/search.py +182 -0
- memorytalk/service/sessions.py +309 -0
- memorytalk/util/__init__.py +0 -0
- memorytalk/util/dsl.py +327 -0
- memorytalk/util/ids.py +66 -0
- memorytalk/util/snippet.py +82 -0
- memorytalk/util/ttl.py +60 -0
- memorytalk-0.4.0.dist-info/METADATA +215 -0
- memorytalk-0.4.0.dist-info/RECORD +71 -0
- memorytalk-0.4.0.dist-info/WHEEL +5 -0
- memorytalk-0.4.0.dist-info/entry_points.txt +2 -0
- memorytalk-0.4.0.dist-info/licenses/LICENSE +201 -0
- memorytalk-0.4.0.dist-info/top_level.txt +1 -0
memorytalk/__init__.py
ADDED
|
File without changes
|
memorytalk/__main__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Base adapter interface for CLI `sync`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseAdapter(ABC):
|
|
9
|
+
"""Adapter for a source platform (claude-code / codex / ...).
|
|
10
|
+
|
|
11
|
+
`iter_sessions(root)` yields ingest payloads — dicts with the shape of
|
|
12
|
+
POST /v2/sessions body, including a computed sha256 for content hashing.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
source_name: str
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def iter_sessions(self, root: Path | None = None) -> Iterator[dict]: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ADAPTERS: dict[str, type[BaseAdapter]] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(cls: type[BaseAdapter]) -> type[BaseAdapter]:
|
|
25
|
+
ADAPTERS[cls.source_name] = cls
|
|
26
|
+
return cls
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_adapter(name: str) -> BaseAdapter:
|
|
30
|
+
cls = ADAPTERS.get(name)
|
|
31
|
+
if not cls:
|
|
32
|
+
raise ValueError(f"unknown adapter: {name}")
|
|
33
|
+
return cls()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Claude Code adapter — reads ~/.claude/projects/*.jsonl conversation files
|
|
2
|
+
and produces POST /v2/sessions payloads.
|
|
3
|
+
|
|
4
|
+
Porting note: v1 produced Session model objects; v2 wants the HTTP request
|
|
5
|
+
body shape (raw platform session_id, dict rounds, sha256 over source bytes).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterator
|
|
12
|
+
from urllib.parse import unquote
|
|
13
|
+
|
|
14
|
+
from memorytalk.adapters.base import BaseAdapter, register
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register
|
|
18
|
+
class ClaudeCodeAdapter(BaseAdapter):
|
|
19
|
+
source_name = "claude-code"
|
|
20
|
+
|
|
21
|
+
DEFAULT_ROOT = Path.home() / ".claude" / "projects"
|
|
22
|
+
|
|
23
|
+
def iter_sessions(self, root: Path | None = None) -> Iterator[dict]:
|
|
24
|
+
d = Path(root) if root else self.DEFAULT_ROOT
|
|
25
|
+
if not d.exists():
|
|
26
|
+
return
|
|
27
|
+
for path in sorted(d.rglob("*.jsonl")):
|
|
28
|
+
payload = self._convert_file(path)
|
|
29
|
+
if payload:
|
|
30
|
+
yield payload
|
|
31
|
+
|
|
32
|
+
def _convert_file(self, path: Path) -> dict | None:
|
|
33
|
+
raw_bytes = path.read_bytes()
|
|
34
|
+
sha256 = hashlib.sha256(raw_bytes).hexdigest()
|
|
35
|
+
session_id = path.stem # raw platform id, no sess_ prefix
|
|
36
|
+
project_dir = path.parent.name
|
|
37
|
+
project_name = unquote(project_dir)
|
|
38
|
+
|
|
39
|
+
rounds: list[dict] = []
|
|
40
|
+
created_at: str | None = None
|
|
41
|
+
for line in raw_bytes.decode("utf-8", errors="replace").splitlines():
|
|
42
|
+
line = line.strip()
|
|
43
|
+
if not line:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
msg = json.loads(line)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
continue
|
|
49
|
+
r = self._parse_message(msg)
|
|
50
|
+
if r is None:
|
|
51
|
+
continue
|
|
52
|
+
rounds.append(r)
|
|
53
|
+
if created_at is None and r.get("timestamp"):
|
|
54
|
+
created_at = r["timestamp"]
|
|
55
|
+
|
|
56
|
+
if not rounds:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"session_id": session_id,
|
|
61
|
+
"source": self.source_name,
|
|
62
|
+
"created_at": created_at or "",
|
|
63
|
+
"metadata": {"project": project_name, "path": str(path)},
|
|
64
|
+
"sha256": sha256,
|
|
65
|
+
"rounds": rounds,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def _parse_message(self, msg: dict) -> dict | None:
|
|
69
|
+
msg_type = msg.get("type")
|
|
70
|
+
if msg_type not in ("user", "assistant"):
|
|
71
|
+
return None
|
|
72
|
+
role = "human" if msg_type == "user" else "assistant"
|
|
73
|
+
speaker = "user" if msg_type == "user" else "assistant"
|
|
74
|
+
raw_content = msg.get("message", {}).get("content", [])
|
|
75
|
+
blocks = self._parse_content(raw_content)
|
|
76
|
+
if not blocks:
|
|
77
|
+
if isinstance(raw_content, str):
|
|
78
|
+
blocks = [{"type": "text", "text": raw_content}]
|
|
79
|
+
else:
|
|
80
|
+
return None
|
|
81
|
+
return {
|
|
82
|
+
"round_id": msg.get("uuid", ""),
|
|
83
|
+
"parent_id": msg.get("parentUuid"),
|
|
84
|
+
"timestamp": msg.get("timestamp"),
|
|
85
|
+
"speaker": speaker,
|
|
86
|
+
"role": role,
|
|
87
|
+
"content": blocks,
|
|
88
|
+
"is_sidechain": bool(msg.get("isSidechain")),
|
|
89
|
+
"cwd": msg.get("cwd"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def _parse_content(self, content_list) -> list[dict]:
|
|
93
|
+
if not isinstance(content_list, list):
|
|
94
|
+
return []
|
|
95
|
+
out: list[dict] = []
|
|
96
|
+
for block in content_list:
|
|
97
|
+
if not isinstance(block, dict):
|
|
98
|
+
continue
|
|
99
|
+
t = block.get("type")
|
|
100
|
+
if t == "text":
|
|
101
|
+
text = block.get("text") or ""
|
|
102
|
+
if text:
|
|
103
|
+
out.append({"type": "text", "text": text})
|
|
104
|
+
elif t == "thinking":
|
|
105
|
+
thinking = block.get("thinking") or ""
|
|
106
|
+
if thinking:
|
|
107
|
+
out.append({"type": "thinking", "thinking": thinking})
|
|
108
|
+
elif t == "tool_use":
|
|
109
|
+
name = block.get("name", "tool")
|
|
110
|
+
inp = block.get("input", "")
|
|
111
|
+
if isinstance(inp, dict):
|
|
112
|
+
inp = json.dumps(inp, ensure_ascii=False)
|
|
113
|
+
out.append({"type": "text", "text": f"[{name}] {inp}"})
|
|
114
|
+
elif t == "tool_result":
|
|
115
|
+
c = block.get("content", "")
|
|
116
|
+
if isinstance(c, list):
|
|
117
|
+
c = json.dumps(c, ensure_ascii=False)
|
|
118
|
+
out.append({"type": "text", "text": str(c)})
|
|
119
|
+
return out
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""FastAPI app factory for v2 — async setup via lifespan."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from memorytalk.config import Config, ConfigValidationError
|
|
11
|
+
from memorytalk.provider.embedding import (
|
|
12
|
+
EmbedderValidationError,
|
|
13
|
+
get_embedder,
|
|
14
|
+
validate_embedder,
|
|
15
|
+
)
|
|
16
|
+
from memorytalk.service import (
|
|
17
|
+
CardService, EventWriter, LinkService, RebuildService,
|
|
18
|
+
SearchService, SessionService,
|
|
19
|
+
)
|
|
20
|
+
from memorytalk.provider.lancedb import LanceStore
|
|
21
|
+
from memorytalk.provider.storage import LocalStorage
|
|
22
|
+
from memorytalk.repository import SQLiteStore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_app(config: Config | None = None) -> FastAPI:
|
|
26
|
+
config = config or Config()
|
|
27
|
+
try:
|
|
28
|
+
config.validate()
|
|
29
|
+
except ConfigValidationError as e:
|
|
30
|
+
print(f"[memory-talk] config validation failed: {e}", file=sys.stderr)
|
|
31
|
+
raise SystemExit(2) from e
|
|
32
|
+
|
|
33
|
+
config.ensure_dirs()
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def lifespan(app: FastAPI):
|
|
37
|
+
# Async startup — open DB, LanceDB, probe embedding, wire services.
|
|
38
|
+
storage = LocalStorage(config.data_root)
|
|
39
|
+
db = await SQLiteStore.create(config.db_path, storage)
|
|
40
|
+
vectors = await LanceStore.create(config.vectors_dir, dim=config.settings.embedding.dim)
|
|
41
|
+
embedder = get_embedder(config)
|
|
42
|
+
events = EventWriter(db)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
await validate_embedder(config)
|
|
46
|
+
except EmbedderValidationError as e:
|
|
47
|
+
print(f"[memory-talk] embedding startup check failed: {e}", file=sys.stderr)
|
|
48
|
+
raise SystemExit(2) from e
|
|
49
|
+
|
|
50
|
+
app.state.config = config
|
|
51
|
+
app.state.storage = storage
|
|
52
|
+
app.state.db = db
|
|
53
|
+
app.state.vectors = vectors
|
|
54
|
+
app.state.embedder = embedder
|
|
55
|
+
app.state.sessions = SessionService(
|
|
56
|
+
config=config, db=db, vectors=vectors, events=events,
|
|
57
|
+
)
|
|
58
|
+
app.state.cards = CardService(
|
|
59
|
+
config=config, db=db, vectors=vectors, embedder=embedder, events=events,
|
|
60
|
+
)
|
|
61
|
+
app.state.links = LinkService(config=config, db=db, events=events)
|
|
62
|
+
app.state.search = SearchService(
|
|
63
|
+
config=config, db=db, vectors=vectors, embedder=embedder,
|
|
64
|
+
)
|
|
65
|
+
app.state.rebuild = RebuildService(
|
|
66
|
+
config=config, db=db, vectors=vectors, embedder=embedder,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
yield
|
|
70
|
+
|
|
71
|
+
await db.close()
|
|
72
|
+
|
|
73
|
+
app = FastAPI(title="memory.talk v2", lifespan=lifespan)
|
|
74
|
+
app.state.config = config
|
|
75
|
+
app.state.status = "running"
|
|
76
|
+
|
|
77
|
+
@app.middleware("http")
|
|
78
|
+
async def rebuild_gate(request: Request, call_next):
|
|
79
|
+
status = getattr(request.app.state, "status", "running")
|
|
80
|
+
if status == "running":
|
|
81
|
+
return await call_next(request)
|
|
82
|
+
if request.method == "GET" and request.url.path == "/v2/status":
|
|
83
|
+
return await call_next(request)
|
|
84
|
+
return JSONResponse({"error": "rebuilding"}, status_code=503)
|
|
85
|
+
|
|
86
|
+
from memorytalk.api.status import router as status_router
|
|
87
|
+
app.include_router(status_router, prefix="/v2")
|
|
88
|
+
|
|
89
|
+
for name in ("sessions", "cards", "links", "tags", "search", "view", "log", "rebuild"):
|
|
90
|
+
try:
|
|
91
|
+
mod = __import__(f"memorytalk.api.{name}", fromlist=["router"])
|
|
92
|
+
app.include_router(mod.router, prefix="/v2")
|
|
93
|
+
except ImportError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return app
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_data_root = os.environ.get("MEMORY_TALK_DATA_ROOT")
|
|
100
|
+
app = create_app(Config(_data_root) if _data_root else Config())
|
memorytalk/api/cards.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""POST /v2/cards."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import CreateCardRequest, CreateCardResponse
|
|
7
|
+
from memorytalk.service import CardConflictError, CardServiceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/cards", response_model=CreateCardResponse)
|
|
14
|
+
async def post_cards(payload: CreateCardRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.cards.create(payload)
|
|
17
|
+
except CardConflictError as e:
|
|
18
|
+
raise HTTPException(status_code=409, detail=str(e))
|
|
19
|
+
except CardServiceError as e:
|
|
20
|
+
raise HTTPException(status_code=400, detail=str(e))
|
memorytalk/api/links.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""POST /v2/links."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import CreateLinkRequest, CreateLinkResponse
|
|
7
|
+
from memorytalk.service import LinkNotFoundError, LinkServiceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/links", response_model=CreateLinkResponse)
|
|
14
|
+
async def post_links(payload: CreateLinkRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.links.create(payload)
|
|
17
|
+
except LinkNotFoundError as e:
|
|
18
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
19
|
+
except LinkServiceError as e:
|
|
20
|
+
raise HTTPException(status_code=400, detail=str(e))
|
memorytalk/api/log.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""POST /v2/log — prefix-dispatched event stream."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.util.ids import IdKind, InvalidIdError, parse_id
|
|
7
|
+
from memorytalk.schemas import LogRequest, LogResponse
|
|
8
|
+
from memorytalk.service import (
|
|
9
|
+
CardNotFound, CardServiceError, SessionNotFound, SessionServiceError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post("/log", response_model=LogResponse)
|
|
17
|
+
async def post_log(payload: LogRequest, request: Request):
|
|
18
|
+
try:
|
|
19
|
+
kind, _ = parse_id(payload.id)
|
|
20
|
+
except InvalidIdError:
|
|
21
|
+
raise HTTPException(status_code=400, detail="invalid id prefix")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
if kind == IdKind.CARD:
|
|
25
|
+
return await request.app.state.cards.log(payload.id)
|
|
26
|
+
if kind == IdKind.SESSION:
|
|
27
|
+
return await request.app.state.sessions.log(payload.id)
|
|
28
|
+
except (CardNotFound, SessionNotFound) as e:
|
|
29
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
30
|
+
except (CardServiceError, SessionServiceError) as e:
|
|
31
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
32
|
+
|
|
33
|
+
raise HTTPException(status_code=400, detail="invalid id prefix")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""POST /v2/rebuild."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import RebuildResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.post("/rebuild", response_model=RebuildResponse)
|
|
13
|
+
async def post_rebuild(request: Request):
|
|
14
|
+
app = request.app
|
|
15
|
+
if getattr(app.state, "status", "running") != "running":
|
|
16
|
+
raise HTTPException(status_code=409, detail="rebuild already in progress")
|
|
17
|
+
app.state.status = "rebuilding"
|
|
18
|
+
try:
|
|
19
|
+
return await app.state.rebuild.rebuild()
|
|
20
|
+
finally:
|
|
21
|
+
app.state.status = "running"
|
memorytalk/api/search.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""POST /v2/search."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import SearchRequest, SearchResponse
|
|
7
|
+
from memorytalk.service import SearchError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/search", response_model=SearchResponse)
|
|
14
|
+
async def post_search(payload: SearchRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.search.search(payload)
|
|
17
|
+
except SearchError as e:
|
|
18
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""POST /v2/sessions — ingest."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import IngestSessionRequest, IngestSessionResponse
|
|
7
|
+
from memorytalk.service import SessionServiceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/sessions", response_model=IngestSessionResponse)
|
|
14
|
+
async def post_sessions(payload: IngestSessionRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.sessions.ingest(payload)
|
|
17
|
+
except SessionServiceError as e:
|
|
18
|
+
raise HTTPException(status_code=400, detail=str(e))
|
memorytalk/api/status.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""GET /v2/status — stats and running info."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from fastapi import APIRouter, Request
|
|
4
|
+
|
|
5
|
+
from memorytalk.schemas import StatusResponse
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.get("/status", response_model=StatusResponse)
|
|
11
|
+
async def get_status(request: Request) -> StatusResponse:
|
|
12
|
+
config = request.app.state.config
|
|
13
|
+
db = request.app.state.db
|
|
14
|
+
return StatusResponse(
|
|
15
|
+
data_root=str(config.data_root),
|
|
16
|
+
settings_path=str(config.settings_path),
|
|
17
|
+
status=getattr(request.app.state, "status", "running"),
|
|
18
|
+
sessions_total=await db.sessions.count(),
|
|
19
|
+
cards_total=await db.cards.count(),
|
|
20
|
+
links_total=await db.links.count(),
|
|
21
|
+
searches_total=await db.search_log.count(),
|
|
22
|
+
vector_provider=config.settings.vector.provider,
|
|
23
|
+
relation_provider=config.settings.relation.provider,
|
|
24
|
+
embedding_provider=config.settings.embedding.provider,
|
|
25
|
+
)
|
memorytalk/api/tags.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""POST /v2/tags/{add,remove}."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import TagsRequest, TagsResponse
|
|
7
|
+
from memorytalk.service import SessionNotFound, SessionServiceError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/tags/add", response_model=TagsResponse)
|
|
14
|
+
async def post_tags_add(payload: TagsRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.sessions.add_tags(payload)
|
|
17
|
+
except SessionNotFound as e:
|
|
18
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
19
|
+
except SessionServiceError as e:
|
|
20
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post("/tags/remove", response_model=TagsResponse)
|
|
24
|
+
async def post_tags_remove(payload: TagsRequest, request: Request):
|
|
25
|
+
try:
|
|
26
|
+
return await request.app.state.sessions.remove_tags(payload)
|
|
27
|
+
except SessionNotFound as e:
|
|
28
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
29
|
+
except SessionServiceError as e:
|
|
30
|
+
raise HTTPException(status_code=400, detail=str(e))
|
memorytalk/api/view.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""POST /v2/view — prefix-dispatched read."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.util.ids import IdKind, InvalidIdError, parse_id
|
|
7
|
+
from memorytalk.schemas import ViewRequest, ViewResponse
|
|
8
|
+
from memorytalk.service import (
|
|
9
|
+
CardNotFound, CardServiceError, SessionNotFound, SessionServiceError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post("/view", response_model=ViewResponse)
|
|
17
|
+
async def post_view(payload: ViewRequest, request: Request):
|
|
18
|
+
try:
|
|
19
|
+
kind, _ = parse_id(payload.id)
|
|
20
|
+
except InvalidIdError:
|
|
21
|
+
raise HTTPException(status_code=400, detail="invalid id prefix")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
if kind == IdKind.CARD:
|
|
25
|
+
return await request.app.state.cards.view(payload.id)
|
|
26
|
+
if kind == IdKind.SESSION:
|
|
27
|
+
return await request.app.state.sessions.view(payload.id)
|
|
28
|
+
except (CardNotFound, SessionNotFound) as e:
|
|
29
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
30
|
+
except (CardServiceError, SessionServiceError) as e:
|
|
31
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
32
|
+
|
|
33
|
+
raise HTTPException(status_code=400, detail="invalid id prefix")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""memory-talk v2 CLI root."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from memorytalk.cli.server import server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""memory-talk v2."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
main.add_command(server)
|
|
14
|
+
|
|
15
|
+
# Other command groups are attached as they're implemented.
|
|
16
|
+
for _name in ("card", "tag", "link", "sync", "search", "view", "log", "rebuild", "setup"):
|
|
17
|
+
try:
|
|
18
|
+
_mod = __import__(f"memorytalk.cli.{_name}", fromlist=[_name])
|
|
19
|
+
main.add_command(getattr(_mod, _name))
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|