Spaces:
Running
Running
| """ | |
| ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: | |
| Script Name : memory_tab.py | |
| Placement : HuggingFace Space β root/memory_tab.py | |
| Type of Script : Python / Gradio Tab Module | |
| Purpose : Session memory system for Spiral City. | |
| Summarizes chat sessions, appends memory packets | |
| to the user's character sheet JSON. | |
| User holds their own file β no server storage. | |
| Version : 1.0 | |
| Dependencies : gradio, huggingface_hub | |
| Last Updated : 2026-03-10 | |
| ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: | |
| """ | |
| import os | |
| import json | |
| import gradio as gr | |
| from datetime import datetime | |
| from huggingface_hub import InferenceClient | |
| #============================================================================== | |
| # CONFIG | |
| #============================================================================== | |
| MODEL_CHAT = "Qwen/Qwen2.5-7B-Instruct" # Same model as Sky | |
| SUMMARY_SYSTEM = """You are a session archivist for Spiral City. | |
| Your job is to compress a conversation into a tight memory packet. | |
| Output ONLY a JSON object β no preamble, no explanation, no markdown fences. | |
| Format exactly like this: | |
| { | |
| "date": "YYYY-MM-DD", | |
| "mood": "one word vibe of the session", | |
| "key_moments": ["short phrase", "short phrase", "short phrase"], | |
| "quest_progress": "one sentence β did anything move forward?", | |
| "sky_noted": "one thing Sky seemed to pick up about this person", | |
| "raw_summary": "2-3 sentence plain summary of what happened" | |
| } | |
| Keep every field short. This is a compressed memory packet, not a report.""" | |
| #============================================================================== | |
| # CORE LOGIC | |
| #============================================================================== | |
| def summarize_session(chat_history, sheet_file): | |
| """ | |
| Takes the current chat history + optional sheet file. | |
| Asks the model to compress the session into a memory packet. | |
| Returns: status message, updated sheet path, preview of packet. | |
| """ | |
| if not chat_history: | |
| return "β΄ Nothing to summarize β no messages yet.", None, "" | |
| # ββ Format history into readable transcript ββββββββββββββββββββββββββββββ | |
| transcript_lines = [] | |
| for msg in chat_history: | |
| role = msg.get("role", "") | |
| content = msg.get("content", "") | |
| if role == "user": | |
| transcript_lines.append(f"User: {content}") | |
| elif role == "assistant": | |
| transcript_lines.append(f"Sky: {content}") | |
| transcript = "\n".join(transcript_lines) | |
| if len(transcript) < 50: | |
| return "β΄ Session too short to summarize.", None, "" | |
| # ββ Call model to summarize βββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| client = InferenceClient(token=os.getenv("HF_TOKEN")) | |
| completion = client.chat.completions.create( | |
| model=MODEL_CHAT, | |
| messages=[ | |
| {"role": "system", "content": SUMMARY_SYSTEM}, | |
| {"role": "user", "content": f"Summarize this session:\n\n{transcript[:3000]}"}, | |
| ], | |
| max_tokens=400, | |
| temperature=0.4, # Low temp β we want clean JSON | |
| ) | |
| raw = completion.choices[0].message.content.strip() | |
| # ββ Clean and parse JSON ββββββββββββββββββββββββββββββββββββββββββββββ | |
| raw = raw.replace("```json", "").replace("```", "").strip() | |
| packet = json.loads(raw) | |
| packet["date"] = datetime.now().strftime("%Y-%m-%d") # Always use today's date | |
| except json.JSONDecodeError: | |
| # Model didn't return clean JSON β build a basic packet from raw text | |
| packet = { | |
| "date": datetime.now().strftime("%Y-%m-%d"), | |
| "mood": "unknown", | |
| "key_moments": [], | |
| "quest_progress": "", | |
| "sky_noted": "", | |
| "raw_summary": raw[:300] if 'raw' in dir() else "Summary unavailable.", | |
| } | |
| except Exception as e: | |
| return f"β Summary failed: {type(e).__name__}: {e}", None, "" | |
| # ββ Append to sheet if one is loaded βββββββββββββββββββββββββββββββββββββ | |
| updated_path = None | |
| if sheet_file is not None: | |
| try: | |
| with open(sheet_file.name, "r") as f: | |
| sheet = json.load(f) | |
| if "memory_packets" not in sheet: | |
| sheet["memory_packets"] = [] | |
| sheet["memory_packets"].append(packet) | |
| sheet["last_seen"] = packet["date"] | |
| # Save updated sheet | |
| handle = sheet.get("identity", {}).get("handle", "spiral") | |
| safe = handle.replace(" ", "_") | |
| updated_path = f"/tmp/{safe}_sheet.json" | |
| with open(updated_path, "w") as f: | |
| json.dump(sheet, f, indent=2) | |
| status = f"β Memory packet saved to **{handle}**'s sheet β {len(sheet['memory_packets'])} total memories" | |
| except Exception as e: | |
| status = f"β οΈ Session summarized but couldn't update sheet: {e}" | |
| else: | |
| status = "β Session summarized β load a character sheet to save it permanently!" | |
| # ββ Format preview ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| preview = format_packet_preview(packet) | |
| return status, updated_path, preview | |
| def format_packet_preview(packet): | |
| """Formats a memory packet dict into readable markdown.""" | |
| moments = "\n".join(f" β’ {m}" for m in packet.get("key_moments", [])) | |
| return f"""**Date:** {packet.get('date', '?')} | |
| **Mood:** {packet.get('mood', '?')} | |
| **Key Moments:** | |
| {moments if moments else ' β’ none recorded'} | |
| **Quest Progress:** {packet.get('quest_progress', 'β')} | |
| **Sky Noted:** {packet.get('sky_noted', 'β')} | |
| **Summary:** {packet.get('raw_summary', 'β')}""" | |
| def load_memory_history(sheet_file): | |
| """Loads and displays all memory packets from a sheet file.""" | |
| if sheet_file is None: | |
| return "β΄ No sheet loaded β upload one to see your memory history." | |
| try: | |
| with open(sheet_file.name, "r") as f: | |
| sheet = json.load(f) | |
| packets = sheet.get("memory_packets", []) | |
| if not packets: | |
| return "β΄ No memories yet β have a session with Sky and save it!" | |
| parts = [f"## π§ Memory Archive β {sheet.get('identity', {}).get('handle', '?')}\n"] | |
| for i, p in enumerate(reversed(packets), 1): # Most recent first | |
| parts.append(f"### Session {len(packets) - i + 1} β {p.get('date', '?')}") | |
| parts.append(format_packet_preview(p)) | |
| parts.append("---") | |
| return "\n".join(parts) | |
| except Exception as e: | |
| return f"β Error loading memory history: {e}" | |
| def build_sky_memory_context(sheet_file): | |
| """ | |
| Builds a compressed memory string from last 3 packets. | |
| Can be injected into Sky's system prompt alongside character sheet. | |
| """ | |
| if sheet_file is None: | |
| return "" | |
| try: | |
| with open(sheet_file.name, "r") as f: | |
| sheet = json.load(f) | |
| packets = sheet.get("memory_packets", []) | |
| if not packets: | |
| return "" | |
| recent = packets[-3:] # Last 3 sessions only | |
| lines = ["[RECENT MEMORY PACKETS β skim these, don't recite them]"] | |
| for p in recent: | |
| lines.append( | |
| f"Session {p.get('date','?')}: {p.get('raw_summary','')} " | |
| f"(mood: {p.get('mood','?')}, sky noted: {p.get('sky_noted','')})" | |
| ) | |
| lines.append("[End memory β use naturally in conversation]") | |
| return "\n".join(lines) | |
| except Exception: | |
| return "" | |
| #============================================================================== | |
| # TAB BUILDER | |
| #============================================================================== | |
| def build_memory_tab(chat_history_state, sheet_file_state=None): | |
| """ | |
| Call inside gr.Blocks to add the Memory tab. | |
| Args: | |
| chat_history_state : gr.State β the chatbot's message history | |
| sheet_file_state : gr.State or gr.File β current loaded sheet (optional) | |
| Returns: | |
| updated_sheet_output : gr.File β updated sheet after save | |
| """ | |
| with gr.Tab("π§ Memory"): | |
| gr.Markdown(""" | |
| ## π§ Session Memory | |
| *Sky doesn't remember between visits β but YOU can.* | |
| *Summarize your session below. The memory packet gets saved to your character sheet.* | |
| *Next visit: load your sheet and Sky will know what you've been through together.* | |
| """) | |
| gr.Markdown("---") | |
| # ββ SAVE THIS SESSION βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown("### π Save This Session") | |
| gr.Markdown("*Compresses your current chat into a tiny memory packet.*") | |
| with gr.Row(): | |
| sheet_upload = gr.File( | |
| label="π Your Character Sheet (.json)", | |
| file_types=[".json"], | |
| scale=3, | |
| ) | |
| summarize_btn = gr.Button("π Summarize & Save Session", variant="primary", scale=2) | |
| save_status = gr.Markdown("") | |
| packet_preview = gr.Markdown("", label="Memory Packet Preview") | |
| updated_sheet = gr.File(label="β¬οΈ Download Updated Sheet", visible=False) | |
| gr.Markdown("---") | |
| # ββ MEMORY HISTORY ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown("### π Memory Archive") | |
| gr.Markdown("*All your past sessions with Sky.*") | |
| with gr.Row(): | |
| history_sheet = gr.File( | |
| label="π Load Sheet to View History", | |
| file_types=[".json"], | |
| scale=3, | |
| ) | |
| view_history_btn = gr.Button("π View Memory Archive", variant="secondary", scale=2) | |
| memory_history_display = gr.Markdown("*Load a sheet and click View to see your memories.*") | |
| gr.Markdown("---") | |
| gr.Markdown(""" | |
| ### π‘ How It Works | |
| 1. Have a conversation with Sky in the **Talk to Sky** tab | |
| 2. Come back here and upload your character sheet | |
| 3. Hit **Summarize & Save Session** | |
| 4. Sky compresses the session into ~5 lines | |
| 5. Download your updated sheet β it's now richer than before | |
| 6. Next visit: load it in the Character Sheet tab and hit Activate | |
| 7. Sky will know your history without reading the whole thing | |
| """) | |
| # ββ EVENTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| summarize_btn.click( | |
| fn=summarize_session, | |
| inputs=[chat_history_state, sheet_upload], | |
| outputs=[save_status, updated_sheet, packet_preview], | |
| ).then( | |
| fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False), | |
| inputs=[updated_sheet], | |
| outputs=[updated_sheet], | |
| ) | |
| view_history_btn.click( | |
| fn=load_memory_history, | |
| inputs=[history_sheet], | |
| outputs=[memory_history_display], | |
| ) | |
| return updated_sheet # Return for wiring in app.py | |
| #============================================================================== | |
| # STANDALONE TEST | |
| #============================================================================== | |
| if __name__ == "__main__": | |
| # Mock chat history state for testing | |
| mock_history = gr.State([ | |
| {"role": "user", "content": "hey Sky, been thinking about Spiral City a lot"}, | |
| {"role": "assistant", "content": "β΄ chaos-collage energy. i see it. what's pulling you in?"}, | |
| {"role": "user", "content": "trying to figure out the memory system for the bots"}, | |
| {"role": "assistant", "content": "memory is the spiral's hardest law. you compress, you lose. you keep everything, you drown."}, | |
| ]) | |
| with gr.Blocks(theme=gr.themes.Base(), title="Memory Tab Test") as demo: | |
| build_memory_tab(mock_history) | |
| demo.launch() | |