Xây Dựng Agent AI Quản Lý Danh Mục Chứng Khoán Toàn Diện Với Mastra, AG-UI và CopilotKit

Mở Khóa Tiềm Năng AI: Giới Thiệu Agent Quản Lý Danh Mục Chứng Khoán Thông Minh

Trong kỷ nguyên số, việc quản lý danh mục đầu tư chứng khoán đòi hỏi sự nhanh nhạy và khả năng phân tích dữ liệu lớn. Các nhà đầu tư và nhà phát triển đang tìm kiếm những giải pháp thông minh hơn để tự động hóa, tối ưu hóa và tương tác hiệu quả với thông tin thị trường. Bài viết này sẽ hướng dẫn bạn từng bước xây dựng một agent AI quản lý danh mục chứng khoán fullstack, có khả năng phân tích, tương tác theo thời gian thực và hiển thị dữ liệu trực quan trên giao diện người dùng.

Chúng ta sẽ khám phá cách tích hợp một agent AI mạnh mẽ được xây dựng bằng framework Mastra với giao thức AG-UI để tạo ra trải nghiệm tương tác liền mạch. Hơn nữa, chúng ta sẽ kết nối agent này với frontend sử dụng CopilotKit, cho phép người dùng trò chuyện và nhận phản hồi được truyền tải trực tiếp ra giao diện người dùng.

Những gì bạn sẽ học được trong hướng dẫn này:

  • Tìm hiểu về AG-UI Protocol và vai trò của nó.
  • Tích hợp các agent AI Mastra với AG-UI Protocol.
  • Tích hợp frontend với agent AI AG-UI + Mastra bằng CopilotKit để trò chuyện và truyền tải phản hồi.

AG-UI Protocol là gì? Cầu Nối Tương Tác Giữa Frontend và Agent AI

Giao thức Tương tác Người dùng Agent (AG-UI), được phát triển bởi CopilotKit, là một giao thức mã nguồn mở, nhẹ, dựa trên sự kiện, được thiết kế để tạo điều kiện cho các tương tác phong phú, thời gian thực giữa frontend và các agent AI. AG-UI đóng vai trò là cầu nối quan trọng, cho phép các ứng dụng web giao tiếp hiệu quả với các tác nhân AI phức tạp.

AG-UI Protocol mang lại những khả năng mạnh mẽ như:

  • Truyền thông dựa trên sự kiện: Kích hoạt các hành động và phản hồi dựa trên các sự kiện cụ thể.
  • Quản lý trạng thái: Đồng bộ hóa trạng thái giữa frontend và agent AI một cách hiệu quả.
  • Sử dụng công cụ (Tool Usage): Cho phép agent AI sử dụng các công cụ bên ngoài hoặc kích hoạt các hành động trên frontend.
  • Truyền tải phản hồi từ agent AI: Đảm bảo trải nghiệm người dùng mượt mà bằng cách truyền tải phản hồi theo thời gian thực.

Để truyền tải thông tin giữa frontend và agent AI, AG-UI sử dụng nhiều loại sự kiện khác nhau:

  • Sự kiện vòng đời (Lifecycle events): Đánh dấu sự bắt đầu hoặc kết thúc của việc thực thi tác vụ của agent. Ví dụ: RUN_STARTEDRUN_FINISHED.
  • Sự kiện tin nhắn văn bản (Text message events): Xử lý việc truyền tải phản hồi từ agent đến frontend. Ví dụ: TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENTTEXT_MESSAGE_END.
  • Sự kiện gọi công cụ (Tool call events): Quản lý việc thực thi các công cụ của agent. Ví dụ: TOOL_CALL_START, TOOL_CALL_ARGSTOOL_CALL_END.
  • Sự kiện quản lý trạng thái (State management events): Giữ cho trạng thái của frontend và agent AI luôn đồng bộ. Ví dụ: STATE_SNAPSHOTSTATE_DELTA.

Bạn có thể tìm hiểu thêm về AG-UI Protocol và kiến trúc của nó tại tài liệu AG-UI.

Chúng ta sẽ thấy cách các sự kiện này được sử dụng trong quá trình tích hợp Mastra và CopilotKit.

Chuẩn Bị Để Bắt Đầu: Các Yêu Cầu Cần Có

Để có thể theo dõi hướng dẫn này một cách hiệu quả, bạn cần có kiến thức cơ bản về lập trình web, đặc biệt là với React hoặc Next.js. Ngoài ra, chúng ta sẽ sử dụng các công nghệ và công cụ sau:

  • TypeScript: Một ngôn ngữ lập trình mạnh mẽ, xây dựng trên JavaScript, mang lại công cụ tốt hơn cho mọi quy mô dự án.
  • Mastra: Một framework agent TypeScript mã nguồn mở, được thiết kế để cung cấp các yếu tố cơ bản cần thiết để xây dựng các ứng dụng và tính năng AI.
  • OpenAI API Key: Để cho phép chúng ta thực hiện các tác vụ khác nhau bằng cách sử dụng các mô hình GPT; đối với hướng dẫn này, đảm bảo bạn có quyền truy cập vào mô hình GPT-4.
  • CopilotKit: Một framework copilot mã nguồn mở để xây dựng chatbot AI tùy chỉnh, agent AI trong ứng dụng và các vùng văn bản.

Tích Hợp Agent AI Mastra với AG-UI Protocol: Xây Dựng Backend Mạnh Mẽ

Để bắt đầu, chúng ta sẽ clone repository demo và thiết lập môi trường backend:

git clone https://github.com/TheGreatBonnie/open-ag-ui-demo-mastra
cd open-ag-ui-demo-mastra/agent

Tiếp theo, cài đặt các dependencies sử dụng Pnpm:

pnpm install

Sau đó, tạo một file .env với OpenAI API Key của bạn:

OPENAI_API_KEY=<<your-OpenAI-key-here>>

Chạy agent bằng lệnh sau:

pnpx ts-node src/ag-ui-mastra.ts

Để kiểm tra tích hợp AG-UI + Mastra AI, chạy lệnh curl sau trên https://reqbin.com/curl:

curl -X POST "http://localhost:8000/mastra-agent" \
  -H "Content-Type: application/json" \
  -d '{
    "thread_id": "test_thread_123",
    "run_id": "test_run_456",
    "messages": [
      {
        "id": "msg_1",
        "role": "user",
        "content": "Analyze AAPL stock with a $10000 investment from 2023-01-01"
      }
    ],
    "tools": [],
    "context": [],
    "forwarded_props": {},
    "state": {}
  }'

Bây giờ, chúng ta sẽ đi sâu vào các bước tích hợp:

Bước 1: Định Nghĩa và Cấu Hình Luồng Công Việc Agent AI Mastra

