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

Web Search

Agent gọi tool web_search khi cần tra cứu thông tin thời gian thực. Server crawl và summarize kết quả, trả về cả nội dung lẫn danh sách nguồn.


Flow​

1. Agent gọi tool web_search với query + prompt
2. Server tìm kiếm via Google Serper API
3. Server crawl 5 URL đầu tiên để lấy nội dung
4. AI summarize nội dung + trích xuất sources
5. Trả về summary text + artifact.sources

Realtime Stream​

Bước 1 — Tool_use Block​

Agent gọi web_search, server stream tool_use block với input đầy đủ trong content_block_start:

{
"type": "content_block_start",
"index": 2,
"content_block": {
"type": "tool_use",
"id": "toolu_01",
"name": "web_search",
"tool_content_message": "Tìm kiếm web",
"input": {
"query": "cổ phiếu HPG giá hôm nay",
"prompt": "Tìm giá và thông tin giao dịch mới nhất của HPG"
}
}
}
{
"type": "content_block_stop",
"index": 2
}

Bước 2 — Tool_result Block​

Sau khi server hoàn thành crawl và summarize, emit tool_result block kèm artifact:

{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "tool_result",
"tool_use_id": "toolu_01",
"name": "web_search",
"status": "success",
"content": "HPG đang giao dịch ở 25,500 VND (+2.0%)...\n\nSources:\n[Simplize](https://simplize.vn/...)",
"artifact": {
"query": "cổ phiếu HPG giá hôm nay",
"sources": [
{
"url": "https://simplize.vn/co-phieu/HPG",
"title": "Cổ phiếu HPG — Tập đoàn Hòa Phát",
"snippet": "HPG: 25,500 VND (+2.0%) - Khối lượng: 12.3M",
"domain": "simplize.vn",
"favicon": "https://www.google.com/s2/favicons?domain=simplize.vn&sz=16"
},
{
"url": "https://cafef.vn/...",
"title": "HPG tăng mạnh trong phiên sáng",
"snippet": "...",
"domain": "cafef.vn",
"favicon": null
}
]
}
}
}
{
"type": "content_block_stop",
"index": 3
}

artifact không stream từng field — được trả về đầy đủ trong một content_block_start.

Cập Nhật Side Panel​

Frontend lắng nghe tool_result event của web_search để cập nhật WebSearchSourcesPanel:

function handleSearchSourceSSEEvent(data, setSearchSources) {
if (data.type === 'content_block_start' && data.content_block?.type === 'tool_result') {
if (data.content_block.name === 'web_search' && data.content_block.artifact) {
const group = data.content_block.artifact;
if (group.query && group.sources) {
setSearchSources(prev => [...(prev || []), group]);
}
}
}
}

Mỗi lần web_search được gọi, một nhóm mới được append vào danh sách sources — không replace.


Artifact Structure​

interface WebSearchArtifact {
query: string;
sources: WebSearchSource[];
}

interface WebSearchSource {
url: string;
title: string;
snippet: string;
domain: string;
favicon: string | null;
}

Favicon fallback: Nếu favicon null → tự tạo từ domain:

const favicon = source.favicon
?? `https://www.google.com/s2/favicons?domain=${source.domain}&sz=16`;

History (F5 Reload)​

Sources Side Panel​

Panel restore từ workspace.sources trong history response:

{
"workspace": {
"sources": [
{
"query": "cổ phiếu HPG giá hôm nay",
"sources": [
{
"url": "https://simplize.vn/co-phieu/HPG",
"title": "Cổ phiếu HPG — Tập đoàn Hòa Phát",
"snippet": "HPG: 25,500 VND (+2.0%)",
"domain": "simplize.vn",
"favicon": "https://www.google.com/s2/favicons?domain=simplize.vn&sz=16"
}
]
}
]
}
}
const restoredSources = historyData?.workspace?.sources || null;
setSearchSources(restoredSources);

Blocks Trong AI Message​

Trong history, web_search xuất hiện như cặp tool_use trong msg.content và tool_result trong message kế tiếp (role: "tool"):

[
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01",
"name": "web_search",
"input": {
"query": "cổ phiếu HPG giá hôm nay",
"prompt": "..."
}
}
]
},
{
"role": "tool",
"tool_call_id": "toolu_01",
"name": "web_search",
"content": "HPG đang giao dịch ở 25,500 VND...",
"status": "success",
"artifact": {
"query": "cổ phiếu HPG giá hôm nay",
"sources": [
{
"url": "...",
"title": "...",
"domain": "simplize.vn",
"favicon": "..."
}
]
}
}
]

parseV2History() merge tool messages vào AI message rồi build blocks:

// parseV2History(): nhìn về phía trước để lấy tool messages
let j = i + 1;
while (j < messages.length && messages[j].role === 'tool') {
blocks.push({
type: 'tool_result',
tool_use_id: toolMsg.tool_call_id,
name: toolMsg.name,
content: toolMsg.content,
status: toolMsg.status || 'success',
artifact: toolMsg.artifact, // sources nằm đây
});
j++;
}

// Sau mergeToolBlocks(), kết quả AI message:
{
id: "session-history-1",
type: "ai_answer",
isStreaming: false,
blocks: [
{
type: "tool_use",
id: "toolu_01",
name: "web_search",
input: { query: "cổ phiếu HPG giá hôm nay" },
toolResult: {
type: "tool_result",
status: "success",
content: "HPG đang giao dịch...",
artifact: {
query: "cổ phiếu HPG giá hôm nay",
sources: [{ url: "...", title: "...", domain: "simplize.vn", favicon: "..." }]
}
}
}
]
}

renderBlock() detect block.name === 'web_search' → render WebSearchBlock với block.toolResult.artifact.sources.


Frontend Display​

WebSearchBlock (Inline)​

🔍 Tìm kiếm web  [3 results]  ▶
↓ click để expand
┌────────────────────────────────────────────┐
│ 🌐 Cổ phiếu HPG — Tập đoàn Hòa Phát │
│ simplize.vn ↗ │
│ 🌐 HPG tăng mạnh trong phiên sáng │
│ cafef.vn ↗ │
│ 🌐 Hòa Phát Q1/2024 lợi nhuận tăng 30% │
│ vnexpress.net ↗ │
└────────────────────────────────────────────┘
  • Click source → mở trong tab mới (target="_blank")
  • Show query text + số lượng result dạng badge

WebSearchSourcesPanel (Sidebar)​

  • Hiển thị tất cả search groups của session, group theo query
  • Click source → mở tab mới
  • Slide panel drawer (320–600px, resizable) cho expanded view
  • Persist qua các message trong cùng session

Ví Dụ: Xử Lý Sources​

function WebSearchBlock({ block }) {
const [expanded, setExpanded] = useState(false);
const sources = block.toolResult?.artifact?.sources || [];

return (
<div>
<button onClick={() => setExpanded(!expanded)}>
🔍 Tìm kiếm web [{sources.length} results] {expanded ? '▼' : '▶'}
</button>
{expanded && (
<ul>
{sources.map((s, i) => (
<li key={i}>
<img
src={s.favicon ?? `https://www.google.com/s2/favicons?domain=${s.domain}&sz=16`}
width={16} height={16}
alt=""
/>
<a href={s.url} target="_blank" rel="noopener noreferrer">
{s.title}
</a>
<span>{s.domain}</span>
</li>
))}
</ul>
)}
</div>
);
}