← Phase 1 Phase 1 • Day 5 Reboot

Local SE Agent Masterclass

MacBook M3 16GB • Complete Implementation Standard
📍 ส่วนที่ 1: หมุดตั้งโจทย์ & วัตถุประสงค์หลัก (Title Connection)

Chapter 5 — Building Tool 2: Maven Build & Test Runner

ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?

ใน Day 4 เราติด "ดวงตา (File Reader)" ให้ Agent สามารถเปิดส่องดูโค้ดบนเครื่องได้แล้ว แต่ถ้า Agent ทำได้แค่อ่านโค้ด มันก็ยังเป็นได้แค่ "คนดูที่วิจารณ์แต่ลงมือทำอะไรไม่ได้"

Day 5 คือก้าวสำคัญในการติด "มือและเท้าสำหรับการปฏิบัติการ (Actionable Execution Layer)" ผ่านเครื่องมือตัวที่ 2: Maven Build & Test Runner Tool ซึ่งจะเชื่อมต่อ Python เข้ากับ Process ของระบบปฏิบัติการผ่าน subprocess.run() เพื่อสั่งคอมไพล์โค้ดจริง รันชุดทดสอบ Unit Tests จริง และดักจับ Stack Trace จาก Log ย้อนกลับมาให้โมเดลอ่าน

หัวใจสำคัญระดับ Senior ไม่ใช่แค่การรันคำสั่ง แต่คือ "Process Sandboxing & Resource Isolation": การตั้งเวลาตัดจบ (timeout) เพื่อกัน infinite loops, การเช็ค Return Code (`0` = ผ่าน, `!= 0` = บั๊ก), และการสกัด Error Log Slicing เพื่อไม่ให้ Log ยาวพันบรรทัดระเบิด Context Window ของ Mac M3!

✓ Day 1: Setup & Ollama API ✓ Day 2: JSON Mode & Structured Output ✓ Day 3: Tool Calling & Dispatcher ✓ Day 4: File System Reader Day 5: Maven & Test Runner Day 6+: Autonomous ReAct Loop
📂 ส่วนที่ 2: ผังโครงสร้างโปรเจกต์จริง & ตำแหน่งไฟล์ (Project File Structure)

โค้ดประจำ Day 5 จะต่อยอดโครงสร้าง se_agent/ โดยเพิ่มโมดูล core/tools/test_runner.py, โฟลเดอร์จำลองโปรเจกต์ Java sample_maven_project/, และสคริปต์ test_maven_runner.py:

se_agent/
├── requirements.txt                       # ollama>=0.4.0
├── test_connection.py                    # Day 1
├── test_structured_output.py             # Day 2
├── test_tool_dispatch.py                 # Day 3
├── test_file_reader.py                   # Day 4
├── test_maven_runner.py                 # [ไฟล์ใหม่ Day 5] สคริปต์รัน E2E ให้ AI สั่งรันเทสจริงและจับบั๊ก
├── sample_maven_project/                  # [โฟลเดอร์ใหม่ Day 5] โปรเจกต์จำลองเพื่อรันเทสจริง
│   ├── pom.xml                           # คอนฟิก Maven / JUnit
│   ├── src/main/java/Calculator.java      # โค้ดที่มีบั๊กแฝง
│   └── src/test/java/CalculatorTest.java  # เทสเคสที่จะล้มเหลว
├── core/
│   ├── __init__.py
│   ├── client.py                          # ตัวยิง Ollama Daemon
│   ├── parser.py                          # แปลง JSON ตอบกลับ
│   ├── dispatcher.py                      # Dispatcher Registry
│   └── tools/
│       ├── __init__.py
│       ├── file_reader.py                  # Safe File Reader (Day 4)
│       └── test_runner.py                  # [ไฟล์ใหม่ Day 5] Subprocess Maven Test Execution Tool
└── main.py
📍 core/tools/test_runner.py ทำหน้าที่อะไร?

