eraz3r commited on
Commit
d697cd6
Β·
verified Β·
1 Parent(s): 52b002f

Update core/ai_engine.py

Browse files
Files changed (1) hide show
  1. core/ai_engine.py +199 -198
core/ai_engine.py CHANGED
@@ -1,12 +1,13 @@
1
  """
2
  core/ai_engine.py
3
- All Groq API calls β€” task parsing, intelligent RAG-style scheduling, journaling.
 
4
  """
5
 
6
  import json
7
  import re
8
  import os
9
- from datetime import datetime, date, timedelta
10
  from copy import deepcopy
11
 
12
  from groq import Groq
@@ -19,7 +20,10 @@ def init_groq(api_key: str = None):
19
  global _client
20
  key = api_key or os.environ.get("GROQ_API_KEY", "")
21
  if not key:
22
- raise ValueError("GROQ_API_KEY is not set. Add it in Space Settings -> Repository Secrets.")
 
 
 
23
  _client = Groq(api_key=key)
24
 
25
 
@@ -32,6 +36,7 @@ def _groq() -> Groq:
32
  # ── Shared util ───────────────────────────────────────────────────────────────
33
 
34
  def safe_json_parse(text: str):
 
35
  try:
36
  return json.loads(text)
37
  except json.JSONDecodeError:
@@ -50,34 +55,66 @@ def safe_json_parse(text: str):
50
 
51
  # ── Module 1: Task Capture ────────────────────────────────────────────────────
52
 
53
- TASK_CAPTURE_PROMPT = """You are a task classification assistant for a productivity app called Second Brain.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- Take the user's raw task description and return a structured JSON object.
 
 
 
56
 
57
- TODAY'S DATE: {today}
 
 
 
 
 
 
 
 
 
58
 
59
- Dimensions:
60
- 1. title (string): Clean, action-oriented task title.
61
- 2. life_area (string): ONE of: Work, Health, Learning, Finance, Personal, Family, Other
62
- 3. urgency (string): ONE of: Habit | Urgent | Not Urgent
63
- 4. importance (string): ONE of: Move the Needle | Important | Not Important
64
- 5. state_of_mind (string): ONE of: Flow | Easy | Quick | Personal
65
  6. time_estimate (integer): Realistic minutes to complete.
66
- 7. deadline_date (string | null): If the user mentions ANY deadline, due date, or "by X" β€” extract as YYYY-MM-DD.
67
- Examples: "by Friday" -> next Friday's date, "due March 1" -> "2026-03-01", "end of month" -> last day of current month.
68
- If no deadline mentioned, return null.
69
- 8. clarifications_needed (array): Short specific questions if uncertain about any dimension. Return [] if confident.
70
 
71
- RETURN: Valid JSON only. No markdown, no prose. All 8 fields always present.
72
- CRITICAL: If user says "by [date]", "due [date]", "before [date]", "deadline [date]" β€” ALWAYS populate deadline_date."""
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
 
75
  def parse_task_with_groq(raw_text: str, user_context: dict = None,
76
  user_goals: list = None, life_areas: list = None) -> dict:
 
 
77
  context_hint = ""
78
  if user_context and user_context.get("learned_patterns", {}).get("notes"):
79
  notes = user_context["learned_patterns"]["notes"]
80
- context_hint += f"\n\nUser patterns: {'; '.join(notes[-3:])}"
81
  if user_goals:
82
  context_hint += f"\nUser goals: {'; '.join(user_goals[:5])}"
83
  if life_areas:
@@ -102,233 +139,190 @@ def parse_task_with_groq(raw_text: str, user_context: dict = None,
102
  "life_area": None, "urgency": None, "importance": None,
103
  "state_of_mind": None, "time_estimate": None,
104
  "deadline_date": None,
105
- "clarifications_needed": ["Could you give more details? Which area, how urgent, how long?"]
 
 
 
 
106
  }
107
  return result
108
 
109
 
110
- # ── Module 2: Intelligent RAG-style Task Scheduler ────────────────────────────
111
 
