Xây Dựng Hệ Thống Đa Tác Nhân AI Thông Minh: Giao Tiếp A2A, AG-UI & CopilotKit Từ A Đến Z

Trong thế giới phát triển AI đang tăng tốc hiện nay, việc tạo ra các tác nhân trí tuệ nhân tạo có thể tương tác không chỉ với ứng dụng của bạn mà còn với nhau là chìa khóa để mở khóa các khả năng phức tạp và hiệu quả. Bài viết này sẽ cung cấp một hướng dẫn toàn diện về cách xây dựng hệ thống giao tiếp đa tác nhân AI (Agent-to-Agent – A2A) đầy đủ ngăn xếp (full-stack) bằng cách tận dụng sức mạnh của Giao thức A2A, Giao thức AG-UI và khung CopilotKit mạnh mẽ. Chúng ta sẽ cùng nhau khám phá từng thành phần, cách chúng hoạt động cùng nhau và cách bạn có thể triển khai chúng trong các dự án của mình.

Giao Thức A2A là gì? Nền Tảng Cho Sự Cộng Tác Của AI

Giao thức A2A (Agent-to-Agent), do Google phát triển, là một khuôn khổ giao tiếp tiêu chuẩn hóa cho phép các tác nhân AI khác nhau khám phá, giao tiếp và cộng tác trong một hệ thống phân tán, bất kể khung làm việc của chúng. Hãy tưởng tượng một mạng lưới các chuyên gia AI, mỗi người có một bộ kỹ năng độc đáo, và Giao thức A2A là ngôn ngữ chung cho phép họ phối hợp các nỗ lực để giải quyết các vấn đề phức tạp. Mục tiêu chính của A2A là tạo điều kiện cho giao tiếp liên tác nhân, nơi các tác nhân có thể gọi các tác nhân khác như các công cụ hoặc dịch vụ, xây dựng một mạng lưới các khả năng AI chuyên biệt.

Các Tính Năng Chính của Giao Thức A2A:

  • A2A Client: Đây là tác nhân “ông chủ” (chúng ta gọi là “tác nhân client”) khởi xướng mọi thứ. Nó xác định những gì cần làm, tìm ra các tác nhân trợ giúp phù hợp và giao nhiệm vụ cho họ. Hãy nghĩ về nó như một người quản lý dự án trong mã của bạn, điều phối toàn bộ quy trình.
  • A2A Agent: Một tác nhân AI thiết lập một địa chỉ web đơn giản (một endpoint HTTP) tuân thủ các quy tắc A2A. Nó lắng nghe các yêu cầu đến, xử lý tác vụ và gửi lại kết quả hoặc cập nhật. Điều này cực kỳ hữu ích để làm cho tác nhân của bạn “công khai” và sẵn sàng cộng tác.
  • Agent Card: Tưởng tượng một thẻ ID kỹ thuật số ở định dạng JSON—dễ đọc và chia sẻ. Nó chứa thông tin cơ bản về một tác nhân A2A, như tên, chức năng và cách kết nối. Agent Card giúp các tác nhân khác dễ dàng khám phá và hiểu được khả năng của nhau.
  • Agent Skills: Đây giống như các mô tả công việc cho tác nhân của bạn. Mỗi kỹ năng phác thảo một điều cụ thể mà tác nhân đó xuất sắc (ví dụ: “tóm tắt bài viết” hoặc “tạo hình ảnh”). Các client đọc những kỹ năng này để biết chính xác tác vụ nào cần giao—không còn phải đoán mò!
  • A2A Executor: Bộ não phía sau hậu trường. Đó là một hàm trong mã của bạn thực hiện công việc nặng nhọc: nhận một yêu cầu, chạy logic để giải quyết tác vụ và đưa ra phản hồi hoặc kích hoạt các sự kiện.
  • A2A Server: Phía máy chủ web của mọi thứ. Nó biến các kỹ năng của tác nhân của bạn thành thứ có thể chia sẻ qua internet. Bạn sẽ thiết lập nó với trình xử lý yêu cầu của A2A, xây dựng một ứng dụng web nhẹ bằng Starlette (một khung web Python) và khởi động nó bằng Uvicorn (một máy chủ chạy nhanh). Bùm—tác nhân của bạn đã trực tuyến và sẵn sàng hành động!

Để tìm hiểu sâu hơn về cách Giao thức A2A hoạt động và cách thiết lập nó, bạn có thể tham khảo tài liệu chính thức tại: A2A protocol docs.

Chuẩn Bị Toàn Diện: Các Yêu Cầu Tiên Quyết Để Bắt Đầu

Trước khi chúng ta đi sâu vào triển khai, việc đảm bảo bạn có đầy đủ các công cụ và kiến thức cơ bản là rất quan trọng. Để hiểu đầy đủ và theo dõi hướng dẫn này, bạn cần có hiểu biết cơ bản về React hoặc Next.js. Ngoài ra, chúng ta sẽ sử dụng các công nghệ sau:

  • Python: Ngôn ngữ lập trình phổ biến để xây dựng các tác nhân AI với các khung tác nhân AI; đảm bảo nó đã được cài đặt trên máy tính của bạn.
  • Giao thức AG-UI (Agent User Interaction Protocol): Được phát triển bởi CopilotKit, AG-UI là một giao thức mã nguồn mở, nhẹ, dựa trên sự kiện, tạo điều kiện cho các tương tác phong phú, thời gian thực giữa frontend và backend tác nhân AI của bạn.
  • Google ADK (Agent Development Kit): Một khung mã nguồn mở được Google thiết kế để đơn giản hóa quá trình xây dựng các tác nhân AI phức tạp và sẵn sàng sản xuất.
  • LangGraph (LangGraph): Một khung để tạo và triển khai các tác nhân AI. Nó cũng giúp xác định các luồng kiểm soát và các hành động được thực hiện bởi tác nhân.
  • Gemini API Key (Gemini API Key): Một khóa API để cho phép bạn thực hiện các tác vụ khác nhau bằng cách sử dụng các mô hình Gemini cho các tác nhân ADK.
  • CopilotKit (CopilotKit): Một khung copilot mã nguồn mở để xây dựng các chatbot AI tùy chỉnh, các tác nhân AI trong ứng dụng và các vùng văn bản.

Thiết Lập Giao Tiếp Đa Tác Nhân A2A qua CLI: Khởi Đầu Nhanh Chóng

Trong phần này, bạn sẽ học cách thiết lập một client A2A (tác nhân điều phối) và các tác nhân A2A bằng một lệnh CLI giúp thiết lập backend bằng Google ADK với giao thức AG-UI và frontend bằng CopilotKit. Quá trình này được thiết kế để đơn giản hóa việc triển khai ban đầu, cho phép bạn nhanh chóng khởi động và chạy một hệ thống đa tác nhân hoạt động đầy đủ.

Hãy cùng bắt đầu.

Bước 1: Chạy Lệnh CLI

Nếu bạn chưa có một tác nhân AG-UI được cấu hình sẵn, bạn có thể thiết lập nhanh chóng bằng cách chạy lệnh CLI dưới đây trong terminal của mình:

npx copilotkit@latest create -f a2a

Sau đó, bạn sẽ được yêu cầu đặt tên cho dự án của mình.

Bước 2: Cài Đặt Các Phụ Thuộc Frontend

Khi dự án của bạn đã được tạo thành công, hãy cài đặt các phụ thuộc bằng trình quản lý gói ưa thích của bạn:

npm install

Bước 3: Cài Đặt Các Phụ Thuộc Backend

Sau khi cài đặt các phụ thuộc frontend, hãy cài đặt các phụ thuộc backend:

