Spaces:
Running on Zero
Running on Zero
Download evaluation_runner.py from akshit-Gupta/Lexi-memory-engine: direct link, hf CLI and curl.
- Browser
- Download file 32.7 kB
-
https://huggingface.co/spaces/akshit-Gupta/Lexi-memory-engine/resolve/main/evaluation_runner.py
- Command line
-
hf download hf://spaces/akshit-Gupta/Lexi-memory-engine/evaluation_runner.py
-
curl -L -o evaluation_runner.py https://huggingface.co/spaces/akshit-Gupta/Lexi-memory-engine/resolve/main/evaluation_runner.py
32.7 kB
| from __future__ import annotations | |
| import csv | |
| import json | |
| import re | |
| import time | |
| from pathlib import Path | |
| from typing import Any | |
| from llm_service import GeminiService | |
| from memory_engine import ( | |
| apply_memories, | |
| build_scene, | |
| detect_generation_overrides, | |
| detect_memory_reversals, | |
| extract_edit_candidates, | |
| find_memory, | |
| incorporate_llm_proposals, | |
| learn_from_edit, | |
| memory_context, | |
| memory_summary, | |
| normalize_memory, | |
| trim_history, | |
| ) | |
| from routing_policy import ( | |
| candidate_ids as route_candidate_ids, | |
| local_memory_decision, | |
| should_escalate_to_llm, | |
| ) | |
| from semantic_memory import quick_retrieve_memories, retrieve_memories | |
| REQUIRED_COLUMNS = ["chain_id", "step", "action", "mode"] | |
| OPTIONAL_COLUMNS = { | |
| "note_title", | |
| "input", | |
| "before", | |
| "after", | |
| "expect_output", | |
| "expect_contains", | |
| "expect_not_contains", | |
| "expect_event", | |
| "expect_memory", | |
| "expect_memory_absent", | |
| "expect_status", | |
| "expect_memory_status", | |
| "expect_negative_min", | |
| "expect_positive_min", | |
| "expect_exception_min", | |
| "expect_scope", | |
| "expect_route", | |
| "comment", | |
| "reset", | |
| } | |
| DEPRECATED_COLUMNS = {"use_llm", "use_ml"} | |
| EVENT_GROUPS = { | |
| "learn": {"memory_candidate", "memory_learned"}, | |
| "ignore": { | |
| "ignored_edit", | |
| "no_change", | |
| "undo", | |
| "no_memory_reversal", | |
| "no_generation_override", | |
| "context_skip", | |
| }, | |
| "negative_evidence": { | |
| "memory_negative_evidence", | |
| "memory_override", | |
| "memory_conflict", | |
| }, | |
| "apply": { | |
| "fast_path", | |
| "context_llm", | |
| "llm_format", | |
| "fast_fallback", | |
| }, | |
| "do_not_apply": {"context_skip"}, | |
| } | |
| def _bool(value: str) -> bool: | |
| return str(value or "").strip().lower() in { | |
| "1", "true", "yes", "y" | |
| } | |
| def _split(value: str) -> list[str]: | |
| value = str(value or "").strip() | |
| if not value: | |
| return [] | |
| value = value.replace("|", ";").replace("\n", ";") | |
| return [item.strip() for item in value.split(";") if item.strip()] | |
| def _memory_pair(value: str) -> tuple[str, str] | None: | |
| value = str(value or "").strip() | |
| if "=>" not in value: | |
| return None | |
| source, target = value.split("=>", 1) | |
| source = source.strip() | |
| target = target.strip() | |
| return (source, target) if source and target else None | |
| def _memory_pairs(value: str) -> list[tuple[str, str]]: | |
| return [ | |
| pair | |
| for item in _split(value) | |
| for pair in [_memory_pair(item)] | |
| if pair is not None | |
| ] | |
| def _memory_status_map(value: str) -> dict[tuple[str, str], str]: | |
| result: dict[tuple[str, str], str] = {} | |
| for item in _split(value): | |
| if "=" not in item: | |
| continue | |
| pair_text, status = item.rsplit("=", 1) | |
| pair = _memory_pair(pair_text) | |
| status = status.strip() | |
| if pair and status: | |
| result[pair] = status | |
| return result | |
| def _memory_pair_exact_exists( | |
| memories: list[dict[str, Any]], | |
| pair: tuple[str, str], | |
| ) -> bool: | |
| source, target = pair | |
| return any( | |
| str(memory.get("source", "")) == source | |
| and str(memory.get("target", "")) == target | |
| for memory in memories | |
| ) | |
| def _normalize_route(route: str) -> str: | |
| value = str(route or "").strip().lower() | |
| if value in {"deterministic", "local", "local no-memory"}: | |
| return "local" | |
| if value in {"semantic", "ml", "minilm"}: | |
| return "ml" | |
| if value in {"llm", "gemini"}: | |
| return "llm" | |
| return value or "local" | |
| def _semantic_event(result: dict[str, Any], action: str) -> str: | |
| event = str(result.get("event", "")) | |
| if action == "edit": | |
| for group, values in EVENT_GROUPS.items(): | |
| if event in values: | |
| return group | |
| return event | |
| changes = result.get("changes") or [] | |
| if changes: | |
| return "apply" | |
| candidate_ids = result.get("candidate_memory_ids") or [] | |
| if event == "context_skip": | |
| return "do_not_apply" if candidate_ids else "format" | |
| if event in {"llm_format", "fast_path", "ml_path", "fast_fallback", "context_llm"}: | |
| return "do_not_apply" if candidate_ids else "format" | |
| return event | |
| def _memory_snapshot(memories: list[dict[str, Any]]) -> list[dict[str, Any]]: | |
| return [ | |
| { | |
| "id": memory["id"], | |
| "source": memory["source"], | |
| "target": memory["target"], | |
| "kind": memory["kind"], | |
| "status": memory["status"], | |
| "confidence": memory["confidence"], | |
| "positive": memory["positive_evidence_count"], | |
| "negative": memory["negative_evidence_count"], | |
| "explicit": memory["explicit_evidence_count"], | |
| "exceptions": len(memory.get("exceptions", []) or []), | |
| "scope": memory.get("scope", "global"), | |
| } | |
| for memory in memory_summary(memories)["memories"] | |
| ] | |
| def _assert_row(row: dict[str, str], result: dict[str, Any]) -> tuple[bool, list[str]]: | |
| failures: list[str] = [] | |
| actual_output = str(result.get("actual_output", "")) | |
| memories = result.get("memories", []) | |
| semantic_event = str(result.get("semantic_event", "")) | |
| route = _normalize_route(result.get("route", "")) | |
| expected_output = str(row.get("expect_output", "")).strip() | |
| if expected_output and actual_output != expected_output: | |
| failures.append( | |
| f"output mismatch: expected={expected_output!r} actual={actual_output!r}" | |
| ) | |
| actual_folded = actual_output.casefold() | |
| for needle in _split(row.get("expect_contains", "")): | |
| if needle.casefold() not in actual_folded: | |
| failures.append(f"output missing {needle!r}") | |
| for needle in _split(row.get("expect_not_contains", "")): | |
| if needle not in actual_output: | |
| continue | |
| failures.append( | |
| f"output unexpectedly contains {needle!r}" | |
| ) | |
| expected_event = str(row.get("expect_event", "")).strip().lower() | |
| if expected_event: | |
| allowed = EVENT_GROUPS.get( | |
| expected_event, | |
| {expected_event}, | |
| ) | |
| if semantic_event != expected_event and str(result.get("event", "")) not in allowed: | |
| failures.append( | |
| f"event mismatch: expected semantic={expected_event!r} " | |
| f"actual semantic={semantic_event!r} raw={result.get('event', '')!r}" | |
| ) | |
| expected_route = str(row.get("expect_route", "")).strip().lower() | |
| if expected_route and route != _normalize_route(expected_route): | |
| failures.append( | |
| f"route mismatch: expected={expected_route!r} actual={route!r}" | |
| ) | |
| expected_pairs = _memory_pairs(row.get("expect_memory", "")) | |
| expected_status = str( | |
| row.get("expect_status", "") | |
| ).strip() | |
| expected_memory_status = _memory_status_map( | |
| row.get("expect_memory_status", "") | |
| ) | |
| for expected_pair in expected_pairs: | |
| memory = find_memory( | |
| memories, | |
| *expected_pair, | |
| ) | |
| if memory is None: | |
| failures.append( | |
| "expected memory missing: " | |
| f"{expected_pair[0]} => {expected_pair[1]}" | |
| ) | |
| continue | |
| pair_expected_status = expected_memory_status.get( | |
| expected_pair, | |
| expected_status, | |
| ) | |
| if pair_expected_status and memory.get("status") != pair_expected_status: | |
| failures.append( | |
| "memory status mismatch for " | |
| f"{expected_pair[0]} => {expected_pair[1]}: " | |
| f"expected={pair_expected_status!r} " | |
| f"actual={memory.get('status')!r}" | |
| ) | |
| for field, label in ( | |
| ("expect_negative_min", "negative evidence"), | |
| ("expect_positive_min", "positive evidence"), | |
| ): | |
| value = str(row.get(field, "")).strip() | |
| if not value: | |
| continue | |
| minimum = int(float(value)) | |
| key = ( | |
| "negative_evidence_count" | |
| if field == "expect_negative_min" | |
| else "positive_evidence_count" | |
| ) | |
| actual = int(memory.get(key, 0) or 0) | |
| if actual < minimum: | |
| failures.append( | |
| f"{label} too low for {expected_pair[0]} => {expected_pair[1]}: " | |
| f"expected>={minimum} actual={actual}" | |
| ) | |
| exception_min = str( | |
| row.get("expect_exception_min", "") | |
| ).strip() | |
| if exception_min: | |
| minimum = int(float(exception_min)) | |
| actual = len(memory.get("exceptions", []) or []) | |
| if actual < minimum: | |
| failures.append( | |
| f"exceptions too few for {expected_pair[0]} => {expected_pair[1]}: " | |
| f"expected>={minimum} actual={actual}" | |
| ) | |
| expected_scope = str( | |
| row.get("expect_scope", "") | |
| ).strip() | |
| if expected_scope and memory.get("scope") != expected_scope: | |
| failures.append( | |
| f"scope mismatch for {expected_pair[0]} => {expected_pair[1]}: " | |
| f"expected={expected_scope!r} actual={memory.get('scope')!r}" | |
| ) | |
| for value in _split(row.get("expect_memory_absent", "")): | |
| pair = _memory_pair(value) | |
| if pair and _memory_pair_exact_exists(memories, pair): | |
| failures.append( | |
| "unexpected exact memory exists: " | |
| f"{pair[0]} => {pair[1]}" | |
| ) | |
| return not failures, failures | |
| class ChainState: | |
| def __init__(self) -> None: | |
| self.notes: dict[str, str] = {} | |
| self.memories: list[dict[str, Any]] = [] | |
| self.last_generation: dict[str, Any] = {} | |
| self.last_trace: dict[str, Any] = {} | |
| self.history: list[dict[str, Any]] = [] | |
| def note(self, title: str) -> str: | |
| return self.notes.get(title, "") | |
| def set_note(self, title: str, content: str) -> None: | |
| self.notes[title] = content | |
| def reset(self) -> None: | |
| self.notes.clear() | |
| self.memories.clear() | |
| self.last_generation = {} | |
| self.last_trace = {} | |
| self.history.clear() | |
| class EvaluationRunner: | |
| def __init__(self, gemini: GeminiService | None = None) -> None: | |
| self.gemini = gemini or GeminiService() | |
| def _retrieve( | |
| self, | |
| text: str, | |
| note_title: str, | |
| note_content: str, | |
| memories: list[dict[str, Any]], | |
| policy: str, | |
| ) -> tuple[list[dict[str, Any]], float, str]: | |
| started = time.perf_counter() | |
| allow_ml = policy in {"auto", "ml_assisted", "force_llm"} | |
| if allow_ml: | |
| results = retrieve_memories( | |
| text, | |
| memories, | |
| note_title=note_title, | |
| scene_text=note_content[-600:], | |
| top_k=5, | |
| ) | |
| backend = ( | |
| results[0].get("retrieval_backend", "local") | |
| if results | |
| else "local/no-memory" | |
| ) | |
| else: | |
| results, _ = quick_retrieve_memories( | |
| text, | |
| memories, | |
| top_k=5, | |
| note_title=note_title, | |
| scene_text=note_content[-600:], | |
| ) | |
| backend = ( | |
| results[0].get( | |
| "retrieval_backend", | |
| "local lexical + context", | |
| ) | |
| if results | |
| else "local/no-memory" | |
| ) | |
| elapsed = round( | |
| (time.perf_counter() - started) * 1000, | |
| 2, | |
| ) | |
| return results, elapsed, backend | |
| def _fast_format(self, text: str) -> str: | |
| value = re.sub( | |
| r"\s+", | |
| " ", | |
| (text or "").strip(), | |
| ) | |
| value = re.sub( | |
| r"\b(?:um+|uh+|er+|ah+|hmm+)\b[\s,]*", | |
| " ", | |
| value, | |
| flags=re.IGNORECASE, | |
| ) | |
| value = re.sub( | |
| r"\b([A-Za-z]+)(?:\s+\1\b)+", | |
| r"\1", | |
| value, | |
| flags=re.IGNORECASE, | |
| ) | |
| value = re.sub( | |
| r"\s+([,.!?;:])", | |
| r"\1", | |
| value, | |
| ) | |
| value = re.sub( | |
| r"([,.!?;:])(?=\w)", | |
| r"\1 ", | |
| value, | |
| ) | |
| value = re.sub( | |
| r"\s{2,}", | |
| " ", | |
| value, | |
| ).strip() | |
| if not value: | |
| return "" | |
| value = value[0].upper() + value[1:] | |
| if value[-1] not in ".!?": | |
| value += "." | |
| return value | |
| def _candidate_ids(self, retrieved: list[dict[str, Any]]) -> set[str]: | |
| return route_candidate_ids(retrieved) | |
| def _route_name(self, backend: str, llm_used: bool) -> str: | |
| backend_text = str(backend or "").lower() | |
| ml_used = ( | |
| "minilm" in backend_text | |
| or "tf-idf" in backend_text | |
| or backend_text.startswith("ml ") | |
| or backend_text.startswith("ml:") | |
| ) | |
| if ml_used and llm_used: | |
| return "ml+llm" | |
| if llm_used: | |
| return "llm" | |
| if ml_used: | |
| return "ml" | |
| return "local" | |
| def _generate( | |
| self, | |
| state: ChainState, | |
| row: dict[str, str], | |
| policy: str, | |
| ) -> dict[str, Any]: | |
| note_title = str(row.get("note_title", "Default") or "Default") | |
| text = str(row.get("input", "") or "").strip() | |
| mode = str(row.get("mode", "Raw ASR") or "Raw ASR") | |
| note_content = state.note(note_title) | |
| scene = build_scene(note_title, note_content, text) | |
| retrieved, retrieval_ms, backend = self._retrieve( | |
| text, | |
| note_title, | |
| note_content, | |
| state.memories, | |
| policy, | |
| ) | |
| selected_ids = self._candidate_ids(retrieved) | |
| decision, local_ids, decision_reason, decision_details = local_memory_decision(retrieved) | |
| llm_metrics: dict[str, Any] = {"model": None, "api": None, "latency_ms": 0.0} | |
| llm_used = False | |
| rewritten = self._fast_format(text) | |
| decision_type = "context_skip" | |
| reason = decision_reason | |
| if policy == "force_llm" and self.gemini.available: | |
| try: | |
| result, llm_metrics = self.gemini.format_text( | |
| text, | |
| note_title, | |
| mode, | |
| scene["text"], | |
| memory_context(state.memories, selected_ids, limit=4), | |
| ) | |
| llm_used = True | |
| rewritten = result.formatted_text.strip() | |
| proposed = {memory_id for memory_id in result.memory_ids if memory_id in selected_ids} | |
| selected_ids = proposed | |
| decision_type = "llm_format" | |
| reason = "Force-LLM ablation requested a full LLM formatting pass." | |
| except Exception as exc: | |
| llm_metrics = {"model": self.gemini.model, "api": "interactions_v2", "latency_ms": 0.0, "error": str(exc)} | |
| decision_type = "fast_fallback" | |
| reason = "Gemini failed; Lexi used the local path." | |
| elif decision == "apply": | |
| selected_ids = local_ids | |
| decision_type = "fast_path" | |
| reason = decision_reason | |
| elif decision == "ambiguous" and policy == "auto" and should_escalate_to_llm( | |
| retrieved, | |
| decision, | |
| self.gemini.available, | |
| ): | |
| try: | |
| decision_result, llm_metrics = self.gemini.decide_memory_application( | |
| text, | |
| scene["text"], | |
| note_title, | |
| memory_context(state.memories, selected_ids, limit=4), | |
| ) | |
| llm_used = True | |
| proposed_ids = {memory_id for memory_id in decision_result.memory_ids if memory_id in selected_ids} | |
| if decision_result.decision == "apply" and proposed_ids: | |
| selected_ids = proposed_ids | |
| decision_type = "context_llm" | |
| reason = f"Local and ML evidence was ambiguous; Gemini resolved only among retrieved IDs: {decision_result.rationale}" | |
| else: | |
| selected_ids = set() | |
| decision_type = "context_skip" | |
| reason = f"Ambiguity remained unresolved, so Lexi did not intervene: {decision_result.rationale}" | |
| except Exception as exc: | |
| llm_metrics = {"model": self.gemini.model, "api": "interactions_v2", "latency_ms": 0.0, "error": str(exc)} | |
| selected_ids = set() | |
| decision_type = "context_skip" | |
| reason = "Context remained ambiguous and Lexi deliberately avoided intervention." | |
| else: | |
| selected_ids = set() | |
| decision_type = "context_skip" | |
| reason = decision_reason | |
| rewritten, changes = apply_memories( | |
| rewritten, | |
| state.memories, | |
| selected_ids=selected_ids, | |
| note_title=note_title, | |
| scene_text=note_content[-600:], | |
| ) | |
| state.set_note( | |
| note_title, | |
| rewritten if not note_content.strip() else note_content + "\n\n" + rewritten, | |
| ) | |
| state.last_generation = { | |
| "note_title": note_title, | |
| "input": text, | |
| "output": rewritten, | |
| "changes": changes, | |
| } | |
| route = self._route_name(backend, llm_used) | |
| trace = { | |
| "stage": "generation", | |
| "decision": decision_type, | |
| "semantic_event": _semantic_event( | |
| {"event": decision_type, "changes": changes, "retrieved": retrieved, "candidate_memory_ids": [item["memory_id"] for item in retrieved if item.get("term_present")]}, | |
| "generate", | |
| ), | |
| "mode": mode, | |
| "input": text, | |
| "output": rewritten, | |
| "reason": reason, | |
| "route": route, | |
| "retrieved": retrieved, | |
| "changes": changes, | |
| "retrieval_backend": backend, | |
| "routing": { | |
| "local_decision": decision, | |
| "local_details": decision_details, | |
| "llm_escalated": llm_used, | |
| }, | |
| "timing": {"retrieval_ms": retrieval_ms, "llm_ms": llm_metrics.get("latency_ms", 0.0)}, | |
| "model_metrics": llm_metrics, | |
| } | |
| state.last_trace = trace | |
| state.history.append(trace) | |
| return { | |
| "actual_output": rewritten, | |
| "event": decision_type, | |
| "semantic_event": trace["semantic_event"], | |
| "route": route, | |
| "retrieval_backend": backend, | |
| "retrieval_ms": retrieval_ms, | |
| "llm_ms": llm_metrics.get("latency_ms", 0.0), | |
| "ml_used": route in {"ml", "ml+llm"}, | |
| "llm_used": llm_used, | |
| "changes": changes, | |
| "candidate_memory_ids": [item["memory_id"] for item in retrieved if item.get("term_present") or item.get("retrieval_score", 0) >= 0.56], | |
| "memories": state.memories, | |
| "trace": trace, | |
| } | |
| def _edit( | |
| self, | |
| state: ChainState, | |
| row: dict[str, str], | |
| policy: str, | |
| ) -> dict[str, Any]: | |
| note_title = str(row.get("note_title", "Default") or "Default") | |
| before = str(row.get("before", "") or "") | |
| after = str(row.get("after", "") or "") | |
| state.set_note(note_title, after) | |
| previous_edit = state.history[-1] if state.history else {} | |
| last_generation = state.last_generation or {} | |
| llm_metrics: dict[str, Any] = {} | |
| llm_used = False | |
| updated, event = detect_generation_overrides( | |
| before, | |
| after, | |
| state.memories, | |
| last_generation, | |
| note_title, | |
| ) | |
| state.last_generation = {} | |
| if event.get("type") != "memory_override": | |
| updated, event = detect_memory_reversals( | |
| before, | |
| after, | |
| state.memories, | |
| note_title, | |
| ) | |
| if event.get("type") not in {"memory_override", "memory_negative_evidence"}: | |
| if policy == "force_llm" and self.gemini.available: | |
| try: | |
| analysis, llm_metrics = self.gemini.analyze_edit( | |
| before, | |
| after, | |
| note_title, | |
| build_scene(note_title, before, after)["text"], | |
| ) | |
| llm_used = True | |
| updated, event = incorporate_llm_proposals( | |
| state.memories, | |
| [proposal.model_dump() for proposal in analysis.proposals], | |
| before, | |
| after, | |
| note_title=note_title, | |
| decision=analysis.decision, | |
| ) | |
| event["llm_decision"] = analysis.model_dump() | |
| except Exception as exc: | |
| llm_metrics = {"model": self.gemini.model, "api": "interactions_v2", "latency_ms": 0.0, "error": str(exc)} | |
| updated, event = learn_from_edit( | |
| before, after, state.memories, | |
| previous_edit=previous_edit, | |
| last_generation={}, | |
| note_title=note_title, | |
| ) | |
| else: | |
| updated, event = learn_from_edit( | |
| before, after, state.memories, | |
| previous_edit=previous_edit, | |
| last_generation={}, | |
| note_title=note_title, | |
| ) | |
| state.memories = [normalize_memory(memory) for memory in updated] | |
| semantic_event = _semantic_event({"event": event.get("type", "unknown")}, "edit") | |
| route = "llm" if llm_used else "local" | |
| trace = { | |
| "stage": "learning", | |
| "decision": event.get("type", "unknown"), | |
| "semantic_event": semantic_event, | |
| "before": before, | |
| "after": after, | |
| "note_title": note_title, | |
| "event": event, | |
| "route": route, | |
| "model_metrics": llm_metrics, | |
| } | |
| state.last_trace = trace | |
| state.history.append(trace) | |
| return { | |
| "actual_output": after, | |
| "event": event.get("type", "unknown"), | |
| "semantic_event": semantic_event, | |
| "route": route, | |
| "retrieval_backend": "n/a", | |
| "retrieval_ms": 0.0, | |
| "llm_ms": llm_metrics.get("latency_ms", 0.0), | |
| "ml_used": False, | |
| "llm_used": llm_used, | |
| "changes": [], | |
| "memories": state.memories, | |
| "trace": trace, | |
| } | |
| def run_rows( | |
| self, | |
| rows: list[dict[str, str]], | |
| policy: str = "auto", | |
| ) -> dict[str, Any]: | |
| if policy not in { | |
| "auto", | |
| "local_only", | |
| "ml_assisted", | |
| "no_ml", | |
| "force_llm", | |
| }: | |
| policy = "auto" | |
| chains: dict[str, ChainState] = {} | |
| results: list[dict[str, Any]] = [] | |
| started_total = time.perf_counter() | |
| for raw in rows: | |
| row = { | |
| str(k): str(v or "") | |
| for k, v in raw.items() | |
| } | |
| chain_id = row.get("chain_id", "default") or "default" | |
| state = chains.setdefault( | |
| chain_id, | |
| ChainState(), | |
| ) | |
| if _bool(row.get("reset", "0")): | |
| state.reset() | |
| action = ( | |
| row.get("action", "generate") | |
| or "generate" | |
| ).strip().lower() | |
| if policy == "force_llm" and not self.gemini.available: | |
| actual = { | |
| "actual_output": "", | |
| "event": "error", | |
| "semantic_event": "error", | |
| "route": "llm", | |
| "memories": state.memories, | |
| "trace": {}, | |
| } | |
| actual["error"] = "Gemini is not configured." | |
| elif action == "reset": | |
| state.reset() | |
| actual = { | |
| "actual_output": "", | |
| "event": "reset", | |
| "semantic_event": "reset", | |
| "route": "local", | |
| "memories": state.memories, | |
| "trace": {}, | |
| } | |
| elif action == "edit": | |
| actual = self._edit( | |
| state, | |
| row, | |
| policy, | |
| ) | |
| elif action == "generate": | |
| if policy == "force_llm" and self.gemini.available: | |
| effective_policy = "auto" | |
| original = row.get("mode", "Raw ASR") | |
| if original == "Formatted": | |
| actual = self._generate( | |
| state, | |
| row, | |
| effective_policy, | |
| ) | |
| else: | |
| actual = self._generate( | |
| state, | |
| row, | |
| effective_policy, | |
| ) | |
| else: | |
| actual = self._generate( | |
| state, | |
| row, | |
| policy, | |
| ) | |
| else: | |
| actual = { | |
| "actual_output": "", | |
| "event": "error", | |
| "semantic_event": "error", | |
| "route": "local", | |
| "memories": state.memories, | |
| "trace": {}, | |
| "error": f"Unknown action: {action}", | |
| } | |
| passed, failures = _assert_row( | |
| row, | |
| actual, | |
| ) | |
| results.append( | |
| { | |
| "chain_id": chain_id, | |
| "step": row.get("step", ""), | |
| "action": action, | |
| "mode": row.get("mode", ""), | |
| "note_title": row.get("note_title", ""), | |
| "status": "PASS" if passed else "FAIL", | |
| "passed": passed, | |
| "event": actual.get("event", ""), | |
| "semantic_event": actual.get("semantic_event", ""), | |
| "route": actual.get("route", ""), | |
| "ml_used": actual.get("ml_used", False), | |
| "llm_used": actual.get("llm_used", False), | |
| "actual_output": actual.get("actual_output", ""), | |
| "failures": " | ".join(failures), | |
| "retrieval_backend": actual.get("retrieval_backend", ""), | |
| "retrieval_ms": actual.get("retrieval_ms", 0.0), | |
| "llm_ms": actual.get("llm_ms", 0.0), | |
| "memory_count": len(actual.get("memories", [])), | |
| "memory_state": json.dumps( | |
| _memory_snapshot( | |
| actual.get("memories", []) | |
| ), | |
| ensure_ascii=False, | |
| ), | |
| "trace": json.dumps( | |
| actual.get("trace", {}), | |
| ensure_ascii=False, | |
| ), | |
| "comment": row.get("comment", ""), | |
| } | |
| ) | |
| passed = sum( | |
| bool(item["passed"]) | |
| for item in results | |
| ) | |
| llm_calls = sum( | |
| bool(item.get("llm_used")) | |
| for item in results | |
| ) | |
| ml_calls = sum( | |
| bool(item.get("ml_used")) | |
| for item in results | |
| ) | |
| route_counts: dict[str, int] = {} | |
| for item in results: | |
| route = _normalize_route(item.get("route", "")) | |
| route_counts[route] = route_counts.get(route, 0) + 1 | |
| return { | |
| "summary": { | |
| "policy": policy, | |
| "total_steps": len(results), | |
| "passed_steps": passed, | |
| "failed_steps": len(results) - passed, | |
| "pass_rate": round( | |
| passed / len(results), | |
| 4, | |
| ) if results else 0.0, | |
| "llm_calls": llm_calls, | |
| "ml_calls": ml_calls, | |
| "route_counts": route_counts, | |
| "total_llm_ms": round( | |
| sum( | |
| float(item.get("llm_ms", 0.0) or 0.0) | |
| for item in results | |
| ), | |
| 1, | |
| ), | |
| "total_ml_ms": round( | |
| sum( | |
| float(item.get("retrieval_ms", 0.0) or 0.0) | |
| for item in results | |
| if item.get("ml_used") | |
| ), | |
| 1, | |
| ), | |
| "total_runtime_ms": round( | |
| ( | |
| time.perf_counter() | |
| - started_total | |
| ) * 1000, | |
| 1, | |
| ), | |
| "gemini_available": self.gemini.available, | |
| }, | |
| "results": results, | |
| } | |
| def load_csv(path: str | Path) -> list[dict[str, str]]: | |
| with open( | |
| path, | |
| "r", | |
| encoding="utf-8-sig", | |
| newline="", | |
| ) as handle: | |
| reader = csv.DictReader(handle) | |
| if reader.fieldnames is None: | |
| raise ValueError("CSV has no header row.") | |
| field_map = { | |
| str(field).strip(): field | |
| for field in reader.fieldnames | |
| if field is not None | |
| } | |
| fields = set(field_map) | |
| missing = [ | |
| column | |
| for column in REQUIRED_COLUMNS | |
| if column not in fields | |
| ] | |
| if missing: | |
| raise ValueError( | |
| "CSV is missing required columns: " | |
| + ", ".join(missing) | |
| + ". Required columns are chain_id, step, action, mode." | |
| ) | |
| deprecated = sorted(fields.intersection(DEPRECATED_COLUMNS)) | |
| if deprecated: | |
| raise ValueError( | |
| "Remove " | |
| + ", ".join(deprecated) | |
| + ". Lexi now chooses ML/LLM routing automatically; " | |
| "the CSV must describe only the user journey and expected behavior." | |
| ) | |
| rows = [] | |
| for index, raw in enumerate(reader, start=2): | |
| row = { | |
| clean_key: str(raw.get(original_key, "") or "") | |
| for clean_key, original_key in field_map.items() | |
| } | |
| if not any(value.strip() for value in row.values()): | |
| continue | |
| action = row.get("action", "").strip().lower() | |
| if action == "generate" and not row.get("input", "").strip(): | |
| raise ValueError( | |
| f"Row {index}: generate rows require input." | |
| ) | |
| if action == "edit" and ( | |
| not row.get("before", "").strip() | |
| and not row.get("after", "").strip() | |
| ): | |
| raise ValueError( | |
| f"Row {index}: edit rows require before/after." | |
| ) | |
| rows.append(row) | |
| return rows | |
| def write_reports( | |
| report: dict[str, Any], | |
| output_dir: str | Path, | |
| ) -> tuple[str, str]: | |
| output = Path(output_dir) | |
| output.mkdir( | |
| parents=True, | |
| exist_ok=True, | |
| ) | |
| stamp = time.strftime( | |
| "%Y%m%d_%H%M%S" | |
| ) | |
| json_path = output / f"lexi_evaluation_{stamp}.json" | |
| csv_path = output / f"lexi_evaluation_{stamp}.csv" | |
| json_path.write_text( | |
| json.dumps( | |
| report, | |
| indent=2, | |
| ensure_ascii=False, | |
| ), | |
| encoding="utf-8", | |
| ) | |
| results = report.get("results", []) | |
| fieldnames = list(results[0].keys()) if results else [] | |
| with csv_path.open( | |
| "w", | |
| encoding="utf-8", | |
| newline="", | |
| ) as handle: | |
| writer = csv.DictWriter( | |
| handle, | |
| fieldnames=fieldnames, | |
| ) | |
| if fieldnames: | |
| writer.writeheader() | |
| writer.writerows(results) | |
| return str(json_path), str(csv_path) | |