Chapter 11 — Production-Grade Rich CLI & Interactive REPL Shell
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 1 ถึง Day 10 เราสร้างอวัยวะครบหมดทุกส่วนแล้ว: มีตาอ่านไฟล์ (Day 4), มีเท้าสั่งรันเทส (Day 5), มีสมอง ReAct Loop (Day 6), มีความจำระยะสั้น Sliding Window (Day 8), มีความจำระยะยาว ChromaDB (Day 9), และมีมือแก้โค้ดพร้อม Diff + Rollback (Day 10)
แต่ปัญหาคอขาดบาดตายของการใช้งานจริงคือ: ใน Day 7 เราใช้แค่คำสั่ง print() ดิบๆ บน Terminal ธรรมดา ซึ่งเวลา Agent ใช้เวลาคิด 10-15 วินาที หน้าจอ Terminal จะ **"ค้างนิ่งสนิท ไร้สัญญาณชีพ (Freeze)"** มนุษย์ไม่รู้เลยว่า AI กำลังคิด กำลังรันเทส หรือเครื่อง Mac ค้างไปแล้ว! และผลลัพธ์ที่พิมพ์ออกมาก็เป็นข้อความยาวเหยียด ไม่มีสีสัน ไม่มีตารางสรุป และอ่านโค้ดแทบไม่รู้เรื่อง!
Day 11 เราจึงต้องสร้าง Production-Grade Terminal Cockpit ด้วยไลบรารีระดับอุตสาหกรรม rich โดยยกเครื่องหน้าจอห้องบังคับการใหม่ทั้งหมด:
1. Animated Live Spinners: ตัวหมุนอนิเมชั่นรายงานสถานะ Real-time ขณะ AI กำลังคิดหรือรันคำสั่ง
2. Syntax Highlighting & Markdown Panels: แสดงผลลัพธ์โค้ด กรอบข้อความ และตารางสรุปผลอย่างสวยงาม
3. Graceful REPL Shell with History: โหมดโต้ตอบต่อเนื่องที่รองรับการยกเลิก Ctrl+C อย่างสง่างาม ไม่พ่น Error แดงเถือก!
โค้ดประจำ Day 11 จะติดตั้ง rich เพิ่มใน requirements.txt สร้างโมดูลควบคุมหน้าจอแสดงผล core/ui.py และอัปเกรด main.py สู่ห้องบังคับการระดับ Production:
ทำหน้าที่เป็น Presentation Layer Manager รวบรวมเมธอดการวาด Banner ต้อนรับ, การสร้าง Live Spinner ขณะรอ Ollama หรือ Maven, การเรนเดอร์ Markdown Panels, ตารางสรุปการทำงานของ Tools, และการจัดแสดง Unified Diff ด้วยสีเขียว/แดงอย่างมืออาชีพ
ยกระดับจาก CLI ข้อความดิบ สู่ Interactive Cockpit Shell มีแถบสถานะชัดเจนว่ากำลังรันโมเดลไหน, ใช้หน่วยความจำเท่าไหร่, แสดงข้อคิด (Thought) ในกล่องสีม่วง, แสดง Tool Call ในกล่องสีฟ้า และแสดงผลลัพธ์สุดท้ายในกล่องสีเขียว
รับคำสั่งพร้อมปุ่ม Escape/Quit ดักจับ Ctrl+C นุ่มนวล
หมุนอนิเมชั่น Real-time แจ้งว่า Agent กำลังทำอะไร ไม่ค้าง
แยกกล่อง Thought, Action, และ Observation ชัดเจน
พรีวิวโค้ดไฮไลต์สี และตารางสรุปผลลัพธ์ระดับ Enterprise
1. เมื่อสั่งรันเทส 20 วินาที หน้าจอค้างนิ่ง มนุษย์นึกว่าโปรแกรมแฮงก์แล้วเผลอกด Ctrl+C ทำลายงาน
2. ข้อความ Thought ปนกับ Stack Trace อ่านยากมาก ตาลาย
3. เมื่อกด Ctrl+C จะเกิด Traceback แดงเถือกพังทลายดูไม่เป็นมืออาชีพ
1. มี Spinner อนิเมชั่นบอกตลอด เช่น "กำลังคิดรอบที่ 2...", "กำลังรัน mvn test..." ทำให้มนุษย์อุ่นใจ
2. แสดง Thought เป็นกล่องสีเหลือง, Action เป็นกล่องสีฟ้า และผลลัพธ์เป็นกล่องสีเขียว กวาดสายตาตรวจได้ใน 2 วินาที
3. มี Diff สีสันสดใส และรองรับการปิดโปรแกรมอย่างสง่างาม 100%
# 1.1 ไฟล์ requirements.txt (อัปเดตเพิ่ม rich)
ollama>=0.4.0
chromadb>=0.4.24
rich>=13.7.0
# -------------------------------------------------------------
# 1.2 ไฟล์ core/__init__.py (อัปเดต Export TerminalUI)
"""
Core Package ของ Autonomous Software Engineering Agent
[อัปเดต Day 11]: เพิ่ม TerminalUI สำหรับ Production CLI Cockpit
"""
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser, JSONDecodeError
from core.dispatcher import ToolDispatcher, ToolExecutionError
from core.engine import ReActAgentEngine, MaxIterationsReachedError
from core.ui import TerminalUI
__all__ = [
"OllamaLLMClient",
"StructuredOutputParser",
"JSONDecodeError",
"ToolDispatcher",
"ToolExecutionError",
"ReActAgentEngine",
"MaxIterationsReachedError",
"TerminalUI"
]
import sys
from typing import Optional, Any
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.text import Text
class TerminalUI:
"""
ตัวจัดการการแสดงผลบน Terminal ระดับ Production (Presentation Layer)
ใช้ไลบรารี Rich เพื่อสร้างประสบการณ์ผู้ใช้เทียบเท่า CLI สากล
"""
def __init__(self):
# 1. สร้าง Console กลางสำหรับควบคุมการเรนเดอร์ทั้งหมด
self.console = Console()
def print_banner(self, model_name: str, max_iterations: int) -> None:
"""วาด Banner ต้อนรับเข้าสู่ Cockpit พร้อมข้อมูลระบบ"""
banner_text = Text()
banner_text.append("🤖 LOCAL SE AGENT COCKPIT\n", style="bold magenta")
banner_text.append("Autonomous Engineering Companion บนชิป Apple Silicon M3\n\n", style="italic slate-300")
banner_text.append(f"📦 Active Model: {model_name} | 🛡️ Guardrail Limit: {max_iterations} steps\n", style="cyan")
banner_text.append("🔒 100% Local • 0% Cloud Leak • Free Forever", style="green")
panel = Panel(
banner_text,
title="[bold yellow]Personal AI Workstation[/bold yellow]",
subtitle="[slate-500]Phase 1.5 Standard[/slate-500]",
border_style="magenta",
padding=(1, 2)
)
self.console.print(panel)
def print_thought(self, step: int, total_steps: int, thought: str) -> None:
"""แสดงกระบวนการคิด (Chain-of-Thought) ของโมเดลในกรอบสีม่วง/เหลือง"""
md = Markdown(thought)
panel = Panel(
md,
title=f"[bold yellow]💡 Step {step}/{total_steps} • Agent Thought[/bold yellow]",
border_style="yellow",
padding=(0, 1)
)
self.console.print(panel)
def print_action(self, tool_name: str, arguments: dict) -> None:
"""แสดงคำสั่งการเรียกใช้เครื่องมือ (Tool Action) พร้อมตารางพารามิเตอร์"""
table = Table(show_header=True, header_style="bold cyan", border_style="slate-800", box=None)
table.add_column("Parameter", style="cyan", width=20)
table.add_column("Value", style="white")
for k, v in arguments.items():
table.add_row(str(k), str(v))
panel = Panel(
table,
title=f"[bold cyan]🛠️ Tool Invocation: {tool_name}[/bold cyan]",
border_style="cyan",
padding=(0, 1)
)
self.console.print(panel)
def print_observation(self, observation: str) -> None:
"""แสดงผลลัพธ์จากการรันเครื่องมือ (Observation)"""
obs_str = str(observation).strip()
# ตัดทอนหากข้อความยาวเกินไป เพื่อไม่ให้รกหน้าจอ แต่ยังคงสาระสำคัญ
if len(obs_str) > 500:
display_text = obs_str[:500] + "\n\n... [ข้อความยาวเกิน 500 ตัวอักษร ถูกตัดทอนในการแสดงผล] ..."
else:
display_text = obs_str
panel = Panel(
Text(display_text, style="slate-300"),
title="[bold green]👁️ Observation Result[/bold green]",
border_style="green",
padding=(0, 1)
)
self.console.print(panel)
def print_diff(self, diff_text: str, filename: str) -> None:
"""แสดง Unified Diff พร้อมไฮไลต์สีเขียว/แดงแบบ Git"""
if not diff_text.strip():
self.console.print(f"[yellow]ℹ️ ไม่มีเนื้อหาความต่างในไฟล์ {filename}[/yellow]")
return
# ใช้ Rich Syntax ในภาษา diff เพื่อไฮไลต์ + และ - โดยอัตโนมัติ
syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
panel = Panel(
syntax,
title=f"[bold yellow]🔍 Unified Diff Preview: {filename}[/bold yellow]",
border_style="yellow",
padding=(0, 1)
)
self.console.print(panel)
def print_final_answer(self, final_answer: str) -> None:
"""แสดงผลสรุปงานสุดท้าย (Final Answer) ในกรอบสีเขียวสมบูรณ์แบบ"""
md = Markdown(final_answer)
panel = Panel(
md,
title="[bold green]🎯 Mission Accomplished • Final Report[/bold green]",
border_style="bold green",
padding=(1, 2)
)
self.console.print(panel)
def status(self, message: str):
"""Context Manager สำหรับแสดง Live Spinner หมุนวนขณะกำลังทำงาน"""
return self.console.status(f"[bold yellow]{message}[/bold yellow]", spinner="dots")
def print_error(self, message: str) -> None:
"""แสดงข้อความแจ้งเตือนความผิดพลาดในกรอบสีแดง"""
self.console.print(Panel(Text(message, style="bold red"), title="[bold red]🛑 System Alert[/bold red]", border_style="red"))
def prompt_user(self, prompt_text: str = "🧑💻 คุณ > ") -> str:
"""รับข้อความคำสั่งจากผู้ใช้"""
try:
return self.console.input(f"\n[bold magenta]{prompt_text}[/bold magenta]").strip()
except (KeyboardInterrupt, EOFError):
return "exit"
ชำแหละไวยากรณ์สำคัญใน core/ui.py
from rich.console import Console; self.console = Console()
Terminal Renderer Central: หัวใจของ Rich ที่ตรวจจับขนาดความกว้างหน้าจอ Terminal ของ Mac และรองรับ ANSI Color Codes แบบ 24-bit TrueColor
self.console.status(message, spinner="dots")
Live Background Spinner: Context Manager ที่สร้าง Thread เบื้องหลังเพื่อหมุนอนิเมชั่นจุดไข่ปลาบน Terminal ป้องกันปัญหาหน้าจอค้างนิ่งสนิทขณะรอ Ollama หรือ Maven
Syntax(diff_text, "diff", theme="monokai")
Lexer-based Code Highlight: ตัวแปลงข้อความ Diff ให้ออกสีเขียว (+) สำหรับบรรทัดเพิ่ม และสีแดง (-) สำหรับบรรทัดลบแบบเดียวกับ GitHub หรือ VS Code
import sys
import argparse
from core import (
OllamaLLMClient,
ToolDispatcher,
ReActAgentEngine,
MaxIterationsReachedError,
TerminalUI
)
from core.tools import read_file, run_maven_test, apply_code_patch, rollback_file
DEFAULT_SYSTEM_PROMPT = (
"You are an Autonomous Senior Software Engineering Agent running locally on Apple Silicon Mac.\n"
"You operate in a strict ReAct loop (Reason, Act, Observe).\n"
"You have access to tools to inspect local code, run Maven tests, and safely apply refactoring patches.\n\n"
"Available Tools:\n"
"1. read_file(filepath: string): Reads file content safely from workspace.\n"
"2. run_maven_test(project_dir: string): Runs Maven unit tests for a Java project.\n"
"3. apply_code_patch(filepath: string, new_code: string): Safely applies code patch with automatic backup and diff.\n"
"4. rollback_file(filepath: string): Instantly restores original file from .bak backup.\n\n"
"Response Rules:\n"
"- When taking action, reply strictly with JSON:\n"
"{\n"
' "thought": "Your reasoning why you take this step",\n'
' "tool": "tool_name",\n'
' "arguments": { ... }\n'
"}\n\n"
"- When task is completed, reply strictly with JSON:\n"
"{\n"
' "thought": "Summary of achievements",\n'
' "final_answer": "Comprehensive conclusion and explanation for human developer"\n'
"}\n\n"
"Respond ONLY with valid JSON."
)
def create_agent(model_name: str, max_iterations: int, ui: TerminalUI) -> ReActAgentEngine:
"""Factory Function: ประกอบร่างระบบพร้อมเชื่อมต่อ UI"""
with ui.status(f"กำลังเชื่อมต่อ Ollama Daemon และตรวจสอบโมเดล '{model_name}'..."):
client = OllamaLLMClient(model_name=model_name)
if not client.check_health():
ui.print_error(f"ไม่พบโมเดล '{model_name}' หรือ Ollama Daemon ไม่ได้เปิดทำงาน!\nกรุณารันคำสั่ง 'ollama list' หรือ 'ollama serve'")
sys.exit(1)
# ลงทะเบียน Tools ทั้ง 4 ชิ้น (Reader, Runner, Refactor, Rollback)
dispatcher = ToolDispatcher()
dispatcher.register("read_file", read_file)
dispatcher.register("run_maven_test", run_maven_test)
dispatcher.register("apply_code_patch", apply_code_patch)
dispatcher.register("rollback_file", rollback_file)
return ReActAgentEngine(client=client, dispatcher=dispatcher, max_iterations=max_iterations)
def run_single_task(agent: ReActAgentEngine, task: str, ui: TerminalUI) -> None:
"""สั่งประมวลผลโจทย์ 1 งานพร้อมแสดงผลผ่าน UI"""
ui.console.print(f"\n[bold cyan]🎯 เริ่มต้นปฏิบัติภารกิจ:[/bold cyan] [italic white]\"{task}\"[/italic white]\n" + "─" * 60)
try:
# เริ่มลูป ReAct
final_answer = agent.run(user_prompt=task, system_prompt=DEFAULT_SYSTEM_PROMPT)
ui.print_final_answer(final_answer)
except MaxIterationsReachedError as err:
ui.print_error(f"Guardrail Alert: {err}")
except KeyboardInterrupt:
ui.console.print("\n[yellow]⚠️ ผู้ใช้สั่งยกเลิกงานกลางคัน (Ctrl+C)[/yellow]")
except Exception as err:
ui.print_error(f"เกิดข้อผิดพลาดไม่คาดคิด: {err}")
def run_interactive_mode(agent: ReActAgentEngine, ui: TerminalUI) -> None:
"""เปิดโหมดห้องบังคับการ REPL Shell พูดคุยต่อเนื่อง"""
ui.console.print("\n[bold magenta]💬 เข้าสู่โหมด Interactive Cockpit Shell[/bold magenta] [slate-500](พิมพ์ 'exit' หรือ 'quit' เพื่อออก)[/slate-500]")
ui.console.print("─" * 60)
while True:
user_input = ui.prompt_user()
if not user_input:
continue
if user_input.lower() in ["exit", "quit", "q"]:
ui.console.print("\n[bold green]👋 ลาก่อนครับ! ขอบคุณที่ใช้งาน Local SE Agent Cockpit[/bold green]")
break
run_single_task(agent, user_input, ui)
def main():
parser = argparse.ArgumentParser(description="Local SE Agent Production Rich CLI")
parser.add_argument("-t", "--task", type=str, help="คำสั่งงานเดี่ยว เช่น: -t 'ตรวจบั๊กใน sample_maven_project'")
parser.add_argument("-i", "--interactive", action="store_true", help="เปิดโหมด Interactive REPL Prompt")
parser.add_argument("-m", "--model", type=str, default="qwen2.5-coder:7b", help="ชื่อโมเดล Ollama")
parser.add_argument("--max-iterations", type=int, default=6, help="จำกัดรอบสูงสุด")
args = parser.parse_args()
ui = TerminalUI()
# แสดง Banner ต้อนรับ
ui.print_banner(model_name=args.model, max_iterations=args.max_iterations)
# สร้าง Agent
agent = create_agent(model_name=args.model, max_iterations=args.max_iterations, ui=ui)
if args.task:
run_single_task(agent, args.task, ui)
elif args.interactive or len(sys.argv) == 1:
run_interactive_mode(agent, ui)
if __name__ == "__main__":
main()
import time
from core.ui import TerminalUI
def main():
ui = TerminalUI()
# 1. ทดสอบ Banner
ui.print_banner(model_name="qwen2.5-coder:7b", max_iterations=6)
time.sleep(1)
# 2. ทดสอบ Live Status Spinner
with ui.status("กำลังจำลองการคิดและรันคำสั่งเบื้องหลัง 2 วินาที..."):
time.sleep(2)
# 3. ทดสอบกล่อง Thought
ui.print_thought(
step=1,
total_steps=6,
thought="ฉันตรวจสอบพบว่า Unit Test ใน `CalculatorTest.java` ล้มเหลว เนื่องจากสูตรลดราคาผิด"
)
# 4. ทดสอบกล่อง Action Table
ui.print_action(
tool_name="apply_code_patch",
arguments={"filepath": "Calculator.java", "lines_changed": 3}
)
# 5. ทดสอบพรีวิว Unified Diff
sample_diff = (
"--- a/Calculator.java\n"
"+++ b/Calculator.java\n"
"@@ -3,2 +3,2 @@\n"
"- return price - (discount * 2.0);\n"
"+ return price - discount;\n"
)
ui.print_diff(diff_text=sample_diff, filename="Calculator.java")
# 6. ทดสอบกล่อง Observation
ui.print_observation("BUILD SUCCESS: การรันชุดทดสอบผ่าน 100% เรียบร้อยแล้ว!")
# 7. ทดสอบกล่อง Final Report
ui.print_final_answer(
"### สรุปผลการปฏิบัติงาน\n"
"- แก้ไขบั๊กใน `Calculator.java` เรียบร้อย\n"
"- รัน `mvn test` ผ่านฉลุย 100%\n"
"- สร้างสำเนาแบ็กอัป `.bak` เรียบร้อย ปลอดภัย 100%"
)
print("\n🎉 ผ่านการทดสอบคอมโพเนนต์ UI ทั้งหมด 100%! สวยงามระดับ Copilot Production Standard!")
if __name__ == "__main__":
main()
เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งไลบรารี rich และเปิดใช้งานห้องบังคับการใหม่ของคุณทันที:
cd ~/se_agent
source .venv/bin/activate
pip install "rich>=13.7.0"
ไลบรารี Rich มีขนาดเบามาก ติดตั้งเสร็จในเวลาไม่กี่วินาทีบน Mac
touch core/ui.py test_rich_ui.py
- อัปเดตไฟล์ requirements.txt และ core/__init__.py ตาม ส่วนที่ 4 (ไฟล์ที่ 1)
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน core/ui.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปอัปเดตทับไฟล์ main.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 4) ไปวางใน test_rich_ui.py
python test_rich_ui.py
🎉 ผลลัพธ์: คุณจะเห็น Terminal เปล่งประกายด้วย Banner สีม่วง/ทอง, อนิเมชั่น Spinner จุดไข่ปลาหมุนวน, กล่อง Thought สีเหลือง, กล่อง Action สีฟ้า, ตารางพารามิเตอร์ และ Unified Diff สีเขียว/แดงอย่างสวยงาม!
python main.py --interactive
พิมพ์คำสั่งคุยกับ Agent เช่น "ช่วยตรวจบั๊กใน sample_maven_project แล้วแก้ให้เทสผ่าน" แล้วชม AI ทำงานแบบ Real-time พร้อมกราฟิกสวยงามได้เลย!
ข้อที่ 1: วัตถุประสงค์หลักของการนำไลบรารี `rich` มาสร้าง Production-Grade CLI ใน Day 11 คืออะไร?
ข้อที่ 2: ปัญหาคลาสสิกของการใช้คำสั่ง `print()` ธรรมดาขณะ Agent กำลังคิดหรือรันเทสนานๆ คืออะไร?
ข้อที่ 3: ฟังก์ชัน `console.status()` ในไลบรารี Rich ทำหน้าที่อะไรในระดับสถาปัตยกรรม?
ข้อที่ 4: คลาสใดใน Rich ที่ใช้สำหรับไฮไลต์สีไวยากรณ์โค้ดและข้อความ Unified Diff (เช่น บรรทัด + สีเขียว และ - สีแดง)?
ข้อที่ 5: การแยกโมดูล `core/ui.py` ออกจาก `core/engine.py` สอดคล้องกับหลักการออกแบบซอฟต์แวร์ข้อใด?
ข้อที่ 6: การแสดงผลข้อคิด (Thought) ในกรอบสีเหลือง และคำสั่งเรียก Tool ในกรอบสีฟ้า ช่วยมนุษย์ได้อย่างไร?
ข้อที่ 7: เมื่อผู้ใช้กด `Ctrl+C` (KeyboardInterrupt) ใน Terminal REPL Shell โค้ดที่ดีควรมีพฤติกรรมอย่างไร?
ข้อที่ 8: คอมโพเนนต์ `rich.table.Table` ถูกนำมาใช้แสดงผลส่วนใดใน TerminalUI ของเรา?
ข้อที่ 9: คอมโพเนนต์ `rich.markdown.Markdown` มีประโยชน์อย่างไรเมื่อ Agent ตอบกลับข้อความยาวๆ?
ข้อที่ 10: หาก Observation หรือ Error Log มีความยาว 2,000 บรรทัด ฟังก์ชัน `print_observation()` จัดการอย่างไร?
ข้อที่ 11: ธงคำสั่ง `-t / --task` กับ `-i / --interactive` ใน `main.py` แตกต่างกันอย่างไร?
ข้อที่ 12: การติดตั้งไลบรารี `rich` บน Mac M3 16GB มีผลกระทบต่อประสิทธิภาพเครื่องอย่างไร?
ข้อที่ 13: เมื่อเรามี Rich CLI ที่สมบูรณ์แบบใน Day 11 ใน **Day 12 (Error Handling & Safety Guardrails)** เราจะยกระดับด้านใด?
ข้อที่ 14: ทำไมการมี Production Cockpit CLI จึงทำให้คุณรู้สึกว่า Agent มีชีวิตและน่าเชื่อถือมากขึ้น?
ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อคุณมีห้องบังคับการ Rich CLI Cockpit ส่วนตัวบน Mac M3 รู้สึกอย่างไร?
🏁 สรุปหมุดเชื่อมโยง Day 11 (Chapter Wrap-up)
ใน Day 11 — Production-Grade Rich CLI & Interactive REPL Shell คุณได้สร้างโมดูล core/ui.py ติดตั้งระบบ Presentation Layer ระดับอุตสาหกรรมด้วย rich อัปเกรด main.py ให้มีทั้ง Live Status Spinner ที่ป้องกันหน้าจอค้าง, Styled Panels แยก Thought/Action/Observation ชัดเจน, Syntax Highlighting Diff สีเขียว/แดง และโหมด Interactive REPL Shell ที่รับมือ Ctrl+C ได้อย่างสง่างาม ตอนนี้ Agent ของคุณมีหน้าตาห้องบังคับการระดับโปรดักชันที่สวยงามเทียบเท่า Copilot แล้ว พร้อมก้าวเข้าสู่ Day 12: Error Handling & Safety Guardrails เพื่อติดตั้งเกราะคุ้มกันความปลอดภัยขั้นสูงสุดต่อไปครับ!