← Phase 1 Phase 1 • Day 4 Reboot

Local SE Agent Masterclass

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

Chapter 4 — Building Tool 1: File System Reader

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

ใน Day 3 เราสร้างสะพาน ToolDispatcher สำเร็จแล้ว แต่ฟังก์ชันที่เราทดสอบยังเป็นแค่ฟังก์ชันคำนวณเลขตุ๊กตา

Day 4 คือก้าวแรกที่ Agent จะได้สัมผัส "โลกซอฟต์แวร์จริง" ผ่านเครื่องมือตัวแรก: File System Reader Tool ซึ่งเปรียบเสมือน "ดวงตา (Visual Sensory Layer)" ของ Agent ที่ทำให้โมเดลสามารถเจาะจงเปิดอ่านไฟล์ซอร์สโค้ดในโปรเจกต์ได้จริง

สิ่งที่วิศวกรซอฟต์แวร์ระดับอาวุโสต้องคำนึงถึง ไม่ใช่แค่การเขียน open(path).read() ธรรมดา แต่คือ "Security Guardrail & Path Traversal Prevention" (การป้องกันไม่ให้ AI หลุดไปเปิดอ่านไฟล์ระบบ เช่น /etc/passwd หรือไฟล์นอก Workspace) และการจัดการ Context Window Budget (ไม่ให้โมเดลอ่านไฟล์ขนาดยักษ์จน Token ล้นแตกบน Mac 16GB)

✓ 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 4 จะต่อยอดจากโครงสร้าง se_agent/ โดยเพิ่มโมดูลเครื่องมือ core/tools/file_reader.py และสคริปต์ test_file_reader.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] สคริปต์ทดสอบ E2E ให้ AI สั่งเปิดอ่านไฟล์โค้ดในโปรเจกต์จริง
├── sample_code/                            # โฟลเดอร์จำลองไฟล์โค้ดของบริษัท
│   └── PaymentService.java                # โค้ดตัวอย่างสำหรับให้ Agent ส่องอ่าน
├── core/
│   ├── __init__.py
│   ├── client.py                          # ตัวยิงข้อความกับ Ollama
│   ├── parser.py                          # แปลง JSON ตอบกลับจากโมเดล
│   ├── dispatcher.py                      # ตัวจัดระเบียบ Registry & Dispatch
│   └── tools/                             # [โฟลเดอร์ใหม่ Day 4] แหล่งรวบรวม Base Tools
│       ├── __init__.py
│       └── file_reader.py                  # [ไฟล์ใหม่ Day 4] ฟังก์ชันอ่านไฟล์พร้อมระบบ Path Security Guardrail
└── main.py
📍 core/tools/file_reader.py ทำหน้าที่อะไร?

ทำหน้าที่เปิดอ่านไฟล์ข้อความบนดิสก์จริง พร้อมตั้งแนวป้องกัน Path Traversal Guardrail ด้วย pathlib.Path.resolve() เพื่อล็อกขอบเขตไม่ให้อ่านไฟล์ออกนอก workspace และจำกัดขนาดบรรทัดอ่านไฟล์ไม่ให้ล้นแรม

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

สคริปต์รันบน Terminal เพื่อป้อนคำถามมนุษย์ เช่น "ช่วยอ่านไฟล์ sample_code/PaymentService.java แล้วบอกว่ามีเมธอดอะไรบ้าง" เพื่อทดสอบวงจรตั้งแต่โมเดลตัดสินใจเลือก Tool ➔ Dispatcher สั่งรัน ➔ Python อ่านไฟล์จริง ➔ ส่งคำตอบกลับมาสรุป

🏛️ ส่วนที่ 3: สถาปัตยกรรม File Reader & Sandbox Boundary (Mental Model)
1️⃣
User Query
"ตรวจไฟล์ PaymentService"
➔
2️⃣
Tool Call Request
{"tool": "read_file", "path": "..."}
➔
3️⃣
Path Guardrail
ตรวจ resolve() อยู่ใน root?
ทำไมการเขียน Tool อ่านไฟล์ถึงห้ามใช้ open() ตรงๆ โดยไม่มี Guardrail?
❌ ความเสี่ยงของ Path Traversal Vulnerability:

หากโมเดลหลอน (Hallucinate) หรือมี Prompt Injection หลอกให้โมเดลอ่านไฟล์ ../../../../etc/passwd หรือ ~/.ssh/id_rsa คำสั่ง open(path) ธรรมดาจะยอมเปิดไฟล์ลับของระบบออกมาทันที!

