sa-assistant 0.1.1__tar.gz → 0.2.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.
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/PKG-INFO +2 -1
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/pyproject.toml +4 -2
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/PKG-INFO +2 -1
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/SOURCES.txt +2 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/entry_points.txt +1 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/requires.txt +1 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/top_level.txt +1 -0
- sa_assistant-0.2.0/src/tui/__init__.py +5 -0
- sa_assistant-0.2.0/src/tui/app.py +269 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/README.md +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/setup.cfg +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/__init__.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/orchestrator.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/specialists.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/__init__.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/discovery.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/errors.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/models.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/parser.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/prompt.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/tool.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/__init__.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/callback.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/components.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/console.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/mdstream.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/mcp_client/client.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/model/__init__.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/model/load.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/prompts/__init__.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/prompts/system_prompts.py +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/dependency_links.txt +0 -0
- {sa_assistant-0.1.1 → sa_assistant-0.2.0}/test/test_main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sa-assistant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SA Assistant - AWS Solutions Architect Professional Agent with Multi-Agent Architecture
|
|
5
5
|
Author-email: onesuit <wltks2155@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -30,6 +30,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
30
30
|
Requires-Dist: rich>=13.0.0
|
|
31
31
|
Requires-Dist: strands-agents>=1.13.0
|
|
32
32
|
Requires-Dist: strands-agents-tools>=0.2.16
|
|
33
|
+
Requires-Dist: textual>=0.85.0
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
36
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sa-assistant"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "SA Assistant - AWS Solutions Architect Professional Agent with Multi-Agent Architecture"
|
|
10
10
|
readme = "README.md"
|
|
@@ -44,7 +44,8 @@ dependencies = [
|
|
|
44
44
|
"pyyaml >= 6.0",
|
|
45
45
|
"rich >= 13.0.0",
|
|
46
46
|
"strands-agents >= 1.13.0",
|
|
47
|
-
"strands-agents-tools >= 0.2.16"
|
|
47
|
+
"strands-agents-tools >= 0.2.16",
|
|
48
|
+
"textual >= 0.85.0"
|
|
48
49
|
]
|
|
49
50
|
|
|
50
51
|
[project.optional-dependencies]
|
|
@@ -61,6 +62,7 @@ Issues = "https://github.com/onesuit/SaAssistant/issues"
|
|
|
61
62
|
|
|
62
63
|
[project.scripts]
|
|
63
64
|
sa-assistant = "main:run_cli"
|
|
65
|
+
sa-assistant-tui = "main:run_tui"
|
|
64
66
|
|
|
65
67
|
[tool.setuptools]
|
|
66
68
|
include-package-data = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sa-assistant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SA Assistant - AWS Solutions Architect Professional Agent with Multi-Agent Architecture
|
|
5
5
|
Author-email: onesuit <wltks2155@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -30,6 +30,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
30
30
|
Requires-Dist: rich>=13.0.0
|
|
31
31
|
Requires-Dist: strands-agents>=1.13.0
|
|
32
32
|
Requires-Dist: strands-agents-tools>=0.2.16
|
|
33
|
+
Requires-Dist: textual>=0.85.0
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
36
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""SA Assistant TUI Application - Main Textual App class."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from textual import work
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import ScrollableContainer, Vertical
|
|
11
|
+
from textual.widgets import Footer, Header, Input, Static, RichLog, Collapsible
|
|
12
|
+
from textual.worker import Worker
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.markdown import Markdown
|
|
15
|
+
|
|
16
|
+
from strands import Agent
|
|
17
|
+
from strands.agent.conversation_manager import SlidingWindowConversationManager
|
|
18
|
+
from strands_tools import file_read, file_write, shell, image_reader
|
|
19
|
+
|
|
20
|
+
from model.load import load_sonnet
|
|
21
|
+
from prompts.system_prompts import ORCHESTRATOR_PROMPT
|
|
22
|
+
from agents.orchestrator import consult_guru, list_gurus
|
|
23
|
+
from agents.specialists import consult_specialist, list_specialists, parallel_research
|
|
24
|
+
from agentskills import discover_skills, generate_skills_prompt, create_skill_tool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CSS_PATH = Path(__file__).parent / "styles.tcss"
|
|
28
|
+
SKILLS_DIR = Path(__file__).parent.parent / "skills"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SessionHeader(Static):
|
|
32
|
+
"""Top header showing session info, tokens, cost."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, session_id: str = None):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.session_start = datetime.now()
|
|
37
|
+
self.session_id = session_id or self.session_start.strftime("%Y-%m-%dT%H:%M:%S")
|
|
38
|
+
self.token_count = 0
|
|
39
|
+
self.cost = 0.0
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
yield Static(id="session-info")
|
|
43
|
+
|
|
44
|
+
def on_mount(self) -> None:
|
|
45
|
+
self._update_display()
|
|
46
|
+
|
|
47
|
+
def _update_display(self) -> None:
|
|
48
|
+
info = self.query_one("#session-info", Static)
|
|
49
|
+
info.update(
|
|
50
|
+
f"# Session - {self.session_id} "
|
|
51
|
+
f"[dim]{self.token_count:,} tokens · ${self.cost:.2f}[/dim]"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def update_stats(self, tokens: int = 0, cost: float = 0.0) -> None:
|
|
55
|
+
self.token_count += tokens
|
|
56
|
+
self.cost += cost
|
|
57
|
+
self._update_display()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentIndicator(Static):
|
|
61
|
+
"""Bottom indicator showing current agent and model."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, agent_name: str = "SA Assistant", model_name: str = "Claude Sonnet"):
|
|
64
|
+
super().__init__()
|
|
65
|
+
self.agent_name = agent_name
|
|
66
|
+
self.model_name = model_name
|
|
67
|
+
|
|
68
|
+
def on_mount(self) -> None:
|
|
69
|
+
self._update_display()
|
|
70
|
+
|
|
71
|
+
def _update_display(self) -> None:
|
|
72
|
+
self.update(f"[cyan]■[/cyan] {self.agent_name} · [dim]{self.model_name}[/dim]")
|
|
73
|
+
|
|
74
|
+
def set_agent(self, name: str, model: str = None) -> None:
|
|
75
|
+
self.agent_name = name
|
|
76
|
+
if model:
|
|
77
|
+
self.model_name = model
|
|
78
|
+
self._update_display()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MessageSection(Collapsible):
|
|
82
|
+
"""Collapsible section for a single message/action."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, title: str, content: str = "", collapsed: bool = False):
|
|
85
|
+
super().__init__(title=title, collapsed=collapsed)
|
|
86
|
+
self._content = content
|
|
87
|
+
self._log: Optional[RichLog] = None
|
|
88
|
+
|
|
89
|
+
def compose(self) -> ComposeResult:
|
|
90
|
+
self._log = RichLog(highlight=True, markup=True, wrap=True)
|
|
91
|
+
yield self._log
|
|
92
|
+
|
|
93
|
+
def on_mount(self) -> None:
|
|
94
|
+
if self._content and self._log:
|
|
95
|
+
self._log.write(self._content)
|
|
96
|
+
|
|
97
|
+
def append(self, text: str) -> None:
|
|
98
|
+
if self._log:
|
|
99
|
+
self._log.write(text)
|
|
100
|
+
|
|
101
|
+
def update_content(self, text: str) -> None:
|
|
102
|
+
if self._log:
|
|
103
|
+
self._log.clear()
|
|
104
|
+
self._log.write(text)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ChatContainer(ScrollableContainer):
|
|
108
|
+
"""Main scrollable container for chat messages."""
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
super().__init__()
|
|
112
|
+
self._sections: list[MessageSection] = []
|
|
113
|
+
|
|
114
|
+
def add_user_message(self, text: str) -> MessageSection:
|
|
115
|
+
section = MessageSection(title=f"[green]You[/green]", content=text, collapsed=False)
|
|
116
|
+
self._sections.append(section)
|
|
117
|
+
self.mount(section)
|
|
118
|
+
section.scroll_visible()
|
|
119
|
+
return section
|
|
120
|
+
|
|
121
|
+
def add_assistant_message(self, title: str = "SA Assistant") -> MessageSection:
|
|
122
|
+
section = MessageSection(title=f"[#FF9900]{title}[/#FF9900]", collapsed=False)
|
|
123
|
+
self._sections.append(section)
|
|
124
|
+
self.mount(section)
|
|
125
|
+
section.scroll_visible()
|
|
126
|
+
return section
|
|
127
|
+
|
|
128
|
+
def add_tool_section(self, tool_name: str, collapsed: bool = True) -> MessageSection:
|
|
129
|
+
emoji_map = {
|
|
130
|
+
"consult_guru": "🧙",
|
|
131
|
+
"consult_specialist": "🔬",
|
|
132
|
+
"file_read": "📄",
|
|
133
|
+
"file_write": "💾",
|
|
134
|
+
"shell": "🖥️",
|
|
135
|
+
"skill": "📚",
|
|
136
|
+
}
|
|
137
|
+
emoji = emoji_map.get(tool_name, "🔧")
|
|
138
|
+
section = MessageSection(title=f"[cyan]{emoji} {tool_name}[/cyan]", collapsed=collapsed)
|
|
139
|
+
self._sections.append(section)
|
|
140
|
+
self.mount(section)
|
|
141
|
+
return section
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SAAssistantApp(App):
|
|
145
|
+
"""SA Assistant Textual TUI Application."""
|
|
146
|
+
|
|
147
|
+
CSS_PATH = "styles.tcss"
|
|
148
|
+
|
|
149
|
+
TITLE = "SA Assistant"
|
|
150
|
+
SUB_TITLE = "AWS Solutions Architect Agent"
|
|
151
|
+
|
|
152
|
+
BINDINGS = [
|
|
153
|
+
Binding("escape", "cancel", "Cancel", show=True),
|
|
154
|
+
Binding("ctrl+c", "quit", "Quit", show=True),
|
|
155
|
+
Binding("ctrl+l", "clear", "Clear", show=True),
|
|
156
|
+
Binding("ctrl+t", "toggle_dark", "Theme", show=True),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
def __init__(self):
|
|
160
|
+
super().__init__()
|
|
161
|
+
self._agent: Optional[Agent] = None
|
|
162
|
+
self._current_section: Optional[MessageSection] = None
|
|
163
|
+
self._response_buffer = ""
|
|
164
|
+
self._init_agent()
|
|
165
|
+
|
|
166
|
+
def _init_agent(self) -> None:
|
|
167
|
+
discovered_skills = discover_skills(SKILLS_DIR)
|
|
168
|
+
skills_prompt = generate_skills_prompt(discovered_skills)
|
|
169
|
+
full_prompt = ORCHESTRATOR_PROMPT + skills_prompt
|
|
170
|
+
skill_tool = create_skill_tool(discovered_skills, SKILLS_DIR) if discovered_skills else None
|
|
171
|
+
|
|
172
|
+
tools = [
|
|
173
|
+
consult_guru,
|
|
174
|
+
list_gurus,
|
|
175
|
+
consult_specialist,
|
|
176
|
+
list_specialists,
|
|
177
|
+
parallel_research,
|
|
178
|
+
file_read,
|
|
179
|
+
file_write,
|
|
180
|
+
image_reader,
|
|
181
|
+
shell,
|
|
182
|
+
]
|
|
183
|
+
if skill_tool:
|
|
184
|
+
tools.append(skill_tool)
|
|
185
|
+
|
|
186
|
+
conversation_manager = SlidingWindowConversationManager(window_size=20)
|
|
187
|
+
|
|
188
|
+
self._agent = Agent(
|
|
189
|
+
model=load_sonnet(enable_thinking=False),
|
|
190
|
+
conversation_manager=conversation_manager,
|
|
191
|
+
system_prompt=full_prompt,
|
|
192
|
+
tools=tools,
|
|
193
|
+
callback_handler=self._streaming_callback,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _streaming_callback(self, **kwargs) -> None:
|
|
197
|
+
if "data" in kwargs:
|
|
198
|
+
self._response_buffer += kwargs["data"]
|
|
199
|
+
if self._current_section:
|
|
200
|
+
self.call_from_thread(self._current_section.update_content, self._response_buffer)
|
|
201
|
+
|
|
202
|
+
if "current_tool_use" in kwargs:
|
|
203
|
+
tool_use = kwargs["current_tool_use"]
|
|
204
|
+
if isinstance(tool_use, dict):
|
|
205
|
+
tool_name = tool_use.get("name", "tool")
|
|
206
|
+
self.call_from_thread(self._add_tool_section, tool_name)
|
|
207
|
+
|
|
208
|
+
def _add_tool_section(self, tool_name: str) -> None:
|
|
209
|
+
chat = self.query_one(ChatContainer)
|
|
210
|
+
chat.add_tool_section(tool_name, collapsed=True)
|
|
211
|
+
|
|
212
|
+
def compose(self) -> ComposeResult:
|
|
213
|
+
yield Header()
|
|
214
|
+
yield SessionHeader()
|
|
215
|
+
yield ChatContainer()
|
|
216
|
+
yield AgentIndicator()
|
|
217
|
+
yield Input(placeholder="Type your message... (Enter to send)")
|
|
218
|
+
yield Footer()
|
|
219
|
+
|
|
220
|
+
def on_mount(self) -> None:
|
|
221
|
+
self.query_one(Input).focus()
|
|
222
|
+
|
|
223
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
224
|
+
user_input = event.value.strip()
|
|
225
|
+
if not user_input:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
event.input.value = ""
|
|
229
|
+
|
|
230
|
+
if user_input.lower() in ("exit", "quit", "종료"):
|
|
231
|
+
self.exit()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
chat = self.query_one(ChatContainer)
|
|
235
|
+
chat.add_user_message(user_input)
|
|
236
|
+
|
|
237
|
+
self._current_section = chat.add_assistant_message()
|
|
238
|
+
self._response_buffer = ""
|
|
239
|
+
|
|
240
|
+
self.run_agent(user_input)
|
|
241
|
+
|
|
242
|
+
@work(thread=True)
|
|
243
|
+
def run_agent(self, prompt: str) -> None:
|
|
244
|
+
try:
|
|
245
|
+
self._agent(prompt)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
if self._current_section:
|
|
248
|
+
self.call_from_thread(
|
|
249
|
+
self._current_section.update_content, f"[red]Error: {e}[/red]"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def action_cancel(self) -> None:
|
|
253
|
+
self.workers.cancel_all()
|
|
254
|
+
|
|
255
|
+
def action_clear(self) -> None:
|
|
256
|
+
chat = self.query_one(ChatContainer)
|
|
257
|
+
chat.remove_children()
|
|
258
|
+
|
|
259
|
+
def action_toggle_dark(self) -> None:
|
|
260
|
+
self.dark = not self.dark
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_app() -> None:
|
|
264
|
+
app = SAAssistantApp()
|
|
265
|
+
app.run()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
run_app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|