abstractassistant 0.2.0__py3-none-any.whl → 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -242,14 +242,15 @@ class LLMManager:
242
242
  self.current_model = model
243
243
  self._initialize_llm()
244
244
 
245
- def generate_response(self, message: str, provider: str = None, model: str = None) -> str:
245
+ def generate_response(self, message: str, provider: str = None, model: str = None, media: Optional[List[str]] = None) -> str:
246
246
  """Generate a response using the session for context persistence.
247
-
247
+
248
248
  Args:
249
249
  message: User message
250
250
  provider: Optional provider override
251
251
  model: Optional model override
252
-
252
+ media: Optional list of file paths to attach (images, PDFs, Office docs, etc.)
253
+
253
254
  Returns:
254
255
  Generated response text
255
256
  """
@@ -258,24 +259,27 @@ class LLMManager:
258
259
  self.set_provider(provider, model)
259
260
  elif model and model != self.current_model:
260
261
  self.set_model(model)
261
-
262
+
262
263
  try:
263
264
  # Ensure we have a session
264
265
  if self.current_session is None:
265
266
  self.create_new_session()
266
-
267
- # Generate response using session - CLEAN AND SIMPLE
268
- # response = session.generate('What is my name?') # Remembers context
269
- response = self.current_session.generate(message)
270
-
267
+
268
+ # Generate response using session with optional media files
269
+ # AbstractCore 2.4.5+ supports media=[] parameter for file attachments
270
+ if media and len(media) > 0:
271
+ response = self.current_session.generate(message, media=media)
272
+ else:
273
+ response = self.current_session.generate(message)
274
+
271
275
  # Handle response format
272
276
  if hasattr(response, 'content'):
273
277
  response_text = response.content
274
278
  else:
275
279
  response_text = str(response)
276
-
280
+
277
281
  return response_text
278
-
282
+
279
283
  except Exception as e:
280
284
  return f"Error generating response: {str(e)}"
281
285
 
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Create macOS App Bundle for AbstractAssistant.
4
+
5
+ This script can be run after installation to create a macOS app bundle
6
+ that allows launching AbstractAssistant from the Dock.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ def main():
14
+ """Create macOS app bundle for AbstractAssistant."""
15
+ try:
16
+ # Import the app bundle generator
17
+ from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
18
+
19
+ # Find the package directory
20
+ import abstractassistant
21
+ package_dir = Path(abstractassistant.__file__).parent
22
+
23
+ # Create the generator and build the app bundle
24
+ generator = MacOSAppBundleGenerator(package_dir)
25
+
26
+ print("🍎 Creating macOS app bundle for AbstractAssistant...")
27
+ success = generator.generate_app_bundle()
28
+
29
+ if success:
30
+ print("\n🎉 Success!")
31
+ print(" AbstractAssistant is now available in your Applications folder")
32
+ print(" You can launch it from the Dock or Spotlight!")
33
+ return 0
34
+ else:
35
+ print("\n❌ Failed to create app bundle")
36
+ return 1
37
+
38
+ except ImportError as e:
39
+ print(f"❌ Error: {e}")
40
+ print(" Make sure AbstractAssistant is properly installed")
41
+ return 1
42
+ except Exception as e:
43
+ print(f"❌ Unexpected error: {e}")
44
+ return 1
45
+
46
+
47
+ if __name__ == "__main__":
48
+ sys.exit(main())
@@ -6,23 +6,99 @@ This module provides an authentic iPhone Messages UI for displaying chat history
6
6
  import re
7
7
  from datetime import datetime
8
8
  from typing import Dict, List
9
+ import markdown
10
+ from markdown.extensions.fenced_code import FencedCodeExtension
11
+ from markdown.extensions.tables import TableExtension
12
+ from markdown.extensions.nl2br import Nl2BrExtension
13
+ from pygments import highlight
14
+ from pygments.lexers import get_lexer_by_name, TextLexer
15
+ from pygments.formatters import HtmlFormatter
9
16
 
10
17
  try:
11
18
  from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