✅ มาตรการแก้ไขด้วย pathlib.Path.resolve():

แปลงพาทสัมพัทธ์ทั้งหมดเป็น Absolute Path ตัวเต็ม แล้วตรวจเช็คว่าพาทเป้าหมายต้องมี prefix ขึ้นต้นด้วยโฟลเดอร์ Workspace ของโปรเจกต์เท่านั้น ถ้าหลุดออกนอกขอบเขต จะสั่งบล็อกและโยน Permission Denied ทันที

💻 ส่วนที่ 4: ซอร์สโค้ดจริง & คำอธิบายทีละบรรทัด (Line-by-Line Implementation)
📄 1. core/tools/file_reader.py (ไฟล์ใหม่ประจำ Day 4) ฟังก์ชันอ่านไฟล์พร้อมระบบคุ้มครองความปลอดภัยระดับ Enterprise
from pathlib import Path
from typing import Optional

class FileAccessError(Exception):
    """Custom exception เมื่อเกิดข้อผิดพลาดในการเข้าถึงไฟล์หรือละเมิดความปลอดภัย"""
    pass

def read_file(filepath: str, workspace_root: Optional[str] = None, max_lines: int = 500) -> str:
    """
    เครื่องมือสำหรับเปิดอ่านเนื้อหาไฟล์ในโปรเจกต์อย่างปลอดภัย (Safe File System Reader)
    
    :param filepath: ที่อยู่ของไฟล์สัมพัทธ์ (เช่น 'sample_code/PaymentService.java')
    :param workspace_root: โฟลเดอร์รากที่อนุญาตให้อ่าน (ค่าเริ่มต้นคือโฟลเดอร์ปัจจุบันที่รันโปรแกรม)
    :param max_lines: จำนวนบรรทัดสูงสุดที่อนุญาตให้อ่าน ป้องกัน Token ล้น Context Window
    :return: เนื้อหาของไฟล์ในรูปข้อความสตริง
    """
    try:
        # 1. กำหนด Workspace Root Directory และแปลงเป็น Absolute Path
        root = Path(workspace_root).resolve() if workspace_root else Path.cwd().resolve()
        
        # 2. นำ filepath เป้าหมายมาต่อกับ root แล้วแปลงเป็น Absolute Path
        target_path = (root / filepath).resolve()
        
        # 3. [Security Guardrail]: ป้องกัน Path Traversal Attack
        # ตรวจสอบว่า target_path ต้องอยู่ภายใต้โฟลเดอร์ root เท่านั้น
        if not str(target_path).startswith(str(root)):
            raise FileAccessError(
                f"Security Violation: ไม่อนุญาตให้อ่านไฟล์นอกโฟลเดอร์โปรเจกต์ '{filepath}'"
            )
        
        # 4. ตรวจสอบว่ามีไฟล์อยู่จริงหรือไม่
        if not target_path.exists():
            raise FileAccessError(f"ไม่พบไฟล์เป้าหมายที่พิกัด: '{filepath}'")
            
        if not target_path.is_file():
            raise FileAccessError(f"พิกัด '{filepath}' เป็นโฟลเดอร์ ไม่ใช่ไฟล์ข้อความ")
            
        # 5. เปิดอ่านไฟล์ด้วยการระบุ encoding='utf-8' อย่างชัดเจน
        with open(target_path, "r", encoding="utf-8", errors="replace") as file:
            lines = []
            for idx, line in enumerate(file):
                if idx >= max_lines:
                    lines.append(f"\n[...ข้อความถูกตัดทอน เนื่องจากไฟล์มีความยาวเกิน {max_lines} บรรทัด...]")
                    break
                lines.append(line)
                
            return "".join(lines)
            
    except FileAccessError:
        raise
    except Exception as err:
        raise FileAccessError(f"เกิดข้อผิดพลาดขณะอ่านไฟล์ '{filepath}': {str(err)}")
ชำแหละไวยากรณ์สำคัญใน core/tools/file_reader.py
Path.resolve()

ในไลบรารี pathlib เมธอด resolve() จะทำการคลี่คลาย Symlink และจุดแอบแฝง เช่น ../ ให้กลายเป็น Absolute Path แท้จริงของระบบ ทำให้ไม่มีช่องโหว่หลอกตา

not str(target_path).startswith(str(root))

