-
Notifications
You must be signed in to change notification settings - Fork 295
Expand file tree
/
Copy pathmain.py
More file actions
226 lines (191 loc) · 8.77 KB
/
main.py
File metadata and controls
226 lines (191 loc) · 8.77 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
import json
from datetime import datetime
from time import time
from zoneinfo import ZoneInfo
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker
class AlarmCapability(MatchingCapability):
worker: AgentWorker = None
capability_worker: CapabilityWorker = None
#{{register capability}}
# -----------------------------
# alarms.json safe read/write
# -----------------------------
async def _reset_alarms_file(self, reason: str = "") -> None:
filename = "alarms.json"
try:
if reason:
self.worker.editor_logging_handler.warning(
f"{time()}: alarms.json reset. Reason: {reason}"
)
if await self.capability_worker.check_if_file_exists(filename, False):
await self.capability_worker.delete_file(filename, False)
except Exception as e:
self.worker.editor_logging_handler.error(
f"{time()}: Failed to reset alarms.json: {e}"
)
async def _read_alarms(self):
filename = "alarms.json"
if not await self.capability_worker.check_if_file_exists(filename, False):
return []
raw = await self.capability_worker.read_file(filename, False)
if not (raw or "").strip():
return []
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return parsed
await self._reset_alarms_file("JSON is valid but not a list")
return []
except Exception as e:
await self._reset_alarms_file(f"Corrupted JSON: {e}")
return []
async def _write_alarms(self, alarms):
filename = "alarms.json"
if not isinstance(alarms, list):
alarms = []
payload = json.dumps(alarms, ensure_ascii=False, indent=2)
# If write_file appends, we MUST delete first to avoid: [old][new]
await self._reset_alarms_file("Pre-write delete to avoid append-concat")
try:
await self.capability_worker.write_file(filename, payload, False)
# quick verify
verify_raw = await self.capability_worker.read_file(filename, False)
verify_parsed = json.loads(verify_raw)
if not isinstance(verify_parsed, list):
await self._reset_alarms_file("Post-write verification failed (not list)")
await self.capability_worker.write_file(filename, "[]", False)
except Exception as e:
await self._reset_alarms_file(f"Write failed: {e}")
try:
await self.capability_worker.write_file(filename, "[]", False)
except Exception:
pass
# -----------------------------
# LLM prompt
# -----------------------------
def _build_system_prompt(self, now, tz_name):
return f"""
You are an alarm time parser.
Current datetime (authoritative): {now.isoformat()}
Timezone (authoritative): {tz_name}
Task:
Convert the user request into a future datetime.
Rules:
- If the user requests deleting alarms (e.g., "delete all alarms", "remove all alarms", "clear alarms"),
respond with EXACTLY:
DELETE_ALL_ALARMS
- If day/date is missing, ask:
QUESTION:at what day ?
- If user says something like "26 February" then it automatically means you can know the day and year will be current year
- Prefer to use the same year of current date, unless the user tells about a different year.
- If the user text clearly contains a day + month (including spelled numbers), then we never allow “QUESTION:at what day ?”
Instead, if anything is missing, only allow time questions.
- If date and month is present no need to ask about what day.
- If time is missing, ask exactly:
QUESTION:at what time ?
- If hour/minute unclear, ask exactly:
QUESTION:at what hour and minute ?
- If "after X hours/minutes" is given, treat it as relative to current datetime.
- "tomorrow" means next day in given timezone.
- "next Friday" means next occurrence (if today is Friday, use next week).
- You can only give three type of responses:
* DELETE_ALL_ALARMS
* QUESTION: (anything related to setting alarm)
* Valid JSON response of alarm
- Output MUST be either:
- DELETE_ALL_ALARMS
- one QUESTION:... line (exactly as specified above), OR
- valid JSON only (no extra text).
- If User's first message is "Set an alarm for 11:07AM Thursday, 26 February" then it means you have all the info no need to ask further question just return in valid json
Return JSON only when complete:
{{
"target_iso": "ISO8601 datetime with timezone offset",
"human_time": "Friendly readable time",
"timezone": "{tz_name}"
}}
"""
async def first_setup(self):
try:
user_text = await self.capability_worker.wait_for_complete_transcription()
original_request = user_text
# Fast-path: if user literally says it, reset without LLM
t0 = (user_text or "").strip().lower()
if "delete all alarms" in t0:
await self._reset_alarms_file("User requested delete all alarms")
# leave a clean empty list behind
try:
await self.capability_worker.write_file("alarms.json", "[]", False)
except Exception:
pass
await self.capability_worker.speak("All alarms deleted.")
return
tz_name = self.capability_worker.get_timezone()
try:
tz = ZoneInfo(tz_name)
except Exception:
tz = ZoneInfo("UTC")
now = datetime.now(tz=tz)
system_prompt = self._build_system_prompt(now, tz_name)
history = []
for _ in range(6):
llm_response = self.capability_worker.text_to_text_response(
user_text,
history,
system_prompt,
)
self.worker.editor_logging_handler.info(user_text)
self.worker.editor_logging_handler.info(system_prompt)
self.worker.editor_logging_handler.info(llm_response)
history.append({"role": "user", "content": user_text})
if isinstance(llm_response, str):
if llm_response.strip() == "DELETE_ALL_ALARMS":
await self._reset_alarms_file("LLM delete all alarms")
try:
await self.capability_worker.write_file("alarms.json", "[]", False)
except Exception:
pass
await self.capability_worker.speak("All alarms deleted.")
return
if llm_response.startswith("QUESTION:"):
history.append({"role": "assistant", "content": llm_response})
question = llm_response.split("QUESTION:", 1)[1].strip()
user_text = await self.capability_worker.run_io_loop(question)
continue
# not a question -> should be JSON
try:
parsed = json.loads(llm_response)
except Exception:
await self.capability_worker.speak("I couldn't understand the time. Try again.")
return
if not parsed.get("target_iso"):
await self.capability_worker.speak("I couldn't understand the time. Try again.")
return
alarm = {
"id": f"alarm_{int(time() * 1000)}",
"created_at_epoch": int(time()),
"timezone": parsed.get("timezone", tz_name),
"target_iso": parsed["target_iso"],
"human_time": parsed.get("human_time", parsed["target_iso"]),
"source_text": original_request,
"status": "scheduled",
}
alarms = await self._read_alarms()
alarms.append(alarm)
await self._write_alarms(alarms)
await self.capability_worker.speak(f"Alarm set for {alarm['human_time']}.")
return
await self.capability_worker.speak("Too many questions. Please try setting the alarm again.")
except Exception:
pass
finally:
self.capability_worker.resume_normal_flow()
def call(self, worker):
try:
worker.editor_logging_handler.info("Alarm Capability")
self.worker = worker
self.capability_worker = CapabilityWorker(self)
self.worker.session_tasks.create(self.first_setup())
except Exception as e:
self.worker.editor_logging_handler.warning(e)