Trước khi tích hợp AG-UI Protocol với các agent AI Mastra, bạn cần định nghĩa và cấu hình luồng công việc của agent AI Mastra. Ví dụ trong file src/mastra/workflows/stock-analysis-workflow.ts:

/**
 * LUỒNG CÔNG VIỆC CHÍNH: Phân tích Chứng khoán
 *
 * Đây là công cụ điều phối luồng công việc chính, kết nối tất cả các bước
 * để cung cấp phân tích chứng khoán hoàn chỉnh từ truy vấn người dùng đến thông tin chi tiết.
 */
const stockAnalysisWorkflow = createWorkflow({
  id: "stock-analysis-workflow",
  // Định nghĩa schema đầu vào của luồng công việc
  inputSchema: z.object({
    messages: z.any(), // Tin nhắn hội thoại
    availableCash: z.number().describe("Số tiền mặt khả dụng của người dùng"),
    toolLogs: z
      .array(
        z.object({
          message: z.string().describe("Tin nhắn hiển thị cho người dùng"),
          status: z.string().describe("Trạng thái của tin nhắn"),
        })
      )
      .describe("Các log công cụ của luồng công việc"),
    emitEvent: z.function().input(z.any()).output(z.any()), // Hàm để phát sự kiện cập nhật trạng thái UI
    investmentPortfolio: z
      .array(
        z.object({
          ticker: z.string(),
          amount: z.number(),
        })
      )
      .describe("Danh mục đầu tư của người dùng"),
  }),
  // Định nghĩa schema đầu ra của luồng công việc
  outputSchema: z.object({
    skip: z.boolean().describe("Có nên bỏ qua bước này hay không"),
    investmentPortfolio: z
      .array(
        z.object({
          ticker: z.string(),
          amount: z.number(),
        })
      )
      .describe("Danh mục đầu tư của người dùng"),
    textMessage: z.string().describe("Tin nhắn văn bản hiển thị cho người dùng"),
    toolLogs: z
      .array(
        z.object({
          message: z.string().describe("Tin nhắn hiển thị cho người dùng"),
          status: z.string().describe("Trạng thái của tin nhắn"),
        })
      )
      .describe("Các log công cụ của luồng công việc"),
    availableCash: z.number().describe("Tiền mặt khả dụng sau đầu tư"),
    // Dữ liệu hiệu suất chuỗi thời gian
    result: z.array(
      z.object({
        date: z.string().describe("Ngày"),
        portfolioValue: z.number().describe("Giá trị danh mục tại thời điểm"),
        benchmarkValue: z.number().describe("Giá trị chuẩn tại thời điểm"),
      })
    ),
    // Hiệu suất cổ phiếu riêng lẻ
    totalReturns: z.array(
      z.object({
        ticker: z.string().describe("Mã cổ phiếu"),
        rets: z.number().describe("Tổng lợi nhuận từ cổ phiếu"),
        retsNum: z
          .number()
          .describe("Tổng lợi nhuận từ cổ phiếu dưới dạng số"),
      })
    ),
    // Phân bổ danh mục đầu tư
    allocations: z.array(
      z.object({
        ticker: z.string().describe("Dữ liệu mã cổ phiếu"),
        percentOfAllocation: z
          .number()
          .describe("Phần trăm phân bổ của mã cổ phiếu này"),
        value: z.number().describe("Giá trị hiện tại của mã cổ phiếu trong danh mục"),
        returnPercent: z
          .number()
          .describe("Phần trăm lợi nhuận từ mã cổ phiếu này"),
      })
    ),
    // Thông tin thị trường được tạo ra
    bullInsights: z.array(
      z.object({
        title: z.string().describe("Tiêu đề thông tin chi tiết"),
        description: z.string().describe("Mô tả thông tin chi tiết"),
        emoji: z.string().describe("Emoji của thông tin chi tiết"),
      })
    ),
    bearInsights: z.array(
      z.object({
        title: z.string().describe("Tiêu đề thông tin chi tiết"),
        description: z.string().describe("Mô tả thông tin chi tiết"),
        emoji: z.string().describe("Emoji của thông tin chi tiết"),
      })
    ),
  }),
})
  // Xâu chuỗi các bước công việc theo thứ tự:
  .then(fetchInformationFromUserQuery) // Bước 1: Trích xuất các tham số đầu tư từ truy vấn người dùng
  .then(gatherStockInformation) // Bước 2: Lấy dữ liệu lịch sử cổ phiếu từ Yahoo Finance
  .then(calculateInvestmentReturns) // Bước 3: Tính toán hiệu suất và lợi nhuận danh mục
  .then(gatherInsights); // Bước 4: Tạo thông tin thị trường bằng LLM

// Thiết lập và khởi tạo luồng công việc
stockAnalysisWorkflow.commit(); // Hoàn thiện định nghĩa luồng công việc
stockAnalysisWorkflow.createRun(); // Tạo một instance chạy luồng công việc mới

// Export luồng công việc để sử dụng trong các module khác
export { stockAnalysisWorkflow };

Bước 2: Đăng Ký Luồng Công Việc Agent AI Mastra Với Instance Mastra

Khi bạn đã định nghĩa và cấu hình luồng công việc của Mastra AI agent, hãy đăng ký luồng công việc đó bằng cách sử dụng thuộc tính workflows trong instance Mastra chính, như được hiển thị trong file src/mastra/index.ts:

// Import các dependencies cần thiết cho cấu hình framework Mastra
import { Mastra } from "@mastra/core/mastra"; // Lớp framework Mastra cốt lõi để điều phối agent và luồng công việc
import { PinoLogger } from "@mastra/loggers"; // Thư viện logging có cấu trúc để gỡ lỗi và giám sát
import { LibSQLStore } from "@mastra/libsql"; // Nhà cung cấp lưu trữ cơ sở dữ liệu cho telemetry, đánh giá và sự kiên trì
import { stockAnalysisAgent } from "./agents/stock-analysis-agent"; // Agent phân tích chứng khoán thông minh
import { stockAnalysisWorkflow } from "./workflows/stock-analysis-workflow"; // Luồng công việc phân tích chứng khoán hoàn chỉnh

/**
 * Cấu hình Framework Mastra
 *
 * File này đóng vai trò là điểm cấu hình và khởi tạo trung tâm cho toàn bộ
 * hệ thống phân tích chứng khoán. Nó tập hợp tất cả các thành phần:
 *
 * 1. Agents - Giao diện hội thoại thông minh hiểu truy vấn người dùng
 * 2. Workflows - Quy trình nghiệp vụ nhiều bước thực hiện phân tích phức tạp
 * 3. Storage - Lớp cơ sở dữ liệu để lưu trữ và dữ liệu telemetry
 * 4. Logging - Logging có cấu trúc để gỡ lỗi và giám sát
 *
 * Instance Mastra hoạt động như một công cụ điều phối chính, điều phối tất cả các
 * thành phần này và cung cấp một giao diện thống nhất cho ứng dụng.
 */