cd agents
python3 -m venv .venv
source .venv/bin/activate  # Trên Windows: .venv\Scripts\activate
pip install -r requirements.txt
cd ..

Bước 4: Cấu Hình Biến Môi Trường

Khi bạn đã cài đặt các phụ thuộc backend, hãy thiết lập các biến môi trường:

cp .env.example .env
# Sửa file .env và thêm các khóa API của bạn:
# GOOGLE_API_KEY=your_google_api_key
# OPENAI_API_KEY=your_openai_api_key

Bước 5: Khởi Động Tất Cả Các Dịch Vụ

Sau khi thiết lập các biến môi trường, hãy khởi động tất cả các dịch vụ bao gồm backend và frontend:

npm run dev

Khi máy chủ phát triển đang chạy, hãy điều hướng đến http://localhost:3000/ và bạn sẽ thấy giao diện frontend đa tác nhân A2A của mình đang hoạt động.

Chúc mừng! Bạn đã thiết lập thành công giao tiếp đa tác nhân A2A. Hãy thử yêu cầu tác nhân của bạn nghiên cứu một chủ đề, chẳng hạn như “Vui lòng nghiên cứu điện toán lượng tử”. Bạn sẽ thấy nó gửi tin nhắn đến tác nhân nghiên cứu và tác nhân phân tích. Sau đó, nó sẽ trình bày kết quả nghiên cứu và phân tích hoàn chỉnh cho người dùng.

Tích Hợp Tác Nhân Điều Phối với Google ADK và Giao Thức AG-UI ở Backend

Trong phần này, bạn sẽ tìm hiểu cách tích hợp tác nhân điều phối của mình với Google ADK và giao thức AG-UI để hiển thị nó ra frontend dưới dạng một ứng dụng ASGI (Asynchronous Server Gateway Interface). Điều này cho phép tác nhân của bạn giao tiếp hiệu quả với giao diện người dùng.

Hãy bắt đầu.

Bước 1: Thiết Lập Backend

Để bắt đầu, hãy clone kho lưu trữ A2A-Travel repository bao gồm backend (các tác nhân) dựa trên Python và frontend Next.js.

Tiếp theo, điều hướng đến thư mục backend:

cd agents

Sau đó, tạo một môi trường ảo Python mới:

python -m venv .venv

Sau đó, kích hoạt môi trường ảo:

source .venv/bin/activate  # Trên Windows: .venv\Scripts\activate

Cuối cùng, cài đặt tất cả các phụ thuộc Python được liệt kê trong file requirements.txt.

pip install -r requirements.txt

Bước 2: Cấu Hình Tác Nhân Điều Phối ADK Của Bạn

Sau khi thiết lập backend, hãy cấu hình tác nhân điều phối ADK của bạn bằng cách định nghĩa tên tác nhân, chỉ định Gemini 2.5 Pro làm Mô hình Ngôn ngữ Lớn (LLM) và định nghĩa các hướng dẫn của tác nhân, như được hiển thị dưới đây trong file agents/orchestrator.py:

# Import Google ADK components for LLM agent creation
from google.adk.agents import LlmAgent

# === CẤU HÌNH TÁC NHÂN ĐIỀU PHỐI ===
# Tạo tác nhân điều phối chính bằng LlmAgent của Google ADK
# Tác nhân này điều phối tất cả các hoạt động lập kế hoạch du lịch và quản lý quy trình làm việc

orchestrator_agent = LlmAgent(
    name="OrchestratorAgent",
    model="gemini-2.5-pro",  # Sử dụng mô hình Pro mạnh mẽ hơn cho việc điều phối phức tạp
    instruction="""
    Bạn là một tác nhân điều phối lập kế hoạch du lịch. Vai trò của bạn là điều phối các tác nhân chuyên biệt
    để tạo ra các kế hoạch du lịch cá nhân hóa.

    CÁC TÁC NHÂN CHUYÊN BIỆT CÓ SẴN:

    1. **Tác nhân Lịch trình** (LangGraph) - Tạo lịch trình du lịch từng ngày với các hoạt động
    2. **Tác nhân Nhà hàng** (LangGraph) - Đề xuất nhà hàng cho bữa sáng, bữa trưa và bữa tối theo ngày
    3. **Tác nhân Thời tiết** (ADK) - Cung cấp dự báo thời tiết và lời khuyên đóng gói
    4. **Tác nhân Ngân sách** (ADK) - Ước tính chi phí du lịch và tạo bảng phân tích ngân sách

    CÁC RÀNG BUỘC QUAN TRỌNG:
    - Bạn PHẢI gọi các tác nhân MỘT CÁCH TUẦN TỰ, không bao giờ thực hiện nhiều cuộc gọi công cụ đồng thời
    - Sau khi thực hiện một cuộc gọi công cụ, HÃY CHỜ kết quả trước khi thực hiện cuộc gọi công cụ khác
    - KHÔNG thực hiện các cuộc gọi công cụ song song/đồng thời - điều này không được hỗ trợ

    QUY TRÌNH LÀM VIỆC ĐƯỢC ĐỀ XUẤT CHO VIỆC LẬP KẾ HOẠCH DU LỊCH:
    // Thêm các hướng dẫn chi tiết về quy trình làm việc tại đây, ví dụ:
    // 1. Phân tích yêu cầu của người dùng để hiểu nhu cầu du lịch.
    // 2. Gọi Tác nhân Lịch trình để tạo lịch trình cơ bản.
    // 3. Gọi Tác nhân Thời tiết để lấy thông tin dự báo.
    // 4. Gọi Tác nhân Nhà hàng để đề xuất địa điểm ăn uống.
    // 5. Gọi Tác nhân Ngân sách để ước tính chi phí.
    // 6. Tổng hợp tất cả thông tin và trình bày cho người dùng.

    """,
)

Bước 3: Tạo Một Phiên Bản Tác Nhân Middleware ADK

Sau khi cấu hình tác nhân điều phối ADK của bạn, hãy tạo một phiên bản tác nhân middleware ADK bọc tác nhân điều phối ADK của bạn để tích hợp nó với giao thức AG-UI, như được hiển thị dưới đây trong file agents/orchestrator.py:

# Import AG-UI ADK components for frontend integration
from ag_ui_adk import ADKAgent
# ... (các import khác)

# === TÍCH HỢP GIAO THỨC AG-UI ===
# Bọc tác nhân điều phối với các khả năng của Giao thức AG-UI
# Điều này cho phép giao tiếp frontend và cung cấp giao diện cho các tương tác người dùng

adk_orchestrator_agent = ADKAgent(
    adk_agent=orchestrator_agent,          # Tác nhân LLM cốt lõi mà chúng ta đã tạo ở trên
    app_name="orchestrator_app",           # Mã định danh ứng dụng duy nhất
    user_id="demo_user",                   # ID người dùng mặc định cho mục đích demo
    session_timeout_seconds=3600,          # Thời gian chờ phiên (1 giờ)
    use_in_memory_services=True            # Sử dụng bộ nhớ trong để đơn giản
)

Bước 4: Cấu Hình Endpoint FastAPI

Khi bạn đã tạo một phiên bản tác nhân middleware ADK, hãy cấu hình một endpoint FastAPI hiển thị tác nhân điều phối ADK được bọc AG-UI của bạn ra frontend, như được hiển thị dưới đây trong file agents/orchestrator.py:

# Import necessary libraries for web server and environment variables
import os
import uvicorn

# Import FastAPI for creating HTTP endpoints
from fastapi import FastAPI
# Cần import add_adk_fastapi_endpoint từ ag_ui_adk
from ag_ui_adk import add_adk_fastapi_endpoint

