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.
Files changed (33) hide show
  1. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/PKG-INFO +2 -1
  2. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/pyproject.toml +4 -2
  3. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/PKG-INFO +2 -1
  4. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/SOURCES.txt +2 -0
  5. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/entry_points.txt +1 -0
  6. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/requires.txt +1 -0
  7. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/top_level.txt +1 -0
  8. sa_assistant-0.2.0/src/tui/__init__.py +5 -0
  9. sa_assistant-0.2.0/src/tui/app.py +269 -0
  10. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/README.md +0 -0
  11. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/setup.cfg +0 -0
  12. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/__init__.py +0 -0
  13. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/orchestrator.py +0 -0
  14. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agents/specialists.py +0 -0
  15. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/__init__.py +0 -0
  16. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/discovery.py +0 -0
  17. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/errors.py +0 -0
  18. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/models.py +0 -0
  19. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/parser.py +0 -0
  20. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/prompt.py +0 -0
  21. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/agentskills/tool.py +0 -0
  22. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/__init__.py +0 -0
  23. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/callback.py +0 -0
  24. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/components.py +0 -0
  25. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/console.py +0 -0
  26. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/cli/mdstream.py +0 -0
  27. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/mcp_client/client.py +0 -0
  28. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/model/__init__.py +0 -0
  29. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/model/load.py +0 -0
  30. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/prompts/__init__.py +0 -0
  31. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/prompts/system_prompts.py +0 -0
  32. {sa_assistant-0.1.1 → sa_assistant-0.2.0}/src/sa_assistant.egg-info/dependency_links.txt +0 -0
  33. {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.1.1
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.1.1"
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.1.1
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"
@@ -26,4 +26,6 @@ src/sa_assistant.egg-info/dependency_links.txt
26
26
  src/sa_assistant.egg-info/entry_points.txt
27
27
  src/sa_assistant.egg-info/requires.txt
28
28
  src/sa_assistant.egg-info/top_level.txt
29
+ src/tui/__init__.py
30
+ src/tui/app.py
29
31
  test/test_main.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  sa-assistant = main:run_cli
3
+ sa-assistant-tui = main:run_tui
@@ -8,6 +8,7 @@ pyyaml>=6.0
8
8
  rich>=13.0.0
9
9
  strands-agents>=1.13.0
10
10
  strands-agents-tools>=0.2.16
11
+ textual>=0.85.0
11
12
 
12
13
  [dev]
13
14
  pytest>=7.0.0
@@ -5,3 +5,4 @@ mcp_client
5
5
  model
6
6
  prompts
7
7
  skills
8
+ tui
@@ -0,0 +1,5 @@
1
+ """SA Assistant TUI - Textual-based Terminal User Interface."""
2
+
3
+ from .app import SAAssistantApp, run_app
4
+
5
+ __all__ = ["SAAssistantApp", "run_app"]
@@ -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