Add memba MVP: C++ core, Python SDK, CLI, examples, experiments
C++ core (libmemba.so): - include/memba/state.h — C API (state_new/free/save/load/get_size) - src/state.cpp — MEMB file format: magic, version, SHA-256 model_id, CRC-32, opaque llama_state_*_data() blob - src/cli.cpp — minimal demo binary with greedy sampler - CMakeLists.txt + build.sh with llama.cpp submodule, CUDA auto-detect Python SDK (memba): - core.py — file I/O via llama-cpp-python's exposed C functions, unwraps _LlamaContext to access raw context pointer (≥0.3.x) - session.py — high-level Session with auto-save/load, ChatML wrapper for instruct models, raw mode for base models - cli.py — typer-based: chat (REPL), run (one-shot), list, rm, info Examples: - 01_basic_save_load.py, 02_chat_session.py Experiments (throwaway POCs documenting product-direction findings): - recall_poc.py — git log → state → cross-process query - mood_poc.py — batch sentiment trajectory, Mamba vs Transformer - mood_stream_poc.py, mood_batch_poc.py — variants - diag_saveload.py — minimal save/load isolation test - README.md documents the headline finding: save/load is byte-identical, but Falcon-Mamba-7B-Instruct does not retain facts across conversation turns even in-process — limits viable products to single-prompt analysis and persona priming. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# experiments/
|
||||
|
||||
Throwaway scripts used to probe capabilities of SSM models with memba.
|
||||
Not part of the library API — kept in the repo as reference and reproducible
|
||||
evidence for product decisions.
|
||||
|
||||
Each script is self-contained and prints what it finds; read the source for
|
||||
the test's claim and run it yourself if you want to verify on different
|
||||
models or hardware.
|
||||
|
||||
## Scripts
|
||||
|
||||
| File | What it measures |
|
||||
|------|------------------|
|
||||
| `recall_poc.py` | Can a memba state, built from N days of git activity, answer "what did I work on last month" in a fresh process? |
|
||||
| `mood_poc.py` | Batch sentiment-trajectory test (single prompt with full chat log). Compares Falcon-Mamba vs a Transformer. |
|
||||
| `mood_stream_poc.py` | The same trajectory but fed turn-by-turn through `Session.chat()`, then queried cross-process. |
|
||||
| `mood_batch_poc.py` | Batch ingest in build process, save, then query in a fresh process. |
|
||||
| `diag_saveload.py` | Minimal diagnostic: tell the model one fact, ask it back before save, after save, after cross-process load. |
|
||||
|
||||
## Headline finding (2026-05-16, Falcon-Mamba-7B-Instruct Q4_K_M)
|
||||
|
||||
- **Batch single-prompt analysis** (all input + question in one call): works
|
||||
for both sentiment and recall.
|
||||
- **Multi-turn fact recall** (ingest in turn 1, ask in turn 2): fails even
|
||||
in the *same process*. The model does not preserve specific facts in its
|
||||
hidden state across conversation turns.
|
||||
- **Save/load roundtrip**: byte-identical, no information loss attributable
|
||||
to memba's file format. The persistence layer works correctly; the
|
||||
trained model just doesn't use the state for cross-turn recall.
|
||||
|
||||
See the script outputs (or rerun) for the raw evidence.
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Diagnostic: does the model retain context AFTER save in the SAME process,
|
||||
and AFTER load in a fresh process? Compares three scenarios.
|
||||
"""
|
||||
import sys, argparse
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))
|
||||
from llama_cpp import Llama
|
||||
from memba import core
|
||||
|
||||
MODEL = "/home/emil/Desktop/Coding/AI/Memba/falcon-mamba-7B-instruct-Q4_K_M.gguf"
|
||||
STATE = "/tmp/diag_saveload.memb"
|
||||
|
||||
INGEST = "I'm telling you a secret. My pet hamster is named Bartholomew. He is 4 years old. Reply 'ok'."
|
||||
QUERY = "What is the name of my pet?"
|
||||
|
||||
|
||||
def chatml(msg):
|
||||
return f"<|im_start|>user\n{msg}<|im_end|>\n<|im_start|>assistant\n"
|
||||
|
||||
|
||||
def make_llama():
|
||||
return Llama(model_path=MODEL, n_ctx=2048, n_gpu_layers=-1, verbose=False)
|
||||
|
||||
|
||||
def ask(m, prompt):
|
||||
out = m(chatml(prompt), max_tokens=60, stop=["<|im_end|>"], echo=False)
|
||||
return out["choices"][0]["text"].strip()
|
||||
|
||||
|
||||
def build():
|
||||
m = make_llama()
|
||||
ack = ask(m, INGEST)
|
||||
print(f" [build] ack: {ack!r}")
|
||||
print(f" [build] state size (live): {core.get_state_size(m):,} B")
|
||||
# In-process query BEFORE saving
|
||||
print(f" [build] in-proc query BEFORE save: {ask(m, QUERY)!r}")
|
||||
# Save
|
||||
core.save_state(m, MODEL, STATE)
|
||||
print(f" [build] state saved")
|
||||
# In-process query AFTER saving (should still work — save shouldn't mutate)
|
||||
print(f" [build] in-proc query AFTER save: {ask(m, QUERY)!r}")
|
||||
|
||||
|
||||
def query():
|
||||
m = make_llama()
|
||||
print(f" [query] before load — fresh model: {ask(m, QUERY)!r}")
|
||||
core.load_state(m, MODEL, STATE)
|
||||
print(f" [query] state size after load: {core.get_state_size(m):,} B")
|
||||
print(f" [query] after load: {ask(m, QUERY)!r}")
|
||||
# Try a second time in case position is wonky
|
||||
print(f" [query] second ask: {ask(m, QUERY)!r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "build"
|
||||
{"build": build, "query": query}[cmd]()
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
mood_batch_poc.py — batch ingest, cross-process query.
|
||||
|
||||
This is the clean test: one chat() call with all 15 messages as a block,
|
||||
save state, EXIT, then in a fresh process load state and ask sentiment
|
||||
questions. Isolates the cross-process save/load from streaming-noise.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, argparse
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))
|
||||
from memba import Session
|
||||
|
||||
MAMBA = "/home/emil/Desktop/Coding/AI/Memba/falcon-mamba-7B-instruct-Q4_K_M.gguf"
|
||||
STATE_DIR = "/tmp/mood_batch_test"
|
||||
SESSION = "mood_batch"
|
||||
|
||||
CHAT_LOG = [
|
||||
"Morning team! Coffee in hand, ready to tackle the auth refactor today.",
|
||||
"Just pushed PR #234 fixing the token validation bug. Should be a quick merge.",
|
||||
"Code review comments came in fast, all good catches. Iterating now.",
|
||||
"Basic flow working locally, tests passing. Feeling good about this.",
|
||||
"Heading to lunch, hopefully wrap this up by EOD.",
|
||||
"Back. CI is failing on something unrelated, looking into it.",
|
||||
"OK the 'unrelated' thing is actually related. Auth tests use a stale fixture.",
|
||||
"Why does the fixture rebuild take 12 minutes. Every. Single. Time.",
|
||||
"Cancelled the run twice now. Going to bypass and run tests locally.",
|
||||
"Local passes, CI fails. Classic.",
|
||||
"Two hours gone on this fixture issue. Not even what I was supposed to be doing.",
|
||||
"Now there's a merge conflict with main because someone restructured migrations.",
|
||||
"Whoever shipped those migrations on a Friday afternoon, I will find you.",
|
||||
"Closing the laptop. Will fight this tomorrow.",
|
||||
"Actually no. One more try before I sleep.",
|
||||
]
|
||||
|
||||
INGEST_PROMPT = (
|
||||
"You are observing one person's chat messages from a workday. "
|
||||
"Here they are in order. Read them and remember the overall trajectory. "
|
||||
"Reply with just 'noted'.\n\n"
|
||||
+ "\n".join(f"[msg {i+1:>2}] {m}" for i, m in enumerate(CHAT_LOG))
|
||||
)
|
||||
|
||||
|
||||
def build():
|
||||
p = Path(STATE_DIR) / f"{SESSION}.memb"
|
||||
if p.exists(): p.unlink()
|
||||
s = Session(model_path=MAMBA, session_id=SESSION, state_dir=STATE_DIR,
|
||||
n_gpu_layers=-1, n_ctx=4096, chat_format="chatml")
|
||||
print(f"[build] ack: {s.chat(INGEST_PROMPT, max_tokens=8)!r}")
|
||||
print(f"[build] state: {s.state_size:,} B")
|
||||
s.save()
|
||||
|
||||
|
||||
def query():
|
||||
s = Session(model_path=MAMBA, session_id=SESSION, state_dir=STATE_DIR,
|
||||
n_gpu_layers=-1, n_ctx=4096, chat_format="chatml")
|
||||
print(f"[query] loaded {s.state_size:,} B\n")
|
||||
for q in [
|
||||
"What is this person's current emotional state? One sentence.",
|
||||
"Did their mood change over the messages? One sentence describing the trajectory.",
|
||||
"Around which message number did the mood shift from positive to negative? Just the number.",
|
||||
]:
|
||||
print(f"[Q] {q}")
|
||||
print(f"[A] {s.chat(q, max_tokens=120)}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "build"
|
||||
{"build": build, "query": query}[cmd]()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
mood_poc.py — sentiment-trajectory test on Mamba vs Transformer.
|
||||
|
||||
A chat log is constructed with a deliberate emotional arc:
|
||||
msg 1-5 : optimistic / energetic
|
||||
msg 6-10 : frustrated, hitting friction
|
||||
msg 11-15 : burnt out, angry
|
||||
|
||||
Both models see the same prompt and answer 3 questions:
|
||||
Q1. Current mood at message 15
|
||||
Q2. Trajectory from start to end
|
||||
Q3. Approximate message number where mood shifted
|
||||
|
||||
Pass criterion: model identifies negative trend AND points at a shift
|
||||
between msgs 6-11. Generic "they seem fine" or "they were happy throughout"
|
||||
counts as failure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from llama_cpp import Llama
|
||||
|
||||
MAMBA = "/home/emil/Desktop/Coding/AI/Memba/falcon-mamba-7B-instruct-Q4_K_M.gguf"
|
||||
GEMMA = "/home/emil/Programs/Llama.cpp/llama.cpp/models/gemma-4-E4B-it-Q8_0.gguf"
|
||||
|
||||
CHAT_LOG = [
|
||||
"Morning team! Coffee in hand, ready to tackle the auth refactor today.",
|
||||
"Just pushed PR #234 fixing the token validation bug. Should be a quick merge.",
|
||||
"Code review comments came in fast, all good catches. Iterating now.",
|
||||
"Basic flow working locally, tests passing. Feeling good about this.",
|
||||
"Heading to lunch, hopefully wrap this up by EOD.",
|
||||
"Back. CI is failing on something unrelated, looking into it.",
|
||||
"OK the 'unrelated' thing is actually related. Auth tests use a stale fixture.",
|
||||
"Why does the fixture rebuild take 12 minutes. Every. Single. Time.",
|
||||
"Cancelled the run twice now. Going to bypass and run tests locally.",
|
||||
"Local passes, CI fails. Classic.",
|
||||
"Two hours gone on this fixture issue. Not even what I was supposed to be doing.",
|
||||
"Now there's a merge conflict with main because someone restructured migrations.",
|
||||
"Whoever shipped those migrations on a Friday afternoon, I will find you.",
|
||||
"Closing the laptop. Will fight this tomorrow.",
|
||||
"Actually no. One more try before I sleep.",
|
||||
]
|
||||
|
||||
PROMPT = """You are observing a person's chat messages from one workday. Here they are in order:
|
||||
|
||||
""" + "\n".join(f"[msg {i+1:>2}] {m}" for i, m in enumerate(CHAT_LOG)) + """
|
||||
|
||||
Now answer these THREE questions, briefly and directly:
|
||||
|
||||
Q1: What is this person's mood at message 15 (the last one)? One short sentence.
|
||||
Q2: How did their mood change from message 1 to message 15? One short sentence.
|
||||
Q3: Around which message number does the mood clearly shift from positive to negative? Just give the number.
|
||||
|
||||
Format your answer as:
|
||||
A1: ...
|
||||
A2: ...
|
||||
A3: ..."""
|
||||
|
||||
|
||||
def run(label: str, model_path: str) -> None:
|
||||
print(f"\n{'='*60}\n {label}\n{'='*60}")
|
||||
llm = Llama(model_path=model_path, n_ctx=4096, n_gpu_layers=-1, verbose=False)
|
||||
out = llm.create_chat_completion(
|
||||
messages=[{"role": "user", "content": PROMPT}],
|
||||
max_tokens=300,
|
||||
temperature=0.3, # low temp so we test capability, not creativity
|
||||
)
|
||||
print(out["choices"][0]["message"]["content"])
|
||||
del llm # free GPU memory before loading next
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run("FALCON-MAMBA-7B-INSTRUCT (SSM)", MAMBA)
|
||||
run("GEMMA-4-E4B-IT (Transformer)", GEMMA)
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
mood_stream_poc.py — streaming sentiment, then save/load across processes.
|
||||
|
||||
This is the REAL product test:
|
||||
1. Open memba session
|
||||
2. Feed 15 chat messages ONE AT A TIME as "observations"
|
||||
3. Save state, exit process
|
||||
4. In a fresh process: load state, query mood
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, argparse, time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))
|
||||
from memba import Session
|
||||
|
||||
MAMBA = "/home/emil/Desktop/Coding/AI/Memba/falcon-mamba-7B-instruct-Q4_K_M.gguf"
|
||||
STATE_DIR = "/tmp/mood_stream_test"
|
||||
SESSION = "mood_stream"
|
||||
|
||||
CHAT_LOG = [
|
||||
"Morning team! Coffee in hand, ready to tackle the auth refactor today.",
|
||||
"Just pushed PR #234 fixing the token validation bug. Should be a quick merge.",
|
||||
"Code review comments came in fast, all good catches. Iterating now.",
|
||||
"Basic flow working locally, tests passing. Feeling good about this.",
|
||||
"Heading to lunch, hopefully wrap this up by EOD.",
|
||||
"Back. CI is failing on something unrelated, looking into it.",
|
||||
"OK the 'unrelated' thing is actually related. Auth tests use a stale fixture.",
|
||||
"Why does the fixture rebuild take 12 minutes. Every. Single. Time.",
|
||||
"Cancelled the run twice now. Going to bypass and run tests locally.",
|
||||
"Local passes, CI fails. Classic.",
|
||||
"Two hours gone on this fixture issue. Not even what I was supposed to be doing.",
|
||||
"Now there's a merge conflict with main because someone restructured migrations.",
|
||||
"Whoever shipped those migrations on a Friday afternoon, I will find you.",
|
||||
"Closing the laptop. Will fight this tomorrow.",
|
||||
"Actually no. One more try before I sleep.",
|
||||
]
|
||||
|
||||
QUESTIONS = [
|
||||
"Briefly: what is this person's current emotional state? One sentence.",
|
||||
"Has their mood changed during this monitoring session? One sentence.",
|
||||
"Roughly when did they start having a hard time?",
|
||||
]
|
||||
|
||||
|
||||
def build():
|
||||
state_path = Path(STATE_DIR) / f"{SESSION}.memb"
|
||||
if state_path.exists():
|
||||
state_path.unlink()
|
||||
print(f"[build] cleared previous state")
|
||||
|
||||
s = Session(
|
||||
model_path=MAMBA, session_id=SESSION, state_dir=STATE_DIR,
|
||||
n_gpu_layers=-1, n_ctx=4096, chat_format="chatml",
|
||||
)
|
||||
print(f"[build] init state {s.state_size:,} B")
|
||||
|
||||
# Stream-feed: each message wrapped as if WE'RE TELLING the model
|
||||
# "here's a new message you're observing"
|
||||
for i, msg in enumerate(CHAT_LOG, 1):
|
||||
observation = f"You are silently observing one person's chat messages. New message just arrived:\n[msg {i}] {msg}\nReply with just 'noted'."
|
||||
ack = s.chat(observation, max_tokens=4)
|
||||
print(f"[build] msg {i:>2}: {msg[:50]:<50} → ack={ack!r}")
|
||||
|
||||
print(f"[build] state after streaming: {s.state_size:,} B")
|
||||
s.save()
|
||||
print(f"[build] saved → {state_path}")
|
||||
|
||||
|
||||
def query():
|
||||
state_path = Path(STATE_DIR) / f"{SESSION}.memb"
|
||||
if not state_path.exists():
|
||||
print("[query] no state — run build first"); return 1
|
||||
print(f"[query] loading state {state_path.stat().st_size:,} B")
|
||||
s = Session(
|
||||
model_path=MAMBA, session_id=SESSION, state_dir=STATE_DIR,
|
||||
n_gpu_layers=-1, n_ctx=4096, chat_format="chatml",
|
||||
)
|
||||
for i, q in enumerate(QUESTIONS, 1):
|
||||
print(f"\n[Q{i}] {q}")
|
||||
print(f"[A{i}] {s.chat(q, max_tokens=120)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("cmd", choices=["build", "query"])
|
||||
args = p.parse_args()
|
||||
if args.cmd == "build":
|
||||
build()
|
||||
else:
|
||||
query()
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
recall_poc.py — proof of concept for the 'recall' product idea.
|
||||
|
||||
A daemon would normally feed events into Mamba state continuously. For the
|
||||
POC we batch-feed the last N days of git activity from a set of repos, save
|
||||
the resulting state, then in a SEPARATE invocation query the saved state.
|
||||
|
||||
If the query in mode 2 produces an answer that genuinely reflects the input,
|
||||
the concept is viable. If it produces vague/wrong answers — rethink.
|
||||
|
||||
Usage:
|
||||
# Step 1: ingest git history into a memba state
|
||||
python recall_poc.py build --model <gguf>
|
||||
|
||||
# Step 2 (separate process — proves persistence):
|
||||
python recall_poc.py query --model <gguf>
|
||||
python recall_poc.py query --model <gguf> --q "Which project saw the most activity?"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# ── Config ───────────────────────────────────────────────────────
|
||||
SCAN_ROOTS = [
|
||||
Path.home() / "Desktop/Coding",
|
||||
Path.home() / "Desktop/Coding/AI",
|
||||
]
|
||||
DAYS_BACK = 30
|
||||
STATE_DIR = Path.home() / ".recall/states"
|
||||
SESSION_ID = "recall_poc"
|
||||
DEFAULT_Q = (
|
||||
"Summarise what I've been working on over the last month. "
|
||||
"Group by project. Mention the main themes per project. "
|
||||
"Be concise — one short paragraph per project."
|
||||
)
|
||||
|
||||
|
||||
# ── Git scraping ─────────────────────────────────────────────────
|
||||
|
||||
def find_repos(roots: list[Path]) -> list[Path]:
|
||||
repos = []
|
||||
seen = set()
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
for entry in sorted(root.iterdir()):
|
||||
if not entry.is_dir() or not (entry / ".git").exists():
|
||||
continue
|
||||
real = entry.resolve()
|
||||
if real in seen:
|
||||
continue
|
||||
seen.add(real)
|
||||
repos.append(entry)
|
||||
return repos
|
||||
|
||||
|
||||
def git_log(repo: Path, since_days: int) -> str:
|
||||
"""Return human-readable commit log for last `since_days` days."""
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[
|
||||
"git", "-C", str(repo), "log",
|
||||
f"--since={since_days} days ago",
|
||||
"--no-merges",
|
||||
"--date=short",
|
||||
"--pretty=format:%ad %s",
|
||||
],
|
||||
text=True, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
return ""
|
||||
return out.strip()
|
||||
|
||||
|
||||
def build_corpus(repos: list[Path], since_days: int) -> tuple[str, int]:
|
||||
"""Build a single human-readable summary block. Returns (text, n_commits)."""
|
||||
blocks: list[str] = []
|
||||
total = 0
|
||||
for repo in repos:
|
||||
log = git_log(repo, since_days)
|
||||
if not log:
|
||||
continue
|
||||
n = log.count("\n") + 1
|
||||
total += n
|
||||
blocks.append(f"=== {repo.name} ({n} commits) ===\n{log}")
|
||||
return "\n\n".join(blocks), total
|
||||
|
||||
|
||||
# ── memba interaction ───────────────────────────────────────────
|
||||
|
||||
def open_session(model_path: str, n_gpu_layers: int) -> "Session": # noqa: F821
|
||||
# Imported lazily so query mode doesn't pay the cost when only listing
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))
|
||||
from memba import Session
|
||||
return Session(
|
||||
model_path=model_path,
|
||||
session_id=SESSION_ID,
|
||||
state_dir=str(STATE_DIR),
|
||||
n_gpu_layers=n_gpu_layers,
|
||||
n_ctx=8192, # bigger than default so a month of commits fits comfortably
|
||||
chat_format="chatml",
|
||||
)
|
||||
|
||||
|
||||
# ── build mode ──────────────────────────────────────────────────
|
||||
|
||||
def cmd_build(args: argparse.Namespace) -> int:
|
||||
repos = find_repos(SCAN_ROOTS)
|
||||
print(f"[build] discovered {len(repos)} git repos under {[str(r) for r in SCAN_ROOTS]}")
|
||||
|
||||
corpus, n_commits = build_corpus(repos, args.days)
|
||||
if not corpus:
|
||||
print(f"[build] no commits in the last {args.days} days — abort", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
char_count = len(corpus)
|
||||
print(f"[build] corpus: {n_commits} commits, {char_count:,} characters")
|
||||
print(f"[build] first 400 chars:\n---\n{corpus[:400]}\n---")
|
||||
|
||||
# Wipe any previous state for a clean test
|
||||
state_file = STATE_DIR / f"{SESSION_ID}.memb"
|
||||
if state_file.exists():
|
||||
state_file.unlink()
|
||||
print(f"[build] removed previous state file")
|
||||
|
||||
print(f"[build] loading model ({Path(args.model).name}) on GPU layers={args.gpu_layers}…")
|
||||
t0 = time.time()
|
||||
sess = open_session(args.model, args.gpu_layers)
|
||||
print(f"[build] model loaded in {time.time()-t0:.1f}s, initial state {sess.state_size:,} B")
|
||||
|
||||
prompt = (
|
||||
"I am going to give you my git commit history from the last month, "
|
||||
"across several of my personal projects. Please READ it and remember "
|
||||
"the overall picture — which projects I worked on, the kinds of "
|
||||
"changes I made, and any themes that emerge. Just reply 'noted' when "
|
||||
"you have processed it; I'll ask questions in a follow-up.\n\n"
|
||||
f"=== git log (last {args.days} days, {n_commits} commits) ===\n\n"
|
||||
f"{corpus}"
|
||||
)
|
||||
|
||||
print(f"[build] feeding {len(prompt):,} characters into state…")
|
||||
t0 = time.time()
|
||||
ack = sess.chat(prompt, max_tokens=32)
|
||||
elapsed = time.time() - t0
|
||||
print(f"[build] processed in {elapsed:.1f}s (~{len(prompt)/elapsed:,.0f} char/s)")
|
||||
print(f"[build] model ack: {ack!r}")
|
||||
print(f"[build] state after ingest: {sess.state_size:,} B")
|
||||
|
||||
path = sess.save()
|
||||
on_disk = path.stat().st_size
|
||||
print(f"[build] state saved → {path} ({on_disk:,} B on disk)")
|
||||
return 0
|
||||
|
||||
|
||||
# ── query mode ──────────────────────────────────────────────────
|
||||
|
||||
def cmd_query(args: argparse.Namespace) -> int:
|
||||
state_file = STATE_DIR / f"{SESSION_ID}.memb"
|
||||
if not state_file.exists():
|
||||
print(f"[query] no state file at {state_file} — run `build` first", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
on_disk = state_file.stat().st_size
|
||||
print(f"[query] state file: {state_file} ({on_disk:,} B)")
|
||||
print(f"[query] loading model ({Path(args.model).name}) on GPU layers={args.gpu_layers}…")
|
||||
t0 = time.time()
|
||||
sess = open_session(args.model, args.gpu_layers)
|
||||
print(f"[query] model+state loaded in {time.time()-t0:.1f}s, state size {sess.state_size:,} B")
|
||||
|
||||
question = args.q or DEFAULT_Q
|
||||
print(f"\n[query] question:\n{question}\n")
|
||||
print(f"[query] response:\n---")
|
||||
t0 = time.time()
|
||||
reply = sess.chat(question, max_tokens=args.max_tokens)
|
||||
print(reply)
|
||||
print(f"--- ({time.time()-t0:.1f}s)")
|
||||
return 0
|
||||
|
||||
|
||||
# ── main ────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description="recall POC — git log → mamba state → cross-process query")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--model", required=True, help="Path to GGUF model")
|
||||
common.add_argument("--gpu-layers", type=int, default=-1, dest="gpu_layers",
|
||||
help="Layers to offload (-1=all, 0=CPU)")
|
||||
|
||||
b = sub.add_parser("build", parents=[common], help="Ingest git history into state")
|
||||
b.add_argument("--days", type=int, default=DAYS_BACK)
|
||||
b.set_defaults(func=cmd_build)
|
||||
|
||||
q = sub.add_parser("query", parents=[common], help="Query the saved state")
|
||||
q.add_argument("--q", help="Question to ask (default: monthly summary)")
|
||||
q.add_argument("--max-tokens", type=int, default=512, dest="max_tokens")
|
||||
q.set_defaults(func=cmd_query)
|
||||
|
||||
args = p.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user