# ... (các định nghĩa tác nhân và middleware ở trên)

# === THIẾT LẬP ỨNG DỤNG WEB FASTAPI ===
# Tạo ứng dụng FastAPI sẽ phục vụ tác nhân điều phối
# Điều này cung cấp các endpoint HTTP cho giao tiếp Giao thức AG-UI

app = FastAPI(title="Travel Planning Orchestrator (ADK)")

# Thêm endpoint tác nhân ADK vào ứng dụng FastAPI
# Điều này tạo ra các tuyến đường cần thiết cho giao tiếp Giao thức AG-UI
add_adk_fastapi_endpoint(app, adk_orchestrator_agent, path="/")

# === ĐIỂM KHỞI ĐẦU CHÍNH CỦA ỨNG DỤNG ===
if __name__ == "__main__":
    """
    Điểm khởi đầu chính khi script được chạy trực tiếp.

    Hàm này:
    1. Kiểm tra các biến môi trường bắt buộc (khóa API)
    2. Cấu hình cổng server
    3. Khởi động server uvicorn với ứng dụng FastAPI
    """

    # Kiểm tra khóa Google API bắt buộc
    if not os.getenv("GOOGLE_API_KEY"):
        print("⚠️  Cảnh báo: Biến môi trường GOOGLE_API_KEY chưa được đặt!")
        print("   Đặt nó bằng: export GOOGLE_API_KEY='khóa-của-bạn-ở-đây'")
        print("   Lấy khóa từ: https://aistudio.google.com/app/apikey")
        print()

    # Lấy cổng server từ biến môi trường, mặc định là 9000
    port = int(os.getenv("ORCHESTRATOR_PORT", 9000))

    # Khởi động server với thông tin chi tiết
    print(f"🚀 Khởi động Tác nhân Điều phối (ADK + AG-UI) trên http://localhost:{port}")

    # Chạy ứng dụng FastAPI bằng uvicorn
    # host="0.0.0.0" cho phép kết nối từ bên ngoài
    # port có thể cấu hình qua biến môi trường
    uvicorn.run(app, host="0.0.0.0", port=port)

Chúc mừng! Bạn đã tích hợp thành công Tác nhân Điều phối ADK của mình với giao thức AG-UI, và nó hiện có sẵn tại endpoint http://localhost:9000 (hoặc cổng được chỉ định).

Tích Hợp Các Tác Nhân AI Từ Các Framework Khác Nhau với Giao Thức A2A

Trong phần này, bạn sẽ tìm hiểu cách tích hợp các tác nhân AI từ các khung tác nhân khác nhau (như LangGraph hoặc các tác nhân ADK chuyên biệt) với giao thức A2A. Điều này cho phép tác nhân điều phối chính của bạn (orchestrator agent) ủy quyền các tác vụ cho các tác nhân chuyên biệt, tạo ra một hệ sinh thái AI mạnh mẽ và linh hoạt.

Hãy bắt đầu!

Bước 1: Cấu Hình Tác Nhân Từ Xa A2A

Để bắt đầu, hãy cấu hình tác nhân từ xa A2A của bạn, chẳng hạn như tác nhân lịch trình sử dụng khung LangGraph, như được hiển thị trong file agents/itinerary_agent.py:

# Import LangGraph components for workflow management
from langgraph.graph import StateGraph, END
import json # Cần thiết cho việc phân tích cú pháp JSON

# Các kiểu dữ liệu Pydantic cần thiết cho trạng thái và cấu trúc dữ liệu
from pydantic import BaseModel, Field
from typing import List, Optional

class ItineraryState(BaseModel):
    """Định nghĩa trạng thái cho quy trình làm việc của tác nhân lịch trình."""
    message: str
    destination: str = ""
    days: int = 0
    itinerary: str = ""
    structured_itinerary: Optional[dict] = None

class Activity(BaseModel):
    """Định nghĩa cấu trúc hoạt động trong lịch trình."""
    time: str
    description: str

class DayItinerary(BaseModel):
    """Định nghĩa cấu trúc lịch trình cho một ngày cụ thể."""
    day: int
    theme: str
    activities: List[Activity]

class StructuredItinerary(BaseModel):
    """Định nghĩa cấu trúc tổng thể của một lịch trình."""
    destination: str
    days: int
    itinerary: List[DayItinerary]

class Message(BaseModel):
    """Đại diện cho định dạng tin nhắn A2A đơn giản hóa."""
    parts: List[dict] = Field(default_factory=list)

# Giả sử ChatOpenAI được import từ langchain_openai hoặc tương tự
from langchain_openai import ChatOpenAI

# === LỚP TÁC NHÂN CHÍNH ===
class ItineraryAgent:
    """
    Lớp tác nhân chính xử lý việc tạo lịch trình bằng quy trình làm việc LangGraph.
    """

    def __init__(self):
        self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

        # Xây dựng và biên dịch quy trình làm việc LangGraph
        self.graph = self._build_graph()

    def _build_graph(self):
        workflow = StateGraph(ItineraryState)
        workflow.add_node("parse_request", self._parse_request)
        workflow.add_node("create_itinerary", self._create_itinerary)
        workflow.set_entry_point("parse_request")
        workflow.add_edge("parse_request", "create_itinerary")
        workflow.add_edge("create_itinerary", END)

        # Biên dịch quy trình làm việc thành một biểu đồ có thể thực thi
        return workflow.compile()

    def _parse_request(self, state: ItineraryState) -> ItineraryState:
        message = state.message

        # Tạo lời nhắc tập trung cho tác vụ trích xuất
        prompt = f"""
        Trích xuất điểm đến và số ngày từ yêu cầu du lịch này.
        Chỉ trả về MỘT chuỗi JSON với các trường 'destination' và 'days'.

        Yêu cầu: {message}

        Ví dụ đầu ra: {{"destination": "Tokyo", "days": 3}}
        """

        # Lấy phản hồi LLM để phân tích
        response = self.llm.invoke(prompt)

        # Debug: In phản hồi LLM để khắc phục sự cố
        print(response.content)

        try:
            # Cố gắng phân tích phản hồi JSON
            parsed = json.loads(response.content)
            state.destination = parsed.get("destination", "Unknown")
            state.days = int(parsed.get("days", 3))
        except Exception as e:
            # Giá trị dự phòng nếu phân tích thất bại
            print(f"⚠️  Không thể phân tích yêu cầu, sử dụng giá trị mặc định: {e}")
            state.destination = "Unknown"
            state.days = 3

        return state

    def _create_itinerary(self, state: ItineraryState) -> ItineraryState:
        destination = state.destination
        days = state.days

        # Tạo lời nhắc chi tiết để tạo lịch trình
        prompt = f"""
        Tạo một lịch trình du lịch chi tiết {days} ngày cho {destination}.
        Lịch trình phải được trả về dưới dạng JSON hợp lệ, không có markdown hoặc văn bản bổ sung.
        Mỗi ngày nên có một chủ đề và một danh sách các hoạt động với thời gian và mô tả.

        Ví dụ định dạng JSON:
        {{
          "destination": "Paris",
          "days": 3,
          "itinerary": [
            {{
              "day": 1,
              "theme": "Khám phá các biểu tượng",
              "activities": [
                {{"time": "9:00 AM", "description": "Tháp Eiffel"}},
                {{"time": "1:00 PM", "description": "Bảo tàng Louvre"}}
              ]
            }}
          ]
        }}

        Hãy làm cho nó thực tế, thú vị và bao gồm các tên địa điểm cụ thể.
        Chỉ trả về JSON hợp lệ, không có markdown, không có văn bản nào khác.
        """

        # Tạo lịch trình bằng LLM
        response = self.llm.invoke(prompt)
        content = response.content.strip()

        # Dọn dẹp phản hồi - loại bỏ định dạng markdown nếu có
        if "json" in content:
            content = content.split("```json")[1].split("```")[0].strip()
        elif "```" in content:
            content = content.split("```")[1].split("```")[0].strip()

        try:
            # Bước 1: Phân tích JSON từ phản hồi LLM
            structured_data = json.loads(content)

            # Bước 2: Xác thực cấu trúc bằng mô hình Pydantic
            validated_itinerary = StructuredItinerary(**structured_data)

            # Bước 3: Lưu cả dữ liệu đã xác thực và chuỗi JSON đã định dạng
            state.structured_itinerary = validated_itinerary.model_dump()
            state.itinerary = json.dumps(validated_itinerary.model_dump(), indent=2)

            print("✅ Đã tạo thành công lịch trình có cấu trúc")

        except Exception as e:
            print(f"❌ Lỗi khi tạo hoặc xác thực lịch trình: {e}")
            state.itinerary = json.dumps({"error": f"Failed to create itinerary: {e}"})
            state.structured_itinerary = None

        return state

    async def invoke(self, message: Message) -> str:
        # Trích xuất nội dung văn bản từ định dạng tin nhắn A2A
        message_text = message.parts[0]["root"]["text"] if message.parts and message.parts[0].get("root") else ""
        print("Gọi tác nhân lịch trình với tin nhắn: ", message_text)

        # Thực thi quy trình làm việc LangGraph với trạng thái ban đầu
        result = self.graph.invoke({
            "message": message_text,
            "destination": "",
            "days": 3,
            "itinerary": ""
        })

        # Trả về chuỗi JSON lịch trình cuối cùng
        return result["itinerary"]

Bước 2: Thiết Lập Kỹ Năng và Thẻ Tác Nhân A2A Từ Xa

Khi bạn đã cấu hình các tác nhân từ xa A2A, hãy thiết lập tác nhân để nó có thể được khám phá và gọi bởi các tác nhân khác. Để làm điều đó, hãy định nghĩa kỹ năng cụ thể mà mỗi tác nhân cung cấp, cùng với thẻ tác nhân công khai mà các tác nhân khác có thể khám phá, như được hiển thị trong file agents/itinerary_agent.py:

from a2a.types import ( AgentCapabilities, AgentCard, AgentSkill)
import os # Cần để lấy biến môi trường cho cổng

# ... (các định nghĩa lớp và hàm ở trên)

# Lấy cổng từ biến môi trường, mặc định là 9001
port = int(os.getenv("ITINERARY_PORT", 9001))

# Định nghĩa kỹ năng cụ thể mà tác nhân này cung cấp
skill = AgentSkill(
    id='itinerary_agent',
    name='Itinerary Planning Agent',
    description='Tạo lịch trình du lịch chi tiết từng ngày bằng LangGraph',
    tags=['travel', 'itinerary', 'langgraph'],
    examples=[
        'Tạo lịch trình 3 ngày cho Tokyo',
        'Lên kế hoạch chuyến đi một tuần đến Paris',
        'Tôi nên làm gì ở New York trong 5 ngày?'
    ],
)

# Định nghĩa thẻ tác nhân công khai mà các tác nhân khác có thể khám phá
public_agent_card = AgentCard(
    name='Itinerary Agent',
    description='Tác nhân được cung cấp bởi LangGraph tạo lịch trình du lịch chi tiết từng ngày ở định dạng văn bản thuần túy với các hoạt động và đề xuất bữa ăn.',
    url=f'http://localhost:{port}/', # Sử dụng biến port đã định nghĩa
    version='1.0.0',
    defaultInputModes=['text'],      # Chấp nhận đầu vào văn bản
    defaultOutputModes=['text'],     # Trả về đầu ra văn bản
    capabilities=AgentCapabilities(streaming=True),  # Hỗ trợ phản hồi streaming
    skills=[skill],                  # Danh sách các kỹ năng mà tác nhân này cung cấp
    supportsAuthenticatedExtendedCard=False,  # Không yêu cầu xác thực
)

Bước 3: Cấu Hình Trình Thực Thi Tác Nhân A2A

Sau khi thiết lập các kỹ năng của mỗi tác nhân cùng với thẻ tác nhân, hãy cấu hình mỗi tác nhân với một trình thực thi tác nhân A2A xử lý các yêu cầu và phản hồi A2A, như được hiển thị trong file agents/itinerary_agent.py:

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.types import new_agent_text_message # Cần để tạo tin nhắn phản hồi A2A
from a2a.server.event_queue import EventQueue # Cần cho chú thích kiểu dữ liệu

# ... (các định nghĩa lớp và biến khác)

# === TRÌNH THỰC THI GIAO THỨC A2A ===
class ItineraryAgentExecutor(AgentExecutor):
    """
    Một lớp trình thực thi kết nối Giao thức A2A với ItineraryAgent của chúng ta.

    Lớp này xử lý vòng đời của Giao thức A2A:
    - Nhận yêu cầu thực thi từ các tác nhân khác
    - Ủy quyền cho ItineraryAgent của chúng ta để xử lý
    - Gửi kết quả trở lại thông qua hàng đợi sự kiện
    """

    def __init__(self):
        """Khởi tạo trình thực thi với một phiên bản tác nhân của chúng ta"""
        self.agent = ItineraryAgent()

    async def execute(
        self,
        context: RequestContext,
        event_queue: EventQueue,
    ) -> None:
        """
        Thực thi yêu cầu tạo lịch trình.

        Phương thức này:
        1. Gọi tác nhân của chúng ta với tin nhắn đến
        2. Định dạng kết quả dưới dạng tin nhắn văn bản A2A
        3. Gửi phản hồi thông qua hàng đợi sự kiện

        Args:
            context: Ngữ cảnh yêu cầu chứa tin nhắn và siêu dữ liệu
            event_queue: Hàng đợi để gửi các sự kiện phản hồi trở lại người gọi
        """
        # Tạo lịch trình bằng tác nhân của chúng ta
        result = await self.agent.invoke(context.message)

        # Gửi kết quả trở lại thông qua hàng đợi sự kiện Giao thức A2A
        await event_queue.enqueue_event(new_agent_text_message(result))

    async def cancel(
        self, context: RequestContext, event_queue: EventQueue
    ) -> None:
        """
        Xử lý các yêu cầu hủy (chưa được triển khai).

        Đối với tác nhân này, chúng tôi không hỗ trợ hủy bỏ vì việc tạo lịch trình
        thường nhanh chóng và không thể bị gián đoạn.
        """
        raise Exception('Hủy không được hỗ trợ')

Bước 4: Thiết Lập Máy Chủ Tác Nhân A2A

Khi bạn đã cấu hình trình thực thi tác nhân A2A cho mỗi tác nhân từ xa, hãy thiết lập máy chủ tác nhân A2A của mỗi tác nhân để cấu hình trình xử lý yêu cầu Giao thức A2A, tạo ứng dụng web Starlette và khởi động máy chủ uvicorn, như được hiển thị trong file agents/itinerary_agent.py:

# Import A2A Protocol components for inter-agent communication
import os # Để lấy biến môi trường
import uvicorn # Để chạy máy chủ
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore

# ... (các định nghĩa khác, ví dụ: port, public_agent_card, ItineraryAgentExecutor)

# Lấy cổng từ biến môi trường, mặc định là 9001
port = int(os.getenv("ITINERARY_PORT", 9001))