export const mastra = new Mastra({
  // Bước 1: Đăng ký tất cả các luồng công việc có sẵn
  // Luồng công việc là các quy trình nhiều bước có thể được thực thi bởi agent hoặc kích hoạt trực tiếp
  workflows: { stockAnalysisWorkflow }, // Đăng ký luồng công việc phân tích chứng khoán cho các tính toán đầu tư

  // Bước 2: Đăng ký tất cả các agent có sẵn
  // Agent là giao diện thông minh có thể hiểu ngôn ngữ tự nhiên và thực thi luồng công việc
  agents: { stockAnalysisAgent }, // Đăng ký agent phân tích chứng khoán để xử lý các cuộc hội thoại người dùng

  // Bước 3: Cấu hình lưu trữ dữ liệu
  // Lưu trữ xử lý việc lưu giữ dữ liệu telemetry, kết quả đánh giá và trạng thái hệ thống
  storage: new LibSQLStore({
    // Sử dụng bộ nhớ trong để phát triển/kiểm tra (dữ liệu bị mất khi quá trình dừng)
    // Đối với sản xuất: thay đổi thành "file:../mastra.db" để lưu dữ liệu vào đĩa
    url: ":memory:", // Cơ sở dữ liệu trong bộ nhớ - nhanh nhưng không bền vững
  }),

  // Bước 4: Cấu hình logging có cấu trúc
  // Logger ghi lại các sự kiện hệ thống, lỗi và thông tin gỡ lỗi
  logger: new PinoLogger({
    name: "Mastra", // Tên logger để xác định nguồn log
    level: "info", // Mức độ log - ghi lại tin nhắn info, warn và error (lọc bỏ debug/trace)
  }),
});

Bước 3: Tạo Endpoint Với Express

Sau khi đăng ký luồng công việc của Mastra AI agent với instance Mastra, bạn cần tạo một endpoint với Express để xử lý các yêu cầu và phản hồi từ frontend. Sau đó, import instance Mastra, như được hiển thị trong file src/ag-ui-mastra.ts:

// =============================================================================
// PHẦN IMPORTS VÀ DEPENDENCIES
// =============================================================================

// Tải các biến môi trường từ file .env
// Điều này phải được import đầu tiên để đảm bảo các biến môi trường có sẵn
import "dotenv/config";

// Import framework Express.js và định nghĩa kiểu
// Express cung cấp máy chủ HTTP và chức năng middleware
import express, { Request, Response } from "express";

// Import các kiểu và schema cốt lõi của AG-UI để xác thực đầu vào và các loại sự kiện
// Chúng cung cấp các định nghĩa giao thức cho giao tiếp Agent Gateway UI
import {
  RunAgentInputSchema, // Schema để xác thực các yêu cầu agent đến
  RunAgentInput, // Giao diện TypeScript cho dữ liệu đầu vào của agent
  EventType, // Liệt kê tất cả các loại sự kiện có thể có
  Message, // Giao diện cho cấu trúc tin nhắn trò chuyện
} from "@ag-ui/core";

// Import bộ mã hóa sự kiện cho định dạng Server-Sent Events (SSE)
// Điều này xử lý việc mã hóa các sự kiện để truyền tải thời gian thực
import { EventEncoder } from "@ag-ui/encoder";

// Import trình tạo UUID để tạo ID tin nhắn duy nhất
// Được sử dụng để theo dõi các tin nhắn và lệnh gọi công cụ riêng lẻ
import { v4 as uuidv4 } from "uuid";

// Import instance Mastra đã cấu hình chứa agent phân tích chứng khoán của chúng ta
// Đây là công cụ luồng công việc AI chính xử lý các yêu cầu người dùng
import { mastra } from "./mastra";

// =============================================================================
// THIẾT LẬP ỨNG DỤNG EXPRESS
// =============================================================================

// Tạo instance ứng dụng Express
const app = express();

// Kích hoạt middleware phân tích JSON body cho các yêu cầu đến
// Điều này cho phép máy chủ phân tích các payload JSON từ các yêu cầu HTTP
app.use(express.json());

// =============================================================================
// TRIỂN KHAI ENDPOINT AGENT CHÍNH
// =============================================================================

// Định nghĩa endpoint mastra-agent chính (Agent Workflow Protocol)
// Endpoint này xử lý giao tiếp truyền tải với các agent AG-UI
app.post("/mastra-agent", async (req: Request, res: Response) => {

  //...

});

// =============================================================================
// PHẦN KHỞI TẠO MÁY CHỦ
// =============================================================================

// KHỞI ĐỘNG MÁY CHỦ EXPRESS
// Cấu hình và khởi động máy chủ HTTP trên cổng 8000
app.listen(8000, () => {
  console.log("Server running on http://localhost:8000");
  console.log("AG-UI endpoint available at http://localhost:8000/mastra-agent");
});

Bước 4: Cấu Hình Xác Thực Đầu Vào, Server-Sent Events (SSE) và Bộ Mã Hóa AG-UI Protocol

Bên trong endpoint máy chủ Express của bạn, cấu hình xác thực đầu vào, thiết lập các tiêu đề phản hồi Server-Sent Events và khởi tạo instance bộ mã hóa sự kiện của AG-UI Protocol để định dạng các sự kiện cho truyền tải SSE, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // BƯỚC 1: Phân tích và Xác thực Đầu vào
    // Phân tích nội dung yêu cầu đến bằng RunAgentInputSchema để đảm bảo
    // nó chứa tất cả các trường bắt buộc (threadId, runId, messages, v.v.)
    const input: RunAgentInput = RunAgentInputSchema.parse(req.body);

    // BƯỚC 2: Thiết lập Tiêu đề Phản hồi Server-Sent Events (SSE)
    // Cấu hình tiêu đề HTTP để kích hoạt giao tiếp truyền tải thời gian thực
    res.setHeader("Content-Type", "text/event-stream"); // Kích hoạt định dạng SSE
    res.setHeader("Cache-Control", "no-cache"); // Ngăn chặn caching trình duyệt
    res.setHeader("Connection", "keep-alive"); // Giữ kết nối mở để truyền tải

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // ...

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Bước 5: Cấu Hình Các Sự Kiện Vòng Đời của AG-UI Protocol

Trong endpoint máy chủ Express của bạn, cấu hình các sự kiện vòng đời của AG-UI Protocol để đánh dấu sự bắt đầu hoặc kết thúc việc thực thi tác vụ của agent, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // ...

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // BƯỚC 4: Gửi Sự kiện Bắt đầu Chạy
    // Thông báo cho client rằng việc chạy agent đã bắt đầu xử lý
    const runStarted = {
      type: EventType.RUN_STARTED,
      threadId: input.threadId,
      runId: input.runId,
    };
    res.write(encoder.encode(runStarted));

    // ...

    // BƯỚC 12: Hoàn tất Chạy Agent
    // Gửi sự kiện cuối cùng để chỉ ra rằng toàn bộ quá trình chạy agent đã hoàn tất
    const runFinished = {
      type: EventType.RUN_FINISHED,
      threadId: input.threadId, // Tham chiếu đến cuộc trò chuyện
      runId: input.runId, // Tham chiếu đến lần chạy cụ thể này
    };
    res.write(encoder.encode(runFinished));

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Bước 6: Cấu Hình Các Sự Kiện Quản Lý Trạng Thái AG-UI Protocol Trong Các Bước Luồng Công Việc Của Bạn

Trong file luồng công việc của Mastra AI agent, cấu hình sự kiện quản lý trạng thái STATE_DELTA của AG-UI Protocol trong các bước luồng công việc của bạn để phát ra các cập nhật trạng thái UI hiển thị trạng thái xử lý, như được hiển thị dưới đây:

import { EventType } from "@ag-ui/core"; // Các loại sự kiện cho cập nhật trạng thái UI

/**
 * BƯỚC 1: Trích xuất các Tham số Đầu tư từ Truy vấn Người dùng
 *
 * Bước này sử dụng LLM để phân tích truy vấn ngôn ngữ tự nhiên của người dùng và trích xuất
 * các tham số đầu tư có cấu trúc như mã cổ phiếu, số tiền, ngày, v.v.
 */
const fetchInformationFromUserQuery = createStep({
  id: "fetch-information-from-user-query",
  description: "Lấy thông tin từ truy vấn người dùng",
  // Định nghĩa schema đầu vào - dữ liệu mà bước này mong đợi nhận được

  // ...

  // Định nghĩa schema đầu ra - dữ liệu mà bước này sẽ tạo ra

  // ...

  execute: async ({ inputData }) => {
    try {
      // Bước 1.1: Khởi tạo dữ liệu và chuẩn bị prompt phân tích
      let data = inputData;
      await new Promise((resolve) => setTimeout(resolve, 0)); // Một chút độ trễ cho xử lý bất đồng bộ

      // Bước 1.2: Chèn ngữ cảnh danh mục đầu tư vào prompt phân tích chứng khoán
      data.messages[0].content = STOCK_ANALYST_PROMPT.replace(
        "{{PORTFOLIO_DATA_CONTEXT}}",
        JSON.stringify(inputData.investmentPortfolio)
      );

      // Bước 1.3: Phát ra cập nhật trạng thái UI để hiển thị trạng thái xử lý
      if (inputData?.emitEvent && typeof inputData.emitEvent === "function") {
        inputData.emitEvent({
          type: EventType.STATE_DELTA,
          delta: [
            {
              op: "add",
              path: "/toolLogs/-",
              value: {
                message: "Đang lấy thông tin từ truy vấn người dùng",
                status: "processing",
              },
            },
          ],
        });
        inputData.toolLogs.push({
          message: "Đang lấy thông tin từ truy vấn người dùng",
          status: "processing",
        });
        await new Promise((resolve) => setTimeout(resolve, 0));
      }

      // ...

       return {
          skip: true, // Bỏ qua các bước phân tích tiếp theo
          availableCash: inputData.availableCash,
          emitEvent: inputData.emitEvent,
          textMessage: response.choices[0].message.content,
        };
      } else {
        // Bước 1.7: Phân tích các tham số đầu tư được trích xuất từ lời gọi công cụ
        let toolResult;

        // ...

        // Bước 1.9: Cập nhật trạng thái UI thành hoàn tất
        if (inputData?.emitEvent && typeof inputData.emitEvent === "function") {
          let index = inputData.toolLogs.length - 1;
          inputData.emitEvent({
            type: EventType.STATE_DELTA,
            delta: [
              {
                op: "replace",
                path: `/toolLogs/${index}/status`,
                value: "completed",
              },
            ],
          });
          await new Promise((resolve) => setTimeout(resolve, 0));
        }

        // Bước 1.10: Trả về các tham số được trích xuất cho bước tiếp theo
        return {
          ...toolResult,
          skip: false, // Tiếp tục phân tích
          availableCash: inputData.availableCash,
          investmentPortfolio: inputData.investmentPortfolio,
          emitEvent: inputData.emitEvent,
          textMessage: "",
          toolLogs: inputData.toolLogs,
        };
      }
    } catch (error) {
      console.log(error);
      throw error;
    }
  },
});

Sau đó, trong endpoint Express, khởi tạo trạng thái agent của bạn bằng cách sử dụng sự kiện quản lý trạng thái STATE_SNAPSHOT của AG-UI Protocol, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // ...

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // ...

    // BƯỚC 5: Khởi tạo Trạng thái Agent
    // Gửi snapshot trạng thái ban đầu với các giá trị mặc định cho dữ liệu tài chính
    // Điều này cung cấp cho UI trạng thái hiện tại của danh mục đầu tư
    const stateSnapshot = {
      type: EventType.STATE_SNAPSHOT,
      snapshot: {
        availableCash: input.state?.availableCash || 100000, // Mặc định $100k nếu không được cung cấp
        investmentSummary: input.state?.investmentSummary || {}, // Đối tượng tóm tắt rỗng
        investmentPortfolio: input.state?.investmentPortfolio || [], // Mảng danh mục rỗng
        toolLogs: [], // Khởi tạo mảng log công cụ rỗng
      },
    };
    res.write(encoder.encode(stateSnapshot));
    await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép vòng lặp sự kiện xử lý

    // ...

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Bước 7: Tích Hợp và Khởi Tạo Luồng Công Việc Mastra AI Agent với AG-UI Protocol

Trong endpoint máy chủ Express của bạn, tích hợp luồng công việc Mastra AI agent từ instance Mastra với AG-UI Protocol, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // ...

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // ...

    // BƯỚC 6: Lấy Luồng Công việc Phân tích Chứng khoán
    // Lấy luồng công việc phân tích chứng khoán đã cấu hình sẵn từ Mastra
    const stockAnalysis = mastra.getWorkflow("stockAnalysisWorkflow");

    // BƯỚC 7: Định nghĩa Trợ giúp Phát sự kiện
    // Tạo một hàm trợ giúp để phát các sự kiện tới luồng SSE
    function emitEvent(data: any) {
      res.write(encoder.encode(data));
    }

    // BƯỚC 8: Tạo và Bắt đầu Thực thi Luồng Công việc
    // Khởi tạo một instance chạy luồng công việc mới và bắt đầu xử lý
    const workflow = await stockAnalysis.createRunAsync();
    const result = await workflow.start({
      inputData: {
        messages: input.messages, // Tin nhắn người dùng từ cuộc trò chuyện
        availableCash: input.state?.availableCash || 1000000, // Quỹ đầu tư khả dụng
        emitEvent: emitEvent, // Callback phát sự kiện
        investmentPortfolio: input.state?.investmentPortfolio || [], // Danh mục hiện tại
        toolLogs: [], // Khởi tạo log công cụ
      },
    });

    // BƯỚC 9: Đặt lại Trạng thái Log Công cụ
    // Xóa mọi log công cụ trước đó để bắt đầu mới cho lần chạy này
    emitEvent({
      type: EventType.STATE_DELTA,
      delta: [{ op: "replace", path: "/toolLogs", value: [] }],
    });
    await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép xử lý

    // ...

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Bước 8: Cấu Hình Sự Kiện Tool AG-UI Protocol cho Human-in-the-Loop (HITL)

