abstractassistant 0.3.4__tar.gz → 0.4.0__tar.gz

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.
Files changed (47) hide show
  1. abstractassistant-0.4.0/ACKNOWLEDGMENTS.md +34 -0
  2. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/CHANGELOG.md +28 -0
  3. abstractassistant-0.4.0/PKG-INFO +168 -0
  4. abstractassistant-0.4.0/README.md +123 -0
  5. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/app.py +69 -6
  6. abstractassistant-0.4.0/abstractassistant/cli.py +170 -0
  7. abstractassistant-0.4.0/abstractassistant/core/agent_host.py +583 -0
  8. abstractassistant-0.4.0/abstractassistant/core/llm_manager.py +382 -0
  9. abstractassistant-0.4.0/abstractassistant/core/session_index.py +293 -0
  10. abstractassistant-0.4.0/abstractassistant/core/session_store.py +79 -0
  11. abstractassistant-0.4.0/abstractassistant/core/tool_policy.py +58 -0
  12. abstractassistant-0.4.0/abstractassistant/core/transcript_summary.py +434 -0
  13. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/history_dialog.py +504 -29
  14. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/provider_manager.py +2 -2
  15. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/qt_bubble.py +2289 -489
  16. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant.egg-info/SOURCES.txt +7 -2
  17. abstractassistant-0.4.0/docs/INSTALLATION.md +37 -0
  18. abstractassistant-0.4.0/docs/architecture.md +181 -0
  19. abstractassistant-0.4.0/docs/getting-started.md +125 -0
  20. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/pyproject.toml +26 -8
  21. abstractassistant-0.3.4/ACKNOWLEDGMENTS.md +0 -164
  22. abstractassistant-0.3.4/PKG-INFO +0 -297
  23. abstractassistant-0.3.4/README.md +0 -255
  24. abstractassistant-0.3.4/abstractassistant/cli.py +0 -151
  25. abstractassistant-0.3.4/abstractassistant/core/llm_manager.py +0 -475
  26. abstractassistant-0.3.4/docs/architecture.md +0 -317
  27. abstractassistant-0.3.4/docs/getting-started.md +0 -394
  28. abstractassistant-0.3.4/docs/installation.md +0 -464
  29. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/CONTRIBUTING.md +0 -0
  30. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/LICENSE +0 -0
  31. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/MANIFEST.in +0 -0
  32. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/__init__.py +0 -0
  33. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/config.py +0 -0
  34. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/core/__init__.py +0 -0
  35. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/core/tts_manager.py +0 -0
  36. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/create_app_bundle.py +0 -0
  37. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/__init__.py +0 -0
  38. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/chat_bubble.py +0 -0
  39. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/toast_manager.py +0 -0
  40. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/toast_window.py +0 -0
  41. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/tts_state_manager.py +0 -0
  42. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/ui/ui_styles.py +0 -0
  43. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/utils/__init__.py +0 -0
  44. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/utils/icon_generator.py +0 -0
  45. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/utils/markdown_renderer.py +0 -0
  46. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/abstractassistant/web_server.py +0 -0
  47. {abstractassistant-0.3.4 → abstractassistant-0.4.0}/setup.cfg +0 -0
@@ -0,0 +1,34 @@
1
+ # Acknowledgments
2
+
3
+ AbstractAssistant is built on top of a number of open-source projects. We’re grateful to all maintainers and contributors.
4
+
5
+ This list is not exhaustive. The source of truth for install-time dependencies is `pyproject.toml`.
6
+
7
+ ## Core libraries
8
+
9
+ Agent/runtime foundations:
10
+ - **AbstractAgent** — agent loop + tool calling interface
11
+ - **AbstractRuntime** — durable runs/session storage + tool execution boundary
12
+ - **AbstractCore** — provider-agnostic LLM interface (`create_llm`) and provider/model utilities
13
+
14
+ UI and desktop integration:
15
+ - **PyQt5** — native UI framework
16
+ - **pystray** — menu bar / system tray integration
17
+ - **Pillow** — tray icon rendering and image utilities
18
+
19
+ Rendering and UX helpers:
20
+ - **markdown**, **pymdown-extensions** — Markdown rendering
21
+ - **Pygments** — syntax highlighting
22
+ - **pyperclip** — clipboard integration
23
+ - **plyer** — native notifications
24
+
25
+ Configuration:
26
+ - **tomli** / **tomli-w** — TOML parsing/writing (compatibility across Python versions)
27
+
28
+ ## Optional extras
29
+
30
+ - **AbstractVoice** — voice/audio capabilities (installed via `abstractassistant[full]` when enabled)
31
+
32
+ ## Development tooling
33
+
34
+ - **pytest**, **black**, **isort**, **mypy**
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to AbstractAssistant will be documented in this file.
4
4
 
