eraz3r commited on
Commit
7d63e80
Β·
verified Β·
1 Parent(s): 75efc02

Update core/ai_engine.py

Browse files
Files changed (1) hide show
  1. core/ai_engine.py +188 -196
core/ai_engine.py CHANGED
@@ -1,13 +1,12 @@
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,10 +19,7 @@ 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
 
@@ -36,7 +32,6 @@ def _groq() -> Groq:
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,61 +50,28 @@ def safe_json_parse(text: str):
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:
@@ -131,190 +93,227 @@ def parse_task_with_groq(raw_text: str, user_context: dict = None,
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,
@@ -328,30 +327,25 @@ What should I ask next? Return session_complete: true if enough has been covered
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."""
@@ -367,9 +361,7 @@ Return the complete updated context JSON."""
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
@@ -415,4 +407,4 @@ def create_blank_context(user_id, preferences: dict = None) -> dict:
415
  "avg_completion_rate": 0.0,
416
  "last_7_day_completion_rates": []
417
  }
418
- }
 
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
  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
  # ── Shared util ───────────────────────────────────────────────────────────────
33
 
34
  def safe_json_parse(text: str):
 
35
  try:
36
  return json.loads(text)
37
  except json.JSONDecodeError:
 
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
+ Dimensions:
58
+ 1. title (string): Clean, action-oriented task title.
59
+ 2. life_area (string): ONE of: Work, Health, Learning, Finance, Personal, Family, Other
60
+ 3. urgency (string): ONE of: Habit | Urgent | Not Urgent
61
+ 4. importance (string): ONE of: Move the Needle | Important | Not Important
62
+ 5. state_of_mind (string): ONE of: Flow | Easy | Quick | Personal
63
  6. time_estimate (integer): Realistic minutes to complete.
64
+ 7. clarifications_needed (array): Short specific questions if uncertain about any dimension. Return [] if confident.
65
 
66
+ RETURN: Valid JSON only. No markdown, no prose. All 7 fields always present."""
 
 
 
 
 
 
 
 
 
67
 
68
 
69
  def parse_task_with_groq(raw_text: str, user_context: dict = None,
70
  user_goals: list = None, life_areas: list = None) -> dict:
 
 
71
  context_hint = ""
72
  if user_context and user_context.get("learned_patterns", {}).get("notes"):
73
  notes = user_context["learned_patterns"]["notes"]
74
+ context_hint += f"\n\nUser patterns: {'; '.join(notes[-3:])}"
75
  if user_goals:
76
  context_hint += f"\nUser goals: {'; '.join(user_goals[:5])}"
77
  if life_areas:
 
93
  "title": raw_text,
94
  "life_area": None, "urgency": None, "importance": None,
95
  "state_of_mind": None, "time_estimate": None,
96
+ "clarifications_needed": ["Could you give more details? Which area, how urgent, how long?"]
 
 
 
 
97
  }
98
  return result
99
 
100
 
101
+ # ── Module 2: Intelligent RAG-style Task Scheduler ────────────────────────────
102
 
103
+ SMART_SCHEDULER_PROMPT = """You are an intelligent task scheduler for Second Brain.
104
 
105
+ You assign UNSCHEDULED TASKS to specific future dates, like a smart personal assistant who understands the
106
+ user's rhythms, goals, energy patterns, and the current time.
107
 
108
+ REASONING PROCESS:
109
+ 1. Read the user request carefully β€” honour it above all else
110
+ - "clear my day" / "nothing today" / "free today" = assign NOTHING to today
111
+ - "schedule for tomorrow" = assign to tomorrow
112
+ - "this week" = spread across next 5 days
113
+ - No explicit date = use next 1-7 days intelligently
114
+ 2. Never assign to a date/time in the past (current datetime is given)
115
+ 3. If current time is past 18:00, treat today as unavailable unless user explicitly asks
116
+ 4. Prioritise by: deadline proximity > urgency > importance > goal alignment
117
+ 5. Match tasks to days by state_of_mind:
118
+ - Flow = peak days (Mon-Thu mornings if peak=Morning)
119
+ - Quick/Easy = any day, fill gaps
120
+ - Habit = today or tomorrow
121
+ 6. Spread load β€” don't stack everything on one day
122
+ 7. Tasks with deadlines must land BEFORE that deadline
123
 
124
  RETURN FORMAT (JSON only, no markdown):
