abstractassistant 0.3.5__py3-none-any.whl → 0.4.0__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.
- abstractassistant/app.py +69 -6
- abstractassistant/cli.py +104 -85
- abstractassistant/core/agent_host.py +583 -0
- abstractassistant/core/llm_manager.py +338 -431
- abstractassistant/core/session_index.py +293 -0
- abstractassistant/core/session_store.py +79 -0
- abstractassistant/core/tool_policy.py +58 -0
- abstractassistant/core/transcript_summary.py +434 -0
- abstractassistant/ui/history_dialog.py +504 -29
- abstractassistant/ui/qt_bubble.py +2276 -477
- abstractassistant-0.4.0.dist-info/METADATA +168 -0
- {abstractassistant-0.3.5.dist-info → abstractassistant-0.4.0.dist-info}/RECORD +16 -11
- {abstractassistant-0.3.5.dist-info → abstractassistant-0.4.0.dist-info}/WHEEL +1 -1
- {abstractassistant-0.3.5.dist-info → abstractassistant-0.4.0.dist-info}/entry_points.txt +1 -0
- abstractassistant-0.3.5.dist-info/METADATA +0 -297
- {abstractassistant-0.3.5.dist-info → abstractassistant-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.3.5.dist-info → abstractassistant-0.4.0.dist-info}/top_level.txt +0 -0
abstractassistant/app.py
CHANGED
|
@@ -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()
|
abstractassistant/cli.py
CHANGED
|
@@ -1,76 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
CLI entry point for AbstractAssistant.
|
|
2
|
+
"""CLI entry point for AbstractAssistant.
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
Packaging invariant:
|
|
5
|
+
- `assistant --help` must not import GUI/voice stacks (optional dependencies).
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
9
10
|
import argparse
|
|
11
|
+
import sys
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Optional
|
|
12
14
|
|
|
13
|
-
from .app import AbstractAssistantApp
|
|
14
|
-
from .config import Config
|
|
15
|
-
|
|
16
15
|
|
|
17
16
|
def create_parser() -> argparse.ArgumentParser:
|
|
18
17
|
"""Create the command-line argument parser."""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
description="AbstractAssistant - AI at your fingertips",
|
|
22
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
-
epilog="""
|
|
24
|
-
Examples:
|
|
25
|
-
assistant # Launch with default settings
|
|
26
|
-
assistant --config custom.toml # Use custom config file
|
|
27
|
-
assistant --provider openai # Set default provider
|
|
28
|
-
assistant --model gpt-4o # Set default model
|
|
29
|
-
assistant --debug # Enable debug mode
|
|
30
|
-
|
|
31
|
-
For more information, visit: https://github.com/yourusername/abstractassistant
|
|
32
|
-
""",
|
|
33
|
-
)
|
|
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)")
|
|
34
20
|
|
|
35
|
-
parser.add_argument(
|
|
36
|
-
"--config",
|
|
37
|
-
type=str,
|
|
38
|
-
help="Path to configuration file (default: config.toml)",
|
|
39
|
-
default=None,
|
|
40
|
-
)
|
|
21
|
+
parser.add_argument("--version", action="version", version="abstractassistant (agentic) v1")
|
|
41
22
|
|
|
42
|
-
parser.add_argument(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
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")
|
|
48
30
|
|
|
49
|
-
parser.
|
|
50
|
-
"--model",
|
|
51
|
-
type=str,
|
|
52
|
-
help="Default model name",
|
|
53
|
-
)
|
|
31
|
+
sub = parser.add_subparsers(dest="command")
|
|
54
32
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
action="store_true",
|
|
58
|
-
help="Enable debug mode",
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
parser.add_argument(
|
|
33
|
+
tray = sub.add_parser("tray", help="Run the macOS tray app")
|
|
34
|
+
tray.add_argument(
|
|
62
35
|
"--listening-mode",
|
|
63
36
|
type=str,
|
|
64
|
-
choices=["none", "stop", "wait", "full"],
|
|
65
|
-
help="Voice listening mode (none: no STT, stop: continuous listen/stop on 'STOP' keyword, wait: listen when TTS idle, full: continuous listen/interrupt on any speech)",
|
|
37
|
+
choices=["none", "stop", "wait", "full", "ptt"],
|
|
66
38
|
default="wait",
|
|
39
|
+
help="Voice listening mode (requires [full] extra for STT/TTS)",
|
|
67
40
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
version="AbstractAssistant 1.0.0",
|
|
73
|
-
)
|
|
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)")
|
|
74
45
|
|
|
75
46
|
return parser
|
|
76
47
|
|
|
@@ -103,37 +74,84 @@ def main() -> int:
|
|
|
103
74
|
args = parser.parse_args()
|
|
104
75
|
|
|
105
76
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if args.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
)
|
|
135
154
|
app.run()
|
|
136
|
-
|
|
137
155
|
return 0
|
|
138
156
|
|
|
139
157
|
except KeyboardInterrupt:
|
|
@@ -141,8 +159,9 @@ def main() -> int:
|
|
|
141
159
|
return 0
|
|
142
160
|
except Exception as e:
|
|
143
161
|
print(f"❌ Error starting AbstractAssistant: {e}")
|
|
144
|
-
if args
|
|
162
|
+
if getattr(args, "debug", False):
|
|
145
163
|
import traceback
|
|
164
|
+
|
|
146
165
|
traceback.print_exc()
|
|
147
166
|
return 1
|
|
148
167
|
|