feat: support reasoning models like DeepSeek R1
This commit is contained in:
30
.dockerignore
Normal file
30
.dockerignore
Normal file
@ -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
|
24
README.md
24
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!
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
|
||||
## 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
|
||||
|
||||
|
20
README_zh.md
20
README_zh.md
@ -16,7 +16,25 @@
|
||||
|
||||
喜欢本项目请点 ⭐ 收藏!
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
|
||||
## 最近更新
|
||||
|
||||
25/02/14
|
||||
|
||||
- 支持 DeepSeek R1 等思维链模型
|
||||
- 改进了模型兼容性,改进异常处理
|
||||
|
||||
25/02/13
|
||||
|
||||
- 大幅缩减了网页体积
|
||||
- 支持配置搜索时使用的语言
|
||||
- 支持 Docker 部署
|
||||
- 修复“导出 PDF”不可用的问题
|
||||
|
||||
25/02/12
|
||||
- 添加中文支持。模型会自动使用用户的语言回答了。
|
||||
- 修复一些 bug
|
||||
|
||||
## 使用指南
|
||||
|
||||
|
@ -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') }}
|
||||
</h2>
|
||||
|
||||
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
|
||||
<ReasoningAccordion
|
||||
v-model="selectedNode.generateQueriesReasoning"
|
||||
loading
|
||||
/>
|
||||
|
||||
<!-- Research goal -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.researchGoal') }}
|
||||
</h3>
|
||||
<!-- Root node has no additional information -->
|
||||
<p v-if="selectedNode.id === '0'">
|
||||
{{ t('webBrowsing.startNode.description') }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.researchGoal') }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="selectedNode.researchGoal"
|
||||
class="prose max-w-none"
|
||||
v-html="marked(selectedNode.researchGoal, { gfm: true })"
|
||||
/>
|
||||
|
||||
<!-- Visited URLs -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.visitedUrls') }}
|
||||
</h3>
|
||||
@ -245,9 +270,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Learnings -->
|
||||
<h3 class="text-lg font-semibold mt-2">
|
||||
{{ t('webBrowsing.learnings') }}
|
||||
</h3>
|
||||
|
||||
<ReasoningAccordion
|
||||
v-model="selectedNode.generateQueriesReasoning"
|
||||
loading
|
||||
/>
|
||||
<p
|
||||
v-for="(learning, index) in selectedNode.learnings"
|
||||
class="prose max-w-none"
|
||||
|
52
components/ReasoningAccordion.vue
Normal file
52
components/ReasoningAccordion.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<!-- Shows an accordion for reasoning (CoT) content. The accordion is default invisible,
|
||||
until modelValue's length > 0 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
const items = computed(() => [
|
||||
{
|
||||
icon: 'i-lucide-brain',
|
||||
content: modelValue.value,
|
||||
},
|
||||
])
|
||||
const currentOpen = ref('0')
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.loading) {
|
||||
currentOpen.value = '0'
|
||||
} else {
|
||||
currentOpen.value = '-1'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UAccordion
|
||||
v-if="modelValue"
|
||||
v-model="currentOpen"
|
||||
class="border border-gray-200 dark:border-gray-800 rounded-lg px-3 sm:px-4"
|
||||
:items="items"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #leading="{ item }">
|
||||
<div
|
||||
:class="[
|
||||
loading && 'animate-pulse',
|
||||
'flex items-center gap-2 text-(--ui-primary)',
|
||||
]"
|
||||
>
|
||||
<UIcon :name="item.icon" size="20" />
|
||||
{{ loading ? $t('modelThinking') : $t('modelThinkingComplete') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<p class="text-sm text-gray-500 whitespace-pre-wrap mb-4">
|
||||
{{ item.content }}
|
||||
</p>
|
||||
</template>
|
||||
</UAccordion>
|
||||
</template>
|
@ -15,6 +15,7 @@
|
||||
}>()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const reasoningContent = ref('')
|
||||
const feedback = ref<ResearchFeedbackResult[]>([])
|
||||
|
||||
const isLoading = ref(false)
|
||||
@ -39,7 +40,14 @@
|
||||
numQuestions,
|
||||
language: t('language', {}, { locale: locale.value }),
|
||||
})) {
|
||||
const questions = f.questions!.filter((s) => typeof s === 'string')
|
||||
if (f.type === 'reasoning') {
|
||||
reasoningContent.value += f.delta
|
||||
} else if (f.type === 'error') {
|
||||
error.value = f.message
|
||||
} else if (f.type === 'object') {
|
||||
const questions = f.value.questions!.filter(
|
||||
(s) => typeof s === 'string',
|
||||
)
|
||||
// Incrementally update modelValue
|
||||
for (let i = 0; i < questions.length; i += 1) {
|
||||
if (feedback.value[i]) {
|
||||
@ -51,6 +59,9 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (f.type === 'bad-end') {
|
||||
error.value = t('invalidStructuredOutput')
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Error getting feedback:', e)
|
||||
@ -66,6 +77,7 @@
|
||||
function clear() {
|
||||
feedback.value = []
|
||||
error.value = ''
|
||||
reasoningContent.value = ''
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@ -85,13 +97,16 @@
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-if="!feedback.length && !error">
|
||||
<div v-if="!feedback.length && !reasoningContent && !error">
|
||||
{{ $t('modelFeedback.waiting') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="error" class="text-red-500 whitespace-pre-wrap">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<ReasoningAccordion v-model="reasoningContent" :loading="isLoading" />
|
||||
|
||||
<div
|
||||
v-for="(feedback, index) in feedback"
|
||||
class="flex flex-col gap-2"
|
||||
|
@ -102,7 +102,7 @@
|
||||
block
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
|
||||
{{ isLoadingFeedback ? $t('researchTopic.researching') : $t('researchTopic.start') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
|
@ -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 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||
|
||||
<ReasoningAccordion
|
||||
v-if="reasoningContent"
|
||||
v-model="reasoningContent"
|
||||
class="mb-4"
|
||||
:loading="loading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="reportContent"
|
||||
id="report-content"
|
||||
class="prose prose-sm max-w-none p-6 bg-gray-50 dark:bg-gray-800 dark:text-white rounded-lg shadow"
|
||||
v-html="reportHtml"
|
||||
/>
|
||||
<template v-else>
|
||||
<div v-if="error" class="text-red-500">{{ error }}</div>
|
||||
<div v-else>
|
||||
{{
|
||||
loading
|
||||
? $t('researchReport.generating')
|
||||
: $t('researchReport.waiting')
|
||||
loading ? $t('researchReport.generating') : $t('researchReport.waiting')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
@ -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[]
|
||||
|
@ -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}`)
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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. 模型反馈",
|
||||
|
@ -29,6 +29,7 @@ export type PartialSearchResult = DeepPartial<SearchResult>
|
||||
|
||||
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>${contents
|
||||
.map((content) => `<content>\n${content}\n</content>`)
|
||||
.join('\n')}</contents>`,
|
||||
`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<ResearchResult> {
|
||||
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
|
||||
const id = childNodeId(nodeId, i)
|
||||
if (chunk.type === 'object') {
|
||||
searchResult = chunk.value
|
||||
onProgress({
|
||||
type: 'processing_serach_result',
|
||||
result: parsedLearnings,
|
||||
result: chunk.value,
|
||||
query: searchQuery.query,
|
||||
nodeId: childNodeId(nodeId, i),
|
||||
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}`,
|
||||
|
@ -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>${query}</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,
|
||||
)
|
||||
|
19
package.json
19
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": {
|
||||
|
277
pnpm-lock.yaml
generated
277
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { parsePartialJson } from '@ai-sdk/ui-utils'
|
||||
import type { TextStreamPart } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export type DeepPartial<T> = T extends object
|
||||
@ -7,6 +8,13 @@ export type DeepPartial<T> = T extends object
|
||||
: { [P in keyof T]?: DeepPartial<T[P]> }
|
||||
: T
|
||||
|
||||
export type ParseStreamingJsonEvent<T> =
|
||||
| { type: 'object'; value: DeepPartial<T> }
|
||||
| { 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<T extends z.ZodType>(
|
||||
textStream: AsyncIterable<string>,
|
||||
fullStream: AsyncIterable<TextStreamPart<any>>,
|
||||
_schema: T,
|
||||
isValid: (value: DeepPartial<z.infer<T>>) => boolean,
|
||||
): AsyncGenerator<DeepPartial<z.infer<T>>> {
|
||||
): AsyncGenerator<ParseStreamingJsonEvent<z.infer<T>>> {
|
||||
let rawText = ''
|
||||
let isParseSuccessful = false
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
rawText += chunk
|
||||
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<z.infer<T>>
|
||||
yield {
|
||||
type: 'object',
|
||||
value: parsed.value as DeepPartial<z.infer<T>>,
|
||||
}
|
||||
} else {
|
||||
console.debug(`Failed to parse JSON:`, rawText)
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user