ทำหน้าที่ยิงคำสั่ง Terminal mvn test ผ่าน subprocess.run() ควบคุม timeout 120 วินาที ตรวจสอบ Exit code และดักจับ stdout/stderr ตัดทอนให้เหลือเฉพาะส่วน Failure Summary เพื่อส่งต่อให้ AI วิเคราะห์

📍 test_maven_runner.py ทำหน้าที่อะไร?

สคริปต์รันบน Terminal เพื่อทดสอบส่งโจทย์: "ช่วยรันเทสใน sample_maven_project ให้หน่อยว่าผ่านไหม ถ้าไม่ผ่านให้บอกว่าพังตรงไหน" AI จะตัดสินใจเรียก Tool รันเทสจริง ➔ ได้ผล Error Log ➔ วิเคราะห์สาเหตุให้ฟัง

🏛️ ส่วนที่ 3: สถาปัตยกรรม Process Execution & Log Truncation (Mental Model)
1️⃣
User Query
"ตรวจเทส Calculator"
➔
2️⃣
Action JSON
{"tool": "run_maven_test"}
➔
3️⃣
Subprocess Run
mvn test (Timeout: 120s)
➔
4️⃣
Log Truncation
ตัดเฉพาะ 25 บรรทัดล่าง
ทำไมการรันคำสั่ง OS ด้วย Python ถึงต้องใช้ Subprocess ปลอดภัยอย่างไร?
❌ ความเสี่ยงของ os.system() หรือ Unbounded Run:

ถ้าใช้ os.system("mvn test") เราจะไม่สามารถดักจับ output เข้าตัวแปรได้, ถ้าคำสั่งค้างหรือติด loop จะไม่มี timeout มาตัดจบ, และเสี่ยงต่อ Command Injection หากส่ง string ต่อกันตรงๆ

✅ มาตรการแก้ไขด้วย subprocess.run(list, capture_output=True):

ส่งคำสั่งเป็น List ["mvn", "test"] ป้องกัน Shell Injection, แยก stdout/stderr ชัดเจน, กำหนด timeout ชัดเจน และสกัดเฉพาะ Error Summary ส่งกลับให้โมเดลประหยัด Token สูงสุด

💻 ส่วนที่ 4: ซอร์สโค้ดจริง & คำอธิบายทีละบรรทัด (Line-by-Line Implementation)
📄 1. core/tools/test_runner.py (ไฟล์ใหม่ประจำ Day 5) เครื่องมือรัน Maven Test พร้อมการสกัด Error Log
import subprocess
import shutil
from pathlib import Path
from typing import Optional, Dict, Any

class BuildExecutionError(Exception):
    """Custom exception สำหรับข้อผิดพลาดที่ไม่สามารถรัน Build Process ได้"""
    pass

