Repository navigation
feat(duckduckgo-web): add tool-call translation via webTools prompt injection #3253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { BaseExecutor, type ExecuteInput } from "./base.ts"; | ||
| import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; | ||
| import { serializeToolsToPrompt, parseToolCallsFromText } from "../translator/webTools.ts"; | ||
|
|
||
| export const DUCKDUCKGO_BASE = "https://duckduckgo.com"; | ||
| const STATUS_URL = `${DUCKDUCKGO_BASE}/duckchat/v1/status`; | ||
|
|
@@ -64,7 +65,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| } | ||
|
|
||
| async execute(input: ExecuteInput) { | ||
| const { model, messages, stream, signal, upstreamHeaders } = input; | ||
| const { model, messages, stream, signal, upstreamHeaders, body } = input; | ||
|
|
||
| if (!messages || messages.length === 0) { | ||
| return new Response( | ||
|
|
@@ -73,6 +74,14 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| ); | ||
| } | ||
|
|
||
| const bodyObj = (body || {}) as Record<string, unknown>; | ||
| const requestedTools = bodyObj.tools; | ||
| const hasTools = Array.isArray(requestedTools) && requestedTools.length > 0; | ||
| const toolSystemPrompt = hasTools ? serializeToolsToPrompt(requestedTools) : ""; | ||
| const effectiveMessages = toolSystemPrompt | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Unconditionally prepending a system message can collide with an existing system message in the user's messages array. If |
||
| ? [{ role: "system", content: toolSystemPrompt }, ...messages] | ||
| : messages; | ||
|
|
||
| // Acquire session from pool for fingerprint rotation | ||
| const pool = this.getPool(); | ||
| let session; | ||
|
|
@@ -117,7 +126,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| }, | ||
| body: JSON.stringify({ | ||
| model, | ||
| messages, | ||
| messages: effectiveMessages, | ||
| stream: stream !== false, | ||
| }), | ||
| signal: mergedSignal, | ||
|
|
@@ -145,13 +154,13 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| }, | ||
| body: JSON.stringify({ | ||
| model, | ||
| messages, | ||
| messages: effectiveMessages, | ||
| stream: stream !== false, | ||
| }), | ||
| signal: mergedSignal, | ||
| }); | ||
|
|
||
| return this.processResponse(retryResponse, stream !== false); | ||
| return this.processResponse(retryResponse, stream !== false, hasTools, requestedTools); | ||
| } | ||
| return new Response( | ||
| JSON.stringify({ error: { message: "Service unavailable" } }), | ||
|
|
@@ -167,7 +176,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| ); | ||
| } | ||
|
|
||
| const result = this.processResponse(chatResponse, stream !== false); | ||
| const result = this.processResponse(chatResponse, stream !== false, hasTools, requestedTools); | ||
|
|
||
| // Report pool status based on response | ||
| if (pool && session) { | ||
|
|
@@ -222,7 +231,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| } | ||
| } | ||
|
|
||
| private async processResponse(response: Response, streaming: boolean): Promise<Response> { | ||
| private async processResponse(response: Response, streaming: boolean, hasTools?: boolean, requestedTools?: unknown): Promise<Response> { | ||
| if (!response.ok) { | ||
| const body = await response.text(); | ||
| return new Response(body, { | ||
|
|
@@ -231,7 +240,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| }); | ||
| } | ||
|
|
||
| if (streaming) { | ||
| if (streaming && !hasTools) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: When |
||
| const reader = response.body?.getReader(); | ||
| if (!reader) { | ||
| return new Response( | ||
|
|
@@ -304,6 +313,24 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { | |
| } | ||
| } | ||
|
|
||
| if (hasTools) { | ||
| const { content: cleanedContent, toolCalls } = parseToolCallsFromText( | ||
| fullContent, `call-${Date.now()}`, requestedTools | ||
| ); | ||
| const choice: Record<string, unknown> = { | ||
| index: 0, | ||
| message: { role: "assistant", content: cleanedContent }, | ||
| finish_reason: toolCalls ? "tool_calls" : "stop", | ||
| }; | ||
| if (toolCalls) { | ||
| (choice.message as Record<string, unknown>).tool_calls = toolCalls; | ||
| (choice.message as Record<string, unknown>).content = null; | ||
| } | ||
| return new Response(JSON.stringify({ choices: [choice] }), { | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
Comment on lines
+316
to
+332
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When if (hasTools) {
const { content: cleanedContent, toolCalls } = parseToolCallsFromText(
fullContent,
"call-" + Date.now(),
requestedTools
);
if (streaming) {
const chunks: string[] = [];
if (toolCalls) {
chunks.push(JSON.stringify({
choices: [{
index: 0,
delta: { role: "assistant", content: null, tool_calls: toolCalls },
finish_reason: null
}]
}));
chunks.push(JSON.stringify({
choices: [{
index: 0,
delta: {},
finish_reason: "tool_calls"
}]
}));
} else {
if (cleanedContent) {
chunks.push(JSON.stringify({
choices: [{
index: 0,
delta: { role: "assistant", content: cleanedContent },
finish_reason: null
}]
}));
}
chunks.push(JSON.stringify({
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
}]
}));
}
const sseData = chunks.map(function(c) { return "data: " + c + "\n\n"; }).join("") + "data: [DONE]\n\n";
return new Response(sseData, {
headers: { "Content-Type": "text/event-stream" },
});
} else {
const choice: Record<string, unknown> = {
index: 0,
message: { role: "assistant", content: cleanedContent },
finish_reason: toolCalls ? "tool_calls" : "stop",
};
if (toolCalls) {
(choice.message as Record<string, unknown>).tool_calls = toolCalls;
(choice.message as Record<string, unknown>).content = null;
}
return new Response(JSON.stringify({ choices: [choice] }), {
headers: { "Content-Type": "application/json" },
});
}
} |
||
|
|
||
| const openaiResponse = { | ||
| choices: [ | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the Repository Style Guide (Rule 9), you must always include tests when changing production code under
open-sse/. Please add unit/integration tests for the new tool-call translation logic in thetests/directory.References