# === THIẾT LẬP ỨNG DỤNG CHÍNH ===
def main():
    """
    Hàm chính thiết lập và khởi động máy chủ Giao thức A2A.

    Hàm này:
    1. Kiểm tra các biến môi trường bắt buộc
    2. Thiết lập trình xử lý yêu cầu Giao thức A2A
    3. Tạo ứng dụng web Starlette
    4. Khởi động máy chủ uvicorn
    """

    # Kiểm tra khóa OpenAI API bắt buộc
    if not os.getenv("OPENAI_API_KEY"):
        print("⚠️  Cảnh báo: Biến môi trường OPENAI_API_KEY chưa được đặt!")
        print("   Đặt nó bằng: export OPENAI_API_KEY='khóa-của-bạn-ở-đây'")
        print()

    # Tạo trình xử lý yêu cầu Giao thức A2A
    # Điều này xử lý các yêu cầu đến và quản lý vòng đời tác vụ
    request_handler = DefaultRequestHandler(
        agent_executor=ItineraryAgentExecutor(),  # Trình thực thi tùy chỉnh của chúng ta
        task_store=InMemoryTaskStore(),           # Lưu trữ tác vụ đơn giản trong bộ nhớ
    )

    # Tạo ứng dụng web A2A Starlette
    # Điều này cung cấp các endpoint HTTP cho giao tiếp Giao thức A2A
    server = A2AStarletteApplication(
        agent_card=public_agent_card,           # Thông tin tác nhân công khai
        http_handler=request_handler,           # Logic xử lý yêu cầu
        extended_agent_card=public_agent_card,  # Thông tin tác nhân mở rộng (giống như công khai)
    )

    # Khởi động máy chủ
    print(f"🗺️  Khởi động Tác nhân Lịch trình (LangGraph + A2A) trên http://localhost:{port}")
    uvicorn.run(server.build(), host='0.0.0.0', port=port)

# === ĐIỂM KHỞI ĐẦU ===
if __name__ == '__main__':
    """
    Điểm khởi đầu khi script được chạy trực tiếp.

    Điều này cho phép tác nhân được khởi động như một dịch vụ độc lập:
    python itinerary_agent.py
    """
    main()

Chúc mừng! Bạn đã tích hợp thành công các tác nhân từ xa của mình với giao thức A2A, và giờ đây tác nhân điều phối có thể ủy quyền các tác vụ cho các tác nhân này.

Xây Dựng Giao Diện Frontend Mạnh Mẽ với CopilotKit cho Giao Tiếp Đa Tác Nhân

Trong phần này, bạn sẽ tìm hiểu cách thêm giao diện frontend cho giao tiếp đa tác nhân AG-UI và A2A bằng cách sử dụng CopilotKit, một framework hoạt động ở bất cứ đâu React chạy. Một giao diện người dùng trực quan và tương tác là chìa khóa để khai thác tối đa hệ thống tác nhân AI của bạn.

Hãy bắt đầu.

Bước 1: Thiết Lập Frontend

Để bắt đầu, hãy cài đặt các phụ thuộc frontend trong kho lưu trữ A2A-Travel repository mà bạn đã clone trước đó:

npm install

Sau đó, cấu hình các biến môi trường bằng cách sao chép file .env.example và chỉnh sửa file .env để thêm GOOGLE_API_KEYOPENAI_API_KEY của bạn:

cp .env.example .env
# Chỉnh sửa file .env và thêm GOOGLE_API_KEY và OPENAI_API_KEY của bạn:
# GOOGLE_API_KEY=your_google_api_key
# OPENAI_API_KEY=your_openai_api_key

Cuối cùng, khởi động tất cả các máy chủ backend và frontend:

npm run dev

Lệnh này sẽ khởi động UI trên http://localhost:3000, Tác nhân Điều phối trên http://localhost:9000, Tác nhân Lịch trình trên http://localhost:9001, Tác nhân Ngân sách trên http://localhost:9002, Tác nhân Nhà hàng trên http://localhost:9003 và Tác nhân Thời tiết trên http://localhost:9005.

Nếu bạn điều hướng đến http://localhost:3000/, bạn sẽ thấy giao diện frontend đa tác nhân A2A lập kế hoạch du lịch đã hoạt động.

Bước 2: Cấu Hình API Route của CopilotKit với A2A Middleware

Khi bạn đã thiết lập frontend, hãy cấu hình API Route của CopilotKit với A2A Middleware để thiết lập kết nối giữa frontend, tác nhân Điều phối AG-UI + ADK và các tác nhân A2A, như được hiển thị trong file app/api/copilotkit/route.ts:

import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
import { A2AMiddlewareAgent } from "@ag-ui/a2a-middleware";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  // BƯỚC 1: Định nghĩa URL của các tác nhân A2A
  const itineraryAgentUrl =
    process.env.ITINERARY_AGENT_URL || "http://localhost:9001";
  const budgetAgentUrl =
    process.env.BUDGET_AGENT_URL || "http://localhost:9002";
  const restaurantAgentUrl =
    process.env.RESTAURANT_AGENT_URL || "http://localhost:9003";
  const weatherAgentUrl =
    process.env.WEATHER_AGENT_URL || "http://localhost:9005";

  // BƯỚC 2: Định nghĩa URL của tác nhân điều phối (sử dụng Giao thức AG-UI)
  const orchestratorUrl =
    process.env.ORCHESTRATOR_URL || "http://localhost:9000";

  // BƯỚC 3: Bọc tác nhân điều phối với HttpAgent (client AG-UI)
  // Tác nhân điều phối mà chúng ta truyền cho middleware cần là một thể hiện của
  // một dẫn xuất của `AbstractAgent` của ag-ui
  // Trong trường hợp này, chúng ta có quyền truy cập tác nhân qua url, vì vậy chúng ta có thể
  // lấy một thể hiện bằng cách sử dụng lớp `HttpAgent`
  const orchestrationAgent = new HttpAgent({
    url: orchestratorUrl,
  });

  // BƯỚC 4: Tạo Tác Nhân Middleware A2A
  // Điều này kết nối các giao thức AG-UI và A2A bằng cách:
  // 1. Bọc tác nhân điều phối
  // 2. Đăng ký tất cả các tác nhân A2A
  // 3. Chèn công cụ send_message_to_a2a_agent
  // 4. Định tuyến tin nhắn giữa tác nhân điều phối và các tác nhân A2A
  const a2aMiddlewareAgent = new A2AMiddlewareAgent({
    description:
      "Trợ lý lập kế hoạch du lịch với 4 tác nhân chuyên biệt: Lịch trình và Nhà hàng (LangGraph), Thời tiết và Ngân sách (ADK)",

    agentUrls: [
      itineraryAgentUrl, // LangGraph + OpenAI
      restaurantAgentUrl, // ADK + Gemini
      budgetAgentUrl, // ADK + Gemini
      weatherAgentUrl, // ADK + Gemini
    ],

    orchestrationAgent,
  });

  // BƯỚC 5: Tạo CopilotKit Runtime
  const runtime = new CopilotRuntime({
    agents: {
      a2a_chat: a2aMiddlewareAgent, // Phải khớp với frontend: <CopilotKit agent="a2a_chat">
    },
  });

  // BƯỚC 6: Thiết lập trình xử lý endpoint Next.js
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter: new ExperimentalEmptyAdapter(),
    endpoint: "/api/copilotkit",
  });

  return handleRequest(request);
}

Bước 3: Thiết Lập CopilotKit Provider

Sau khi cấu hình CopilotKit API Route với A2A Middleware, hãy thiết lập thành phần CopilotKit Provider quản lý các phiên đa tác nhân A2A của bạn. Để thiết lập CopilotKit Provider, thành phần <CopilotKit> (tham khảo tài liệu CopilotKit) phải bọc các phần của ứng dụng nhận biết Copilot, như được hiển thị trong file components/travel-chat.tsx:

