Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions lib/chat_models/chat_anthropic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1463,8 +1463,6 @@ defmodule LangChain.ChatModels.ChatAnthropic do
})
end

defp get_token_usage(_usage_data), do: nil

@doc """
Generate a config map that can later restore the model's configuration.
"""
Expand Down
12 changes: 11 additions & 1 deletion lib/chat_models/chat_google_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,18 @@ defmodule LangChain.ChatModels.ChatGoogleAI do
nil ->
nil

%Message{role: :system, content: content} ->
%Message{role: :system, content: content} when is_binary(content) ->
%{"parts" => [%{"text" => content}]}

%Message{role: :system, content: content} when is_list(content) ->
# Extract text from ContentPart structures
text_content =
content
|> Enum.filter(&match?(%ContentPart{type: :text}, &1))
|> Enum.map(& &1.content)
|> Enum.join(" ")

%{"parts" => [%{"text" => text_content}]}
end

messages_for_api =
Expand Down
79 changes: 74 additions & 5 deletions lib/chat_models/chat_mistral_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ defmodule LangChain.ChatModels.ChatMistralAI do
# For choosing a specific tool call (like forcing a function execution).
field :tool_choice, :map

# JSON Schema to validate the output format (for structured JSON output)
field :json_schema, :map

# Whether to force a JSON response format
field :json_response, :boolean, default: false

# A list of callback handlers
field :callbacks, {:array, :map}, default: []
end
Expand All @@ -73,7 +79,9 @@ defmodule LangChain.ChatModels.ChatMistralAI do
:safe_prompt,
:random_seed,
:stream,
:tool_choice
:tool_choice,
:json_schema,
:json_response
]
@required_fields [
:model
Expand Down Expand Up @@ -126,6 +134,38 @@ defmodule LangChain.ChatModels.ChatMistralAI do
|> Utils.conditionally_add_to_map(:max_tokens, mistral.max_tokens)
|> Utils.conditionally_add_to_map(:tools, get_tools_for_api(mistral, tools))
|> Utils.conditionally_add_to_map(:tool_choice, get_tool_choice(mistral))
|> Utils.conditionally_add_to_map(:response_format, set_response_format(mistral))
end

# Creates the response_format field for JSON output when json_response is true.
# If json_schema is provided, it will be included in the response format.
#
# For Mistral, the format is as follows:
# https://docs.mistral.ai/capabilities/structured-output/custom_structured_output/
# {
# "type": "json_schema",
# "json_schema": {
# "schema": { ... },
# "name": "output",
# "strict": true
# }
# }
@spec set_response_format(t()) :: map() | nil
defp set_response_format(%ChatMistralAI{json_response: true, json_schema: schema})
when is_map(schema) and map_size(schema) > 0 do
# The schema should already be in the correct format
schema
end

defp set_response_format(%ChatMistralAI{json_response: true}) do
# For Mistral, when no schema is provided, we use json_object type
%{
"type" => "json_object"
}
end

defp set_response_format(%ChatMistralAI{}) do
nil
end

# Add a more complete function to map tools. This mirrors ChatOpenAI approach.
Expand Down Expand Up @@ -179,23 +219,50 @@ defmodule LangChain.ChatModels.ChatMistralAI do

def for_api(%_{} = model, %Message{role: :assistant, tool_calls: tool_calls} = msg)
when is_list(tool_calls) do
content =
case msg.content do
content when is_binary(content) -> content
content when is_list(content) -> ContentPart.parts_to_string(content)
nil -> nil
end

%{
"role" => :assistant,
"content" => msg.content
"content" => content
}
|> Utils.conditionally_add_to_map("tool_calls", Enum.map(tool_calls, &for_api(model, &1)))
end

def for_api(%_{} = model, %Message{role: :user, content: content} = msg)
def for_api(%_{} = _model, %Message{role: :user, content: content} = msg)
when is_list(content) do
# A user message can hold an array of ContentParts
%{
"role" => msg.role,
"content" => Enum.map(content, &for_api(model, &1))
"content" => ContentPart.parts_to_string(content)
}
|> Utils.conditionally_add_to_map("name", msg.name)
end

# Handle messages with ContentPart content for non-user roles
def for_api(%_{} = model, %Message{content: content} = msg) when is_list(content) do
role = get_message_role(model, msg.role)

%{
"role" => role,
"content" => ContentPart.parts_to_string(content)
}
|> Utils.conditionally_add_to_map("name", msg.name)
|> Utils.conditionally_add_to_map(
"tool_calls",
Enum.map(msg.tool_calls || [], &for_api(model, &1))
)
end

# Handle ContentPart structures
def for_api(%_{} = _model, %ContentPart{type: :text, content: content}) do
content
end

# ToolResult => stand-alone message with "role: :tool"
def for_api(%_{} = _model, %ToolResult{type: :function} = result) do
%{
Expand Down Expand Up @@ -649,7 +716,9 @@ defmodule LangChain.ChatModels.ChatMistralAI do
:max_tokens,
:safe_prompt,
:random_seed,
:stream
:stream,
:json_schema,
:json_response
],
@current_config_version
)
Expand Down
26 changes: 24 additions & 2 deletions lib/chat_models/chat_ollama_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ defmodule LangChain.ChatModels.ChatOllamaAI do
alias LangChain.ChatModels.ChatModel
alias LangChain.ChatModels.ChatOpenAI
alias LangChain.Message
alias LangChain.Message.ContentPart
alias LangChain.Message.ToolCall
alias LangChain.Message.ToolResult
alias LangChain.MessageDelta
Expand Down Expand Up @@ -242,9 +243,16 @@ defmodule LangChain.ChatModels.ChatOllamaAI do

