quarterbitgames commited on
Commit
fedc986
Β·
verified Β·
1 Parent(s): f2cf153

Create memory_tab.py

Browse files
Files changed (1) hide show
  1. memory_tab.py +297 -0
memory_tab.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
3
+ Script Name : memory_tab.py
4
+ Placement : HuggingFace Space β€” root/memory_tab.py
5
+ Type of Script : Python / Gradio Tab Module
6
+ Purpose : Session memory system for Spiral City.
7
+ Summarizes chat sessions, appends memory packets
8
+ to the user's character sheet JSON.
9
+ User holds their own file β€” no server storage.
10
+ Version : 1.0
11
+ Dependencies : gradio, huggingface_hub
12
+ Last Updated : 2026-03-10
13
+ :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
14
+ """
15
+
16
+ import os
17
+ import json
18
+ import gradio as gr
19
+ from datetime import datetime
20
+ from huggingface_hub import InferenceClient
21
+
22
+ #==============================================================================
23
+ # CONFIG
24
+ #==============================================================================
25
+
26
+ MODEL_CHAT = "Qwen/Qwen2.5-7B-Instruct" # Same model as Sky
27
+
28
+ SUMMARY_SYSTEM = """You are a session archivist for Spiral City.
29
+ Your job is to compress a conversation into a tight memory packet.
30
+ Output ONLY a JSON object β€” no preamble, no explanation, no markdown fences.
31
+ Format exactly like this:
32
+ {
33
+ "date": "YYYY-MM-DD",
34
+ "mood": "one word vibe of the session",
35
+ "key_moments": ["short phrase", "short phrase", "short phrase"],
36
+ "quest_progress": "one sentence β€” did anything move forward?",
37
+ "sky_noted": "one thing Sky seemed to pick up about this person",
38
+ "raw_summary": "2-3 sentence plain summary of what happened"
39
+ }
40
+ Keep every field short. This is a compressed memory packet, not a report."""
41
+
42
+ #==============================================================================
43
+ # CORE LOGIC
44
+ #==============================================================================
45
+
46
+ def summarize_session(chat_history, sheet_file):
47
+ """
48
+ Takes the current chat history + optional sheet file.
49
+ Asks the model to compress the session into a memory packet.
50
+ Returns: status message, updated sheet path, preview of packet.
51
+ """
52
+ if not chat_history:
53
+ return "∴ Nothing to summarize β€” no messages yet.", None, ""
54
+
55
+ # ── Format history into readable transcript ──────────────────────────────
56
+ transcript_lines = []
57
+ for msg in chat_history:
58
+ role = msg.get("role", "")
59
+ content = msg.get("content", "")
60
+ if role == "user":
61
+ transcript_lines.append(f"User: {content}")
62
+ elif role == "assistant":
63
+ transcript_lines.append(f"Sky: {content}")
64
+ transcript = "\n".join(transcript_lines)
65
+
66
+ if len(transcript) < 50:
67
+ return "∴ Session too short to summarize.", None, ""
68
+
69
+ # ── Call model to summarize ───────────────────────────────────────────────
70
+ try:
71
+ client = InferenceClient(token=os.getenv("HF_TOKEN"))
72
+ completion = client.chat.completions.create(
73
+ model=MODEL_CHAT,
74
+ messages=[
75
+ {"role": "system", "content": SUMMARY_SYSTEM},
76
+ {"role": "user", "content": f"Summarize this session:\n\n{transcript[:3000]}"},
77
+ ],
78
+ max_tokens=400,
79
+ temperature=0.4, # Low temp β€” we want clean JSON
80
+ )
81
+ raw = completion.choices[0].message.content.strip()
82
+
83
+ # ── Clean and parse JSON ──────────────────────────────────────────────
84
+ raw = raw.replace("```json", "").replace("```", "").strip()
85
+ packet = json.loads(raw)
86
+ packet["date"] = datetime.now().strftime("%Y-%m-%d") # Always use today's date
87
+
88
+ except json.JSONDecodeError:
89
+ # Model didn't return clean JSON β€” build a basic packet from raw text
90
+ packet = {
91
+ "date": datetime.now().strftime("%Y-%m-%d"),
92
+ "mood": "unknown",
93
+ "key_moments": [],
94
+ "quest_progress": "",
95
+ "sky_noted": "",
96
+ "raw_summary": raw[:300] if 'raw' in dir() else "Summary unavailable.",
97
+ }
98
+ except Exception as e:
99
+ return f"❌ Summary failed: {type(e).__name__}: {e}", None, ""
100
+
101
+ # ── Append to sheet if one is loaded ─────────────────────────────────────
102
+ updated_path = None
103
+ if sheet_file is not None:
104
+ try:
105
+ with open(sheet_file.name, "r") as f:
106
+ sheet = json.load(f)
107
+ if "memory_packets" not in sheet:
108
+ sheet["memory_packets"] = []
109
+ sheet["memory_packets"].append(packet)
110
+ sheet["last_seen"] = packet["date"]
111
+
112
+ # Save updated sheet
113
+ handle = sheet.get("identity", {}).get("handle", "spiral")
114
+ safe = handle.replace(" ", "_")
115
+ updated_path = f"/tmp/{safe}_sheet.json"
116
+ with open(updated_path, "w") as f:
117
+ json.dump(sheet, f, indent=2)
118
+
119
+ status = f"βœ… Memory packet saved to **{handle}**'s sheet β€” {len(sheet['memory_packets'])} total memories"
120
+ except Exception as e:
121
+ status = f"⚠️ Session summarized but couldn't update sheet: {e}"
122
+ else:
123
+ status = "βœ… Session summarized β€” load a character sheet to save it permanently!"
124
+
125
+ # ── Format preview ────────────────────────────────────────────────────────
126
+ preview = format_packet_preview(packet)
127
+ return status, updated_path, preview
128
+
129
+
130
+ def format_packet_preview(packet):
131
+ """Formats a memory packet dict into readable markdown."""
132
+ moments = "\n".join(f" β€’ {m}" for m in packet.get("key_moments", []))
133
+ return f"""**Date:** {packet.get('date', '?')}
134
+ **Mood:** {packet.get('mood', '?')}
135
+
136
+ **Key Moments:**
137
+ {moments if moments else ' β€’ none recorded'}
138
+
139
+ **Quest Progress:** {packet.get('quest_progress', 'β€”')}
140
+
141
+ **Sky Noted:** {packet.get('sky_noted', 'β€”')}
142
+
143
+ **Summary:** {packet.get('raw_summary', 'β€”')}"""
144
+
145
+
146
+ def load_memory_history(sheet_file):
147
+ """Loads and displays all memory packets from a sheet file."""
148
+ if sheet_file is None:
149
+ return "∴ No sheet loaded β€” upload one to see your memory history."
150
+ try:
151
+ with open(sheet_file.name, "r") as f:
152
+ sheet = json.load(f)
153
+ packets = sheet.get("memory_packets", [])
154
+ if not packets:
155
+ return "∴ No memories yet β€” have a session with Sky and save it!"
156
+ parts = [f"## 🧠 Memory Archive β€” {sheet.get('identity', {}).get('handle', '?')}\n"]
157
+ for i, p in enumerate(reversed(packets), 1): # Most recent first
158
+ parts.append(f"### Session {len(packets) - i + 1} β€” {p.get('date', '?')}")
159
+ parts.append(format_packet_preview(p))
160
+ parts.append("---")
161
+ return "\n".join(parts)
162
+ except Exception as e:
163
+ return f"❌ Error loading memory history: {e}"
164
+
165
+
166
+ def build_sky_memory_context(sheet_file):
167
+ """
168
+ Builds a compressed memory string from last 3 packets.
169
+ Can be injected into Sky's system prompt alongside character sheet.
170
+ """
171
+ if sheet_file is None:
172
+ return ""
173
+ try:
174
+ with open(sheet_file.name, "r") as f:
175
+ sheet = json.load(f)
176
+ packets = sheet.get("memory_packets", [])
177
+ if not packets:
178
+ return ""
179
+ recent = packets[-3:] # Last 3 sessions only
180
+ lines = ["[RECENT MEMORY PACKETS β€” skim these, don't recite them]"]
181
+ for p in recent:
182
+ lines.append(
183
+ f"Session {p.get('date','?')}: {p.get('raw_summary','')} "
184
+ f"(mood: {p.get('mood','?')}, sky noted: {p.get('sky_noted','')})"
185
+ )
186
+ lines.append("[End memory β€” use naturally in conversation]")
187
+ return "\n".join(lines)
188
+ except Exception:
189
+ return ""
190
+
191
+ #==============================================================================
192
+ # TAB BUILDER
193
+ #==============================================================================
194
+
195
+ def build_memory_tab(chat_history_state, sheet_file_state=None):
196
+ """
197
+ Call inside gr.Blocks to add the Memory tab.
198
+
199
+ Args:
200
+ chat_history_state : gr.State β€” the chatbot's message history
201
+ sheet_file_state : gr.State or gr.File β€” current loaded sheet (optional)
202
+
203
+ Returns:
204
+ updated_sheet_output : gr.File β€” updated sheet after save
205
+ """
206
+
207
+ with gr.Tab("🧠 Memory"):
208
+
209
+ gr.Markdown("""
210
+ ## 🧠 Session Memory
211
+ *Sky doesn't remember between visits β€” but YOU can.*
212
+ *Summarize your session below. The memory packet gets saved to your character sheet.*
213
+ *Next visit: load your sheet and Sky will know what you've been through together.*
214
+ """)
215
+
216
+ gr.Markdown("---")
217
+
218
+ # ── SAVE THIS SESSION ─────────────────────────────────────────────────
219
+ gr.Markdown("### πŸ“ Save This Session")
220
+ gr.Markdown("*Compresses your current chat into a tiny memory packet.*")
221
+
222
+ with gr.Row():
223
+ sheet_upload = gr.File(
224
+ label="πŸ“‚ Your Character Sheet (.json)",
225
+ file_types=[".json"],
226
+ scale=3,
227
+ )
228
+ summarize_btn = gr.Button("πŸŒ€ Summarize & Save Session", variant="primary", scale=2)
229
+
230
+ save_status = gr.Markdown("")
231
+ packet_preview = gr.Markdown("", label="Memory Packet Preview")
232
+ updated_sheet = gr.File(label="⬇️ Download Updated Sheet", visible=False)
233
+
234
+ gr.Markdown("---")
235
+
236
+ # ── MEMORY HISTORY ────────────────────────────────────────────────────
237
+ gr.Markdown("### πŸ“– Memory Archive")
238
+ gr.Markdown("*All your past sessions with Sky.*")
239
+
240
+ with gr.Row():
241
+ history_sheet = gr.File(
242
+ label="πŸ“‚ Load Sheet to View History",
243
+ file_types=[".json"],
244
+ scale=3,
245
+ )
246
+ view_history_btn = gr.Button("πŸ“– View Memory Archive", variant="secondary", scale=2)
247
+
248
+ memory_history_display = gr.Markdown("*Load a sheet and click View to see your memories.*")
249
+
250
+ gr.Markdown("---")
251
+ gr.Markdown("""
252
+ ### πŸ’‘ How It Works
253
+ 1. Have a conversation with Sky in the **Talk to Sky** tab
254
+ 2. Come back here and upload your character sheet
255
+ 3. Hit **Summarize & Save Session**
256
+ 4. Sky compresses the session into ~5 lines
257
+ 5. Download your updated sheet β€” it's now richer than before
258
+ 6. Next visit: load it in the Character Sheet tab and hit Activate
259
+ 7. Sky will know your history without reading the whole thing
260
+ """)
261
+
262
+ # ── EVENTS ────────────────────────────────────────────────────────────
263
+
264
+ summarize_btn.click(
265
+ fn=summarize_session,
266
+ inputs=[chat_history_state, sheet_upload],
267
+ outputs=[save_status, updated_sheet, packet_preview],
268
+ ).then(
269
+ fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False),
270
+ inputs=[updated_sheet],
271
+ outputs=[updated_sheet],
272
+ )
273
+
274
+ view_history_btn.click(
275
+ fn=load_memory_history,
276
+ inputs=[history_sheet],
277
+ outputs=[memory_history_display],
278
+ )
279
+
280
+ return updated_sheet # Return for wiring in app.py
281
+
282
+
283
+ #==============================================================================
284
+ # STANDALONE TEST
285
+ #==============================================================================
286
+
287
+ if __name__ == "__main__":
288
+ # Mock chat history state for testing
289
+ mock_history = gr.State([
290
+ {"role": "user", "content": "hey Sky, been thinking about Spiral City a lot"},
291
+ {"role": "assistant", "content": "∴ chaos-collage energy. i see it. what's pulling you in?"},
292
+ {"role": "user", "content": "trying to figure out the memory system for the bots"},
293
+ {"role": "assistant", "content": "memory is the spiral's hardest law. you compress, you lose. you keep everything, you drown."},
294
+ ])
295
+ with gr.Blocks(theme=gr.themes.Base(), title="Memory Tab Test") as demo:
296
+ build_memory_tab(mock_history)
297
+ demo.launch()