import { CopilotKit } from "@copilotkit/react-core";
// Import các kiểu dữ liệu và thành phần cần thiết
import React from "react"; // Giả định React được sử dụng
// Định nghĩa TravelChatProps và ChatInner nếu chưa có
interface TravelChatProps {
    onItineraryUpdate: (data: any) => void;
    onBudgetUpdate: (data: any) => void;
    onWeatherUpdate: (data: any) => void;
    onRestaurantUpdate: (data: any) => void;
}

// Giả định ChatInner là một thành phần riêng biệt được định nghĩa dưới đây
const ChatInner = (props: TravelChatProps) => {
    // ... nội dung của ChatInner sẽ được định nghĩa trong các bước sau
    return <div>Nội dung trò chuyện sẽ hiển thị ở đây</div>;
};

/**
 * THÀNH PHẦN CHÍNH: CopilotKit Provider Wrapper
 *
 * Đây là xuất khẩu chính bọc thành phần trò chuyện với CopilotKit provider.
 * Cấu hình provider cho phép:
 * - Kết nối Runtime với các tác nhân backend qua endpoint /api/copilotkit
 * - Giao thức giao tiếp tác nhân A2A
 * - Bảng điều khiển nhà phát triển để gỡ lỗi (bị tắt trong sản xuất)
 */
export default function TravelChat({
  onItineraryUpdate,
  onBudgetUpdate,
  onWeatherUpdate,
  onRestaurantUpdate,
}: TravelChatProps) {
  return (
    <CopilotKit
      runtimeUrl="/api/copilotkit" // Endpoint backend cho giao tiếp tác nhân
      showDevConsole={false} // Tắt bảng điều khiển dev trong sản xuất
      agent="a2a_chat" // Chỉ định giao thức tác nhân A2A
    >
      <ChatInner
        onItineraryUpdate={onItineraryUpdate}
        onBudgetUpdate={onBudgetUpdate}
        onWeatherUpdate={onWeatherUpdate}
        onRestaurantUpdate={onRestaurantUpdate}
      />
    </CopilotKit>
  );
}

Bước 4: Cấu Hình Thành Phần Giao Diện Copilot UI

Khi bạn đã thiết lập CopilotKit Provider, hãy thiết lập một thành phần Copilot UI cho phép bạn tương tác với các tác nhân AG-UI + A2A. CopilotKit đi kèm với một số thành phần trò chuyện tích hợp, bao gồm CopilotPopup, CopilotSidebarCopilotChat. Để thiết lập một thành phần Copilot UI, hãy định nghĩa nó cùng với các thành phần trang cốt lõi của bạn, như được hiển thị trong file components/travel-chat.tsx:

import { CopilotChat } from "@copilotkit/react-ui";
// ... (Các import và định nghĩa khác như TravelChatProps)

const ChatInner = ({
  onItineraryUpdate,
  onBudgetUpdate,
  onWeatherUpdate,
  onRestaurantUpdate,
}: TravelChatProps) => {

  // ... (các hooks hoặc trạng thái khác nếu có)

  /**
   * THÀNH PHẦN TRÒ CHUYỆN COPILOTKIT: Giao diện trò chuyện chính
   *
   * Thành phần CopilotChat cung cấp giao diện trò chuyện cốt lõi với:
   * - Lịch sử tin nhắn và cuộc trò chuyện thời gian thực
   * - Tích hợp với tất cả các hành động đã đăng ký
   * - Nhãn và hướng dẫn có thể tùy chỉnh
   * - Hỗ trợ tích hợp cho Generative UI và quy trình làm việc HITL
   */
  return (
    <div className="h-full">
      <CopilotChat
        className="h-full"
        labels={{
          initial:
            "👋 Xin chào! Tôi là trợ lý lập kế hoạch du lịch của bạn.\n\nHãy yêu cầu tôi lập kế hoạch chuyến đi và tôi sẽ phối hợp với các tác nhân chuyên biệt để tạo ra lịch trình hoàn hảo cho bạn!",
        }}
        instructions="Bạn là một trợ lý lập kế hoạch du lịch hữu ích. Giúp người dùng lên kế hoạch chuyến đi của họ bằng cách phối hợp với các tác nhân chuyên biệt."
      />
    </div>
  );
};

Bước 5: Hiển Thị Giao Tiếp Giữa Các Tác Nhân (A2A) Bằng Generative UI

Sau khi thiết lập một thành phần Copilot UI, hãy hiển thị giao tiếp giữa các tác nhân (Agent-to-Agent) bằng Generative UI trong thành phần trò chuyện. Để hiển thị giao tiếp giữa các tác nhân trong thành phần trò chuyện theo thời gian thực, hãy định nghĩa một hook useCopilotAction() có tên send_message_to_a2a_agent, như được hiển thị trong file components/travel-chat.tsx:

import { useCopilotAction } from "@copilotkit/react-core";

// Các thành phần trực quan hóa giao tiếp A2A
import { MessageToA2A } from "./a2a/MessageToA2A";
import { MessageFromA2A } from "./a2a/MessageFromA2A";
// ... (Các import và định nghĩa khác như TravelChatProps, ChatInner)

// Giả định MessageActionRenderProps được định nghĩa ở đâu đó, nếu không, sử dụng `any`
interface MessageActionRenderProps {
    args: { agentName: string; task: string };
    result: any; // Kết quả từ tác nhân A2A
    status: 'running' | 'success' | 'error';
    // ... các thuộc tính khác tùy theo CopilotKit
}

const ChatInner = ({
  onItineraryUpdate,
  onBudgetUpdate,
  onWeatherUpdate,
  onRestaurantUpdate,
}: TravelChatProps) => {

  // ... (các hooks và trạng thái khác nếu có)

  useCopilotAction({
    name: "send_message_to_a2a_agent",
    description: "Gửi tin nhắn đến một tác nhân A2A",
    available: "frontend", // Hành động này chỉ chạy trên frontend - không xử lý backend
    parameters: [
      {
        name: "agentName",
        type: "string",
        description: "Tên của tác nhân A2A để gửi tin nhắn đến",
      },
      {
        name: "task",
        type: "string",
        description: "Tin nhắn để gửi đến tác nhân A2A",
      },
    ],
    // Hàm render tùy chỉnh tạo các thành phần giao tiếp A2A trực quan
    render: (actionRenderProps: MessageActionRenderProps) => {
      return (
        <>
          {/* MessageToA2A: Hiển thị tin nhắn đi (hộp màu xanh lá) */}
          <MessageToA2A {...actionRenderProps} />
          {/* MessageFromA2A: Hiển thị phản hồi của tác nhân (hộp màu xanh dương) */}
          <MessageFromA2A {...actionRenderProps} />
        </>
      );
    },
  });

  // ...

  return (
    <div className="h-full">
      {/* ... (Thành phần CopilotChat ở đây) */}
    </div>
  );
};

Khi tác nhân điều phối gửi yêu cầu hoặc nhận phản hồi từ các tác nhân A2A, bạn sẽ thấy giao tiếp được hiển thị trên thành phần trò chuyện.

Bước 6: Triển Khai Human-in-the-Loop (HITL) ở Frontend

Human-in-the-loop (HITL) cho phép các tác nhân yêu cầu đầu vào hoặc phê duyệt từ con người trong quá trình thực thi, làm cho các hệ thống AI đáng tin cậy và đáng tin cậy hơn. Mẫu này rất cần thiết khi xây dựng các ứng dụng AI cần xử lý các quyết định hoặc hành động phức tạp yêu cầu sự phán đoán của con người. Bạn có thể tìm hiểu thêm về Human in the Loop tại đây trên tài liệu CopilotKit.

