Packages
langchain
0.9.2
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/tools/deep_research_client.ex
defmodule LangChain.Tools.DeepResearchClient do
@moduledoc """
HTTP client for OpenAI Deep Research API.
This module handles the three main operations for OpenAI Deep Research:
1. Creating a research request
2. Checking request status
3. Retrieving completed results
Uses the same authentication and HTTP client patterns as other OpenAI integrations
in LangChain, reusing the existing `:openai_key` configuration.
## Configuration
The endpoint can be customized for alternative providers (Azure, OpenRouter, etc.):
# In config.exs or runtime.exs
config :langchain, :deep_research_endpoint, "https://custom-endpoint.com/v1/responses"
If not configured, defaults to OpenAI's standard endpoint.
"""
require Logger
alias LangChain.Config
@doc """
Creates a new deep research request.
## Parameters
- `query`: The research question or topic
- `options`: Optional parameters including:
- `:model` - The model to use (defaults to "o3-deep-research-2025-06-26")
- `:system_message` - Optional guidance for research approach
- `:max_tool_calls` - Maximum number of tool calls to make
- `:summary` - Summary mode: "auto" or "detailed" (defaults to "auto")
- `:include_code_interpreter` - Include code interpreter tool (defaults to true)
- `:endpoint` - Custom endpoint URL (defaults to "https://api.openai.com/v1/responses")
## Returns
- `{:ok, request_id}` on success
- `{:error, reason}` on failure
"""
@spec create_research(String.t(), map()) :: {:ok, String.t()} | {:error, String.t()}
def create_research(query, options \\ %{}) do
model = Map.get(options, :model, "o3-deep-research-2025-06-26")
system_message = Map.get(options, :system_message)
max_tool_calls = Map.get(options, :max_tool_calls)
summary = Map.get(options, :summary, "auto")
include_code_interpreter = Map.get(options, :include_code_interpreter, true)
endpoint = Map.get(options, :endpoint, get_default_endpoint())
# Build the request body according to OpenAI Deep Research API
request_body =
build_request_body(
query,
model,
system_message,
max_tool_calls,
summary,
include_code_interpreter
)
Logger.debug("Creating deep research request with body: #{inspect(request_body)}")
case make_request(:post, endpoint, request_body) do
{:ok, %{"id" => request_id}} ->
{:ok, request_id}
{:ok, _response} ->
{:error, "Unexpected response format from OpenAI API"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Checks the status of a deep research request.
## Parameters
- `request_id`: The ID returned from create_research/2
- `endpoint`: Optional custom endpoint URL
## Returns
- `{:ok, status_map}` containing status information
- `{:error, reason}` on failure
"""
@spec check_status(String.t(), String.t() | nil) :: {:ok, map()} | {:error, String.t()}
def check_status(request_id, endpoint \\ nil) do
base_url = endpoint || get_default_endpoint()
url = "#{base_url}/#{request_id}"
case make_request(:get, url) do
{:ok, response} ->
status_map = %{
status: Map.get(response, "status"),
error: get_in(response, ["error", "message"])
}
{:ok, status_map}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Retrieves the results of a completed deep research request.
## Parameters
- `request_id`: The ID of the completed research request
- `endpoint`: Optional custom endpoint URL
## Returns
- `{:ok, result_map}` containing the research findings and metadata
- `{:error, reason}` on failure
"""
@spec get_results(String.t(), String.t() | nil) :: {:ok, map()} | {:error, String.t()}
def get_results(request_id, endpoint \\ nil) do
base_url = endpoint || get_default_endpoint()
url = "#{base_url}/#{request_id}"
case make_request(:get, url) do
{:ok, response} ->
case Map.get(response, "status") do
"completed" ->
result = extract_research_result(response)
{:ok, result}
other_status ->
{:error, "Research not completed, status: #{other_status}"}
end
{:error, reason} ->
{:error, reason}
end
end
# Private functions
@spec build_request_body(
String.t(),
String.t(),
String.t() | nil,
integer() | nil,
String.t(),
boolean()
) :: map()
defp build_request_body(
query,
model,
system_message,
max_tool_calls,
summary,
include_code_interpreter
) do
# Build tools array based on include_code_interpreter
tools =
if include_code_interpreter do
[
%{type: "web_search_preview"},
%{type: "code_interpreter", container: %{type: "auto", file_ids: []}}
]
else
[%{type: "web_search_preview"}]
end
base_body = %{
model: model,
input: query,
background: true,
tools: tools,
reasoning: %{summary: summary}
}
# Add optional parameters if provided
base_body
|> maybe_add_field(:instructions, system_message)
|> maybe_add_field(:max_tool_calls, max_tool_calls)
end
@spec maybe_add_field(map(), atom(), any()) :: map()
defp maybe_add_field(map, _key, nil), do: map
defp maybe_add_field(map, key, value), do: Map.put(map, key, value)
@spec extract_research_result(map()) :: map()
defp extract_research_result(response) do
# Extract the main research text from the output array
output_text = extract_output_text(response)
# Extract source information from annotations or web search calls
sources = extract_sources(response)
%{
output_text: output_text,
sources: sources,
usage: Map.get(response, "usage"),
model: Map.get(response, "model")
}
end
@spec extract_output_text(map()) :: String.t()
defp extract_output_text(response) do
response
|> get_in(["output"])
|> case do
nil ->
"No output available"
output when is_list(output) ->
output
|> Enum.find(&(Map.get(&1, "type") == "message"))
|> case do
nil ->
"No message output found"
message ->
message
|> get_in(["content"])
|> case do
nil ->
"No content found"
content when is_list(content) ->
content
|> Enum.find(&(Map.get(&1, "type") == "output_text"))
|> case do
nil -> "No text content found"
text_content -> Map.get(text_content, "text", "No text available")
end
_ ->
"Invalid content format"
end
end
_ ->
"Invalid output format"
end
end
@spec extract_sources(map()) :: list()
defp extract_sources(response) do
# Look for sources in multiple places: annotations and web search calls
annotation_sources = extract_annotation_sources(response)
web_search_sources = extract_web_search_sources(response)
(annotation_sources ++ web_search_sources)
|> Enum.uniq_by(&Map.get(&1, "url"))
end
@spec extract_annotation_sources(map()) :: list()
defp extract_annotation_sources(response) do
response
|> get_in(["output"])
|> case do
output when is_list(output) ->
output
|> Enum.flat_map(fn item ->
item
|> get_in(["content"])
|> case do
content when is_list(content) ->
content
|> Enum.flat_map(fn content_item ->
Map.get(content_item, "annotations", [])
end)
_ ->
[]
end
end)
_ ->
[]
end
end
@spec extract_web_search_sources(map()) :: list()
defp extract_web_search_sources(response) do
response
|> get_in(["output"])
|> case do
output when is_list(output) ->
output
|> Enum.filter(&(Map.get(&1, "type") == "web_search_call"))
|> Enum.flat_map(&extract_sources_from_search_call/1)
_ ->
[]
end
end
@spec extract_sources_from_search_call(map()) :: list()
defp extract_sources_from_search_call(_search_call) do
# This would need to be refined based on actual API response structure
# For now, return empty list as we'll primarily rely on annotations
[]
end
@spec make_request(atom(), String.t(), map() | nil) :: {:ok, map()} | {:error, String.t()}
defp make_request(method, url, body \\ nil) do
headers = build_headers()
request_options = [
method: method,
url: url,
headers: headers,
# 30 seconds for API calls
receive_timeout: 30_000,
# Disable Req-level retry. See https://github.com/brainlid/langchain/issues/503
retry: false
]
request_options =
if body do
Keyword.put(request_options, :json, body)
else
request_options
end
Logger.debug("Making #{method} request to #{url}")
case Req.request(request_options) do
{:ok, %Req.Response{status: status, body: response_body}} when status in 200..299 ->
{:ok, response_body}
{:ok, %Req.Response{status: status, body: error_body}} ->
{:error, extract_error_message(error_body, status)}
{:error, %Req.TransportError{reason: reason}} ->
{:error, "Network error: #{inspect(reason)}"}
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
@spec build_headers() :: list()
defp build_headers() do
api_key = get_api_key()
[
{"authorization", "Bearer #{api_key}"},
{"content-type", "application/json"},
{"user-agent", "LangChain-Elixir/1.0"}
]
end
@spec get_api_key() :: String.t()
defp get_api_key() do
# Reuse the same OpenAI API key resolution as ChatOpenAI
Config.resolve(:openai_key, "")
end
@spec get_default_endpoint() :: String.t()
defp get_default_endpoint() do
# Check for configured endpoint, otherwise use OpenAI default
Config.resolve(:deep_research_endpoint, "https://api.openai.com/v1/responses")
end
@spec extract_error_message(map() | String.t(), integer()) :: String.t()
defp extract_error_message(error_body, status) when is_map(error_body) do
case get_in(error_body, ["error", "message"]) do
nil -> "HTTP #{status}: #{inspect(error_body)}"
message -> "HTTP #{status}: #{message}"
end
end
defp extract_error_message(error_body, status) do
"HTTP #{status}: #{inspect(error_body)}"
end
end