abstractassistant 0.3.5__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.
- abstractassistant-0.4.0/ACKNOWLEDGMENTS.md +34 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/CHANGELOG.md +23 -0
- abstractassistant-0.4.0/PKG-INFO +168 -0
- abstractassistant-0.4.0/README.md +123 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/app.py +69 -6
- abstractassistant-0.4.0/abstractassistant/cli.py +170 -0
- abstractassistant-0.4.0/abstractassistant/core/agent_host.py +583 -0
- abstractassistant-0.4.0/abstractassistant/core/llm_manager.py +382 -0
- abstractassistant-0.4.0/abstractassistant/core/session_index.py +293 -0
- abstractassistant-0.4.0/abstractassistant/core/session_store.py +79 -0
- abstractassistant-0.4.0/abstractassistant/core/tool_policy.py +58 -0
- abstractassistant-0.4.0/abstractassistant/core/transcript_summary.py +434 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/history_dialog.py +504 -29
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/qt_bubble.py +2276 -477
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant.egg-info/SOURCES.txt +7 -2
- abstractassistant-0.4.0/docs/INSTALLATION.md +37 -0
- abstractassistant-0.4.0/docs/architecture.md +181 -0
- abstractassistant-0.4.0/docs/getting-started.md +125 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/pyproject.toml +26 -11
- abstractassistant-0.3.5/ACKNOWLEDGMENTS.md +0 -164
- abstractassistant-0.3.5/PKG-INFO +0 -297
- abstractassistant-0.3.5/README.md +0 -255
- abstractassistant-0.3.5/abstractassistant/cli.py +0 -151
- abstractassistant-0.3.5/abstractassistant/core/llm_manager.py +0 -475
- abstractassistant-0.3.5/docs/architecture.md +0 -330
- abstractassistant-0.3.5/docs/getting-started.md +0 -394
- abstractassistant-0.3.5/docs/installation.md +0 -464
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/CONTRIBUTING.md +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/LICENSE +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/MANIFEST.in +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/__init__.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/config.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/core/__init__.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/core/tts_manager.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/create_app_bundle.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/__init__.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/chat_bubble.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/provider_manager.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/toast_manager.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/toast_window.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/tts_state_manager.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/ui/ui_styles.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/utils/__init__.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/utils/icon_generator.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/utils/markdown_renderer.py +0 -0
- {abstractassistant-0.3.5 → abstractassistant-0.4.0}/abstractassistant/web_server.py +0 -0
- {abstractassistant-0.3.5 → 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,29 @@
|
|
|
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
|
+
|
|
5
28
|
## [0.3.5] - 2026-01-07
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -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__(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
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())
|