Download e2e/test_knowledge.py from abc1181/Jaarvis: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/spaces/abc1181/Jaarvis/resolve/main/e2e/test_knowledge.py
- Command line
-
hf download hf://spaces/abc1181/Jaarvis/e2e/test_knowledge.py
-
curl -L -o test_knowledge.py https://huggingface.co/spaces/abc1181/Jaarvis/resolve/main/e2e/test_knowledge.py
13.5 kB
| """E2E — Phase 6: knowledge, transcripts and the recall that goes in the prompt. | |
| The claim under test is narrow and important: **JARVIS stops asking things it | |
| was already told.** So the checks are not "does the vector store work" — they | |
| are: | |
| * a paraphrase with NO shared keywords still finds the document (that is | |
| embeddings, not grep); | |
| * a past conversation turn is findable by what was said, not by document id; | |
| * the prompt block is empty when there is nothing to say, so the model is | |
| never handed an empty "here is what you remember" header; | |
| * deleting a document really removes it; | |
| * the lexical fallback works with no API key, because a vector store that | |
| returns nothing and looks broken is worse than one that degrades honestly. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import shutil | |
| import socket | |
| import subprocess | |
| import sys | |
| import time | |
| import urllib.error | |
| import urllib.request | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| PORT = int(os.environ.get("JARVIS_E2E_PORT") or "3117") | |
| BASE = f"http://127.0.0.1:{PORT}" | |
| DATA = ROOT / "e2e" / "data_knowledge" | |
| LOG = Path("/tmp/jarvis_e2e_knowledge.log") | |
| _failures: list[str] = [] | |
| _passes: list[str] = [] | |
| def check(name: str, ok: bool, detail: str = "") -> None: | |
| if ok: | |
| _passes.append(name) | |
| show = detail if detail and any(c.isdigit() for c in str(detail)) else "" | |
| print(f" PASS {name}" + (f" — {show}" if show else ""), flush=True) | |
| else: | |
| _failures.append(f"{name}: {detail}") | |
| print(f" FAIL {name} — {str(detail)[:150]}", flush=True) | |
| # ── unit ───────────────────────────────────────────────────────────────────── | |
| def unit() -> None: | |
| print("== unit: store, chunking, lexical fallback ==", flush=True) | |
| # keep the unit phase out of the repo root | |
| tmp = Path("/tmp/jarvis_e2e_knowledge_unit") | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| tmp.mkdir(parents=True, exist_ok=True) | |
| os.environ["JARVIS_DATA"] = str(tmp) | |
| sys.path.insert(0, str(ROOT)) | |
| from core import knowledge as K | |
| K.reset() | |
| check("empty_prompt_block", K.prompt_block() == "", | |
| repr(K.prompt_block()[:40])) | |
| check("empty_search", K.search("") == [] and K.search(" ") == []) | |
| try: | |
| K.ingest("nothing", "y") | |
| check("rejects_empty_text", False, "accepted 1 char") | |
| except ValueError: | |
| check("rejects_empty_text", True) | |
| # chunking: paragraphs joined, oversized paragraphs hard-wrapped | |
| long_para = "word " * 600 | |
| parts = K._chunk_text(long_para) | |
| check("chunks.hard_wraps_long_text", len(parts) > 1, f"{len(parts)} parts") | |
| check("chunks.respect_the_limit", all(len(p) <= K.CHUNK_CHARS + 60 for p in parts), | |
| max(len(p) for p in parts)) | |
| joined = K._chunk_text("one para\n\ntwo para\n\nthree para") | |
| check("chunks.keep_paragraphs", len(joined) == 1 and "two para" in joined[0], | |
| joined) | |
| # turns | |
| K.record_turn("user", "my hourly rate is 85 euros, I do Next.js and React") | |
| K.record_turn("jarvis", "Noted — 85 EUR per hour") | |
| check("turns.recorded", K.turn_count() == 2, K.turn_count()) | |
| check("turns.ignore_empty", K.record_turn("user", " ") is False) | |
| hits = K.search("what do I charge") | |
| kinds = {h["kind"] for h in hits} | |
| check("turns.findable_by_meaning", "turn" in kinds, [h["kind"] for h in hits[:3]]) | |
| check("turns.filter_by_role", | |
| all(r["role"] == "jarvis" for r in K.recent_turns(5, role="jarvis"))) | |
| # lexical fallback: no key, and it must still find things | |
| real = K._api_key | |
| K._api_key = lambda: "" | |
| try: | |
| K.ingest("Client Acme", "Acme wants a Next.js dashboard. Budget 4000 EUR. " | |
| "Contact ops@acme.example. Weekly sprints, Slack.", source="note") | |
| hits = K.search("acme budget euros") | |
| check("fallback.finds_documents", any(h["kind"] == "doc" for h in hits), | |
| [h["kind"] for h in hits[:3]]) | |
| check("fallback.still_finds_turns", any(h["kind"] == "turn" for h in hits)) | |
| check("fallback.mode_says_lexical", K.stats()["mode"] == "lexical", K.stats()) | |
| finally: | |
| K._api_key = real | |
| # a nonsense query returns nothing rather than everything | |
| check("nonsense_query_is_empty", K.search("zzqqxx unrelated gibberish") == []) | |
| # prompt block | |
| block = K.prompt_block() | |
| check("prompt.has_section", "[WHAT YOU KNOW]" in block, block[:60]) | |
| check("prompt.mentions_documents", "stored document" in block, block[:120]) | |
| check("prompt.uses_real_newlines", "\\n" not in block and "\n" in block, | |
| repr(block[:60])) | |
| K.note_query("what is the acme budget") | |
| check("note_query_stashes_recall", "[WHAT YOU KNOW]" in K.prompt_block()) | |
| K.note_query("hi") # too short to be worth a recall | |
| # forget | |
| docs = K.docs() | |
| check("docs.listed", len(docs) >= 1, len(docs)) | |
| target = docs[0]["id"] | |
| check("forget.removes", K.forget(target) is True) | |
| check("forget.gone", all(d["id"] != target for d in K.docs())) | |
| check("forget.missing_is_false", K.forget(99999) is False) | |
| check("chunks_went_with_it", | |
| all(h.get("doc_id") != target for h in K.search("acme budget"))) | |
| def embeddings() -> None: | |
| """Only meaningful with a real key; skipped honestly otherwise.""" | |
| print("== unit: semantic recall (needs the API key) ==", flush=True) | |
| sys.path.insert(0, str(ROOT)) | |
| from core import knowledge as K | |
| K.reset() | |
| if not K._api_key(): | |
| check("semantic.skipped_no_key", True, "no key on this machine") | |
| return | |
| d = K.ingest("Client Acme", "Acme wants a Next.js dashboard. Budget 4000 EUR. " | |
| "Contact ops@acme.example. Weekly sprints, Slack.", source="note") | |
| if not d.get("embedded"): | |
| check("semantic.embeddings_work", False, "no vectors stored") | |
| return | |
| check("semantic.embeddings_work", True, f"{d['chunks']} embedded") | |
| # the real test: no shared keywords with the source text | |
| hits = K.search("what does the customer have money for") | |
| check("semantic.paraphrase_finds_it", | |
| any(h["kind"] == "doc" for h in hits), | |
| [(h["kind"], h["score"]) for h in hits[:2]]) | |
| hits2 = K.search("how do they prefer to work together") | |
| check("semantic.second_paraphrase", | |
| any("weekly" in (h.get("text") or "").lower() or h["kind"] == "doc" | |
| for h in hits2), [(h["kind"], h["score"]) for h in hits2[:2]]) | |
| check("semantic.stats_mode", K.stats()["mode"] == "semantic", K.stats()["mode"]) | |
| # ── server ─────────────────────────────────────────────────────────────────── | |
| def _port_free(port: int) -> bool: | |
| s = socket.socket() | |
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| try: | |
| s.bind(("127.0.0.1", port)) | |
| return True | |
| except OSError: | |
| return False | |
| finally: | |
| s.close() | |
| def start() -> subprocess.Popen: | |
| if not _port_free(PORT): | |
| raise RuntimeError(f"port {PORT} in use") | |
| if LOG.exists(): | |
| LOG.unlink() | |
| shutil.rmtree(DATA, ignore_errors=True) | |
| DATA.mkdir(parents=True, exist_ok=True) | |
| env = os.environ.copy() | |
| env.update({"JARVIS_MODE": "server", "JARVIS_PORT": str(PORT), | |
| "JARVIS_DATA": str(DATA), "PYTHONUNBUFFERED": "1"}) | |
| p = subprocess.Popen([sys.executable, "-u", "main.py"], cwd=str(ROOT), env=env, | |
| stdout=LOG.open("w"), stderr=subprocess.STDOUT, start_new_session=True) | |
| end = time.time() + 60 | |
| while time.time() < end: | |
| try: | |
| with urllib.request.urlopen(f"{BASE}/login", timeout=2) as r: | |
| if r.status < 500: | |
| return p | |
| except Exception: | |
| pass | |
| time.sleep(0.25) | |
| p.kill() | |
| raise RuntimeError("server did not start:\n" | |
| + (LOG.read_text(errors="replace")[-1200:] if LOG.exists() else "")) | |
| _tok = "" | |
| def token() -> str: | |
| global _tok | |
| if _tok: | |
| return _tok | |
| import re | |
| r = urllib.request.Request(BASE + "/api/bootstrap-key", data=b"{}", method="POST") | |
| with urllib.request.urlopen(r, timeout=30) as x: | |
| key = json.loads(x.read())["key"] | |
| with urllib.request.urlopen(f"{BASE}/auto-login?key={key}", timeout=30) as x: | |
| html = x.read().decode(errors="replace") | |
| _tok = re.search(r"sessionStorage\.setItem\('jarvis_token','([^']+)'", html).group(1) | |
| return _tok | |
| def api(path: str, body: dict | None = None, tok: str | None = None): | |
| data = json.dumps(body).encode() if body is not None else None | |
| r = urllib.request.Request(BASE + path, data=data, | |
| method="POST" if data else "GET") | |
| if data: | |
| r.add_header("Content-Type", "application/json") | |
| if tok: | |
| r.add_header("Authorization", f"Bearer {tok}") | |
| try: | |
| with urllib.request.urlopen(r, timeout=90) as x: | |
| b = x.read() | |
| return x.status, (json.loads(b) if b else {}) | |
| except urllib.error.HTTPError as e: | |
| b = e.read() | |
| try: | |
| return e.code, (json.loads(b) if b else {}) | |
| except Exception: | |
| return e.code, {} | |
| def served() -> None: | |
| print("== server: /api/knowledge ==", flush=True) | |
| tok = token() | |
| check("auth.401_search", api("/api/knowledge?q=x")[0] == 401) | |
| check("auth.401_post", api("/api/knowledge", {"title": "x", "text": "y"})[0] == 401) | |
| check("auth.401_forget", api("/api/knowledge/forget", {"id": 1})[0] == 401) | |
| st, d = api("/api/knowledge?q=anything", tok=tok) | |
| check("get.search_shape", | |
| st == 200 and "hits" in d and "stats" in d, sorted(d)[:4]) | |
| st, d = api("/api/knowledge?view=stats", tok=tok) | |
| check("get.stats", st == 200 and "docs" in d.get("stats", {}), d.get("stats")) | |
| st, d = api("/api/knowledge?view=docs", tok=tok) | |
| check("get.docs", st == 200 and isinstance(d.get("docs"), list), st) | |
| st, d = api("/api/knowledge?view=recent", tok=tok) | |
| check("get.recent", st == 200 and isinstance(d.get("turns"), list), st) | |
| st, d = api("/api/knowledge", | |
| {"title": "Client Globex", "text": "Globex pays 3000 EUR for a " | |
| "landing page. Primary contact sales@globex.example.", | |
| "source": "client"}, tok=tok) | |
| check("post.stores", st == 201 and d.get("chunks", 0) >= 1, d) | |
| doc_id = d.get("id") | |
| st, d = api("/api/knowledge?q=globex%20payment", tok=tok) | |
| found = [h for h in d.get("hits", []) if h.get("kind") == "doc"] | |
| check("post.findable", any("Globex" in (h.get("title") or "") for h in found), | |
| [h.get("title") for h in d.get("hits", [])[:3]]) | |
| st, d = api("/api/knowledge", {"title": "x", "text": "y"}, tok=tok) | |
| check("post.rejects_too_short", st == 400, st) | |
| st, d = api("/api/knowledge/forget", {"id": doc_id}, tok=tok) | |
| check("post.forget", st == 200 and d.get("ok") is True, d) | |
| st, d = api("/api/knowledge?q=globex", tok=tok) | |
| check("post.forget_removed_it", | |
| not any("Globex" in (h.get("title") or "") for h in d.get("hits", [])), | |
| [h.get("title") for h in d.get("hits", [])[:3]]) | |
| # forgetting is a delete, so it goes through the policy's audit trail | |
| st, d = api("/api/audit?action=knowledge.forget&limit=5", tok=tok) | |
| rows = d.get("rows", []) | |
| check("forget_is_audited", bool(rows) and rows[0]["tier"] == "delete", | |
| rows[0] if rows else d) | |
| app = (ROOT / "dashboard" / "static" / "app.html").read_text(encoding="utf-8") | |
| for probe, label in [('id="kn-btn"', "header button"), | |
| ("function openKnowledge(", "panel opener"), | |
| ("_knRender", "renderer"), | |
| ("_knForget", "forget action"), | |
| ("/api/knowledge?view=recent", "recent tab")]: | |
| check(f"client.{label.replace(' ', '_')}", probe in app, probe) | |
| mp = (ROOT / "main.py").read_text(encoding="utf-8") | |
| check("tool.declared", '"name": "knowledge"' in mp) | |
| check("tool.dispatched", 'elif name == "knowledge":' in mp) | |
| check("tool.turns_persisted", 'record_turn("user"' in mp) | |
| check("tool.recall_in_prompt", "_kn.prompt_block()" in mp) | |
| def main() -> int: | |
| unit() | |
| embeddings() | |
| proc = start() | |
| try: | |
| served() | |
| finally: | |
| proc.terminate() | |
| try: | |
| proc.wait(timeout=8) | |
| except Exception: | |
| proc.kill() | |
| log = LOG.read_text(errors="replace") if LOG.exists() else "" | |
| check("boot.no_traceback", "Traceback (most recent call last)" not in log, | |
| log[-400:] if "Traceback" in log else "") | |
| print() | |
| print(f"PASS {len(_passes)} FAIL {len(_failures)}") | |
| for f in _failures: | |
| print(f" x {f}") | |
| return 1 if _failures else 0 | |
| def _hard_exit(code: int) -> None: | |
| """Leave immediately. A suite that has printed its result can still hang in | |
| interpreter teardown (a Playwright browser, a thread that never joins), which | |
| is the difference between a 2-minute suite and a 15-minute one.""" | |
| try: | |
| sys.stdout.flush() | |
| sys.stderr.flush() | |
| except Exception: | |
| pass | |
| os._exit(code) | |
| if __name__ == "__main__": | |
| _hard_exit(main()) | |