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
411 lines
12 KiB
TypeScript
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;
|
|
}
|