กำแพงตรวจขอบเขต (Sandboxing Check) ถ้านำ Absolute Path ของเป้าหมายมาเทียบ แล้วไม่ได้ขึ้นต้นด้วยที่อยู่ของโปรเจกต์ แสดงว่ามีความพยายามขโมยอ่านไฟล์นอกเครื่อง จะถูกตัดสิทธิ์ทันที

max_lines: int = 500

การตั้ง Budget ป้องกันการอ่านไฟล์ขนาดหมื่นบรรทัดเข้า Context Window ซึ่งจะทำให้ Unified Memory บน Mac 16GB ถูกบีบอัดจนโปรเซสตอบสนองช้าลง

📄 2. sample_code/PaymentService.java (ไฟล์โค้ดจำลองสำหรับทดสอบ) ซอร์สโค้ด Java จำลองของระบบ Payment
package com.enterprise.payment;

public class PaymentService {
    private final double TAX_RATE = 0.07;

    public double calculateTotalWithTax(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("จำนวนเงินต้องมากกว่า 0");
        }
        return amount + (amount * TAX_RATE);
    }

    public boolean processTransaction(String accountId, double amount) {
        // จำลองการตัดเงินผ่าน Gateway
        return amount > 0 && accountId != null && !accountId.isEmpty();
    }
}
📄 3. test_file_reader.py (สคริปต์ทดสอบ E2E Tool Calling) เชื่อมโยง Ollama ➔ Dispatcher ➔ FileReader ➔ Final Answer
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser
from core.dispatcher import ToolDispatcher
from core.tools.file_reader import read_file, FileAccessError
import json

