Chuyển tới nội dung chính

HTTP API — Lịch Sử Hội Thoại

Mục Đích

HTTP API lịch sử dùng khi:

  • F5 reload — load lại toàn bộ hội thoại
  • Switch session — chuyển sang session khác
  • Khởi động app — load session gần nhất

GET /v2/sessions/{session_id}/history

Request

GET /v2/sessions/abc-123/history
Authorization: Bearer {access_token}

Response

{
"session_id": "abc-123",
"messages": [...],
"message_count": 4,
"status": "completed",
"pending_approval": null,
"display_mode": "agent",
"workspace": null,
"agent_status": "completed",
"last_event_id": "01HXYZ999"
}
FieldMô tả
messagesMảng messages theo thứ tự (xem bên dưới)
status"completed", "cancelled", "error", "waiting_approval"
pending_approvalApproval request đang chờ (nếu status = "waiting_approval")
display_mode"agent" hoặc "chat"
last_event_idEvent ID cuối cùng — dùng khi subscribe WebSocket để replay
agent_statusTrạng thái Redis của task

Cấu Trúc messages

Mảng messages gồm 3 loại role: "user", "assistant", "tool".

Message Của User (role: "user")

{
"role": "user",
"content": [
{
"type": "text",
"text": "Phân tích cổ phiếu VNM"
}
],
"uuid": "msg-uuid-001"
}

Khi có file đính kèm:

{
"role": "user",
"content": [
{
"type": "attachment",
"url": "https://example.com/bao-cao.pdf",
"name": "bao-cao.pdf",
"mime_type": "application/pdf"
},
{
"type": "text",
"text": "Phân tích file này cho tôi"
}
],
"uuid": "msg-uuid-002"
}

Message Của AI (role: "assistant")

{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "Tôi cần tra giá cổ phiếu VNM..."
},
{
"type": "tool_use",
"id": "toolu_01XyzAbc",
"name": "search_stock",
"input": {
"symbol": "VNM"
}
},
{
"type": "text",
"text": "Cổ phiếu **VNM** đang giao dịch ở **82,000 VND**.",
"is_final": true
}
],
"tool_calls": [
{
"id": "toolu_01XyzAbc",
"name": "search_stock",
"tool_content_message": "Tìm kiếm cổ phiếu",
"args": {
"symbol": "VNM"
}
}
],
"uuid": "msg-uuid-003",
"parent_uuid": "msg-uuid-002",
"status": "completed",
"can_retry": false
}

Khi bị dừng giữa chừng:

{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Người dùng đã dừng cuộc trò chuyện. Gửi tin nhắn mới để tiếp tục",
"is_final": true
}
],
"status": "cancelled",
"can_retry": false
}

Message Kết Quả Tool (role: "tool")

{
"role": "tool",
"tool_call_id": "toolu_01XyzAbc",
"name": "search_stock",
"content": "VNM - Vinamilk\nGiá: 82,000 VND\nThay đổi: -1.2%",
"status": "success",
"artifact": null,
"uuid": "msg-uuid-004",
"parent_uuid": "msg-uuid-003"
}
FieldMô tả
tool_call_idKhớp với id trong tool_use của assistant
status"success", "error", hoặc "cancelled"
artifactDữ liệu artifact nếu có

Ví Dụ Response Đầy Đủ

{
"session_id": "abc-123",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Phân tích cổ phiếu VNM"
}
],
"uuid": "u1"
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "Cần tra giá VNM."
},
{
"type": "tool_use",
"id": "toolu_01",
"name": "search_stock",
"input": {
"symbol": "VNM"
}
},
{
"type": "text",
"text": "VNM đang ở 82,000 VND.",
"is_final": true
}
],
"tool_calls": [
{
"id": "toolu_01",
"name": "search_stock",
"tool_content_message": "Tìm kiếm",
"args": {
"symbol": "VNM"
}
}
],
"uuid": "a1",
"parent_uuid": "u1",
"status": "completed",
"can_retry": false
},
{
"role": "tool",
"tool_call_id": "toolu_01",
"name": "search_stock",
"content": "VNM: 82,000 VND (-1.2%)",
"status": "success",
"artifact": null,
"uuid": "t1",
"parent_uuid": "a1"
}
],
"message_count": 3,
"status": "completed",
"pending_approval": null,
"display_mode": "agent",
"workspace": null,
"agent_status": "completed",
"last_event_id": "01HXYZ999"
}

Xử Lý status Trong History