125
  {
126
+ "assignments": [
 
127
  {
128
+ "task_id": 123,
129
+ "title": "Task title",
130
+ "assigned_date": "YYYY-MM-DD",
131
+ "reasoning": "1 sentence why this date"
 
 
 
 
132
  }
133
  ],
134
+ "skipped": [
135
+ {
136
+ "task_id": 456,
137
+ "title": "Task title",
138
+ "reason": "why not assigned"
139
+ }
140
+ ],
141
+ "summary": "2-3 sentence plain-English explanation of what was scheduled and why"
142
+ }
143
+
144
+ NEVER assign to a past date. NEVER ignore an explicit user instruction about when to schedule."""
145
+
146
+
147
+ def smart_schedule_tasks(
148
+ tasks: list,
149
+ user_context: dict,
150
+ user_goals: list,
151
+ scheduling_prompt: str,
152
+ current_dt: datetime = None,
153
+ ) -> dict:
154
+ """
155
+ RAG-style scheduler: reads context + goals + patterns + current time + user request,
156
+ assigns each unscheduled task to a specific future date.
157
+ """
158
+ if current_dt is None:
159
+ current_dt = datetime.now()
160
+
161
+ today = current_dt.date()
162
+ tomorrow = today + timedelta(days=1)
163
+ next_7 = [(today + timedelta(days=i)).isoformat() for i in range(8)]
164
+
165
+ prefs = user_context.get("preferences", {})
166
+ patterns = user_context.get("learned_patterns", {})
167
+ feedback = user_context.get("scheduling_feedback", {})
168
+
169
+ context_block = f"""CURRENT DATE/TIME: {current_dt.strftime('%Y-%m-%d %H:%M')} ({current_dt.strftime('%A')})
170
+ TODAY: {today.isoformat()} | TOMORROW: {tomorrow.isoformat()}
171
+ NEXT 7 DAYS: {', '.join(next_7)}
172
+
173
+ USER PREFERENCES:
174
+ - Wake: {prefs.get('wake_time', '08:00')} | Sleep: {prefs.get('sleep_time', '23:00')}
175
+ - Peak focus: {prefs.get('focus_peak', 'Morning')}
176
+ - Max flow block: {prefs.get('max_flow_block_minutes', 90)} min
177
+
178
+ LEARNED PATTERNS:
179
+ - Productive times: {patterns.get('productive_times', 'unknown')}
180
+ - Low energy times: {patterns.get('low_energy_times', 'unknown')}
181
+ - Avg task overrun: {patterns.get('avg_task_overrun_pct', 0)}%
182
+ - Flow batching: {patterns.get('flow_batch_capable', 'unknown')}
183
+ - Commonly skipped: {patterns.get('common_skipped_task_types', [])}
184
+ - Notes: {'; '.join(patterns.get('notes', [])[-3:])}
185
+
186
+ HISTORY:
187
+ - Days tracked: {feedback.get('total_days_scheduled', 0)}
188
+ - Avg completion: {round(feedback.get('avg_completion_rate', 0) * 100)}%
189
+
190
+ GOALS:
191
+ {chr(10).join(f'- {g}' for g in (user_goals or [])) or '(none set)'}"""
192
+
193
+ tasks_block = json.dumps([{
194
+ "task_id": t.get("id", t.get("task_id")),
195
+ "title": t.get("title"),
196
+ "life_area": t.get("life_area"),
197
+ "urgency": t.get("urgency"),
198
+ "importance": t.get("importance"),
199
+ "state_of_mind": t.get("state_of_mind"),
200
+ "time_estimate": t.get("time_estimate"),
201
+ "deadline_date": t.get("deadline_date") or "none",
202
+ } for t in tasks], indent=2)
203
+
204
+ user_message = f"""{context_block}
205
+
206
+ UNSCHEDULED TASKS ({len(tasks)} tasks):
207
+ {tasks_block}
208
+
209
+ USER REQUEST: "{scheduling_prompt}"
210
+
211
+ Assign each task to the best date. Follow the user request precisely."""
212
 
213
+ try:
214
+ response = _groq().chat.completions.create(
215
+ model=GROQ_MODEL,
216
+ messages=[
217
+ {"role": "system", "content": SMART_SCHEDULER_PROMPT},
218
+ {"role": "user", "content": user_message}
219
+ ],
220
+ max_tokens=2048,
221
+ temperature=0.15,
222
+ )
223
+ result = safe_json_parse(response.choices[0].message.content.strip())
224
+ except Exception as e:
225
+ result = None
226
 
 
227
  if result is None:
228
+ return {
229
+ "assignments": [],
230
+ "skipped": [{"task_id": t.get("id", t.get("task_id")), "title": t.get("title"),
231
+ "reason": "AI scheduling failed"} for t in tasks],
232
+ "summary": "Scheduling failed β€” please try again or rephrase your request."
233
  }
234
 
235
+ # Safety pass: strip any assignments set in the past
236
+ safe_assignments = []
237
+ for a in result.get("assignments", []):
238
+ try:
239
+ assigned = date.fromisoformat(a["assigned_date"])
240
+ if assigned >= today:
241
+ safe_assignments.append(a)
242
+ else:
243
+ result.setdefault("skipped", []).append({
244
+ "task_id": a.get("task_id"),
245
+ "title": a.get("title", ""),
246
+ "reason": f"AI tried to assign to past date {a['assigned_date']} β€” blocked"
247
+ })
248
+ except (ValueError, KeyError):
249
+ pass
250
+
251
+ result["assignments"] = safe_assignments
252
  return result
253
 
254
 
255
  # ── Module 3: Journaling ──────────────────────────────────────────────────────
256
 
257
+ JOURNAL_QUESTION_PROMPT = """You are a reflective journaling coach for Second Brain.
258
 