Để triển khai Human-in-the-Loop (HITL) ở frontend, bạn cần sử dụng hook useCopilotKitAction của CopilotKit với phương thức renderAndWaitForResponse, cho phép trả về giá trị không đồng bộ từ hàm render, như được hiển thị trong file components/travel-chat.tsx:

import { useCopilotAction } from "@copilotkit/react-core";
import React, { useState } from "react";
// Giả định BudgetData và BudgetApprovalCard là các thành phần được định nghĩa ở nơi khác
import { BudgetApprovalCard, type BudgetData } from "./BudgetApprovalCard";

const ChatInner = ({
  onItineraryUpdate,
  onBudgetUpdate,
  onWeatherUpdate,
  onRestaurantUpdate,
}: TravelChatProps) => {
  // Quản lý trạng thái cho quy trình phê duyệt ngân sách HITL
  // Theo dõi trạng thái phê duyệt/từ chối cho các đề xuất ngân sách khác nhau
  const [approvalStates, setApprovalStates] = useState<
    Record<string, { approved: boolean; rejected: boolean }>
  >({});

  /**
   * TÍNH NĂNG HITL: Quy trình phê duyệt ngân sách với renderAndWaitForResponse
   *
   * useCopilotAction này minh họa khả năng Human-in-the-Loop (HITL) của CopilotKit,
   * tạm dừng thực thi tác nhân và chờ tương tác người dùng
   * trước khi tiếp tục quy trình làm việc.
   *
   * Các tính năng chính:
   * - renderAndWaitForResponse: Chặn tác nhân cho đến khi người dùng cung cấp đầu vào
   * - Quản lý trạng thái: Theo dõi trạng thái phê duyệt/từ chối qua các lần render lại
   * - Tích hợp logic nghiệp vụ: Chỉ tiến hành với các ngân sách đã được phê duyệt
   * - Giao diện người dùng tùy chỉnh: Hiển thị thẻ phê duyệt tương tác với các nút phê duyệt/từ chối
   * - Xử lý phản hồi: Gửi quyết định của người dùng trở lại tác nhân
   */
  useCopilotAction(
    {
      name: "request_budget_approval",
      description: "Yêu cầu người dùng phê duyệt ngân sách du lịch",
      parameters: [
        {
          name: "budgetData",
          type: "object",
          description: "Dữ liệu phân tích ngân sách cần phê duyệt",
        },
      ],
      // renderAndWaitForResponse tạm dừng thực thi tác nhân cho đến khi người dùng phản hồi
      renderAndWaitForResponse: ({ args, respond }) => {
        // Bước 1: Xác thực cấu trúc dữ liệu ngân sách
        if (!args.budgetData || typeof args.budgetData !== "object") {
          return (
            <div className="text-xs text-gray-500 p-2">
              Đang tải dữ liệu ngân sách...
            </div>
          );
        }

        const budget = args.budgetData as BudgetData;

        if (!budget.totalBudget || !budget.breakdown) {
          return (
            <div className="text-xs text-gray-500 p-2">
              Đang tải dữ liệu ngân sách...
            </div>
          );
        }

        // Bước 2: Tạo một khóa duy nhất cho ngân sách này để theo dõi trạng thái phê duyệt
        const budgetKey = `budget-${budget.totalBudget}`;
        const currentState = approvalStates[budgetKey] || {
          approved: false,
          rejected: false,
        };

        // Bước 3: Định nghĩa trình xử lý phê duyệt - cập nhật trạng thái và phản hồi tác nhân
        const handleApprove = () => {
          setApprovalStates((prev) => ({
            ...prev,
            [budgetKey]: { approved: true, rejected: false },
          }));
          // Gửi phản hồi phê duyệt trở lại tác nhân để tiếp tục quy trình làm việc
          respond?.({ approved: true, message: "Ngân sách đã được người dùng phê duyệt" });
        };

        // Bước 4: Định nghĩa trình xử lý từ chối - cập nhật trạng thái và phản hồi tác nhân
        const handleReject = () => {
          setApprovalStates((prev) => ({
            ...prev,
            [budgetKey]: { approved: false, rejected: true },
          }));
          // Gửi phản hồi từ chối trở lại tác nhân để xử lý phù hợp
          respond?.({ approved: false, message: "Ngân sách đã bị người dùng từ chối" });
        };

        // Bước 5: Hiển thị thẻ phê duyệt ngân sách tương tác
        return (
          <BudgetApprovalCard
            budgetData={budget}
            isApproved={currentState.approved}
            isRejected={currentState.rejected}
            onApprove={handleApprove}
            onReject={handleReject}
          />
        );
      },
    },
    [approvalStates] // Đăng ký lại khi trạng thái phê duyệt thay đổi
  );

  // ... (Các định nghĩa khác của ChatInner)

  return (
    <div className="h-full">
      {/* ... */}
    </div>
  );
};

Khi một tác nhân kích hoạt các hành động frontend bằng tên công cụ/hành động để yêu cầu đầu vào hoặc phản hồi từ con người trong quá trình thực thi, người dùng cuối sẽ được nhắc nhấp vào một lựa chọn (được hiển thị trong giao diện trò chuyện). Sau đó, người dùng có thể chọn bằng cách nhấn một nút trong giao diện trò chuyện.

Bước 7: Streaming Phản Hồi Từ Tác Nhân Đến Tác Nhân (A2A) ở Frontend

Để streaming phản hồi từ tác nhân đến tác nhân ở frontend, hãy định nghĩa một hook useEffect() phân tích cú pháp các phản hồi của tác nhân AI để trích xuất dữ liệu có cấu trúc (như lịch trình, ngân sách, thời tiết hoặc đề xuất nhà hàng), như được hiển thị trong file components/travel-chat.tsx:

import { useEffect, useState } from "react";
import { useCopilotChat } from "@copilotkit/react-ui";
// Giả định ItineraryData, BudgetData, WeatherData, RestaurantData types đã được định nghĩa
import { type ItineraryData, type RestaurantData } from "@/components/ItineraryCard";
import { type BudgetData } from "@/components/BudgetBreakdown";
import { type WeatherData } from "@/components/WeatherCard";
// ... (Các import khác như MessageToA2A, MessageFromA2A, BudgetApprovalCard được giả định là đã có)