5
+ ## [0.4.0]
6
+
7
+ ### Added
8
+ - **Agent host + durability**: new core modules for agent runs and durable state: `agent_host`, `session_index`, `session_store`, `tool_policy`, `transcript_summary`.
9
+ - **CLI**: `assistant run` for one-turn agentic runs in terminal; `assistant tray` as the explicit tray-mode entry.
10
+ - **Tests**: added basic + integration coverage for the new host/session/tool-policy/summarization components.
11
+ - **Reports**: added research notes under `reports/` (dated 2026-02-04).
12
+
13
+ ### Changed
14
+ - **Docs & README**: rewritten/updated to reflect the agentic host model, tool approval boundary, and install profiles.
15
+ - **Dependencies**: updated `pyproject.toml` (profiles/requirements refined).
16
+ - **Qt bubble UI**: significant updates to `qt_bubble.py` (input/action controls, tool allowlist UI, voice-mode behaviors, theming).
17
+
18
+ ### Fixed
19
+ - **Qt chat bubble input actions**: Fixed first-open layout race that caused the right-side action buttons to overlap. The action column now deterministically sizes to the input row, keeps strict 1:1 square buttons, and preserves 1px vertical spacing.
20
+ - **Tray app shutdown**: Ctrl+C (SIGINT) and SIGTERM now trigger a clean Qt shutdown path (stop timers, hide tray icon, destroy bubble) instead of an abrupt interpreter exit.
21
+ - **Voice toggle icon**: The mic indicator now defaults to a clearly struck "mic off" icon when Full Voice Mode is disabled (non-listening default).
22
+ - **Voice mode UI switch**: Exiting Full Voice Mode now reliably restores the normal interface; while listening, attachment + tools controls remain available and Send is hidden.
23
+ - **Voice mode shutdown**: Stopping Full Voice Mode now force-stops listening/speaking, turns off TTS, restores Ready (green) status, and prevents late STT/TTS callbacks from flipping the UI back to LISTENING.
24
+
25
+ ### Removed
26
+ - `ROADMAP.md` (removed from the repository).
27
+
28
+ ## [0.3.5] - 2026-01-07
29
+
30
+ ### Fixed
31
+ - **Packaging / installability**: narrowed the default `abstractcore[...]` dependency set to avoid GPU-only stacks (notably vLLM) being installed on macOS, which could break `assistant --help` due to transitive `datasets/pyarrow` incompatibilities.
32
+
5
33
 
6
34
  ## [0.3.4] - 2025-10-27
