abstractassistant 0.3.2__py3-none-any.whl → 0.3.4__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.
@@ -6,21 +6,363 @@ This script can be run after installation to create a macOS app bundle
6
6
  that allows launching AbstractAssistant from the Dock.
7
7
  """
8
8
 
9
+ import os
9
10
  import sys
11
+ import shutil
12
+ import subprocess
10
13
  from pathlib import Path
11
14
 
15
+ try:
16
+ from PIL import Image
17
+ PIL_AVAILABLE = True
18
+ except ImportError:
19
+ PIL_AVAILABLE = False
20
+
21
+
22
+ class MacOSAppBundleGenerator:
23
+ """Generates macOS app bundles for AbstractAssistant."""
24
+
25
+ def __init__(self, package_dir: Path):
26
+ """Initialize the app bundle generator.
27
+
28
+ Args:
29
+ package_dir: Path to the abstractassistant package directory
30
+ """
31
+ self.package_dir = package_dir
32
+ self.app_name = "AbstractAssistant"
33
+ self.app_bundle_path = Path("/Applications") / f"{self.app_name}.app"
34
+
35
+ def is_macos(self) -> bool:
36
+ """Check if running on macOS."""
37
+ return sys.platform == "darwin"
38
+
39
+ def has_permissions(self) -> bool:
40
+ """Check if we have permissions to write to /Applications."""
41
+ try:
42
+ test_file = Path("/Applications") / ".test_write_permission"
43
+ test_file.touch()
44
+ test_file.unlink()
45
+ return True
46
+ except (PermissionError, OSError):
47
+ return False
48
+
49
+ def create_app_bundle_structure(self) -> bool:
50
+ """Create the basic app bundle directory structure."""
51
+ try:
52
+ # Create main directories
53
+ contents_dir = self.app_bundle_path / "Contents"
54
+ macos_dir = contents_dir / "MacOS"
55
+ resources_dir = contents_dir / "Resources"
56
+
57
+ for directory in [contents_dir, macos_dir, resources_dir]:
58
+ directory.mkdir(parents=True, exist_ok=True)
59
+
60
+ return True
61
+ except Exception as e:
62
+ print(f"Error creating app bundle structure: {e}")
63
+ return False
64
+
65
+ def generate_app_icon(self) -> bool:
66
+ """Generate or preserve the app icon."""
67
+ try:
68
+ icon_path = self.app_bundle_path / "Contents" / "Resources" / "icon.png"
69
+
70
+ # Look for custom icons in multiple locations
71
+ custom_icon_paths = [
72
+ # Project directory bundle (development) - inside package directory
73
+ Path(self.package_dir).parent / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png",
74
+ # Also check if it's in the package directory itself
75
+ Path(self.package_dir) / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png",
76
+ # Any icon.png in the project root
77
+ Path(self.package_dir).parent / "icon.png",
78
+ # Any icon.png in the package directory
79
+ Path(self.package_dir) / "icon.png",
80
+ ]
81
+
82
+ custom_icon_found = False
83
+
84
+ # Try each custom icon location
85
+ for custom_path in custom_icon_paths:
86
+ if custom_path and custom_path.exists():
87
+ print(f"Using existing custom icon from {custom_path}")
88
+ shutil.copy2(str(custom_path), str(icon_path))
89
+ custom_icon_found = True
90
+ break
91
+
92
+ # If no custom icon found, try to restore from git
93
+ if not custom_icon_found:
94
+ try:
95
+ git_icon_path = self.package_dir.parent / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png"
96
+ if git_icon_path.parent.parent.parent.exists(): # Check if AbstractAssistant.app exists
97
+ # Try to get the icon from git
98
+ result = subprocess.run([
99
+ 'git', 'show', 'HEAD:AbstractAssistant.app/Contents/Resources/icon.png'
100
+ ], cwd=str(self.package_dir.parent), capture_output=True)
101
+
102
+ if result.returncode == 0:
103
+ print("Restoring custom icon from git history")
104
+ with open(str(icon_path), 'wb') as f:
105
+ f.write(result.stdout)
106
+ custom_icon_found = True
107
+ except Exception as git_error:
108
+ print(f"Could not restore icon from git: {git_error}")
109
+
110
+ # If still no custom icon, generate one
111
+ if not custom_icon_found:
112
+ print("Generating new icon using IconGenerator")
113
+ # Import the icon generator
114
+ sys.path.insert(0, str(self.package_dir))
115
+ from abstractassistant.utils.icon_generator import IconGenerator
116
+
117
+ # Generate high-resolution icon
118
+ generator = IconGenerator(size=512)
119
+ icon = generator.create_app_icon('blue', animated=False)
120
+
121
+ # Save as PNG
122
+ icon.save(str(icon_path))
123
+
124
+ # Create ICNS file
125
+ return self._create_icns_file(icon_path)
126
+
127
+ except Exception as e:
128
+ print(f"Error generating app icon: {e}")
129
+ return False
130
+
131
+ def _create_icns_file(self, png_path: Path) -> bool:
132
+ """Create ICNS file from PNG using macOS iconutil."""
133
+ try:
134
+ # Create iconset directory
135
+ iconset_dir = png_path.parent / "temp_icons.iconset"
136
+ iconset_dir.mkdir(exist_ok=True)
137
+
138
+ # Load the PNG and create different sizes
139
+ icon = Image.open(png_path)
140
+ sizes = [
141
+ (16, 'icon_16x16.png'),
142
+ (32, 'icon_16x16@2x.png'),
143
+ (32, 'icon_32x32.png'),
144
+ (64, 'icon_32x32@2x.png'),
145
+ (128, 'icon_128x128.png'),
146
+ (256, 'icon_128x128@2x.png'),
147
+ (256, 'icon_256x256.png'),
148
+ (512, 'icon_256x256@2x.png'),
149
+ (512, 'icon_512x512.png'),
150
+ (1024, 'icon_512x512@2x.png')
151
+ ]
152
+
153
+ for size, filename in sizes:
154
+ resized = icon.resize((size, size), Image.Resampling.LANCZOS)
155
+ resized.save(iconset_dir / filename)
156
+
157
+ # Convert to ICNS
158
+ icns_path = png_path.parent / "icon.icns"
159
+ result = subprocess.run([
160
+ 'iconutil', '-c', 'icns', str(iconset_dir),
161
+ '-o', str(icns_path)
162
+ ], capture_output=True, text=True)
163
+
164
+ # Clean up
165
+ shutil.rmtree(iconset_dir)
166
+
167
+ return result.returncode == 0
168
+
169
+ except Exception as e:
170
+ print(f"Error creating ICNS file: {e}")
171
+ return False
172
+
173
+ def create_info_plist(self) -> bool:
174
+ """Create the Info.plist file."""
175
+ try:
176
+ plist_content = '''<?xml version="1.0" encoding="UTF-8"?>
177
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
178
+ <plist version="1.0">
179
+ <dict>
180
+ <key>CFBundleExecutable</key>
181
+ <string>AbstractAssistant</string>
182
+ <key>CFBundleIdentifier</key>
183
+ <string>ai.abstractcore.abstractassistant</string>
184
+ <key>CFBundleName</key>
185
+ <string>AbstractAssistant</string>
186
+ <key>CFBundleDisplayName</key>
187
+ <string>AbstractAssistant</string>
188
+ <key>CFBundleVersion</key>
189
+ <string>0.3.2</string>
190
+ <key>CFBundleShortVersionString</key>
191
+ <string>0.3.2</string>
192
+ <key>CFBundlePackageType</key>
193
+ <string>APPL</string>
194
+ <key>CFBundleSignature</key>
195
+ <string>????</string>
196
+ <key>CFBundleIconFile</key>
197
+ <string>icon.icns</string>
198
+ <key>LSMinimumSystemVersion</key>
199
+ <string>10.15</string>
200
+ <key>NSHighResolutionCapable</key>
201
+ <true/>
202
+ <key>NSRequiresAquaSystemAppearance</key>
203
+ <false/>
204
+ <key>LSUIElement</key>
205
+ <true/>
206
+ <key>NSAppleScriptEnabled</key>
207
+ <false/>
208
+ <key>CFBundleDocumentTypes</key>
209
+ <array/>
210
+ <key>NSPrincipalClass</key>
211
+ <string>NSApplication</string>
212
+ </dict>
213
+ </plist>'''
214
+
215
+ plist_path = self.app_bundle_path / "Contents" / "Info.plist"
216
+ plist_path.write_text(plist_content)
217
+ return True
218
+
219
+ except Exception as e:
220
+ print(f"Error creating Info.plist: {e}")
221
+ return False
222
+
223
+ def create_launch_script(self) -> bool:
224
+ """Create the executable launch script."""
225
+ try:
226
+ script_content = '''#!/bin/bash
227
+
228
+ # AbstractAssistant macOS App Launcher
229
+ # This script launches the AbstractAssistant application
230
+
231
+ # Set up environment paths for GUI launch (common locations)
232
+ export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$PATH"
233
+
234
+ # Add user-specific Python paths if they exist
235
+ if [ -d "$HOME/.pyenv/shims" ]; then
236
+ export PATH="$HOME/.pyenv/shims:$PATH"
237
+ fi
238
+
239
+ if [ -d "$HOME/.local/bin" ]; then
240
+ export PATH="$HOME/.local/bin:$PATH"
241
+ fi
242
+
243
+ if [ -d "/opt/anaconda3/bin" ]; then
244
+ export PATH="/opt/anaconda3/bin:$PATH"
245
+ fi
246
+
247
+ if [ -d "$HOME/anaconda3/bin" ]; then
248
+ export PATH="$HOME/anaconda3/bin:$PATH"
249
+ fi
250
+
251
+ # Function to find Python with abstractassistant installed
252
+ find_python_with_abstractassistant() {
253
+ # First try pyenv's active Python (most reliable)
254
+ if [ -x "$HOME/.pyenv/shims/python3" ]; then
255
+ if "$HOME/.pyenv/shims/python3" -c "import abstractassistant" 2>/dev/null; then
256
+ echo "$HOME/.pyenv/shims/python3"
257
+ return 0
258
+ fi
259
+ fi
260
+
261
+ # Try PATH-based search (respects current environment)
262
+ for python_cmd in python3 python python3.13 python3.12 python3.11 python3.10 python3.9; do
263
+ if command -v "$python_cmd" >/dev/null 2>&1; then
264
+ if "$python_cmd" -c "import abstractassistant" 2>/dev/null; then
265
+ echo "$python_cmd"
266
+ return 0
267
+ fi
268
+ fi
269
+ done
270
+
271
+ # Try specific pyenv versions (sorted by version number, newest first)
272
+ for version_dir in $(ls -1v "$HOME/.pyenv/versions/" 2>/dev/null | sort -V -r); do
273
+ py="$HOME/.pyenv/versions/$version_dir/bin/python3"
274
+ if [ -x "$py" ] && "$py" -c "import abstractassistant" 2>/dev/null; then
275
+ echo "$py"
276
+ return 0
277
+ fi
278
+ done
279
+
280
+ # Try other common locations
281
+ for python_path in \\
282
+ "/usr/local/bin/python3" \\
283
+ "/opt/homebrew/bin/python3" \\
284
+ "/usr/bin/python3" \\
285
+ "/opt/anaconda3/bin/python" \\
286
+ "$HOME/anaconda3/bin/python" \\
287
+ "/usr/local/anaconda3/bin/python"; do
288
+
289
+ if [ -x "$python_path" ] && "$python_path" -c "import abstractassistant" 2>/dev/null; then
290
+ echo "$python_path"
291
+ return 0
292
+ fi
293
+ done
294
+
295
+ return 1
296
+ }
297
+
298
+ # Find Python with AbstractAssistant
299
+ PYTHON_EXEC=$(find_python_with_abstractassistant)
300
+
301
+ if [ -z "$PYTHON_EXEC" ]; then
302
+ osascript -e 'display dialog "AbstractAssistant not found in any Python installation.\\n\\nPlease install it with:\\npip install abstractassistant\\n\\nOr run the create-app-bundle command after installation." with title "AbstractAssistant" buttons {"OK"} default button "OK" with icon caution'
303
+ exit 1
304
+ fi
305
+
306
+ # Change to a neutral directory to avoid importing development versions
307
+ cd /tmp
308
+
309
+ # Launch the assistant
310
+ exec "$PYTHON_EXEC" -m abstractassistant.cli "$@"'''
311
+
312
+ script_path = self.app_bundle_path / "Contents" / "MacOS" / "AbstractAssistant"
313
+ script_path.write_text(script_content)
314
+
315
+ # Make executable
316
+ os.chmod(script_path, 0o755)
317
+ return True
318
+
319
+ except Exception as e:
320
+ print(f"Error creating launch script: {e}")
321
+ return False
322
+
323
+ def generate_app_bundle(self) -> bool:
324
+ """Generate the complete macOS app bundle."""
325
+ if not self.is_macos():
326
+ print("macOS app bundle generation is only available on macOS")
327
+ return False
328
+
329
+ if not self.has_permissions():
330
+ print("Insufficient permissions to create app bundle in /Applications")
331
+ print("Please run with sudo or manually copy the app bundle")
332
+ return False
333
+
334
+ print("Creating macOS app bundle...")
335
+
336
+ # Remove existing bundle if it exists
337
+ if self.app_bundle_path.exists():
338
+ shutil.rmtree(self.app_bundle_path)
339
+
340
+ # Create bundle structure
341
+ if not self.create_app_bundle_structure():
342
+ return False
343
+
344
+ # Generate icon
345
+ if not self.generate_app_icon():
346
+ return False
347
+
348
+ # Create Info.plist
349
+ if not self.create_info_plist():
350
+ return False
351
+
352
+ # Create launch script
353
+ if not self.create_launch_script():
354
+ return False
355
+
356
+ print(f"✅ macOS app bundle created successfully!")
357
+ print(f" Location: {self.app_bundle_path}")
358
+ print(f" You can now launch AbstractAssistant from the Dock!")
359
+
360
+ return True
361
+
12
362
 
13
363
  def main():
14
364
  """Create macOS app bundle for AbstractAssistant."""
15
365
  try:
16
- # Import the app bundle generator
17
- try:
18
- import setup_macos_app
19
- MacOSAppBundleGenerator = setup_macos_app.MacOSAppBundleGenerator
20
- except ImportError:
21
- # Fallback: try importing from abstractassistant package
22
- from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
23
-
24
366
  # Find the package directory
25
367
  import abstractassistant
26
368
  package_dir = Path(abstractassistant.__file__).parent
@@ -121,8 +121,10 @@ class ChatBubble:
121
121
  )
122
122
  self.text_input.pack(fill="both", expand=True)
123
123
 
124
- # Bind Enter key for sending (Cmd+Enter on macOS)
125
- self.text_input.bind("<Command-Return>", lambda e: self._send_message())
124
+ # Bind keyboard shortcuts for message sending
125
+ # Enter = send message, Shift+Enter = new line
126
+ self.text_input.bind("<Return>", self._handle_enter_key)
127
+ self.text_input.bind("<KP_Enter>", self._handle_enter_key) # Numpad Enter
126
128
 
127
129
  # Controls frame
128
130
  controls_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
@@ -271,6 +273,17 @@ class ChatBubble:
271
273
  text_color=status_colors.get(status, "gray")
272
274
  )
273
275
 
276
+ def _handle_enter_key(self, event):
277
+ """Handle Enter key press in text input."""
278
+ # Check if Shift is held down
279
+ if event.state & 0x1: # Shift modifier
280
+ # Shift+Enter: Allow default behavior (new line)
281
+ return None
282
+ else:
283
+ # Plain Enter: Send message
284
+ self._send_message()
285
+ return "break" # Prevent default behavior
286
+
274
287
  def _send_message(self):
275
288
  """Send the current message."""
276
289
  if self.is_sending or self.text_input is None: