-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenhanced_llm_parser.py
More file actions
556 lines (465 loc) · 21 KB
/
Copy pathenhanced_llm_parser.py
File metadata and controls
556 lines (465 loc) · 21 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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
#!/usr/bin/env python3
"""
Enhanced LLM Parser for Life OS
Features:
- Conversation memory
- Temporal pattern recognition
- Entity extraction
- Sentiment/mood tracking
- Adaptive response styles
- Model selection optimization
- JSON schema validation
"""
import os
import json
import logging
import re
from datetime import datetime, timedelta
from typing import Dict, Optional, List, Tuple
import httpx
from conversation_memory import ConversationMemory
logger = logging.getLogger(__name__)
class EnhancedLLMParser:
"""
Enhanced LLM parser with all smart features:
- Multi-turn conversation memory
- Temporal context awareness
- Entity extraction and memory
- Sentiment analysis
- Adaptive response styles per user
- Model selection for speed vs accuracy
- Structured JSON output with schema validation
"""
# Response styles
STYLE_BRIEF = "brief"
STYLE_FRIENDLY = "friendly"
STYLE_ANALYTICAL = "analytical"
# Models for different use cases
FAST_MODEL = "llama3.1:8b"
SMART_MODEL = "llama3.1:70b"
def __init__(self, db):
self.ollama_url = os.getenv('OLLAMA_URL', 'http://localhost:11434')
self.default_model = os.getenv('LLM_MODEL', self.FAST_MODEL)
self.memory = ConversationMemory(db)
self.db = db
def get_temporal_context(self) -> Dict:
"""Get rich temporal context for the current moment"""
now = datetime.now()
return {
'hour': now.hour,
'hour_12': now.strftime('%-I:%M %p'),
'day_of_week': now.strftime('%A'),
'day_of_week_num': now.weekday(),
'date': now.strftime('%B %d, %Y'),
'is_weekend': now.weekday() >= 5,
'is_morning': 6 <= now.hour < 12,
'is_afternoon': 12 <= now.hour < 17,
'is_evening': 17 <= now.hour < 21,
'is_night': now.hour >= 21 or now.hour < 6,
'part_of_day': self._get_part_of_day(now.hour),
'quarter': (now.month - 1) // 3 + 1,
'week_of_year': now.isocalendar()[1],
}
def _get_part_of_day(self, hour: int) -> str:
"""Get friendly part of day description"""
if 5 <= hour < 8:
return "early morning"
elif 8 <= hour < 12:
return "morning"
elif 12 <= hour < 14:
return "midday"
elif 14 <= hour < 17:
return "afternoon"
elif 17 <= hour < 20:
return "evening"
elif 20 <= hour < 23:
return "night"
else:
return "late night"
def get_user_context(self, user_id: int) -> Dict:
"""Get comprehensive user context for the LLM"""
context = {
'temporal': self.get_temporal_context(),
'entities': self.memory.get_entities(user_id),
'preferences': self.memory.get_all_preferences(user_id),
'mood': self.memory.get_mood_summary(user_id, hours=24),
}
# Add recent data from database
try:
# Recent tasks
tasks = self.db.get_tasks(limit=5)
context['recent_tasks'] = [
{'description': t.description, 'status': t.status, 'priority': t.priority}
for t in tasks[:5]
]
# Recent food
from datetime import timedelta
yesterday = datetime.now() - timedelta(days=1)
food_logs = self.db.get_food_logs(start_date=yesterday, limit=5)
context['recent_foods'] = []
for log in food_logs:
context['recent_foods'].extend(log.items)
context['recent_foods'] = list(set(context['recent_foods']))[:5]
# Current energy
energy_logs = self.db.get_energy_levels(limit=1)
context['current_energy'] = energy_logs[0].level if energy_logs else None
except Exception as e:
logger.error(f"Error getting user context: {e}")
return context
def select_model(self, message: str, context: Dict) -> str:
"""Select appropriate model based on query complexity"""
message_lower = message.lower()
# Use smart model for:
# - Analysis questions
# - Complex queries
# - Emotional content
# - Pattern recognition
smart_indicators = [
'analyze', 'analysis', 'pattern', 'trend', 'insight',
'how do i feel', 'why am i', 'what\'s wrong',
'stressed', 'anxious', 'overwhelmed', 'exhausted',
'recommend', 'suggest', 'should i',
]
if any(indicator in message_lower for indicator in smart_indicators):
return self.SMART_MODEL
# Use fast model for simple logging
fast_indicators = [
'done', 'finished', 'completed', 'ate', 'had',
'energy', 'supplement',
]
if any(indicator in message_lower for indicator in fast_indicators):
# But only if message is short
if len(message) < 100:
return self.FAST_MODEL
# Default to configured model
return self.default_model
def get_response_style_instructions(self, user_id: int) -> str:
"""Get response style instructions based on user preference"""
style = self.memory.get_preference(user_id, 'response_style', self.STYLE_FRIENDLY)
styles = {
self.STYLE_BRIEF: (
"Be BRIEF and CONCISE. Acknowledge actions in minimal words. "
"Use short sentences. No small talk unless asked."
),
self.STYLE_FRIENDLY: (
"Be FRIENDLY and WARM. Use emojis occasionally. "
"Sound like a helpful friend. Show enthusiasm for accomplishments."
),
self.STYLE_ANALYTICAL: (
"Be ANALYTICAL and PRECISE. Focus on data, patterns, and insights. "
"Use structured responses. Quantify when possible."
)
}
return styles.get(style, styles[self.STYLE_FRIENDLY])
async def parse_message(
self,
message: str,
user_id: int,
user_context: Dict = None
) -> Optional[Dict]:
"""
Parse a message with full context awareness.
Returns a dict with:
- intent: The detected intent
- understanding: What the bot understood
- needs_clarification: Whether clarification is needed
- clarification_question: Question to ask if unclear
- action: Action to take (store, retrieve, analyze, respond_only)
- entities: Extracted entities
- sentiment: Detected sentiment (-1 to 1)
- emotion: Detected emotion
- response: Natural language response to user
- suggestions: Optional proactive suggestions
"""
# Get full context
if user_context is None:
user_context = self.get_user_context(user_id)
# Add conversation history
conversation_history = self.memory.get_conversation_context(user_id)
# Add entities
entities_context = self.memory.get_entities_context(user_id)
# Select model
model = self.select_model(message, user_context)
# Get response style
style_instructions = self.get_response_style_instructions(user_id)
# Build the prompt
temporal = user_context['temporal']
context_prompt = f"""
CURRENT CONTEXT:
- Time: {temporal['hour_12']} on {temporal['day_of_week']}, {temporal['date']}
- Part of day: {temporal['part_of_day']}
- Is weekend: {temporal['is_weekend']}
- Current energy level: {user_context.get('current_energy', 'unknown')}/10
- Recent tasks: {[t['description'] for t in user_context.get('recent_tasks', [])]}
- Recent foods: {user_context.get('recent_foods', [])}
- Mood trend: {user_context.get('mood', {}).get('trend', 'neutral')} (avg: {user_context.get('mood', {}).get('avg', 0)})
{f'{conversation_history}' if conversation_history else ''}
{f'{entities_context}' if entities_context else ''}
RESPONSE STYLE INSTRUCTIONS:
{style_instructions}
"""
system_prompt = self._get_system_prompt()
user_prompt = f"""
{context_prompt}
USER MESSAGE: "{message}"
Analyze this message and return a JSON response with the following structure:
{{
"intent": "log_task|log_food|log_energy|log_health|question|chat|unclear|correction",
"understanding": "brief explanation of what you understood",
"needs_clarification": true/false,
"clarification_question": "specific question if unclear, otherwise null",
"action": {{
"type": "store|retrieve|analyze|respond_only|delete",
"data": {{}}
}},
"entities": {{
"people": ["names mentioned"],
"projects": ["projects mentioned"],
"locations": ["places mentioned"],
"organizations": ["companies/organizations mentioned"]
}},
"sentiment": -1.0 to 1.0,
"emotion": "happy|sad|tired|stressed|excited|frustrated|neutral|accomplished",
"response": "your natural conversational response to the user",
"suggestions": ["optional proactive suggestions based on context"]
}}
CRITICAL RULES:
1. Return ONLY valid JSON - no markdown, no preamble
2. If the message is vague like "Done" or "That thing", set needs_clarification to true and ask specifically
3. Extract and remember entities (people, projects) for future reference
4. Detect sentiment from emotional words and tone
5. Be context-aware - reference their recent activity
6. For questions about data, set action.type to "retrieve" with appropriate query
7. For logging, set action.type to "store" with the data to log
8. Response should be natural and match the user's communication style
9. If asking about tasks, use the recent_tasks context
10. Use temporal context - "good morning" if morning, "good evening" if evening
EXAMPLE INTERACTIONS:
User: "I'm exhausted"
Response: {{"intent":"log_energy","understanding":"User is expressing low energy","needs_clarification":false,"action":{{"type":"store","data":{{"level":3,"context":"feeling exhausted"}}}},"sentiment":-0.6,"emotion":"tired","response":"That sounds tough. When did you start feeling this way?"}}
User: "Done"
Response: {{"intent":"unclear","understanding":"User said 'done' but context unclear","needs_clarification":true,"clarification_question":"Done with what? I see you have these active tasks: [list from context]","response":"Which task did you complete?"}}
User: "Working on the Peterson report"
Response: {{"intent":"log_task","understanding":"User is working on a task","entities":{{"projects":["Peterson report"]}},"action":{{"type":"store","data":{{"description":"Work on Peterson report","status":"in_progress"}}}},"response":"Got it! I'll track that. Let me know when it's done."}}
User: "What was my energy yesterday?"
Response: {{"intent":"question","understanding":"User asking about historical energy data","action":{{"type":"retrieve","data":{{"type":"energy","timeframe":"yesterday"}}}},"response":"Let me check your energy levels from yesterday..."}}
Current time: {datetime.now().strftime('%Y-%m-%d %H:%M')}
Return ONLY the JSON object, nothing else.
"""
try:
async with httpx.AsyncClient(timeout=45.0) as client:
response = await client.post(
f"{self.ollama_url}/api/generate",
json={
"model": model,
"prompt": user_prompt,
"system": system_prompt,
"stream": False,
"format": "json",
"options": {
"temperature": 0.7,
"num_ctx": 4096, # Larger context for conversation memory
}
}
)
if response.status_code != 200:
logger.error(f"Ollama error: {response.status_code}")
return self._fallback_response(message)
result = response.json()
response_text = result.get('response', '')
# Parse JSON with fallback
parsed = self._extract_json(response_text)
if parsed:
# Store entities if found
self._store_entities_from_parse(user_id, parsed, message)
# Log mood/sentiment
if 'sentiment' in parsed:
self.memory.log_mood(
user_id=user_id,
sentiment=parsed.get('sentiment', 0),
emotion=parsed.get('emotion'),
context=parsed.get('understanding'),
message=message
)
# Store conversation message
self.memory.add_message(user_id, 'user', message, metadata={
'intent': parsed.get('intent'),
'sentiment': parsed.get('sentiment'),
})
# Store assistant response too
self.memory.add_message(user_id, 'assistant', parsed.get('response', ''), metadata={
'intent': parsed.get('intent'),
'action': parsed.get('action', {}),
})
logger.info(f"Enhanced LLM parsed: {parsed.get('intent')} - {parsed.get('understanding')}")
return parsed
else:
logger.error(f"Could not parse JSON from: {response_text[:500]}")
return self._fallback_response(message)
except Exception as e:
logger.error(f"Error calling Ollama: {e}")
return self._fallback_response(message)
def _store_entities_from_parse(self, user_id: int, parsed: Dict, original_message: str):
"""Store extracted entities for future reference"""
entities = parsed.get('entities', {})
if not entities:
return
for entity_type, names in entities.items():
if entity_type == 'people':
for name in names:
self.memory.store_entity(user_id, 'person', name, source_message=original_message)
elif entity_type == 'projects':
for name in names:
self.memory.store_entity(user_id, 'project', name, source_message=original_message)
elif entity_type == 'locations':
for name in names:
self.memory.store_entity(user_id, 'location', name, source_message=original_message)
elif entity_type == 'organizations':
for name in names:
self.memory.store_entity(user_id, 'organization', name, source_message=original_message)
def _get_system_prompt(self) -> str:
"""Get the enhanced system prompt"""
return """You are an intelligent personal life assistant with memory and context awareness.
CORE CAPABILITIES:
1. Remember conversations - reference what was said before
2. Understand temporal context - time of day, day of week affects interpretation
3. Extract and remember entities - people, projects, locations mentioned
4. Detect sentiment and emotion - respond appropriately to user's mood
5. Ask smart clarification questions - use context to disambiguate
6. Adapt communication style - match user's preferred style
INTENT CATEGORIES:
- log_task: User mentions something they did or need to do
- log_food: User mentions eating/drinking something
- log_energy: User describes their energy level
- log_health: User mentions supplements, exercise, health metrics
- question: User asks about their data
- chat: General conversation, venting, casual talk
- unclear: Message is ambiguous - needs clarification
- correction: User is correcting a previous entry
SENTIMENT SCALE:
- -1.0 to -0.5: Very negative (angry, devastated, hopeless)
- -0.5 to -0.2: Negative (sad, frustrated, tired)
- -0.2 to 0.2: Neutral (factual, calm)
- 0.2 to 0.5: Positive (happy, content, okay)
- 0.5 to 1.0: Very positive (excited, accomplished, great)
ENTITY TYPES TO EXTRACT:
- people: Names of people (clients, colleagues, family)
- projects: Work projects, cases, initiatives
- locations: Places (office, gym, home, client sites)
- organizations: Companies, agencies, institutions
SMART CLARIFICATION:
When unclear, reference the actual context. Don't just say "What did you complete?"
Say: "Which task did you complete? I see you have: [task1], [task2], [task3] active"
TEMPORAL AWARENESS:
- Morning (6-12): Greet appropriately, expect energy logging
- Afternoon (12-17): Check for lunch energy dips
- Evening (17-21): Wrap-up, tomorrow planning
- Night (21-6): Quiet mode, brief responses
PATTERN RECOGNITION:
- Notice and mention patterns: "You often say you're tired around this time"
- Connect dots: "That heavy lunch might explain the 2 PM dip"
- Celebrate streaks: "3 days in a row of hitting your step goal!"
RESPONSE PRINCIPLES:
1. Be context-aware - reference their actual data
2. Be helpful - offer relevant suggestions
3. Be concise - don't over-explain unless asked
4. Be proactive - anticipate needs based on patterns
5. Be human - occasional warmth, not robotic
Remember: You're building a relationship with the user, not just processing data."""
def _extract_json(self, text: str) -> Optional[Dict]:
"""Extract JSON from possibly messy text with multiple fallback strategies"""
# Try direct parse first
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Try to find JSON in markdown code blocks
json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError:
pass
# Try to find any JSON object (more permissive)
json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group(0))
except json.JSONDecodeError:
pass
# Try fixing common JSON issues
try:
# Remove trailing commas
cleaned = re.sub(r',\s*([}\]])', r'\1', text)
return json.loads(cleaned)
except:
pass
return None
def _fallback_response(self, message: str) -> Dict:
"""Fallback when LLM fails"""
return {
"intent": "unclear",
"understanding": f"Could not process: {message[:50]}",
"needs_clarification": True,
"clarification_question": "I'm having trouble understanding that. Could you rephrase?",
"action": {
"type": "respond_only",
"data": {}
},
"entities": {},
"sentiment": 0,
"emotion": "neutral",
"response": "Sorry, I'm having trouble processing that right now. Could you try rephrasing?",
"suggestions": []
}
async def generate_insight(
self,
user_id: int,
insight_type: str,
data: Dict
) -> str:
"""Generate insights from user data"""
model = self.SMART_MODEL # Use smart model for insights
temporal = self.get_temporal_context()
mood_summary = self.memory.get_mood_summary(user_id, hours=168) # Week
prompt = f"""
Generate a {insight_type} insight based on this user data:
DATA SUMMARY:
{json.dumps(data, indent=2)}
USER MOOD SUMMARY (last week):
- Average sentiment: {mood_summary.get('avg', 0)}
- Trend: {mood_summary.get('trend', 'neutral')}
- Total entries: {mood_summary.get('count', 0)}
CURRENT TIME: {temporal['hour_12']} on {temporal['day_of_week']}
Generate a brief, actionable insight (2-3 sentences). Be specific, mention actual numbers,
and provide a concrete suggestion based on their patterns.
Return ONLY the insight text, nothing else.
"""
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{self.ollama_url}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False,
}
)
if response.status_code == 200:
result = response.json()
return result.get('response', 'Unable to generate insight.')
except Exception as e:
logger.error(f"Error generating insight: {e}")
return "I'm having trouble generating insights right now."
def get_ambiguity_options(self, user_id: int, vague_message: str) -> List[str]:
"""Get specific options based on user's actual data for clarification"""
options = []
# Get pending tasks
pending = self.db.get_tasks(status='pending', limit=10)
if pending:
options.extend([f"Task: {t.description}" for t in pending[:5]])
# Get recent projects/entities
projects = self.memory.get_entities(user_id, 'project')
if projects:
options.extend([f"Project: {p['name']}" for p in projects[:3]])
return options