def run_maven_test(project_dir: str = ".", test_name: Optional[str] = None, timeout_seconds: int = 120) -> str:
    """
    เครื่องมือสำหรับสั่งรัน Unit Tests ของโปรเจกต์ Java ผ่าน Maven
    
    :param project_dir: ไดเรกทอรีของโปรเจกต์ที่มีไฟล์ pom.xml
    :param test_name: (ทางเลือก) ระบุชื่อคลาสเทสเฉพาะเจาะจง เช่น 'CalculatorTest'
    :param timeout_seconds: เวลาสูงสุดที่อนุญาตให้คำสั่งรัน ป้องกันการติด Infinite Loop
    :return: รายงานผลลัพธ์การรันเทส (BUILD SUCCESS หรือ BUILD FAILURE พร้อม Stack Trace)
    """
    # 1. แปลง project_dir เป็น Absolute Path และตรวจสอบไฟล์ pom.xml
    target_path = Path(project_dir).resolve()
    pom_file = target_path / "pom.xml"

    if not pom_file.exists():
        raise BuildExecutionError(f"ไม่พบไฟล์ 'pom.xml' ในโฟลเดอร์: {target_path}")

    # 2. ตรวจสอบว่าในเครื่อง Mac มีคำสั่ง mvn ติดตั้งอยู่หรือไม่
    mvn_bin = shutil.which("mvn")
    if not mvn_bin:
        # Fallback: ตรวจสอบว่ามี Maven Wrapper (mvnw) อยู่ในโปรเจกต์หรือไม่
        mvnw = target_path / "mvnw"
        if mvnw.exists():
            mvn_cmd = [str(mvnw)]
        else:
            raise BuildExecutionError("ไม่พบคำสั่ง 'mvn' หรือ 'mvnw' ในระบบ โปรดติดตั้ง Maven ก่อนใช้งาน")
    else:
        mvn_cmd = [mvn_bin]

    # 3. จัดเตรียมคำสั่ง Command ในรูปแบบ List
    # -B: Batch mode (ไม่แสดงสีหรือข้อความ interactive)
    # -Dtest=... : ระบุชื่อเทสเจาะจงถ้ามี
    cmd = mvn_cmd + ["test", "-B"]
    if test_name:
        cmd.append(f"-Dtest={test_name}")

    try:
        # 4. ยิง Process ผ่าน subprocess.run
        process = subprocess.run(
            cmd,
            cwd=str(target_path),
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            encoding="utf-8",
            errors="replace"
        )

        stdout = process.stdout or ""
        stderr = process.stderr or ""
        full_output = stdout + "\n" + stderr

        # 5. วิเคราะห์ Exit Code (0 = สำเร็จ, ไม่ใช่ 0 = ล้มเหลว)
        if process.returncode == 0:
            return (
                "✅ BUILD SUCCESS: การรันชุดทดสอบทั้งหมดผ่านเรียบร้อย 100%!\n"
                f"รายละเอียดผลลัพธ์:\n{_extract_summary_lines(stdout, 15)}"
            )
        else:
            # สกัดเฉพาะบรรทัดล่างสุดที่สรุป Failure เพื่อไม่ให้กิน Context Window
            failure_summary = _extract_summary_lines(full_output, 35)
            return (
                f"❌ BUILD FAILURE: ตรวจพบข้อผิดพลาดในชุดทดสอบ (Exit Code {process.returncode})\n"
                "--------------------------------------------------\n"
                f"{failure_summary}\n"
                "--------------------------------------------------"
            )

    except subprocess.TimeoutExpired:
        raise BuildExecutionError(f"Timeout Error: คำสั่งรันเทสใช้เวลานานเกินกว่า {timeout_seconds} วินาที")
    except Exception as err:
        raise BuildExecutionError(f"เกิดข้อผิดพลาดขณะรันกระบวนการ Maven: {str(err)}")

def _extract_summary_lines(output: str, max_lines: int = 30) -> str:
    """ฟังก์ชันช่วยตัดทอน Log ยาวๆ ให้เหลือเฉพาะส่วนท้ายที่สำคัญ"""
    lines = [line.strip() for line in output.splitlines() if line.strip()]
    if len(lines) <= max_lines:
        return "\n".join(lines)
    return "[...ตัดทอนบันทึกตอนต้นออก...]\n" + "\n".join(lines[-max_lines:])
ชำแหละไวยากรณ์สำคัญใน core/tools/test_runner.py
shutil.which("mvn")

ในภาษา Python คำสั่ง shutil.which() ใช้สแกนหาตำแหน่ง binary executable บน PATH ของระบบ เช่น คืนค่า /opt/homebrew/bin/mvn ช่วยตรวจสอบก่อนรันว่ามีโปรแกรมจริงไหม

subprocess.run(cmd, cwd=str(target_path), capture_output=True, timeout=120)

cwd สั่งให้ process ย้ายไปทำงานในโฟลเดอร์โปรเจกต์เป้าหมาย, capture_output=True ดักทั้ง stdout และ stderr เข้าตัวแปรในหน่วยความจำ, และ timeout ตัดจบหากเกิด deadlocks

