| import os |
| import re |
| import json |
| import sqlite3 |
| import requests |
| import base64 |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.responses import FileResponse |
| from fastapi import UploadFile, File |
| from pydantic import BaseModel |
| from gradio import Server |
|
|
| |
| |
| |
| try: |
| import spaces |
| except ImportError: |
| |
| class spaces: |
| @staticmethod |
| def GPU(func): |
| return func |
|
|
| @spaces.GPU |
| def dummy_gpu_trigger(): |
| """Dummy function to satisfy HF ZeroGPU startup checks""" |
| return None |
|
|
| |
| |
| |
| DB_DIR = "/data" if os.path.exists("/data") else "." |
| DB_PATH = os.path.join(DB_DIR, "memrl_memory.db") |
|
|
| def init_db(): |
| os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS episodic_memory ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| query TEXT, |
| language TEXT DEFAULT 'multi', |
| canonical_intent TEXT, |
| canonical_slots_json TEXT, |
| action_json TEXT, |
| q_value REAL, |
| UNIQUE(query, canonical_intent) |
| ) |
| """) |
| |
| cursor.execute("SELECT COUNT(*) FROM episodic_memory") |
| if cursor.fetchone()[0] == 0: |
| baseline_memories = [ |
| ("draw a red circle", "en", "draw_shape", json.dumps({"shape": "circle", "color": "red", "size": 100, "x": "center", "y": "center"}), |
| json.dumps([{"shape": "circle", "color": "red", "size": 100, "x": "center", "y": "center"}]), 1.0), |
| ("dibuja un circulo azul en el centro", "es", "draw_shape", json.dumps({"shape": "circle", "color": "blue", "size": 100, "x": "center", "y": "center"}), |
| json.dumps([{"shape": "circle", "color": "blue", "size": 100, "x": "center", "y": "center"}]), 1.0), |
| ("make a green square", "en", "draw_shape", json.dumps({"shape": "square", "color": "green", "size": 120, "x": "center", "y": "center"}), |
| json.dumps([{"shape": "square", "color": "green", "size": 120, "x": "center", "y": "center"}]), 1.0), |
| ("clear canvas", "en", "clear", json.dumps({"shape": "clear"}), |
| json.dumps([{"shape": "clear", "color": "white", "size": 0}]), 1.0) |
| ] |
| cursor.executemany( |
| "INSERT INTO episodic_memory (query, language, canonical_intent, canonical_slots_json, action_json, q_value) VALUES (?, ?, ?, ?, ?, ?)", |
| baseline_memories |
| ) |
| conn.commit() |
| conn.close() |
|
|
| def levenshtein_similarity(s1: str, s2: str) -> float: |
| s1 = s1.lower().strip().strip(".?,!") |
| s2 = s2.lower().strip().strip(".?,!") |
| if s1 == s2: |
| return 1.0 |
| m, n = len(s1), len(s2) |
| if max(m, n) == 0: |
| return 1.0 |
| |
| dp = [[0] * (n + 1) for _ in range(m + 1)] |
| for i in range(m + 1): |
| dp[i][0] = i |
| for j in range(n + 1): |
| dp[0][j] = j |
| |
| for i in range(1, m + 1): |
| for j in range(1, n + 1): |
| if s1[i-1] == s2[j-1]: |
| dp[i][j] = dp[i-1][j-1] |
| else: |
| dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 |
| |
| return 1.0 - (dp[m][n] / max(m, n)) |
|
|
| def find_memory_match(query: str, threshold: float = 0.75) -> dict: |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| cursor.execute("SELECT query, action_json, q_value FROM episodic_memory") |
| rows = cursor.fetchall() |
| conn.close() |
| |
| best_match = None |
| max_sim = 0.0 |
| |
| for db_query, action_json, q_value in rows: |
| sim = levenshtein_similarity(query, db_query) |
| if sim > max_sim: |
| max_sim = sim |
| best_match = (db_query, action_json, q_value) |
| |
| if max_sim >= threshold and best_match: |
| return { |
| "matched_query": best_match[0], |
| "action": json.loads(best_match[1]), |
| "q_value": best_match[2], |
| "similarity": max_sim |
| } |
| return None |
|
|
| |
| |
| |
| MODAL_API_URL = os.getenv("MODAL_API_URL", "https://idebroy--memrl-canvas-backend-api.modal.run") |
|
|
| def rule_based_fallback(user_text: str) -> list: |
| lower_text = user_text.lower() |
| |
| |
| parts = re.split(r'\band\b|\bthen\b|,', lower_text) |
| actions = [] |
| |
| shapes_list = ["circle", "square", "rectangle", "triangle", "star", "pentagon", "hexagon", "oval", "heart", "line", "arrow", "text", "clear", "wipe", "reset"] |
| |
| |
| color_variants = { |
| "red": ["red", "rojo", "rouge"], |
| "blue": ["blue", "azul", "bleu"], |
| "green": ["green", "verde", "vert"], |
| "yellow": ["yellow", "amarillo", "jaune"], |
| "orange": ["orange", "naranja"], |
| "purple": ["purple", "morado", "violet", "pourpre"], |
| "pink": ["pink", "rosa", "rose"], |
| "black": ["black", "negro", "noir"], |
| "white": ["white", "blanco", "blanc"], |
| "cyan": ["cyan"], |
| "magenta": ["magenta"], |
| } |
| |
| for part in parts: |
| part = part.strip() |
| if not part: |
| continue |
| |
| |
| if any(w in part for w in ["clear", "wipe", "reset"]): |
| actions.append({"shape": "clear", "color": "white", "size": 0}) |
| continue |
| |
| |
| color = "black" |
| part_lower = part.lower() |
| for eng_color, variants in color_variants.items(): |
| if any(v in part_lower for v in variants): |
| color = eng_color |
| break |
| |
| |
| size = 100 |
| size_match = re.search(r"\b(\d+)\b", part) |
| if size_match: |
| val = int(size_match.group(1)) |
| if 30 <= val <= 300: |
| size = val |
| |
| |
| px = "center" |
| if "left" in part: |
| px = "left" |
| elif "right" in part: |
| px = "right" |
| |
| py = "center" |
| if "top" in part: |
| py = "top" |
| elif "bottom" in part: |
| py = "bottom" |
| |
| |
| if any(w in part for w in ["path", "freehand", "go", "move", "draw line to"]): |
| operations = [] |
| operations.append({"type": "start", "x": px, "y": py}) |
| |
| |
| words = part.replace(",", " ").split() |
| for i, word in enumerate(words): |
| if word in ["right", "east"] and i + 1 < len(words): |
| val = re.sub("[^0-9]", "", words[i+1]) |
| if val: operations.append({"type": "line", "dx": int(val), "dy": 0}) |
| elif word in ["left", "west"] and i + 1 < len(words): |
| val = re.sub("[^0-9]", "", words[i+1]) |
| if val: operations.append({"type": "line", "dx": -int(val), "dy": 0}) |
| elif word in ["down", "south"] and i + 1 < len(words): |
| val = re.sub("[^0-9]", "", words[i+1]) |
| if val: operations.append({"type": "line", "dx": 0, "dy": int(val)}) |
| elif word in ["up", "north"] and i + 1 < len(words): |
| val = re.sub("[^0-9]", "", words[i+1]) |
| if val: operations.append({"type": "line", "dx": 0, "dy": -int(val)}) |
| elif word in ["dot", "point"]: |
| operations.append({"type": "dot", "radius": 8}) |
| |
| if len(operations) > 1: |
| actions.append({ |
| "shape": "path", |
| "color": color, |
| "thickness": 5, |
| "operations": operations |
| }) |
| continue |
| |
| |
| text_match = re.search(r'["\']([^"\']+)["\']', part) |
| is_text_cmd = any(w in part for w in ["text", "write", "say", "label", "title"]) |
| if is_text_cmd: |
| label = text_match.group(1).strip() if text_match else (part.split()[-1] if len(part.split()) > 1 else "label") |
| font_size = max(24, min(size, 160)) |
| actions.append({ |
| "shape": "text", |
| "text": label, |
| "color": color, |
| "size": font_size, |
| "x": px, |
| "y": py |
| }) |
| continue |
|
|
| |
| shape_found = None |
| for s in shapes_list: |
| if s in part: |
| shape_found = s |
| break |
| |
| |
| if shape_found in ["line", "arrow"]: |
| start_x = "left" if "left" in part else "center" |
| start_y = "top" if "top" in part else "center" |
| end_x = 380 if "right" in part else 250 |
| end_y = 380 if "bottom" in part else 250 |
| actions.append({ |
| "shape": shape_found, |
| "color": color, |
| "start_x": start_x, |
| "start_y": start_y, |
| "end_x": end_x, |
| "end_y": end_y |
| }) |
| elif shape_found: |
| actions.append({ |
| "shape": shape_found, |
| "color": color, |
| "size": size, |
| "x": px, |
| "y": py |
| }) |
| else: |
| |
| actions.append({ |
| "shape": "circle", |
| "color": color, |
| "size": size, |
| "x": px, |
| "y": py |
| }) |
| |
| return actions |
|
|
| |
|
|
| def get_rag_examples(user_text: str, limit: int = 3): |
| """Simple RAG retrieval for few-shot examples (language-agnostic focus for demo).""" |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| cursor.execute( |
| "SELECT query, language, canonical_intent, canonical_slots_json, action_json FROM episodic_memory " |
| "WHERE q_value >= 0.4 ORDER BY q_value DESC, id DESC LIMIT ?", (limit,) |
| ) |
| rows = cursor.fetchall() |
| conn.close() |
| examples = [] |
| for q, lang, cintent, cslots, act in rows: |
| if cintent: |
| ex = f"User (lang={lang or 'multi'}): \"{q}\"\nCanonical: intent={cintent}, slots={cslots}\nAction: {act}" |
| else: |
| ex = f"User (lang={lang or 'multi'}): \"{q}\"\nAction: {act}" |
| examples.append(ex) |
| return "\n\n".join(examples) if examples else "No strong prior examples yet." |
|
|
|
|
| def canonical_slots_to_action(canonical: dict) -> list: |
| """Convert a canonical slots dict back into a proper action object (robust fallback for demo).""" |
| if not canonical or "slots" not in canonical: |
| return [] |
| slots = canonical.get("slots", {}) |
| intent = canonical.get("intent", "draw_shape") |
|
|
| if intent == "clear": |
| return [{"shape": "clear", "color": "white", "size": 0}] |
|
|
| shape = slots.get("shape") or "circle" |
| color = slots.get("color") or "black" |
| size = slots.get("size") or 100 |
| x = slots.get("x") or "center" |
| y = slots.get("y") or "center" |
|
|
| if shape in ["line", "arrow"]: |
| return [{ |
| "shape": shape, |
| "color": color, |
| "start_x": x, |
| "start_y": y, |
| "end_x": 380 if str(x) == "right" else 250, |
| "end_y": 380 if str(y) == "bottom" else 250 |
| }] |
|
|
| if shape == "path": |
| return [{ |
| "shape": "path", |
| "color": color, |
| "thickness": 5, |
| "operations": [ |
| {"type": "start", "x": x, "y": y}, |
| {"type": "line", "dx": 80, "dy": 0} |
| ] |
| }] |
|
|
| if shape == "text": |
| return [{ |
| "shape": "text", |
| "text": slots.get("text") or "label", |
| "color": color, |
| "size": max(24, min(size, 160)), |
| "x": x, |
| "y": y |
| }] |
|
|
| |
| return [{ |
| "shape": shape, |
| "color": color, |
| "size": size, |
| "x": x, |
| "y": y |
| }] |
|
|
|
|
| def get_canonical_from_text(text: str) -> dict: |
| """Lightweight canonical extractor using the multilingual rule logic. |
| Enables MemRL to match across languages on intent + slots instead of raw transcription string. |
| """ |
| if not text: |
| return {"intent": "unknown", "slots": {}} |
| lower = text.lower().strip() |
| parts = re.split(r'\band\b|\bthen\b|,', lower) |
|
|
| intent = "draw_shape" |
| slots = {} |
|
|
| for part in parts: |
| part = part.strip() |
| if not part: |
| continue |
|
|
| if any(w in part for w in ["clear", "wipe", "reset"]): |
| return {"intent": "clear", "slots": {}} |
|
|
| |
| color = extract_color(part) if 'extract_color' in dir() else "black" |
| if color and color != "black": |
| slots["color"] = color |
|
|
| |
| size_match = re.search(r"\b(\d+)\b", part) |
| if size_match: |
| val = int(size_match.group(1)) |
| if 30 <= val <= 300: |
| slots["size"] = val |
|
|
| |
| if "left" in part: |
| slots["x"] = "left" |
| elif "right" in part: |
| slots["x"] = "right" |
| if any(w in part for w in ["top", "arriba", "haut"]): |
| slots["y"] = "top" |
| elif any(w in part for w in ["bottom", "abajo", "bas"]): |
| slots["y"] = "bottom" |
| if any(w in part for w in ["middle", "center", "centro", "centre", "mitad", "medio"]): |
| slots.setdefault("x", "center") |
| slots.setdefault("y", "center") |
|
|
| |
| for s in ["circle", "square", "rectangle", "triangle", "star", "pentagon", "hexagon", "oval", "heart", "line", "arrow", "path", "text"]: |
| if s in part: |
| slots["shape"] = s |
| break |
|
|
| if any(w in part for w in ["text", "write", "escribe", "écris", "label"]): |
| intent = "text_label" |
| text_match = re.search(r'["\']([^"\']+)["\']', part) |
| if text_match: |
| slots["text"] = text_match.group(1).strip() |
| elif "memrl" in part: |
| slots["text"] = "MemRL" |
|
|
| if "shape" not in slots and intent == "draw_shape": |
| slots["shape"] = "circle" |
|
|
| return {"intent": intent, "slots": slots} |
|
|
|
|
| def query_gemma_interpreter(user_text: str) -> list: |
| rag_context = get_rag_examples(user_text) |
| prompt = f"""You are a multilingual ASR command interpreter for a drawing canvas. |
| Convert the spoken text into a language-agnostic CANONICAL representation + the concrete drawing actions. |
| |
| CRITICAL RULES: |
| - Always output BOTH "canonical" and "actions". |
| - "actions" MUST be a NON-EMPTY array of valid drawing objects (use the schemas below). |
| - Fill reasonable defaults for any missing parameters (color=black, size=100, position=center). |
| - "miidle", "medio", "middle", "centro" → "center". |
| - Colors: ALWAYS normalize to English CSS names (red for rojo/rouge, blue for azul/bleu, green for verde/vert, etc.). Never leave color in Spanish/French. |
| - Never return "actions": [] |
| |
| Output EXACTLY this JSON structure (no other keys, no explanation): |
| |
| {{ |
| "canonical": {{ |
| "intent": "draw_shape", |
| "slots": {{"shape": "circle", "color": "red", "size": 100, "x": "center", "y": "center"}} |
| }}, |
| "actions": [ ... valid action objects ... ] |
| }} |
| |
| Available intents: "draw_shape", "draw_path", "clear", "text_label" |
| Available shapes: "circle", "square", "rectangle", "triangle", "star", "pentagon", "hexagon", "oval", "heart", "line", "arrow", "path", "text", "clear" |
| Positions: "left", "right", "center", "top", "bottom" (or numbers) |
| Colors: standard English CSS names (red, blue, green, yellow, black, white, etc.) — always normalize from other languages |
| |
| RAG examples (follow the style): |
| {rag_context} |
| |
| User input: "{user_text}" |
| |
| Return ONLY the JSON object above.""" |
|
|
| response = None |
| payload = { |
| "prompt": prompt, |
| "api_key": os.getenv("MODAL_API_KEY") |
| } |
| |
| try: |
| print(f"Querying Gemma interpreter on Modal at {MODAL_API_URL}/gemma...") |
| resp = requests.post(f"{MODAL_API_URL}/gemma", json=payload, timeout=60) |
| if resp.status_code == 200: |
| response = resp.json().get("response") |
| else: |
| print(f"Modal Gemma API returned status {resp.status_code}: {resp.text}") |
| except Exception as err: |
| print(f"Modal Gemma API failed to connect: {str(err)}") |
|
|
| if not response: |
| print("Falling back to local rule-based parser...") |
| return rule_based_fallback(user_text) |
|
|
| try: |
| print("Gemma raw response:", response) |
| |
| cleaned = response.strip() |
| data = None |
| try: |
| data = json.loads(cleaned) |
| except: |
| pass |
|
|
| actions = [] |
| if isinstance(data, dict): |
| actions = data.get("actions", []) |
| |
| if not actions and "canonical" in data: |
| actions = canonical_slots_to_action(data["canonical"]) |
| else: |
| |
| match = re.search(r"\[.*?\]", cleaned, re.DOTALL) |
| if match: |
| cleaned = match.group(0) |
| else: |
| match_brace = re.search(r"\{.*?\}", cleaned, re.DOTALL) |
| if match_brace: |
| cleaned = "[" + match_brace.group(0) + "]" |
| actions = json.loads(cleaned) if cleaned else [] |
|
|
| if not actions: |
| |
| actions = rule_based_fallback(user_text) |
| return actions |
| except Exception as e: |
| print(f"Gemma JSON parsing failed: {str(e)}") |
| return rule_based_fallback(user_text) |
|
|
| |
| |
| |
| app = Server() |
|
|
| @app.post("/api/transcribe") |
| async def transcribe_endpoint(audio: UploadFile = File(...)): |
| raw_text = "" |
| try: |
| content = await audio.read() |
| audio_b64 = base64.b64encode(content).decode("utf-8") |
| |
| payload = { |
| "audio_base64": audio_b64, |
| "api_key": os.getenv("MODAL_API_KEY") |
| } |
| |
| print(f"Calling Modal ASR service at {MODAL_API_URL}/transcribe...") |
| resp = requests.post(f"{MODAL_API_URL}/transcribe", json=payload, timeout=30) |
| if resp.status_code == 200: |
| raw_text = resp.json().get("text", "").strip() |
| print(f"Transcribed text from Modal: '{raw_text}'") |
| else: |
| print(f"Modal transcription API returned status {resp.status_code}: {resp.text}") |
| except Exception as e: |
| print(f"Modal ASR connection/transcription failed: {str(e)}") |
| |
| if not raw_text: |
| return { |
| "transcription": "", |
| "status": "No speech detected or Modal backend offline", |
| "action": [], |
| "canonical": None, |
| "language": "multi", |
| "q_value": 0.0, |
| "similarity": 0.0 |
| } |
| |
| |
| current_canonical = get_canonical_from_text(raw_text) |
| lang = "multi" |
| lower = raw_text.lower() |
| if any(w in lower for w in ["dibuja", "circulo", "cuadrado", "rojo", "azul", "verde"]): |
| lang = "es" |
| elif any(w in lower for w in ["bana", "laal", "hara", "square", "circle", "center"]): |
| lang = "hi" |
| else: |
| lang = "en" |
|
|
| |
| match = find_memory_match(raw_text, threshold=0.75) |
|
|
| |
| |
| canonical_match = None |
| if current_canonical.get("intent"): |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| cursor.execute( |
| "SELECT query, language, canonical_intent, canonical_slots_json, action_json, q_value " |
| "FROM episodic_memory WHERE canonical_intent = ? AND q_value >= 0.5 " |
| "ORDER BY q_value DESC LIMIT 5", |
| (current_canonical["intent"],) |
| ) |
| rows = cursor.fetchall() |
| conn.close() |
|
|
| best_score = 0.0 |
| for q, l, cint, cslots_json, act_json, qv in rows: |
| try: |
| stored_slots = json.loads(cslots_json) if cslots_json else {} |
| except: |
| stored_slots = {} |
| current_slots = current_canonical.get("slots", {}) |
| overlap = sum(1 for k in ["shape", "color", "x", "y"] if k in current_slots and k in stored_slots and current_slots.get(k) == stored_slots.get(k)) |
| score = qv * (0.6 + 0.4 * (overlap / 4.0)) |
| if score > best_score: |
| best_score = score |
| canonical_match = { |
| "action": json.loads(act_json), |
| "q_value": qv, |
| "similarity": round(score, 3), |
| "matched_query": q |
| } |
|
|
| |
| effective_match = None |
| status = "" |
| if canonical_match and canonical_match["q_value"] >= 0.7: |
| effective_match = canonical_match |
| status = "MemRL Template Hit (language-agnostic via canonical)" |
| elif match and match["q_value"] >= 0.8: |
| effective_match = match |
| status = "MemRL Match Found (Auto-Executed)" |
| elif match and match["q_value"] >= 0.4: |
| effective_match = match |
| status = "MemRL Suggestion (Low Confidence)" |
| elif canonical_match and canonical_match["q_value"] >= 0.5: |
| effective_match = canonical_match |
| status = "MemRL Template Hit (language-agnostic via canonical)" |
|
|
| if effective_match: |
| return { |
| "transcription": raw_text, |
| "status": status, |
| "action": effective_match["action"], |
| "canonical": current_canonical, |
| "language": lang, |
| "q_value": effective_match["q_value"], |
| "similarity": effective_match.get("similarity", 0.0) |
| } |
|
|
| |
| action = query_gemma_interpreter(raw_text) |
| canonical = current_canonical if current_canonical.get("intent") else None |
|
|
| return { |
| "transcription": raw_text, |
| "status": "Gemma + RAG (canonical, multilingual)", |
| "action": action, |
| "canonical": canonical, |
| "language": lang, |
| "q_value": 0.0, |
| "similarity": 0.0 |
| } |
|
|
| class ReinforceRequest(BaseModel): |
| query: str |
| action_json: str |
| reward: float |
| corrected_action_json: str = None |
| language: str = "multi" |
| canonical_intent: str = None |
| canonical_slots_json: str = None |
|
|
| @app.post("/api/reinforce") |
| def reinforce_endpoint(req: ReinforceRequest): |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| |
| query = req.query.lower().strip() |
| alpha = 0.3 |
| initial_q = 0.5 |
| |
| cursor.execute("SELECT action_json, q_value FROM episodic_memory WHERE query = ?", (query,)) |
| row = cursor.fetchone() |
| |
| msg = "" |
| lang = req.language or "multi" |
| canon_intent = req.canonical_intent |
| canon_slots = req.canonical_slots_json |
|
|
| |
| if not canon_intent and req.corrected_action_json: |
| try: |
| acts = json.loads(req.corrected_action_json) |
| if acts and isinstance(acts, list) and len(acts) > 0: |
| first = acts[0] |
| canon_intent = first.get("shape", "draw_shape") if first.get("shape") != "clear" else "clear" |
| canon_slots = json.dumps({k: v for k, v in first.items() if k != "shape"}) |
| except: |
| pass |
|
|
| if req.reward == 1.0: |
| |
| |
| target_action = req.corrected_action_json or req.action_json |
| if row: |
| old_action, old_q = row |
| new_q = old_q + alpha * (1.0 - old_q) |
| cursor.execute( |
| "UPDATE episodic_memory SET action_json = ?, q_value = ?, language = ?, canonical_intent = ?, canonical_slots_json = ? WHERE query = ?", |
| (target_action, new_q, lang, canon_intent, canon_slots, query) |
| ) |
| msg = f"Reinforced memory '{query}' (corrected, lang-agnostic) Q-value to {new_q:.3f}." |
| else: |
| new_q = initial_q + alpha * (1.0 - initial_q) |
| cursor.execute( |
| "INSERT INTO episodic_memory (query, language, canonical_intent, canonical_slots_json, action_json, q_value) VALUES (?, ?, ?, ?, ?, ?)", |
| (query, lang, canon_intent, canon_slots, target_action, new_q) |
| ) |
| msg = f"Created new language-agnostic memory '{query}' with Q-value {new_q:.3f}." |
| else: |
| |
| if req.corrected_action_json: |
| |
| corrected_action = req.corrected_action_json |
| new_q = 0.80 |
| if row: |
| cursor.execute( |
| "UPDATE episodic_memory SET action_json = ?, q_value = ?, language = ?, canonical_intent = ?, canonical_slots_json = ? WHERE query = ?", |
| (corrected_action, new_q, lang, canon_intent, canon_slots, query) |
| ) |
| else: |
| cursor.execute( |
| "INSERT INTO episodic_memory (query, language, canonical_intent, canonical_slots_json, action_json, q_value) VALUES (?, ?, ?, ?, ?, ?)", |
| (query, lang, canon_intent, canon_slots, corrected_action, new_q) |
| ) |
| msg = f"Saved user manual override mapping for '{query}' (lang-agnostic) with Q-value {new_q:.3f}." |
| else: |
| |
| if row: |
| old_action, old_q = row |
| new_q = old_q + alpha * (0.0 - old_q) |
| cursor.execute("UPDATE episodic_memory SET q_value = ? WHERE query = ?", (new_q, query)) |
| msg = f"Decayed memory '{query}' Q-value to {new_q:.3f}." |
| else: |
| msg = f"No memory mapping found for '{query}' to decay." |
| |
| conn.commit() |
| conn.close() |
| return {"status": "success", "message": msg} |
|
|
| @app.get("/api/memories") |
| def memories_endpoint(): |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| cursor.execute("SELECT query, language, canonical_intent, canonical_slots_json, action_json, q_value FROM episodic_memory ORDER BY q_value DESC") |
| rows = cursor.fetchall() |
| conn.close() |
| |
| memories = [] |
| for query, language, canonical_intent, canonical_slots_json, action_json, q_value in rows: |
| mem = { |
| "query": query, |
| "language": language or "multi", |
| "action": json.loads(action_json), |
| "q_value": round(q_value, 3) |
| } |
| if canonical_intent: |
| mem["canonical_intent"] = canonical_intent |
| if canonical_slots_json: |
| try: |
| mem["canonical"] = json.loads(canonical_slots_json) |
| except: |
| pass |
| memories.append(mem) |
| return {"memories": memories} |
|
|
| @app.post("/api/clear_memories") |
| def clear_memories_endpoint(): |
| conn = sqlite3.connect(DB_PATH) |
| cursor = conn.cursor() |
| cursor.execute("DROP TABLE IF EXISTS episodic_memory") |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS episodic_memory ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| query TEXT, |
| language TEXT DEFAULT 'multi', |
| canonical_intent TEXT, |
| canonical_slots_json TEXT, |
| action_json TEXT, |
| q_value REAL, |
| UNIQUE(query, canonical_intent) |
| ) |
| """) |
| conn.commit() |
| conn.close() |
| return {"status": "success", "message": "All episodic memories cleared. (Fresh DB for demo)"} |
|
|
| |
| |
| |
| app.mount("/static", StaticFiles(directory="static"), name="static") |
|
|
| @app.get("/") |
| def read_root(): |
| return FileResponse("static/index.html") |
|
|
| if __name__ == "__main__": |
| init_db() |
| |
| app.launch(server_name="0.0.0.0", server_port=7860) |
|
|