112
- SMART_SCHEDULER_PROMPT = """You are an intelligent task scheduler for Second Brain.
113
 
114
- You assign UNSCHEDULED TASKS to specific future dates AND time blocks, like a smart personal assistant
115
- who understands the user's rhythms, goals, energy patterns, and current time.
116
 
117
- REASONING PROCESS:
118
- 1. Read the user request carefully β€” honour it above all else
119
- - "clear my day" / "nothing today" / "free today" = assign NOTHING to today
120
- - "schedule for tomorrow" = assign to tomorrow
121
- - "this week" = spread across next 5 days
122
- - No explicit date = use next 1-7 days intelligently
123
- 2. Never assign to a date/time in the past (current datetime is given)
124
- 3. If current time is past 18:00, treat today as unavailable unless user explicitly asks
125
- 4. Prioritise by: deadline proximity > urgency > importance > goal alignment
126
- 5. Match tasks to time blocks by state_of_mind:
127
- - Flow = peak focus window (morning if peak=Morning), 60-90 min blocks
128
- - Easy = mid-morning or early afternoon, 20-45 min
129
- - Quick = any gap, 10-20 min
130
- - Personal = end of day or lunch, flexible
131
- 6. Respect wake/sleep times. Add 10min breaks between tasks. Don't exceed max_flow_block.
132
- 7. Spread load sensibly β€” don't overstack one day
133
- 8. Tasks with deadlines must land BEFORE that deadline
134
 
135
  RETURN FORMAT (JSON only, no markdown):
136
  {
137
- "assignments": [
 
138
  {
139
- "task_id": 123,
140
- "title": "Task title",
141
- "assigned_date": "YYYY-MM-DD",
142
- "start_time": "09:00",
143
- "end_time": "10:30",
144
- "reasoning": "1 sentence why this slot"
 
 
145
  }
146
  ],
147
- "skipped": [
148
- {
149
- "task_id": 456,
150
- "title": "Task title",
151
- "reason": "why not assigned"
152
- }
153
- ],
154
- "summary": "2-3 sentence plain-English explanation of what was scheduled and why",
155
- "warnings": ["any overload or conflict warnings"]
156
- }
157
-
158
- NEVER assign to a past date/time. NEVER ignore explicit user scheduling instructions."""
159
-
160
-
161
- def smart_schedule_tasks(
162
- tasks: list,
163
- user_context: dict,
164
- user_goals: list,
165
- scheduling_prompt: str,
166
- current_dt: datetime = None,
167
- ) -> dict:
168
- """
169
- RAG-style scheduler: reads context + goals + patterns + current time + user request,
170
- assigns each unscheduled task to a specific future date.
171
- """
172
- if current_dt is None:
173
- current_dt = datetime.now()
174
-
175
- today = current_dt.date()
176
- tomorrow = today + timedelta(days=1)
177
- next_7 = [(today + timedelta(days=i)).isoformat() for i in range(8)]
178
-
179
- prefs = user_context.get("preferences", {})
180
- patterns = user_context.get("learned_patterns", {})
181
- feedback = user_context.get("scheduling_feedback", {})
182
-
183
- goals_lines = "\n".join(f"- {g}" for g in (user_goals or [])) or "(none set)"
184
- context_block = f"""CURRENT DATE/TIME: {current_dt.strftime('%Y-%m-%d %H:%M')} ({current_dt.strftime('%A')})
185
- TODAY: {today.isoformat()} | TOMORROW: {tomorrow.isoformat()}
186
- NEXT 7 DAYS: {', '.join(next_7)}
187
-
188
- USER PREFERENCES:
189
- - Wake: {prefs.get('wake_time', '08:00')} | Sleep: {prefs.get('sleep_time', '23:00')}
190
- - Peak focus: {prefs.get('focus_peak', 'Morning')}
191
- - Max flow block: {prefs.get('max_flow_block_minutes', 90)} min
192
-
193
- LEARNED PATTERNS:
194
- - Productive times: {patterns.get('productive_times', 'unknown')}
195
- - Low energy times: {patterns.get('low_energy_times', 'unknown')}
196
- - Avg task overrun: {patterns.get('avg_task_overrun_pct', 0)}%
197
- - Flow batching: {patterns.get('flow_batch_capable', 'unknown')}
198
- - Commonly skipped: {patterns.get('common_skipped_task_types', [])}
199
- - Notes: {'; '.join(patterns.get('notes', [])[-3:])}
200
-
201
- HISTORY:
202
- - Days tracked: {feedback.get('total_days_scheduled', 0)}
203
- - Avg completion: {round(feedback.get('avg_completion_rate', 0) * 100)}%
204
-
205
- GOALS:
206
- {goals_lines}"""
207
-
208
- tasks_block = json.dumps([{
209
- "task_id": t.get("id", t.get("task_id")),
210
- "title": t.get("title"),
211
- "life_area": t.get("life_area"),
212
- "urgency": t.get("urgency"),
213
- "importance": t.get("importance"),
214
- "state_of_mind": t.get("state_of_mind"),
215
- "time_estimate": t.get("time_estimate"),
216
- "deadline_date": t.get("deadline_date") or "none",
217
- } for t in tasks], indent=2)
218
-
219
- user_message = f"""{context_block}
220
-
221
- UNSCHEDULED TASKS ({len(tasks)} tasks):
222
- {tasks_block}
223
-
224
- USER REQUEST: "{scheduling_prompt}"
225
-
226
- Assign each task to the best date. Follow the user request precisely."""
227
 
228
- try:
229
- response = _groq().chat.completions.create(
230
- model=GROQ_MODEL,
231
- messages=[
232
- {"role": "system", "content": SMART_SCHEDULER_PROMPT},
233
- {"role": "user", "content": user_message}
234
- ],
235
- max_tokens=2048,
236
- temperature=0.15,
237
- )
238
- result = safe_json_parse(response.choices[0].message.content.strip())
239
- except Exception as e:
240
- result = None
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  if result is None:
243
- return {
244
- "assignments": [],
245
- "skipped": [{"task_id": t.get("id", t.get("task_id")), "title": t.get("title"),
246
- "reason": "AI scheduling failed"} for t in tasks],
247
- "summary": "Scheduling failed β€” please try again or rephrase your request."
248
  }
249
 
250
- # Safety pass: strip any assignments set in the past
251
- safe_assignments = []
252
- for a in result.get("assignments", []):
253
- try:
254
- assigned = date.fromisoformat(a["assigned_date"])
255
- if assigned >= today:
256
- safe_assignments.append(a)
257
- else:
258
- result.setdefault("skipped", []).append({
259
- "task_id": a.get("task_id"),
260
- "title": a.get("title", ""),
261
- "reason": f"AI tried to assign to past date {a['assigned_date']} β€” blocked"
262
- })
263
- except (ValueError, KeyError):
264
- pass
265
-
266
- result["assignments"] = safe_assignments
267
  return result
268
 
269
 
270
  # ── Module 3: Journaling ──────────────────────────────────────────────────────
271
 
272
- JOURNAL_QUESTION_PROMPT = """You are a reflective journaling coach for Second Brain.
273
 
274
- Given user context, today's tasks, and the conversation so far β€” decide what to ask next.
 
275
 
276
- FOCUS: completion reasons, energy patterns, time estimate accuracy, flow placement, overall balance.
 
 
 
 
 
 
277
 
278
- RULES: ONE question at a time. Build on prior answers. After 5-7 exchanges, signal session_complete.
279
- Warm, efficient tone β€” 2-minute check-in.
 
 
 
280
 
281
- RETURN (JSON only):
282
- {"question": "...", "question_focus": "...", "session_complete": false}
283
- OR: {"question": null, "question_focus": null, "session_complete": true}"""
 
284
 
285
 
