Packages
langchain
0.4.0
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/research_result.ex
defmodule LangChain.Tools.DeepResearch.ResearchResult do
@moduledoc """
Represents the final result of a completed Deep Research request.
This schema captures the research findings, citations, usage statistics,
and other metadata from a successful research operation.
"""
use Ecto.Schema
import Ecto.Changeset
@type t() :: %__MODULE__{
id: String.t(),
output_text: String.t(),
model: String.t() | nil,
created_at: integer() | nil,
completion_time: integer() | nil,
sources: [any()],
usage: any() | nil,
tool_calls: [any()]
}
@primary_key false
embedded_schema do
field :id, :string
field :output_text, :string
field :model, :string
field :created_at, :integer
field :completion_time, :integer
embeds_many :sources, Source do
field :title, :string
field :url, :string
field :start_index, :integer
field :end_index, :integer
field :snippet, :string
end
embeds_one :usage, Usage do
field :input_tokens, :integer
field :output_tokens, :integer
field :total_tokens, :integer
field :reasoning_tokens, :integer
end
embeds_many :tool_calls, ToolCall do
field :type, :string
field :status, :string
field :action, :map
field :result, :map
end
end
@doc """
Creates a changeset for research result.
"""
@spec changeset(__MODULE__.t(), map()) :: Ecto.Changeset.t()
def changeset(result \\ %__MODULE__{}, attrs) do
result
|> cast(attrs, [:id, :output_text, :model, :created_at, :completion_time])
|> cast_embed(:sources, with: &source_changeset/2)
|> cast_embed(:usage, with: &usage_changeset/2)
|> cast_embed(:tool_calls, with: &tool_call_changeset/2)
|> validate_required([:id, :output_text])
end
@doc """
Creates a ResearchResult from an OpenAI API response.
"""
@spec from_api_response(map()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()}
def from_api_response(response) do
attrs = %{
id: Map.get(response, "id"),
output_text: extract_output_text(response),
model: Map.get(response, "model"),
created_at: Map.get(response, "created_at"),
completion_time: calculate_completion_time(response),
sources: extract_sources(response),
usage: Map.get(response, "usage"),
tool_calls: extract_tool_calls(response)
}
changeset = changeset(%__MODULE__{}, attrs)
if changeset.valid? do
{:ok, apply_changes(changeset)}
else
{:error, changeset}
end
end
@doc """
Gets the total number of sources cited in the research.
"""
@spec source_count(__MODULE__.t()) :: integer()
def source_count(%__MODULE__{sources: sources}), do: length(sources)
@doc """
Gets the total number of tool calls made during research.
"""
@spec tool_call_count(__MODULE__.t()) :: integer()
def tool_call_count(%__MODULE__{tool_calls: tool_calls}), do: length(tool_calls)
@doc """
Formats the result for display, including the main text and source summary.
"""
@spec format_for_display(__MODULE__.t()) :: String.t()
def format_for_display(%__MODULE__{} = result) do
source_summary = format_sources(result.sources)
output = """
## Research Findings
#{result.output_text}
"""
if source_summary != "" do
output <> "\n\n## Sources\n\n#{source_summary}"
else
output
end
end
@doc """
Extracts just the URLs from the sources for easy reference.
"""
@spec source_urls(__MODULE__.t()) :: [String.t()]
def source_urls(%__MODULE__{sources: sources}) do
Enum.map(sources, & &1.url)
end
# Private functions
@spec source_changeset(map(), map()) :: Ecto.Changeset.t()
defp source_changeset(source, attrs) do
source
|> cast(attrs, [:title, :url, :start_index, :end_index, :snippet])
|> validate_required([:url])
end
@spec usage_changeset(map(), map()) :: Ecto.Changeset.t()
defp usage_changeset(usage, attrs) do
usage
|> cast(attrs, [:input_tokens, :output_tokens, :total_tokens, :reasoning_tokens])
|> validate_number(:input_tokens, greater_than_or_equal_to: 0)
|> validate_number(:output_tokens, greater_than_or_equal_to: 0)
|> validate_number(:total_tokens, greater_than_or_equal_to: 0)
|> validate_number(:reasoning_tokens, greater_than_or_equal_to: 0)
end
@spec tool_call_changeset(map(), map()) :: Ecto.Changeset.t()
defp tool_call_changeset(tool_call, attrs) do
tool_call
|> cast(attrs, [:type, :status, :action, :result])
|> validate_required([:type])
|> validate_inclusion(:type, ["web_search_call", "code_interpreter_call", "mcp_tool_call"])
end
@spec extract_output_text(map()) :: String.t()
defp extract_output_text(response) do
response
|> get_in(["output"])
|> case do
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
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
_ ->
"No output available"
end
end
@spec extract_sources(map()) :: [map()]
defp extract_sources(response) do
# Extract sources from annotations in the output
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)
|> Enum.map(&normalize_source/1)
_ ->
[]
end
end
@spec normalize_source(map()) :: map()
defp normalize_source(annotation) do
%{
title: Map.get(annotation, "title"),
url: Map.get(annotation, "url"),
start_index: Map.get(annotation, "start_index"),
end_index: Map.get(annotation, "end_index"),
snippet: Map.get(annotation, "snippet")
}
end
@spec extract_tool_calls(map()) :: [map()]
defp extract_tool_calls(response) do
response
|> get_in(["output"])
|> case do
output when is_list(output) ->
output
|> Enum.filter(
&(Map.get(&1, "type") in ["web_search_call", "code_interpreter_call", "mcp_tool_call"])
)
|> Enum.map(&normalize_tool_call/1)
_ ->
[]
end
end
@spec normalize_tool_call(map()) :: map()
defp normalize_tool_call(tool_call) do
%{
type: Map.get(tool_call, "type"),
status: Map.get(tool_call, "status"),
action: Map.get(tool_call, "action"),
result: Map.get(tool_call, "result")
}
end
@spec calculate_completion_time(map()) :: integer() | nil
defp calculate_completion_time(_response) do
# If we have completion timestamp info, calculate duration
# For now, return nil as this would need to be tracked externally
nil
end
@spec format_sources([map()]) :: String.t()
defp format_sources(sources) when is_list(sources) do
sources
|> Enum.with_index(1)
|> Enum.map(fn {source, index} ->
title = source.title || "Untitled"
url = source.url || "No URL"
"#{index}. #{title} - #{url}"
end)
|> Enum.join("\n")
end
defp format_sources(_), do: ""
end