-
Notifications
You must be signed in to change notification settings - Fork 289
Expand file tree
/
Copy pathmain.py
More file actions
305 lines (248 loc) · 12.6 KB
/
main.py
File metadata and controls
305 lines (248 loc) · 12.6 KB
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
299
300
301
302
303
304
305
from typing import ClassVar, Set
import json
import os
import time
from datetime import datetime
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker
class VoiceMemoryCapture(MatchingCapability):
worker: AgentWorker = None
capability_worker: CapabilityWorker = None
#{{register_capability}}
FILE_NAME: ClassVar[str] = "voice_memory_entries.json"
MAX_ENTRIES: ClassVar[int] = 100
EXIT_WORDS: ClassVar[Set[str]] = {"stop", "exit", "quit", "done", "cancel", "bye", "goodbye", "never mind"}
def call(self, worker: AgentWorker):
self.worker = worker
self.capability_worker = CapabilityWorker(self.worker)
self.worker.session_tasks.create(self.run())
async def run(self):
"""Main logic when the ability is triggered."""
try:
# Safely get trigger text from history
history = self.worker.agent_memory.full_message_history
trigger_text = ""
if history:
last_msg = history[-1]
if isinstance(last_msg, dict):
trigger_text = last_msg.get("content", "")
elif hasattr(last_msg, "content"):
trigger_text = last_msg.content
else:
trigger_text = str(last_msg)
lower_text = trigger_text.lower().strip()
# Keyword-based mode detection
mode = "save"
content = trigger_text
query = trigger_text
if any(kw in lower_text for kw in ["forget", "delete", "remove", "erase", "clear", "get rid of"]):
mode = "delete"
query = trigger_text
elif any(kw in lower_text for kw in [
"list", "lists", "list my", "list all", "list everything", "list the things", "list of",
"show all", "show my", "recap", "recap my", "recap of", "summarize", "summarize my",
"summary", "give me a summary", "quick summary", "full summary", "memory recap",
"how many memories", "count my", "how many do I have", "what memories do I have",
"overview of", "tell me about my memories", "my memories recap"
]):
mode = "list"
elif any(kw in lower_text for kw in [
"what did I save", "what do I have", "do I have anything", "what did I remember",
"find my note", "search my memories", "what do I have on"
]):
mode = "recall"
query = trigger_text
if mode == "save":
await self.handle_save(content)
elif mode == "recall":
await self.handle_recall(query)
elif mode == "delete":
await self.handle_delete(query)
elif mode == "list":
await self.handle_list()
else:
await self.capability_worker.speak("Not sure what you want. Try 'remember that...' to save or 'what did I save about...' to recall.")
except Exception as e:
await self.capability_worker.speak("Something went wrong with memory. Try again?")
if hasattr(self.worker, "editor_logging_handler"):
self.worker.editor_logging_handler.warning(f"Memory error: {str(e)}")
finally:
self.capability_worker.resume_normal_flow()
async def handle_save(self, content: str):
if not content.strip():
await self.capability_worker.speak("What would you like me to remember?")
content = await self.capability_worker.user_response()
if not content.strip():
await self.capability_worker.speak("Nothing to save. Exiting.")
return
await self.capability_worker.speak("One sec... saving.")
classify_prompt = """You are a memory classifier. Extract the core fact from the user's voice input. Return ONLY valid JSON, no markdown fences.
{
"summary": "clean one-sentence summary of what to remember",
"category": "idea | reminder | person | place | thing | note",
"keywords": ["keyword1", "keyword2", "keyword3"]
}
Examples:
Input: 'remember that sarahs birthday is june 12th'
Output: {"summary": "Sarah's birthday is June 12th", "category": "person", "keywords": ["sarah", "birthday", "june"]}
Input: 'dont let me forget we need more dog food'
Output: {"summary": "Need to buy more dog food", "category": "reminder", "keywords": ["dog food", "buy", "groceries"]}"""
raw = self.capability_worker.text_to_text_response(classify_prompt)
raw = raw.replace("```json", "").replace("```", "").strip()
try:
parsed = json.loads(raw)
except Exception:
parsed = {"summary": content, "category": "note", "keywords": []}
entry = {
"id": str(int(time.time())),
"timestamp": datetime.now().isoformat(),
"raw_input": content,
"summary": parsed["summary"],
"category": parsed["category"],
"keywords": parsed["keywords"]
}
memories = []
if await self.capability_worker.check_if_file_exists(self.FILE_NAME, temp=False):
raw_file = await self.capability_worker.read_file(self.FILE_NAME, temp=False)
memories = json.loads(raw_file)
if len(memories) >= self.MAX_ENTRIES:
await self.capability_worker.speak(
f"Your memory is full at {self.MAX_ENTRIES} entries. "
"Want me to read the oldest ones so you can decide what to remove?"
)
return
memories.append(entry)
await self.capability_worker.delete_file(self.FILE_NAME, temp=False)
await self.capability_worker.write_file(self.FILE_NAME, json.dumps(memories), temp=False)
await self.capability_worker.speak(f"Got it. I saved: {parsed['summary']}")
await self.capability_worker.speak("Anything else to save?")
more = await self.capability_worker.user_response()
if more.strip() and not any(w in more.lower() for w in self.EXIT_WORDS):
await self.handle_save(more)
async def handle_recall(self, query: str):
if not query.strip():
await self.capability_worker.speak("What topic are you looking for?")
query = await self.capability_worker.user_response()
if not query.strip():
await self.capability_worker.speak("No topic given. Exiting.")
return
await self.capability_worker.speak("One sec... checking memories.")
memories = []
if await self.capability_worker.check_if_file_exists(self.FILE_NAME, temp=False):
raw = await self.capability_worker.read_file(self.FILE_NAME, temp=False)
memories = json.loads(raw)
else:
await self.capability_worker.speak("You don't have any saved memories yet. Say 'remember that' to start saving.")
return
if not memories:
await self.capability_worker.speak("No memories saved yet.")
return
current_time = datetime.now()
enriched_memories = []
for m in memories:
try:
ts = datetime.fromisoformat(m["timestamp"])
days_ago = (current_time - ts).days
m["days_ago"] = days_ago
except Exception:
m["days_ago"] = 0
enriched_memories.append(m)
recall_prompt = f"""You are a memory retrieval assistant. The user has saved memories over time.
Given their query and the list of saved memories, return the top 3 most relevant matches as JSON.
Return ONLY valid JSON, no markdown fences:
[
{{"id": "...", "summary": "...", "days_ago": number}},
...
]
If nothing matches, return an empty array: []
MEMORIES: {json.dumps(enriched_memories)}
QUERY: {query}"""
raw = self.capability_worker.text_to_text_response(recall_prompt)
raw = raw.replace("```json", "").replace("```", "").strip()
try:
matches = json.loads(raw)
except Exception:
matches = []
if not matches:
await self.capability_worker.speak("I didn't find anything about that. Want to try a different search?")
else:
response = "I found these: "
for m in matches[:3]:
days = m.get("days_ago", 0)
day_str = f"{days} day{'s' if days != 1 else ''} ago" if days >= 0 else "recently"
response += f"{day_str} you saved: {m['summary']}. "
await self.capability_worker.speak(response)
await self.capability_worker.speak("Want to search for something else?")
more = await self.capability_worker.user_response()
if more.strip() and not any(w in more.lower() for w in self.EXIT_WORDS):
await self.handle_recall(more)
async def handle_delete(self, query: str):
await self.capability_worker.speak("One sec... looking for that memory.")
memories = []
if await self.capability_worker.check_if_file_exists(self.FILE_NAME, temp=False):
raw = await self.capability_worker.read_file(self.FILE_NAME, temp=False)
memories = json.loads(raw)
else:
await self.capability_worker.speak("No memories saved yet. Nothing to delete.")
return
if not memories:
await self.capability_worker.speak("No memories saved yet.")
return
delete_prompt = f"""You are a memory deletion assistant.
From the saved memories, find the entry that best matches the user's delete request.
Return ONLY the ID of the entry to delete (as a string), or "none" if no match.
MEMORIES: {json.dumps(memories)}
DELETE REQUEST: {query}"""
raw = self.capability_worker.text_to_text_response(delete_prompt)
raw = raw.replace("```json", "").replace("```", "").strip()
target_id = raw.strip()
if target_id == "none" or not target_id or target_id not in [e["id"] for e in memories]:
await self.capability_worker.speak("I couldn't find a matching memory to delete. Try describing it exactly (e.g. 'delete my wife's birthday').")
return
entry = next((e for e in memories if e["id"] == target_id), None)
if not entry:
await self.capability_worker.speak("Couldn't find that memory.")
return
await self.capability_worker.speak(f"Delete '{entry['summary']}'? Say yes to confirm.")
confirm = await self.capability_worker.user_response()
if "yes" in confirm.lower():
memories = [e for e in memories if e["id"] != target_id]
await self.capability_worker.delete_file(self.FILE_NAME, temp=False)
await self.capability_worker.write_file(self.FILE_NAME, json.dumps(memories), temp=False)
await self.capability_worker.speak(f"Deleted '{entry['summary']}'. Gone now.")
else:
await self.capability_worker.speak("Delete cancelled.")
async def handle_list(self):
await self.capability_worker.speak("One sec... counting your memories.")
memories = []
if await self.capability_worker.check_if_file_exists(self.FILE_NAME, temp=False):
raw = await self.capability_worker.read_file(self.FILE_NAME, temp=False)
memories = json.loads(raw)
else:
await self.capability_worker.speak("You don't have any saved memories yet.")
return
if not memories:
await self.capability_worker.speak("No memories saved yet.")
return
count = len(memories)
categories = {}
for m in memories:
cat = m.get("category", "unknown")
categories[cat] = categories.get(cat, 0) + 1
summary = f"You have {count} saved memories. "
if categories:
parts = []
for cat, num in sorted(categories.items(), key=lambda x: x[1], reverse=True):
parts.append(f"{num} {cat}{'s' if num != 1 else ''}")
summary += ", ".join(parts) + ". "
else:
summary += "No categories yet. "
summary += "Want me to go through them?"
await self.capability_worker.speak(summary)
await self.capability_worker.speak("Say 'yes' or a category (like 'reminders' or 'people') to hear more, or 'stop' to exit.")
more = await self.capability_worker.user_response()
if more.strip() and not any(w in more.lower() for w in self.EXIT_WORDS):
await self.capability_worker.speak("Going deeper coming soon. For now, try specific recall like 'what did I save about [topic]'.")
else:
await self.capability_worker.speak("Okay, done listing.")