From e7296df78f1a1e31e1ba431602b8daf9eeec3a49 Mon Sep 17 00:00:00 2001 From: AnotiaWang Date: Fri, 14 Feb 2025 15:20:02 +0800 Subject: [PATCH] feat: support reasoning models like DeepSeek R1 --- .dockerignore | 30 ++++ README.md | 24 ++- README_zh.md | 20 ++- components/DeepResearch.vue | 39 ++++- components/ReasoningAccordion.vue | 52 ++++++ components/ResearchFeedback.vue | 37 ++-- components/ResearchForm.vue | 2 +- components/ResearchReport.vue | 40 +++-- components/Tree.vue | 8 +- composables/useAiProvider.ts | 10 +- i18n/en.json | 3 + i18n/zh.json | 5 +- lib/deep-research.ts | 83 +++++++-- lib/feedback.ts | 5 +- package.json | 19 +- pnpm-lock.yaml | 277 ++++++++++++++++++++---------- utils/json.ts | 66 +++++-- 17 files changed, 549 insertions(+), 171 deletions(-) create mode 100644 .dockerignore create mode 100644 components/ReasoningAccordion.vue diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d9aaa47 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +# TypeScript +*.tsbuildinfo + +# Nuxt generate +.generate diff --git a/README.md b/README.md index 73eaff0..9eb3619 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ Features: - 🚀 **Safe & Secure**: Everything (config, API requests, ...) stays in your browser locally - 🕙 **Realtime feedback**: Stream AI responses and reflect on the UI in real-time -- 🌳 **Search visualization**: Shows the research process using a tree structure +- 🌳 **Search visualization**: Shows the research process using a tree structure. Supports searching in different languages - 📄 **Export as PDF**: Export the final research report as a PDF -- 🌐 **Search in different languages**: Useful when you want to get search results in a different language - 🤖 **Supports more models**: Uses plain prompts instead of newer, less widely supported features like Structured Outputs. This ensures to work with more providers that haven't caught up with the latest OpenAI capabilities. +- 🐳 **Docker support**: Deploy in your environment in one-line command Currently available providers: @@ -20,7 +20,25 @@ Currently available providers: Please give a 🌟 Star if you like this project! - + + +## Recent updates + +25/02/14 + +- Supported reasoning models like DeepSeek R1 +- Improved compatibility with more models & error handling + +25/02/13 + +- Significantly reduced bundle size +- Supported searching in different languages +- Added Docker support +- Fixed "export as PDF" issues + +25/02/12 +- Added Chinese translation. The models will respond in the user's language. +- Various fixes ## How to use diff --git a/README_zh.md b/README_zh.md index e04a3bf..af0cb90 100644 --- a/README_zh.md +++ b/README_zh.md @@ -16,7 +16,25 @@ 喜欢本项目请点 ⭐ 收藏! - + + +## 最近更新 + +25/02/14 + +- 支持 DeepSeek R1 等思维链模型 +- 改进了模型兼容性,改进异常处理 + +25/02/13 + +- 大幅缩减了网页体积 +- 支持配置搜索时使用的语言 +- 支持 Docker 部署 +- 修复“导出 PDF”不可用的问题 + +25/02/12 +- 添加中文支持。模型会自动使用用户的语言回答了。 +- 修复一些 bug ## 使用指南 diff --git a/components/DeepResearch.vue b/components/DeepResearch.vue index 69df586..526dae5 100644 --- a/components/DeepResearch.vue +++ b/components/DeepResearch.vue @@ -38,6 +38,14 @@ } switch (step.type) { + case 'generating_query_reasoning': { + if (node) { + node.generateQueriesReasoning = + (node.generateQueriesReasoning ?? '') + step.delta + } + break + } + case 'generating_query': { if (!node) { // 创建新节点 @@ -86,10 +94,17 @@ break } + case 'processing_serach_result_reasoning': { + if (node) { + node.generateLearningsReasoning = + (node.generateLearningsReasoning ?? '') + step.delta + } + break + } + case 'processing_serach_result': { if (node) { node.learnings = step.result.learnings || [] - node.followUpQuestions = step.result.followUpQuestions || [] } break } @@ -157,6 +172,8 @@ selectedNode.value = undefined searchResults.value = {} isLoading.value = true + // Clear the root node's reasoning content + tree.value.generateQueriesReasoning = '' try { const searchLanguage = config.value.webSearch.searchLanguage ? t('language', {}, { locale: config.value.webSearch.searchLanguage }) @@ -211,20 +228,28 @@ {{ selectedNode.label ?? $t('webBrowsing.generating') }} + + + + +

+ {{ t('webBrowsing.researchGoal') }} +

{{ t('webBrowsing.startNode.description') }}