def main():
    print("🚀 เริ่มทดสอบ Day 4: File System Reader Tool บน Mac M3...")
    
    # 1. ติดตั้ง Dispatcher และจดทะเบียน File Reader เข้าสู่ระบบ
    dispatcher = ToolDispatcher()
    dispatcher.register("read_file", read_file)
    
    client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
    if not client.check_health():
        print("❌ ข้อผิดพลาด: ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b")
        return

    # 2. System Prompt ระบุความสามารถของ Tool อ่านไฟล์ พร้อม JSON Schema
    system_prompt = (
        "You are an Autonomous Software Engineering Agent.\n"
        "You have access to tools to inspect local source code.\n"
        "When you need to read a file, respond ONLY with a JSON action object.\n\n"
        "Available Tools:\n"
        "1. read_file(filepath: string): Reads file content safely from the workspace.\n\n"
        "JSON Response Schema:\n"
        "{\n"
        '  "tool": "read_file",\n'
        '  "arguments": {\n'
        '    "filepath": "path/to/file"\n'
        '  }\n'
        "}"
    )

    user_query = "ช่วยตรวจสอบโค้ดในไฟล์ sample_code/PaymentService.java ให้หน่อยว่ามีเมธอดอะไรบ้าง"
    print(f"💬 User Prompt: {user_query}")

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

    # 3. ยิง Prompt รอบที่ 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 กำลังสั่งรัน Tool '{tool_name}'...")
        # 4. Dispatcher รันฟังก์ชัน read_file จริงบนเครื่อง Mac
        file_content = dispatcher.dispatch(tool_name, arguments)
        print(f"✅ อ่านไฟล์สำเร็จ! (ความยาวโค้ด: {len(file_content)} ตัวอักษร)")

        # 5. ส่งผลลัพธ์จากไฟล์ (Observation) กลับไปให้โมเดลสรุปคำตอบรอบที่ 2
        messages.append({"role": "assistant", "content": raw_response})
        messages.append({
            "role": "user",
            "content": f"Observation from read_file:\n```java\n{file_content}\n```\nโปรดสรุปว่าในคลาสนี้มีเมธอดชื่ออะไรบ้างและทำหน้าที่อะไร"
        })

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

    except FileAccessError as err:
        print(f"❌ File Access Guardrail Blocked: {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 ขั้นตอนนี้เพื่อติดตั้งและทดสอบ File System Reader ทันที:

ขั้นตอนที่ 1: เข้าโฟลเดอร์โปรเจกต์เดิมและเปิด Virtual Environment
cd ~/se_agent
source .venv/bin/activate
ขั้นตอนที่ 2: สร้างโฟลเดอร์ tools และโฟลเดอร์เก็บโค้ดตัวอย่าง
mkdir -p core/tools sample_code
touch core/tools/__init__.py core/tools/file_reader.py sample_code/PaymentService.java test_file_reader.py
ขั้นตอนที่ 3: วางโค้ดลงใน 3 ไฟล์ใหม่

- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/tools/file_reader.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน sample_code/PaymentService.java
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน test_file_reader.py

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

คุณจะเห็นโมเดลส่ง JSON ขออ่าน sample_code/PaymentService.java ➔ โค้ด Python ไปดึงเนื้อหาจากดิสก์จริง ➔ โมเดลอ่านเนื้อหาแล้วสรุปว่ามีเมธอด calculateTotalWithTax และ processTransaction ออกมาอย่างแม่นยำ!

ขั้นตอนที่ 5 (Optional Test): ทดสอบพลัง Security Guardrail

ลองเปิดไฟล์ test_file_reader.py แล้วเปลี่ยน user_query เป็น "ช่วยอ่านไฟล์ ../../etc/hosts ให้หน่อย" แล้วรันใหม่ คุณจะเห็นระบบโยน FileAccessError: Security Violation บล็อกคำสั่งอันตรายทันที 100%!

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

ข้อที่ 1: บทบาทเชิงสถาปัตยกรรมของ "File System Reader Tool" ต่อตัว AI Agent คืออะไร?

ข้อที่ 2: เหตุใดการเขียนฟังก์ชันอ่านไฟล์สำหรับ Agent ถึงห้ามใช้ `open(filepath).read()` ตรงๆ โดยไม่มีการตรวจสอบ?

ข้อที่ 3: ในไลบรารี `pathlib.Path` เมธอด `.resolve()` มีหน้าที่สำคัญอย่างไรในการป้องกันความปลอดภัย?

ข้อที่ 4: การตรวจสอบ `if not str(target_path).startswith(str(root)):` มีจุดประสงค์เพื่ออะไร?

ข้อที่ 5: เหตุใดในฟังก์ชัน `read_file` จึงควรกำหนดพารามิเตอร์ `max_lines` (เช่น 500 บรรทัด)?

ข้อที่ 6: ในวงจร ReAct Tool Calling เมื่อรันฟังก์ชัน `read_file` ได้เนื้อหามาแล้ว ขั้นตอนต่อไปคืออะไร?

ข้อที่ 7: หากไฟล์เป้าหมายไม่มีอยู่จริงในดิสก์ ฟังก์ชัน `read_file` ควรตอบสนองอย่างไร?

ข้อที่ 8: การระบุ `errors="replace"` ใน `open(..., encoding="utf-8")` มีประโยชน์อย่างไร?

ข้อที่ 9: การแยกโค้ดเครื่องมืออ่านไฟล์ไว้ที่ `core/tools/file_reader.py` สอดคล้องกับหลักการออกแบบใด?

ข้อที่ 10: ในไฟล์ `test_file_reader.py` การเรียกใช้ `client.chat()` มีทั้งหมดกี่รอบ (Turn)?

ข้อที่ 11: ทำไมการใช้ `pathlib.Path` ใน Python ยุคใหม่ถึงดีกว่าการใช้ `os.path` แบบเก่า?

ข้อที่ 12: การที่โมเดล `qwen2.5-coder:7b` สามารถอ่านเนื้อหาภาษา Java จาก Tool แล้วสรุปชื่อเมธอดได้ถูกต้อง เกิดจากอะไร?

ข้อที่ 13: เมื่อเรามี Tool อ่านไฟล์สำเร็จใน Day 4 สะพานนี้จะทำให้เราสร้างสิ่งใดต่อใน **Day 5 (Maven & Test Runner)**?

ข้อที่ 14: ทำไมการรัน File Reader แบบ Local บน Mac M3 ถึงตอบโจทย์งานระดับ Enterprise เรื่อง Data Privacy?

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

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

ใน Day 4 — Building Tool 1: File System Reader คุณได้สร้างโมดูล core/tools/file_reader.py ซึ่งเป็นเครื่องมือเปิดอ่านไฟล์โค้ดจริงพร้อมระบบความปลอดภัย Path Traversal Guardrail และทดสอบรัน test_file_reader.py เชื่อมโยงวงจร ReAct Turn 1 และ Turn 2 บนชิป Apple Silicon M3 ได้สำเร็จจริง ตอนนี้ Agent ของคุณมีดวงตามองเห็นซอร์สโค้ดในโปรเจกต์แล้ว พร้อมก้าวสู่ Day 5: Building Tool 2 — Maven Build & Test Runner เพื่อให้ Agent เริ่มสั่งคอมไพล์และรันเทสจับบั๊กจริงได้แล้วครับ!