def for_api(%Message{role: :assistant, tool_calls: tool_calls} = msg)
when is_list(tool_calls) do
content =
case msg.content do
content when is_binary(content) -> content
content when is_list(content) -> ContentPart.parts_to_string(content)
nil -> nil
end

%{
"role" => :assistant,
"content" => msg.content
"content" => content
}
|> Utils.conditionally_add_to_map("tool_calls", Enum.map(tool_calls, &for_api(&1)))
end
Expand Down Expand Up @@ -292,11 +300,25 @@ defmodule LangChain.ChatModels.ChatOllamaAI do
def for_api(%Message{role: :user, content: content} = msg) when is_list(content) do
%{
"role" => msg.role,
"content" => Enum.map(content, &for_api(&1))
"content" => ContentPart.parts_to_string(content)
}
|> Utils.conditionally_add_to_map("name", msg.name)
end

# Handle messages with ContentPart content for non-user roles
def for_api(%Message{content: content} = msg) when is_list(content) do
%{
"role" => msg.role,
"content" => ContentPart.parts_to_string(content)
}
|> Utils.conditionally_add_to_map("name", msg.name)
end

# Handle ContentPart structures
def for_api(%ContentPart{type: :text, content: content}) do
content
end

defp get_tools_for_api(nil), do: []

defp get_tools_for_api(tools) do
Expand Down
5 changes: 5 additions & 0 deletions lib/chat_models/chat_open_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ defmodule LangChain.ChatModels.ChatOpenAI do
raise LangChainError, "PromptTemplates must be converted to messages."
end

# Handle ContentPart structures directly
def for_api(%_{} = model, %ContentPart{} = part) do
content_part_for_api(model, part)
end

@doc """
Convert a list of ContentParts to the expected map of data for the OpenAI API.
"""
Expand Down
10 changes: 9 additions & 1 deletion lib/chat_models/chat_perplexity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ defmodule LangChain.ChatModels.ChatPerplexity do
alias LangChain.ChatModels.ChatModel
alias LangChain.Message
alias LangChain.MessageDelta
alias LangChain.Message.ContentPart
alias LangChain.Message.ToolCall
alias LangChain.TokenUsage
alias LangChain.LangChainError
Expand Down Expand Up @@ -283,9 +284,16 @@ defmodule LangChain.ChatModels.ChatPerplexity do
"""
@spec for_api(t(), Message.t()) :: %{String.t() => any()}
def for_api(%ChatPerplexity{}, %Message{} = msg) do
content =
case msg.content do
content when is_binary(content) -> content
content when is_list(content) -> ContentPart.parts_to_string(content)
nil -> nil
end

%{
"role" => msg.role,
"content" => msg.content
"content" => content
}
end

Expand Down
16 changes: 11 additions & 5 deletions lib/chat_models/chat_vertex_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,26 @@ defmodule LangChain.ChatModels.ChatVertexAI do
end

defp for_api(%Message{role: :system} = message) do
%{"parts" => %{"text" => message.content}}
# System messages should return a single text part, not a list
case get_message_contents(message) do
[%{"text" => text}] -> %{"parts" => %{"text" => text}}
_ -> %{"parts" => %{"text" => message.content}}
end
end

defp for_api(%Message{role: :user, content: content}) when is_list(content) do
%{
"role" => "user",
"role" => map_role(:user),
"parts" => Enum.map(content, &for_api(&1))
}
end

defp for_api(%Message{} = message) do
content_parts = get_message_contents(message) || []

%{
"role" => map_role(message.role),
"parts" => [%{"text" => message.content}]
"parts" => content_parts
}
end

Expand Down Expand Up @@ -257,9 +263,9 @@ defmodule LangChain.ChatModels.ChatVertexAI do

defp for_api(%ContentPart{type: :file_url} = part) do
%{
"file_data" => %{
"fileData" => %{
"mimeType" => Keyword.fetch!(part.options, :media),
"file_uri" => part.content
"fileUri" => part.content
}
}
end
Expand Down
Loading