-
+
{{ $t('modelFeedback.waiting') }}
diff --git a/components/ResearchReport.vue b/components/ResearchReport.vue index 4504f9f..993fb09 100644 --- a/components/ResearchReport.vue +++ b/components/ResearchReport.vue @@ -16,6 +16,7 @@ const error = ref('') const loading = ref(false) const loadingExportPdf = ref(false) + const reasoningContent = ref('') const reportContent = ref('') const reportHtml = computed(() => marked(reportContent.value, { silent: true, gfm: true, breaks: true }), @@ -29,9 +30,20 @@ loading.value = true error.value = '' reportContent.value = '' + reasoningContent.value = '' try { - for await (const chunk of writeFinalReport(params).textStream) { - reportContent.value += chunk + for await (const chunk of writeFinalReport(params).fullStream) { + if (chunk.type === 'reasoning') { + reasoningContent.value += chunk.textDelta + } else if (chunk.type === 'text-delta') { + reportContent.value += chunk.textDelta + } else if (chunk.type === 'error') { + error.value = t('researchReport.error', [ + chunk.error instanceof Error + ? chunk.error.message + : String(chunk.error), + ]) + } } reportContent.value += `\n\n## ${t( 'researchReport.sources', @@ -158,21 +170,25 @@
+
{{ error }}
+ + +
- +
+ {{ + loading ? $t('researchReport.generating') : $t('researchReport.waiting') + }} +
diff --git a/components/Tree.vue b/components/Tree.vue index de3ce95..2003857 100644 --- a/components/Tree.vue +++ b/components/Tree.vue @@ -6,11 +6,15 @@ export type TreeNode = { id: string - /** Label, represents the search query */ + /** Label, represents the search query. Generated from parent node. */ label: string + /** The research goal of this node. Generated from parent node. */ researchGoal?: string + /** Reasoning content when generating queries for the next iteration. */ + generateQueriesReasoning?: string + /** Reasoning content when generating learnings for this iteration. */ + generateLearningsReasoning?: string learnings?: string[] - followUpQuestions?: string[] visitedUrls?: string[] status?: TreeNodeStatus children: TreeNode[] diff --git a/composables/useAiProvider.ts b/composables/useAiProvider.ts index 31fa575..c880098 100644 --- a/composables/useAiProvider.ts +++ b/composables/useAiProvider.ts @@ -1,14 +1,18 @@ -import { createOpenAI } from '@ai-sdk/openai' +import { createDeepSeek } from '@ai-sdk/deepseek' +import { extractReasoningMiddleware, wrapLanguageModel } from 'ai' export const useAiModel = () => { const config = useConfigStore() switch (config.config.ai.provider) { case 'openai-compatible': - const openai = createOpenAI({ + const deepseek = createDeepSeek({ apiKey: config.config.ai.apiKey, baseURL: config.aiApiBase, }) - return openai(config.config.ai.model) + return wrapLanguageModel({ + model: deepseek(config.config.ai.model), + middleware: extractReasoningMiddleware({ tagName: 'think' }), + }) default: throw new Error(`Unknown AI provider: ${config.config.ai.provider}`) } diff --git a/i18n/en.json b/i18n/en.json index 370302b..6c18bf8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,5 +1,8 @@ { "language": "English", + "modelThinking": "Thinking...", + "modelThinkingComplete": "Thinking complete", + "invalidStructuredOutput": "The model returned content which is incomplete or invalid and cannot be parsed. Try using a bigger or \"smarter\" model.", "index": { "projectDescription": "This is a web UI for {0} that allows AI to search online and dig deeper on its own based on specific questions, and then output a research report.\nThis project features streaming AI responses for realtime feedback, and visualization of the research process using a tree structure.\nAll API requests are directly sent from your browser. No remote data stored.", "missingConfigTitle": "Config not set", diff --git a/i18n/zh.json b/i18n/zh.json index 260e1e6..2e2e7d7 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -1,5 +1,8 @@ { "language": "中文", + "modelThinking": "思考中...", + "modelThinkingComplete": "思考完毕", + "invalidStructuredOutput": "模型返回的内容无效或不完整,无法解析。请尝试换一个更大或者更“聪明”的模型。", "index": { "projectDescription": "Deep Research 是 {0} 的可视化 UI,可以让 AI 根据特定问题联网搜索并自行深挖,并输出研究报告。\n本项目可以流式传输 AI 的回答来实时反馈,并使用树状结构可视化搜索过程。\n全部 API 请求都在浏览器本地完成。", "missingConfigTitle": "需要配置 API", @@ -45,7 +48,7 @@ "breadth": "研究广度 (Breadth)", "breadthHelp": "第一次迭代中的搜索次数。后续每轮迭代的搜索次数为上一轮的一半。", "start": "开始研究", - "researching": "正在研究..." + "researching": "研究中..." }, "modelFeedback": { "title": "2. 模型反馈", diff --git a/lib/deep-research.ts b/lib/deep-research.ts index ca13c8d..e5bf36c 100644 --- a/lib/deep-research.ts +++ b/lib/deep-research.ts @@ -29,6 +29,7 @@ export type PartialSearchResult = DeepPartial export type ResearchStep = | { type: 'generating_query'; result: PartialSearchQuery; nodeId: string } + | { type: 'generating_query_reasoning'; delta: string; nodeId: string } | { type: 'generated_query' query: string @@ -43,6 +44,11 @@ export type ResearchStep = result: PartialSearchResult nodeId: string } + | { + type: 'processing_serach_result_reasoning' + delta: string + nodeId: string + } | { type: 'processed_search_result' query: string @@ -110,7 +116,7 @@ export function generateSearchQueries({ '\n', )}` : '', - `You MUST respond in JSON with the following schema: ${jsonSchema}`, + `You MUST respond in JSON matching this JSON schema: ${jsonSchema}`, lp, ].join('\n\n') return streamText({ @@ -160,7 +166,7 @@ function processSearchResult({ `${contents .map((content) => `\n${content}\n`) .join('\n')}`, - `You MUST respond in JSON with the following schema: ${jsonSchema}`, + `You MUST respond in JSON matching this JSON schema: ${jsonSchema}`, languagePrompt(language), ].join('\n\n') @@ -234,6 +240,8 @@ export async function deepResearch({ /** Force the LLM to generate serp queries in a certain language */ searchLanguage?: string }): Promise { + const { t } = useNuxtApp().$i18n + try { const searchQueriesResult = generateSearchQueries({ query, @@ -246,12 +254,12 @@ export async function deepResearch({ let searchQueries: PartialSearchQuery[] = [] - for await (const parsedQueries of parseStreamingJson( - searchQueriesResult.textStream, + for await (const chunk of parseStreamingJson( + searchQueriesResult.fullStream, searchQueriesTypeSchema, (value) => !!value.queries?.length && !!value.queries[0]?.query, )) { - if (parsedQueries.queries) { + if (chunk.type === 'object' && chunk.value.queries) { for (let i = 0; i < searchQueries.length; i++) { onProgress({ type: 'generating_query', @@ -259,7 +267,27 @@ export async function deepResearch({ nodeId: childNodeId(nodeId, i), }) } - searchQueries = parsedQueries.queries + searchQueries = chunk.value.queries + } else if (chunk.type === 'reasoning') { + onProgress({ + type: 'generating_query_reasoning', + delta: chunk.delta, + nodeId, + }) + } else if (chunk.type === 'error') { + onProgress({ + type: 'error', + message: chunk.message, + nodeId, + }) + break + } else if (chunk.type === 'bad-end') { + onProgress({ + type: 'error', + message: t('invalidStructuredOutput'), + nodeId, + }) + break } } @@ -272,6 +300,7 @@ export async function deepResearch({ }) } + // Run in parallel and limit the concurrency const results = await Promise.all( searchQueries.map((searchQuery, i) => limit(async () => { @@ -287,6 +316,7 @@ export async function deepResearch({ nodeId: childNodeId(nodeId, i), }) try { + // Use Tavily to search the web const result = await useTavily().search(searchQuery.query, { maxResults: 5, }) @@ -314,18 +344,41 @@ export async function deepResearch({ }) let searchResult: PartialSearchResult = {} - for await (const parsedLearnings of parseStreamingJson( - searchResultGenerator.textStream, + for await (const chunk of parseStreamingJson( + searchResultGenerator.fullStream, searchResultTypeSchema, (value) => !!value.learnings?.length, )) { - searchResult = parsedLearnings - onProgress({ - type: 'processing_serach_result', - result: parsedLearnings, - query: searchQuery.query, - nodeId: childNodeId(nodeId, i), - }) + const id = childNodeId(nodeId, i) + if (chunk.type === 'object') { + searchResult = chunk.value + onProgress({ + type: 'processing_serach_result', + result: chunk.value, + query: searchQuery.query, + nodeId: id, + }) + } else if (chunk.type === 'reasoning') { + onProgress({ + type: 'processing_serach_result_reasoning', + delta: chunk.delta, + nodeId: id, + }) + } else if (chunk.type === 'error') { + onProgress({ + type: 'error', + message: chunk.message, + nodeId: id, + }) + break + } else if (chunk.type === 'bad-end') { + onProgress({ + type: 'error', + message: t('invalidStructuredOutput'), + nodeId: id, + }) + break + } } console.log( `Processed search result for ${searchQuery.query}`, diff --git a/lib/feedback.ts b/lib/feedback.ts index a125bee..29b9261 100644 --- a/lib/feedback.ts +++ b/lib/feedback.ts @@ -28,7 +28,7 @@ export function generateFeedback({ const jsonSchema = JSON.stringify(zodToJsonSchema(schema)) const prompt = [ `Given the following query from the user, ask ${numQuestions} follow up questions to clarify the research direction. Return a maximum of ${numQuestions} questions, but feel free to return less if the original query is clear: ${query}`, - `You MUST respond in JSON with the following schema: ${jsonSchema}`, + `You MUST respond in JSON matching this JSON schema: ${jsonSchema}`, languagePrompt(language), ].join('\n\n') @@ -37,12 +37,13 @@ export function generateFeedback({ system: systemPrompt(), prompt, onError({ error }) { + console.error(`generateFeedback`, error) throw error }, }) return parseStreamingJson( - stream.textStream, + stream.fullStream, feedbackTypeSchema, (value: PartialFeedback) => !!value.questions && value.questions.length > 0, ) diff --git a/package.json b/package.json index 0b9985e..0a5436a 100644 --- a/package.json +++ b/package.json @@ -12,27 +12,28 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@ai-sdk/openai": "^1.1.9", - "@ai-sdk/ui-utils": "^1.1.11", - "@ai-sdk/vue": "^1.1.11", + "@ai-sdk/deepseek": "^0.1.10", + "@ai-sdk/openai": "^1.1.11", + "@ai-sdk/ui-utils": "^1.1.14", + "@ai-sdk/vue": "^1.1.15", "@iconify-json/lucide": "^1.2.26", "@nuxt/ui": "3.0.0-alpha.12", "@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/i18n": "9.2.0", - "@pinia/nuxt": "^0.10.0", + "@pinia/nuxt": "^0.10.1", "@tailwindcss/typography": "^0.5.16", - "@tavily/core": "^0.0.3", - "ai": "^4.1.28", - "js-tiktoken": "^1.0.18", + "@tavily/core": "^0.3.1", + "ai": "^4.1.38", + "js-tiktoken": "^1.0.19", "jspdf": "^2.5.2", "marked": "^15.0.7", "nuxt": "^3.15.4", "p-limit": "^6.2.0", - "pinia": "^3.0.0", + "pinia": "^3.0.1", "tailwindcss": "^4.0.5", "vue": "latest", "vue-router": "latest", - "zod": "^3.24.1", + "zod": "^3.24.2", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 244d037..f8fb315 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,18 @@ importers: .: dependencies: + '@ai-sdk/deepseek': + specifier: ^0.1.10 + version: 0.1.10(zod@3.24.2) '@ai-sdk/openai': - specifier: ^1.1.9 - version: 1.1.9(zod@3.24.1) + specifier: ^1.1.11 + version: 1.1.11(zod@3.24.2) '@ai-sdk/ui-utils': - specifier: ^1.1.11 - version: 1.1.11(zod@3.24.1) + specifier: ^1.1.14 + version: 1.1.14(zod@3.24.2) '@ai-sdk/vue': - specifier: ^1.1.11 - version: 1.1.11(vue@3.5.13(typescript@5.7.3))(zod@3.24.1) + specifier: ^1.1.15 + version: 1.1.15(vue@3.5.13(typescript@5.7.3))(zod@3.24.2) '@iconify-json/lucide': specifier: ^1.2.26 version: 1.2.26 @@ -30,20 +33,20 @@ importers: specifier: 9.2.0 version: 9.2.0(@vue/compiler-dom@3.5.13)(eslint@9.20.1(jiti@2.4.2))(magicast@0.3.5)(rollup@4.34.6)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) '@pinia/nuxt': - specifier: ^0.10.0 - version: 0.10.0(magicast@0.3.5)(pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))) + specifier: ^0.10.1 + version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@4.0.6) '@tavily/core': - specifier: ^0.0.3 - version: 0.0.3 + specifier: ^0.3.1 + version: 0.3.1 ai: - specifier: ^4.1.28 - version: 4.1.28(react@19.0.0)(zod@3.24.1) + specifier: ^4.1.38 + version: 4.1.38(react@19.0.0)(zod@3.24.2) js-tiktoken: - specifier: ^1.0.18 - version: 1.0.18 + specifier: ^1.0.19 + version: 1.0.19 jspdf: specifier: ^2.5.2 version: 2.5.2 @@ -57,8 +60,8 @@ importers: specifier: ^6.2.0 version: 6.2.0 pinia: - specifier: ^3.0.0 - version: 3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + specifier: ^3.0.1 + version: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) tailwindcss: specifier: ^4.0.5 version: 4.0.6 @@ -69,11 +72,11 @@ importers: specifier: latest version: 4.5.0(vue@3.5.13(typescript@5.7.3)) zod: - specifier: ^3.24.1 - version: 3.24.1 + specifier: ^3.24.2 + version: 3.24.2 zod-to-json-schema: specifier: ^3.24.1 - version: 3.24.1(zod@3.24.1) + version: 3.24.1(zod@3.24.2) devDependencies: '@vueuse/core': specifier: ^12.5.0 @@ -84,14 +87,26 @@ importers: packages: - '@ai-sdk/openai@1.1.9': - resolution: {integrity: sha512-t/CpC4TLipdbgBJTMX/otzzqzCMBSPQwUOkYPGbT/jyuC86F+YO9o+LS0Ty2pGUE1kyT+B3WmJ318B16ZCg4hw==} + '@ai-sdk/deepseek@0.1.10': + resolution: {integrity: sha512-JCYFEYog365yFXXrbySAwuYfe6zHKuBPmdOIa3xl0LSE0p1GcvGkZn80lNPLLzqR8GO8S4qUZoH+HL1kqYHaYA==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/provider-utils@2.1.6': - resolution: {integrity: sha512-Pfyaj0QZS22qyVn5Iz7IXcJ8nKIKlu2MeSAdKJzTwkAks7zdLaKVB+396Rqcp1bfQnxl7vaduQVMQiXUrgK8Gw==} + '@ai-sdk/openai-compatible@0.1.10': + resolution: {integrity: sha512-AR5Acly7U64wDgVdVs9AqaMHgVps/35TMUMDS4akSZjsB4TdAhKnWdMXmACzCxmbCySHMoMnKY41XgvCyFN1pQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/openai@1.1.11': + resolution: {integrity: sha512-gyqjoRvycmN3OGeK2SJXwhROv2ZZuP+SXbiAOoJf0ehWkqwkQSVaHigmg6OYLznmXusVHAvYD7SRgysXoJmuog==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.1.8': + resolution: {integrity: sha512-1j9niMUAFlCBdYRYJr1yoB5kwZcRFBVuBiL1hhrf0ONFNrDiJYA6F+gROOuP16NHhezMfTo60+GeeV1xprHFjg==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -103,8 +118,8 @@ packages: resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==} engines: {node: '>=18'} - '@ai-sdk/react@1.1.11': - resolution: {integrity: sha512-vfjZ7w2M+Me83HTMMrnnrmXotz39UDCMd27YQSrvt2f1YCLPloVpLhP+Y9TLZeFE/QiiRCrPYLDQm6aQJYJ9PQ==} + '@ai-sdk/react@1.1.14': + resolution: {integrity: sha512-4Y2d37l52TzOZNgH2KxXiWkJc4R7Yr+1k0VryOpZslqFHK0cclinFgWlclzF6Qn8C1pNMdhhxSEx1/N7SQZeKQ==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -115,8 +130,8 @@ packages: zod: optional: true - '@ai-sdk/ui-utils@1.1.11': - resolution: {integrity: sha512-1SC9W4VZLcJtxHRv4Y0aX20EFeaEP6gUvVqoKLBBtMLOgtcZrv/F/HQRjGavGugiwlS3dsVza4X+E78fiwtlTA==} + '@ai-sdk/ui-utils@1.1.14': + resolution: {integrity: sha512-JQXcnPRnDfeH1l503s/8+SxJdmgyUKC3QvKjOpTV6Z/LyRWJZrruBoZnVB1OrL9o/WHEguC+rD+p9udv281KzQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -124,8 +139,8 @@ packages: zod: optional: true - '@ai-sdk/vue@1.1.11': - resolution: {integrity: sha512-Zw8x+CoJLhPAa2Esn8X0OCAtUqoDRZ9eBcuDag8g+fLbmwdHLzUjgh4mgABKmUYXBcCFmrN+SlowzM4X6yprVQ==} + '@ai-sdk/vue@1.1.15': + resolution: {integrity: sha512-HO54RTfT/MbPZd8wO+lnwXLKpu/ys6P733Ui5IqW5AbpRQ79zShLfV3+Wqu5q9rKXO42brwRHZApigC5Ex7zKQ==} engines: {node: '>=18'} peerDependencies: vue: ^3.3.4 @@ -851,10 +866,10 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@pinia/nuxt@0.10.0': - resolution: {integrity: sha512-crYQgsqhEnf0HbOuaLYyLR9hyWK2lYUjcCYhFV4vgo3YThrLdnRKfBLa31au1uqcmgDqoX4mEm2OS3DO8GxR7w==} + '@pinia/nuxt@0.10.1': + resolution: {integrity: sha512-xrpkKZHSmshPK6kQzboJ+TZiZ5zj73gBCI5SfiUaJkKKS9gx4B1hLEzJIjxZl0/HS5jRWrIvQ+u9ulvIRlNiow==} peerDependencies: - pinia: ^3.0.0 + pinia: ^3.0.1 '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -1160,8 +1175,8 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 - '@tavily/core@0.0.3': - resolution: {integrity: sha512-17fsQnuxgkUSpKDq0+JWvfy4JZwe/b7/HVb2dqwFKWd5SILR+86OHL5C4TjEu6gO4DT6yWuH7ZNW9s+IlG+K3Q==} + '@tavily/core@0.3.1': + resolution: {integrity: sha512-7jyvPWG4Zjst0s4v0FMLO1f/dfHqs4FnqvKm86zOGYzXxSfxHu0isbLzlwjJad0csYwF0kifdlECTuNouHfr5A==} '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} @@ -1291,8 +1306,8 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-api@7.7.1': - resolution: {integrity: sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==} + '@vue/devtools-api@7.7.2': + resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==} '@vue/devtools-core@7.6.8': resolution: {integrity: sha512-8X4roysTwzQ94o7IobjVcOd1aZF5iunikrMrHPI2uUdigZCi2kFTQc7ffYiFiTNaLElCpjOhCnM7bo7aK1yU7A==} @@ -1302,11 +1317,11 @@ packages: '@vue/devtools-kit@7.6.8': resolution: {integrity: sha512-JhJ8M3sPU+v0P2iZBF2DkdmR9L0dnT5RXJabJqX6o8KtFs3tebdvfoXV2Dm3BFuqeECuMJIfF1aCzSt+WQ4wrw==} - '@vue/devtools-kit@7.7.1': - resolution: {integrity: sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==} + '@vue/devtools-kit@7.7.2': + resolution: {integrity: sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==} - '@vue/devtools-shared@7.7.1': - resolution: {integrity: sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==} + '@vue/devtools-shared@7.7.2': + resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==} '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -1416,8 +1431,8 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - ai@4.1.28: - resolution: {integrity: sha512-DrYyVGK6HKx8TVzwuJOhemH9phdcyMfix5d7giyMYH9tu71Cvfs/Hi4zZPs9KPGDJnZYMLJna7CLs6l5CgRW7g==} + ai@4.1.38: + resolution: {integrity: sha512-jveFmoUbAn05B0OHlbIxUyIjVdhzEIwxFP1ZJtugMLXD6800RyhuaEzFRRBUk5WquLT4Hokm9uwAjCDJ7187dw==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -1586,6 +1601,14 @@ packages: magicast: optional: true + c12@2.0.2: + resolution: {integrity: sha512-NkvlL5CHZt9kPswJYDCUYtTaMt7JOfcpsnNncfj7sWsc13x6Wz+GiTpBtqZOojFlzyTHui8+OAfR6praV6PYaQ==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2516,8 +2539,8 @@ packages: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} - js-tiktoken@1.0.18: - resolution: {integrity: sha512-hFYx4xYf6URgcttcGvGuOBJhTxPYZ2R5eIesqCaNRJmYH8sNmsfTeWg4yu//7u1VD/qIUkgKJTpGom9oHXmB4g==} + js-tiktoken@1.0.19: + resolution: {integrity: sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3060,6 +3083,9 @@ packages: pathe@2.0.2: resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3077,8 +3103,8 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} - pinia@3.0.0: - resolution: {integrity: sha512-Go23UsqaeABb4OYNmpDkE9VwDnqmbbjGzWpQhi3xfNkSPO6ZP+Ttt0EMo2J4DHXW+T0l3EqRneeXdyV/oJg/Mg==} + pinia@3.0.1: + resolution: {integrity: sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg==} peerDependencies: typescript: '>=4.4.4' vue: ^2.7.0 || ^3.5.11 @@ -3799,6 +3825,10 @@ packages: resolution: {integrity: sha512-y5ZYDG+j7IB45+Y6CIkWIKou4E1JFigCUw6vI+h15HdYAKmT0oQWcawnxXuwJG8srJyXhIZuWz5uXB1MQ/ARZw==} engines: {node: '>=18.20.6'} + unimport@4.1.1: + resolution: {integrity: sha512-j9+fijH6aDd05yv1fXlyt7HSxtOWtGtrZeYTVBsSUg57Iuf+Ps2itIZjeyu7bEQ4k0WOgYhHrdW8m/pJgOpl5g==} + engines: {node: '>=18.12.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3819,6 +3849,10 @@ packages: resolution: {integrity: sha512-unB2e2ogZwEoMw/X0Gq1vj2jaRKLmTh9wcSEJggESPllcrZI68uO7B8ykixbXqsSwG8r9T7qaHZudXIC/3qvhw==} engines: {node: '>=18.12.0'} + unplugin-utils@0.2.4: + resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} + engines: {node: '>=18.12.0'} + unplugin-vue-components@28.0.0: resolution: {integrity: sha512-vYe0wSyqTVhyNFIad1iiGyQGhG++tDOMgohqenMDOAooMJP9vvzCdXTqCVx20A0rCQXFNjgoRbSeDAioLPH36Q==} engines: {node: '>=14'} @@ -3860,6 +3894,10 @@ packages: resolution: {integrity: sha512-Q3LU0e4zxKfRko1wMV2HmP8lB9KWislY7hxXpxd+lGx0PRInE4vhMBVEZwpdVYHvtqzhSrzuIfErsob6bQfCzw==} engines: {node: '>=18.12.0'} + unplugin@2.2.0: + resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} + engines: {node: '>=18.12.0'} + unstorage@1.14.4: resolution: {integrity: sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==} peerDependencies: @@ -4228,52 +4266,65 @@ packages: peerDependencies: zod: ^3.24.1 - zod@3.24.1: - resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} snapshots: - '@ai-sdk/openai@1.1.9(zod@3.24.1)': + '@ai-sdk/deepseek@0.1.10(zod@3.24.2)': + dependencies: + '@ai-sdk/openai-compatible': 0.1.10(zod@3.24.2) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + zod: 3.24.2 + + '@ai-sdk/openai-compatible@0.1.10(zod@3.24.2)': dependencies: '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - zod: 3.24.1 + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + zod: 3.24.2 - '@ai-sdk/provider-utils@2.1.6(zod@3.24.1)': + '@ai-sdk/openai@1.1.11(zod@3.24.2)': + dependencies: + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + zod: 3.24.2 + + '@ai-sdk/provider-utils@2.1.8(zod@3.24.2)': dependencies: '@ai-sdk/provider': 1.0.7 eventsource-parser: 3.0.0 nanoid: 3.3.8 secure-json-parse: 2.7.0 optionalDependencies: - zod: 3.24.1 + zod: 3.24.2 '@ai-sdk/provider@1.0.7': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@1.1.11(react@19.0.0)(zod@3.24.1)': + '@ai-sdk/react@1.1.14(react@19.0.0)(zod@3.24.2)': dependencies: - '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.14(zod@3.24.2) swr: 2.3.2(react@19.0.0) throttleit: 2.1.0 optionalDependencies: react: 19.0.0 - zod: 3.24.1 + zod: 3.24.2 - '@ai-sdk/ui-utils@1.1.11(zod@3.24.1)': + '@ai-sdk/ui-utils@1.1.14(zod@3.24.2)': dependencies: '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - zod-to-json-schema: 3.24.1(zod@3.24.1) + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + zod-to-json-schema: 3.24.1(zod@3.24.2) optionalDependencies: - zod: 3.24.1 + zod: 3.24.2 - '@ai-sdk/vue@1.1.11(vue@3.5.13(typescript@5.7.3))(zod@3.24.1)': + '@ai-sdk/vue@1.1.15(vue@3.5.13(typescript@5.7.3))(zod@3.24.2)': dependencies: - '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.14(zod@3.24.2) swrv: 1.1.0(vue@3.5.13(typescript@5.7.3)) optionalDependencies: vue: 3.5.13(typescript@5.7.3) @@ -5036,7 +5087,7 @@ snapshots: '@nuxt/kit@3.15.4(magicast@0.3.5)': dependencies: - c12: 2.0.1(magicast@0.3.5) + c12: 2.0.2(magicast@0.3.5) consola: 3.4.0 defu: 6.1.4 destr: 2.0.3 @@ -5047,14 +5098,14 @@ snapshots: knitwork: 1.2.0 mlly: 1.7.4 ohash: 1.1.4 - pathe: 2.0.2 + pathe: 2.0.3 pkg-types: 1.3.1 scule: 1.3.0 semver: 7.7.1 std-env: 3.8.0 ufo: 1.5.4 unctx: 2.4.1 - unimport: 4.1.0 + unimport: 4.1.1 untyped: 1.5.2 transitivePeerDependencies: - magicast @@ -5342,10 +5393,10 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 - '@pinia/nuxt@0.10.0(magicast@0.3.5)(pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))': + '@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) - pinia: 3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + pinia: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) transitivePeerDependencies: - magicast - supports-color @@ -5604,10 +5655,10 @@ snapshots: '@tanstack/virtual-core': 3.13.0 vue: 3.5.13(typescript@5.7.3) - '@tavily/core@0.0.3': + '@tavily/core@0.3.1': dependencies: axios: 1.7.9 - js-tiktoken: 1.0.18 + js-tiktoken: 1.0.19 transitivePeerDependencies: - debug @@ -5799,14 +5850,14 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-api@7.7.1': + '@vue/devtools-api@7.7.2': dependencies: - '@vue/devtools-kit': 7.7.1 + '@vue/devtools-kit': 7.7.2 '@vue/devtools-core@7.6.8(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.38.1)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))': dependencies: '@vue/devtools-kit': 7.6.8 - '@vue/devtools-shared': 7.7.1 + '@vue/devtools-shared': 7.7.2 mitt: 3.0.1 nanoid: 5.0.9 pathe: 1.1.2 @@ -5817,7 +5868,7 @@ snapshots: '@vue/devtools-kit@7.6.8': dependencies: - '@vue/devtools-shared': 7.7.1 + '@vue/devtools-shared': 7.7.2 birpc: 0.2.19 hookable: 5.5.3 mitt: 3.0.1 @@ -5825,9 +5876,9 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-kit@7.7.1': + '@vue/devtools-kit@7.7.2': dependencies: - '@vue/devtools-shared': 7.7.1 + '@vue/devtools-shared': 7.7.2 birpc: 0.2.19 hookable: 5.5.3 mitt: 3.0.1 @@ -5835,7 +5886,7 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-shared@7.7.1': + '@vue/devtools-shared@7.7.2': dependencies: rfdc: 1.4.1 @@ -5942,17 +5993,17 @@ snapshots: agent-base@7.1.3: {} - ai@4.1.28(react@19.0.0)(zod@3.24.1): + ai@4.1.38(react@19.0.0)(zod@3.24.2): dependencies: '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - '@ai-sdk/react': 1.1.11(react@19.0.0)(zod@3.24.1) - '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) + '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + '@ai-sdk/react': 1.1.14(react@19.0.0)(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.14(zod@3.24.2) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 optionalDependencies: react: 19.0.0 - zod: 3.24.1 + zod: 3.24.2 ajv@6.12.6: dependencies: @@ -6126,6 +6177,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@2.0.2(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.1.8 + defu: 6.1.4 + dotenv: 16.4.7 + giget: 1.2.4 + jiti: 2.4.2 + mlly: 1.7.4 + ohash: 1.1.4 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 1.3.1 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} callsites@3.1.0: {} @@ -7055,7 +7123,7 @@ snapshots: js-levenshtein@1.1.6: {} - js-tiktoken@1.0.18: + js-tiktoken@1.0.19: dependencies: base64-js: 1.5.1 @@ -7757,6 +7825,8 @@ snapshots: pathe@2.0.2: {} + pathe@2.0.3: {} + perfect-debounce@1.0.0: {} performance-now@2.1.0: @@ -7768,9 +7838,9 @@ snapshots: picomatch@4.0.2: {} - pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)): + pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)): dependencies: - '@vue/devtools-api': 7.7.1 + '@vue/devtools-api': 7.7.2 vue: 3.5.13(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 @@ -7781,7 +7851,7 @@ snapshots: dependencies: confbox: 0.1.8 mlly: 1.7.4 - pathe: 2.0.2 + pathe: 2.0.3 pluralize@8.0.0: {} @@ -8536,6 +8606,23 @@ snapshots: unplugin: 2.1.2 unplugin-utils: 0.2.3 + unimport@4.1.1: + dependencies: + acorn: 8.14.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.3 + local-pkg: 1.0.0 + magic-string: 0.30.17 + mlly: 1.7.4 + pathe: 2.0.3 + picomatch: 4.0.2 + pkg-types: 1.3.1 + scule: 1.3.0 + strip-literal: 3.0.0 + unplugin: 2.2.0 + unplugin-utils: 0.2.4 + universalify@2.0.1: {} unplugin-auto-import@19.0.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@12.5.0(typescript@5.7.3))(rollup@4.34.6): @@ -8558,6 +8645,11 @@ snapshots: pathe: 2.0.2 picomatch: 4.0.2 + unplugin-utils@0.2.4: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.2 + unplugin-vue-components@28.0.0(@babel/parser@7.26.8)(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.34.6)(vue@3.5.13(typescript@5.7.3)): dependencies: '@antfu/utils': 0.7.10 @@ -8637,6 +8729,11 @@ snapshots: acorn: 8.14.0 webpack-virtual-modules: 0.6.2 + unplugin@2.2.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + unstorage@1.14.4(db0@0.2.3)(ioredis@5.5.0): dependencies: anymatch: 3.1.3 @@ -8941,8 +9038,8 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.24.1(zod@3.24.1): + zod-to-json-schema@3.24.1(zod@3.24.2): dependencies: - zod: 3.24.1 + zod: 3.24.2 - zod@3.24.1: {} + zod@3.24.2: {} diff --git a/utils/json.ts b/utils/json.ts index 2d20251..18e6ade 100644 --- a/utils/json.ts +++ b/utils/json.ts @@ -1,4 +1,5 @@ import { parsePartialJson } from '@ai-sdk/ui-utils' +import type { TextStreamPart } from 'ai' import { z } from 'zod' export type DeepPartial = T extends object @@ -7,6 +8,13 @@ export type DeepPartial = T extends object : { [P in keyof T]?: DeepPartial } : T +export type ParseStreamingJsonEvent = + | { type: 'object'; value: DeepPartial } + | { type: 'reasoning'; delta: string } + | { type: 'error'; message: string } + /** The call finished with invalid content that can't be parsed as JSON */ + | { type: 'bad-end'; rawText: string } + export function removeJsonMarkdown(text: string) { text = text.trim() if (text.startsWith('```json')) { @@ -23,32 +31,56 @@ export function removeJsonMarkdown(text: string) { } /** - * 解析流式的 JSON 数据 - * @param textStream 字符串流 - * @param _schema zod schema 用于类型验证 - * @param isValid 自定义验证函数,用于判断解析出的 JSON 是否有效 - * @returns 异步生成器,yield 解析后的数据 + * Parse streaming JSON text + * @param fullStream Returned by AI SDK + * @param _schema zod schema for type definition + * @param isValid Custom validation function to check if the parsed JSON is valid */ export async function* parseStreamingJson( - textStream: AsyncIterable, + fullStream: AsyncIterable>, _schema: T, isValid: (value: DeepPartial>) => boolean, -): AsyncGenerator>> { +): AsyncGenerator>> { let rawText = '' let isParseSuccessful = false - for await (const chunk of textStream) { - rawText += chunk - const parsed = parsePartialJson(removeJsonMarkdown(rawText)) + for await (const chunk of fullStream) { + if (chunk.type === 'reasoning') { + yield { type: 'reasoning', delta: chunk.textDelta } + continue + } + if (chunk.type === 'error') { + yield { + type: 'error', + message: + chunk.error instanceof Error + ? chunk.error.message + : String(chunk.error), + } + continue + } + if (chunk.type === 'text-delta') { + rawText += chunk.textDelta + const parsed = parsePartialJson(removeJsonMarkdown(rawText)) - isParseSuccessful = - parsed.state === 'repaired-parse' || parsed.state === 'successful-parse' - if (isParseSuccessful && isValid(parsed.value as any)) { - yield parsed.value as DeepPartial> - } else { - console.debug(`Failed to parse JSON:`, rawText) + isParseSuccessful = + parsed.state === 'repaired-parse' || parsed.state === 'successful-parse' + if (isParseSuccessful && isValid(parsed.value as any)) { + yield { + type: 'object', + value: parsed.value as DeepPartial>, + } + } else { + console.debug(`Failed to parse JSON: ${removeJsonMarkdown(rawText)}`) + } } } - return { isSuccessful: isParseSuccessful } + // If the last chunk parses failed, return an error + if (!isParseSuccessful) { + yield { + type: 'bad-end', + rawText, + } + } }