Trong endpoint máy chủ Express của bạn, định nghĩa các sự kiện gọi công cụ của AG-UI Protocol mà một agent có thể sử dụng để kích hoạt các hành động frontend bằng cách gọi hành động frontend sử dụng tên công cụ để yêu cầu phản hồi từ người dùng, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // ...

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // ...

    // BƯỚC 11: Xử lý Kết quả Luồng Công việc
    // Kiểm tra xem luồng công việc có thực thi thành công và tạo ra dữ liệu biểu đồ không
    if (result?.status === "success" && result?.result?.result?.length > 0) {
      // BƯỚC 11A: Xử lý Phản hồi Hiển thị Biểu đồ/Bảng
      // Luồng công việc đã tạo ra dữ liệu phù hợp để hiển thị biểu đồ và bảng

      // BƯỚC 11A.1: Bắt đầu Lời gọi Công cụ để Hiển thị Biểu đồ
      // Thông báo cho client rằng một lời gọi công cụ đang bắt đầu
      const toolcallStart = {
        type: EventType.TOOL_CALL_START,
        toolCallId: uuidv4(), // Mã định danh duy nhất cho lời gọi công cụ này
        toolCallName: "render_standard_charts_and_table", // Tên công cụ được gọi
      };
      emitEvent(toolcallStart);
      await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép xử lý

      // BƯỚC 11A.2: Gửi Các Đối số Lời gọi Công cụ
      // Truyền dữ liệu biểu đồ/bảng làm đối số cho công cụ hiển thị
      const toolcallArgs = {
        type: EventType.TOOL_CALL_ARGS,
        toolCallId: toolcallStart.toolCallId, // Tham chiếu đến lời gọi công cụ
        delta: JSON.stringify(result.result), // Serialize dữ liệu kết quả
      };
      emitEvent(toolcallArgs);
      await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép xử lý

      // BƯỚC 11A.3: Kết thúc Lời gọi Công cụ
      // Báo hiệu rằng lời gọi công cụ đã hoàn tất
      const toolcallEnd = {
        type: EventType.TOOL_CALL_END,
        toolCallId: toolcallStart.toolCallId, // Tham chiếu đến lời gọi công cụ
      };
      emitEvent(toolcallEnd);
      await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép xử lý
    } else {

      // ...

    }

    // ...

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Bước 9: Cấu Hình Sự Kiện Tin Nhắn Văn Bản AG-UI Protocol

Khi bạn đã cấu hình các sự kiện công cụ của AG-UI Protocol, hãy định nghĩa các sự kiện tin nhắn văn bản của AG-UI Protocol để xử lý việc truyền tải phản hồi từ agent đến frontend, như được hiển thị dưới đây:

app.post("/mastra-agent", async (req: Request, res: Response) => {
  try {
    // ...

    // BƯỚC 3: Khởi tạo Bộ mã hóa Sự kiện
    // Tạo instance bộ mã hóa để định dạng các sự kiện cho truyền tải SSE
    const encoder = new EventEncoder();

    // ...

    // BƯỚC 11: Xử lý Kết quả Luồng Công việc
    // Kiểm tra xem luồng công việc có thực thi thành công và tạo ra dữ liệu biểu đồ không
    if (result?.status === "success" && result?.result?.result?.length > 0) {

      // ...

    } else {
      // BƯỚC 11B: Xử lý Phản hồi Văn bản
      // Luồng công việc đã tạo ra một tin nhắn văn bản thay vì dữ liệu biểu đồ

      // BƯỚC 11B.1: Bắt đầu Luồng Tin nhắn Văn bản
      // Bắt đầu truyền tải phản hồi văn bản tới client
      const textMessageStart = {
        type: EventType.TEXT_MESSAGE_START,
        messageId, // Sử dụng ID tin nhắn đã tạo
        role: "assistant", // Chỉ ra đây là phản hồi từ trợ lý
      };
      res.write(encoder.encode(textMessageStart));
      await new Promise((resolve) => setTimeout(resolve, 0)); // Cho phép xử lý

      // BƯỚC 11B.2: Trích xuất Nội dung Phản hồi
      // Lấy tin nhắn văn bản từ kết quả luồng công việc, với giá trị mặc định là chuỗi rỗng
      const response =
        result?.status === "success" ? result.result.textMessage : "";

      // BƯỚC 11B.3: Truyền tải Phản hồi theo Chunks
      // Chia phản hồi thành các chunk nhỏ hơn để trải nghiệm truyền tải mượt mà
      const chunkSize = 100; // Số ký tự mỗi chunk
      for (let i = 0; i < response.length; i += chunkSize) {
        const chunk = response.slice(i, i + chunkSize); // Trích xuất chunk

        // Gửi chunk tới client
        const textMessageContent = {
          type: EventType.TEXT_MESSAGE_CONTENT,
          messageId, // Tham chiếu tin nhắn
          delta: chunk, // Chunk văn bản
        };
        res.write(encoder.encode(textMessageContent));

        // Thêm một chút độ trễ giữa các chunk để hiệu ứng truyền tải mượt mà
        await new Promise((resolve) => setTimeout(resolve, 50));
      }

      // BƯỚC 11B.4: Kết thúc Luồng Tin nhắn Văn bản
      // Báo hiệu rằng tin nhắn văn bản đã hoàn tất
      const textMessageEnd = {
        type: EventType.TEXT_MESSAGE_END,
        messageId, // Tham chiếu tin nhắn
      };
      res.write(encoder.encode(textMessageEnd));
    }

    // ...

    // BƯỚC 13: Đóng Kết nối SSE
    // Kết thúc luồng phản hồi để hoàn tất yêu cầu HTTP
    res.end();
  } catch (error) {
    // =============================================================================
    // PHẦN XỬ LÝ LỖI
    // =============================================================================
   }
});

Chúc mừng! Bạn đã tích hợp thành công một agent AI Mastra với AG-UI Protocol. Bây giờ, hãy xem cách thêm frontend vào agent AI AG-UI + Mastra.

Tích Hợp Frontend Với Agent AI Mastra + AG-UI Sử Dụng CopilotKit: Nâng Cao Trải Nghiệm Người Dùng

