// OpenAI native tool-calling gather. Non-streaming /v1/chat/completions loop // using function tool_calls + role:"tool" result messages. import { PROVIDERS } from "../askProvider"; import { abortError, EMPTY_PARAMS, FETCH_BUDGET, FETCH_PARAMS, FETCH_TOOL_DESCRIPTION, FETCH_TOOL_NAME, FINISH_TOOL_DESCRIPTION, FINISH_TOOL_NAME, type ParsedToolCall, postJson, REPORT_BUDGET, REPORT_PARAMS, REPORT_TOOL_DESCRIPTION, REPORT_TOOL_NAME, SEARCH_PARAMS, SEARCH_TOOL_DESCRIPTION, SEARCH_TOOL_NAME, type NativeGatherContext, } from "./shared"; function buildTools(includeFetch: boolean, includeReport: boolean) { const tools: { type: "function"; function: { name: string; description: string; parameters: unknown }; }[] = [ { type: "function", function: { name: SEARCH_TOOL_NAME, description: SEARCH_TOOL_DESCRIPTION, parameters: SEARCH_PARAMS, }, }, ]; if (includeFetch) { tools.push({ type: "function", function: { name: FETCH_TOOL_NAME, description: FETCH_TOOL_DESCRIPTION, parameters: FETCH_PARAMS, }, }); } if (includeReport) { tools.push({ type: "function", function: { name: REPORT_TOOL_NAME, description: REPORT_TOOL_DESCRIPTION, parameters: REPORT_PARAMS, }, }); } tools.push({ type: "function", function: { name: FINISH_TOOL_NAME, description: FINISH_TOOL_DESCRIPTION, parameters: EMPTY_PARAMS, }, }); return tools; } type RawToolCall = { id?: string; function?: { name?: string; arguments?: string }; }; // Pure: extract tool calls from an OpenAI chat-completion response. export function parseOpenAIToolCalls(json: unknown): ParsedToolCall[] { const msg = (json as { choices?: { message?: { tool_calls?: unknown } }[] }) .choices?.[0]?.message; const calls = (msg as { tool_calls?: unknown })?.tool_calls; if (!Array.isArray(calls)) return []; return (calls as RawToolCall[]).map((c) => { let query = ""; let video = ""; let aroundSeconds: number | undefined; let section: string | undefined; let content: string | undefined; try { const args = JSON.parse(c.function?.arguments ?? "{}"); if (typeof args.query === "string") query = args.query; if (typeof args.video === "string") video = args.video; if (typeof args.aroundSeconds === "number") aroundSeconds = args.aroundSeconds; if (typeof args.section === "string") section = args.section; if (typeof args.content === "string") content = args.content; } catch { /* malformed args → empty */ } return { id: c.id ?? "", name: c.function?.name ?? "", query, video, aroundSeconds, ...(section !== undefined ? { section } : {}), ...(content !== undefined ? { content } : {}), }; }); } export async function openaiGather(ctx: NativeGatherContext): Promise { const canFetch = !!ctx.runFetchContext; const canReport = !!ctx.runUpdateReport; const tools = buildTools(canFetch, canReport); const messages: unknown[] = [ { role: "system", content: ctx.system }, ...ctx.history.map((m) => ({ role: m.role, content: m.content })), { role: "user", content: ctx.question }, ]; let searchesRun = 0; let fetchesRun = 0; let reportsRun = 0; const maxRounds = ctx.budget + 1 + (canFetch ? FETCH_BUDGET : 0) + (canReport ? REPORT_BUDGET : 0); for (let round = 0; round <= maxRounds; round++) { if (ctx.signal?.aborted) throw abortError(); const json = await postJson( "https://api.openai.com/v1/chat/completions", { authorization: `Bearer ${ctx.apiKey}` }, { model: ctx.model || PROVIDERS.openai.defaultModel, max_tokens: 512, tools, tool_choice: "auto", messages, }, "OpenAI", ctx.signal, { onDebug: ctx.onDebug, model: ctx.model }, ); const rawMsg = (json as { choices?: { message?: unknown }[] }).choices?.[0] ?.message; const calls = parseOpenAIToolCalls(json); const wantsSearch = calls.some( (c) => c.name === SEARCH_TOOL_NAME && c.query.trim() !== "", ); const wantsFetch = canFetch && calls.some((c) => c.name === FETCH_TOOL_NAME && c.video.trim() !== ""); const wantsReport = canReport && calls.some( (c) => c.name === REPORT_TOOL_NAME && (c.section ?? "").trim() !== "", ); // No actionable tool call → done gathering. if (!wantsSearch && !wantsFetch && !wantsReport) return; // Replay the assistant message with its tool_calls, then answer EVERY call // (OpenAI requires a tool response for each tool_call id). messages.push(rawMsg); for (const call of calls) { let content: string; if (call.name === SEARCH_TOOL_NAME && call.query.trim() !== "") { if (searchesRun < ctx.budget) { searchesRun += 1; content = await ctx.runSearch(call.query); } else { content = "Search budget reached — answer with the excerpts gathered so far."; } } else if ( canFetch && call.name === FETCH_TOOL_NAME && call.video.trim() !== "" ) { if (fetchesRun < FETCH_BUDGET) { fetchesRun += 1; content = await ctx.runFetchContext!(call.video, call.aroundSeconds); } else { content = "Reached the limit on transcript reads this turn."; } } else if ( canReport && call.name === REPORT_TOOL_NAME && (call.section ?? "").trim() !== "" ) { if (reportsRun < REPORT_BUDGET) { reportsRun += 1; content = await ctx.runUpdateReport!(call.section!, call.content ?? ""); } else { content = "Reached the limit on report updates this turn."; } } else { content = "Acknowledged."; } messages.push({ role: "tool", tool_call_id: call.id, content }); } } }