7
35
 
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: abstractassistant
3
+ Version: 0.4.0
4
+ Summary: A sleek (macOS) system tray application providing instant access to LLMs
5
+ Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lpalbou/abstractassistant
8
+ Project-URL: Repository, https://github.com/lpalbou/abstractassistant
9
+ Project-URL: Issues, https://github.com/lpalbou/abstractassistant/issues
10
+ Keywords: ai,llm,macos,system-tray,assistant
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Desktop Environment
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: abstractagent>=0.3.0
25
+ Requires-Dist: tomli>=2.0.0; python_version < "3.11"
26
+ Requires-Dist: tomli-w>=1.0.0
27
+ Requires-Dist: pystray>=0.19.4
28
+ Requires-Dist: Pillow>=10.0.0
29
+ Requires-Dist: PyQt5>=5.15.0
30
+ Requires-Dist: markdown>=3.5.0
31
+ Requires-Dist: pygments>=2.16.0
32
+ Requires-Dist: pymdown-extensions>=10.0
33
+ Requires-Dist: pyperclip>=1.8.2
34
+ Requires-Dist: plyer>=2.1.0
35
+ Provides-Extra: lite
36
+ Provides-Extra: full
37
+ Requires-Dist: abstractvoice>=0.6.0; extra == "full"
38
+ Requires-Dist: abstractcore[anthropic,lmstudio,media,ollama,openai,tokens]>=2.11.0; extra == "full"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
41
+ Requires-Dist: black>=23.0.0; extra == "dev"
42
+ Requires-Dist: isort>=5.12.0; extra == "dev"
43
+ Requires-Dist: mypy>=1.5.0; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # AbstractAssistant
47
+
48
+ One-click, tray-accessible agent host for AbstractFramework.
49
+
50
+ AbstractAssistant runs **agentic** loops (ReAct/CodeAct/MemAct) on top of:
51
+ - **AbstractAgent** (agent patterns)
52
+ - **AbstractRuntime** (durable runs, waits, ledgers)
53
+ - **AbstractCore** (provider/tool normalization)
54
+ - Optional: **AbstractVoice** (STT/TTS)
55
+
56
+ Docs:
57
+ - `docs/getting-started.md`
58
+ - `docs/architecture.md`
59
+
60
+ ## Install profiles
61
+
62
+ - `abstractassistant` (default) == `lite`: tray UI + agent backend (no voice)
63
+ - `abstractassistant[full]`: voice (STT/TTS) + broader provider/media extras
64
+
65
+ ```bash
66
+ pip install "abstractassistant"
67
+ # or
68
+ pip install "abstractassistant[full]"
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ Tray (macOS):
74
+ ```bash
75
+ assistant tray
76
+ ```
77
+
78
+ Alias:
79
+ ```bash
80
+ abstractassistant tray
81
+ ```
82
+
83
+ Terminal (one turn):
84
+ ```bash
85
+ assistant run --prompt "What is in this repo and where do I start?"
86
+ ```
87
+
88
+ Provider/model override:
89
+ ```bash
90
+ assistant run --provider ollama --model qwen3:4b-instruct --prompt "Summarize my changes"
91
+ ```
92
+
93
+ ## Tool approvals (important)
94
+
95
+ AbstractAssistant enforces a durable tool boundary:
96
+ - read-only / known-safe tools can auto-run
97
+ - anything else pauses and requires approval (tray dialog or terminal prompt)
98
+
99
+ This aligns with the framework’s durability + safety model: tools are executed by the host, not persisted as callables inside run state.
100
+
101
+ ## Data & durability
102
+
103
+ By default, assistant state is stored in `~/.abstractassistant/` (configurable via `--data-dir`):
104
+ - `session.json`: fast UI snapshot (transcript + last run id)
105
+ - `runtime/`: run store + ledger + artifacts (source of truth)
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ pip install -e ".[dev,lite]"
111
+ python -m pytest -q
112
+ assistant tray --debug
113
+ ```
114
+ - **📱 Unobtrusive**: Lives quietly in your menu bar until needed
115
+ - **🔊 Conversational**: Optional voice mode for natural AI interactions
116
+
117
+ ## 📚 Documentation
118
+
119
+ | Guide | Description |
120
+ |-------|------------|
121
+ | [📖 Installation Guide](docs/installation.md) | Complete setup instructions, prerequisites, and troubleshooting |
122
+ | [🎯 Getting Started Guide](docs/getting-started.md) | Step-by-step usage guide with all features explained |
123
+ | [🏗️ Architecture Guide](docs/architecture.md) | Technical documentation and development information |
124
+
125
+ ## 📋 Requirements
126
+
127
+ - **macOS**: 10.14+ (Mojave or later)
128
+ - **Python**: 3.9+
129
+ - **Qt Framework**: PyQt5, PySide2, or PyQt6 (automatically detected)
130
+ - **Dependencies**: [AbstractCore](https://github.com/lpalbou/abstractcore) and [AbstractVoice](https://github.com/lpalbou/abstractvoice) (automatically installed)
131
+
132
+ ## 🤝 Contributing
133
+
134
+ Contributions welcome! Please read the architecture documentation and follow the established patterns:
135
+
136
+ - **Clean Code**: Follow PEP 8 and use type hints
137
+ - **Modular Design**: Keep components focused and reusable
138
+ - **Modern UI/UX**: Maintain the sleek, native feel
139
+ - **Error Handling**: Always include graceful fallbacks
140
+ - **Documentation**: Update docs for any new features
141
+
142
+ ## 📄 License
143
+
144
+ MIT License - see LICENSE file for details.
145
+
146
+ ## 🙏 Acknowledgments
147
+
148
+ AbstractAssistant is built on excellent open-source projects:
149
+
150
+ ### Core Dependencies
151
+ - **[AbstractCore](https://github.com/lpalbou/abstractcore)**: Universal LLM interface - enables seamless multi-provider support
152
+ - **[AbstractVoice](https://github.com/lpalbou/abstractvoice)**: High-quality text-to-speech engine with natural voice synthesis
153
+
154
+ ### Framework & UI
155
+ - **[PyQt5/PySide2/PyQt6](https://www.qt.io/)**: Cross-platform GUI framework for the modern interface
156
+ - **[pystray](https://github.com/moses-palmer/pystray)**: Cross-platform system tray integration
157
+ - **[Pillow](https://python-pillow.org/)**: Image processing for dynamic icon generation
158
+
159
+ ### Part of the AbstractX Ecosystem
160
+ AbstractAssistant integrates seamlessly with other AbstractX projects:
161
+ - 🧠 **[AbstractCore](https://github.com/lpalbou/abstractcore)**: Universal LLM provider interface
162
+ - 🗣️ **[AbstractVoice](https://github.com/lpalbou/abstractvoice)**: Advanced text-to-speech capabilities
163
+
164
+ See [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md) for complete attribution.
165
+
166
+ ---
167
+
168
+ **Built with ❤️ for macOS users who want AI at their fingertips**
@@ -0,0 +1,123 @@
1
+ # AbstractAssistant
2
+
3
+ One-click, tray-accessible agent host for AbstractFramework.
4
+
5
+ AbstractAssistant runs **agentic** loops (ReAct/CodeAct/MemAct) on top of:
6
+ - **AbstractAgent** (agent patterns)
7
+ - **AbstractRuntime** (durable runs, waits, ledgers)
8
+ - **AbstractCore** (provider/tool normalization)
9
+ - Optional: **AbstractVoice** (STT/TTS)
10
+
11
+ Docs:
12
+ - `docs/getting-started.md`
13
+ - `docs/architecture.md`
14
+
15
+ ## Install profiles
16
+
17
+ - `abstractassistant` (default) == `lite`: tray UI + agent backend (no voice)
18
+ - `abstractassistant[full]`: voice (STT/TTS) + broader provider/media extras
19
+
20
+ ```bash
21
+ pip install "abstractassistant"
22
+ # or
23
+ pip install "abstractassistant[full]"
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ Tray (macOS):
29
+ ```bash
30
+ assistant tray
31
+ ```
32
+
33
+ Alias:
34
+ ```bash
35
+ abstractassistant tray
36
+ ```
37
+
38
+ Terminal (one turn):
39
+ ```bash
40
+ assistant run --prompt "What is in this repo and where do I start?"
41
+ ```
42
+
43
+ Provider/model override:
44
+ ```bash
45
+ assistant run --provider ollama --model qwen3:4b-instruct --prompt "Summarize my changes"
46
+ ```
47
+
48
+ ## Tool approvals (important)
49
+
50
+ AbstractAssistant enforces a durable tool boundary:
51
+ - read-only / known-safe tools can auto-run
52
+ - anything else pauses and requires approval (tray dialog or terminal prompt)
53
+
54
+ This aligns with the framework’s durability + safety model: tools are executed by the host, not persisted as callables inside run state.
55
+
56
+ ## Data & durability
57
+
58
+ By default, assistant state is stored in `~/.abstractassistant/` (configurable via `--data-dir`):
59
+ - `session.json`: fast UI snapshot (transcript + last run id)
60
+ - `runtime/`: run store + ledger + artifacts (source of truth)
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ pip install -e ".[dev,lite]"
66
+ python -m pytest -q
67
+ assistant tray --debug
68
+ ```
69
+ - **📱 Unobtrusive**: Lives quietly in your menu bar until needed
70
+ - **🔊 Conversational**: Optional voice mode for natural AI interactions
71
+
72
+ ## 📚 Documentation
73
+
74
+ | Guide | Description |
75
+ |-------|------------|
76
+ | [📖 Installation Guide](docs/installation.md) | Complete setup instructions, prerequisites, and troubleshooting |
77
+ | [🎯 Getting Started Guide](docs/getting-started.md) | Step-by-step usage guide with all features explained |
78
+ | [🏗️ Architecture Guide](docs/architecture.md) | Technical documentation and development information |
79
+
80
+ ## 📋 Requirements
81
+
82
+ - **macOS**: 10.14+ (Mojave or later)
83
+ - **Python**: 3.9+
84
+ - **Qt Framework**: PyQt5, PySide2, or PyQt6 (automatically detected)
85
+ - **Dependencies**: [AbstractCore](https://github.com/lpalbou/abstractcore) and [AbstractVoice](https://github.com/lpalbou/abstractvoice) (automatically installed)
86
+
87
+ ## 🤝 Contributing
88
+
89
+ Contributions welcome! Please read the architecture documentation and follow the established patterns:
90
+
91
+ - **Clean Code**: Follow PEP 8 and use type hints
92
+ - **Modular Design**: Keep components focused and reusable
93
+ - **Modern UI/UX**: Maintain the sleek, native feel
94
+ - **Error Handling**: Always include graceful fallbacks
95
+ - **Documentation**: Update docs for any new features
96
+
97
+ ## 📄 License
98
+
99
+ MIT License - see LICENSE file for details.
100
+
101
+ ## 🙏 Acknowledgments
102
+
103
+ AbstractAssistant is built on excellent open-source projects:
104
+
105
+ ### Core Dependencies
106
+ - **[AbstractCore](https://github.com/lpalbou/abstractcore)**: Universal LLM interface - enables seamless multi-provider support
107
+ - **[AbstractVoice](https://github.com/lpalbou/abstractvoice)**: High-quality text-to-speech engine with natural voice synthesis
108
+
109
+ ### Framework & UI
110
+ - **[PyQt5/PySide2/PyQt6](https://www.qt.io/)**: Cross-platform GUI framework for the modern interface
111
+ - **[pystray](https://github.com/moses-palmer/pystray)**: Cross-platform system tray integration
112
+ - **[Pillow](https://python-pillow.org/)**: Image processing for dynamic icon generation
113
+
114
+ ### Part of the AbstractX Ecosystem
115
+ AbstractAssistant integrates seamlessly with other AbstractX projects:
116
+ - 🧠 **[AbstractCore](https://github.com/lpalbou/abstractcore)**: Universal LLM provider interface
117
+ - 🗣️ **[AbstractVoice](https://github.com/lpalbou/abstractvoice)**: Advanced text-to-speech capabilities
118
+
119
+ See [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md) for complete attribution.
120
+
121
+ ---
122
+
123
+ **Built with ❤️ for macOS users who want AI at their fingertips**
@@ -6,6 +6,8 @@ Handles system tray integration, UI coordination, and application lifecycle.
6
6
 
7
7
  import threading
8
8
  import time
9
+ import signal
10
+ from pathlib import Path
9
11
  from typing import Optional
10
12
 
11
13
  import pystray
@@ -131,17 +133,26 @@ class EnhancedClickableIcon(pystray.Icon):
131
133
  class AbstractAssistantApp:
132
134
  """Main application class coordinating all components."""
133
135
 
134
- def __init__(self, config: Optional[Config] = None, debug: bool = False, listening_mode: str = "wait"):
136
+ def __init__(
137
+ self,
138
+ config: Optional[Config] = None,
139
+ debug: bool = False,
140
+ listening_mode: str = "wait",
141
+ *,
142
+ data_dir: Optional[Path] = None,
143
+ ):
135
144
  """Initialize the AbstractAssistant application.
136
145
 
137
146
  Args:
138
147
  config: Configuration object (uses default if None)
139
148
  debug: Enable debug mode
140
149
  listening_mode: Voice listening mode (none, stop, wait, full)
150
+ data_dir: Assistant base data dir (sessions + runtime stores)
141
151
  """
142
152
  self.config = config or Config.default()
143
153
  self.debug = debug
144
154
  self.listening_mode = listening_mode
155
+ self.data_dir = Path(data_dir).expanduser() if data_dir is not None else None
145
156
 
146
157
  # Validate configuration
147
158
  if not self.config.validate():
@@ -152,7 +163,7 @@ class AbstractAssistantApp:
152
163
  # Initialize components
153
164
  self.icon: Optional[pystray.Icon] = None
154
165
  self.bubble_manager: Optional[QtBubbleManager] = None
155
- self.llm_manager: LLMManager = LLMManager(config=self.config, debug=self.debug)
166
+ self.llm_manager: LLMManager = LLMManager(config=self.config, debug=self.debug, data_dir=self.data_dir)
156
167
  self.icon_generator: IconGenerator = IconGenerator(size=self.config.system_tray.icon_size)
157
168
 
158
169
  # Application state
@@ -786,6 +797,20 @@ class AbstractAssistantApp:
786
797
 
787
798
  if self.icon:
788
799
  self.icon.stop()
800
+
801
+ # Stop/hide Qt tray icon if we are running in Qt mode.
802
+ try:
803
+ if hasattr(self, "qt_tray_icon") and self.qt_tray_icon is not None:
804
+ self.qt_tray_icon.hide()
805
+ except Exception:
806
+ pass
807
+
808
+ # Stop click timer used for single/double click detection (Qt mode).
809
+ try:
810
+ if hasattr(self, "click_timer") and self.click_timer is not None:
811
+ self.click_timer.stop()
812
+ except Exception:
813
+ pass
789
814
 
790
815
  # Clean up bubble manager
791
816
  if self.bubble_manager:
@@ -797,6 +822,20 @@ class AbstractAssistantApp:
797
822
 
798
823
  if self.debug:
799
824
  print("✅ AbstractAssistant quit successfully")
825
+
826
+ def _request_qt_quit(self) -> None:
827
+ """Request a graceful quit on the Qt event loop (safe to call from SIGINT handler)."""
828
+ # Always run cleanup first; then quit the Qt event loop.
829
+ try:
830
+ self.quit_application()
831
+ except Exception:
832
+ pass
833
+
834
+ try:
835
+ if hasattr(self, "qt_app") and self.qt_app:
836
+ self.qt_app.quit()
837
+ except Exception:
838
+ pass
800
839
 
801
840
  def run(self):
802
841
  """Start the application using Qt event loop for proper threading."""
@@ -833,15 +872,39 @@ class AbstractAssistantApp:
833
872
  print("AbstractAssistant started. Check your menu bar!")
834
873
  print("Click the icon to open the chat interface.")
835
874
 
875
+ # Ctrl+C / SIGTERM should shut down cleanly (avoid macOS "python quit unexpectedly").
876
+ # We schedule the quit on the Qt loop to keep teardown ordered.
877
+ def _handle_sigint(_signum, _frame):
878
+ try:
879
+ QTimer.singleShot(0, self._request_qt_quit)
880
+ except Exception:
881
+ self._request_qt_quit()
882
+
883
+ try:
884
+ signal.signal(signal.SIGINT, _handle_sigint)
885
+ signal.signal(signal.SIGTERM, _handle_sigint)
886
+ except Exception:
887
+ # If signals are not available/allowed in this context, we still handle KeyboardInterrupt below.
888
+ pass
889
+
836
890
  # Run Qt event loop (this blocks until quit)
837
- self.qt_app.exec_()
891
+ try:
892
+ self.qt_app.exec_()
893
+ except KeyboardInterrupt:
894
+ # Ensure a graceful shutdown when Ctrl+C interrupts the event loop.
895
+ self._request_qt_quit()
896
+ return
838
897
 
839
898
  except ImportError:
840
899
  if self.debug:
841
900
  print("❌ PyQt5 not available. Falling back to pystray...")
842
901
  # Fallback to original pystray implementation
843
902
  self.icon = self.create_system_tray_icon()
844
- self.icon.run()
903
+ try:
904
+ self.icon.run()
905
+ except KeyboardInterrupt:
906
+ self.quit_application()
907
+ return
845
908
 
846
909
  def _create_qt_system_tray_icon(self):
847
910
  """Create Qt-based system tray icon with smooth animations."""
@@ -996,5 +1059,5 @@ class AbstractAssistantApp:
996
1059
  if self.debug:
997
1060
  print("🔄 Qt: Quit requested")
998
1061
 
999
- if hasattr(self, 'qt_app') and self.qt_app:
1000
- self.qt_app.quit()
1062
+ # Route through the same cleanup path (menu Quit should behave like Ctrl+C).
1063
+ self._request_qt_quit()
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry point for AbstractAssistant.
3
+
4
+ Packaging invariant:
5
+ - `assistant --help` must not import GUI/voice stacks (optional dependencies).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ def create_parser() -> argparse.ArgumentParser:
17
+ """Create the command-line argument parser."""
18
+ prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "abstractassistant"
19
+ parser = argparse.ArgumentParser(prog=prog, description="AbstractAssistant (agentic tray + CLI)")
20
+
21
+ parser.add_argument("--version", action="version", version="abstractassistant (agentic) v1")
22
+
23
+ parser.add_argument("--config", type=str, default=None, help="Path to config.toml (optional)")
24
+ parser.add_argument("--provider", type=str, default=None, help="LLM provider id (e.g. ollama, lmstudio, openai)")
25
+ parser.add_argument("--model", type=str, default=None, help="Model name/id for the provider")
26
+ parser.add_argument("--agent", type=str, default=None, choices=["react", "codeact", "memact"], help="Agent kind")
27
+ parser.add_argument("--data-dir", type=str, default=None, help="Assistant data dir (runtime stores + session)")
28
+ parser.add_argument("--workspace-root", type=str, default=None, help="Workspace root for filesystem-ish tools")
29
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
30
+
31
+ sub = parser.add_subparsers(dest="command")
32
+
33
+ tray = sub.add_parser("tray", help="Run the macOS tray app")
34
+ tray.add_argument(
35
+ "--listening-mode",
36
+ type=str,
37
+ choices=["none", "stop", "wait", "full", "ptt"],
38
+ default="wait",
39
+ help="Voice listening mode (requires [full] extra for STT/TTS)",
40
+ )
41
+
42
+ run = sub.add_parser("run", help="Run one agentic turn in the terminal")
43
+ run.add_argument("--prompt", type=str, required=True, help="User prompt text")
44
+ run.add_argument("--approve-all-tools", action="store_true", help="Auto-approve all tool calls (dangerous)")
45
+
46
+ return parser
47
+
48
+
49
+ def find_config_file(config_path: Optional[str] = None) -> Optional[Path]:
50
+ """Find the configuration file."""
51
+ if config_path:
52
+ config_file = Path(config_path)
53
+ if config_file.exists():
54
+ return config_file
55
+ else:
56
+ print(f"Warning: Config file '{config_path}' not found.")
57
+ return None
58
+
59
+ # Look for config.toml in current directory, then package directory
60
+ current_dir = Path.cwd()
61
+ package_dir = Path(__file__).parent.parent
62
+
63
+ for directory in [current_dir, package_dir]:
64
+ config_file = directory / "config.toml"
65
+ if config_file.exists():
66
+ return config_file
67
+
68
+ return None
69
+
70
+
71
+ def main() -> int:
72
+ """Main entry point for the CLI."""
73
+ parser = create_parser()
74
+ args = parser.parse_args()
75
+
76
+ try:
77
+ command = args.command or "tray"
78
+
79
+ if command == "run":
80
+ from .core.agent_host import AgentHost, AgentHostConfig
81
+
82
+ provider = str(args.provider or "ollama")
83
+ model = str(args.model or "qwen3:4b-instruct")
84
+ agent_kind = str(args.agent or "react")
85
+ data_dir = Path(args.data_dir).expanduser() if args.data_dir else (Path.home() / ".abstractassistant")
86
+
87
+ host = AgentHost(
88
+ AgentHostConfig(
89
+ provider=provider,
90
+ model=model,
91
+ agent_kind=agent_kind,
92
+ data_dir=data_dir,
93
+ workspace_root=args.workspace_root,
94
+ )
95
+ )
96
+
97
+ def _approve(tool_calls):
98
+ if args.approve_all_tools:
99
+ return True
100
+ # Prompt for dangerous/unknown batches; safe-only batches auto-approve.
101
+ if not host.tool_policy.requires_approval(tool_calls):
102
+ return True
103
+ print("\nTool approval required:")
104
+ for tc in tool_calls:
105
+ name = tc.get("name")
106
+ arguments = tc.get("arguments")
107
+ print(f"- {name}({arguments})")
108
+ ans = input("Approve this batch? [y/N] ").strip().lower()
109
+ return ans in {"y", "yes"}
110
+
111
+ def _ask_user(wait):
112
+ prompt = str(getattr(wait, "prompt", "") or "Input required:")
113
+ return input(f"\n{prompt}\n> ").strip()
114
+
115
+ final = ""
116
+ for ev in host.run_turn(user_text=args.prompt, approve_tools=_approve, ask_user=_ask_user):
117
+ typ = ev.get("type")
118
+ if typ == "assistant":
119
+ final = str(ev.get("content") or "")
120
+ if typ == "error":
121
+ raise RuntimeError(str(ev.get("error") or "error"))
122
+ print(final)
123
+ return 0
124
+
125
+ # tray (default)
126
+ try:
127
+ from .config import Config # lightweight
128
+
129
+ config_file = find_config_file(args.config)
130
+ config = Config.from_file(config_file) if config_file else Config.default()
131
+ if args.provider:
132
+ config.llm.default_provider = str(args.provider)
133
+ if args.model:
134
+ config.llm.default_model = str(args.model)
135
+ except Exception:
136
+ config = None
137
+
138
+ try:
139
+ from .app import AbstractAssistantApp
140
+ except Exception as e:
141
+ print("AbstractAssistant tray mode requires GUI dependencies.")
142
+ print('Install (tray): pip install -U "abstractassistant"')
143
+ print('Install (tray + voice): pip install -U "abstractassistant[full]"')
144
+ print('From source (editable): pip install -e ".[lite]"')
145
+ print(f"Import error: {e}")
146
+ return 2
147
+
148
+ app = AbstractAssistantApp(
149
+ config=config,
150
+ debug=bool(args.debug),
151
+ listening_mode=str(getattr(args, "listening_mode", "wait")),
152
+ data_dir=Path(args.data_dir).expanduser() if getattr(args, "data_dir", None) else None,
153
+ )
154
+ app.run()
155
+ return 0
156
+
157
+ except KeyboardInterrupt:
158
+ print("\n👋 AbstractAssistant stopped by user")
159
+ return 0
160
+ except Exception as e:
161
+ print(f"❌ Error starting AbstractAssistant: {e}")
162
+ if getattr(args, "debug", False):
163
+ import traceback
164
+
165
+ traceback.print_exc()
166
+ return 1
167
+
168
+
169
+ if __name__ == "__main__":
170
+ sys.exit(main())