286
- SYNTHESIS_PROMPT = """You are a pattern recognition engine for Second Brain.
287
 
288
- Given a completed journaling conversation, return the UPDATED user context JSON.
289
 
290
- UPDATE learned_patterns based on evidence: productive_times, low_energy_times, avg_task_overrun_pct,
291
- flow_batch_capable, best_life_areas_morning, common_skipped_task_types.
292
- Append 1-2 new insight notes (never remove existing).
 
 
 
 
 
293
 
294
- ALWAYS UPDATE: scheduling_feedback (total_days_scheduled +1, rolling avg, last_7_rates),
295
- history_summary (append today, keep last 14), last_updated (now), version (+1).
 
 
 
 
 
296
 
297
- RETURN: Complete updated context JSON only. No markdown. Conservative β€” only update on clear evidence."""
 
 
 
298
 
299
 
300
  def build_opening_question(context: dict, tasks_today: list) -> dict:
301
- total = len(tasks_today)
302
- completed = sum(1 for t in tasks_today if t.get("completed", False))
 
303
  incomplete = [t for t in tasks_today if not t.get("completed", False)]
304
 
305
  if total == 0:
306
- q = "No tasks were scheduled today β€” intentional, or did things go sideways?"
307
  elif completed == 0:
308
- q = f"None of today's {total} tasks got marked complete β€” derailed, or the plan didn't fit?"
309
  elif completed == total:
310
- q = f"You completed all {total} tasks β€” great day! Did it feel natural, or were you grinding through it?"
311
  elif len(incomplete) == 1:
312
- q = f'Almost everything done β€” the one task left was "{incomplete[0]["title"]}". What got in the way?'
313
  else:
314
  rate = round(completed / total * 100)
315
  titles = ", ".join(f'"{t["title"]}"' for t in incomplete[:2])
316
- q = f"You completed {completed}/{total} tasks ({rate}%). Tasks like {titles} didn't get done β€” time, energy, or something else?"
317
 
318
  return {"question": q, "question_focus": "completion_overview", "session_complete": False}
319
 
320
 
321
- def get_next_journal_question(context: dict, tasks_today: list, conversation_history: list) -> dict:
 
322
  user_message = f"""USER CONTEXT:
323
  {json.dumps(context, indent=2)}
324
 
325
- TODAY'S TASKS:
326
  {json.dumps(tasks_today, indent=2)}
327
 
328
- CONVERSATION ({len(conversation_history)} exchanges):
329
  {json.dumps(conversation_history, indent=2)}
330
 
331
- What should I ask next?"""
332
 
333
  response = _groq().chat.completions.create(
334
  model=GROQ_MODEL,
@@ -342,25 +336,30 @@ What should I ask next?"""
342
 
343
  result = safe_json_parse(response.choices[0].message.content.strip())
344
  if result is None:
345
- result = {"question": response.choices[0].message.content.strip(),
346
- "question_focus": "general", "session_complete": False}
 
 
 
347
  return result
348
 
349
 
350
- def synthesize_journal(context: dict, tasks_today: list, conversation_history: list) -> dict:
351
- total = len(tasks_today)
 
 
352
  completed = sum(1 for t in tasks_today if t.get("completed", False))
353
  completion_rate = round(completed / total, 2) if total > 0 else 0.0
354
 
355
- user_message = f"""USER CONTEXT:
356
  {json.dumps(context, indent=2)}
357
 
358
- TODAY'S TASKS:
359
  {json.dumps(tasks_today, indent=2)}
360
 
361
- Completion rate: {completion_rate} ({completed}/{total})
362
 
363
- CONVERSATION:
364
  {json.dumps(conversation_history, indent=2)}
365
 
366
  Return the complete updated context JSON."""
@@ -376,7 +375,9 @@ Return the complete updated context JSON."""
376
  )
377
 
378
  updated = safe_json_parse(response.choices[0].message.content.strip())
 
379
  if updated is None:
 
380
  updated = deepcopy(context)
381
  updated["last_updated"] = datetime.now().isoformat()
382
  updated["version"] = context.get("version", 1) + 1
 
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
 
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
 
 
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:
 
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. deadline_date (string or null): Extract YYYY-MM-DD if user mentions any deadline.
94
+ Today is {today}. Examples: "by Friday" β†’ next Friday's date, "due March 1" β†’ "2026-03-01".
95
+ Return null if no deadline is mentioned.
96
+
97
+ 8. clarifications_needed (array of strings):
98
+ If NOT confident about a dimension, add a short specific question.
99
+ If everything is clear, return []
100
+
101
+ STRICT RULES:
102
+ - Return ONLY valid JSON. No markdown, no explanation.
103
+ - Never guess if uncertain β€” ask a clarification question instead.
104
+ - Always return all 8 fields.
105
+ - ALWAYS extract deadline_date if user says "by X", "due X", "before X", "deadline X".
106
+
107
+ Example: {"title": "Finish project proposal", "life_area": "Work", "urgency": "Urgent", "importance": "Move the Needle", "state_of_mind": "Flow", "time_estimate": 90, "deadline_date": null, "clarifications_needed": []}"""
108
 
109
 
110
  def parse_task_with_groq(raw_text: str, user_context: dict = None,
111
  user_goals: list = None, life_areas: list = None) -> dict:
112
+ """Parse raw task text into structured dimensions using Groq."""
113
+ # Build context hint from AI memory + goals
114
  context_hint = ""
115
  if user_context and user_context.get("learned_patterns", {}).get("notes"):
116
  notes = user_context["learned_patterns"]["notes"]
117
+ context_hint += f"\n\nUser context notes (use to inform classification): {'; '.join(notes[-3:])}"
118
  if user_goals:
119
  context_hint += f"\nUser goals: {'; '.join(user_goals[:5])}"
120
  if life_areas:
 
139
  "life_area": None, "urgency": None, "importance": None,
140
  "state_of_mind": None, "time_estimate": None,
141
  "deadline_date": None,
142
+ "clarifications_needed": [
143
+ "Could you give more details about this task?",
144
+ "Which area of your life does this belong to?",
145
+ "Is this urgent or flexible?"
146
+ ]
147
  }
148
  return result
149
 
150
 
151
+ # ── Module 2: Scheduling ──────────────────────────────────────────────────────
152
 
153
+ SCHEDULING_SYSTEM_PROMPT = """You are an intelligent daily scheduler for a productivity app called The Second Brain.
154
 
155
+ You receive a USER CONTEXT (preferences + learned patterns), a TASK LIST, and a SCHEDULING PROMPT.
156
+ Return a time-blocked schedule as a JSON object.
157
 
158
+ SCHEDULING RULES:
159
+ - Respect wake_time and sleep_time from context
160
+ - Place Flow tasks during the user's peak focus time
161
+ - If avg_task_overrun_pct > 0, add buffer proportionally to time estimates
162
+ - If flow_batch_capable is true, group Flow tasks; otherwise space them out
163
+ - Place Quick and Easy tasks around transitions and low-energy windows
164
+ - Place Personal/Habit tasks at day boundaries (start or end of day)
165
+ - Urgent tasks are scheduled before Not Urgent ones
166
+ - Move the Needle tasks get the best time slots
167
+ - Add 5-10 min breaks between tasks
168
+ - Respect any fixed commitments mentioned in the scheduling prompt
169
+ - Do NOT schedule past sleep_time
170
+ - If tasks won't realistically fit, put them in deferred_tasks
171
+ - If context is minimal (new user), use sensible defaults
 
 
 
172
 
173
  RETURN FORMAT (JSON only, no markdown):
174
  {
175
+ "schedule_date": "YYYY-MM-DD",
176
+ "scheduled_tasks": [
177
  {
178
+ "task_id": "(id from input or index)",
179
+ "title": "...",
180
+ "life_area": "...",
181
+ "start_time": "HH:MM",
182
+ "end_time": "HH:MM",
183
+ "duration_minutes": 60,
184
+ "state_of_mind": "...",
185
+ "scheduling_reason": "1-sentence explanation"
186
  }
187
  ],
188
+ "deferred_tasks": [{"task_id": "...", "title": "...", "reason": "..."}],
189
+ "day_summary": "2-3 sentences on day structure and reasoning",
190
+ "warnings": ["any concerns e.g. day overloaded"]
191
+ }"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ def generate_schedule(context: dict, tasks: list, scheduling_prompt: str,
195
+ goals: list = None, schedule_date: str = None) -> dict:
196
+ if not schedule_date:
197
+ schedule_date = str(date.today())
198
+
199
+ goals_section = ""
200
+ if goals:
201
+ goals_section = "\nUSER GOALS:\n" + "\n".join(f"- {g}" for g in goals)
202
+
203
+ user_message = f"""Schedule Date: {schedule_date}
204
+
205
+ USER CONTEXT:
206
+ {json.dumps(context, indent=2)}
207
+ {goals_section}
208
+ TASKS TO SCHEDULE ({len(tasks)} tasks):
209
+ {json.dumps(tasks, indent=2)}
210
+
211
+ USER SCHEDULING PROMPT:
212
+ {scheduling_prompt}
213
+
214
+ Generate the optimal schedule."""
215
+
216
+ response = _groq().chat.completions.create(
217
+ model=GROQ_MODEL,
218
+ messages=[
219
+ {"role": "system", "content": SCHEDULING_SYSTEM_PROMPT},
220
+ {"role": "user", "content": user_message}
221
+ ],
222
+ max_tokens=2048,
223
+ temperature=0.2,
224
+ )
225
+
226
+ result = safe_json_parse(response.choices[0].message.content.strip())
227
  if result is None:
228
+ result = {
229
+ "error": "Could not parse schedule response.",
230
+ "raw": response.choices[0].message.content
 
 
231
  }
232
 
233
+ result["schedule_date"] = schedule_date
234
+ result["generated_at"] = datetime.now().isoformat()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  return result
236
 
237
 
238
  # ── Module 3: Journaling ──────────────────────────────────────────────────────
239
 
240
+ JOURNAL_QUESTION_PROMPT = """You are a reflective journaling coach for a productivity app called The Second Brain.
241
 
242
+ You receive the user's context, today's schedule with completion status, and the conversation so far.
243
+ Your job: decide what targeted question to ask NEXT.
244
 
245
+ FOCUS AREAS (cover what's most relevant, don't ask all):
246
+ - Tasks not completed β€” why? wrong time? too tired? overestimated?
247
+ - Tasks that took much longer than estimated
248
+ - Energy levels β€” when were they sharp vs drained?
249
+ - Whether Flow tasks were placed well or hard to start
250
+ - Whether the day felt balanced or overloaded
251
+ - Patterns the user noticed about themselves
252
 
253
+ RULES:
254
+ - Ask ONE question at a time. Short and specific.
255
+ - Build on previous answers β€” don't repeat covered ground.
256
+ - After 4-6 good exchanges, signal completion.
257
+ - Keep tone warm and efficient β€” 2-minute check-in, not therapy.
258
 
259
+ RETURN FORMAT (JSON only):
260
+ {"question": "Your next question", "question_focus": "what aspect this targets", "session_complete": false}
261
+ OR when done:
262
+ {"question": null, "question_focus": null, "session_complete": true}"""
263
 
264
 
265
+ SYNTHESIS_PROMPT = """You are a pattern recognition engine for a productivity app called The Second Brain.
266
 
267
+ You have a completed journaling conversation. Extract learnings and return the UPDATED user context JSON.
268
 
269
+ UPDATE these fields in learned_patterns based on conversation evidence:
270
+ - productive_times: when user felt sharp/focused
271
+ - low_energy_times: when they felt drained or skipped tasks
272
+ - avg_task_overrun_pct: recalculate from actual vs estimated times mentioned
273
+ - flow_batch_capable: update if user gave clear evidence
274
+ - best_life_areas_morning: what they completed well before noon
275
+ - common_skipped_task_types: patterns in what gets consistently skipped
276
+ - notes: append 1-2 new insight notes (keep existing ones)
277
 
278
+ ALWAYS UPDATE:
279
+ - scheduling_feedback.total_days_scheduled: +1
280
+ - scheduling_feedback.avg_completion_rate: rolling average
281
+ - scheduling_feedback.last_7_day_completion_rates: append today, keep last 7
282
+ - history_summary: append brief today summary, keep last 14
283
+ - last_updated: now
284
+ - version: +1
285
 
286
+ RULES:
287
+ - Return ONLY the complete updated context JSON. Nothing else.
288
+ - Never remove existing patterns β€” only update or append.
289
+ - Be conservative β€” only update if there is clear evidence in the conversation."""
290
 
291
 
292
  def build_opening_question(context: dict, tasks_today: list) -> dict:
293
+ """Generate the first journal question based on task completion at a glance."""
294
+ total = len(tasks_today)
295
+ completed = sum(1 for t in tasks_today if t.get("completed", False))
296
  incomplete = [t for t in tasks_today if not t.get("completed", False)]
297
 
298
  if total == 0:
299
+ q = "It looks like you didn't have any tasks scheduled today β€” was that intentional or did things go sideways?"
300
  elif completed == 0:
301
+ 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?"
302
  elif completed == total:
303
+ q = f"You completed all {total} tasks today β€” great day! Did the schedule feel natural, or were you pushing through?"
304
  elif len(incomplete) == 1:
305
+ q = f'You got almost everything done β€” the one task left was "{incomplete[0]["title"]}". What got in the way?'
306
  else:
307
  rate = round(completed / total * 100)
308
  titles = ", ".join(f'"{t["title"]}"' for t in incomplete[:2])
309
+ q = f"You completed {completed}/{total} tasks ({rate}%). Tasks like {titles} didn't get done β€” was that time, energy, or something else?"
310
 
311
  return {"question": q, "question_focus": "completion_overview", "session_complete": False}
312
 
313
 
314
+ def get_next_journal_question(context: dict, tasks_today: list,
315
+ conversation_history: list) -> dict:
316
  user_message = f"""USER CONTEXT:
317
  {json.dumps(context, indent=2)}
318
 
319
+ TODAY'S SCHEDULE (with completion):
320
  {json.dumps(tasks_today, indent=2)}
321
 
322
+ CONVERSATION SO FAR ({len(conversation_history)} exchanges):
323
  {json.dumps(conversation_history, indent=2)}
324
 
325
+ What should I ask next? Return session_complete: true if enough has been covered."""
326
 
327
  response = _groq().chat.completions.create(
328
  model=GROQ_MODEL,
 
336
 
337
  result = safe_json_parse(response.choices[0].message.content.strip())
338
  if result is None:
339
+ result = {
340
+ "question": response.choices[0].message.content.strip(),
341
+ "question_focus": "general",
342
+ "session_complete": False
343
+ }
344
  return result
345
 
346
 
347
+ def synthesize_journal(context: dict, tasks_today: list,
348
+ conversation_history: list) -> dict:
349
+ """Synthesize conversation into updated context. Fallback to manual stats update if LLM fails."""
350
+ total = len(tasks_today)
351
  completed = sum(1 for t in tasks_today if t.get("completed", False))
352
  completion_rate = round(completed / total, 2) if total > 0 else 0.0
353
 
354
+ user_message = f"""USER CONTEXT (current):
355
  {json.dumps(context, indent=2)}
356
 
357
+ TODAY'S SCHEDULE + COMPLETION:
358
  {json.dumps(tasks_today, indent=2)}
359
 
360
+ Today's completion rate: {completion_rate} ({completed}/{total})
361
 
362
+ FULL JOURNALING CONVERSATION:
363
  {json.dumps(conversation_history, indent=2)}
364
 
365
  Return the complete updated context JSON."""
 
375
  )
376
 
377
  updated = safe_json_parse(response.choices[0].message.content.strip())
378
+
379
  if updated is None:
380
+ # Fallback: update stats manually if synthesis fails
381
  updated = deepcopy(context)
382
  updated["last_updated"] = datetime.now().isoformat()
383
  updated["version"] = context.get("version", 1) + 1