process.returncode == 0

Exit Code ของคำสั่งระดับ OS สากล หากส่งคืนเลข 0 แปลว่าทุก test case สำเร็จสมบูรณ์ หากเป็นเลขอื่น (เช่น 1) แปลว่ามี assertion failure หรือ compile error

📄 2. sample_maven_project/pom.xml (ไฟล์คอนฟิก Maven จำลอง) ไฟล์ตั้งค่า Build และ JUnit 5
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>sample-maven-project</artifactId>
    <version>1.0.0</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
📄 3. sample_maven_project/src/.../Calculator.java & CalculatorTest.java โค้ดจำลองที่มีบั๊กแฝง เพื่อทดสอบให้รันเทสแล้วล้มเหลว
// 3.1: src/main/java/Calculator.java (มีบั๊ก: คำนวณภาษีผิดตัวเลข)
public class Calculator {
    public int multiply(int a, int b) {
        return a * b;
    }
    public double applyDiscount(double price, double discount) {
        // บั๊กจำลอง: เขียนผิดเป็นลบเงินสองเท่า
        return price - (discount * 2.0);
    }
}

// 3.2: src/test/java/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {
    @Test
    void testMultiply() {
        Calculator calc = new Calculator();
        assertEquals(20, calc.multiply(4, 5));
    }

    @Test
    void testApplyDiscount() {
        Calculator calc = new Calculator();
        // คาดหวัง: 100 - 10 = 90.0 แต่โค้ดส่งคืน 80.0 -> เกิด AssertionFailedError!
        assertEquals(90.0, calc.applyDiscount(100.0, 10.0), 0.001);
    }
}
📄 4. test_maven_runner.py (สคริปต์ทดสอบ E2E Tool Calling) เชื่อมโยง Ollama ➔ Dispatcher ➔ MavenRunner ➔ Error Analysis
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser
from core.dispatcher import ToolDispatcher
from core.tools.test_runner import run_maven_test, BuildExecutionError

def main():
    print("🚀 เริ่มทดสอบ Day 5: Maven Build & Test Runner Tool บน Mac M3...")
    
    # 1. ติดตั้ง Dispatcher และจดทะเบียน Tool ทั้งสองตัว (File Reader + Maven Runner)
    dispatcher = ToolDispatcher()
    dispatcher.register("run_maven_test", run_maven_test)

    client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
    if not client.check_health():
        print("❌ ข้อผิดพลาด: ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b")
        return

    # 2. System Prompt ประกาศเครื่องมือรันเทส
    system_prompt = (
        "You are an Autonomous Software Engineering Agent.\n"
        "You have tools to build and run automated tests for Java Maven projects.\n"
        "When you need to test code, respond ONLY with a JSON action object.\n\n"
        "Available Tools:\n"
        "1. run_maven_test(project_dir: string, test_name: string|null): Runs Maven unit tests.\n\n"
        "JSON Response Schema:\n"
        "{\n"
        '  "tool": "run_maven_test",\n'
        '  "arguments": {\n'
        '    "project_dir": "path/to/project"\n'
        '  }\n'
        "}"
    )

    user_query = "ช่วยรันเทสในโปรเจกต์ sample_maven_project ให้หน่อยว่าผ่านไหม และเกิดปัญหาอะไรขึ้น"
    print(f"💬 User Prompt: {user_query}")

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_query}
    ]

    # 3. Turn 1: ให้โมเดลขอเรียก Tool
    print("\n📡 กำลังส่งโจทย์ให้โมเดลประมวลผล...")
    raw_response = client.chat(messages, temperature=0.1, json_mode=True)
    print(f"📥 โมเดลตัดสินใจส่ง JSON Action:\n{raw_response}")

    try:
        action = StructuredOutputParser.parse(raw_response)
        tool_name = action.get("tool")
        arguments = action.get("arguments", {})

        print(f"\n⚙️ Dispatcher กำลังสั่งรัน Process '{tool_name}'...")
        # 4. Dispatcher รันคำสั่ง mvn test จริงผ่าน Terminal
        test_output = dispatcher.dispatch(tool_name, arguments)
        print(f"✅ บันทึกผลลัพธ์จาก Process:\n{test_output}")

        # 5. Turn 2: ส่งผลลัพธ์ Observation กลับไปให้โมเดลวิเคราะห์สาเหตุและแนวทางแก้ไข
        messages.append({"role": "assistant", "content": raw_response})
        messages.append({
            "role": "user",
            "content": f"Observation from run_maven_test:\n```text\n{test_output}\n```\nโปรดวิเคราะห์ว่าทำไมเทสถึงล้มเหลว คาดหวังค่าอะไรและได้ค่าอะไรกลับมา"
        })

        print("\n🧠 โมเดลกำลังอ่าน Stack Trace และสังเคราะห์คำตอบ...")
        final_answer = client.chat(messages, temperature=0.2, json_mode=False)
        print("\n🎯 สรุปผลการวิเคราะห์จาก AI Agent:")
        print("--------------------------------------------------")
        print(final_answer)
        print("--------------------------------------------------")
        print("🎉 สำเร็จ 100%! Agent ของคุณสามารถสั่งรันเทสจริงและจับบั๊กในระดับ OS Process ได้แล้ว!")

    except BuildExecutionError as err:
        print(f"❌ Build Execution Error: {err}")
    except Exception as err:
        print(f"❌ Error: {err}")

