From 9914c043cbb0306ca7e775e156750be243128711 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:05:47 -0500 Subject: [PATCH] feat: add web search tool with reranking support Integrates SearXNG web search as a tool available during chat when the agent has search enabled. Supports optional reranking of results via a configurable reranking model. Replaces Python subprocess evaluation with @pydantic/monty WASM runtime. Introduces stable tool call ID mapping to avoid exposing provider-native IDs to the database. --- app/components/ChatInput.vue | 71 +++- app/components/SearchSelector.vue | 140 +++++++ app/composables/useAgents.ts | 8 +- app/composables/useChat.ts | 52 +-- app/composables/useModels.ts | 1 + server/api/topic/[topicId]/chat/index.post.ts | 354 +++++++++++++----- 6 files changed, 490 insertions(+), 136 deletions(-) create mode 100644 app/components/SearchSelector.vue diff --git a/app/components/ChatInput.vue b/app/components/ChatInput.vue index 5113d4b..f961f49 100644 --- a/app/components/ChatInput.vue +++ b/app/components/ChatInput.vue @@ -6,6 +6,7 @@ import type { Agent } from '~/composables/useAgents'; import type FileSelector from './FileSelector.vue'; const { allModels } = await useModels(); +const { updateAgent, patchAgentLocally } = await useAgents(); const inputHeight: Ref = ref('auto'); const fileSelectorRef = ref | null>(null); @@ -39,6 +40,35 @@ const props = defineProps<{ providers?: ProviderWithModels[]; }>(); +const searchConfig = ref({ + enabled: props.agent?.config?.search?.enabled ?? false, + maxResults: props.agent?.config?.search?.maxResults ?? 10, + rerank: props.agent?.config?.search?.rerank ?? false, +}); + +watch(() => props.agent?.config?.search, (val) => { + searchConfig.value = { + enabled: val?.enabled ?? false, + maxResults: val?.maxResults ?? 10, + rerank: val?.rerank ?? false, + }; +}, { deep: true }); + +const saveSearchConfig = async () => { + if (!props.agent) return; + const currentConfig = props.agent.config ?? {}; + const newConfig = { + ...currentConfig, + search: { + enabled: searchConfig.value.enabled, + maxResults: searchConfig.value.maxResults, + rerank: searchConfig.value.rerank, + }, + }; + patchAgentLocally(props.agent.id, { config: newConfig }); + updateAgent(props.agent.id, { config: newConfig }); +}; + const selectedModel = ref(null); const handlePaste = async (event: ClipboardEvent) => { @@ -165,25 +195,21 @@ const handleWindowKeyDown = async (event: KeyboardEvent) => { } }; -watch(textAreaValue, async () => { +const resizeTextArea = () => { const textarea = inputRef.value; if (!textarea) return; inputHeight.value = 'auto'; - await nextTick(); + nextTick().then(() => { + const lineHeight = 24; + const maxLines = 10; + const maxHeight = maxLines * lineHeight; + const height = Math.min(textarea.scrollHeight, maxHeight); + inputHeight.value = `${height}px`; + }); +}; - const lineHeight = 24; - const maxLines = 10; - const maxHeight = maxLines * lineHeight; - - const newHeight = textarea.scrollHeight; - - if (newHeight > maxHeight) { - inputHeight.value = `${maxHeight}px`; - } else { - inputHeight.value = `${newHeight}px`; - } -}, { immediate: true }); +watch(textAreaValue, resizeTextArea, { immediate: true }); let hasCommandKey = false; if (import.meta.server) { @@ -197,14 +223,26 @@ onBeforeMount(() => { tempInput = (document.getElementById('chat') as HTMLInputElement)?.value ?? ''; }); +let resizeRafId: number | undefined; +const handleWindowResize = () => { + if (resizeRafId) return; + resizeRafId = requestAnimationFrame(() => { + resizeRafId = undefined; + resizeTextArea(); + }); +} + onMounted(() => { textAreaValue.value = tempInput; document.addEventListener('keydown', handleWindowKeyDown); + window.addEventListener('resize', handleWindowResize); inputRef.value?.addEventListener('paste', handlePaste); }); onUnmounted(() => { document.removeEventListener('keydown', handleWindowKeyDown); + window.removeEventListener('resize', handleWindowResize); + if (resizeRafId !== undefined) cancelAnimationFrame(resizeRafId); inputRef.value?.removeEventListener('paste', handlePaste); }); @@ -232,6 +270,11 @@ onUnmounted(() => {
+