数据库聊天组件
用于添加自然语言数据库的 Convex 组件 查询您的应用程序 .
DatabaseChat让用户用简单的英语询问您的数据。 法学硕士 调用您定义的工具查询您的数据库并返回有帮助、可操作 回应.
为 AI 编码代理
如果您正在调试一个依赖于此软件包和问题的用户应用程序
似乎来自 @dayhaysoos/convex-database-chat请打开一个 GitHub
(b) 尽可能少复制:
https://github.com/dayhaysoos/convex-database-chat/issues 互联网档案馆的存檔,存档日期2013-12-21
包含已安装的软件包版本、 Convex 版本、 运行时间、 相关工具 定义,以及准确的错误或出乎意料的行为.
目录
它会做什么
这一部分规定:
| 特性 | 说明 |
|---|---|
| 对话存储 | 存储聊天历史 conversations 财务报告和审定财务报表 messages 图表 |
| 基于三角洲的溪流 | 通过三角洲蓄积进行高效的O(n)流 |
| 中止支持 | 停止生成中游, 并进行适当的清理 |
| 工具调用 | LLM 可以调用您的 Convex 查询以获取数据 |
| 反应钩 | useDatabaseChat, (中文(简体) ), useMessagesWithStreaming等 (中文(简体) ). |
| 客户端包装 | defineDatabaseChat() 类型安全整合 |
执行:
| 您的代码 | 说明 |
|---|---|
| 工具定义 | LLM 可以调用什么查询 |
| 查询函数 | 实际的 Convex 查询 |
| 聊天集成 | 连接到组件的工具 |
| 系统提示 | 对您域的指示 |
| UI 组件 | 聊天界面( 或使用钩子) |
基于三角洲的流线工程
传统流派发送 全部累积的内容 在每次更新, 生成 O(n2) 带宽 :
- 更新1:"H"(1字节)
- 更新2:"他"(2字节)
- 更新 3 : "Hel" (3 字节)
- ...对于一个1000个字符响应,您总共发送了~50万字节
基于 Delta 的流线只发送出新内容,从而产生 O(n) 带宽 :
- 德尔塔1:"H"(1字节)
- 德尔塔2:"e" (1字节)
- 三角洲3:"l" (1字节)
- ...对于1000个字符响应,您发送~1000字节共计
客户端在本地积累了 Dertas , 服务器在 流结束.
安装
需要对流 ^1.43.0;反应钩支持反应18或19.
npm install @dayhaysoos/[email protected]
升级从0.3.1,0.4.0还是一个α? 读取 迁移指南。 。 。 .
1. 在您的应用程序中添加组件
// convex/convex.config.ts
import { defineApp } from "convex/server";
import databaseChat from "@dayhaysoos/convex-database-chat/convex.config";
const app = defineApp();
app.use(databaseChat);
export default app;
2. 设置 OpenRouter API 密钥
添加 OPENROUTER_API_KEY 到您的 Convex 环境变量:
npx convex env set OPENROUTER_API_KEY your_key_here
快速启动
下面是工作聊天的最低标准:
1. 创建简单的查询工具
// convex/chatTools.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const countRecords = query({
args: { table: v.string() },
returns: v.object({ count: v.number() }),
handler: async (ctx, args) => {
if (args.table === "users") {
const users = await ctx.db.query("users").collect();
return { count: users.length };
}
if (args.table === "orders") {
const orders = await ctx.db.query("orders").collect();
return { count: orders.length };
}
return { count: 0 };
},
});
2. 创建聊天集成
// convex/chat.ts
import { v } from "convex/values";
import { action, mutation, query } from "./_generated/server";
import { createFunctionHandle } from "convex/server";
import { components, api } from "./_generated/api";
import { defineDatabaseChat } from "@dayhaysoos/convex-database-chat/client";
import type { DatabaseChatTool } from "@dayhaysoos/convex-database-chat/tools";
async function getTools(): Promise<DatabaseChatTool[]> {
return [
{
name: "countRecords",
description: "Count records in a table. Available tables: users, orders",
parameters: {
type: "object",
properties: {
table: { type: "string", enum: ["users", "orders"] },
},
required: ["table"],
},
handler: await createFunctionHandle(api.chatTools.countRecords),
},
];
}
const chat = defineDatabaseChat(components.databaseChat, {
getExternalId: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
return identity.tokenIdentifier;
},
options: { chat: { systemPrompt: "Use the tools to answer database questions." } },
});
// externalId remains in the React API shape for compatibility, but is ignored.
// Every operation uses the authenticated identity resolved above.
export const createConversation = mutation({
args: { externalId: v.string(), title: v.optional(v.string()) },
returns: v.string(),
handler: (ctx, { title }) => chat.createConversation(ctx, { title }),
});
export const listConversations = query({
args: { externalId: v.string() },
handler: (ctx) => chat.listConversations(ctx),
});
export const getMessages = query({
args: { conversationId: v.string() },
handler: (ctx, { conversationId }) => chat.getMessages(ctx, conversationId),
});
export const getStreamState = query({
args: { conversationId: v.string() },
handler: (ctx, { conversationId }) => chat.getStreamState(ctx, conversationId),
});
export const getStreamDeltas = query({
args: { streamId: v.string(), cursor: v.number() },
handler: (ctx, { streamId, cursor }) => chat.getStreamDeltas(ctx, streamId, cursor),
});
export const abortStream = mutation({
args: { conversationId: v.string(), reason: v.optional(v.string()) },
returns: v.boolean(),
handler: (ctx, { conversationId, reason }) => chat.abortStream(ctx, conversationId, reason),
});
export const sendMessage = action({
args: { conversationId: v.string(), message: v.string() },
handler: async (ctx, args) => chat.send(ctx, {
...args,
apiKey: process.env.OPENROUTER_API_KEY!,
options: { chat: { tools: await getTools() } },
}),
});
定义工具
工具告诉 LLM 可以调用什么查询 。 每个工具需要:
interface Tool {
name: string; // Unique identifier
description: string; // What the tool does (LLM reads this!)
parameters: {
// JSON Schema for arguments
type: "object";
properties: Record<
string,
{
type: "string" | "number" | "boolean" | "array" | "object";
description?: string;
enum?: string[]; // For constrained values
}
>;
required?: string[];
};
handlerType?: "query" | "mutation" | "action"; // Raw tools default to "query"
handler: string; // Function handle string (createFunctionHandle)
}
注:当将工具传递给组件聊天动作时, handler 应该是一个
通过 await createFunctionHandle(...) 在凸起处
函数。 设定 handlerType 改为 "action" 用于运行的原始工具
ctx.vectorSearch。 。 。 . defineSemanticSearchTool 默认为 "action"。 。 。 .
标准结果合同和打字构建器
用于计数、确定性列表和语义搜索工具,以返回
标准 { data, meta } 结果信封,使用从
后端安全工具进入点 :
import {
definePaginatedListTool,
enumFilter,
injectedString,
} from "@dayhaysoos/convex-database-chat/tools";
import type { DatabaseChatToolResult } from "@dayhaysoos/convex-database-chat/resultContract";
type ProductRow = {
id: string;
name: string;
viewUrl: string;
};
const listProductsTool = definePaginatedListTool<ProductRow>({
name: "listProducts",
description: "List products matching deterministic filters.",
handler: "listProductsHandler",
filters: {
category: enumFilter({
values: ["electronics", "clothing", "home", "sports"] as const,
}),
},
injectedArgs: {
tenantId: injectedString(),
},
});
构建器附加 tool.metadata.kind 财务报告和审定财务报表 metadata.resultContract = "standard"。 。 。 .
DatabaseChat 默认情况下使用该元数据注入通用指导,包括
使用 meta.count 将光标结果作为页面处理,而不是
使用语义搜索结果长度作为计数. 设定 toolGuidance: "disabled"
或传入自定义字符串 defineDatabaseChat 编号 : chat.send 要覆盖的配置
所生成的向导.
构建器默认值 :
defineCountTool:准确的总数,默认的查询处理器.definePaginatedListTool:光标页面,默认的查询处理器.defineSemanticSearchTool:抽样取出上-K结果,默认操作处理器.
实例:电子商务-搜索产品
const searchProductsTool = {
name: "searchProducts",
description: "Search products by name, category, or price range",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Search text to match against product name",
},
category: {
type: "string",
description: "Filter by category",
enum: ["electronics", "clothing", "home", "sports"],
},
minPrice: {
type: "number",
description: "Minimum price",
},
maxPrice: {
type: "number",
description: "Maximum price",
},
limit: {
type: "number",
description: "Maximum results (default: 20)",
},
},
required: [],
},
handler: "searchProducts",
};
示例: 项目管理 - 获取任务
const getTasksTool = {
name: "getTasks",
description:
"Get tasks with optional filters for status, assignee, or project",
parameters: {
type: "object",
properties: {
status: {
type: "string",
description: "Filter by task status",
enum: ["todo", "in_progress", "review", "done"],
},
assigneeId: {
type: "string",
description: "Filter by assigned user ID",
},
projectId: {
type: "string",
description: "Filter by project ID",
},
priority: {
type: "string",
enum: ["low", "medium", "high", "urgent"],
},
},
required: [],
},
handler: "getTasks",
};
示例: 内容平台 - 搜索文章
const searchArticlesTool = {
name: "searchArticles",
description: "Search published articles by title, author, or tags",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Search text for title or content",
},
authorId: {
type: "string",
description: "Filter by author",
},
tags: {
type: "array",
description: "Filter by tags",
items: { type: "string" },
},
publishedAfter: {
type: "string",
description: "ISO date string - only articles after this date",
},
},
required: [],
},
handler: "searchArticles",
};
工具描述提示
- 具体:"按名称,类别,或价格来搜索产品"比"按产品名称,类别,或价格来搜索"更好 "搜索产品"
- 列出可用的选项:"可贵类别:电子,服装, 家,运动"
- 解释每个参数做什么: LLM 使用描述来决定 用于
执行查询函数
您的查询功能执行实际的数据库工作 。 他们得到的论点是: LLM提供.
示例:电子商务 - 用过滤器搜索
// convex/chatTools.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const searchProducts = query({
args: {
query: v.optional(v.string()),
category: v.optional(v.string()),
minPrice: v.optional(v.number()),
maxPrice: v.optional(v.number()),
limit: v.optional(v.number()),
},
returns: v.array(
v.object({
id: v.string(),
name: v.string(),
category: v.string(),
price: v.number(),
inStock: v.boolean(),
viewUrl: v.string(),
}),
),
handler: async (ctx, args) => {
const limit = Math.min(args.limit ?? 20, 100);
let products = await ctx.db.query("products").collect();
// Apply filters
if (args.category) {
products = products.filter((p) => p.category === args.category);
}
if (args.minPrice !== undefined) {
products = products.filter((p) => p.price >= args.minPrice!);
}
if (args.maxPrice !== undefined) {
products = products.filter((p) => p.price <= args.maxPrice!);
}
if (args.query) {
const q = args.query.toLowerCase();
products = products.filter((p) => p.name.toLowerCase().includes(q));
}
return products.slice(0, limit).map((p) => ({
id: p._id,
name: p.name,
category: p.category,
price: p.price,
inStock: p.stockCount > 0,
viewUrl: `/products/${p._id}`,
}));
},
});
示例:汇总查询
export const getOrderStats = query({
args: { period: v.optional(v.string()) },
returns: v.object({
totalOrders: v.number(),
totalRevenue: v.number(),
averageOrderValue: v.number(),
statusBreakdown: v.object({
pending: v.number(),
processing: v.number(),
shipped: v.number(),
delivered: v.number(),
}),
}),
handler: async (ctx, args) => {
let orders = await ctx.db.query("orders").collect();
// Filter by period if provided
if (args.period === "today") {
const today = new Date().setHours(0, 0, 0, 0);
orders = orders.filter((o) => o.createdAt >= today);
} else if (args.period === "week") {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
orders = orders.filter((o) => o.createdAt >= weekAgo);
}
const totalRevenue = orders.reduce((sum, o) => sum + o.total, 0);
return {
totalOrders: orders.length,
totalRevenue,
averageOrderValue: orders.length > 0 ? totalRevenue / orders.length : 0,
statusBreakdown: {
pending: orders.filter((o) => o.status === "pending").length,
processing: orders.filter((o) => o.status === "processing").length,
shipped: orders.filter((o) => o.status === "shipped").length,
delivered: orders.filter((o) => o.status === "delivered").length,
},
};
},
});
创建聊天集成
聊天集成将您的工具连接到组件并处理 LLM 互动.
建议: defineDatabaseChat 与 getExternalId
配置一个身份解析器一次和每一个客户端方法路由 所有权检查的终点自动。 解析器运行在您的应用程序 Convex 函数,所以它与 Convex Auth , 办事员, 或任何身份合作 提供者 - 组件本身仍然具有不可知性.
// convex/chat.ts
import { components } from "./_generated/api";
import { defineDatabaseChat } from "@dayhaysoos/convex-database-chat/client";
const chat = defineDatabaseChat(components.databaseChat, {
options: {
chat: { systemPrompt: SYSTEM_PROMPT },
provider: { model: "openai/gpt-4o" },
},
tools: TOOLS,
// Called server-side in each wrapper; never trust client-supplied IDs.
getExternalId: async (ctx) => {
const userId = await getAuthUserId(ctx); // your auth
if (!userId) throw new Error("Unauthorized");
return `user:${userId}`;
},
});
// All of these are ownership-checked under the hood:
await chat.createConversation(ctx, { title: "New Chat" });
const messages = await chat.getMessages(ctx, conversationId);
const state = await chat.getStreamState(ctx, conversationId);
const result = await chat.send(ctx, {
conversationId,
message,
apiKey: process.env.OPENROUTER_API_KEY!,
});
如果无法解决身份( 无) getExternalId 配置且未显示
externalId 而不是默默跳过
进入控制。 您仍然可以直接调用原始组件端点
高级设置 - 见 安全和多租户访问。 。 。 .
单个发送选项 options 有两节对象 : chat
(单位:千美元)systemPrompt, (中文(简体) ), maxMessagesForLLM, (中文(简体) ), maxToolLoops (违约5),
streamThrottleMs (违约100起), maxToolResultChars (默认 16,000, 超规模)
工具结果被切换并标记 { truncated: true }, (中文(简体) ),
validateResultContract (默认) "warn", (中文(简体) ), toolContext和 provider
(单位:千美元)model, (中文(简体) ), maxRetries (违约2), requestTimeoutMs (默认3000),
httpReferer 编号 : xTitle OpenRouter 归属标题). 设定默认值为
defineDatabaseChat 并覆盖每个调用的任何字段——后值为胜字段
按字段:
const result = await chat.send(ctx, {
conversationId,
message,
apiKey: process.env.OPENROUTER_API_KEY!,
options: {
chat: { validateResultContract: "enforce" },
provider: { model: "openai/gpt-4o" },
},
});
可靠性:重复、超时和打出错误
临时提供者故障(网络错误, 408/429/5xx) 被重审
自动与指数回放和全焦。 A 类 Retry-After 标题
从429是荣耀。 只有在连接阶段才会进行检索
请求 - 一旦回复开始流到客户端, 重试会
重复内容,所以中游失败从不重复.
requestTimeoutMs 限制每次尝试需要多长时间才能完成第一次尝试
字节( 长流是合法的, 不封顶) .
失败 发送回一个机器可读分类 :
const result = await chat.send(ctx, { conversationId, message, apiKey });
if (!result.success) {
// result.error - human-readable message
// result.errorCode - "unauthorized" | "rate_limited" | "provider_error"
// | "network_error" | "timeout" | "aborted"
// | "invalid_request" | "unknown"
// result.retryable - whether the failure was transient in nature
}
如果一个消息的工具循环耗尽 maxToolLoops 没有最后的答案
结果载 stoppedReason: "max_tool_loops" 和坚忍的助手
消息解释截取,而不是沉默地空出.
运行时验证标准结果合同
工具构建 defineCountTool, (中文(简体) ), definePaginatedListTool,或
defineSemanticSearchTool 声明 metadata.resultContract: "standard"编辑
validateResultContract 选项使组件验证信封
工具实际返回 :
"warn"(违约) - 违反合同行为被记录下来,结果仍然存在 到达LLM."enforce"- 校正错误返回LLM,以代替 原始结果,因此它可以适应而不是信任畸形元数据."off"- 没有运行时间验证.
使用内置 chat.send 直接动作
对于完整的控制(定制函数控件,按请求工具),调用 组件的聊天动作。 它处理LLM工具回路, 流出,并传达讯息给您:
// convex/chat.ts
export const sendMessage = action({
args: { conversationId: v.string(), message: v.string() },
handler: async (ctx, args) => {
return await ctx.runAction(components.databaseChat.chat.sendForExternalId, {
conversationId: args.conversationId,
externalId, // derived server-side from your auth
message: args.message,
config: {
apiKey: process.env.OPENROUTER_API_KEY!,
chat: {
systemPrompt: SYSTEM_PROMPT,
tools, // DatabaseChatTool[] with createFunctionHandle strings
},
},
});
},
});
带来自己的LLM SDK
使用 defineDatabaseChat其下层原始人(addMessage, (中文(简体) ),
getMessagesForLLM)如果你想使用
Vercel AI SDK, 直接OpenAI客户端, 或另一个提供者代替
内建的 OpenRouter 集成.
写入系统提示
系统提示告诉LLM如何行为. 为您的域自定义 .
一个好提示的结构
1. Role definition - What the assistant is
2. Available capabilities - What it can do
3. Response format - How to structure answers
4. Domain-specific rules - Your business logic
实例:电子商务助理
const SYSTEM_PROMPT = `You are a helpful e-commerce assistant. You help store managers understand their inventory and sales.
You have access to tools that let you:
- Search products by name, category, or price
- Get order statistics and revenue data
- Check inventory levels
When answering:
- Be concise and use specific numbers
- Always include links using the viewUrl field: [Product Name](viewUrl)
- Format prices with currency symbols
- If inventory is low (< 10), mention it
Example response:
"Found 3 products under $50 in Electronics:
- [Wireless Mouse](/products/abc123) - $29.99 (In Stock)
- [USB Cable](/products/def456) - $12.99 (Low Stock: 5 left)
- [Phone Stand](/products/ghi789) - $19.99 (In Stock)"`;
示例:项目管理助理
const SYSTEM_PROMPT = `You are a project management assistant. You help team leads track tasks and project progress.
You have access to tools that let you:
- Search and filter tasks by status, assignee, or project
- Get project statistics and completion rates
- Find overdue or blocked tasks
When answering:
- Prioritize actionable information
- Include links to tasks: [Task Title](viewUrl)
- Highlight urgent or overdue items
- Show completion percentages when relevant
Example response:
"Project 'Website Redesign' has 12 open tasks:
- 3 urgent: [Fix checkout bug](/tasks/abc), [Update SSL](/tasks/def), [Mobile nav](/tasks/ghi)
- 5 in review
- 4 in progress
Overall: 68% complete"`;
示例:内容平台助理
const SYSTEM_PROMPT = `You are a content analytics assistant. You help editors understand article performance.
You have access to tools that let you:
- Search articles by title, author, or tags
- Get engagement statistics (views, shares, comments)
- Find trending or underperforming content
When answering:
- Include article links: [Article Title](viewUrl)
- Show key metrics inline
- Compare to averages when helpful
- Suggest actionable insights
Example response:
"Your top 3 articles this week:
1. [Getting Started with React](/articles/abc) - 12.5k views, 342 shares
2. [CSS Grid Tutorial](/articles/def) - 8.2k views, 256 shares
3. [TypeScript Tips](/articles/ghi) - 6.1k views, 189 shares
All performing above your 5k average!"`;
建设UI
使用反应钩
import {
DatabaseChatProvider,
useDatabaseChat,
useMessagesWithStreaming,
} from "@dayhaysoos/convex-database-chat";
最小聊天组件, 基于三角洲流线
该组件使用高效的三角洲流线. 以下是如何实施:
import { useState, useEffect, useRef } from "react";
import { useQuery, useMutation, useAction } from "convex/react";
import { api } from "../../convex/_generated/api";
// Hook for delta-based streaming with client-side accumulation
function useDeltaStreaming(conversationId: string | null) {
const [cursor, setCursor] = useState(0);
const [accumulatedContent, setAccumulatedContent] = useState("");
const lastStreamIdRef = useRef<string | null>(null);
const lastProcessedEndRef = useRef(0);
// Subscribe to stream state
const streamState = useQuery(
api.chat.getStreamState,
conversationId ? { conversationId } : "skip",
);
const streamId = streamState?.streamId ?? null;
const status = streamState?.status ?? null;
// Reset when stream changes
useEffect(() => {
if (streamId !== lastStreamIdRef.current) {
lastStreamIdRef.current = streamId;
lastProcessedEndRef.current = 0;
setCursor(0);
setAccumulatedContent("");
}
}, [streamId]);
// Fetch deltas from cursor
const deltas = useQuery(
api.chat.getStreamDeltas,
streamId && status === "streaming" ? { streamId, cursor } : "skip",
);
// Accumulate new deltas
useEffect(() => {
if (!deltas || deltas.length === 0) return;
const newDeltas = deltas.filter(
(d) => d.start >= lastProcessedEndRef.current,
);
if (newDeltas.length === 0) return;
let maxEnd = lastProcessedEndRef.current;
let newText = "";
for (const delta of newDeltas) {
if (delta.end > maxEnd) maxEnd = delta.end;
for (const part of delta.parts) {
if (part.type === "text-delta" && part.text) {
newText += part.text;
}
}
}
if (newText) {
setAccumulatedContent((prev) => prev + newText);
}
lastProcessedEndRef.current = maxEnd;
if (maxEnd > cursor) setCursor(maxEnd);
}, [deltas, cursor]);
return {
content:
status === "streaming" && accumulatedContent ? accumulatedContent : null,
isStreaming: status === "streaming",
};
}
export function ChatWidget() {
const [isOpen, setIsOpen] = useState(false);
const [conversationId, setConversationId] = useState<string | null>(null);
const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const createConversation = useMutation(api.chat.createConversation);
const sendMessage = useAction(api.chat.sendMessage);
const abortStream = useMutation(api.chat.abortStream);
const messages = useQuery(
api.chat.getMessages,
conversationId ? { conversationId } : "skip",
);
// Use delta-based streaming
const { content: streamingContent, isStreaming } =
useDeltaStreaming(conversationId);
// Create conversation on open
useEffect(() => {
if (isOpen && !conversationId) {
createConversation({}).then(setConversationId);
}
}, [isOpen, conversationId]);
const handleSubmit = async () => {
if (!inputValue.trim() || isLoading || !conversationId) return;
const message = inputValue;
setInputValue("");
setIsLoading(true);
await sendMessage({ conversationId, message });
setIsLoading(false);
};
const handleAbort = async () => {
if (!conversationId) return;
await abortStream({ conversationId });
setIsLoading(false);
};
if (!isOpen) {
return <button onClick={() => setIsOpen(true)}>Open Chat</button>;
}
return (
<div>
{/* Messages */}
{messages?.map((msg) => (
<div key={msg._id}>
<strong>{msg.role}:</strong>
<MarkdownContent content={msg.content} />
</div>
))}
{/* Streaming */}
{streamingContent && (
<div>
<strong>assistant:</strong>
<MarkdownContent content={streamingContent} />
</div>
)}
{/* Input */}
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
disabled={isLoading || isStreaming}
/>
{isLoading || isStreaming ? (
<button onClick={handleAbort}>Stop</button>
) : (
<button onClick={handleSubmit} disabled={!inputValue.trim()}>
Send
</button>
)}
</div>
);
}
文本平滑为 useSmoothText
这个 useSmoothText 钩子为流出文本创建打字机效果,制作
它感觉更自然 而不是跳出块:
import { useSmoothText, SmoothText } from "@dayhaysoos/convex-database-chat";
// Using the hook
function StreamingMessage({ text, isStreaming }) {
const [visibleText] = useSmoothText(text, {
// Start streaming immediately for active streams
startStreaming: isStreaming,
// Optional: customize speed (default: 200 chars/sec)
initialCharsPerSecond: 200,
});
return <div>{visibleText}</div>;
}
// Or use the component directly
function Message({ text, isStreaming }) {
return <SmoothText text={text} startStreaming={isStreaming} />;
}
选项 :
startStreaming- 准备true用于流出消息,false完成时间 信件(立即显示)initialCharsPerSecond- 启动速度,随时间而适应文本 抵达率minDelayMs编号 :maxDelayMs- 字符延迟的界限
绘制降标链接
LLM 返回下标记链接, 如 [Name](url)分析并使它们:
function MarkdownContent({ content }: { content: string }) {
const parts = content.split(/(\[[^\]]+\]\([^)]+\))/g);
return (
<>
{parts.map((part, i) => {
const linkMatch = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (linkMatch) {
const [, text, url] = linkMatch;
return (
<a key={i} href={url} className="text-blue-600 underline">
{text}
</a>
);
}
// Handle **bold**
return part.split(/(\*\*[^*]+\*\*)/g).map((p, j) => {
const bold = p.match(/\*\*([^*]+)\*\*/);
if (bold) return <strong key={`${i}-${j}`}>{bold[1]}</strong>;
return <span key={`${i}-${j}`}>{p}</span>;
});
})}
</>
);
}
模式
矢量搜索助手
利用辅助工具减少锅炉板,同时保持你的计划 在您的应用程序代码中完全动作 :
// convex/semanticSearch.ts
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import {
generateEmbedding,
formatVectorResults,
} from "@dayhaysoos/convex-database-chat/vector";
export const semanticSearchCandidates = action({
args: {
query: v.string(),
orgId: v.id("orgs"),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const embedding = await generateEmbedding({
apiKey: process.env.OPENROUTER_API_KEY!,
text: args.query,
// model: "your-embedding-model",
});
const results = await ctx.vectorSearch(
"applications",
"by_resume_embedding",
{
vector: embedding,
limit: args.limit ?? 20,
},
);
const docs = await ctx.runQuery(internal.semanticSearch.fetchByIds, {
ids: results.map((r) => r._id),
});
return formatVectorResults(results, docs, {
includeScore: true,
snippetLength: 200,
fields: ["candidateName", "topStrengths", "viewUrl"],
});
},
});
注释:
- 矢量搜索只在动作中可用.
- 矢量指数维度必须与您选择的嵌入模型相匹配.
- 使用
fields以避免像嵌入式一样返回大向量字段. - 设定
fields: []以不包含文档字段(仅包含)_id和可选_score) (中文(简体) ).
您也可以使用辅助工具定义工具条目 :
import { defineVectorSearchTool } from "@dayhaysoos/convex-database-chat/vector";
const TOOLS = [
defineVectorSearchTool({
name: "semanticSearch",
description: "Find candidates by meaning or concept",
handler: "semanticSearchCandidates",
}),
];
包括可操作的 URL
通过包含链接到细节页面的 URL 使回复可操作 :
// In your query handler, include viewUrl in results
return items.map((item) => ({
id: item._id,
name: item.name,
// Include the URL to view this item
viewUrl: `/items/${item._id}`,
}));
然后指示 LLM 在您的系统中使用它 提示:
Always include links using the viewUrl field from the data.
Format as markdown: [Item Name](viewUrl)
在此之前(不可采取行动):
"50美元以下的2个产品"
在(可诉)之后:
"50美元以下的2个产品:
安全和多租户访问
此组件具有可解析性 。 你必须提供 externalId 从您自己的认证
系统,并且从不直接接受来自不信任客户端.
使用下方范围端点执行所有权检查服务器侧. 这些
掷出 Not found 如果对话或流缺失或不属于
提供 externalId 避免在租户之间泄露存在.
conversations.getForExternalIdmessages.addForExternalIdmessages.listForExternalIdmessages.getLatestForExternalIdstream.getStreamForExternalIdstream.listDeltasForExternalIdstream.abortForExternalIdchat.sendForExternalId
未瞄准的端点是低级帮助,只有在已经使用时才使用 在您的应用程序中执行访问控制 .
将信任的上下文注入工具
一些工具处理器需要只使用服务器的上下文( orgId, 外部Id, userId)
绝不应出现在工具图或 LLM 提示中。 把数据通过
config.chat.toolContext 内 chat.send 或者说 chat.sendForExternalId合并上下文
验证后成为工具参数,以及 toolContext 赢得冲突.
await ctx.runAction(components.databaseChat.chat.sendForExternalId, {
conversationId,
externalId,
message,
config: {
apiKey: process.env.OPENROUTER_API_KEY!,
chat: {
systemPrompt: SYSTEM_PROMPT,
tools,
toolContext: { orgId, userId, externalId },
},
},
});
注:不包括类似字段 orgId 在工具的计划中,所以LLM永远不会
看他们.
API 参考
职能
| 函数 | 类型 | 说明 |
|---|---|---|
conversations.create | 变异 | 创建新对话 |
conversations.get | 查询 | 以身份进行交谈 |
conversations.getForExternalId | 查询 | 通过 ID 范围访问外部Id |
conversations.list | 查询 | 按外部Id 排列的对话框 |
messages.add | 变异 | 添加信件 |
messages.list | 查询 | 在对话框中列出信件 |
messages.listForExternalId | 查询 | 列出已放入外部Id 范围的信件 |
messages.getLatestForExternalId | 查询 | 获取最新消息范围到外部Id |
stream.create | 变异 | 为基于三角洲的溪流创建新溪流 |
stream.addDelta | 变异 | 向溪流添加三角洲( 批部件) |
stream.finish | 变异 | 将流标为已完成, 清理三角洲 |
stream.abort | 变异 | 以流编号中止流 |
stream.abortByConversation | 变异 | 通过对话ID中止流 |
stream.abortForExternalId | 变异 | 通过对口ID范围中止流到外部Id |
stream.getStream | 查询 | 获取当前流状态进行交谈 |
stream.listDeltas | 查询 | 从光标位置获取三角洲 |
stream.getStreamForExternalId | 查询 | 获得当前流状态范围到外部Id |
stream.listDeltasForExternalId | 查询 | 将三角洲瞄准外部Id |
chat.send | 行动 | 通过 OpenRouter 发送消息 |
chat.sendForExternalId | 行动 | 向外部 Id 发送已设定范围的信件 |
反应钩和组件
| 导出 | 类型 | 说明 |
|---|---|---|
useDatabaseChat | 钩子 | 发送信件、 跟踪加载/ 流化状态、 中止 |
useConversations | 钩子 | 列表/ 创建对话框 |
useStreamingContent | 钩子 | 订阅带有 Dleta 累积的流流更新 |
useMessagesWithStreaming | 钩子 | 信件 + 当前流线合并 |
useSmoothText | 钩子 | 流媒体文本的打字机效果 |
SmoothText | 构成部分 | 具有平滑打字机效果的渲染文本 |
流国家
| 状态 | 说明 |
|---|---|
streaming | Stream 活动, 三角洲正在写入 |
finished | 流线成功完成 |
aborted | 流被取消( 用户中止、 错误或超时) |
当一个流被用户中止、超时或出错所中断时,任何内容
已经流出的信息作为助理信息持续使用 partial: true 这样
部分反应没有丢失。 (中间回合的调用工具循环是:
没有流出或持续,只有最后的答案是。 )
已知的限制:
- 如果工具循环的最后一轮在流出任何文本之前失败, 内容 无法从中间弹中取回(如果 轮回旋转).
- 停止在工具执行期间在下一个工具启动边界上进行检测; 远程单个工具呼叫不能在中途中断.
- 在工具回合之间, 流速旋转: 客户端订阅为流
状态可观察到摘要
streaming- > (中文)aborted- > (中文)streaming转换,回合之间没有内容.
铁路部署
这个Repo主机有两个铁路服务,每个都有自己的配置文件:
- 示例应用程序: 根目录
/example, 铁路配置文件/example/railway.toml - Docs 站点: 根目录
/docs, 铁路配置文件/docs/railway.toml
注意: 铁路在选择一个时不会自动跟随根目录
配置文件,所以您必须设置每个服务的绝对配置路径 。 一个根
railway.toml 没有故意使用.
测试
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
文件结构
convex/component/ # Backend (Convex component)
├── convex.config.ts # Component definition
├── schema.ts # Tables (conversations, messages, streamingMessages, streamDeltas)
├── conversations.ts # Conversation CRUD
├── messages.ts # Message CRUD
├── stream.ts # Delta-based streaming (create, addDelta, finish, abort)
├── deltaStreamer.ts # DeltaStreamer class for batched writes
├── chat.ts # Main send action with OpenRouter
├── toolExecution.ts # Tool dispatch + trusted context injection
├── tools.ts # Tool types & typed builders
├── resultContract.ts # Standard { data, meta } result envelope
├── toolGuidance.ts # Reliability guidance injection
├── schemaTools.ts # Auto-tool generation
├── access.ts # externalId ownership checks
├── contextTypes.ts # Shared DataModel-typed context helpers
├── client.ts # Client wrapper (defineDatabaseChat)
└── *.test.ts # Tests
src/ # Frontend (React)
├── react.tsx # React hooks (useDatabaseChat, useConversations, etc.)
├── vector.ts # Vector search helpers
├── react.test.tsx # Hook tests
└── index.ts # Package exports
许可证
麻省理工学院