12
- QWidget, QLabel, QFrame, QPushButton)
13
- from PyQt6.QtCore import Qt
14
- from PyQt6.QtGui import QFont
19
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
20
+ from PyQt6.QtCore import Qt, QTimer, pyqtSignal
21
+ from PyQt6.QtGui import QFont, QCursor
15
22
  except ImportError:
16
23
  try:
17
24
  from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
18
- QWidget, QLabel, QFrame, QPushButton)
19
- from PyQt5.QtCore import Qt
20
- from PyQt5.QtGui import QFont
25
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
26
+ from PyQt5.QtCore import Qt, QTimer, pyqtSignal
27
+ from PyQt5.QtGui import QFont, QCursor
21
28
  except ImportError:
22
29
  from PySide2.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
23
- QWidget, QLabel, QFrame, QPushButton)
24
- from PySide2.QtCore import Qt
25
- from PySide2.QtGui import QFont
30
+ QWidget, QLabel, QFrame, QPushButton, QApplication)
31
+ from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal
32
+ from PySide2.QtGui import QFont, QCursor
33
+
34
+
35
+ class ClickableBubble(QFrame):
36
+ """Clickable message bubble that copies content to clipboard."""
37
+
38
+ clicked = pyqtSignal()
39
+
40
+ def __init__(self, content: str, is_user: bool, parent=None):
41
+ super().__init__(parent)
42
+ self.content = content
43
+ self.is_user = is_user
44
+ self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
45
+
46
+ # Store original colors for animation
47
+ if is_user:
48
+ self.normal_bg = "#007AFF"
49
+ self.clicked_bg = "#0066CC"
50
+ else:
51
+ self.normal_bg = "#3a3a3c"
52
+ self.clicked_bg = "#4a4a4c"
53
+
54
+ def mousePressEvent(self, event):
55
+ """Handle mouse press with visual feedback."""
56
+ if event.button() == Qt.MouseButton.LeftButton:
57
+ # Apply clicked style (darker)
58
+ self.setStyleSheet(f"""
59
+ QFrame {{
60
+ background: {self.clicked_bg};
61
+ border: none;
62
+ border-radius: 18px;
63
+ max-width: 400px;
64
+ }}
65
+ """)
66
+ super().mousePressEvent(event)
67
+
68
+ def mouseReleaseEvent(self, event):
69
+ """Handle mouse release - copy to clipboard and restore style."""
70
+ if event.button() == Qt.MouseButton.LeftButton:
71
+ # Copy to clipboard
72
+ clipboard = QApplication.clipboard()
73
+ clipboard.setText(self.content)
74
+
75
+ # Visual feedback: glossy effect (lighter color briefly)
76
+ glossy_color = "#0080FF" if self.is_user else "#5a5a5c"
77
+ self.setStyleSheet(f"""
78
+ QFrame {{
79
+ background: {glossy_color};
80
+ border: none;
81
+ border-radius: 18px;
82
+ max-width: 400px;
83
+ }}
84
+ """)
85
+
86
+ # Restore normal color after brief delay
87
+ QTimer.singleShot(200, self._restore_normal_style)
88
+
89
+ self.clicked.emit()
90
+ super().mouseReleaseEvent(event)
91
+
92
+ def _restore_normal_style(self):
93
+ """Restore normal bubble style."""
94
+ self.setStyleSheet(f"""
95
+ QFrame {{
96
+ background: {self.normal_bg};
97
+ border: none;
98
+ border-radius: 18px;
99
+ max-width: 400px;
100
+ }}
101
+ """)
26
102
 
27
103
 
28
104
  class SafeDialog(QDialog):
@@ -97,6 +173,9 @@ class iPhoneMessagesDialog:
97
173
  # Apply authentic iPhone styling
98
174
  dialog.setStyleSheet(iPhoneMessagesDialog._get_authentic_iphone_styles())
99
175
 
176
+ # Auto-scroll to bottom to show the latest messages
177
+ QTimer.singleShot(100, lambda: scroll_area.verticalScrollBar().setValue(scroll_area.verticalScrollBar().maximum()))
178
+
100
179
  return dialog
101
180
 
102
181
  @staticmethod
@@ -161,7 +240,7 @@ class iPhoneMessagesDialog:
161
240
  background: transparent;
162
241
  border: none;
163
242
  text-align: left;
164
- font-family: -apple-system;
243
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
165
244
  }
166
245
  """)
167
246
  nav_layout.addWidget(back_btn)
@@ -175,7 +254,7 @@ class iPhoneMessagesDialog:
175
254
  color: #ffffff;
176
255
  font-size: 17px;
177
256
  font-weight: 600;
178
- font-family: -apple-system;
257
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
179
258
  }
180
259
  """)
181
260
  nav_layout.addWidget(title)
@@ -216,20 +295,20 @@ class iPhoneMessagesDialog:
216
295
  container = QFrame()
217
296
  container.setStyleSheet("background: transparent; border: none;")
218
297
  layout = QHBoxLayout(container)
219
- layout.setContentsMargins(16, 0, 16, 0) # iPhone margins
298
+ layout.setContentsMargins(12, 0, 12, 0) # Tighter margins for more width
220
299
  layout.setSpacing(0)
221
300
 
222
- # Create bubble
223
- bubble = QFrame()
301
+ # Create clickable bubble
302
+ bubble = ClickableBubble(msg['content'], is_user)
224
303
  bubble_layout = QVBoxLayout(bubble)
225
- bubble_layout.setContentsMargins(13, 8, 13, 8) # iPhone padding
304
+ bubble_layout.setContentsMargins(12, 7, 12, 7) # More compact padding
226
305
  bubble_layout.setSpacing(0)
227
306
 
228
307
  # Process content with FULL markdown support
229
308
  content = iPhoneMessagesDialog._process_full_markdown(msg['content'])
230
309
  content_label = QLabel(content)
231
310
  content_label.setWordWrap(True)
232
- content_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
311
+ content_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) # No text selection, bubble handles clicks
233
312
  content_label.setTextFormat(Qt.TextFormat.RichText)
234
313
 
235
314
  if is_user:
@@ -239,17 +318,17 @@ class iPhoneMessagesDialog:
239
318
  background: #007AFF;
240
319
  border: none;
241
320
  border-radius: 18px;
242
- max-width: 320px;
321
+ max-width: 400px;
243
322
  }
244
323
  """)
245
324
  content_label.setStyleSheet("""
246
325
  QLabel {
247
326
  background: transparent;
248
327
  color: #FFFFFF;
249
- font-size: 17px;
328
+ font-size: 14px;
250
329
  font-weight: 400;
251
- line-height: 22px;
252
- font-family: -apple-system;
330
+ line-height: 18px;
331
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
253
332
  }
254
333
  """)
255
334
  # Right align
@@ -262,17 +341,17 @@ class iPhoneMessagesDialog:
262
341
  background: #3a3a3c;
263
342
  border: none;
264
343
  border-radius: 18px;
265
- max-width: 320px;
344
+ max-width: 400px;
266
345
  }
267
346
  """)
268
347
  content_label.setStyleSheet("""
269
348
  QLabel {
270
349
  background: transparent;
271
350
  color: #ffffff;
272
- font-size: 17px;
351
+ font-size: 14px;
273
352
  font-weight: 400;
274
- line-height: 22px;
275
- font-family: -apple-system;
353
+ line-height: 18px;
354
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
276
355
  }
277
356
  """)
278
357
  # Left align
@@ -315,7 +394,7 @@ class iPhoneMessagesDialog:
315
394
  font-size: 13px;
316
395
  font-weight: 400;
317
396
  color: rgba(255, 255, 255, 0.6);
318
- font-family: -apple-system;
397
+ font-family: "SF Pro Text", "Helvetica Neue", sans-serif;
319
398
  padding: 0px;
320
399
  }