259
+ Given user context, today's tasks, and the conversation so far β€” decide what to ask next.
 
260
 
261
+ FOCUS: completion reasons, energy patterns, time estimate accuracy, flow placement, overall balance.
 
 
 
 
 
 
262
 
263
+ RULES: ONE question at a time. Build on prior answers. After 5-7 exchanges, signal session_complete.
264
+ Warm, efficient tone β€” 2-minute check-in.
 
 
 
265
 
266
+ RETURN (JSON only):
267
+ {"question": "...", "question_focus": "...", "session_complete": false}
268
+ OR: {"question": null, "question_focus": null, "session_complete": true}"""
 
269
 
270
 
271
+ SYNTHESIS_PROMPT = """You are a pattern recognition engine for Second Brain.
272
 
273
+ Given a completed journaling conversation, return the UPDATED user context JSON.
274
 
275
+ UPDATE learned_patterns based on evidence: productive_times, low_energy_times, avg_task_overrun_pct,
276
+ flow_batch_capable, best_life_areas_morning, common_skipped_task_types.
277
+ Append 1-2 new insight notes (never remove existing).
 
 
 
 
 
278
 
279
+ ALWAYS UPDATE: scheduling_feedback (total_days_scheduled +1, rolling avg, last_7_rates),
280
+ history_summary (append today, keep last 14), last_updated (now), version (+1).
 
 
 
 
 
281
 
282
+ RETURN: Complete updated context JSON only. No markdown. Conservative β€” only update on clear evidence."""
 
 
 
283
 
284
 
285
  def build_opening_question(context: dict, tasks_today: list) -> dict:
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 = "No tasks were scheduled today β€” intentional, or did things go sideways?"
292
  elif completed == 0:
293
+ q = f"None of today's {total} tasks got marked complete β€” derailed, or the plan didn't fit?"
294
  elif completed == total:
295
+ q = f"You completed all {total} tasks β€” great day! Did it feel natural, or were you grinding through it?"
296
  elif len(incomplete) == 1:
297
+ q = f'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 β€” 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, conversation_history: list) -> dict:
 
307
  user_message = f"""USER CONTEXT:
308
  {json.dumps(context, indent=2)}
309
 
310
+ TODAY'S TASKS:
311
  {json.dumps(tasks_today, indent=2)}
312
 
313
+ CONVERSATION ({len(conversation_history)} exchanges):
314
  {json.dumps(conversation_history, indent=2)}
315
 
316
+ What should I ask next?"""
317
 
318
  response = _groq().chat.completions.create(
319
  model=GROQ_MODEL,
 
327
 
328
  result = safe_json_parse(response.choices[0].message.content.strip())
329
  if result is None:
330
+ result = {"question": response.choices[0].message.content.strip(),
331
+ "question_focus": "general", "session_complete": False}
 
 
 
332
  return result
333
 
334
 
335
+ def synthesize_journal(context: dict, tasks_today: list, conversation_history: list) -> dict:
336
+ total = len(tasks_today)
 
 
337
  completed = sum(1 for t in tasks_today if t.get("completed", False))
338
  completion_rate = round(completed / total, 2) if total > 0 else 0.0
339
 
340
+ user_message = f"""USER CONTEXT:
341
  {json.dumps(context, indent=2)}
342
 
343
+ TODAY'S TASKS:
344
  {json.dumps(tasks_today, indent=2)}
345
 
346
+ Completion rate: {completion_rate} ({completed}/{total})
347
 
348
+ CONVERSATION:
349
  {json.dumps(conversation_history, indent=2)}
350
 
351
  Return the complete updated context JSON."""
 
361
  )
362
 
363
  updated = safe_json_parse(response.choices[0].message.content.strip())
 
364
  if updated is None:
 
365
  updated = deepcopy(context)
366
  updated["last_updated"] = datetime.now().isoformat()
367
  updated["version"] = context.get("version", 1) + 1
 
407
  "avg_completion_rate": 0.0,
408
  "last_7_day_completion_rates": []
409
  }
410
+ }