adikwok commited on
Commit
c61dc03
·
verified ·
1 Parent(s): 88d91ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +716 -329
app.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  import gradio as gr
2
  import requests
3
  import os
@@ -7,6 +11,13 @@ from datetime import datetime, timedelta
7
  from collections import Counter, defaultdict
8
  import re
9
  from pathlib import Path
 
 
 
 
 
 
 
10
 
11
  # Groq API Configuration
12
  API_URL = "https://api.groq.com/openai/v1/chat/completions"
@@ -17,32 +28,275 @@ STORAGE_DIR = Path("chat_storage")
17
  STORAGE_DIR.mkdir(exist_ok=True)
18
  HISTORY_FILE = STORAGE_DIR / "chat_history.json"
19
  ANALYTICS_FILE = STORAGE_DIR / "analytics.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  class ChatStorage:
22
- """Handles persistent JSON storage for chat history"""
23
 
24
  def __init__(self):
25
  self.history_file = HISTORY_FILE
26
  self.analytics_file = ANALYTICS_FILE
 
27
  self.chat_history = self.load_history()
28
  self.analytics = self.load_analytics()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  def load_history(self) -> List[Dict]:
31
  try:
32
  if self.history_file.exists():
33
- with open(self.history_file, ' Retired: 'r', encoding='utf-8') as f:
34
  data = json.load(f)
35
- print(f"📚 Loaded {len(data)} messages from storage")
36
  return data
 
37
  except Exception as e:
38
  print(f"❌ Error loading history: {e}")
39
- return []
40
 
41
  def save_history(self):
42
  try:
43
  with open(self.history_file, 'w', encoding='utf-8') as f:
44
  json.dump(self.chat_history, f, indent=2, ensure_ascii=False)
45
- print(f"💾 Saved {len(self.chat_history)} messages to storage")
46
  except Exception as e:
47
  print(f"❌ Error saving history: {e}")
48
 
@@ -51,9 +305,10 @@ class ChatStorage:
51
  if self.analytics_file.exists():
52
  with open(self.analytics_file, 'r', encoding='utf-8') as f:
53
  return json.load(f)
 
54
  except Exception as e:
55
  print(f"❌ Error loading analytics: {e}")
56
- return {}
57
 
58
  def save_analytics(self):
59
  try:
@@ -62,404 +317,536 @@ class ChatStorage:
62
  except Exception as e:
63
  print(f"❌ Error saving analytics: {e}")
64
 
65
- def add_message(self, role: str, content: str, topic: str = "general"):
66
- message = {
 
 
 
 
 
 
 
 
 
 
67
  "role": role,
68
  "content": content,
69
  "topic": topic,
70
  "timestamp": datetime.now().isoformat(),
71
  "word_count": len(content.split()),
72
- "char_count": len(content)
 
 
 
73
  }
74
- self.chat_history.append(message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  self.save_history()
76
- self.update_analytics(message)
77
 
78
  def update_analytics(self, message: Dict):
79
- today = datetime.now().strftime("%Y-%m-%d")
80
- if "daily_stats" not in self.analytics:
81
- self.analytics["daily_stats"] = {}
82
- if today not in self.analytics["daily_stats"]:
83
- self.analytics["daily_stats"][today] = {
84
- "messages": 0,
85
- "words": 0,
86
- "chars": 0,
87
- "topics": set()
88
- }
89
- stats = self.analytics["daily_stats"][today]
90
- stats["messages"] += 1
91
- stats["words"] += message["word_count"]
92
- stats["chars"] += message["char_count"]
93
- stats["topics"].add(message["topic"])
94
- stats["topics"] = list(stats["topics"])
95
- self.save_analytics()
 
 
 
96
 
97
  def clear_history(self):
98
- self.chat_history.clear()
99
- self.analytics.clear()
100
- self.save_history()
101
- self.save_analytics()
 
 
 
 
 
 
 
 
 
 
102
 
103
  def create_backup(self):
104
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
105
- backup_dir = STORAGE_DIR / "backups"
106
- backup_dir.mkdir(exist_ok=True)
107
- for file in [HISTORY_FILE, ANALYTICS_FILE]:
108
- backup_path = backup_dir / f"{file.name}.{timestamp}.bak"
109
- with open(file, 'r') as src, open(backup_path, 'w') as dst:
110
- dst.write(src.read())
111
- return f"✅ Backup created at {backup_dir}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  class SmartAnalyzer:
114
- """Advanced analytics for chat history"""
115
 
116
  def __init__(self, storage: ChatStorage):
117
  self.storage = storage
118
 
119
  def get_conversation_patterns(self) -> Dict:
120
- if not self.storage.chat_history:
121
- return {"error": "No data available"}
122
- topics = [msg.get("topic", "general") for msg in self.storage.chat_history]
123
- topic_counts = Counter(topics)
124
- timestamps = [datetime.fromisoformat(msg["timestamp"]) for msg in self.storage.chat_history]
125
- hour_counts = Counter([ts.hour for ts in timestamps])
126
- day_counts = Counter([ts.strftime("%A") for ts in timestamps])
127
- all_words = []
128
- for msg in self.storage.chat_history:
129
- if msg["role"] == "user":
 
 
 
 
 
 
130
  words = re.findall(r'\b\w+\b', msg["content"].lower())
131
- all_words.extend(words)
132
- common_words = Counter(all_words).most_common(10)
133
- user_messages = [msg for msg in self.storage.chat_history if msg["role"] == "user"]
134
- avg_words = sum(msg["word_count"] for msg in user_messages) / len(user_messages) if user_messages else 0
135
- return {
136
- "total_messages": len(self.storage.chat_history),
137
- "user_messages": len(user_messages),
138
- "topics": dict(topic_counts),
139
- "peak_hours": dict(sorted(hour_counts.items())),
140
- "peak_days": dict(day_counts),
141
- "common_words": common_words,
142
- "avg_words_per_message": round(avg_words, 1),
143
- "date_range": {
144
- "start": min(timestamps).strftime("%Y-%m-%d") if timestamps else None,
145
- "end": max(timestamps).strftime("%Y-%m-%d") if timestamps else None
 
 
 
 
146
  }
147
- }
 
148
 
149
  def get_mood_trends(self) -> Dict:
150
- if not self.storage.chat_history:
151
- return {"error": "No data available"}
152
- user_messages = [msg for msg in self.storage.chat_history if msg["role"] == "user"]
153
- positive_words = {'good', 'great', 'happy', 'excited', 'amazing', 'wonderful', 'excellent', 'love', 'like', 'enjoy'}
154
- negative_words = {'bad', 'sad', 'angry', 'frustrated', 'terrible', 'awful', 'hate', 'dislike', 'worried', 'stress'}
155
- question_words = {'what', 'how', 'why', 'when', 'where', 'who', 'which'}
156
- sentiments = []
157
- for msg in user_messages:
158
- words = set(re.findall(r'\b\w+\b', msg["content"].lower()))
159
- pos_score = len(words & positive_words)
160
- neg_score = len(words & negative_words)
161
- question_score = len(words & question_words)
162
- if pos_score > neg_score:
163
- sentiment = "positive"
164
- elif neg_score > pos_score:
165
- sentiment = "negative"
166
- elif question_score > 0:
167
- sentiment = "curious"
168
- else:
169
- sentiment = "neutral"
170
- sentiments.append({
171
- "date": datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d"),
172
- "sentiment": sentiment,
173
- "topic": msg.get("topic", "general")
174
- })
175
- daily_moods = defaultdict(list)
176
- for item in sentiments:
177
- daily_moods[item["date"]].append(item["sentiment"])
178
- mood_summary = {}
179
- for date, moods in daily_moods.items():
180
- mood_counter = Counter(moods)
181
- dominant_mood = mood_counter.most_common(1)[0][0]
182
- mood_summary[date] = {
183
- "dominant": dominant_mood,
184
- "distribution": dict(mood_counter)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
- return {
187
- "daily_moods": mood_summary,
188
- "overall_sentiment": Counter([s["sentiment"] for s in sentiments]),
189
- "sentiment_by_topic": self._group_sentiment_by_topic(sentiments)
190
- }
191
 
192
  def _group_sentiment_by_topic(self, sentiments: List[Dict]) -> Dict:
193
- topic_sentiments = defaultdict(list)
194
- for item in sentiments:
195
- topic_sentiments[item["topic"]].append(item["sentiment"])
196
- result = {}
197
- for topic, sent_list in topic_sentiments.items():
198
- result[topic] = dict(Counter(sent_list))
199
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  def get_productivity_insights(self) -> Dict:
202
- if not self.storage.chat_history:
203
- return {"error": "No data available"}
204
- daily_activity = defaultdict(int)
205
- for msg in self.storage.chat_history:
206
- if msg["role"] == "user":
 
 
 
 
207
  date = datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d")
208
  daily_activity[date] += 1
209
- weekly_activity = defaultdict(int)
210
- for msg in self.storage.chat_history:
211
- if msg["role"] == "user":
212
  day = datetime.fromisoformat(msg["timestamp"]).strftime("%A")
213
  weekly_activity[day] += 1
214
- topic_timeline = []
215
- for msg in self.storage.chat_history:
216
- if msg["role"] == "user":
217
  topic_timeline.append({
218
- "date": datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d"),
219
- "topic": msg.get("topic", "general")
 
220
  })
221
- recent_cutoff = datetime.now() - timedelta(days=7)
222
- recent_messages = [
223
- msg for msg in self.storage.chat_history
224
- if datetime.fromisoformat(msg["timestamp"]) > recent_cutoff and msg["role"] == "user"
225
- ]
226
- return {
227
- "daily_activity": dict(sorted(daily_activity.items())),
228
- "weekly_patterns": dict(weekly_activity),
229
- "most_active_day": max(daily_activity.items(), key=lambda x: x[1]) if daily_activity else None,
230
- "recent_activity": len(recent_messages),
231
- "topic_evolution": topic_timeline[-10:],
232
- "consistency_score": self._calculate_consistency(daily_activity)
233
- }
 
 
 
 
 
 
234
 
235
  def _calculate_consistency(self, daily_activity: Dict) -> float:
236
- if len(daily_activity) < 2:
237
- return 0.0
238
- values = list(daily_activity.values())
239
- avg = sum(values) / len(values)
240
- variance = sum((x - avg) ** 2 for x in values) / len(values)
241
- consistency = max(0, 100 - (variance / avg * 10)) if avg > 0 else 0
242
- return round(consistency, 1)
 
 
 
243
 
244
- # Initialize storage and analyzer
245
  storage = ChatStorage()
246
  analyzer = SmartAnalyzer(storage)
 
247
 
248
  def count_tokens_rough(text: str) -> int:
249
- return len(text) // 4
 
 
 
 
 
250
 
251
- def groq_with_memory(message: str, topic: str = "general") -> tuple:
 
252
  if not API_KEY:
253
  return "❌ No API Key found. Please set GROQ_API_KEY environment variable.", ""
 
254
  if not message.strip():
255
  return "❌ Empty message", ""
 
 
 
 
256
  try:
257
  headers = {
258
  "Authorization": f"Bearer {API_KEY}",
259
  "Content-Type": "application/json"
260
  }
 
261
  storage.add_message("user", message.strip(), topic)
 
 
 
262
  messages = []
263
  total_chars = 0
264
  max_chars = 24000
265
- for msg in reversed(storage.chat_history):
266
- msg_content = f"{msg['content']}"
 
267
  msg_chars = len(msg_content)
 
268
  if total_chars + msg_chars < max_chars:
269
  messages.insert(0, {"role": msg["role"], "content": msg["content"]})
270
  total_chars += msg_chars
271
  else:
272
  break
 
273
  if not messages:
274
  messages = [{"role": "user", "content": message.strip()}]
 
275
  payload = {
276
  "model": "gemma2-9b-it",
277
  "messages": messages,
278
  "max_tokens": 2000,
279
  "temperature": 0.7
280
  }
281
- response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
282
- if response.status_code == 200:
283
- result = response.json()
284
- if "choices" in result and result["choices"]:
285
- response_content = result["choices"][0]["message"]["content"]
286
- storage.add_message("assistant", response_content, topic)
287
- return response_content, ""
288
- return f" No response: {result}", ""
289
- return f"❌ HTTP {response.status_code}: {response.text}", ""
 
 
 
 
 
 
 
 
 
 
 
290
  except Exception as e:
291
  return f"❌ Error: {str(e)}", ""
292
 
293
- def get_chat_summary(topic_filter: str = None) -> str:
294
- if not storage.chat_history:
295
- return "❌ No chat history to summarize"
296
- filtered = [msg for msg in storage.chat_history if not topic_filter or msg.get("topic") == topic_filter]
297
- if not filtered:
298
- return f"❌ No messages found for topic: {topic_filter}"
299
- topics = {}
300
- for msg in filtered:
301
- topic = msg.get("topic", "general")
302
- if topic not in topics:
303
- topics[topic] = []
304
- topics[topic].append(msg)
305
- summary = "📋 **Chat Summary**\n\n"
306
- for topic, messages in topics.items():
307
- user_msgs = [m for m in messages if m["role"] == "user"]
308
- ai_msgs = [m for m in messages if m["role"] == "assistant"]
309
- summary += f"**🏷️ Topic: {topic}**\n"
310
- summary += f"- Messages: {len(user_msgs)} user, {len(ai_msgs)} AI\n"
311
- summary += f"- Time span: {messages[0]['timestamp'][:10]} - {messages[-1]['timestamp'][:10]}\n"
312
- summary += f"- Total words: {sum(m['word_count'] for m in user_msgs):,}\n"
313
- if user_msgs:
314
- summary += f"- Key topics discussed: {', '.join(msg['content'][:50] + '...' for msg in user_msgs[:3])}\n"
315
- summary += "\n"
316
- return summary
317
-
318
- def get_full_history(topic_filter: str = None) -> str:
319
- if not storage.chat_history:
320
- return "❌ No chat history available"
321
- filtered = [msg for msg in storage.chat_history if not topic_filter or msg.get("topic") == topic_filter]
322
- if not filtered:
323
- return f"❌ No messages found for topic: {topic_filter}"
324
- history_text = f"📚 **Chat History** ({len(filtered)} messages)\n\n"
325
- current_topic = None
326
- for msg in filtered:
327
- if msg.get("topic") != current_topic:
328
- current_topic = msg.get("topic")
329
- history_text += f"\n**🏷️ Topic: {current_topic}**\n"
330
- history_text += "---\n"
331
- role_icon = "👤" if msg["role"] == "user" else "🤖"
332
- timestamp = msg['timestamp'][:19].replace('T', ' ')
333
- history_text += f"{role_icon} **{timestamp}** ({msg['word_count']} words)\n"
334
- history_text += f"{msg['content']}\n\n"
335
- return history_text
336
-
337
- def get_analytics_report() -> str:
338
- patterns = analyzer.get_conversation_patterns()
339
- moods = analyzer.get_mood_trends()
340
- productivity = analyzer.get_productivity_insights()
341
- if "error" in patterns:
342
- return "❌ No data available for analytics"
343
- report = "📊 **Smart Analytics Report**\n\n"
344
- report += "## 📈 Overview\n"
345
- report += f"- **Total Messages**: {patterns['total_messages']:,}\n"
346
- report += f"- **Your Messages**: {patterns['user_messages']:,}\n"
347
- report += f"- **Average Words per Message**: {patterns['avg_words_per_message']}\n"
348
- report += f"- **Date Range**: {patterns['date_range']['start']} to {patterns['date_range']['end']}\n\n"
349
- report += "## 🏷️ Topic Distribution\n"
350
- for topic, count in sorted(patterns['topics'].items(), key=lambda x: x[1], reverse=True):
351
- percentage = (count / patterns['total_messages']) * 100
352
- report += f"- **{topic}**: {count} messages ({percentage:.1f}%)\n"
353
- report += "\n"
354
- report += "## ⏰ Activity Patterns\n"
355
- if productivity['most_active_day']:
356
- report += f"- **Most Active Day**: {productivity['most_active_day'][0]} ({productivity['most_active_day'][1]} messages)\n"
357
- peak_hours = sorted(patterns['peak_hours'].items(), key=lambda x: x[1], reverse=True)[:3]
358
- report += f"- **Peak Hours**: {', '.join(f'{h}:00 ({c} msgs)' for h, c in peak_hours)}\n"
359
- report += "- **Weekly Activity**:\n"
360
- for day, count in patterns['peak_days'].items():
361
- report += f" - {day}: {count} messages\n"
362
- report += f"- **Consistency Score**: {productivity['consistency_score']}/100\n"
363
- report += f"- **Recent Activity (7 days)**: {productivity['recent_activity']} messages\n\n"
364
- if "error" not in moods:
365
- report += "## 😊 Mood & Sentiment Analysis\n"
366
- overall_sentiment = moods['overall_sentiment']
367
- total_analyzed = sum(overall_sentiment.values())
368
- for sentiment, count in overall_sentiment.most_common():
369
- percentage = (count / total_analyzed) * 100
370
- emoji = {"positive": "😊", "negative": "😔", "curious": "🤔", "neutral": "😐"}.get(sentiment, "❓")
371
- report += f"- **{sentiment.title()}** {emoji}: {count} messages ({percentage:.1f}%)\n"
372
- report += "\n**Sentiment by Topic**:\n"
373
- for topic, sentiments in moods['sentiment_by_topic'].items():
374
- report += f"- **{topic}**: "
375
- sentiment_strs = [f"{sent}({count})" for sent, count in sentiments.items()]
376
- report += ", ".join(sentiment_strs) + "\n"
377
- report += "\n"
378
- report += "## 🔤 Most Common Words\n"
379
- for word, count in patterns['common_words']:
380
- report += f"- **{word}**: {count} times\n"
381
- report += "\n"
382
- if productivity['topic_evolution']:
383
- report += "## 📈 Recent Topic Evolution\n"
384
- for entry in productivity['topic_evolution'][-5:]:
385
- report += f"- {entry['date']}: {entry['topic']}\n"
386
- return report
387
-
388
- def export_data() -> str:
389
- export_data = {
390
- "chat_history": storage.chat_history,
391
- "analytics": storage.analytics,
392
- "export_timestamp": datetime.now().isoformat(),
393
- "total_messages": len(storage.chat_history)
394
- }
395
- return json.dumps(export_data, indent=2, ensure_ascii=False)
396
-
397
- def clear_all_history():
398
- storage.clear_history()
399
- return "✅ All chat history and analytics cleared", "", ""
400
-
401
- def get_topics_list() -> List[str]:
402
- topics = list(set(msg.get("topic", "general") for msg in storage.chat_history))
403
- return ["All Topics"] + sorted(topics)
404
 
405
- def show_current_context():
406
- if not storage.chat_history:
407
- return "❌ No conversation context yet"
408
- messages = []
409
- total_chars = 0
410
- max_chars = 24000
411
- for msg in reversed(storage.chat_history):
412
- msg_chars = len(msg['content'])
413
- if total_chars + msg_chars < max_chars:
414
- messages.insert(0, msg)
415
- total_chars += msg_chars
416
- else:
417
- break
418
- context_text = f"🧠 **Current AI Context** ({len(messages)} messages, ~{total_chars:,} chars)\n\n"
419
- for msg in messages:
420
- role_icon = "👤" if msg["role"] == "user" else "🤖"
421
- timestamp = msg['timestamp'][:19].replace('T', ' ')
422
- context_text += f"{role_icon} **{timestamp}** [{msg.get('topic', 'general')}] ({msg['word_count']} words)\n"
423
- context_text += f"{msg['content'][:150]}{'...' if len(msg['content']) > 150 else ''}\n\n"
424
- context_text += f"\n💡 *AI remembers {len(messages)} messages (~{total_chars:,} characters)*"
425
- context_text += f"\n🔢 *Context limit: {max_chars:,} characters*"
426
- context_text += f"\n💾 *Total stored: {len(storage.chat_history)} messages*"
427
- return context_text
428
 
429
- # Custom CSS for better UI
430
- custom_css = """
431
- .gradio-container {
432
- max-width: 100% !important;
433
- padding: 0 !important;
434
- margin: 0 !important;
435
- }
436
- .main {
437
- max-width: 100% !important;
438
- padding: 10px !important;
439
- }
440
- * {
441
- font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif !important;
442
- }
443
- .response-area textarea {
444
- line-height: 1.7 !important;
445
- padding: 20px !important;
446
- font-size: 15px !important;
447
- }
448
- .history-display textarea {
449
- line-height: 1.7 !important;
450
- padding: 20px !important;
451
- font-size: 15px !important;
452
- }
453
- .input-area textarea {
454
- font-size: 15px !important;
455
- line-height: 1.6 !important;
456
- }
457
- .analytics-display textarea {
458
- line-height: 1.8 !important;
459
- padding: 20px !important;
460
- font-size: 14px !important;
461
- }
462
- """
463
 
464
  # Main Gradio Interface
465
  with gr.Blocks(
 
1
+ import sys
2
+ if sys.version_info < (3, 7):
3
+ raise RuntimeError("This code requires Python 3.7 or higher")
4
+
5
  import gradio as gr
6
  import requests
7
  import os
 
11
  from collections import Counter, defaultdict
12
  import re
13
  from pathlib import Path
14
+ from dataclasses import dataclass, asdict
15
+ import sqlite3
16
+ import threading
17
+ import uuid
18
+ import hashlib
19
+ from time import sleep
20
+ from requests.exceptions import HTTPError
21
 
22
  # Groq API Configuration
23
  API_URL = "https://api.groq.com/openai/v1/chat/completions"
 
28
  STORAGE_DIR.mkdir(exist_ok=True)
29
  HISTORY_FILE = STORAGE_DIR / "chat_history.json"
30
  ANALYTICS_FILE = STORAGE_DIR / "analytics.json"
31
+ DATABASE_FILE = STORAGE_DIR / "chat_database.db"
32
+
33
+ @dataclass
34
+ class ChatMessage:
35
+ """Data class untuk pesan chat"""
36
+ role: str
37
+ content: str
38
+ topic: str
39
+ timestamp: str
40
+ word_count: int
41
+ char_count: int
42
+ session_id: str = "default"
43
+ tags: List[str] = None
44
+ message_id: str = None
45
+
46
+ def __post_init__(self):
47
+ if self.tags is None:
48
+ self.tags = []
49
+ if self.message_id is None:
50
+ self.message_id = str(uuid.uuid4())
51
+
52
+ class DatabaseManager:
53
+ """Mengelola database SQLite untuk pencarian yang lebih efisien"""
54
+
55
+ def __init__(self):
56
+ self.db_path = DATABASE_FILE
57
+ self.lock = threading.Lock()
58
+ self.init_database()
59
+
60
+ def init_database(self):
61
+ """Inisialisasi tabel database"""
62
+ try:
63
+ with sqlite3.connect(self.db_path) as conn:
64
+ conn.execute('''
65
+ CREATE TABLE IF NOT EXISTS messages (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ message_id TEXT UNIQUE,
68
+ role TEXT NOT NULL,
69
+ content TEXT NOT NULL,
70
+ topic TEXT NOT NULL,
71
+ timestamp TEXT NOT NULL,
72
+ word_count INTEGER,
73
+ char_count INTEGER,
74
+ session_id TEXT,
75
+ tags TEXT,
76
+ content_hash TEXT
77
+ )
78
+ ''')
79
+
80
+ conn.execute('''
81
+ CREATE TABLE IF NOT EXISTS sessions (
82
+ session_id TEXT PRIMARY KEY,
83
+ title TEXT,
84
+ created_at TEXT,
85
+ last_activity TEXT,
86
+ message_count INTEGER DEFAULT 0
87
+ )
88
+ ''')
89
+
90
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_content ON messages(content)')
91
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_topic ON messages(topic)')
92
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp)')
93
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_session ON messages(session_id)')
94
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_role ON messages(role)')
95
+
96
+ conn.commit()
97
+ except sqlite3.Error as e:
98
+ raise Exception(f"Failed to initialize database: {str(e)}")
99
+
100
+ def add_message(self, message: ChatMessage):
101
+ """Menambah pesan ke database"""
102
+ try:
103
+ with self.lock:
104
+ content_hash = hashlib.md5(message.content.encode('utf-8')).hexdigest()
105
+ with sqlite3.connect(self.db_path) as conn:
106
+ conn.execute('''
107
+ INSERT OR REPLACE INTO messages
108
+ (message_id, role, content, topic, timestamp, word_count, char_count, session_id, tags, content_hash)
109
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
110
+ ''', (
111
+ message.message_id, message.role, message.content, message.topic,
112
+ message.timestamp, message.word_count, message.char_count,
113
+ message.session_id, json.dumps(message.tags), content_hash
114
+ ))
115
+
116
+ conn.execute('''
117
+ INSERT OR REPLACE INTO sessions (session_id, title, created_at, last_activity, message_count)
118
+ VALUES (?, ?, ?, ?,
119
+ COALESCE((SELECT message_count FROM sessions WHERE session_id = ?), 0) + 1)
120
+ ''', (
121
+ message.session_id,
122
+ message.content[:50] + "..." if len(message.content) > 50 else message.content,
123
+ message.timestamp, message.timestamp, message.session_id
124
+ ))
125
+ conn.commit()
126
+ except sqlite3.Error as e:
127
+ raise Exception(f"Failed to add message to database: {str(e)}")
128
+
129
+ def search_messages(self, query: str, filters: Dict = None) -> List[Dict]:
130
+ """Pencarian pesan dengan berbagai filter"""
131
+ try:
132
+ with sqlite3.connect(self.db_path) as conn:
133
+ conn.row_factory = sqlite3.Row
134
+
135
+ base_query = '''
136
+ SELECT * FROM messages
137
+ WHERE content LIKE ? COLLATE NOCASE
138
+ '''
139
+ params = [f'%{query}%']
140
+
141
+ if filters:
142
+ if filters.get('topic'):
143
+ base_query += ' AND topic = ?'
144
+ params.append(filters['topic'])
145
+ if filters.get('role'):
146
+ base_query += ' AND role = ?'
147
+ params.append(filters['role'])
148
+ if filters.get('session_id'):
149
+ base_query += ' AND session_id = ?'
150
+ params.append(filters['session_id'])
151
+ if filters.get('date_from'):
152
+ base_query += ' AND timestamp >= ?'
153
+ params.append(filters['date_from'])
154
+ if filters.get('date_to'):
155
+ base_query += ' AND timestamp <= ?'
156
+ params.append(filters['date_to'])
157
+
158
+ base_query += ' ORDER BY timestamp DESC LIMIT 100'
159
+
160
+ cursor = conn.execute(base_query, params)
161
+ results = []
162
+ for row in cursor.fetchall():
163
+ result = dict(row)
164
+ result['tags'] = json.loads(result['tags']) if result['tags'] else []
165
+ results.append(result)
166
+
167
+ return results
168
+ except sqlite3.Error as e:
169
+ raise Exception(f"Failed to search messages: {str(e)}")
170
+
171
+ def get_all_messages(self, session_id: str = None) -> List[Dict]:
172
+ """Mengambil semua pesan"""
173
+ try:
174
+ with sqlite3.connect(self.db_path) as conn:
175
+ conn.row_factory = sqlite3.Row
176
+
177
+ if session_id:
178
+ cursor = conn.execute(
179
+ 'SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp',
180
+ (session_id,)
181
+ )
182
+ else:
183
+ cursor = conn.execute('SELECT * FROM messages ORDER BY timestamp')
184
+
185
+ results = []
186
+ for row in cursor.fetchall():
187
+ result = dict(row)
188
+ result['tags'] = json.loads(result['tags']) if result['tags'] else []
189
+ results.append(result)
190
+
191
+ return results
192
+ except sqlite3.Error as e:
193
+ raise Exception(f"Failed to get messages: {str(e)}")
194
+
195
+ def get_sessions(self) -> List[Dict]:
196
+ """Mengambil semua sesi chat"""
197
+ try:
198
+ with sqlite3.connect(self.db_path) as conn:
199
+ conn.row_factory = sqlite3.Row
200
+ cursor = conn.execute('''
201
+ SELECT * FROM sessions
202
+ ORDER BY last_activity DESC
203
+ ''')
204
+ return [dict(row) for row in cursor.fetchall()]
205
+ except sqlite3.Error as e:
206
+ raise Exception(f"Failed to get sessions: {str(e)}")
207
+
208
+ def delete_session(self, session_id: str):
209
+ """Menghapus sesi dan semua pesannya"""
210
+ try:
211
+ with self.lock:
212
+ with sqlite3.connect(self.db_path) as conn:
213
+ conn.execute('DELETE FROM messages WHERE session_id = ?', (session_id,))
214
+ conn.execute('DELETE FROM sessions WHERE session_id = ?', (session_id,))
215
+ conn.commit()
216
+ except sqlite3.Error as e:
217
+ raise Exception(f"Failed to delete session: {str(e)}")
218
+
219
+ def get_statistics(self) -> Dict:
220
+ """Statistik database"""
221
+ try:
222
+ with sqlite3.connect(self.db_path) as conn:
223
+ stats = {}
224
+
225
+ cursor = conn.execute('SELECT COUNT(*) FROM messages')
226
+ stats['total_messages'] = cursor.fetchone()[0]
227
+
228
+ cursor = conn.execute('SELECT role, COUNT(*) FROM messages GROUP BY role')
229
+ stats['by_role'] = dict(cursor.fetchall())
230
+
231
+ cursor = conn.execute('SELECT topic, COUNT(*) FROM messages GROUP BY topic ORDER BY COUNT(*) DESC LIMIT 10')
232
+ stats['by_topic'] = dict(cursor.fetchall())
233
+
234
+ cursor = conn.execute('''
235
+ SELECT DATE(timestamp) as date, COUNT(*)
236
+ FROM messages
237
+ GROUP BY DATE(timestamp)
238
+ ORDER BY date DESC LIMIT 30
239
+ ''')
240
+ stats['by_date'] = dict(cursor.fetchall())
241
+
242
+ cursor = conn.execute('SELECT COUNT(*) FROM sessions')
243
+ stats['total_sessions'] = cursor.fetchone()[0]
244
+
245
+ return stats
246
+ except sqlite3.Error as e:
247
+ raise Exception(f"Failed to get statistics: {str(e)}")
248
 
249
  class ChatStorage:
250
+ """Handles persistent JSON storage dan database untuk chat history"""
251
 
252
  def __init__(self):
253
  self.history_file = HISTORY_FILE
254
  self.analytics_file = ANALYTICS_FILE
255
+ self.db = DatabaseManager()
256
  self.chat_history = self.load_history()
257
  self.analytics = self.load_analytics()
258
+ self.current_session_id = str(uuid.uuid4())
259
+ self.sync_to_database()
260
+
261
+ def sync_to_database(self):
262
+ """Sinkronisasi data dari JSON ke database"""
263
+ try:
264
+ if self.chat_history and not self.db.get_all_messages():
265
+ print("🔄 Syncing JSON data to database...")
266
+ for msg_data in self.chat_history:
267
+ message = ChatMessage(
268
+ role=msg_data.get('role', 'user'),
269
+ content=msg_data.get('content', ''),
270
+ topic=msg_data.get('topic', 'general'),
271
+ timestamp=msg_data.get('timestamp', datetime.now().isoformat()),
272
+ word_count=msg_data.get('word_count', 0),
273
+ char_count=msg_data.get('char_count', 0),
274
+ session_id=msg_data.get('session_id', 'imported'),
275
+ tags=msg_data.get('tags', []),
276
+ message_id=msg_data.get('message_id', str(uuid.uuid4()))
277
+ )
278
+ self.db.add_message(message)
279
+ print("✅ Sync completed")
280
+ except Exception as e:
281
+ print(f"❌ Error syncing to database: {e}")
282
 
283
  def load_history(self) -> List[Dict]:
284
  try:
285
  if self.history_file.exists():
286
+ with open(self.history_file, 'r', encoding='utf-8') as f:
287
  data = json.load(f)
288
+ print(f"📚 Loaded {len(data)} messages from JSON storage")
289
  return data
290
+ return []
291
  except Exception as e:
292
  print(f"❌ Error loading history: {e}")
293
+ return []
294
 
295
  def save_history(self):
296
  try:
297
  with open(self.history_file, 'w', encoding='utf-8') as f:
298
  json.dump(self.chat_history, f, indent=2, ensure_ascii=False)
299
+ print(f"💾 Saved {len(self.chat_history)} messages to JSON storage")
300
  except Exception as e:
301
  print(f"❌ Error saving history: {e}")
302
 
 
305
  if self.analytics_file.exists():
306
  with open(self.analytics_file, 'r', encoding='utf-8') as f:
307
  return json.load(f)
308
+ return {}
309
  except Exception as e:
310
  print(f"❌ Error loading analytics: {e}")
311
+ return {}
312
 
313
  def save_analytics(self):
314
  try:
 
317
  except Exception as e:
318
  print(f"❌ Error saving analytics: {e}")
319
 
320
+ def add_message(self, role: str, content: str, topic: str = "general", tags: List[str] = None):
321
+ """Menambah pesan ke storage dan database"""
322
+ if not content.strip():
323
+ raise ValueError("Content cannot be empty")
324
+ if len(topic) > 100:
325
+ raise ValueError("Topic cannot exceed 100 characters")
326
+ if tags and any(len(tag) > 50 for tag in tags):
327
+ raise ValueError("Tags cannot exceed 50 characters each")
328
+ if role not in ["user", "assistant"]:
329
+ raise ValueError("Role must be 'user' or 'assistant'")
330
+
331
+ message_data = {
332
  "role": role,
333
  "content": content,
334
  "topic": topic,
335
  "timestamp": datetime.now().isoformat(),
336
  "word_count": len(content.split()),
337
+ "char_count": len(content),
338
+ "session_id": self.current_session_id,
339
+ "tags": tags or [],
340
+ "message_id": str(uuid.uuid4())
341
  }
342
+
343
+ try:
344
+ self.chat_history.append(message_data)
345
+ self.save_history()
346
+ message = ChatMessage(**message_data)
347
+ self.db.add_message(message)
348
+ self.update_analytics(message_data)
349
+ except Exception as e:
350
+ if message_data in self.chat_history:
351
+ self.chat_history.remove(message_data)
352
+ self.save_history()
353
+ raise Exception(f"Failed to save message: {str(e)}")
354
+
355
+ def search_messages(self, query: str, filters: Dict = None) -> List[Dict]:
356
+ return self.db.search_messages(query, filters)
357
+
358
+ def get_messages_by_session(self, session_id: str) -> List[Dict]:
359
+ return self.db.get_all_messages(session_id)
360
+
361
+ def get_all_sessions(self) -> List[Dict]:
362
+ return self.db.get_sessions()
363
+
364
+ def create_new_session(self, title: str = None) -> str:
365
+ self.current_session_id = str(uuid.uuid4())
366
+ return self.current_session_id
367
+
368
+ def switch_session(self, session_id: str):
369
+ self.current_session_id = session_id
370
+
371
+ def delete_session(self, session_id: str):
372
+ self.db.delete_session(session_id)
373
+ self.chat_history = [msg for msg in self.chat_history if msg.get('session_id') != session_id]
374
  self.save_history()
 
375
 
376
  def update_analytics(self, message: Dict):
377
+ try:
378
+ today = datetime.now().strftime("%Y-%m-%d")
379
+ if "daily_stats" not in self.analytics:
380
+ self.analytics["daily_stats"] = {}
381
+ if today not in self.analytics["daily_stats"]:
382
+ self.analytics["daily_stats"][today] = {
383
+ "messages": 0,
384
+ "words": 0,
385
+ "chars": 0,
386
+ "topics": []
387
+ }
388
+ stats = self.analytics["daily_stats"][today]
389
+ stats["messages"] += 1
390
+ stats["words"] += message["word_count"]
391
+ stats["chars"] += message["char_count"]
392
+ if message["topic"] not in stats["topics"]:
393
+ stats["topics"].append(message["topic"])
394
+ self.save_analytics()
395
+ except Exception as e:
396
+ print(f"❌ Error updating analytics: {e}")
397
 
398
  def clear_history(self):
399
+ try:
400
+ self.chat_history.clear()
401
+ self.analytics.clear()
402
+ self.save_history()
403
+ self.save_analytics()
404
+ with sqlite3.connect(self.db.db_path) as conn:
405
+ conn.execute('DELETE FROM messages')
406
+ conn.execute('DELETE FROM sessions')
407
+ conn.commit()
408
+ except Exception as e:
409
+ raise Exception(f"Failed to clear history: {str(e)}")
410
+
411
+ def get_database_stats(self) -> Dict:
412
+ return self.db.get_statistics()
413
 
414
  def create_backup(self):
415
+ try:
416
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
417
+ backup_dir = STORAGE_DIR / "backups"
418
+ backup_dir.mkdir(exist_ok=True)
419
+
420
+ for file in [HISTORY_FILE, ANALYTICS_FILE]:
421
+ if file.exists():
422
+ backup_path = backup_dir / f"{file.name}.{timestamp}.bak"
423
+ with open(file, 'r') as src, open(backup_path, 'w') as dst:
424
+ dst.write(src.read())
425
+ if not backup_path.exists():
426
+ raise Exception(f"Failed to create backup for {file.name}")
427
+
428
+ db_backup_path = backup_dir / f"chat_database.db.{timestamp}.bak"
429
+ with sqlite3.connect(self.db.db_path) as src_conn:
430
+ with sqlite3.connect(db_backup_path) as dst_conn:
431
+ src_conn.backup(dst_conn)
432
+ if not db_backup_path.exists():
433
+ raise Exception("Failed to create database backup")
434
+
435
+ return f"✅ Backup created at {backup_dir}"
436
+ except Exception as e:
437
+ return f"❌ Backup failed: {str(e)}"
438
+
439
+ def import_from_json(self, json_path: str):
440
+ try:
441
+ with open(json_path, 'r', encoding='utf-8') as f:
442
+ imported_data = json.load(f)
443
+ for msg_data in imported_data.get('chat_history', []):
444
+ message = ChatMessage(
445
+ role=msg_data.get('role', 'user'),
446
+ content=msg_data.get('content', ''),
447
+ topic=msg_data.get('topic', 'general'),
448
+ timestamp=msg_data.get('timestamp', datetime.now().isoformat()),
449
+ word_count=msg_data.get('word_count', len(msg_data.get('content', '').split())),
450
+ char_count=msg_data.get('char_count', len(msg_data.get('content', ''))),
451
+ session_id=msg_data.get('session_id', 'imported'),
452
+ tags=msg_data.get('tags', []),
453
+ message_id=msg_data.get('message_id', str(uuid.uuid4()))
454
+ )
455
+ self.db.add_message(message)
456
+ self.chat_history.append(asdict(message))
457
+ self.save_history()
458
+ self.update_analytics_from_import(imported_data.get('analytics', {}))
459
+ return f"✅ Imported {len(imported_data.get('chat_history', []))} messages"
460
+ except Exception as e:
461
+ return f"❌ Import failed: {str(e)}"
462
+
463
+ def update_analytics_from_import(self, imported_analytics: Dict):
464
+ try:
465
+ self.analytics.update(imported_analytics)
466
+ self.save_analytics()
467
+ except Exception as e:
468
+ print(f"❌ Error updating analytics from import: {e}")
469
+
470
+ class AdvancedSearchManager:
471
+ """Manager untuk pencarian lanjutan"""
472
+
473
+ def __init__(self, storage: ChatStorage):
474
+ self.storage = storage
475
+
476
+ def search_by_content(self, query: str, exact_match: bool = False) -> List[Dict]:
477
+ try:
478
+ if exact_match:
479
+ filters = {}
480
+ results = self.storage.db.search_messages(query, filters)
481
+ return [r for r in results if query.lower() in r['content'].lower()]
482
+ else:
483
+ return self.storage.search_messages(query)
484
+ except Exception as e:
485
+ raise Exception(f"Failed to search by content: {str(e)}")
486
+
487
+ def search_by_topic(self, topic: str) -> List[Dict]:
488
+ try:
489
+ filters = {'topic': topic}
490
+ return self.storage.search_messages("", filters)
491
+ except Exception as e:
492
+ raise Exception(f"Failed to search by topic: {str(e)}")
493
+
494
+ def search_by_date_range(self, start_date: str, end_date: str) -> List[Dict]:
495
+ try:
496
+ filters = {
497
+ 'date_from': start_date,
498
+ 'date_to': end_date
499
+ }
500
+ return self.storage.search_messages("", filters)
501
+ except Exception as e:
502
+ raise Exception(f"Failed to search by date range: {str(e)}")
503
+
504
+ def search_by_role(self, role: str) -> List[Dict]:
505
+ try:
506
+ filters = {'role': role}
507
+ return self.storage.search_messages("", filters)
508
+ except Exception as e:
509
+ raise Exception(f"Failed to search by role: {str(e)}")
510
+
511
+ def advanced_search(self, query: str = "", topic: str = "", role: str = "",
512
+ date_from: str = "", date_to: str = "", session_id: str = "") -> List[Dict]:
513
+ try:
514
+ filters = {}
515
+ if topic:
516
+ filters['topic'] = topic
517
+ if role:
518
+ filters['role'] = role
519
+ if date_from:
520
+ filters['date_from'] = date_from
521
+ if date_to:
522
+ filters['date_to'] = date_to
523
+ if session_id:
524
+ filters['session_id'] = session_id
525
+ return self.storage.search_messages(query, filters)
526
+ except Exception as e:
527
+ raise Exception(f"Failed to perform advanced search: {str(e)}")
528
+
529
+ def get_similar_messages(self, message_content: str, limit: int = 5) -> List[Dict]:
530
+ try:
531
+ words = re.findall(r'\b\w+\b', message_content.lower())
532
+ common_words = [w for w in words if len(w) > 3]
533
+ similar_messages = []
534
+ for word in common_words[:5]:
535
+ results = self.storage.search_messages(word)
536
+ similar_messages.extend(results)
537
+
538
+ seen = set()
539
+ unique_messages = []
540
+ for msg in similar_messages:
541
+ if msg['message_id'] not in seen:
542
+ seen.add(msg['message_id'])
543
+ unique_messages.append(msg)
544
+ if len(unique_messages) >= limit:
545
+ break
546
+
547
+ return unique_messages
548
+ except Exception as e:
549
+ raise Exception(f"Failed to get similar messages: {str(e)}")
550
+
551
+ def get_conversation_thread(self, message_id: str, context_size: int = 5) -> List[Dict]:
552
+ try:
553
+ all_messages = self.storage.db.get_all_messages()
554
+ target_index = None
555
+
556
+ for i, msg in enumerate(all_messages):
557
+ if msg['message_id'] == message_id:
558
+ target_index = i
559
+ break
560
+
561
+ if target_index is None:
562
+ return []
563
+
564
+ start_index = max(0, target_index - context_size)
565
+ end_index = min(len(all_messages), target_index + context_size + 1)
566
+
567
+ return all_messages[start_index:end_index]
568
+ except Exception as e:
569
+ raise Exception(f"Failed to get conversation thread: {str(e)}")
570
 
571
  class SmartAnalyzer:
572
+ """Advanced analytics untuk chat history dengan database"""
573
 
574
  def __init__(self, storage: ChatStorage):
575
  self.storage = storage
576
 
577
  def get_conversation_patterns(self) -> Dict:
578
+ try:
579
+ stats = self.storage.get_database_stats()
580
+ if stats['total_messages'] == 0:
581
+ return {"error": "No data available"}
582
+
583
+ all_messages = self.storage.db.get_all_messages()
584
+ if not all_messages:
585
+ return {"error": "No data available"}
586
+
587
+ timestamps = [datetime.fromisoformat(msg["timestamp"]) for msg in all_messages]
588
+ hour_counts = Counter([ts.hour for ts in timestamps])
589
+ day_counts = Counter([ts.strftime("%A") for ts in timestamps])
590
+
591
+ user_messages = [msg for msg in all_messages if msg["role"] == "user"]
592
+ all_words = []
593
+ for msg in user_messages:
594
  words = re.findall(r'\b\w+\b', msg["content"].lower())
595
+ all_words.extend([w for w in words if len(w) > 3])
596
+
597
+ common_words = Counter(all_words).most_common(15)
598
+ avg_words = sum(msg["word_count"] for msg in user_messages) / len(user_messages) if user_messages else 0
599
+
600
+ return {
601
+ "total_messages": stats['total_messages'],
602
+ "user_messages": len(user_messages),
603
+ "by_role": stats['by_role'],
604
+ "by_topic": stats['by_topic'],
605
+ "peak_hours": dict(sorted(hour_counts.items())),
606
+ "peak_days": dict(day_counts),
607
+ "common_words": common_words,
608
+ "avg_words_per_message": round(avg_words, 1),
609
+ "date_range": {
610
+ "start": min(timestamps).strftime("%Y-%m-%d") if timestamps else None,
611
+ "end": max(timestamps).strftime("%Y-%m-%d") if timestamps else None
612
+ },
613
+ "total_sessions": stats['total_sessions']
614
  }
615
+ except Exception as e:
616
+ return {"error": f"Failed to get conversation patterns: {str(e)}"}
617
 
618
  def get_mood_trends(self) -> Dict:
619
+ try:
620
+ user_messages = [msg for msg in self.storage.db.get_all_messages() if msg["role"] == "user"]
621
+ if not user_messages:
622
+ return {"error": "No data available"}
623
+
624
+ positive_words = {'good', 'great', 'happy', 'excited', 'amazing', 'wonderful', 'excellent', 'love', 'like', 'enjoy', 'fantastic', 'awesome', 'perfect'}
625
+ negative_words = {'bad', 'sad', 'angry', 'frustrated', 'terrible', 'awful', 'hate', 'dislike', 'worried', 'stress', 'problem', 'issue', 'wrong'}
626
+ question_words = {'what', 'how', 'why', 'when', 'where', 'who', 'which', 'could', 'would', 'should'}
627
+
628
+ sentiments = []
629
+ for msg in user_messages:
630
+ words = set(re.findall(r'\b\w+\b', msg["content"].lower()))
631
+ pos_score = len(words & positive_words)
632
+ neg_score = len(words & negative_words)
633
+ question_score = len(words & question_words)
634
+
635
+ if pos_score > neg_score and pos_score > 0:
636
+ sentiment = "positive"
637
+ elif neg_score > pos_score and neg_score > 0:
638
+ sentiment = "negative"
639
+ elif question_score > 0:
640
+ sentiment = "curious"
641
+ else:
642
+ sentiment = "neutral"
643
+
644
+ sentiments.append({
645
+ "date": datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d"),
646
+ "sentiment": sentiment,
647
+ "topic": msg.get("topic", "general"),
648
+ "session_id": msg.get("session_id", "default")
649
+ })
650
+
651
+ daily_moods = defaultdict(list)
652
+ for item in sentiments:
653
+ daily_moods[item["date"]].append(item["sentiment"])
654
+
655
+ mood_summary = {}
656
+ for date, moods in daily_moods.items():
657
+ mood_counter = Counter(moods)
658
+ dominant_mood = mood_counter.most_common(1)[0][0]
659
+ mood_summary[date] = {
660
+ "dominant": dominant_mood,
661
+ "distribution": dict(mood_counter)
662
+ }
663
+
664
+ return {
665
+ "daily_moods": mood_summary,
666
+ "overall_sentiment": Counter([s["sentiment"] for s in sentiments]),
667
+ "sentiment_by_topic": self._group_sentiment_by_topic(sentiments),
668
+ "sentiment_by_session": self._group_sentiment_by_session(sentiments)
669
  }
670
+ except Exception as e:
671
+ return {"error": f"Failed to get mood trends: {str(e)}"}
 
 
 
672
 
673
  def _group_sentiment_by_topic(self, sentiments: List[Dict]) -> Dict:
674
+ try:
675
+ topic_sentiments = defaultdict(list)
676
+ for item in sentiments:
677
+ topic_sentiments[item["topic"]].append(item["sentiment"])
678
+
679
+ result = {}
680
+ for topic, sent_list in topic_sentiments.items():
681
+ result[topic] = dict(Counter(sent_list))
682
+ return result
683
+ except Exception as e:
684
+ raise Exception(f"Failed to group sentiment by topic: {str(e)}")
685
+
686
+ def _group_sentiment_by_session(self, sentiments: List[Dict]) -> Dict:
687
+ try:
688
+ session_sentiments = defaultdict(list)
689
+ for item in sentiments:
690
+ session_sentiments[item["session_id"]].append(item["sentiment"])
691
+
692
+ result = {}
693
+ for session_id, sent_list in session_sentiments.items():
694
+ result[session_id] = dict(Counter(sent_list))
695
+ return result
696
+ except Exception as e:
697
+ raise Exception(f"Failed to group sentiment by session: {str(e)}")
698
 
699
  def get_productivity_insights(self) -> Dict:
700
+ try:
701
+ all_messages = self.storage.db.get_all_messages()
702
+ user_messages = [msg for msg in all_messages if msg["role"] == "user"]
703
+
704
+ if not user_messages:
705
+ return {"error": "No data available"}
706
+
707
+ daily_activity = defaultdict(int)
708
+ for msg in user_messages:
709
  date = datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d")
710
  daily_activity[date] += 1
711
+
712
+ weekly_activity = defaultdict(int)
713
+ for msg in user_messages:
714
  day = datetime.fromisoformat(msg["timestamp"]).strftime("%A")
715
  weekly_activity[day] += 1
716
+
717
+ topic_timeline = []
718
+ for msg in user_messages[-20:]:
719
  topic_timeline.append({
720
+ "date": datetime.fromisoformat(msg["timestamp"]).strftime("%Y-%m-%d %H:%M"),
721
+ "topic": msg.get("topic", "general"),
722
+ "session_id": msg.get("session_id", "default")
723
  })
724
+
725
+ recent_cutoff = datetime.now() - timedelta(days=7)
726
+ recent_messages = [
727
+ msg for msg in user_messages
728
+ if datetime.fromisoformat(msg["timestamp"]) > recent_cutoff
729
+ ]
730
+
731
+ return {
732
+ "daily_activity": dict(sorted(daily_activity.items())),
733
+ "weekly_patterns": dict(weekly_activity),
734
+ "most_active_day": max(daily_activity.items(), key=lambda x: x[1]) if daily_activity else None,
735
+ "recent_activity": len(recent_messages),
736
+ "topic_evolution": topic_timeline,
737
+ "consistency_score": self._calculate_consistency(daily_activity),
738
+ "total_sessions": len(set(msg.get("session_id", "default") for msg in all_messages)),
739
+ "avg_messages_per_session": len(all_messages) / len(set(msg.get("session_id", "default") for msg in all_messages)) if all_messages else 0
740
+ }
741
+ except Exception as e:
742
+ return {"error": f"Failed to get productivity insights: {str(e)}"}
743
 
744
  def _calculate_consistency(self, daily_activity: Dict) -> float:
745
+ try:
746
+ if len(daily_activity) < 2:
747
+ return 0.0
748
+ values = list(daily_activity.values())
749
+ avg = sum(values) / len(values)
750
+ variance = sum((x - avg) ** 2 for x in values) / len(values)
751
+ consistency = max(0, 100 - (variance / avg * 10)) if avg > 0 else 0
752
+ return round(consistency, 1)
753
+ except Exception as e:
754
+ raise Exception(f"Failed to calculate consistency: {str(e)}")
755
 
756
+ # Initialize storage dan components
757
  storage = ChatStorage()
758
  analyzer = SmartAnalyzer(storage)
759
+ search_manager = AdvancedSearchManager(storage)
760
 
761
  def count_tokens_rough(text: str) -> int:
762
+ """Estimasi token count"""
763
+ try:
764
+ return len(text) // 4
765
+ except Exception as e:
766
+ print(f"❌ Error counting tokens: {e}")
767
+ return 0
768
 
769
+ def groq_with_memory(message: str, topic: str = "general", retries: int = 3) -> tuple:
770
+ """Main chat function dengan Groq API"""
771
  if not API_KEY:
772
  return "❌ No API Key found. Please set GROQ_API_KEY environment variable.", ""
773
+
774
  if not message.strip():
775
  return "❌ Empty message", ""
776
+
777
+ if len(topic) > 100:
778
+ return "❌ Topic cannot exceed 100 characters", ""
779
+
780
  try:
781
  headers = {
782
  "Authorization": f"Bearer {API_KEY}",
783
  "Content-Type": "application/json"
784
  }
785
+
786
  storage.add_message("user", message.strip(), topic)
787
+
788
+ session_messages = storage.get_messages_by_session(storage.current_session_id)
789
+
790
  messages = []
791
  total_chars = 0
792
  max_chars = 24000
793
+
794
+ for msg in reversed(session_messages):
795
+ msg_content = msg['content']
796
  msg_chars = len(msg_content)
797
+
798
  if total_chars + msg_chars < max_chars:
799
  messages.insert(0, {"role": msg["role"], "content": msg["content"]})
800
  total_chars += msg_chars
801
  else:
802
  break
803
+
804
  if not messages:
805
  messages = [{"role": "user", "content": message.strip()}]
806
+
807
  payload = {
808
  "model": "gemma2-9b-it",
809
  "messages": messages,
810
  "max_tokens": 2000,
811
  "temperature": 0.7
812
  }
813
+
814
+ for attempt in range(retries):
815
+ try:
816
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
817
+ response.raise_for_status()
818
+
819
+ result = response.json()
820
+ if "choices" in result and result["choices"]:
821
+ response_content = result["choices"][0]["message"]["content"]
822
+ storage.add_message("assistant", response_content, topic)
823
+ return response_content, ""
824
+ return f"❌ No response: {result}", ""
825
+ except HTTPError as e:
826
+ if response.status_code == 429 and attempt < retries - 1:
827
+ sleep(2 ** attempt) # Exponential backoff
828
+ continue
829
+ return f"❌ HTTP {response.status_code}: {response.text}", ""
830
+
831
+ return "❌ Max retries exceeded", ""
832
+
833
  except Exception as e:
834
  return f"❌ Error: {str(e)}", ""
835
 
836
+ def cleanup_old_messages(days: int = 30) -> str:
837
+ """Hapus pesan yang lebih lama dari jumlah hari tertentu"""
838
+ try:
839
+ cutoff = (datetime.now() - timedelta(days=days)).isoformat()
840
+ with sqlite3.connect(storage.db.db_path) as conn:
841
+ conn.execute('DELETE FROM messages WHERE timestamp < ?', (cutoff,))
842
+ conn.commit()
843
+ storage.chat_history = [msg for msg in storage.chat_history if msg['timestamp'] >= cutoff]
844
+ storage.save_history()
845
+ return f"✅ Cleared messages older than {days} days"
846
+ except Exception as e:
847
+ return f"❌ Failed to clean old messages: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
 
851
  # Main Gradio Interface
852
  with gr.Blocks(