# Bài 3: Triển khai Tool cơ bản với Revit API

> **Khóa học:** Revit API x MCP x AI — Từ Zero đến Plugin hoàn chỉnh
>
> **Mục tiêu:** Học viên sẽ biết cách triển khai một Tool hoàn chỉnh để truy vấn dữ liệu từ Revit, chuyển đổi sang định dạng JSON để AI có thể hiểu và xử lý.
>
> **Thời lượng ước tính:** 90 phút
>
> **Mã nguồn tham khảo:** [deepbim-revit-mcp-plugin](https://github.com/nguyenngocdue/deepbim-revit-mcp-plugin)
>
> **Tham chiếu triển khai Tool/MCP Server:** [revit-mcp-server](https://github.com/nguyenngocdue/revit-mcp-server)

***

## Mục lục

1. [Giới thiệu bài học](#giới-thiệu-bài-học)
2. [Revit API cơ bản](#revit-api-cơ-bản)
3. [Triển khai GetWallsTool](#triển-khai-getwallstool)
4. [Cấu trúc dữ liệu JSON cho AI](#cấu-trúc-dữ-liệu-json-cho-ai)
5. [Dùng AI để viết Tool](#dùng-ai-để-viết-tool)
6. [Bài tập thực hành: Viết GetFloorsTool](#bài-tập-thực-hành)
7. [Câu hỏi tự suy nghĩ](#câu-hỏi-tự-suy-nghĩ)
8. [Tổng kết](#tổng-kết)
9. [Tài liệu tham khảo](#tài-liệu-tham-khảo)

***

## Giới thiệu bài học

Trong bài trước, chúng ta đã tìm hiểu kiến trúc tổng thể của hệ thống MCP và cách Revit giao tiếp với AI thông qua MCP Server. Bài học này sẽ đi sâu vào **thực hành**: triển khai một Tool cụ thể — `GetWallsTool` — để lấy danh sách tường trong Revit project và trả về kết quả dưới dạng JSON.

### Tại sao cần Tool?

Trong kiến trúc MCP, **Tool** là đơn vị nhỏ nhất mà AI có thể gọi để thực hiện một tác vụ cụ thể. Mỗi Tool giống như một "kỹ năng" của AI — AI không trực tiếp truy cập Revit, mà gọi Tool để lấy thông tin hoặc thực hiện hành động.

```mermaid
flowchart LR
    A["🤖 AI Assistant<br/>(Claude, GPT...)"] -->|"Gọi Tool qua MCP"| B["🔗 MCP Server"]
    B -->|"Gọi Revit API"| C["🏗️ Revit Application"]
    C -->|"Trả về dữ liệu"| B
    B -->|"JSON Response"| A
    A -->|"Trả lời người dùng"| D["👤 User"]

    style A fill:#4A90D9,color:#fff
    style B fill:#7B68EE,color:#fff
    style C fill:#E67E22,color:#fff
    style D fill:#27AE60,color:#fff
```

### Kết quả mong đợi sau bài học

* Hiểu cách sử dụng `FilteredElementCollector` để truy vấn phần tử trong Revit
* Triển khai được một Tool hoàn chỉnh với error handling
* Biết cách chuyển đổi đơn vị (feet sang meters)
* Biết cách định dạng JSON để AI đọc được
* Có khả năng tự viết các Tool tương tự cho Floor, Room, Column...

***

## Revit API cơ bản

Trước khi viết Tool, chúng ta cần nắm vững các khái niệm cơ bản của Revit API.

### Các class quan trọng

```mermaid
classDiagram
    class Document {
        +ElementId ActiveViewId
        +String Title
        +String PathName
        +FilteredElementCollector Create()
    }

    class FilteredElementCollector {
        +OfClass(Type) FilteredElementCollector
        +OfCategory(BuiltInCategory) FilteredElementCollector
        +WhereElementIsNotElementType() FilteredElementCollector
        +ToElements() ICollection~Element~
        +ToList() List~Element~
    }

    class Element {
        +ElementId Id
        +String Name
        +Category Category
        +Parameter LookupParameter(string)
        +Location Location
    }

    class Wall {
        +WallType WallType
        +double Width
        +LocationCurve LocationCurve
        +Level Level
    }

    class Floor {
        +FloorType FloorType
        +Level Level
    }

    Document --> FilteredElementCollector : tạo ra
    FilteredElementCollector --> Element : trả về
    Element <|-- Wall : kế thừa
    Element <|-- Floor : kế thừa

    style Document fill:#3498DB,color:#fff
    style FilteredElementCollector fill:#9B59B6,color:#fff
    style Wall fill:#E67E22,color:#fff
    style Floor fill:#E67E22,color:#fff
```

### FilteredElementCollector — "Công cụ truy vấn" của Revit

`FilteredElementCollector` là class quan trọng nhất khi làm việc với Revit API. Nó hoạt động giống như SQL query — bạn xây dựng chuỗi điều kiện lọc để tìm phần tử cần thiết.

```mermaid
flowchart TD
    A["FilteredElementCollector(doc)"] --> B[".OfClass(typeof(Wall))"]
    B --> C[".WhereElementIsNotElementType()"]
    C --> D[".ToElements()"]
    D --> E["Danh sách Wall instances"]

    F["Toàn bộ Element<br/>trong Document"] --> A

    style A fill:#3498DB,color:#fff
    style B fill:#9B59B6,color:#fff
    style C fill:#E74C3C,color:#fff
    style D fill:#27AE60,color:#fff
    style E fill:#F39C12,color:#fff
    style F fill:#95A5A6,color:#fff
```

**Giải thích pipeline:**

| Bước | Method                              | Mục đích                                                           |
| ---- | ----------------------------------- | ------------------------------------------------------------------ |
| 1    | `new FilteredElementCollector(doc)` | Khởi tạo collector trên toàn bộ document                           |
| 2    | `.OfClass(typeof(Wall))`            | Chỉ lấy các element thuộc class `Wall`                             |
| 3    | `.WhereElementIsNotElementType()`   | Chỉ lấy instance (tường cụ thể), bỏ qua WallType (định nghĩa kiểu) |
| 4    | `.ToElements()`                     | Chuyển kết quả thành danh sách `ICollection<Element>`              |

> **Lưu ý quan trọng:** Luôn gọi `.WhereElementIsNotElementType()` nếu bạn muốn lấy các instance thực tế trong mô hình, không phải các định nghĩa kiểu (Type).

### Chuyển đổi đơn vị — Feet sang Meters

Revit lưu trữ tất cả kích thước theo đơn vị **feet nội bộ** (internal units). Khi trả dữ liệu cho AI hoặc người dùng, chúng ta cần chuyển sang **meters** (hoặc millimeters tùy nhu cầu).

```csharp
// Công thức chuyển đổi
// 1 foot = 0.3048 meters
double lengthInFeet = 10.0;
double lengthInMeters = lengthInFeet * 0.3048; // = 3.048 meters

// Hoặc sử dụng UnitUtils của Revit (API >= 2022)
double lengthInMeters2 = UnitUtils.ConvertFromInternalUnits(
    lengthInFeet,
    UnitTypeId.Meters
);
```

***

## Triển khai GetWallsTool

Đây là phần trọng tâm của bài học. Chúng ta sẽ xây dựng `GetWallsTool` từ đầu, bước một.

### Bước 1: Định nghĩa interface ITool

Trước tiên, chúng ta cần một interface chung cho tất cả các Tool trong hệ thống:

```csharp
// File: ITool.cs
namespace DeepBim.RevitMCP.Tools
{
    /// <summary>
    /// Interface cơ bản cho mỗi Tool trong hệ thống MCP.
    /// Mỗi Tool phải implement interface này.
    /// </summary>
    public interface ITool
    {
        /// <summary>
        /// Tên của Tool — AI sẽ dùng tên này để gọi Tool.
        /// Ví dụ: "get_walls", "get_floors", "create_wall"
        /// </summary>
        string Name { get; }

        /// <summary>
        /// Mô tả Tool — AI đọc mô tả này để hiểu khi nào nên gọi Tool.
        /// Càng rõ ràng, AI càng gọi đúng lúc.
        /// </summary>
        string Description { get; }

        /// <summary>
        /// Thực thi Tool và trả về kết quả JSON.
        /// </summary>
        /// <param name="doc">Revit Document hiện tại</param>
        /// <param name="parameters">Các tham số từ AI gửi xuống (có thể null)</param>
        /// <returns>Chuỗi JSON kết quả</returns>
        string Execute(Document doc, Dictionary<string, object> parameters);
    }
}
```

### Bước 2: Triển khai GetWallsTool đầy đủ

```csharp
// File: GetWallsTool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Revit.DB;
using Newtonsoft.Json;

namespace DeepBim.RevitMCP.Tools
{
    /// <summary>
    /// Tool lấy danh sách tất cả các tường (Wall) trong Revit project.
    /// Trả về thông tin cơ bản: Id, Tên, Chiều dài, Chiều cao, Độ dày, Level.
    ///
    /// AI có thể gọi Tool này khi người dùng hỏi:
    /// - "Liệt kê tất cả các tường trong project"
    /// - "Có bao nhiêu tường trong mô hình?"
    /// - "Cho tôi thông tin các tường ở tầng 1"
    /// </summary>
    public class GetWallsTool : ITool
    {
        // === PROPERTIES (implement ITool) ===

        /// <summary>
        /// Tên Tool — phải là snake_case, không dấu, không khoảng trắng.
        /// AI sẽ gọi Tool bằng tên này.
        /// </summary>
        public string Name => "get_walls";

        /// <summary>
        /// Mô tả Tool — viết bằng tiếng Anh để AI hiểu tốt nhất.
        /// Mô tả cần nêu rõ: Tool làm gì, trả về gì, khi nào nên dùng.
        /// </summary>
        public string Description =>
            "Retrieves all wall instances from the current Revit project. " +
            "Returns wall Id, Name, Length (m), Height (m), Width (m), " +
            "Level name, and WallType name. Use this tool when the user " +
            "asks about walls in the model.";

        // === HẰNG SỐ CHUYỂN ĐỔI ĐƠN VỊ ===

        /// <summary>
        /// Hệ số chuyển đổi từ feet (đơn vị nội bộ Revit) sang meters.
        /// 1 foot = 0.3048 meters (chính xác).
        /// </summary>
        private const double FEET_TO_METERS = 0.3048;

        // === PHƯƠNG THỨC CHÍNH ===

        /// <summary>
        /// Thực thi Tool: truy vấn tất cả Wall instances và trả về JSON.
        /// </summary>
        public string Execute(Document doc, Dictionary<string, object> parameters)
        {
            try
            {
                // --- Bước 1: Truy vấn tất cả Wall instances ---
                var walls = new FilteredElementCollector(doc)
                    .OfClass(typeof(Wall))                    // Chỉ lấy element kiểu Wall
                    .WhereElementIsNotElementType()            // Chỉ lấy instance, bỏ WallType
                    .Cast<Wall>()                              // Ép kiểu về Wall
                    .ToList();                                 // Chuyển thành List<Wall>

                // --- Bước 2: Chuyển đổi dữ liệu thành DTO ---
                var wallDataList = new List<WallData>();

                foreach (var wall in walls)
                {
                    var wallData = ExtractWallData(wall);
                    if (wallData != null)
                    {
                        wallDataList.Add(wallData);
                    }
                }

                // --- Bước 3: Tạo response object ---
                var response = new ToolResponse
                {
                    ToolName = Name,
                    Success = true,
                    Message = $"Found {wallDataList.Count} walls in the project.",
                    Data = wallDataList,
                    Metadata = new ResponseMetadata
                    {
                        TotalCount = wallDataList.Count,
                        Unit = "meters",
                        DocumentTitle = doc.Title,
                        Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
                    }
                };

                // --- Bước 4: Serialize thành JSON ---
                return JsonConvert.SerializeObject(response, Formatting.Indented);
            }
            catch (Exception ex)
            {
                // Luôn trả về JSON hợp lệ, ngay cả khi lỗi.
                var errorResponse = new ToolResponse
                {
                    ToolName = Name,
                    Success = false,
                    Message = $"Error retrieving walls: {ex.Message}",
                    Data = null,
                    Metadata = null
                };

                return JsonConvert.SerializeObject(errorResponse, Formatting.Indented);
            }
        }

        // === PHƯƠNG THỨC HỖ TRỢ ===

        /// <summary>
        /// Trích xuất thông tin từ một Wall object thành WallData DTO.
        /// </summary>
        private WallData ExtractWallData(Wall wall)
        {
            try
            {
                // Lấy chiều dài từ LocationCurve
                double lengthFeet = 0;
                if (wall.Location is LocationCurve locationCurve)
                {
                    lengthFeet = locationCurve.Curve.Length;
                }

                // Lấy chiều cao từ tham số "Unconnected Height"
                double heightFeet = wall.get_Parameter(
                    BuiltInParameter.WALL_USER_HEIGHT_PARAM
                )?.AsDouble() ?? 0;

                // Lấy độ dày từ Wall.Width (đơn vị feet)
                double widthFeet = wall.Width;

                // Lấy tên Level
                string levelName = "N/A";
                if (wall.LevelId != ElementId.InvalidElementId)
                {
                    var level = wall.Document.GetElement(wall.LevelId) as Level;
                    levelName = level?.Name ?? "N/A";
                }

                // Lấy tên WallType
                string wallTypeName = wall.WallType?.Name ?? "Unknown";

                // Trả về DTO với đơn vị đã chuyển đổi sang meters
                return new WallData
                {
                    ElementId = wall.Id.IntegerValue,
                    Name = wall.Name ?? "Unnamed Wall",
                    WallTypeName = wallTypeName,
                    LengthMeters = Math.Round(lengthFeet * FEET_TO_METERS, 3),
                    HeightMeters = Math.Round(heightFeet * FEET_TO_METERS, 3),
                    WidthMeters = Math.Round(widthFeet * FEET_TO_METERS, 3),
                    LevelName = levelName,
                    AreaSquareMeters = Math.Round(
                        lengthFeet * FEET_TO_METERS * heightFeet * FEET_TO_METERS, 3
                    )
                };
            }
            catch (Exception)
            {
                // Nếu một tường bị lỗi, bỏ qua và tiếp tục
                return null;
            }
        }
    }

    // === DATA TRANSFER OBJECTS (DTOs) ===

    /// <summary>
    /// DTO chứa thông tin một tường — chỉ gồm dữ liệu cần thiết cho AI.
    /// </summary>
    public class WallData
    {
        [JsonProperty("element_id")]
        public int ElementId { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("wall_type")]
        public string WallTypeName { get; set; }

        [JsonProperty("length_m")]
        public double LengthMeters { get; set; }

        [JsonProperty("height_m")]
        public double HeightMeters { get; set; }

        [JsonProperty("width_m")]
        public double WidthMeters { get; set; }

        [JsonProperty("level")]
        public string LevelName { get; set; }

        [JsonProperty("area_m2")]
        public double AreaSquareMeters { get; set; }
    }

    /// <summary>
    /// Response chuẩn cho mỗi Tool — AI dựa vào cấu trúc này để đọc kết quả.
    /// </summary>
    public class ToolResponse
    {
        [JsonProperty("tool_name")]
        public string ToolName { get; set; }

        [JsonProperty("success")]
        public bool Success { get; set; }

        [JsonProperty("message")]
        public string Message { get; set; }

        [JsonProperty("data")]
        public object Data { get; set; }

        [JsonProperty("metadata")]
        public ResponseMetadata Metadata { get; set; }
    }

    /// <summary>
    /// Metadata bổ sung: tổng số, đơn vị, tên document...
    /// </summary>
    public class ResponseMetadata
    {
        [JsonProperty("total_count")]
        public int TotalCount { get; set; }

        [JsonProperty("unit")]
        public string Unit { get; set; }

        [JsonProperty("document_title")]
        public string DocumentTitle { get; set; }

        [JsonProperty("timestamp")]
        public string Timestamp { get; set; }
    }
}
```

### Bước 3: Sơ đồ chuyển đổi dữ liệu

```mermaid
flowchart TD
    subgraph REVIT["🏗️ Revit Document"]
        R1["Wall Object<br/>- Location (Curve)<br/>- Parameters<br/>- WallType<br/>- LevelId"]
    end

    subgraph EXTRACT["⚙️ ExtractWallData()"]
        E1["Lấy LocationCurve.Length"]
        E2["Lấy WALL_USER_HEIGHT_PARAM"]
        E3["Lấy Wall.Width"]
        E4["Lấy Level.Name"]
        E5["Chuyển đổi Feet → Meters"]
    end

    subgraph DTO["📦 WallData DTO"]
        D1["element_id: 12345<br/>name: 'Basic Wall'<br/>wall_type: 'Generic - 200mm'<br/>length_m: 5.486<br/>height_m: 3.048<br/>width_m: 0.200<br/>level: 'Level 1'<br/>area_m2: 16.721"]
    end

    subgraph JSON["📄 JSON Response"]
        J1["tool_name: 'get_walls'<br/>success: true<br/>data: [...]<br/>metadata: {...}"]
    end

    subgraph AI["🤖 AI Processing"]
        A1["AI đọc JSON<br/>→ Hiểu dữ liệu<br/>→ Trả lời người dùng"]
    end

    R1 --> E1
    R1 --> E2
    R1 --> E3
    R1 --> E4
    E1 --> E5
    E2 --> E5
    E3 --> E5
    E5 --> D1
    E4 --> D1
    D1 --> J1
    J1 --> A1

    style REVIT fill:#E67E22,color:#fff
    style EXTRACT fill:#9B59B6,color:#fff
    style DTO fill:#3498DB,color:#fff
    style JSON fill:#27AE60,color:#fff
    style AI fill:#4A90D9,color:#fff
```

***

## Cấu trúc dữ liệu JSON cho AI

### Tại sao JSON quan trọng?

AI không "nhìn thấy" mô hình Revit. AI chỉ đọc được **văn bản** (text). JSON là định dạng lý tưởng vì:

1. **Có cấu trúc** — AI dễ dàng phân tích (parse) dữ liệu
2. **Tự mô tả** — Tên trường (field name) giải thích ý nghĩa giá trị
3. **Phổ biến** — Hầu hết các AI model được train với JSON
4. **Nhẹ** — Truyền tải dữ liệu nhanh hơn XML

### Kết quả JSON mẫu từ GetWallsTool

```json
{
  "tool_name": "get_walls",
  "success": true,
  "message": "Found 3 walls in the project.",
  "data": [
    {
      "element_id": 12345,
      "name": "Basic Wall - Generic - 200mm",
      "wall_type": "Generic - 200mm",
      "length_m": 5.486,
      "height_m": 3.048,
      "width_m": 0.200,
      "level": "Level 1",
      "area_m2": 16.721
    },
    {
      "element_id": 12346,
      "name": "Basic Wall - Generic - 300mm",
      "wall_type": "Generic - 300mm",
      "length_m": 8.230,
      "height_m": 3.048,
      "width_m": 0.300,
      "level": "Level 1",
      "area_m2": 25.085
    },
    {
      "element_id": 12400,
      "name": "Curtain Wall - Curtain Wall 1",
      "wall_type": "Curtain Wall 1",
      "length_m": 12.000,
      "height_m": 4.000,
      "width_m": 0.050,
      "level": "Level 2",
      "area_m2": 48.000
    }
  ],
  "metadata": {
    "total_count": 3,
    "unit": "meters",
    "document_title": "MyProject.rvt",
    "timestamp": "2026-03-20 14:30:00"
  }
}
```

### Nguyên tắc thiết kế JSON cho AI

| Nguyên tắc          | Mô tả                                     | Ví dụ                               |
| ------------------- | ----------------------------------------- | ----------------------------------- |
| **snake\_case**     | Dùng snake\_case cho tên trường           | `wall_type`, không phải `WallType`  |
| **Đơn vị rõ ràng**  | Ghi đơn vị trong tên trường hoặc metadata | `length_m`, `area_m2`               |
| **Có message**      | Luôn có trường message tóm tắt            | `"Found 3 walls..."`                |
| **Có success flag** | AI biết kết quả thành công hay lỗi        | `"success": true`                   |
| **Flat structure**  | Tránh nested quá sâu (max 2-3 cấp)        | Dữ liệu phẳng, dễ đọc               |
| **Số tròn**         | Làm tròn số thập phân (3 chữ số)          | `5.486`, không phải `5.48600032...` |

***

## Dùng AI để viết Tool

Một trong những ưu điểm lớn của hệ thống MCP là bạn có thể **dùng chính AI để viết code Tool mới**. Đây là phương pháp "prompt engineering" để tạo Tool tự động.

### Prompt mẫu để AI viết Tool

```
Bạn là một C# developer chuyên về Revit API.
Hãy viết một MCP Tool có tên "get_rooms" để lấy danh sách
tất cả các Room trong Revit project.

Yêu cầu:
1. Implement interface ITool (Name, Description, Execute)
2. Sử dụng FilteredElementCollector để truy vấn Room
3. Trả về JSON với các trường: element_id, name, number,
   level, area_m2, perimeter_m
4. Chuyển đổi đơn vị từ feet sang meters
5. Xử lý lỗi (try-catch)
6. Có comment giải thích từng bước

Tham khảo class GetWallsTool sau:
[dán code GetWallsTool vào đây]
```

### Quy trình AI tạo Tool

```mermaid
flowchart TD
    A["1. Developer viết Prompt<br/>Mô tả Tool cần tạo"] --> B["2. AI sinh code C#<br/>Dựa trên mẫu và yêu cầu"]
    B --> C["3. Developer review code<br/>Kiểm tra logic, API calls"]
    C --> D{"Đúng?"}
    D -->|"Chưa đúng"| E["4a. Góp ý cho AI<br/>Chỉ ra lỗi cụ thể"]
    E --> B
    D -->|"Đúng"| F["4b. Tích hợp vào project<br/>Build và test"]
    F --> G["5. Test trong Revit<br/>Chạy thử với dữ liệu thực"]
    G --> H{"OK?"}
    H -->|"Lỗi"| I["6a. Debug<br/>Đọc log, sửa lỗi"]
    I --> C
    H -->|"OK"| J["6b. Hoàn thành<br/>Tool sẵn sàng sử dụng"]

    style A fill:#4A90D9,color:#fff
    style B fill:#9B59B6,color:#fff
    style F fill:#27AE60,color:#fff
    style J fill:#27AE60,color:#fff
```

### Mẹo khi dùng AI để tạo Tool

1. **Cung cấp mẫu (template):** Luôn đính kèm một Tool mẫu (như `GetWallsTool`) để AI hiểu pattern cần theo.
2. **Chỉ rõ Revit API class:** Ghi rõ tên class cần dùng (ví dụ: `SpatialElement`, `Room`, `BuiltInParameter.ROOM_AREA`) vì AI có thể nhầm API version.
3. **Yêu cầu comment:** Yêu cầu AI viết comment cho từng bước, giúp bạn review dễ dàng hơn.
4. **Test từng phần:** Không copy-paste toàn bộ code AI sinh ra. Đọc kỹ, hiểu, rồi mới dùng.

***

## Bố trí các hàm sẵn có — Tận dụng Revit API

Revit API cung cấp rất nhiều method sẵn có. Thay vì viết logic phức tạp, hãy tận dụng chúng:

### Các method hữu ích cho việc tạo Tool

```csharp
// === TRUY VẤN ELEMENT ===

// Lấy tất cả element theo class
var walls = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall))
    .WhereElementIsNotElementType()
    .ToElements();

// Lấy tất cả element theo category
var doors = new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Doors)
    .WhereElementIsNotElementType()
    .ToElements();

// Lọc theo nhiều điều kiện (AND)
var structuralWalls = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall))
    .WhereElementIsNotElementType()
    .Cast<Wall>()
    .Where(w => w.StructuralUsage == StructuralWallUsage.Bearing)
    .ToList();

// === ĐỌC PARAMETER ===

// Lấy parameter built-in
double height = wall.get_Parameter(
    BuiltInParameter.WALL_USER_HEIGHT_PARAM
)?.AsDouble() ?? 0;

// Lấy parameter theo tên
string mark = wall.LookupParameter("Mark")?.AsString() ?? "";

// Lấy parameter kiểu số
double area = wall.get_Parameter(
    BuiltInParameter.HOST_AREA_COMPUTED
)?.AsDouble() ?? 0;

// === THÔNG TIN HÌNH HỌC ===

// Lấy BoundingBox
BoundingBoxXYZ bbox = element.get_BoundingBox(null);
if (bbox != null)
{
    XYZ min = bbox.Min; // Góc dưới-trái-trước
    XYZ max = bbox.Max; // Góc trên-phải-sau
}

// Lấy vị trí (Location)
if (element.Location is LocationPoint locPoint)
{
    XYZ position = locPoint.Point; // Tọa độ (x, y, z) tính bằng feet
}
```

### Bảng tham khảo nhanh — Các BuiltInCategory thường dùng

| BuiltInCategory         | Mô tả      | Class tương ứng               |
| ----------------------- | ---------- | ----------------------------- |
| `OST_Walls`             | Tường      | `Wall`                        |
| `OST_Floors`            | Sàn        | `Floor`                       |
| `OST_Roofs`             | Mái        | `RoofBase`                    |
| `OST_Doors`             | Cửa đi     | `FamilyInstance`              |
| `OST_Windows`           | Cửa sổ     | `FamilyInstance`              |
| `OST_Rooms`             | Phòng      | `Room` (cần `SpatialElement`) |
| `OST_StructuralColumns` | Cột        | `FamilyInstance`              |
| `OST_StructuralFraming` | Dầm        | `FamilyInstance`              |
| `OST_Levels`            | Tầng/Level | `Level`                       |
| `OST_Grids`             | Lưới trục  | `Grid`                        |

***

## Bài tập thực hành: Viết GetFloorsTool

Hãy áp dụng những gì đã học để tự viết `GetFloorsTool` — Tool lấy danh sách tất cả sàn (Floor) trong project.

### Yêu cầu

1. Implement interface `ITool`
2. Tool name: `"get_floors"`
3. Trả về các trường: `element_id`, `name`, `floor_type`, `level`, `area_m2`, `perimeter_m`, `thickness_m`
4. Chuyển đổi đơn vị sang meters
5. Xử lý lỗi đầy đủ (try-catch)
6. Trả về JSON đúng chuẩn `ToolResponse`

### Gợi ý

```csharp
// Lấy danh sách Floor instances
var floors = new FilteredElementCollector(doc)
    .OfClass(typeof(Floor))
    .WhereElementIsNotElementType()
    .Cast<Floor>()
    .ToList();

// Lấy diện tích
double areaFeet2 = floor.get_Parameter(
    BuiltInParameter.HOST_AREA_COMPUTED
)?.AsDouble() ?? 0;
// Chuyển đổi: 1 sq foot = 0.092903 sq meters
double areaM2 = areaFeet2 * 0.092903;

// Lấy chu vi
double perimeterFeet = floor.get_Parameter(
    BuiltInParameter.HOST_PERIMETER_COMPUTED
)?.AsDouble() ?? 0;
double perimeterM = perimeterFeet * 0.3048;
```

<details>

<summary><strong>💡 Xem đáp án GetFloorsTool</strong></summary>

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Revit.DB;
using Newtonsoft.Json;

namespace DeepBim.RevitMCP.Tools
{
    public class GetFloorsTool : ITool
    {
        public string Name => "get_floors";

        public string Description =>
            "Retrieves all floor instances from the current Revit project. " +
            "Returns floor Id, Name, FloorType, Level, Area (m2), " +
            "Perimeter (m), and Thickness (m).";

        private const double FEET_TO_METERS = 0.3048;
        private const double SQ_FEET_TO_SQ_METERS = 0.092903;

        public string Execute(Document doc, Dictionary<string, object> parameters)
        {
            try
            {
                var floors = new FilteredElementCollector(doc)
                    .OfClass(typeof(Floor))
                    .WhereElementIsNotElementType()
                    .Cast<Floor>()
                    .ToList();

                var floorDataList = floors
                    .Select(f => ExtractFloorData(f))
                    .Where(f => f != null)
                    .ToList();

                var response = new ToolResponse
                {
                    ToolName = Name,
                    Success = true,
                    Message = $"Found {floorDataList.Count} floors in the project.",
                    Data = floorDataList,
                    Metadata = new ResponseMetadata
                    {
                        TotalCount = floorDataList.Count,
                        Unit = "meters",
                        DocumentTitle = doc.Title,
                        Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
                    }
                };

                return JsonConvert.SerializeObject(response, Formatting.Indented);
            }
            catch (Exception ex)
            {
                var errorResponse = new ToolResponse
                {
                    ToolName = Name,
                    Success = false,
                    Message = $"Error retrieving floors: {ex.Message}",
                    Data = null,
                    Metadata = null
                };
                return JsonConvert.SerializeObject(errorResponse, Formatting.Indented);
            }
        }

        private FloorData ExtractFloorData(Floor floor)
        {
            try
            {
                double areaFeet2 = floor.get_Parameter(
                    BuiltInParameter.HOST_AREA_COMPUTED
                )?.AsDouble() ?? 0;

                double perimeterFeet = floor.get_Parameter(
                    BuiltInParameter.HOST_PERIMETER_COMPUTED
                )?.AsDouble() ?? 0;

                // Lấy độ dày từ FloorType
                double thicknessFeet = 0;
                var floorType = floor.FloorType;
                var structure = floorType?.GetCompoundStructure();
                if (structure != null)
                {
                    for (int i = 0; i < structure.LayerCount; i++)
                    {
                        thicknessFeet += structure.GetLayerWidth(i);
                    }
                }

                string levelName = "N/A";
                if (floor.LevelId != ElementId.InvalidElementId)
                {
                    var level = floor.Document.GetElement(floor.LevelId) as Level;
                    levelName = level?.Name ?? "N/A";
                }

                return new FloorData
                {
                    ElementId = floor.Id.IntegerValue,
                    Name = floor.Name ?? "Unnamed Floor",
                    FloorTypeName = floorType?.Name ?? "Unknown",
                    LevelName = levelName,
                    AreaSquareMeters = Math.Round(areaFeet2 * SQ_FEET_TO_SQ_METERS, 3),
                    PerimeterMeters = Math.Round(perimeterFeet * FEET_TO_METERS, 3),
                    ThicknessMeters = Math.Round(thicknessFeet * FEET_TO_METERS, 3)
                };
            }
            catch
            {
                return null;
            }
        }
    }

    public class FloorData
    {
        [JsonProperty("element_id")]
        public int ElementId { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("floor_type")]
        public string FloorTypeName { get; set; }

        [JsonProperty("level")]
        public string LevelName { get; set; }

        [JsonProperty("area_m2")]
        public double AreaSquareMeters { get; set; }

        [JsonProperty("perimeter_m")]
        public double PerimeterMeters { get; set; }

        [JsonProperty("thickness_m")]
        public double ThicknessMeters { get; set; }
    }
}
```

</details>

***

## Câu hỏi tự suy nghĩ

Hãy suy nghĩ và trả lời các câu hỏi sau. Click vào từng câu để xem gợi ý trả lời.

### Câu 1: Tại sao phải gọi `.WhereElementIsNotElementType()`?

<details>

<summary><strong>💡 Xem trả lời</strong></summary>

Trong Revit, mỗi category có hai loại element:

* **Element Type** (hay "Symbol"): Là **định nghĩa kiểu**. Ví dụ: "Generic Wall - 200mm" là một WallType — nó định nghĩa độ dày, vật liệu, cấu trúc của kiểu tường đó.
* **Element Instance**: Là **đối tượng cụ thể** trong mô hình. Ví dụ: một bức tường cụ thể ở tầng 1, dài 5m, dùng kiểu "Generic Wall - 200mm".

Khi bạn muốn đếm "có bao nhiêu tường?", bạn cần đếm **instances**, không phải types. Nếu không gọi `.WhereElementIsNotElementType()`, kết quả sẽ lẫn lộn cả Type và Instance, dẫn đến sai số lượng và dữ liệu không chính xác.

```csharp
// SAI — trả về cả WallType và Wall instances
var wrong = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall))
    .ToElements(); // Lẫn type và instance!

// ĐÚNG — chỉ trả về Wall instances
var correct = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall))
    .WhereElementIsNotElementType()
    .ToElements();
```

</details>

### Câu 2: Tại sao không trả về trực tiếp Revit Wall object mà phải chuyển sang DTO?

<details>

<summary><strong>💡 Xem trả lời</strong></summary>

Có 4 lý do chính:

1. **Không serialize được:** Revit objects chứa tham chiếu nội bộ phức tạp (COM objects, pointers). `JsonConvert.SerializeObject` sẽ bị lỗi hoặc tạo JSON không lồ nếu cố serialize trực tiếp.
2. **Quá nhiều dữ liệu:** Một Wall object chứa hàng trăm properties và parameters. AI chỉ cần vài trường cơ bản (Id, Name, Length...). Gửi hết sẽ làm chậm hệ thống và khiến AI "nhầm lẫn".
3. **Kiểm soát đơn vị:** DTO cho phép chuyển đổi đơn vị (feet → meters) trước khi gửi. AI và người dùng không cần biết Revit dùng feet nội bộ.
4. **Bảo mật:** DTO chỉ chứa dữ liệu bạn muốn chia sẻ. Không lo Revit internal state bị expose ra ngoài.

</details>

### Câu 3: Nên dùng `.OfClass()` hay `.OfCategory()` để lọc element?

<details>

<summary><strong>💡 Xem trả lời</strong></summary>

| Tiêu chí  | `.OfClass()`            | `.OfCategory()`             |
| --------- | ----------------------- | --------------------------- |
| Lọc theo  | Class C# (typeof)       | BuiltInCategory enum        |
| Dùng khi  | Biết chính xác class    | Cần lọc theo category Revit |
| Ví dụ     | `OfClass(typeof(Wall))` | `OfCategory(OST_Doors)`     |
| Hiệu suất | Nhanh hơn (lọc sớm)     | Chậm hơn một chút           |

**Nguyên tắc chung:**

* **Dùng `OfClass()`** khi element có class riêng: `Wall`, `Floor`, `Level`, `Grid`, `Room`...
* **Dùng `OfCategory()`** khi element là `FamilyInstance` nhưng thuộc nhiều category: Doors, Windows, Columns, Furniture... (các loại này đều là `FamilyInstance`, không phân biệt được bằng class).

```csharp
// Wall có class riêng → dùng OfClass
var walls = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall));

// Door là FamilyInstance → dùng OfCategory
var doors = new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Doors)
    .WhereElementIsNotElementType();
```

</details>

### Câu 4: Nếu project có 10,000 tường, Tool có bị chậm không? Làm sao tối ưu?

<details>

<summary><strong>💡 Xem trả lời</strong></summary>

Có thể chậm. Các cách tối ưu:

1. **Phân trang (Pagination):** Thêm parameter `page` và `page_size` vào Tool. AI chỉ lấy 50-100 tường mỗi lần.

```csharp
int page = parameters.ContainsKey("page")
    ? Convert.ToInt32(parameters["page"]) : 1;
int pageSize = 50;
var pagedWalls = walls.Skip((page - 1) * pageSize).Take(pageSize).ToList();
```

2. **Lọc theo Level:** Cho phép AI chỉ lấy tường ở một tầng cụ thể.

```csharp
var wallsOnLevel1 = new FilteredElementCollector(doc)
    .OfClass(typeof(Wall))
    .WhereElementIsNotElementType()
    .Cast<Wall>()
    .Where(w => w.LevelId == level1Id)
    .ToList();
```

3. **Chỉ trả về trường cần thiết:** Nếu AI chỉ cần đếm số lượng, không cần trả về hết thông tin chi tiết.
4. **Cache kết quả:** Lưu kết quả vào bộ nhớ tạm, chỉ query lại khi model thay đổi.

</details>

### Câu 5: Viết mô tả (Description) cho Tool như thế nào để AI gọi đúng?

<details>

<summary><strong>💡 Xem trả lời</strong></summary>

Mô tả Tool là **yếu tố quyết định** để AI biết khi nào nên gọi Tool nào. Một số nguyên tắc:

1. **Viết bằng tiếng Anh:** Hầu hết AI model hiểu tiếng Anh tốt nhất.
2. **Nêu rõ "làm gì":** "Retrieves all wall instances..." — rõ ràng hơn "Gets walls."
3. **Nêu rõ "trả về gì":** "Returns wall Id, Name, Length (m)..." — AI biết trước cấu trúc dữ liệu.
4. **Nêu rõ "khi nào dùng":** "Use this when the user asks about walls..." — giúp AI phân biệt các Tool tương tự.
5. **Không quá dài:** 1-3 câu là đủ. Quá dài AI sẽ bỏ qua.

**Ví dụ tốt:**

```
"Retrieves all wall instances from the current Revit project.
Returns wall Id, Name, Length (m), Height (m), Width (m), Level,
and WallType. Use when the user asks about walls in the model."
```

**Ví dụ xấu:**

```
"Gets walls."  // Quá ngắn, AI không biết trả về gì
```

</details>

***

## Tổng kết

Trong bài học này, chúng ta đã học:

| Nội dung                     | Chi tiết                                                                  |
| ---------------------------- | ------------------------------------------------------------------------- |
| **FilteredElementCollector** | Công cụ truy vấn phần tử trong Revit, hoạt động như pipeline lọc          |
| **GetWallsTool**             | Triển khai đầy đủ một Tool: truy vấn, chuyển đổi đơn vị, tạo JSON         |
| **DTO Pattern**              | Tách dữ liệu cần thiết từ Revit object thành object đơn giản để serialize |
| **JSON cho AI**              | Thiết kế cấu trúc JSON rõ ràng, có metadata, có error handling            |
| **AI sinh code**             | Sử dụng prompt engineering để AI viết Tool mới từ template có sẵn         |
| **Đơn vị**                   | Revit lưu feet nội bộ, cần chuyển sang meters khi xuất dữ liệu            |

### Điều cần nhớ

```
FilteredElementCollector → Lọc element → Chuyển DTO → Serialize JSON → AI đọc
```

Mỗi Tool là một "cầu nối" giữa Revit và AI. Thiết kế tốt sẽ giúp AI trả lời chính xác, thiết kế xấu sẽ khiến AI "hiểu nhầm" dữ liệu.

### Chuẩn bị cho bài tiếp theo

Trong **Bài 4: Dựng MCP Server bằng Node.js**, chúng ta sẽ:

* Khởi tạo project Node.js với MCP SDK
* Đăng ký các Tool vào MCP Server
* Kết nối MCP Server với Revit Plugin
* Test thử toàn bộ luồng AI → MCP → Revit

***

## Tài liệu tham khảo

1. **Mã nguồn plugin:** [deepbim-revit-mcp-plugin](https://github.com/nguyenngocdue/deepbim-revit-mcp-plugin) — Repository chính chứa toàn bộ mã nguồn của khóa học.
2. **Mã nguồn MCP Server tham chiếu:** [revit-mcp-server](https://github.com/nguyenngocdue/revit-mcp-server) — Đối chiếu cách tổ chức tool và server thực tế.
3. **Revit API Documentation:** [Revit API Docs](https://www.revitapidocs.com/) — Tra cứu class, method, parameter của Revit API.
4. **Model Context Protocol (MCP):** [MCP Specification](https://modelcontextprotocol.io/) — Đặc tả giao thức MCP của Anthropic.
5. **Newtonsoft.Json:** [Json.NET Documentation](https://www.newtonsoft.com/json/help/html/Introduction.htm) — Thư viện JSON serialization được sử dụng trong project.
6. **The Building Coder:** [Blog](https://thebuildingcoder.typepad.com/) — Blog nổi tiếng về Revit API bởi Jeremy Tammik (Autodesk).

***

> **Bài trước:** [← Bài 2: Demo kết nối thực tiễn](/revit-mcp-ai/phan-1-nen-tang/bai-2.md)
>
> **Bài tiếp theo:** [Bài 4: Dựng MCP Server bằng Node.js →](/revit-mcp-ai/phan-2-xay-dung-tung-thanh-phan/bai-4.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://deepbim.gitbook.io/revit-mcp-ai/phan-2-xay-dung-tung-thanh-phan/bai-3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