if __name__ == "__main__":
    main()
⚡ ส่วนที่ 5: ปฏิบัติการจริงบน Terminal Mac M3 (Step-by-Step Lab)

เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งและทดสอบ Maven Test Runner ทันที:

ขั้นตอนที่ 1: ตรวจสอบความพร้อมของ Maven บนเครื่อง Mac
mvn -version

หากยังไม่มี Maven ในเครื่อง สามารถติดตั้งผ่าน Homebrew ได้ทันที: brew install maven

ขั้นตอนที่ 2: สร้างโครงสร้างโฟลเดอร์สำหรับโปรเจกต์ Java ตัวอย่าง
cd ~/se_agent
source .venv/bin/activate
mkdir -p core/tools sample_maven_project/src/main/java sample_maven_project/src/test/java
touch core/tools/test_runner.py test_maven_runner.py sample_maven_project/pom.xml
touch sample_maven_project/src/main/java/Calculator.java sample_maven_project/src/test/java/CalculatorTest.java
ขั้นตอนที่ 3: คัดลอกโค้ดลงในแต่ละไฟล์

- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/tools/test_runner.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน sample_maven_project/pom.xml
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน Calculator.java และ CalculatorTest.java
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 4) ไปวางใน test_maven_runner.py

ขั้นตอนที่ 4: สั่งรันสคริปต์ทดสอบผลลัพธ์
python test_maven_runner.py

คุณจะเห็นโมเดลส่ง JSON ขอรันเทส ➔ Python ยิงคำสั่ง mvn test จริงใน Terminal ➔ พบว่า testApplyDiscount ล้มเหลว (Expected: 90.0, Actual: 80.0) ➔ AI สรุปสาเหตุของบั๊กใน Calculator.java ได้อย่างแม่นยำ 100%!

ขั้นตอนที่ 5 (Verification Test): ทดสอบแก้บั๊กให้ผ่านฉลุย

ลองเปิดไฟล์ Calculator.java แล้วแก้ไขสูตรเป็น return price - discount; จากนั้นรัน python test_maven_runner.py ซ้ำอีกครั้ง คุณจะเห็นโมเดลรายงานว่า BUILD SUCCESS การรันผ่านเรียบร้อย 100%!

