deep-agent-cli 1.0.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.
@@ -0,0 +1,298 @@
1
+ import sys
2
+ import traceback
3
+ from typing import Any, Dict, List, Generator
4
+ from ui.models.ChatModel import ChatModel
5
+ from ui.views.ChatView import ChatView
6
+ from app.service.ChatService import ChatService
7
+ from app.service.ConfigService import ConfigService
8
+
9
+
10
+ class ChatController:
11
+ """
12
+ Controller đóng vai trò điều phối thông tin giữa Model (trạng thái hệ thống) và View (giao diện Rich).
13
+ Xử lý các sự kiện đầu vào, điều khiển vòng lặp tương tác chính, bắt lỗi và hỗ trợ ngắt suy nghĩ bằng Ctrl+C.
14
+ """
15
+
16
+ def __init__(self, model: ChatModel, view: ChatView):
17
+ self.model = model
18
+ self.view = view
19
+ self.chat_service = ChatService()
20
+ self.config_service = ConfigService()
21
+
22
+ def start_chat_loop(self) -> None:
23
+ """Khởi động vòng lặp tương tác CLI chính."""
24
+ self.view.print_banner()
25
+ self.view.print_system_info(self.model.get_llm_config(), self.model.thread_id)
26
+
27
+ while True:
28
+ try:
29
+ user_input = self.view.prompt_user().strip()
30
+ if not user_input:
31
+ continue
32
+
33
+ # Xử lý các câu lệnh đặc biệt (bắt đầu bằng gấu gạch chéo '/')
34
+ if user_input.startswith("/"):
35
+ should_exit = self._handle_slash_command(user_input)
36
+ if should_exit:
37
+ self.view.print_info("Cảm ơn bạn đã sử dụng Deep Agent. Tạm biệt!")
38
+ break
39
+ continue
40
+
41
+ # Thêm tin nhắn của người dùng vào Model và View
42
+ self.model.add_message("user", user_input)
43
+ self.view.print_human_message(user_input)
44
+
45
+ # Gửi tin nhắn đến Agent và xử lý luồng stream suy nghĩ
46
+ self.view.print_thinking_start()
47
+ generator = self.chat_service.send_message(self.model.thread_id, user_input)
48
+ self._consume_generator(generator)
49
+
50
+ except KeyboardInterrupt:
51
+ # Bắt Ctrl+C ở mức ngoài cùng để tránh crash nếu người dùng nhấn trong lúc nhập
52
+ self.view.print_warning("\n[Vòng lặp] Đã nhấn Ctrl+C. Để thoát, gõ '/exit'.")
53
+ except Exception as e:
54
+ self.view.print_error(f"Lỗi xảy ra trong vòng lặp chính: {e}")
55
+ # Hỗ trợ debug nếu cần
56
+ # traceback.print_exc()
57
+
58
+ def _consume_generator(self, generator: Generator[Dict[str, Any], None, None]) -> None:
59
+ """
60
+ Lặp qua generator từ agent.stream để phân tích và hiển thị các chunk.
61
+ Hỗ trợ bắt KeyboardInterrupt (Ctrl+C) để ngắt luồng suy nghĩ một cách an toàn.
62
+ """
63
+ thinking_displayed = False
64
+ try:
65
+ for chunk in generator:
66
+ if not thinking_displayed:
67
+ # Đã có chunk đầu tiên, bắt đầu stream suy nghĩ
68
+ thinking_displayed = True
69
+
70
+ self._process_stream_chunk(chunk)
71
+
72
+ except KeyboardInterrupt:
73
+ # Bắt Ctrl+C trong khi stream tin nhắn của agent để ngắt luồng suy nghĩ
74
+ self.view.print_thinking_end()
75
+ self.view.print_warning("Đã ngắt luồng suy nghĩ của Agent bằng [bold red]Ctrl+C[/bold red].")
76
+
77
+ # Cập nhật trạng thái kết thúc trong Model
78
+ self.model.update_status("completed")
79
+ except Exception as e:
80
+ self.view.print_thinking_end()
81
+ self.view.print_error(f"Gặp lỗi khi stream phản hồi từ Agent: {e}")
82
+ self.model.update_status("error")
83
+
84
+ def _process_stream_chunk(self, chunk: Dict[str, Any]) -> None:
85
+ """
86
+ Phân tích cú pháp chunk từ agent.stream và gọi View render tương ứng.
87
+ """
88
+ if not isinstance(chunk, dict):
89
+ return
90
+
91
+ # 1. Nếu là chunk trạng thái cuối cùng được sinh ra từ ChatService
92
+ if "status" in chunk:
93
+ self.view.print_thinking_end() # Kết thúc phần in suy nghĩ
94
+ status = chunk["status"]
95
+
96
+ if status == "waiting_approval":
97
+ pending_actions = chunk.get("pending_actions", [])
98
+ self.model.update_status("waiting_approval", pending_actions)
99
+ self._handle_human_in_the_loop()
100
+
101
+ elif status == "completed":
102
+ response = chunk.get("response", "")
103
+ normalized_response = self._normalize_response(response)
104
+ self.view.print_ai_response(normalized_response)
105
+ self.model.add_message("ai", normalized_response)
106
+ self.model.update_status("completed")
107
+ return
108
+
109
+ # 2. Chunk cập nhật từ LangGraph model node (luồng suy nghĩ & tool calls)
110
+ if "model" in chunk:
111
+ model_data = chunk["model"]
112
+ if isinstance(model_data, dict) and "messages" in model_data:
113
+ for msg in model_data["messages"]:
114
+
115
+ # Trích xuất các tool calls
116
+ if hasattr(msg, "tool_calls") and msg.tool_calls:
117
+ self.view.print_thinking_end()
118
+ for tc in msg.tool_calls:
119
+ tool_name = tc.get("name", "")
120
+ tool_args = tc.get("args", {})
121
+ tool_id = tc.get("id", "")
122
+ self.view.print_tool_call(tool_name, tool_args, tool_id)
123
+ # Thêm log tool call vào history của model
124
+ self.model.add_message("ai", f"[Tool Call: {tool_name}]", additional_kwargs={"tool_calls": msg.tool_calls})
125
+
126
+ # Trích xuất luồng suy nghĩ (thinking)
127
+ if hasattr(msg, "content"):
128
+ content = msg.content
129
+ if isinstance(content, list):
130
+ for part in content:
131
+ if isinstance(part, dict) and part.get("type") == "thinking":
132
+ thinking_text = part.get("thinking", "")
133
+ self.view.print_thinking_chunk(thinking_text)
134
+ # Trong tương lai nếu có stream text bình thường thì in ra ở đây
135
+
136
+ # 3. Chunk cập nhật từ LangGraph tools node (kết quả chạy tool)
137
+ if "tools" in chunk:
138
+ tools_data = chunk["tools"]
139
+ if isinstance(tools_data, dict) and "messages" in tools_data:
140
+ for msg in tools_data["messages"]:
141
+ if hasattr(msg, "content") and msg.content:
142
+ tool_name = getattr(msg, "name", "Công cụ")
143
+ self.view.print_tool_response(tool_name, msg.content)
144
+ # Thêm log kết quả vào history của model
145
+ self.model.add_message("tool", msg.content, additional_kwargs={"name": tool_name})
146
+
147
+ def _normalize_response(self, response: Any) -> str:
148
+ """
149
+ Chuẩn hóa phản hồi từ Agent thành một chuỗi (string) duy nhất.
150
+ Tránh lỗi khi response có kiểu là list hoặc các cấu trúc phức tạp từ LangChain/LangGraph.
151
+ """
152
+ if response is None:
153
+ return ""
154
+ if isinstance(response, str):
155
+ return response
156
+ if isinstance(response, list):
157
+ text_parts = []
158
+ for part in response:
159
+ if isinstance(part, str):
160
+ text_parts.append(part)
161
+ elif isinstance(part, dict):
162
+ if part.get("type") == "text":
163
+ text_parts.append(part.get("text", ""))
164
+ elif part.get("type") == "thinking":
165
+ # Bỏ qua phần suy nghĩ do đã hiển thị trước đó trong quá trình stream
166
+ pass
167
+ else:
168
+ text_parts.append(str(part))
169
+
170
+ joined = "".join(text_parts).strip()
171
+ if not joined:
172
+ # Fallback: Trích xuất toàn bộ text hoặc dùng str() nếu list không chứa text block chuẩn
173
+ fallback_parts = []
174
+ for part in response:
175
+ if isinstance(part, dict) and "thinking" in part:
176
+ continue
177
+ fallback_parts.append(str(part))
178
+ joined = " ".join(fallback_parts).strip()
179
+ return joined
180
+
181
+ return str(response)
182
+
183
+ def _handle_human_in_the_loop(self) -> None:
184
+ """Xử lý quá trình Human-in-the-loop bằng cách thu thập ý kiến người dùng và gửi lệnh tiếp tục."""
185
+ pending_actions = self.model.pending_actions
186
+ if not pending_actions:
187
+ return
188
+
189
+ # Hiển thị UI phê duyệt và lấy quyết định
190
+ decisions = self.view.prompt_human_in_the_loop(pending_actions)
191
+
192
+ # Gửi quyết định phê duyệt lại cho ChatService để chạy tiếp
193
+ self.view.print_thinking_start()
194
+ generator = self.chat_service.handle_approval(self.model.thread_id, decisions)
195
+ self._consume_generator(generator)
196
+
197
+ def _handle_slash_command(self, command_line: str) -> bool:
198
+ """
199
+ Xử lý các câu lệnh đặc biệt bắt đầu bằng '/'.
200
+ Trả về True nếu cần thoát chương trình.
201
+ """
202
+ parts = command_line.split(maxsplit=1)
203
+ command = parts[0].lower()
204
+ args = parts[1] if len(parts) > 1 else ""
205
+
206
+ if command in ["/exit", "/quit"]:
207
+ return True
208
+
209
+ elif command == "/help":
210
+ self.view.print_help()
211
+
212
+ elif command == "/config":
213
+ self.view.print_system_info(self.model.get_llm_config(), self.model.thread_id)
214
+
215
+ elif command == "/provider":
216
+ if not args:
217
+ self.view.print_error("Thiếu tham số tên Provider. Ví dụ: /provider google")
218
+ return False
219
+ try:
220
+ self.config_service.change_provider(args)
221
+ self.view.print_info(f"Đã thay đổi LLM Provider sang: [bold]{args}[/bold]")
222
+ except Exception as e:
223
+ self.view.print_error(f"Không thể thay đổi Provider: {e}")
224
+
225
+ elif command == "/model":
226
+ if not args:
227
+ self.view.print_error("Thiếu tham số tên Model. Ví dụ: /model models/gemma-4-31b-it")
228
+ return False
229
+ try:
230
+ self.config_service.change_model(args)
231
+ self.view.print_info(f"Đã thay đổi Model sang: [bold]{args}[/bold]")
232
+ except Exception as e:
233
+ self.view.print_error(f"Không thể thay đổi Model: {e}")
234
+
235
+ elif command == "/apikey":
236
+ if not args:
237
+ self.view.print_error("Thiếu tham số API Key. Ví dụ: /apikey AIzaSy...")
238
+ return False
239
+ try:
240
+ self.config_service.change_api_key(args)
241
+ self.view.print_info("Đã thay đổi API Key thành công.")
242
+ except Exception as e:
243
+ self.view.print_error(f"Không thể thay đổi API Key: {e}")
244
+
245
+ elif command == "/temp":
246
+ if not args:
247
+ self.view.print_error("Thiếu tham số temperature. Ví dụ: /temp 0.5")
248
+ return False
249
+ try:
250
+ temp_val = float(args)
251
+ if 0.0 <= temp_val <= 2.0:
252
+ self.config_service.change_temperature(temp_val)
253
+ self.view.print_info(f"Đã thay đổi Temperature sang: [bold]{temp_val}[/bold]")
254
+ else:
255
+ self.view.print_error("Giá trị temperature phải nằm trong khoảng từ 0.0 đến 2.0")
256
+ except ValueError:
257
+ self.view.print_error("Giá trị temperature phải là một số thực. Ví dụ: /temp 0.7")
258
+ except Exception as e:
259
+ self.view.print_error(f"Không thể thay đổi Temperature: {e}")
260
+
261
+ elif command == "/history":
262
+ history = self.chat_service.get_history(self.model.thread_id)
263
+ if not history:
264
+ self.view.print_info("Lịch sử hội thoại hiện tại trống.")
265
+ else:
266
+ self.view.print_info("--- LỊCH SỬ HỘI THOẠI TRONG THREAD ---")
267
+ for msg in history:
268
+ role = getattr(msg, "type", "ai")
269
+ content = getattr(msg, "content", "")
270
+
271
+ if role == "human":
272
+ self.view.console.print(f"[bold turquoise2]👤 Bạn:[/bold turquoise2] {content}")
273
+ elif role == "ai":
274
+ if isinstance(content, list):
275
+ for part in content:
276
+ if isinstance(part, dict):
277
+ if part.get("type") == "thinking":
278
+ self.view.console.print(f"[italic dim yellow]🤔 Suy nghĩ:[/italic dim yellow] {part.get('thinking', '')}")
279
+ elif part.get("type") == "text":
280
+ self.view.console.print(f"[bold spring_green3]🤖 Agent:[/bold spring_green3] {part.get('text', '')}")
281
+ elif isinstance(part, str):
282
+ self.view.console.print(f"[bold spring_green3]🤖 Agent:[/bold spring_green3] {part}")
283
+ else:
284
+ self.view.console.print(f"[bold spring_green3]🤖 Agent:[/bold spring_green3] {content}")
285
+ elif role == "tool":
286
+ self.view.console.print(f"[bold dodger_blue1]🔧 [Tool {getattr(msg, 'name', '')}]:[/bold dodger_blue1] {content[:200]}...")
287
+ self.view.print_info("---------------------------------------")
288
+
289
+ elif command == "/clear":
290
+ import os
291
+ os.system('cls' if os.name == 'nt' else 'clear')
292
+ self.view.print_banner()
293
+ self.view.print_system_info(self.model.get_llm_config(), self.model.thread_id)
294
+
295
+ else:
296
+ self.view.print_error(f"Lệnh không hợp lệ: '{command}'. Nhập '/help' để xem các lệnh khả dụng.")
297
+
298
+ return False
@@ -0,0 +1 @@
1
+ # Package ui.controllers
ui/models/ChatModel.py ADDED
@@ -0,0 +1,58 @@
1
+ import uuid
2
+ from typing import Any, Dict, List, Optional
3
+ from app.core.LLM import LLMConfig
4
+ from app.service.ConfigService import ConfigService
5
+
6
+
7
+ class ChatModel:
8
+ """
9
+ Model quản lý trạng thái của giao diện Chatbot CLI.
10
+ Lưu trữ thông tin về phiên hội thoại, lịch sử tin nhắn và trạng thái hiện tại (completed, waiting_approval, ...).
11
+ """
12
+
13
+ def __init__(self, thread_id: Optional[str] = None):
14
+ # Tạo thread_id ngẫu nhiên nếu không truyền vào
15
+ self.thread_id: str = thread_id or f"cli-thread-{uuid.uuid4().hex[:8]}"
16
+
17
+ # Trạng thái hiện tại của phiên hội thoại (completed, waiting_approval, error)
18
+ self.status: str = "completed"
19
+
20
+ # Danh sách các action/tool call đang chờ phê duyệt
21
+ self.pending_actions: List[Dict[str, Any]] = []
22
+
23
+ # Lịch sử các tin nhắn hiển thị trên giao diện (human, ai, tool, system)
24
+ self.messages: List[Dict[str, Any]] = []
25
+
26
+ # Service quản lý cấu hình
27
+ self._config_service = ConfigService()
28
+
29
+ @property
30
+ def thread_id_short(self) -> str:
31
+ """Trả về thread_id dạng rút gọn để hiển thị trên UI."""
32
+ return self.thread_id
33
+
34
+ def get_llm_config(self) -> LLMConfig:
35
+ """Lấy cấu hình LLM hiện tại từ ConfigService."""
36
+ return self._config_service.get_llm_config()
37
+
38
+ def update_status(self, status: str, pending_actions: Optional[List[Dict[str, Any]]] = None) -> None:
39
+ """Cập nhật trạng thái phiên hội thoại và danh sách hành động chờ duyệt."""
40
+ self.status = status
41
+ self.pending_actions = pending_actions or []
42
+
43
+ def clear_pending_actions(self) -> None:
44
+ """Xóa danh sách hành động chờ duyệt."""
45
+ self.pending_actions = []
46
+ self.status = "completed"
47
+
48
+ def add_message(self, role: str, content: str, additional_kwargs: Optional[Dict[str, Any]] = None) -> None:
49
+ """Thêm tin nhắn mới vào lịch sử."""
50
+ self.messages.append({
51
+ "role": role,
52
+ "content": content,
53
+ "additional_kwargs": additional_kwargs or {}
54
+ })
55
+
56
+ def clear_history(self) -> None:
57
+ """Xóa lịch sử tin nhắn."""
58
+ self.messages = []
ui/models/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Package ui.models
ui/views/ChatView.py ADDED
@@ -0,0 +1,237 @@
1
+ import json
2
+ from typing import Any, Dict, List, Tuple
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.markdown import Markdown
7
+ from rich.text import Text
8
+ from rich.prompt import Prompt, Confirm
9
+ from rich.box import ROUNDED, DOUBLE
10
+ from app.core.LLM import LLMConfig
11
+
12
+
13
+ class ChatView:
14
+ """
15
+ View chịu trách nhiệm hiển thị giao diện người dùng ra Terminal bằng thư viện Rich.
16
+ Đảm bảo tính trực quan, thẩm mỹ cao (màu sắc HSL hài hòa, các panel nổi bật, tables chi tiết).
17
+ """
18
+
19
+ def __init__(self):
20
+ self.console = Console()
21
+
22
+ def print_banner(self) -> None:
23
+ """In ra Banner chào mừng với thiết kế premium và wowed."""
24
+ banner_text = Text(justify="center")
25
+ banner_text.append("⚡ DEEP AGENT CLI ⚡\n", style="bold violet")
26
+ banner_text.append("Hệ Thống Trợ Lý Lập Trình Tự Trị Tối Tân\n", style="bold cyan")
27
+ banner_text.append("-------------------------------------------\n", style="dim magenta")
28
+ banner_text.append("Gõ ", style="dim white")
29
+ banner_text.append("/help", style="bold yellow")
30
+ banner_text.append(" để xem danh sách lệnh cấu hình hệ thống.\n", style="dim white")
31
+ banner_text.append("Nhấn ", style="dim white")
32
+ banner_text.append("Ctrl+C", style="bold red")
33
+ banner_text.append(" để ngắt suy nghĩ của Agent bất cứ lúc nào.", style="dim white")
34
+
35
+ panel = Panel(
36
+ banner_text,
37
+ title="[bold magenta]Welcome[/bold magenta]",
38
+ border_style="magenta",
39
+ box=DOUBLE,
40
+ padding=(1, 2)
41
+ )
42
+ self.console.print("\n")
43
+ self.console.print(panel)
44
+
45
+ def print_system_info(self, llm_config: LLMConfig, thread_id: str) -> None:
46
+ """Hiển thị thông tin cấu hình hiện tại của hệ thống dưới dạng bảng sleek."""
47
+ table = Table(
48
+ show_header=True,
49
+ header_style="bold cyan",
50
+ box=ROUNDED,
51
+ border_style="dim blue",
52
+ title="[bold blue]⚙️ Trạng Thái Cấu Hình Hiện Tại[/bold blue]",
53
+ title_justify="left"
54
+ )
55
+ table.add_column("Thuộc tính", style="bold white", width=15)
56
+ table.add_column("Giá trị", style="green")
57
+
58
+ table.add_row("Provider", llm_config.provider.upper())
59
+ table.add_row("Model Name", llm_config.model_name)
60
+ table.add_row("Temperature", str(llm_config.temperature))
61
+ table.add_row("Thread ID", thread_id)
62
+
63
+ self.console.print(table)
64
+ self.console.print("\n")
65
+
66
+ def print_help(self) -> None:
67
+ """Hiển thị hướng dẫn các lệnh điều hướng/cấu hình CLI."""
68
+ table = Table(
69
+ show_header=True,
70
+ header_style="bold yellow",
71
+ box=ROUNDED,
72
+ border_style="dim yellow",
73
+ title="[bold yellow]💡 Danh Sách Lệnh CLI[/bold yellow]",
74
+ title_justify="left"
75
+ )
76
+ table.add_column("Lệnh", style="bold cyan")
77
+ table.add_column("Mô tả", style="white")
78
+
79
+ table.add_row("/help", "Hiển thị bảng hướng dẫn này")
80
+ table.add_row("/config", "Xem chi tiết cấu hình hiện tại")
81
+ table.add_row("/provider <tên>", "Thay đổi LLM Provider (google, openai, anthropic, ollama)")
82
+ table.add_row("/model <tên>", "Thay đổi Model tương ứng (ví dụ: gpt-4o, models/gemma-4-31b-it)")
83
+ table.add_row("/apikey <khóa>", "Thay đổi API Key của provider hiện hành")
84
+ table.add_row("/temp <số>", "Thay đổi độ sáng tạo temperature (từ 0.0 đến 2.0)")
85
+ table.add_row("/history", "Xem lại lịch sử trò chuyện trong thread hiện tại")
86
+ table.add_row("/clear", "Xóa màn hình console và in lại banner")
87
+ table.add_row("/exit hoặc /quit", "Thoát ứng dụng CLI")
88
+
89
+ self.console.print(table)
90
+ self.console.print("\n")
91
+
92
+ def print_human_message(self, message: str) -> None:
93
+ """In tin nhắn của người dùng."""
94
+ self.console.print(f"\n[bold turquoise2]👤 Bạn:[/bold turquoise2] {message}")
95
+
96
+ def print_thinking_start(self) -> None:
97
+ """In thông báo bắt đầu suy nghĩ của agent."""
98
+ self.console.print("[italic dim yellow]🤔 Agent đang suy nghĩ...[/italic dim yellow]", end="\n")
99
+
100
+ def print_thinking_chunk(self, chunk_text: str) -> None:
101
+ """Stream luồng suy nghĩ (thinking) ra màn hình với màu dim yellow đặc trưng."""
102
+ self.console.print(chunk_text, style="italic dim yellow", end="\n")
103
+
104
+ def print_thinking_end(self) -> None:
105
+ """Kết thúc luồng suy nghĩ."""
106
+ self.console.print("\n")
107
+
108
+ def print_tool_call(self, tool_name: str, tool_args: Dict[str, Any], tool_id: str) -> None:
109
+ """Hiển thị chi tiết tool được gọi bằng panel nổi bật có màu cam/magenta."""
110
+ args_str = json.dumps(tool_args, indent=2, ensure_ascii=False)
111
+
112
+ table = Table(show_header=False, box=ROUNDED, border_style="bold orange3")
113
+ table.add_row("[bold orange3]🔧 CÔNG CỤ ĐƯỢC GỌI[/bold orange3]", f"[bold cyan]{tool_name}[/bold cyan]")
114
+ table.add_row("[bold white]ID yêu cầu[/bold white]", f"[dim]{tool_id}[/dim]")
115
+ table.add_row("[bold white]Tham số truyền vào[/bold white]", f"[green]{args_str}[/green]")
116
+
117
+ self.console.print("\n")
118
+ self.console.print(Panel(table, border_style="bold orange3", expand=False))
119
+ self.console.print("\n")
120
+
121
+ def print_tool_response(self, tool_name: str, content: str) -> None:
122
+ """Hiển thị kết quả trả về từ công cụ bằng màu xanh lam (blue)."""
123
+ content_preview = content
124
+ if len(content) > 300:
125
+ content_preview = content[:300] + "... (kết quả bị cắt bớt để hiển thị gọn hơn)"
126
+
127
+ panel = Panel(
128
+ content_preview,
129
+ title=f"[bold dodger_blue1]📥 Kết Quả Từ Công Cụ: {tool_name}[/bold dodger_blue1]",
130
+ border_style="dodger_blue1",
131
+ expand=False,
132
+ padding=(0, 2)
133
+ )
134
+ self.console.print(panel)
135
+ self.console.print("\n")
136
+
137
+ def print_ai_response(self, response: Any) -> None:
138
+ """In phản hồi cuối cùng của AI bằng Markdown tuyệt đẹp."""
139
+ self.console.print("\n[bold spring_green3]🤖 Agent:[/bold spring_green3]")
140
+
141
+ # Đảm bảo response là string để tránh lỗi Markdown(list)
142
+ if not isinstance(response, str):
143
+ if isinstance(response, list):
144
+ text_parts = []
145
+ for part in response:
146
+ if isinstance(part, str):
147
+ text_parts.append(part)
148
+ elif isinstance(part, dict):
149
+ if part.get("type") == "text":
150
+ text_parts.append(part.get("text", ""))
151
+ else:
152
+ text_parts.append(str(part))
153
+ response = "".join(text_parts)
154
+ else:
155
+ response = str(response)
156
+
157
+ markdown_response = Markdown(response)
158
+ self.console.print(markdown_response)
159
+ self.console.print("\n")
160
+
161
+ def print_warning(self, message: str) -> None:
162
+ """In ra cảnh báo hệ thống."""
163
+ self.console.print(f"[bold yellow]⚠️ Cảnh báo:[/bold yellow] {message}")
164
+
165
+ def print_error(self, message: str) -> None:
166
+ """In ra lỗi hệ thống."""
167
+ self.console.print(f"[bold red]❌ Lỗi:[/bold red] {message}")
168
+
169
+ def print_info(self, message: str) -> None:
170
+ """In ra thông tin hệ thống."""
171
+ self.console.print(f"[bold green]ℹ️ Thông tin:[/bold green] {message}")
172
+
173
+ def prompt_user(self) -> str:
174
+ """Yêu cầu người dùng nhập câu hỏi hoặc câu lệnh."""
175
+ try:
176
+ return Prompt.ask("\n[bold white]✍️ Nhập tin nhắn hoặc lệnh[/bold white]")
177
+ except KeyboardInterrupt:
178
+ # Cho phép nhấn Ctrl+C ở prompt để thoát
179
+ return "/exit"
180
+
181
+ def prompt_human_in_the_loop(self, pending_actions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
182
+ """
183
+ Hiển thị giao diện Human-in-the-loop để người dùng chọn Approve / Reject.
184
+ Trả về danh sách các quyết định (decisions).
185
+ """
186
+ self.console.print("\n")
187
+ self.console.print(
188
+ Panel(
189
+ "[bold blink yellow]🛑 PHÊ DUYỆT HÀNH ĐỘNG (HUMAN-IN-THE-LOOP)[/bold blink yellow]\n"
190
+ "Agent đang yêu cầu thực hiện các hành động hệ thống nhạy cảm bên dưới.\n"
191
+ "Bạn cần xem xét và cho phép thực thi hoặc từ chối hành động.",
192
+ border_style="yellow",
193
+ box=DOUBLE
194
+ )
195
+ )
196
+
197
+ decisions = []
198
+ for action in pending_actions:
199
+ action_id = action.get("id", "N/A")
200
+ action_name = action.get("action", action.get("name", "N/A"))
201
+ action_args = action.get("args", {})
202
+
203
+ # Vẽ bảng hiển thị chi tiết hành động đang chờ phê duyệt
204
+ table = Table(show_header=True, header_style="bold yellow", box=ROUNDED, border_style="yellow")
205
+ table.add_column("Trường dữ liệu", style="bold white")
206
+ table.add_column("Chi tiết", style="cyan")
207
+
208
+ table.add_row("ID Hành động", action_id)
209
+ table.add_row("Tên Công cụ", action_name)
210
+ table.add_row("Tham số chi tiết", json.dumps(action_args, indent=2, ensure_ascii=False))
211
+
212
+ self.console.print(table)
213
+
214
+ # Đọc quyết định của người dùng: A (Approve) hoặc R (Reject)
215
+ decision_choice = Prompt.ask(
216
+ f"[bold yellow]Quyết định cho công cụ [cyan]{action_name}[/cyan][/bold yellow]",
217
+ choices=["a", "r", "approve", "reject"],
218
+ default="a"
219
+ ).lower()
220
+
221
+ approved = decision_choice in ["a", "approve"]
222
+
223
+ # Hỏi lý do (tuỳ chọn) nếu từ chối hoặc phê duyệt
224
+ reason = ""
225
+ if not approved:
226
+ reason = Prompt.ask("[bold red]Lý do từ chối (tuỳ chọn)[/bold red]", default="")
227
+ else:
228
+ reason = Prompt.ask("[bold green]Lời nhắn gửi cho Agent (tuỳ chọn)[/bold green]", default="")
229
+
230
+ decisions.append({
231
+ "type": "approve" if approved else "reject",
232
+ "action_request_id": action_id,
233
+ "approved": approved,
234
+ "reason": reason if reason else None
235
+ })
236
+
237
+ return decisions
ui/views/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Package ui.views