Files
elizaos--eliza/plugins/plugin-openai/models/research.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

411 lines
12 KiB
TypeScript

/**
* Deep Research model handler
*
* Provides deep research capabilities using OpenAI's o3-deep-research and o4-mini-deep-research models.
* These models can find, analyze, and synthesize hundreds of sources to create comprehensive reports.
*
* @see https://platform.openai.com/docs/guides/deep-research
*/
import type {
IAgentRuntime,
JsonValue,
RecordLlmCallDetails,
ResearchAnnotation,
ResearchCodeInterpreterCall,
ResearchFileSearchCall,
ResearchMcpToolCall,
ResearchMessageOutput,
ResearchOutputItem,
ResearchParams,
ResearchResult,
ResearchTool,
ResearchWebSearchCall,
} from "@elizaos/core";
import { logger, recordLlmCall } from "@elizaos/core";
import { getApiKey, getBaseURL, getResearchModel, getResearchTimeout } from "../utils/config";
// ============================================================================
// Types for OpenAI Responses API
// ============================================================================
/**
* Tool configuration for the Responses API
*/
interface ResponsesApiTool {
type: "web_search_preview" | "file_search" | "code_interpreter" | "mcp";
vector_store_ids?: string[];
container?: { type: "auto" };
server_label?: string;
server_url?: string;
require_approval?: "never";
}
/**
* Raw response from the OpenAI Responses API
*/
interface ResponsesApiResponse {
id: string;
object: string;
status?: "queued" | "in_progress" | "completed" | "failed";
output?: ResponsesApiOutputItem[];
output_text?: string;
error?: {
message: string;
code: string;
};
}
/**
* Raw output item from the Responses API
*/
interface ResponsesApiOutputItem {
id?: string;
type: string;
status?: string;
action?: {
type: string;
query?: string;
url?: string;
};
query?: string;
results?: Array<{
file_id: string;
file_name: string;
score: number;
}>;
code?: string;
output?: string;
server_label?: string;
tool_name?: string;
arguments?: Record<string, unknown>;
result?: unknown;
content?: Array<{
type: string;
text: string;
annotations?: Array<{
url: string;
title: string;
start_index: number;
end_index: number;
}>;
}>;
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Converts ResearchTool params to Responses API tool format
*/
function convertToolToApi(tool: ResearchTool): ResponsesApiTool {
switch (tool.type) {
case "web_search_preview":
return { type: "web_search_preview" };
case "file_search":
return {
type: "file_search",
vector_store_ids: tool.vectorStoreIds,
};
case "code_interpreter":
return {
type: "code_interpreter",
container: tool.container ?? { type: "auto" },
};
case "mcp":
return {
type: "mcp",
server_label: tool.serverLabel,
server_url: tool.serverUrl,
require_approval: tool.requireApproval ?? "never",
};
default:
throw new Error(`Unknown research tool type: ${(tool as ResearchTool).type}`);
}
}
/**
* Converts raw API output items to typed ResearchOutputItem
*/
function convertOutputItem(item: ResponsesApiOutputItem): ResearchOutputItem | null {
switch (item.type) {
case "web_search_call":
return {
id: item.id ?? "",
type: "web_search_call",
status: (item.status as "completed" | "failed") ?? "completed",
action: {
type: (item.action?.type as "search" | "open_page" | "find_in_page") ?? "search",
query: item.action?.query,
url: item.action?.url,
},
} satisfies ResearchWebSearchCall;
case "file_search_call":
return {
id: item.id ?? "",
type: "file_search_call",
status: (item.status as "completed" | "failed") ?? "completed",
query: item.query ?? "",
results: item.results?.map((r) => ({
fileId: r.file_id,
fileName: r.file_name,
score: r.score,
})),
} satisfies ResearchFileSearchCall;
case "code_interpreter_call":
return {
id: item.id ?? "",
type: "code_interpreter_call",
status: (item.status as "completed" | "failed") ?? "completed",
code: item.code ?? "",
output: item.output,
} satisfies ResearchCodeInterpreterCall;
case "mcp_tool_call":
return {
id: item.id ?? "",
type: "mcp_tool_call",
status: (item.status as "completed" | "failed") ?? "completed",
serverLabel: item.server_label ?? "",
toolName: item.tool_name ?? "",
arguments: (item.arguments ?? {}) as Record<string, JsonValue>,
result: item.result as JsonValue,
} satisfies ResearchMcpToolCall;
case "message":
return {
type: "message",
content:
item.content?.map((c) => ({
type: "output_text" as const,
text: c.text,
annotations:
c.annotations?.map((a) => ({
url: a.url,
title: a.title,
startIndex: a.start_index,
endIndex: a.end_index,
})) ?? [],
})) ?? [],
} satisfies ResearchMessageOutput;
default:
// Unknown output type, skip
return null;
}
}
/**
* Extracts text and annotations from the response
*/
function extractTextAndAnnotations(response: ResponsesApiResponse): {
text: string;
annotations: ResearchAnnotation[];
} {
// Try output_text first (convenience field)
if (response.output_text) {
// Find annotations from message output items
const annotations: ResearchAnnotation[] = [];
if (response.output) {
for (const item of response.output) {
if (item.type === "message" && item.content) {
for (const content of item.content) {
if (content.annotations) {
for (const ann of content.annotations) {
annotations.push({
url: ann.url,
title: ann.title,
startIndex: ann.start_index,
endIndex: ann.end_index,
});
}
}
}
}
}
}
return { text: response.output_text, annotations };
}
// Fall back to extracting from message output items
let text = "";
const annotations: ResearchAnnotation[] = [];
if (response.output) {
for (const item of response.output) {
if (item.type === "message" && item.content) {
for (const content of item.content) {
text += content.text;
if (content.annotations) {
for (const ann of content.annotations) {
annotations.push({
url: ann.url,
title: ann.title,
startIndex: ann.start_index,
endIndex: ann.end_index,
});
}
}
}
}
}
}
return { text, annotations };
}
// ============================================================================
// Main Handler
// ============================================================================
/**
* Handles RESEARCH model requests using OpenAI's deep research models.
*
* Deep research models can take tens of minutes to complete tasks.
* Use background mode for long-running tasks.
*
* @param runtime - The agent runtime
* @param params - Research parameters
* @returns Research result with text, annotations, and output items
*
* @example
* ```typescript
* const result = await handleResearch(runtime, {
* input: "Research the economic impact of AI on global labor markets",
* tools: [
* { type: "web_search_preview" },
* { type: "code_interpreter", container: { type: "auto" } }
* ],
* background: true,
* });
* console.log(result.text);
* ```
*/
export async function handleResearch(
runtime: IAgentRuntime,
params: ResearchParams
): Promise<ResearchResult> {
const apiKey = getApiKey(runtime);
if (!apiKey) {
throw new Error(
"OPENAI_API_KEY is required for deep research. Set it in your environment variables or runtime settings."
);
}
const baseURL = getBaseURL(runtime);
const modelName = params.model ?? getResearchModel(runtime);
const timeout = getResearchTimeout(runtime);
logger.debug(`[OpenAI] Starting deep research with model: ${modelName}`);
logger.debug(`[OpenAI] Research input: ${params.input.substring(0, 100)}...`);
// Validate that at least one data source tool is provided
const dataSourceTools = params.tools?.filter(
(t) => t.type === "web_search_preview" || t.type === "file_search" || t.type === "mcp"
);
if (!dataSourceTools || dataSourceTools.length === 0) {
// Default to web search if no tools specified
logger.debug("[OpenAI] No data source tools specified, defaulting to web_search_preview");
params.tools = [{ type: "web_search_preview" }, ...(params.tools ?? [])];
}
// Build the request body for the Responses API
const requestBody: Record<string, unknown> = {
model: modelName,
input: params.input,
};
if (params.instructions) {
requestBody.instructions = params.instructions;
}
if (params.background !== undefined) {
requestBody.background = params.background;
}
if (params.tools && params.tools.length > 0) {
requestBody.tools = params.tools.map(convertToolToApi);
}
if (params.maxToolCalls !== undefined) {
requestBody.max_tool_calls = params.maxToolCalls;
}
if (params.reasoningSummary) {
requestBody.reasoning = { summary: params.reasoningSummary };
}
logger.debug(`[OpenAI] Research request body: ${JSON.stringify(requestBody, null, 2)}`);
const details: RecordLlmCallDetails = {
model: modelName,
systemPrompt: params.instructions ?? "",
userPrompt: params.input,
temperature: 0,
maxTokens: 0,
purpose: "external_llm",
actionType: "openai.responses.create",
};
const data = await recordLlmCall(runtime, details, async () => {
// Make the API request
const response = await fetch(`${baseURL}/responses`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
const errorText = await response.text();
logger.error(`[OpenAI] Research request failed: ${response.status} ${errorText}`);
throw new Error(`Deep research request failed: ${response.status} ${response.statusText}`);
}
const responseData = (await response.json()) as ResponsesApiResponse;
details.response = responseData.output_text ?? "";
return responseData;
});
if (data.error) {
logger.error(`[OpenAI] Research API error: ${data.error.message}`);
throw new Error(`Deep research error: ${data.error.message}`);
}
logger.debug(`[OpenAI] Research response received. Status: ${data.status ?? "completed"}`);
// Extract text and annotations
const { text, annotations } = extractTextAndAnnotations(data);
// Convert output items
const outputItems: ResearchOutputItem[] = [];
if (data.output) {
for (const item of data.output) {
const converted = convertOutputItem(item);
if (converted) {
outputItems.push(converted);
}
}
}
const result: ResearchResult = {
id: data.id,
text,
annotations,
outputItems,
status: data.status,
};
logger.info(
`[OpenAI] Research completed. Text length: ${text.length}, Annotations: ${annotations.length}, Output items: ${outputItems.length}`
);
return result;
}