feat: search in a different language

This commit is contained in:
AnotiaWang
2025-02-13 22:26:43 +08:00
parent 86c63d569b
commit 83f64b5948
9 changed files with 67 additions and 9 deletions
+2 -1
View File
@@ -1,12 +1,13 @@
# Deep Research Web UI # Deep Research Web UI
This is a web UI for https://github.com/dzhng/deep-research. This is a web UI for https://github.com/dzhng/deep-research, with several improvements and fixes.
Features: Features:
- **Realtime feedback**: Stream AI responses and reflect on the UI in real-time - **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
- **Export as PDF**: Export the final research report as a PDF - **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
- **Bring Your Own API Key**: Everything (config, API requests, ...) happens in your browser - **Bring Your Own API Key**: Everything (config, API requests, ...) happens in your browser
- **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. - **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.
+15 -1
View File
@@ -137,7 +137,7 @@
<USeparator class="my-2" /> <USeparator class="my-2" />
<!-- Web search provider --> <!-- Web search -->
<h3 class="font-bold"> {{ $t('settings.webSearch.provider') }} </h3> <h3 class="font-bold"> {{ $t('settings.webSearch.provider') }} </h3>
<UFormField> <UFormField>
<template #help> <template #help>
@@ -164,6 +164,20 @@
:placeholder="$t('settings.webSearch.apiKey')" :placeholder="$t('settings.webSearch.apiKey')"
/> />
</UFormField> </UFormField>
<UFormField :label="$t('settings.webSearch.queryLanguage')">
<template #help>
<i18n-t
class="whitespace-pre-wrap"
keypath="settings.webSearch.queryLanguageHelp"
tag="p"
/>
</template>
<LangSwitcher
:value="config.webSearch.searchLanguage"
@update="config.webSearch.searchLanguage = $event"
private
/>
</UFormField>
</div> </div>
</template> </template>
<template #footer> <template #footer>
+5
View File
@@ -9,6 +9,7 @@
import { marked } from 'marked' import { marked } from 'marked'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const { config } = storeToRefs(useConfigStore())
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'complete', results: ResearchResult): void (e: 'complete', results: ResearchResult): void
}>() }>()
@@ -146,11 +147,15 @@
searchResults.value = {} searchResults.value = {}
isLoading.value = true isLoading.value = true
try { try {
const searchLanguage = config.value.webSearch.searchLanguage
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
: undefined
await deepResearch({ await deepResearch({
query, query,
maxDepth: depth, maxDepth: depth,
breadth, breadth,
language: t('language', {}, { locale: locale.value }), language: t('language', {}, { locale: locale.value }),
searchLanguage,
onProgress: handleResearchProgress, onProgress: handleResearchProgress,
}) })
} catch (error) { } catch (error) {
+23 -3
View File
@@ -1,17 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
const { locale, availableLocales, t, setLocale } = useI18n() const { locale: globalLocale, availableLocales, t, setLocale } = useI18n()
export type Locale = (typeof globalLocale)['value']
export type AvailableLocales = Locale[]
const props = defineProps<{
/** Override display locale */
value?: Locale
/** If true, it will not change global locales */
private?: boolean
}>()
const emit = defineEmits<{
(e: 'update', value: Locale): void
}>()
const localeOptions = availableLocales.map((locale) => ({ const localeOptions = availableLocales.map((locale) => ({
value: locale, value: locale,
label: t('language', {}, { locale }), label: t('language', {}, { locale }),
})) }))
function changeLocale(l: Locale) {
emit('update', l)
if (props.private) return
setLocale(l)
}
</script> </script>
<template> <template>
<USelect <USelect
icon="i-lucide-languages" icon="i-lucide-languages"
:model-value="locale" :model-value="value ?? globalLocale"
:items="localeOptions" :items="localeOptions"
@update:model-value="setLocale($event)" @update:model-value="changeLocale($event)"
/> />
</template> </template>
+3 -1
View File
@@ -27,7 +27,9 @@
"webSearch": { "webSearch": {
"provider": "Web Search Provider", "provider": "Web Search Provider",
"providerHelp": "Currently only supports Tavily. It provides lots of free quota (1000 credits / month).\nGet one API key at {0}.", "providerHelp": "Currently only supports Tavily. It provides lots of free quota (1000 credits / month).\nGet one API key at {0}.",
"apiKey": "API Key" "apiKey": "API Key",
"queryLanguage": "Query Language",
"queryLanguageHelp": "The language of the search query. Useful if you want to get search results in a different language.\nWhen writing conclusions, the AI model will still use the language same as the web UI."
} }
}, },
"researchTopic": { "researchTopic": {
+3 -1
View File
@@ -27,7 +27,9 @@
"webSearch": { "webSearch": {
"provider": "联网搜索服务", "provider": "联网搜索服务",
"providerHelp": "目前仅支持 Tavily,每个月可以免费搜索 1000 次。\n请在 {0} 生成一个 API 密钥。", "providerHelp": "目前仅支持 Tavily,每个月可以免费搜索 1000 次。\n请在 {0} 生成一个 API 密钥。",
"apiKey": "API 密钥" "apiKey": "API 密钥",
"queryLanguage": "使用语言",
"queryLanguageHelp": "修改搜索词的语言。如果你想获取不同的搜索结果(比如查询高质量的英文资料),可以在这里修改。\nAI 模型在总结的时候仍然会使用当前网页的语言。"
} }
}, },
"researchTopic": { "researchTopic": {
+12 -1
View File
@@ -73,12 +73,15 @@ export function generateSearchQueries({
numQueries = 3, numQueries = 3,
learnings, learnings,
language, language,
searchLanguage,
}: { }: {
query: string query: string
language: string language: string
numQueries?: number numQueries?: number
// optional, if provided, the research will continue from the last learning // optional, if provided, the research will continue from the last learning
learnings?: string[] learnings?: string[]
/** Force the LLM to generate serp queries in a certain language */
searchLanguage?: string
}) { }) {
const schema = z.object({ const schema = z.object({
queries: z queries: z
@@ -95,7 +98,11 @@ export function generateSearchQueries({
.describe(`List of SERP queries, max of ${numQueries}`), .describe(`List of SERP queries, max of ${numQueries}`),
}) })
const jsonSchema = JSON.stringify(zodToJsonSchema(schema)) const jsonSchema = JSON.stringify(zodToJsonSchema(schema))
let lp = languagePrompt(language)
if (searchLanguage !== language) {
lp += `Use ${searchLanguage} for the SERP queries.`
}
const prompt = [ const prompt = [
`Given the following prompt from the user, generate a list of SERP queries to research the topic. Return a maximum of ${numQueries} queries, but feel free to return less if the original prompt is clear. Make sure each query is unique and not similar to each other: <prompt>${query}</prompt>\n\n`, `Given the following prompt from the user, generate a list of SERP queries to research the topic. Return a maximum of ${numQueries} queries, but feel free to return less if the original prompt is clear. Make sure each query is unique and not similar to each other: <prompt>${query}</prompt>\n\n`,
learnings learnings
@@ -104,7 +111,7 @@ export function generateSearchQueries({
)}` )}`
: '', : '',
`You MUST respond in JSON with the following schema: ${jsonSchema}`, `You MUST respond in JSON with the following schema: ${jsonSchema}`,
languagePrompt(language), lp,
].join('\n\n') ].join('\n\n')
return streamText({ return streamText({
model: useAiModel(), model: useAiModel(),
@@ -213,6 +220,7 @@ export async function deepResearch({
onProgress, onProgress,
currentDepth = 1, currentDepth = 1,
nodeId = '0', nodeId = '0',
searchLanguage,
}: { }: {
query: string query: string
breadth: number breadth: number
@@ -223,6 +231,8 @@ export async function deepResearch({
onProgress: (step: ResearchStep) => void onProgress: (step: ResearchStep) => void
currentDepth?: number currentDepth?: number
nodeId?: string nodeId?: string
/** Force the LLM to generate serp queries in a certain language */
searchLanguage?: string
}): Promise<ResearchResult> { }): Promise<ResearchResult> {
try { try {
const searchQueriesResult = generateSearchQueries({ const searchQueriesResult = generateSearchQueries({
@@ -230,6 +240,7 @@ export async function deepResearch({
learnings, learnings,
numQueries: breadth, numQueries: breadth,
language, language,
searchLanguage,
}) })
const limit = pLimit(ConcurrencyLimit) const limit = pLimit(ConcurrencyLimit)
+1 -1
View File
@@ -20,7 +20,7 @@ export const systemPrompt = () => {
* @param language the language of the prompt, e.g. `English` * @param language the language of the prompt, e.g. `English`
*/ */
export const languagePrompt = (language: string) => { export const languagePrompt = (language: string) => {
let languagePrompt = `- Respond in ${language}.` let languagePrompt = `Respond in ${language}.`
if (language === '中文') { if (language === '中文') {
languagePrompt += languagePrompt +=
+3
View File
@@ -1,4 +1,5 @@
import { skipHydrate } from 'pinia' import { skipHydrate } from 'pinia'
import type { Locale } from '~/components/LangSwitcher.vue'
export type ConfigAiProvider = 'openai-compatible' export type ConfigAiProvider = 'openai-compatible'
export interface ConfigAi { export interface ConfigAi {
@@ -11,6 +12,8 @@ export interface ConfigAi {
export interface ConfigWebSearch { export interface ConfigWebSearch {
provider: 'tavily' provider: 'tavily'
apiKey?: string apiKey?: string
/** Force the LLM to generate serp queries in a certain language */
searchLanguage?: Locale
} }
export interface Config { export interface Config {