Trong phần này, bạn sẽ học cách tạo kết nối giữa agent AI AG-UI + Mastra của bạn và một frontend sử dụng CopilotKit.

Trước tiên, điều hướng đến thư mục frontend:

cd frontend

Tiếp theo, tạo một file .env với OpenAI API Key của bạn:

OPENAI_API_KEY=<<your-OpenAI-key-here>>

Sau đó, cài đặt các dependencies:

pnpm install

Sau đó, khởi động máy chủ phát triển:

pnpm run dev

Điều hướng đến http://localhost:3000, và bạn sẽ thấy giao diện người dùng của agent AI AG-UI + Mastra đang hoạt động.

Bây giờ, chúng ta hãy xem cách xây dựng giao diện người dùng frontend cho agent AI AG-UI + Mastra bằng CopilotKit.

Bước 1: Tạo Instance HttpAgent

Trước khi tạo instance HttpAgent, trước tiên, hãy hiểu HttpAgent là gì. HttpAgent là một client từ thư viện AG-UI giúp kết nối ứng dụng frontend của bạn với máy chủ của bất kỳ agent AI tương thích AG-UI nào.

Để tạo instance HttpAgent, định nghĩa nó trong một API route như được hiển thị trong file src/app/api/copilotkit/route.ts:

// Import HttpAgent để thực hiện các yêu cầu HTTP đến backend
import { HttpAgent } from "@ag-ui/client";

// Import các thành phần runtime của CopilotKit để thiết lập endpoint API
import {
  CopilotRuntime,
  OpenAIAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";

// Import kiểu NextRequest để xử lý các yêu cầu API của Next.js
import { NextRequest } from "next/server";

// Tạo một instance HttpAgent mới kết nối đến backend nghiên cứu LangGraph đang chạy cục bộ
const mastraAgent = new HttpAgent({
  url: process.env.NEXT_PUBLIC_MASTRA_URL || "http://localhost:8000/mastra-agent",
});

// Khởi tạo runtime CopilotKit với agent nghiên cứu của chúng ta
const runtime = new CopilotRuntime({
  agents: {
    mastraAgent : mastraAgent, // Đăng ký agent nghiên cứu với runtime
  },
});

/**
 * Định nghĩa bộ xử lý POST cho endpoint API
 * Hàm này xử lý các yêu cầu POST đến endpoint /api/copilotkit
 */
export const POST = async (req: NextRequest) => {
  // Cấu hình endpoint CopilotKit cho Next.js app router
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime, // Sử dụng runtime với agent nghiên cứu của chúng ta
    serviceAdapter: new OpenAIAdapter(), // Sử dụng adapter thử nghiệm
    endpoint: "/api/copilotkit", // Định nghĩa đường dẫn endpoint API
  });

  // Xử lý yêu cầu đến với bộ xử lý CopilotKit
  return handleRequest(req);
};

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

Để thiết lập CopilotKit Provider, thành phần CopilotKit phải bao bọc các phần của ứng dụng của bạn có nhận thức về Copilot. Đối với hầu hết các trường hợp sử dụng, việc bao bọc toàn bộ ứng dụng bằng CopilotKit provider là phù hợp, ví dụ, trong file layout.tsx của bạn, như được hiển thị dưới đây trong file src/app/layout.tsx:

// Next.js imports cho metadata và xử lý font
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
// Global styles cho ứng dụng
import "./globals.css";
// CopilotKit UI styles cho các thành phần AI
import "@copilotkit/react-ui/styles.css";
// CopilotKit core component cho chức năng AI
import { CopilotKit } from "@copilotkit/react-core";

// Cấu hình font Geist Sans với biến CSS cho kiểu chữ nhất quán
const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

// Cấu hình font Geist Mono cho code và văn bản monospace
const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

// Cấu hình Metadata cho SEO và thông tin trang
export const metadata: Metadata = {
  title: "AI Stock Portfolio",
  description: "AI Stock Portfolio",
};

// Thành phần layout gốc bao bọc tất cả các trang trong ứng dụng
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        {/* Wrapper CopilotKit kích hoạt chức năng AI trong toàn bộ ứng dụng */}
        {/* runtimeUrl trỏ đến endpoint API cho giao tiếp backend AI */}
        {/* agent chỉ định agent AI nào sẽ sử dụng (stockAgent cho phân tích chứng khoán) */}
        <CopilotKit runtimeUrl="/api/copilotkit" agent="stockAgent">
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}

Bước 3: Thiết Lập Copilot Chat Component

CopilotKit đi kèm với một số thành phần chat tích hợp sẵn bao gồm CopilotPopup, CopilotSidebarCopilotChat.

Để thiết lập một thành phần chat Copilot, định nghĩa nó như được hiển thị trong file src/app/components/prompt-panel.tsx:

// Chỉ thị thành phần client-side cho Next.js
"use client";

import type React from "react";
// Thành phần chat CopilotKit cho tương tác AI
import { CopilotChat } from "@copilotkit/react-ui";

// Giao diện Props cho thành phần PromptPanel
interface PromptPanelProps {
  // Số tiền mặt khả dụng để đầu tư, hiển thị trong panel
  availableCash: number;
}

// Thành phần chính cho panel giao diện chat AI
export function PromptPanel({ availableCash }: PromptPanelProps) {
  // Hàm tiện ích để định dạng số dưới dạng tiền tệ USD
  // Loại bỏ các chữ số thập phân để hiển thị số lượng lớn rõ ràng hơn
  const formatCurrency = (amount: number) => {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      minimumFractionDigits: 0,
      maximumFractionDigits: 0,
    }).format(amount);
  };

  return (
    // Container chính với chiều cao đầy đủ và nền trắng
    <div className="h-full flex flex-col bg-white">
      {/* Phần header với tiêu đề, mô tả và hiển thị tiền mặt */}
      <div className="p-4 border-b border-[#D8D8E5] bg-[#FAFCFA]">
        {/* Phần tiêu đề với biểu tượng và thương hiệu */}
        <div className="flex items-center gap-2 mb-2">
          <span className="text-xl">🪁</span>
          <div>
            <h1 className="text-lg font-semibold text-[#030507] font-['Roobert']">
              Portfolio Chat
            </h1>
            {/* Chỉ báo huy hiệu Pro */}
            <div className="inline-block px-2 py-0.5 bg-[#BEC9FF] text-[#030507] text-xs font-semibold uppercase rounded">
              PRO
            </div>
          </div>
        </div>
        {/* Mô tả khả năng của agent AI */}
        <p className="text-xs text-[#575758]">
          Tương tác với agent AI được hỗ trợ bởi LangGraph để trực quan hóa và phân tích danh mục đầu tư
        </p>

        {/* Phần hiển thị Tiền mặt khả dụng */}
        <div className="mt-3 p-2 bg-[#86ECE4]/10 rounded-lg">
          <div className="text-xs text-[#575758] font-medium">
            Available Cash
          </div>
          <div className="text-sm font-semibold text-[#030507] font-['Roobert']">
            {formatCurrency(availableCash)}
          </div>
        </div>
      </div>
      {/* Giao diện chat CopilotKit với kiểu dáng tùy chỉnh và tin nhắn ban đầu */}
      {/* Chiếm phần lớn chiều cao panel cho cuộc hội thoại */}
      <CopilotChat
        className="h-[78vh] p-2"
        labels={{
          // Tin nhắn chào mừng ban đầu giải thích khả năng và hạn chế của agent AI
          initial: `Tôi là một agent AI LangGraph được thiết kế để phân tích cơ hội đầu tư và theo dõi hiệu suất cổ phiếu theo thời gian. Tôi có thể giúp bạn với truy vấn đầu tư của bạn như thế nào? Ví dụ, bạn có thể yêu cầu tôi phân tích một cổ phiếu như "Đầu tư vào Apple với 10k đô la từ tháng 1 năm 2023". \n\nLưu ý: Agent AI chỉ có quyền truy cập dữ liệu chứng khoán từ 4 năm gần đây nhất`,
        }}
      />
    </div>
  );
}

