feat: search in a different language
This commit is contained in:
@ -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.
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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": {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 +=
|
||||||
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user