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.
This commit is contained in:
Zoe
2026-04-27 12:05:47 -05:00
parent 90d5698e76
commit 9914c043cb
6 changed files with 490 additions and 136 deletions
+4 -4
View File
@@ -123,9 +123,9 @@ export const useAgents = async () => {
a.id === id ? agent : a
);
},
async onResponse() {
await refresh();
}
// async onResponse() {
// await refresh();
// }
});
}
@@ -166,7 +166,7 @@ export const useAgents = async () => {
body: topic,
onRequest() {
agents.value = agents.value.map(a =>
a.id === agentId ? { ...a, topics: [{ ...topic, createdAt: new Date() }, ...a.topics] } : a
a.id === agentId ? { ...a, topics: [{ ...topic, createdAt: new Date() }, ...a.topics] } as AgentWithTopics : a
);
},
onResponseError() {
+30 -22
View File
@@ -52,7 +52,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
if (!msg) continue;
for (const [partId, content] of parts) {
console.log("content", content);
const part = msg.parts?.find(p => p.id === partId);
if (part) {
part.content += content;
@@ -135,7 +134,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const messageBuffer = textDeltaBuffer.get(payload.messageId)!;
const existing = messageBuffer.get(payload.partId) || '';
messageBuffer.set(payload.partId, existing + payload.content);
console.log("messageBuffer", messageBuffer, existing + payload.content);
scheduleFlush();
break;
@@ -303,8 +301,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
baseMessage: BaseMessage,
onRequest?: () => void,
): Promise<Result<void, ChatErrorType>> => {
console.log("sendMessage", baseMessage);
const { user } = useAuth();
if (!user.value) return Err(ChatErrorType.NoUser);
@@ -316,8 +312,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
fileIds: baseMessage.fileIds,
}
console.log("sendMessage", message, baseMessage.content, baseMessage.fileIds);
await $fetch(`/api/topic/${unref(topicId)}/message`, {
method: 'POST',
body: {
@@ -355,19 +349,46 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
}
};
const startGeneration = async (model: ModelWithProvider) => {
console.log(model.provider);
const startGeneration = async (model: ModelWithProvider, parentMessageId?: string, agentIdOverride?: string) => {
const providerApiKeyRes = await getProviderAPIKey(model.provider);
if (providerApiKeyRes.ok === false) {
return providerApiKeyRes;
}
const providerApiKey = providerApiKeyRes.data;
const { agents } = await useAgents();
const agent = agents.value.find(agent => agent.id === (agentIdOverride || topic.value?.agentId));
if (!agent) {
return Err(ChatErrorType.NoAgent);
}
const { settings } = await useUserSettings();
let rerank = undefined;
if (agent.config?.search?.rerank && settings.value.systemAssistants.rerank.enabled) {
const rerankModel = await useModels().then(m => m.allModels.value.find(m => m.id === settings.value.systemAssistants.rerank.modelId));
if (rerankModel) {
const rerankProviderApiKeyRes = await getProviderAPIKey(rerankModel.provider);
if (rerankProviderApiKeyRes.ok === false) {
return rerankProviderApiKeyRes;
}
const rerankProviderApiKey = rerankProviderApiKeyRes.data;
rerank = {
modelId: rerankModel.id,
providerApiKey: rerankProviderApiKey,
};
}
}
await $fetch(`/api/topic/${unref(topicId)}/chat`, {
method: 'POST',
body: {
modelId: model.id,
providerApiKey,
rerank,
parentMessageId,
},
});
}
@@ -399,12 +420,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
}
let targetMessageIndex = topicMessages.indexOf(targetMessage);
const providerApiKeyRes = await getProviderAPIKey(model.provider);
if (providerApiKeyRes.ok === false) {
return providerApiKeyRes;
}
const providerApiKey = providerApiKeyRes.data;
let parentMessageId = undefined;
let focusedMessages: MessageEntity[] | undefined;
if (targetMessage.role === 'user') {
@@ -429,14 +444,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
focusedMessages = topicMessages;
}
await $fetch(`/api/topic/${unref(topicId)}/chat`, {
method: 'POST',
body: {
modelId: model.id,
providerApiKey,
parentMessageId,
},
});
startGeneration(model, parentMessageId);
return Ok(undefined);
}
+1
View File
@@ -6,6 +6,7 @@ export type Provider = typeof schema.providers.$inferSelect;
export type ProviderWithModels = Provider & {
models: Model[];
defaultBaseUrl?: string;
};
export type ModelWithProvider = Model & {