eraz3r commited on
Commit
b03ef17
·
verified ·
1 Parent(s): 9d75f48

Delete ai_engine.py

Browse files
Files changed (1) hide show
  1. ai_engine.py +0 -418
ai_engine.py DELETED
@@ -1,418 +0,0 @@
1
- """
2
- core/ai_engine.py
3
- All Groq API calls — prompts taken verbatim from the tested Colab notebook.
4
- Covers: task parsing, scheduling, journaling Q&A, context synthesis.
5
- """
6
-
7
- import json
8
- import re
9
- import os
10
- from datetime import datetime, date
11
- from copy import deepcopy
12
-
13
- from groq import Groq
14
-
15
- GROQ_MODEL = "llama-3.3-70b-versatile"
16
- _client: Groq = None
17
-
18
-
19
- def init_groq(api_key: str = None):
20
- global _client
21
- key = api_key or os.environ.get("GROQ_API_KEY", "")
22
- if not key:
23
- raise ValueError(
24
- "GROQ_API_KEY is not set. "
25
- "Add it in HuggingFace Space → Settings → Repository secrets."
26
- )
27
- _client = Groq(api_key=key)
28
-
29
-
30
- def _groq() -> Groq:
31
- if _client is None:
32
- init_groq()
33
- return _client
34
-
35
-
36
- # ── Shared util ───────────────────────────────────────────────────────────────
37
-
38
- def safe_json_parse(text: str):
39
- """Parse JSON, stripping markdown fences if present. Returns None on failure."""
40
- try:
41
- return json.loads(text)
42
- except json.JSONDecodeError:
43
- cleaned = re.sub(r'^```(?:json)?\s*|\s*```$', '', text, flags=re.MULTILINE).strip()
44
- try:
45
- return json.loads(cleaned)
46
- except json.JSONDecodeError:
47
- m = re.search(r'\{[\s\S]*\}', cleaned)
48
- if m:
49
- try:
50
- return json.loads(m.group())
51
- except Exception:
52
- pass
53
- return None
54
-
55
-
56
- # ── Module 1: Task Capture ────────────────────────────────────────────────────
57
-
58
- TASK_CAPTURE_PROMPT = """You are a task classification assistant for a productivity app called The Second Brain.
59
-
60
- Your job is to take a user's raw task description and return a structured JSON object.
61
-
62
- Classify the task across these dimensions:
63
-
64
- 1. title (string): A clean, concise, action-oriented task title. Fix grammar.
65
-
66
- 2. life_area (string): Choose ONE from: Work, Health, Learning, Finance, Personal, Family, Other
67
- - Work: job, meetings, deadlines, clients, projects
68
- - Health: exercise, medical, diet, mental health
69
- - Learning: courses, books, skills, studying
70
- - Finance: bills, payments, budgeting, taxes, investments
71
- - Personal: hobbies, errands, home maintenance
72
- - Family: tasks involving family members
73
- - Other: does not fit any category
74
-
75
- 3. urgency (string): Choose ONE from:
76
- - Habit: recurring or routine task
77
- - Urgent: hard deadline or time pressure
78
- - Not Urgent: no specific deadline
79
-
80
- 4. importance (string): Choose ONE from:
81
- - Move the Needle: very high impact
82
- - Important: meaningful, should be done
83
- - Not Important: low real impact
84
-
85
- 5. state_of_mind (string): Choose ONE from:
86
- - Quick: 5-10 mins, very low focus
87
- - Easy: 10-20 mins, low focus
88
- - Flow: deep concentration needed
89
- - Personal: life admin, little goal impact
90
-
91
- 6. time_estimate (integer): Realistic minutes to complete.
92
-
93
- 7. clarifications_needed (array of strings):
94
- If NOT confident about a dimension, add a short specific question.
95
- If everything is clear, return []
96
-
97
- STRICT RULES:
98
- - Return ONLY valid JSON. No markdown, no explanation.
99
- - Never guess if uncertain — ask a clarification question instead.
100
- - Always return all 7 fields.
101
-
102
- Example: {"title": "Finish project proposal", "life_area": "Work", "urgency": "Urgent", "importance": "Move the Needle", "state_of_mind": "Flow", "time_estimate": 90, "clarifications_needed": []}"""
103
-
104
-
105
- def parse_task_with_groq(raw_text: str, user_context: dict = None,
106
- user_goals: list = None, life_areas: list = None) -> dict:
107
- """Parse raw task text into structured dimensions using Groq."""
108
- # Build context hint from AI memory + goals
109
- context_hint = ""
110
- if user_context and user_context.get("learned_patterns", {}).get("notes"):
111
- notes = user_context["learned_patterns"]["notes"]
112
- context_hint += f"\n\nUser context notes (use to inform classification): {'; '.join(notes[-3:])}"
113
- if user_goals:
114
- context_hint += f"\nUser goals: {'; '.join(user_goals[:5])}"
115
- if life_areas:
116
- context_hint += f"\nUser's life areas: {', '.join(life_areas)}"
117
-
118
- response = _groq().chat.completions.create(
119
- model=GROQ_MODEL,
120
- messages=[
121
- {"role": "system", "content": TASK_CAPTURE_PROMPT + context_hint},
122
- {"role": "user", "content": f"Parse this task: {raw_text}"}
123
- ],
124
- max_tokens=512,
125
- temperature=0.1,
126
- )
127
-
128
- result = safe_json_parse(response.choices[0].message.content.strip())
129
- if result is None:
130
- result = {
131
- "title": raw_text,
132
- "life_area": None, "urgency": None, "importance": None,
133
- "state_of_mind": None, "time_estimate": None,
134
- "clarifications_needed": [
135
- "Could you give more details about this task?",
136
- "Which area of your life does this belong to?",
137
- "Is this urgent or flexible?"
138
- ]
139
- }
140
- return result
141
-
142
-
143
- # ── Module 2: Scheduling ──────────────────────────────────────────────────────
144
-
145
- SCHEDULING_SYSTEM_PROMPT = """You are an intelligent daily scheduler for a productivity app called The Second Brain.
146
-
147
- You receive a USER CONTEXT (preferences + learned patterns), a TASK LIST, and a SCHEDULING PROMPT.
148
- Return a time-blocked schedule as a JSON object.
149
-
150
- SCHEDULING RULES:
151
- - Respect wake_time and sleep_time from context
152
- - Place Flow tasks during the user's peak focus time
153
- - If avg_task_overrun_pct > 0, add buffer proportionally to time estimates
154
- - If flow_batch_capable is true, group Flow tasks; otherwise space them out
155
- - Place Quick and Easy tasks around transitions and low-energy windows
156
- - Place Personal/Habit tasks at day boundaries (start or end of day)
157
- - Urgent tasks are scheduled before Not Urgent ones
158
- - Move the Needle tasks get the best time slots
159
- - Add 5-10 min breaks between tasks
160
- - Respect any fixed commitments mentioned in the scheduling prompt
161
- - Do NOT schedule past sleep_time
162
- - If tasks won't realistically fit, put them in deferred_tasks
163
- - If context is minimal (new user), use sensible defaults
164
-
165
- RETURN FORMAT (JSON only, no markdown):
166
- {
167
- "schedule_date": "YYYY-MM-DD",
168
- "scheduled_tasks": [
169
- {
170
- "task_id": "(id from input or index)",
171
- "title": "...",
172
- "life_area": "...",
173
- "start_time": "HH:MM",
174
- "end_time": "HH:MM",
175
- "duration_minutes": 60,
176
- "state_of_mind": "...",
177
- "scheduling_reason": "1-sentence explanation"
178
- }
179
- ],
180
- "deferred_tasks": [{"task_id": "...", "title": "...", "reason": "..."}],
181
- "day_summary": "2-3 sentences on day structure and reasoning",
182
- "warnings": ["any concerns e.g. day overloaded"]
183
- }"""
184
-
185
-
186
- def generate_schedule(context: dict, tasks: list, scheduling_prompt: str,
187
- goals: list = None, schedule_date: str = None) -> dict:
188
- if not schedule_date:
189
- schedule_date = str(date.today())
190
-
191
- goals_section = ""
192
- if goals:
193
- goals_section = "\nUSER GOALS:\n" + "\n".join(f"- {g}" for g in goals)
194
-
195
- user_message = f"""Schedule Date: {schedule_date}
196
-
197
- USER CONTEXT:
198
- {json.dumps(context, indent=2)}
199
- {goals_section}
200
- TASKS TO SCHEDULE ({len(tasks)} tasks):
201
- {json.dumps(tasks, indent=2)}
202
-
203
- USER SCHEDULING PROMPT:
204
- {scheduling_prompt}
205
-
206
- Generate the optimal schedule."""
207
-
208
- response = _groq().chat.completions.create(
209
- model=GROQ_MODEL,
210
- messages=[
211
- {"role": "system", "content": SCHEDULING_SYSTEM_PROMPT},
212
- {"role": "user", "content": user_message}
213
- ],
214
- max_tokens=2048,
215
- temperature=0.2,
216
- )
217
-
218
- result = safe_json_parse(response.choices[0].message.content.strip())
219
- if result is None:
220
- result = {
221
- "error": "Could not parse schedule response.",
222
- "raw": response.choices[0].message.content
223
- }
224
-
225
- result["schedule_date"] = schedule_date
226
- result["generated_at"] = datetime.now().isoformat()
227
- return result
228
-
229
-
230
- # ── Module 3: Journaling ──────────────────────────────────────────────────────
231
-
232
- JOURNAL_QUESTION_PROMPT = """You are a reflective journaling coach for a productivity app called The Second Brain.
233
-
234
- You receive the user's context, today's schedule with completion status, and the conversation so far.
235
- Your job: decide what targeted question to ask NEXT.
236
-
237
- FOCUS AREAS (cover what's most relevant, don't ask all):
238
- - Tasks not completed — why? wrong time? too tired? overestimated?
239
- - Tasks that took much longer than estimated
240
- - Energy levels — when were they sharp vs drained?
241
- - Whether Flow tasks were placed well or hard to start
242
- - Whether the day felt balanced or overloaded
243
- - Patterns the user noticed about themselves
244
-
245
- RULES:
246
- - Ask ONE question at a time. Short and specific.
247
- - Build on previous answers — don't repeat covered ground.
248
- - After 4-6 good exchanges, signal completion.
249
- - Keep tone warm and efficient — 2-minute check-in, not therapy.
250
-
251
- RETURN FORMAT (JSON only):
252
- {"question": "Your next question", "question_focus": "what aspect this targets", "session_complete": false}
253
- OR when done:
254
- {"question": null, "question_focus": null, "session_complete": true}"""
255
-
256
-
257
- SYNTHESIS_PROMPT = """You are a pattern recognition engine for a productivity app called The Second Brain.
258
-
259
- You have a completed journaling conversation. Extract learnings and return the UPDATED user context JSON.
260
-
261
- UPDATE these fields in learned_patterns based on conversation evidence:
262
- - productive_times: when user felt sharp/focused
263
- - low_energy_times: when they felt drained or skipped tasks
264
- - avg_task_overrun_pct: recalculate from actual vs estimated times mentioned
265
- - flow_batch_capable: update if user gave clear evidence
266
- - best_life_areas_morning: what they completed well before noon
267
- - common_skipped_task_types: patterns in what gets consistently skipped
268
- - notes: append 1-2 new insight notes (keep existing ones)
269
-
270
- ALWAYS UPDATE:
271
- - scheduling_feedback.total_days_scheduled: +1
272
- - scheduling_feedback.avg_completion_rate: rolling average
273
- - scheduling_feedback.last_7_day_completion_rates: append today, keep last 7
274
- - history_summary: append brief today summary, keep last 14
275
- - last_updated: now
276
- - version: +1
277
-
278
- RULES:
279
- - Return ONLY the complete updated context JSON. Nothing else.
280
- - Never remove existing patterns — only update or append.
281
- - Be conservative — only update if there is clear evidence in the conversation."""
282
-
283
-
284
- def build_opening_question(context: dict, tasks_today: list) -> dict:
285
- """Generate the first journal question based on task completion at a glance."""
286
- total = len(tasks_today)
287
- completed = sum(1 for t in tasks_today if t.get("completed", False))
288
- incomplete = [t for t in tasks_today if not t.get("completed", False)]
289
-
290
- if total == 0:
291
- q = "It looks like you didn't have any tasks scheduled today — was that intentional or did things go sideways?"
292
- elif completed == 0:
293
- q = f"None of today's {total} tasks got marked complete — was the day unexpectedly derailed, or did the plan just not fit how your day went?"
294
- elif completed == total:
295
- q = f"You completed all {total} tasks today — great day! Did the schedule feel natural, or were you pushing through?"
296
- elif len(incomplete) == 1:
297
- q = f'You got almost everything done — the one task left was "{incomplete[0]["title"]}". What got in the way?'
298
- else:
299
- rate = round(completed / total * 100)
300
- titles = ", ".join(f'"{t["title"]}"' for t in incomplete[:2])
301
- q = f"You completed {completed}/{total} tasks ({rate}%). Tasks like {titles} didn't get done — was that time, energy, or something else?"
302
-
303
- return {"question": q, "question_focus": "completion_overview", "session_complete": False}
304
-
305
-
306
- def get_next_journal_question(context: dict, tasks_today: list,
307
- conversation_history: list) -> dict:
308
- user_message = f"""USER CONTEXT:
309
- {json.dumps(context, indent=2)}
310
-
311
- TODAY'S SCHEDULE (with completion):
312
- {json.dumps(tasks_today, indent=2)}
313
-
314
- CONVERSATION SO FAR ({len(conversation_history)} exchanges):
315
- {json.dumps(conversation_history, indent=2)}
316
-
317
- What should I ask next? Return session_complete: true if enough has been covered."""
318
-
319
- response = _groq().chat.completions.create(
320
- model=GROQ_MODEL,
321
- messages=[
322
- {"role": "system", "content": JOURNAL_QUESTION_PROMPT},
323
- {"role": "user", "content": user_message}
324
- ],
325
- max_tokens=256,
326
- temperature=0.3,
327
- )
328
-
329
- result = safe_json_parse(response.choices[0].message.content.strip())
330
- if result is None:
331
- result = {
332
- "question": response.choices[0].message.content.strip(),
333
- "question_focus": "general",
334
- "session_complete": False
335
- }
336
- return result
337
-
338
-
339
- def synthesize_journal(context: dict, tasks_today: list,
340
- conversation_history: list) -> dict:
341
- """Synthesize conversation into updated context. Fallback to manual stats update if LLM fails."""
342
- total = len(tasks_today)
343
- completed = sum(1 for t in tasks_today if t.get("completed", False))
344
- completion_rate = round(completed / total, 2) if total > 0 else 0.0
345
-
346
- user_message = f"""USER CONTEXT (current):
347
- {json.dumps(context, indent=2)}
348
-
349
- TODAY'S SCHEDULE + COMPLETION:
350
- {json.dumps(tasks_today, indent=2)}
351
-
352
- Today's completion rate: {completion_rate} ({completed}/{total})
353
-
354
- FULL JOURNALING CONVERSATION:
355
- {json.dumps(conversation_history, indent=2)}
356
-
357
- Return the complete updated context JSON."""
358
-
359
- response = _groq().chat.completions.create(
360
- model=GROQ_MODEL,
361
- messages=[
362
- {"role": "system", "content": SYNTHESIS_PROMPT},
363
- {"role": "user", "content": user_message}
364
- ],
365
- max_tokens=2048,
366
- temperature=0.1,
367
- )
368
-
369
- updated = safe_json_parse(response.choices[0].message.content.strip())
370
-
371
- if updated is None:
372
- # Fallback: update stats manually if synthesis fails
373
- updated = deepcopy(context)
374
- updated["last_updated"] = datetime.now().isoformat()
375
- updated["version"] = context.get("version", 1) + 1
376
- sf = updated.setdefault("scheduling_feedback", {})
377
- sf["total_days_scheduled"] = sf.get("total_days_scheduled", 0) + 1
378
- rates = sf.get("last_7_day_completion_rates", [])
379
- rates.append(completion_rate)
380
- sf["last_7_day_completion_rates"] = rates[-7:]
381
- sf["avg_completion_rate"] = round(sum(rates) / len(rates), 2)
382
-
383
- return updated
384
-
385
-
386
- # ── Context helpers ───────────────────────────────────────────────────────────
387
-
388
- def create_blank_context(user_id, preferences: dict = None) -> dict:
389
- prefs = preferences or {}
390
- return {
391
- "user_id": user_id,
392
- "created_at": datetime.now().isoformat(),
393
- "last_updated": datetime.now().isoformat(),
394
- "version": 1,
395
- "preferences": {
396
- "wake_time": prefs.get("wake_time", "08:00"),
397
- "sleep_time": prefs.get("sleep_time", "23:00"),
398
- "focus_peak": prefs.get("focus_peak", "Morning"),
399
- "break_duration_minutes": 10,
400
- "max_flow_block_minutes": 90,
401
- },
402
- "learned_patterns": {
403
- "productive_times": [],
404
- "low_energy_times": [],
405
- "avg_task_overrun_pct": 0,
406
- "flow_batch_capable": None,
407
- "best_life_areas_morning": [],
408
- "habit_completion_rate": {},
409
- "common_skipped_task_types": [],
410
- "notes": []
411
- },
412
- "history_summary": [],
413
- "scheduling_feedback": {
414
- "total_days_scheduled": 0,
415
- "avg_completion_rate": 0.0,
416
- "last_7_day_completion_rates": []
417
- }
418
- }