ساخت پیامهای AI SDK و شبیهسازی مکالمههای از پیش تعریفشده با استفاده از useChat، بدون نیاز به مدل، API Route، درخواست شبکه یا API Key.
@farsiui/helpers/ai-sdk به شما اجازه میدهد یک مکالمهٔ هوش مصنوعی را مستقیماً در کد تعریف کنید و آن را از طریق useChat بهصورت Stream دریافت کنید.
چون مکالمه از چرخهٔ واقعی useChat عبور میکند، کامپوننتهای شما همان رفتاری را خواهند داشت که در محیط واقعی دارند.
این Helper از تمام انواع Partهای پشتیبانیشده توسط AI SDK، از جمله Reasoning، Tool، Data، File، Source و Partهای سفارشی پشتیبانی میکند. Tool Callها حتی میتوانند برای دریافت ورودی واقعی کاربر، از جمله تأییدیه، متوقف شوند. برای این حالت، بخش Human in the Loop را ببینید.
import { createChat } from "@farsiui/helpers/ai-sdk"
const chat = createChat()
.user("What changed in this release?")
.assistant("The release adds keyboard shortcuts and faster search.")
.user("Can you show me the shortcuts?")
.assistant("Press ⌘K to search and ⌘Enter to submit.")Pass the chat to useChat with its initial messages and local transport.
useChat receives the same typed UIMessage[] it would get from a streamed
response.
import { useChat } from "@ai-sdk/react"
function Chat() {
const { messages, sendMessage } = useChat({
messages: chat.get(0), // starts with no messages.
transport: chat.transport(),
})
const nextMessage = chat.next(messages)
return (
<button
disabled={!nextMessage}
onClick={() => {
if (nextMessage) {
void sendMessage(nextMessage)
}
}}
>
Send next message
</button>
)
}get(0) مکالمه را بدون پیام شروع میکند. sendMessage(nextMessage) پیام کاربر بعدی را از مکالمهٔ از پیش تعریفشده ارسال میکند و Transport پاسخ Assistant را بهصورت Stream دریافت میکند.
این Helper رابط کاربری Chat را از مدل و Backend جدا میکند تا بتوانید Frontend را بهتنهایی توسعه دهید. همهچیز بهصورت آفلاین، سریع و قابل تکرار اجرا میشود.
پکیج Helper را نصب کنید:
این Helper در کنار تنظیمات موجود AI SDK شما، یعنی ai و @ai-sdk/react کار میکند.
Helperها را از مسیر زیر Import کنید:
import { ... } from "@farsiui/helpers/ai-sdk"Create a conversation, pass its messages and transport to useChat, then send
each predefined user message with next().
"use client"
import { useChat } from "@ai-sdk/react"
import { createChat } from "@farsiui/helpers/ai-sdk"
const chat = createChat()
.user("What changed in this release?")
.assistant("The release adds keyboard shortcuts and faster search.")
.user("Can you show me the shortcuts?")
.assistant("Press ⌘K to search and ⌘Enter to submit.")
const initialMessages = chat.get(0)
const transport = chat.transport()
export function Chat() {
const { messages, sendMessage, status } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
return (
<div>
{messages.map((message) => (
<div key={message.id}>{/* Render the message */}</div>
))}
<button
disabled={!nextMessage || isBusy}
onClick={() => {
if (nextMessage && !isBusy) {
void sendMessage(nextMessage)
}
}}
>
Send
</button>
</div>
)
}پیامهایی که با این Helper ساخته میشوند از نقشهای user و assistant استفاده میکنند.
برای اضافه کردن پیام کاربر از user() استفاده کنید. اگر مکالمه را با createChat({ messages }) شروع کنید، پیامهای موجود AI SDK نیز حفظ میشوند.
const chat = createChat()
.user("What changed in this release?")
const [message] = chat.get()
message.role // "user"
message.parts // [{ type: "text", text: "What changed in this release?" }]اگر به شناسه یا Metadata نیاز دارید، آنها را بهعنوان آرگومان دوم ارسال کنید:
chat.user("What changed in this release?", {
id: "user-release-question",
metadata: {
source: "docs",
},
})پیامهای کاربر میتوانند شامل فایل نیز باشند. بخش Files را ببینید.
برای اضافه کردن پیام Assistant از assistant() استفاده کنید.
const chat = createChat()
.assistant("The release adds keyboard shortcuts and faster search.")
const [message] = chat.get()
message.role // "assistant"
message.parts // [{ type: "text", text: "The release adds...", state: "done" }]یک String یک Text Part ایجاد میکند. همچنین میتوانید آرایهای از Partهای AI SDK را ارسال کنید:
chat.assistant([
{ type: "text", text: "The release adds keyboard shortcuts." },
{ type: "text", text: "Search is faster too." },
])برای پیامهایی که فقط شامل متن نیستند، از Callback مربوط به writer استفاده کنید:
chat.assistant(({ writer }) => {
writer.reasoning("I should summarize the release.")
writer.text("The release adds keyboard shortcuts and faster search.")
})Partها به همان ترتیبی که متدهای writer را فراخوانی میکنید، به پیام اضافه میشوند.
یک پیام Assistant میتواند شامل انواع مختلفی از Partها باشد. آنها را با writer که از assistant() دریافت میکنید و به همان ترتیبی که میخواهید نمایش داده شوند، اضافه کنید:
chat.assistant(({ writer }) => {
writer.reasoning("Let me check the weather.")
writer
.tool("getWeather", {
input: { city: "San Francisco" },
})
.output({
city: "San Francisco",
temperature: 18,
condition: "Breezy",
})
writer.text("It is 18°C and breezy in San Francisco.")
})برای اضافه کردن یک Text Part از text() استفاده کنید:
chat.assistant(({ writer }) => {
writer.text("The release adds keyboard shortcuts.")
writer.text(" Search is faster too.")
})هر بار فراخوانی text() یک Text Part جداگانه ایجاد میکند. هنگام استفاده از chat.transport()، متن بهصورت کلمهبهکلمه Stream میشود.
با mode: "instant" میتوانید کل Part را یکجا ارسال کنید. همچنین با delayMs میتوانید فاصلهٔ بین Text Deltaها را تغییر دهید:
writer.text("Done.", { mode: "instant" })
writer.text("This part streams more slowly.", { delayMs: 100 })اگر Text Part به یک شناسهٔ پایدار نیاز دارد، مقدار id را مشخص کنید:
writer.text("The final answer.", { id: "answer" })برای اضافه کردن یک Reasoning Part از reasoning() استفاده کنید:
chat.assistant(({ writer }) => {
writer.reasoning("I should check the latest conditions first.")
writer.text("Let me check the weather.")
})reasoning() همان گزینههای id، delayMs و mode را که در text() وجود دارد، پشتیبانی میکند:
writer.reasoning("Checking the forecast.", { mode: "instant" })برای اضافه کردن یک Tool Call از tool() استفاده کنید. این متد یک Handle برمیگرداند که وضعیت Tool را از Input تا Output دنبال میکند.
chat.assistant(({ writer }) => {
writer
.tool("getWeather", {
title: "Checking weather",
input: { city: "San Francisco" },
})
.sleep(900)
.output({
city: "San Francisco",
temperature: 18,
condition: "Breezy",
})
writer.text("It is 18°C and breezy in San Francisco.")
})Tool میتواند با output()، error() یا denied() به پایان برسد:
writer
.tool("getWeather", { input: { city: "San Francisco" } })
.error()
writer
.tool("getWeather", { input: { city: "San Francisco" } })
.denied()با تنظیم dynamic: true، بهجای یک Part تایپشدهٔ tool-<name>، یک Part از نوع dynamic-tool ایجاد میشود:
writer.tool("getWeather", {
dynamic: true,
input: { city: "San Francisco" },
output: {
city: "San Francisco",
temperature: 18,
condition: "Breezy",
},
})برای Type کردن نام Tool، Input و Output، Type پیام را به createChat بدهید. این Type همان پارامتری است که useChat نیز استفاده میکند:
import type { UIMessage } from "ai"
type Tools = {
getWeather: {
input: { city: string }
output: {
city: string
temperature: number
condition: string
}
}
}
type ChatMessage = UIMessage<
unknown,
Record<string, never>,
Tools
>
const chat = createChat<ChatMessage>()سپس همین Type را برای createChat و useChat استفاده کنید:
const { messages } = useChat<ChatMessage>({
messages: chat.get(0),
transport: chat.transport(),
})یک Tool Call میتواند بهجای پایان یافتن در Script، برای دریافت ورودی کاربر متوقف شود. بخش Human in the Loop را ببینید.
یک Tool Call میتواند متوقف شود و کنترل را به کاربر واقعی واگذار کند. این Helper هر دو نوع جریان Human-in-the-Loop در AI SDK را پشتیبانی میکند:
برای متوقف کردن یک Turn، Tool Call را بدون نتیجه باقی بگذارید. Input بهصورت Stream ارسال میشود، Turn پایان پیدا میکند و Part در وضعیت input-available باقی میماند تا Client با addToolOutput نتیجه را ارسال کند.
chat.assistant(({ writer }) => {
writer.text("Answer these and I'll tailor the prototype.")
writer.tool("askQuestions", { input: { questions } })
})با قرار دادن needsApproval: true، Tool را تا تصمیم کاربر متوقف کنید.
در این حالت output به معنی «بعد از تأیید، این خروجی را Stream کن» است، نه اینکه Tool بلافاصله تکمیل شود.
رد کردن کاربر بهصورت خودکار tool-output-denied را Stream میکند. همچنین میتوانید با errorText مشخص کنید که Tool پس از تأیید با خطا پایان پیدا کند.
chat.assistant(({ writer }) => {
writer.text("That will archive 3 drafts. I need your approval.")
writer.tool("archiveDrafts", {
input: { count: 3 },
needsApproval: true,
output: { archived: 3 },
})
})فراخوانی output()، error() یا denied() روی Handle مربوط به Tool دارای needsApproval باعث خطا میشود. تصمیم کاربر وضعیت Tool را مشخص میکند.
اگر بلافاصله بعد از یک Turn متوقفشده، یک Turn جدید تعریف کنید، آن Turn بهعنوان Continuation شناخته میشود.
Continuation هنگام ساخت Script اجرا نمیشود؛ زمانی اجرا میشود که درخواست بعدی از سمت کاربر برسد. Context آن شامل toolCall است که Tool متوقفشده و نتیجهٔ اقدام کاربر را در خود دارد.
chat
.assistant(({ writer }) => {
writer.tool("askQuestions", { input: { questions } })
})
.assistant(({ writer, toolCall }) => {
writer.text(
toolCall?.name === "askQuestions" && toolCall.output
? `Starting with ${toolCall.output.answers.direction}.`
: "Starting now."
)
})برای Toolهای Client، مقدار toolCall.output نتیجهای است که کاربر ارسال کرده است.
برای Approval، مقادیر toolCall.approved و toolCall.denied تصمیم کاربر را مشخص میکنند و خروجی تعریفشدهٔ Tool قبل از محتوای Continuation Stream میشود.
Context مربوط به Continuation شامل messages، یعنی Transcript زنده، و toolCalls، یعنی تمام Tool Callهای متوقفشده در Turnهایی با بیش از یک Tool Call، نیز میشود.
Continuationها باید Pure باقی بمانند. هنگام Regenerate، آنها دوباره بر اساس Transcript فعلی Resolve میشوند؛ بنابراین تغییر تصمیم کاربر میتواند Branch متفاوتی را ایجاد کند.
در سمت Client، از AI SDK به شکل معمول استفاده کنید.
برای ارسال نتیجهٔ Tool از addToolOutput، برای پاسخ به درخواست تأیید از addToolApprovalResponse و برای ارسال خودکار درخواست بعدی از sendAutomaticallyWhen استفاده کنید.
import {
lastAssistantMessageIsCompleteWithApprovalResponses,
lastAssistantMessageIsCompleteWithToolCalls,
} from "ai"
const {
messages,
addToolOutput,
addToolApprovalResponse,
} = useChat({
messages: chat.get(0),
transport: chat.transport(),
sendAutomaticallyWhen: (options) =>
lastAssistantMessageIsCompleteWithToolCalls(options) ||
lastAssistantMessageIsCompleteWithApprovalResponses(options),
})برای یک Tool در انتظار:
addToolOutput({
tool: "askQuestions",
toolCallId: part.toolCallId,
output: { answers },
})برای یک درخواست تأیید:
addToolApprovalResponse({
id: part.approval.id,
approved: true,
})Continuation بهعنوان Step جدیدی از همان پیام Assistant متوقفشده Stream میشود. Client Partهای آن را به همان پیام اضافه میکند، نه اینکه پیام جدیدی بسازد.
Helper نیز مرز Step را ارسال میکند و Message ID مربوط به Continuation را حذف میکند تا Merge در همان پیام انجام شود و ارسال خودکار دوباره Trigger نشود.
در محیط Development، اگر یک Tool با needsApproval فاقد Continuation باشد، یا Continuation بدون Tool Call در انتظار Resolve شود، Helper هشدار میدهد.
با data() یک Part تایپشده از نوع data-* اضافه کنید.
type DataParts = {
weather: {
city: string
status: "loading" | "success"
temperature?: number
condition?: string
}
}
const chat = createChat<
UIMessage<unknown, DataParts>
>().assistant(({ writer }) => {
writer.data({
type: "data-weather",
id: "weather-sf",
data: {
city: "San Francisco",
status: "loading",
},
})
})برای بهروزرسانی یک Part، همان type و id را دوباره ارسال کنید. این روش برای وضعیتهایی مثل تغییر از Loading به Success کاربرد دارد.
writer.data({
type: "data-weather",
id: "weather-sf",
data: {
city: "San Francisco",
status: "success",
temperature: 27,
condition: "Breezy",
},
})اگر میخواهید یک Update فقط به Client Stream شود و در پیام نهایی باقی نماند، transient: true را تنظیم کنید.
writer.data({
type: "data-weather",
data: {
city: "San Francisco",
status: "loading",
},
transient: true,
})برای اضافه کردن فایل به پیام کاربر، از گزینهٔ files استفاده کنید:
chat.user("Summarize this report.", {
files: [
{
filename: "report.pdf",
mediaType: "application/pdf",
url: "https://example.com/report.pdf",
},
],
})برای اضافه کردن فایل به پیام Assistant از file() استفاده کنید:
chat.assistant(({ writer }) => {
writer.file({
filename: "summary.md",
mediaType: "text/markdown",
url: "https://example.com/summary.md",
})
})برای اضافه کردن فایل به یک Reasoning Part از reasoningFile() استفاده کنید:
writer.reasoningFile({
filename: "notes.txt",
mediaType: "text/plain",
url: "https://example.com/notes.txt",
})برای اضافه کردن یک Source از نوع URL از sourceUrl() استفاده کنید:
writer.sourceUrl({
sourceId: "source-1",
title: "Release notes",
url: "https://example.com/releases",
})برای اضافه کردن یک Source از نوع Document از sourceDocument() استفاده کنید:
writer.sourceDocument({
sourceId: "source-2",
title: "Product brief",
mediaType: "application/pdf",
filename: "brief.pdf",
})با stepStart() میتوانید یک مرز Step در AI SDK ایجاد کنید.
chat.assistant(({ writer }) => {
writer.stepStart()
writer.reasoning("I should search the release notes.")
writer.stepStart()
writer.text("Here is what changed.")
})با custom() میتوانید یک Part سفارشی به پیام اضافه کنید.
chat.assistant(({ writer }) => {
writer.custom("app.approval")
})پیام شامل یک Part از نوع custom با مقدار kind مشخصشده خواهد بود.
AI SDK برای kind از قالب {provider}.{provider-type} استفاده میکند. اگر custom() را بدون kind فراخوانی کنید، مقدار پیشفرض "test.output" استفاده میشود.
با get() میتوانید پیامها را از ابتدای مکالمه دریافت کنید:
chat.get() // همهٔ پیامها
chat.get(2) // دو پیام اول
chat.get(0) // مکالمهٔ اولیه بدون پیامget() نسخهٔ Cloneشدهٔ پیامها را برمیگرداند و تغییری در Chat ایجاد نمیکند.
این متد قبل از اولین Continuation متوقف میشود، چون Continuation بدون Transcript زنده پیام مستقلی ندارد. همچنین اگر count از تعداد پیامهای قابل دسترس بیشتر باشد، خطا ایجاد میکند.
برای پیدا کردن پیام بعدی کاربر در مکالمهٔ از پیش تعریفشده، از next() استفاده کنید:
const initialMessages = chat.get(2)
const nextMessage = chat.next(initialMessages)next() همیشه یک Transcript از پیامها دریافت میکند، نه یک Index. پیام بعدی کاربر را برمیگرداند و اگر پیام دیگری باقی نمانده باشد، null برمیگرداند.
برای ادامه دادن یک مکالمهٔ ذخیرهشده یا Fixture، مقدار messages را به createChat بدهید:
import { createChat } from "@farsiui/helpers/ai-sdk"
import type { UIMessage } from "ai"
declare const savedMessages: UIMessage[]
const chat = createChat({
messages: savedMessages,
})
.user("What should we do next?")
.assistant("Turn the open questions into a checklist.")شناسهها، Metadata و Partهای موجود بدون تغییر حفظ میشوند.
متد transport() یک ChatTransport از AI SDK ایجاد میکند که میتوانید مستقیماً به useChat بدهید:
const transport = chat.transport()
const { messages, sendMessage } = useChat({
messages: chat.get(0),
transport,
})وقتی sendMessage() اجرا میشود، Transport پیام Assistant مربوط به Transcript فعلی را پیدا میکند و آن را از طریق چرخهٔ معمول Chat در AI SDK Stream میکند.
ابتدا با Message ID تطبیق میدهد و در صورت نیاز، بر اساس Role و متن آخرین پیام تطبیق انجام میشود.
ارسال خودکار پس از دریافت Tool Result یا Approval Response، Continuation بعدی را Resolve میکند. فقط Regeneration است که یک Turn را بر اساس Message ID دوباره اجرا میکند.
delayMs در سطح Transport مقدار پیشفرض تمام Text و Reasoningهای Streamشده است. برای یک Part خاص میتوانید آن را با writer.text(..., { delayMs }) یا writer.reasoning(..., { delayMs }) تغییر دهید.
const transport = chat.transport({
delayMs: 25,
})وقتی مکالمه دیگر پاسخ Assistant از پیش تعریفشدهای ندارد، از fallback استفاده کنید:
const transport = chat.transport({
fallback: "This demo has no more predefined replies.",
})fallback میتواند آرایهای از Partهای AI SDK یا یک Callback نیز باشد. Callback، Transcript ورودی را دریافت میکند و میتواند بر اساس وضعیت فعلی یک پاسخ بسازد:
const transport = chat.transport({
fallback: ({ writer, messages }) => {
writer.text(
`This example already has ${messages.length} messages.`,
{ mode: "instant" }
)
},
})Fallbackها مانند پاسخهای Assistant Stream میشوند، اما به مکالمهٔ از پیش تعریفشده اضافه نمیشوند.
اگر Fallback تعریف نشده باشد، پس از تمام شدن مکالمه این خطا ایجاد میشود:
"No assistant response found for this transcript."
فراخوانی stop() از useChat، Stream فعال Transport را متوقف میکند. اتصال مجدد پشتیبانی نمیشود و reconnectToStream() مقدار null برمیگرداند.
برای شبیهسازی سرعت یک پاسخ واقعی، میتوانید از Delay استفاده کنید:
const chat = createChat()
.user("Give me a project update.")
.sleep(800)
.assistant(({ writer }) => {
writer.text("The first milestone is complete.", {
mode: "instant",
})
writer.sleep(500)
writer.text(" The next one is ready.")
})
const transport = chat.transport({
delayMs: 50,
})chat.sleep(ms) قبل از شروع پاسخ بعدی Assistant منتظر میماند.writer.sleep(ms) بین Partهای یک پاسخ Assistant مکث میکند.tool.sleep(ms) بین Input و Result یک Tool مکث میکند.transport({ delayMs }) تأخیر پیشفرض بین Deltaهای Text و Reasoning را تعیین میکند.writer.text(text, { delayMs }) و writer.reasoning(text, { delayMs }) این تأخیر را برای یک Part مشخص تغییر میدهند.mode: "instant" کل Text یا Reasoning را در یک Delta ارسال میکند.برای تستهای سریع، از chat.transport({ delayMs: 0 }) همراه با Textهای instant استفاده کنید.
اگر میخواهید کل پاسخ Assistant با خطا مواجه شود، از error() روی Chat استفاده کنید:
const chat = createChat()
.user("Load the report.")
.error("The report could not be loaded.")برای ایجاد خطا پس از Stream شدن Partهای قبلی، از writer.error() استفاده کنید:
chat.assistant(({ writer }) => {
writer.text("I found the report.")
writer.error("The connection closed before it could be read.")
})میتوانید برای هر پیام Metadata تعیین کنید. Type آن از طریق get()، next() و Transport حفظ میشود:
type Metadata = {
model: string
}
const chat = createChat<UIMessage<Metadata>>()
.user("Hello", {
metadata: { model: "demo" },
})
.assistant("Hi.", {
id: "assistant-welcome",
metadata: { model: "demo" },
})اگر Fixture به Prefix سفارشی یا زمان ثابت نیاز دارد، از Options مربوط به Chat استفاده کنید:
const chat = createChat({
messageIdPrefix: "demo-message",
toolCallIdPrefix: "demo-tool",
sourceIdPrefix: "demo-source",
now: "2026-01-01T00:00:00.000Z",
})IDهایی که در messages وجود دارند رزرو میشوند؛ بنابراین IDهای جدید بعد از آنها تولید خواهند شد.
بخشهای قبلی جریانهای اصلی استفاده را پوشش میدهند. این بخش تمام Exportها و Options عمومی را فهرست میکند.
createChat()یک مکالمهٔ Typed ایجاد میکند و رابط Fluent مربوط به Chat را برمیگرداند:
function createChat<UI_MESSAGE extends UIMessage = UIMessage>(
options?: CreateChatOptions<UI_MESSAGE>
): AiSdkChat<UI_MESSAGE>این Type ساختار Metadata، Partهای data-* و تعریف Toolها را مشخص میکند:
type ChatMessage = UIMessage<Metadata, DataParts, Tools>IDهای موجود در messages رزرو میشوند و IDهای جدید پس از Transcript موجود تولید خواهند شد.
AiSdkChatهر متدی که محتوا اضافه میکند، همان Chat را برمیگرداند؛ بنابراین میتوانید متدها را بهصورت زنجیرهای استفاده کنید.
فراخوانی user() یا assistant() بدون محتوا از مقدار "Summarize the uploaded receipt." استفاده میکند.
فراخوانی error() بدون پیام از "An error occurred." استفاده میکند.
get(count) در صورت منفی بودن count یا عدد صحیح نبودن آن، خطا ایجاد میکند.
user()ساختار FilePayload:
type FilePayload = {
type?: "file"
mediaType: string
url: string
filename?: string
providerMetadata?: Record<string, unknown>
}assistant()assistant()transport()برای جزئیات مربوط به Matching، Fallback، Abort و Streaming، بخش Transport را ببینید.
writer داخل assistant(({ writer }) => {}) و Callbackهای Fallback در دسترس است.
فراخوانی text() بدون محتوا از "Summarize the uploaded receipt." استفاده میکند.
فراخوانی reasoning() بدون محتوا از "I need to inspect the available context before answering." استفاده میکند.
فراخوانی error() بدون پیام از "An error occurred." استفاده میکند.
tool() یک Handle با متدهای sleep(delayMs)، output(value)، error(errorText?) و denied() برمیگرداند.
وقتی چرخهٔ Tool بین Input و Result به رویدادهای بیشتری نیاز دارد، از این Handle استفاده کنید.
فراخوانی tool.error() بدون پیام از "Tool call failed." استفاده میکند.
متدهای Resolution روی یک Tool دارای needsApproval خطا ایجاد میکنند.
writer.data({
type: "data-name",
id: "optional-id",
data: value,
transient: false,
})ارسال دوبارهٔ همان type و id، Data Part قبلی را جایگزین میکند.
یک Part با transient: true برای Client Stream میشود، اما در پیام نهایی برگشتی از get() قرار نمیگیرد.
این متدها برای Fieldهای حذفشده، مقدارهای پیشفرض نمونه ارائه میکنند. وقتی مقدار دقیق برای Component یا Test اهمیت دارد، مقدارهای موردنظر خود را صریحاً مشخص کنید.