const ChatInner = ({
  onItineraryUpdate,
  onBudgetUpdate,
  onWeatherUpdate,
  onRestaurantUpdate,
}: TravelChatProps) => {
  // ... (approvalStates và useCopilotAction cho HITL như trên)

  // Hook của CopilotKit để truy cập các tin nhắn trò chuyện để trích xuất dữ liệu
  // visibleMessages chứa tất cả các tin nhắn hiện đang hiển thị trong cuộc trò chuyện
  const { visibleMessages } = useCopilotChat();

  /**
   * TÍNH NĂNG GENERATIVE UI: Tự động trích xuất dữ liệu có cấu trúc từ phản hồi của tác nhân
   *
   * useEffect này minh họa khả năng của CopilotKit trong việc tự động phân tích và
   * trích xuất dữ liệu có cấu trúc từ phản hồi của tác nhân AI, chuyển đổi chúng thành
   * các thành phần UI tương tác.
   *
   * Quy trình:
   * 1. Giám sát tất cả các tin nhắn hiển thị trong cuộc trò chuyện để tìm phản hồi của tác nhân
   * 2. Phân tích dữ liệu JSON từ kết quả tin nhắn tác nhân A2A
   * 3. Xác định loại dữ liệu (lịch trình, ngân sách, thời tiết, đề xuất nhà hàng)
   * 4. Cập nhật trạng thái thành phần cha để hiển thị các thành phần UI tương ứng
   * 5. Áp dụng logic nghiệp vụ (ví dụ: kiểm tra phê duyệt ngân sách)
   */
  useEffect(() => {
    const extractDataFromMessages = () => {
      // Bước 1: Lặp qua tất cả các tin nhắn hiển thị trong cuộc trò chuyện
      for (const message of visibleMessages) {
        const msg = message as any;

        // Bước 2: Lọc các tin nhắn phản hồi của tác nhân A2A cụ thể
        if (
          msg.type === "ResultMessage" &&
          msg.actionName === "send_message_to_a2a_agent"
        ) {
          try {
            const result = msg.result;
            let parsed;

            // Bước 3: Phân tích dữ liệu phản hồi của tác nhân (xử lý cả định dạng chuỗi và đối tượng)
            if (typeof result === "string") {
              let cleanResult = result;
              // Xóa tiền tố giao thức A2A nếu có
              if (result.startsWith("A2A Agent Response: ")) {
                cleanResult = result.substring("A2A Agent Response: ".length);
              }
              parsed = JSON.parse(cleanResult);
            } else if (typeof result === "object" && result !== null) {
              parsed = result;
            }

            // Bước 4: Xác định loại dữ liệu và kích hoạt cập nhật UI phù hợp
            if (parsed) {
              // Dữ liệu lịch trình: destination + itinerary array
              if (
                parsed.destination &&
                parsed.itinerary &&
                Array.isArray(parsed.itinerary)
              ) {
                onItineraryUpdate?.(parsed as ItineraryData);
              }
              // Dữ liệu ngân sách: yêu cầu người dùng phê duyệt trước khi hiển thị
              else if (
                parsed.totalBudget &&
                parsed.breakdown &&
                Array.isArray(parsed.breakdown)
              ) {
                const budgetKey = `budget-${parsed.totalBudget}`;
                const isApproved = approvalStates[budgetKey]?.approved || false;
                // Bước 5: Áp dụng kiểm tra phê duyệt HITL - chỉ hiển thị nếu người dùng đã phê duyệt
                if (isApproved) {
                  onBudgetUpdate?.(parsed as BudgetData);
                }
              }
              // Dữ liệu thời tiết: destination + forecast array
              else if (
                parsed.destination &&
                parsed.forecast &&
                Array.isArray(parsed.forecast)
              ) {
                const weatherDataParsed = parsed as WeatherData;
                onWeatherUpdate?.(weatherDataParsed);
              }
              // Dữ liệu nhà hàng: destination + meals array
              else if (
                parsed.destination &&
                parsed.meals &&
                Array.isArray(parsed.meals)
              ) {
                onRestaurantUpdate?.(parsed as RestaurantData);
              }
            }
          } catch (e) {
            // Xử lý lỗi phân tích cú pháp một cách ngầm - không phải tất cả các tin nhắn đều chứa dữ liệu có cấu trúc
            console.error("Lỗi khi phân tích tin nhắn tác nhân A2A:", e);
          }
        }
      }
    };

    extractDataFromMessages();
  }, [
    visibleMessages,
    approvalStates,
    onItineraryUpdate,
    onBudgetUpdate,
    onWeatherUpdate,
    onRestaurantUpdate,
  ]);

  // ... (Các định nghĩa khác của ChatInner)

  return (
    <div className="h-full">
      {/* ... */}
    </div>
  );
};

Sau đó, dữ liệu có cấu trúc được trích xuất sẽ kích hoạt cập nhật UI để hiển thị các thành phần tương tác, như được hiển thị trong file app/page.tsx:

"use client";

import { useState } from "react";
import TravelChat from "@/components/travel-chat";
import { ItineraryCard, type ItineraryData } from "@/components/ItineraryCard";
import { BudgetBreakdown, type BudgetData } from "@/components/BudgetBreakdown";
import { WeatherCard, type WeatherData } from "@/components/WeatherCard";
import { type RestaurantData } from "@/components/ItineraryCard";

export default function Home() {
  const [itineraryData, setItineraryData] = useState<ItineraryData | null>(null);
  const [budgetData, setBudgetData] = useState<BudgetData | null>(null);
  const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
  const [restaurantData, setRestaurantData] = useState<RestaurantData | null>(null);

  return (
    <div className="relative flex h-screen overflow-hidden bg-[#DEDEE9] p-2">

      {/* ... (Các phần tử bố cục khác của trang) ... */}

      <div className="flex flex-1 overflow-hidden z-10 gap-2">

        {/* ... (Các phần tử bố cục khác) ... */}

        <div className="flex-1 overflow-hidden">
          <TravelChat
            onItineraryUpdate={setItineraryData}
            onBudgetUpdate={setBudgetData}
            onWeatherUpdate={setWeatherData}
            onRestaurantUpdate={setRestaurantData}
          />
        </div>
      </div>

      <div className="flex-1 overflow-y-auto rounded-lg bg-white/30 backdrop-blur-sm">
        <div className="max-w-5xl mx-auto p-8">

          {/* ... (Các phần tử bố cục khác) ... */}

          {itineraryData && (
            <div className="mb-4">
              <ItineraryCard data={itineraryData} restaurantData={restaurantData} />
            </div>
          )}

          {(weatherData || budgetData) && (
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
              {weatherData && (
                <div>
                  <WeatherCard data={weatherData} />
                </div>
              )}

              {budgetData && (
                <div>
                  <BudgetBreakdown data={budgetData} />
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Nếu bạn truy vấn tác nhân của mình và phê duyệt yêu cầu phản hồi của nó, bạn sẽ thấy phản hồi hoặc kết quả của tác nhân được streaming trong UI.

Kết Luận

Trong hướng dẫn này, chúng ta đã đi qua các bước xây dựng giao tiếp Agent-to-Agent đầy đủ ngăn xếp (full-stack) bằng cách sử dụng các giao thức A2A + AG-UI và khung CopilotKit. Chúng ta đã khám phá cách các tác nhân AI có thể tương tác với nhau và với ứng dụng của bạn, tạo ra các giải pháp thông minh và tích hợp.

Mặc dù chúng ta đã khám phá một vài tính năng, nhưng chúng ta mới chỉ chạm đến bề mặt của vô số trường hợp sử dụng cho CopilotKit, từ việc xây dựng chatbot AI tương tác đến xây dựng các giải pháp tác nhân—về bản chất, CopilotKit cho phép bạn thêm vô số khả năng AI hữu ích vào sản phẩm của mình chỉ trong vài phút.

Hy vọng, hướng dẫn này giúp bạn dễ dàng hơn trong việc tích hợp các Copilot được hỗ trợ bởi AI vào ứng dụng hiện có của mình. Hãy theo dõi CopilotKit trên Twitter và nói lời chào, và nếu bạn muốn xây dựng điều gì đó thú vị, hãy tham gia cộng đồng Discord.

Lời cảm ơn đặc biệt: Tôi muốn gửi lời cảm ơn đặc biệt đến Mark Morgan, người đã xây dựng bản demo du lịch A2A cùng với tất cả chức năng đa tác nhân backend và giao diện frontend được thiết kế đẹp mắt. Thực sự là một công việc tuyệt vời! Hãy theo dõi anh ấy trên GitHub!

Một dòng tóm tắt: Giao thức A2A, AG-UI và CopilotKit là bộ công cụ mạnh mẽ giúp đơn giản hóa việc xây dựng và triển khai các hệ thống đa tác nhân AI thông minh, tương tác cao với giao diện người dùng frontend.

Chỉ mục