Bước 4: Đồng Bộ Trạng Thái Agent AI Mastra + AG-UI Với Frontend Bằng CopilotKit Hooks

Trong CopilotKit, CoAgents duy trì một trạng thái chia sẻ kết nối liền mạch giao diện người dùng frontend của bạn với quá trình thực thi của agent. Hệ thống trạng thái chia sẻ này cho phép bạn:

  • Hiển thị tiến trình hiện tại và kết quả trung gian của agent.
  • Cập nhật trạng thái của agent thông qua tương tác UI.
  • Phản ứng với các thay đổi trạng thái theo thời gian thực trên toàn bộ ứng dụng của bạn.

Bạn có thể tìm hiểu thêm về trạng thái chia sẻ của CoAgents tại đây trên tài liệu CopilotKit.

Để đồng bộ trạng thái agent AI AG-UI + Mastra của bạn với frontend, sử dụng hook useCoAgent của CopilotKit để chia sẻ trạng thái agent AI AG-UI + Mastra với frontend của bạn, như được hiển thị trong file src/app/page.tsx:

"use client";

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

// ...

export interface SandBoxPortfolioState {
  performanceData: Array<{
    date: string;
    portfolio: number;
    spy: number;
  }>;
}
export interface InvestmentPortfolio {
  ticker: string;
  amount: number;
}

export default function OpenStocksCanvas() {

  // ...

  const [totalCash, setTotalCash] = useState(1000000);

  const { state, setState } = useCoAgent({
    name: "stockAgent",
    initialState: {
      available_cash: totalCash,
      investment_summary: {} as any,
      investment_portfolio: [] as InvestmentPortfolio[],
    },
  });

    // ...

  return (
    <div className="h-screen bg-[#FAFCFA] flex overflow-hidden">
       {/* ... */}
    </div>
  );
}

Sau đó, hiển thị trạng thái của agent AI AG-UI + Mastra trong giao diện người dùng chat, điều này hữu ích để thông báo cho người dùng về trạng thái của agent một cách trực quan hơn.

Để hiển thị trạng thái của agent AI AG-UI + Mastra trong giao diện người dùng chat, bạn có thể sử dụng hook useCoAgentStateRender, như được hiển thị trong file src/app/page.tsx:

"use client";

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

import { ToolLogs } from "./components/tool-logs";

// ...

export default function OpenStocksCanvas() {

  // ...

  useCoAgentStateRender({
    name: "stockAgent",
    render: ({ state }) => <ToolLogs logs={state.tool_logs} />,
  });

  // ...

  return (
    <div className="h-screen bg-[#FAFCFA] flex overflow-hidden">
      {/* ... */}
    </div>
  );
}

Nếu bạn thực hiện một truy vấn trong chat, bạn sẽ thấy việc thực thi tác vụ trạng thái của agent AI AG-UI + Mastra được hiển thị trong giao diện người dùng chat.

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

Human-in-the-Loop (HITL) cho phép các agent yêu cầu đầu vào hoặc sự chấp thuận của con người trong quá trình thực thi, làm cho các hệ thống AI trở nên đáng tin cậy và đáng tin cậy hơn. Mô hình 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 đòi hỏi 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) trong frontend, bạn cần sử dụng hook useCopilotAction của CopilotKit với phương thức renderAndWaitForResponse, cho phép trả về giá trị bất đồng bộ từ hàm render, như được hiển thị trong file src/app/page.tsx:

"use client";

import {
  useCopilotAction,
} from "@copilotkit/react-core";
import { useEffect, useState } from "react";
// ... (các imports khác như LineChartComponent, BarChartComponent, AllocationTableComponent)

