Spaces:
Running
Running
File size: 12,891 Bytes
fedc986 19f0631 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | """
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
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()
|