📝 ส่วนที่ 6: ชุดควิซประเมินความรู้เชิงวิศวกรรม 15 ข้อ (ตอบปุ๊บรู้ผลทันที)

ข้อที่ 1: บทบาทของ "Maven Build & Test Runner Tool" ต่อสถาปัตยกรรมของ SE Agent คืออะไร?

ข้อที่ 2: เหตุใดการสั่งรันคำสั่งภายนอกใน Python จึงควรใช้ `subprocess.run(["mvn", "test"])` แทน `os.system("mvn test")`?

ข้อที่ 3: ในการสั่งรันคำสั่งระบบ ค่า `returncode == 0` บ่งบอกถึงสิ่งใด?

ข้อที่ 4: การกำหนดพารามิเตอร์ `timeout=120` ใน `subprocess.run()` มีจุดประสงค์เพื่อป้องกันปัญหาใด?

ข้อที่ 5: เหตุใดในฟังก์ชัน `run_maven_test` จึงต้องมีการตัดทอน Log (Truncation) ก่อนส่งกลับให้โมเดล?

ข้อที่ 6: คำสั่ง `shutil.which("mvn")` ทำหน้าที่อะไรในกระบวนการตรวจสอบระบบ?

ข้อที่ 7: อาร์กิวเมนต์ `-B` ในคำสั่ง `mvn test -B` มีประโยชน์อย่างไรต่องานของ AI Agent?

ข้อที่ 8: เมื่อได้ผลลัพธ์ `BUILD FAILURE` พร้อม Stack Trace มาแล้ว ในลูป ReAct ขั้นตอนต่อไปคืออะไร?

ข้อที่ 9: พารามิเตอร์ `cwd=str(target_path)` ใน `subprocess.run()` มีหน้าที่สำคัญอย่างไร?

ข้อที่ 10: หากโฟลเดอร์เป้าหมายไม่มีไฟล์ `pom.xml` ฟังก์ชัน `run_maven_test` ควรตอบสนองอย่างไร?

ข้อที่ 11: การที่โมเดล `qwen2.5-coder:7b` สามารถอ่าน Stack Trace ของ JUnit และชี้จุดผิดในโค้ดได้ เกิดจากอะไร?

ข้อที่ 12: การออกแบบแยกโมดูล `core/tools/test_runner.py` ออกจาก `dispatcher.py` สอดคล้องกับหลักการใด?

ข้อที่ 13: เมื่อเรามีทั้ง Tool 1 (File Reader) และ Tool 2 (Maven Runner) ครบแล้ว ใน **Day 6 (ReAct Loop Engine)** เราจะสร้างอะไรต่อ?

ข้อที่ 14: ทำไมการรัน Maven Test บน Mac M3 แบบ Local ถึงสำคัญต่อความปลอดภัยของข้อมูลในระดับ Enterprise?

ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อเห็นว่า Agent สั่งคอมไพล์โค้ดจริงและจับบั๊กออกมาได้ด้วยตัวเอง รู้สึกอย่างไร?

🏁 สรุปหมุดเชื่อมโยง Day 5 (Chapter Wrap-up)

ใน Day 5 — Building Tool 2: Maven Build & Test Runner คุณได้สร้างโมดูล core/tools/test_runner.py จัดการรัน OS Process ด้วย subprocess.run() ควบคุม Timeout สกัด Error Log และทดสอบรัน test_maven_runner.py เชื่อมโยงการตรวจจับบั๊กในโค้ด Java ได้จริง ตอนนี้ Agent ของคุณมีทั้ง ดวงตามองเห็นไฟล์ (File Reader) และ มือเท้าลงมือทดสอบโค้ด (Test Runner) ครบถ้วนแล้ว พร้อมก้าวเข้าสู่หัวใจสำคัญ: Day 6 — Building the Autonomous ReAct Loop Engine เพื่อให้สมองกลคิดและลงมือทำวนลูปอัตโนมัติจนงานเสร็จได้เองแล้วครับ!