export default function OpenStocksCanvas() {

  // ... (các định nghĩa state và hooks khác)

  useCopilotAction({
    name: "render_standard_charts_and_table",
    description:
      "Đây là một hành động để hiển thị biểu đồ và bảng tiêu chuẩn. Biểu đồ có thể là biểu đồ cột hoặc biểu đồ đường. Bảng có thể là một bảng dữ liệu.",
    renderAndWaitForResponse: ({ args, respond, status }) => {
      useEffect(() => {
        console.log(args, "argsargsargsargsargsaaa");
      }, [args]);
      return (
        <>
          {args?.investment_summary?.percent_allocation_per_stock &&
            args?.investment_summary?.percent_return_per_stock &&
            args?.investment_summary?.performanceData && (
              <>
                <div className="flex flex-col gap-4">
                  <LineChartComponent
                    data={args?.investment_summary?.performanceData}
                    size="small"
                  />
                  <BarChartComponent
                    data={Object.entries(
                      args?.investment_summary?.percent_return_per_stock
                    ).map(([ticker, return1]) => ({
                      ticker,
                      return: return1 as number,
                    }))}
                    size="small"
                  />
                  <AllocationTableComponent
                    allocations={Object.entries(
                      args?.investment_summary?.percent_allocation_per_stock
                    ).map(([ticker, allocation]) => ({
                      ticker,
                      allocation: allocation as number,
                      currentValue:
                        args?.investment_summary.final_prices[ticker] *
                        args?.investment_summary.holdings[ticker],
                      totalReturn:
                        args?.investment_summary.percent_return_per_stock[
                          ticker
                        ],
                    }))}
                    size="small"
                  />
                </div>

                <button
                  hidden={status == "complete"}
                  className="mt-4 rounded-full px-6 py-2 bg-green-50 text-green-700 border border-green-200 shadow-sm hover:bg-green-100 transition-colors font-semibold text-sm"
                  onClick={() => {
                    debugger;
                    if (respond) {
                      setTotalCash(args?.investment_summary?.cash);
                      setCurrentState({
                        ...currentState,
                        returnsData: Object.entries(
                          args?.investment_summary?.percent_return_per_stock
                        ).map(([ticker, return1]) => ({
                          ticker,
                          return: return1 as number,
                        })),
                        allocations: Object.entries(
                          args?.investment_summary?.percent_allocation_per_stock
                        ).map(([ticker, allocation]) => ({
                          ticker,
                          allocation: allocation as number,
                          currentValue:
                            args?.investment_summary?.final_prices[ticker] *
                            args?.investment_summary?.holdings[ticker],
                          totalReturn:
                            args?.investment_summary?.percent_return_per_stock[
                              ticker
                            ],
                        })),
                        performanceData:
                          args?.investment_summary?.performanceData,
                        bullInsights: args?.insights?.bullInsights || [],
                        bearInsights: args?.insights?.bearInsights || [],
                        currentPortfolioValue:
                          args?.investment_summary?.total_value,
                        totalReturns: (
                          Object.values(
                            args?.investment_summary?.returns
                          ) as number[]
                        ).reduce((acc, val) => acc + val, 0),
                      });
                      setInvestedAmount(
                        (
                          Object.values(
                            args?.investment_summary?.total_invested_per_stock
                          ) as number[]
                        ).reduce((acc, val) => acc + val, 0)
                      );
                      setState({
                        ...state,
                        available_cash: totalCash,
                      });
                      respond(
                        "Dữ liệu đã được hiển thị thành công. Cung cấp tóm tắt các khoản đầu tư mà không thực hiện bất kỳ lệnh gọi công cụ nào"
                      );
                    }
                  }}>
                  Chấp nhận
                </button>
                <button
                  hidden={status == "complete"}
                  className="rounded-full px-6 py-2 bg-red-50 text-red-700 border border-red-200 shadow-sm hover:bg-red-100 transition-colors font-semibold text-sm ml-2"
                  onClick={() => {
                    debugger;
                    if (respond) {
                      respond(
                        "Hiển thị dữ liệu bị từ chối. Chỉ cần đưa ra tóm tắt các khoản đầu tư bị từ chối mà không thực hiện bất kỳ lệnh gọi công cụ nào"
                      );
                    }
                  }}>
                  Từ chối
                </button>
              </>
            )}
        </>
      );
    },
  });

  // ...

  return (
    <div className="h-screen bg-[#FAFCFA] flex overflow-hidden">
      {/* ... */}
    </div>
  );
}

Khi một agent 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 của con người trong quá trình thực thi, người dùng cuối sẽ được nhắc với một lựa chọn (được hiển thị bên trong giao diện người dùng chat). Sau đó, người dùng có thể chọn bằng cách nhấn một nút trong giao diện người dùng chat.

Bước 6: Truyền Tải Phản Hồi Từ Agent AI Mastra + AG-UI Lên Frontend Theo Thời Gian Thực

Để truyền tải phản hồi hoặc kết quả từ agent AI AG-UI + Mastra lên frontend, bạn cần truyền các giá trị trường trạng thái của agent đến các thành phần frontend, như được hiển thị trong file src/app/page.tsx:

"use client";

import { useEffect, useState } from "react";
import { PromptPanel } from "./components/prompt-panel";
import { GenerativeCanvas } from "./components/generative-canvas";
import { ComponentTree } from "./components/component-tree";
import { CashPanel } from "./components/cash-panel";

// ...

export default function OpenStocksCanvas() {
  const [currentState, setCurrentState] = useState<PortfolioState>({
    id: "",
    trigger: "",
    performanceData: [],
    allocations: [],
    returnsData: [],
    bullInsights: [],
    bearInsights: [],
    currentPortfolioValue: 0,
    totalReturns: 0,
  });
  const [sandBoxPortfolio, setSandBoxPortfolio] = useState<
    SandBoxPortfolioState[]
  >([]);
  const [selectedStock, setSelectedStock] = useState<string | null>(null);

  // ... (các định nghĩa state và hooks khác)

  return (
    <div className="h-screen bg-[#FAFCFA] flex overflow-hidden">
      {/* Panel Trái - Nhập Prompt */}
      <div className="w-85 border-r border-[#D8D8E5] bg-white flex-shrink-0">
        <PromptPanel availableCash={totalCash} />
      </div>

      {/* Panel Giữa - Generative Canvas */}
      <div className="flex-1 relative min-w-0">
        {/* Thanh Trên cùng với Thông tin Tiền mặt */}
        <div className="absolute top-0 left-0 right-0 bg-white border-b border-[#D8D8E5] p-4 z-10">
          <CashPanel
            totalCash={totalCash}
            investedAmount={investedAmount}
            currentPortfolioValue={
              totalCash + investedAmount + currentState.totalReturns || 0
            }
            onTotalCashChange={setTotalCash}
            onStateCashChange={setState}
          />
        </div>

        <div className="pt-20 h-full">
          <GenerativeCanvas
            setSelectedStock={setSelectedStock}
            portfolioState={currentState}
            sandBoxPortfolio={sandBoxPortfolio}
            setSandBoxPortfolio={setSandBoxPortfolio}
          />
        </div>
      </div>

      {/* Panel Phải - Component Tree (Tùy chọn) */}
      {showComponentTree && (
        <div className="w-64 border-l border-[#D8D8E5] bg-white flex-shrink-0">
          <ComponentTree portfolioState={currentState} />
        </div>
      )}
    </div>
  );
}

Nếu bạn truy vấn agent của mình và chấp thuận 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 agent được truyền tải trong giao diện người dùng.

Kết Luận: Hướng Tới Tương Lai Của Ứng Dụng AI Toàn Diện

Trong hướng dẫn này, chúng ta đã cùng nhau khám phá các bước tích hợp agent AI Mastra với AG-UI Protocol và sau đó thêm một frontend vào các agent này bằng cách sử dụng CopilotKit. Bạn đã học cách xây dựng một hệ thống fullstack mạnh mẽ, cho phép agent AI của bạn tương tác thời gian thực với người dùng, quản lý trạng thái, thực thi công cụ và truyền tải phản hồi một cách mượt mà.

Mặc dù chúng ta chỉ mới khám phá một vài tính năng, nhưng tiềm năng ứng dụng của CopilotKit là vô hạn, từ việc xây dựng chatbot AI tương tác đến phát triển các giải pháp agentic phức tạp. Về cơ bản, CopilotKit cho phép bạn bổ sung 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 rằng hướng dẫn này sẽ giúp bạn dễ dàng tích hợp các agent AI vào ứng dụng hiện có của mình, mở ra cánh cửa cho những trải nghiệm người dùng thông minh và mạnh mẽ hơn.

Hãy theo dõi CopilotKit trên Twitter và tham gia cộng đồng Discord nếu bạn muốn xây dựng những điều thú vị!

Chỉ mục