statusMô tảHành động client
"completed"Lượt hoàn thành bình thườngHiển thị tĩnh
"cancelled"User đã dừngHiển thị terminal block
"error"Lỗi agentHiển thị terminal error block
"waiting_approval"Đang chờ phê duyệtHiển thị approval UI, subscribe WS

Kết Hợp History + WebSocket

Pattern khuyến nghị khi load session:

1. Gọi GET /v2/sessions/{id}/history
2. Render messages từ history
3. Kiểm tra status:
- "completed" → không cần WS
- "cancelled/error" → không cần WS
- "waiting_approval" → subscribe WS, hiển thị approval UI
- "running" → subscribe WS với last_event_id để nhận tiếp
4. Khi subscribe WS với last_event_id:
→ Server replay events từ điểm đó, merge vào UI

GET /v2/sessions

Danh sách tất cả session của user.

Request

GET /v2/sessions?page=1&page_size=20
Authorization: Bearer {access_token}

Response

{
"sessions": [
{
"session_id": "abc-123",
"session_name": "Phân tích VNM",
"created_at": "2024-03-17T10:00:00Z",
"updated_at": "2024-03-17T10:05:00Z"
}
],
"total": 42,
"page": 1,
"page_size": 20
}

POST /v2/sessions/{session_id}/terminate

Dừng agent qua HTTP (thay thế cho lệnh stop qua WS).

Request

POST /v2/sessions/abc-123/terminate
Authorization: Bearer {access_token}

Response

{
"message": "Termination signal sent.",
"session_id": "abc-123"
}

Trả về 202 Accepted. Agent dừng ở lần check tiếp theo.


Ví Dụ: React.js — Load Và Render History

async function loadSession(sessionId, token) {
const res = await fetch(`/v2/sessions/${sessionId}/history`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()

// Chuyển messages thành blocks để render
const blocks = convertHistoryToBlocks(data.messages)
setBlocks(blocks)
setDisplayMode(data.displayMode)

// Nếu session vẫn đang chạy, subscribe WS để nhận tiếp
if (data.status === 'running' || data.status === 'waiting_approval') {
ws.subscribe(sessionId, data.last_event_id)
}

// Nếu có pending approval
if (data.pending_approval) {
setApprovalRequest(data.pending_approval)
}

return data
}

// Chuyển history messages thành blocks để render
function convertHistoryToBlocks(messages) {
const allBlocks = []
const toolUseMap = new Map() // id → index trong allBlocks

for (const msg of messages) {
if (msg.role === 'user') {
allBlocks.push({
type: 'user',
content: msg.content,
uuid: msg.uuid,
isFromHistory: true,
})
} else if (msg.role === 'assistant') {
for (const block of msg.content ?? []) {
const b = {
...block,
isFromHistory: true,
messageStatus: msg.status,
}
if (block.type === 'tool_use') {
toolUseMap.set(block.id, allBlocks.length)
}
allBlocks.push(b)
}
} else if (msg.role === 'tool') {
// Ghép vào tool_use tương ứng
const idx = toolUseMap.get(msg.tool_call_id)
if (idx != null) {
allBlocks[idx].toolResult = msg.content
allBlocks[idx].toolResultError = msg.status === 'error' || msg.status === 'cancelled'
allBlocks[idx].artifact = msg.artifact
}
}
}

return allBlocks
}

Ví Dụ: Flutter (Dart) — Load History

class SessionRepository {
final String baseUrl;
final String token;

SessionRepository({required this.baseUrl, required this.token});

Future<SessionHistory> loadHistory(String sessionId) async {
final response = await http.get(
Uri.parse('$baseUrl/v2/sessions/$sessionId/history'),
headers: {'Authorization': 'Bearer $token'},
);

if (response.statusCode != 200) {
throw Exception('Failed to load history: ${response.statusCode}');
}

return SessionHistory.fromJson(jsonDecode(response.body));
}
}

class SessionHistory {
final String sessionId;
final List<ChatMessage> messages;
final String status;
final String? lastEventId;
final Map<String, dynamic>? pendingApproval;

const SessionHistory({
required this.sessionId,
required this.messages,
required this.status,
this.lastEventId,
this.pendingApproval,
});

factory SessionHistory.fromJson(Map<String, dynamic> json) {
return SessionHistory(
sessionId: json['session_id'] as String,
messages: (json['messages'] as List)
.map((m) => ChatMessage.fromJson(m as Map<String, dynamic>))
.toList(),
status: json['status'] as String,
lastEventId: json['last_event_id'] as String?,
pendingApproval: json['pending_approval'] as Map<String, dynamic>?,
);
}

bool get needsWebSocket =>
status == 'running' || status == 'waiting_approval';
}