321
400
  """)
@@ -340,29 +419,50 @@ class iPhoneMessagesDialog:
340
419
 
341
420
  @staticmethod
342
421
  def _process_full_markdown(text: str) -> str:
343
- """Process markdown formatting for iPhone Messages display."""
344
- # Convert **bold** to <strong>bold</strong>
345
- text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text)
422
+ """Process markdown using proper markdown library with syntax highlighting."""
423
+ # Configure markdown with extensions
424
+ md = markdown.Markdown(
425
+ extensions=[
426
+ FencedCodeExtension(),
427
+ TableExtension(),
428
+ 'nl2br', # Convert newlines to <br>
429
+ ],
430
+ extension_configs={
431
+ 'fenced_code': {
432
+ 'lang_prefix': 'language-',
433
+ }
434
+ }
435
+ )
436
+
437
+ # Convert markdown to HTML
438
+ html = md.convert(text)
346
439
 
347
- # Convert *italic* to <em>italic</em>
348
- text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text)
440
+ # Apply custom styling to the generated HTML
441
+ # Style code blocks
442
+ html = html.replace('<pre>', '<pre style="margin: 6px 0; background: rgba(0,0,0,0.3); border-radius: 6px; padding: 8px; overflow-x: auto;">')
443
+ html = html.replace('<code>', '<code style="font-family: \'SF Mono\', \'Menlo\', \'Monaco\', \'Courier New\', monospace; font-size: 12px; line-height: 1.4; color: #e8e8e8;">')
349
444
 
350
- # Convert `code` to inline code
351
- text = re.sub(r'`([^`]+)`', r'<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px; font-family: monospace;">\1</code>', text)
445
+ # Style tables
446
+ html = html.replace('<table>', '<table style="margin: 6px 0; border-collapse: collapse; width: 100%; font-size: 12px;">')
447
+ html = html.replace('<thead>', '<thead style="background: rgba(0,0,0,0.2);">')
448
+ html = html.replace('<th>', '<th style="padding: 4px 8px; text-align: left; font-weight: 600; border-bottom: 1px solid rgba(255,255,255,0.2);">')
449
+ html = html.replace('<td>', '<td style="padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.1);">')
352
450
 
353
- # Convert headers
354
- text = re.sub(r'^#### (.*$)', r'<h4 style="margin: 8px 0 4px 0; font-weight: 600;">\1</h4>', text, flags=re.MULTILINE)
355
- text = re.sub(r'^### (.*$)', r'<h3 style="margin: 10px 0 5px 0; font-weight: 600;">\1</h3>', text, flags=re.MULTILINE)
356
- text = re.sub(r'^## (.*$)', r'<h2 style="margin: 12px 0 6px 0; font-weight: 600;">\1</h2>', text, flags=re.MULTILINE)
357
- text = re.sub(r'^# (.*$)', r'<h1 style="margin: 14px 0 7px 0; font-weight: 600;">\1</h1>', text, flags=re.MULTILINE)
451
+ # Style headers with minimal spacing
452
+ html = html.replace('<h1>', '<h1 style="margin: 6px 0 2px 0; font-weight: 600; font-size: 17px;">')
453
+ html = html.replace('<h2>', '<h2 style="margin: 5px 0 2px 0; font-weight: 600; font-size: 16px;">')
454
+ html = html.replace('<h3>', '<h3 style="margin: 4px 0 1px 0; font-weight: 600; font-size: 15px;">')
455
+ html = html.replace('<h4>', '<h4 style="margin: 3px 0 1px 0; font-weight: 600; font-size: 14px;">')
358
456
 
359
- # Convert bullet points
360
- text = re.sub(r'^[•\-\*] (.*)$', r'<p style="margin: 2px 0; padding-left: 16px;">• \1</p>', text, flags=re.MULTILINE)
457
+ # Style lists with minimal spacing
458
+ html = html.replace('<ul>', '<ul style="margin: 4px 0; padding-left: 20px;">')
459
+ html = html.replace('<ol>', '<ol style="margin: 4px 0; padding-left: 20px;">')
460
+ html = html.replace('<li>', '<li style="margin: 1px 0; line-height: 1.3;">')
361
461
 
362
- # Convert line breaks to HTML
363
- text = text.replace('\n', '<br>')
462
+ # Style paragraphs with minimal spacing
463
+ html = html.replace('<p>', '<p style="margin: 2px 0; line-height: 1.3;">')
364
464
 
365
- return text
465
+ return html
366
466
 
367
467
  @staticmethod
368
468
  def _get_authentic_iphone_styles() -> str: