Packages
nous
0.12.7
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/messages.ex
defmodule Nous.Messages do
@moduledoc """
Utilities for working with conversations and message lists.
This module provides functions to:
- Work with lists of messages (conversations)
- Convert between internal format and provider-specific formats
- Extract data from conversations
- Parse provider responses into internal format
## Message Format
We use `Nous.Message` structs with standard roles:
- `%Message{role: :system}` - System instructions
- `%Message{role: :user}` - User input (text or multi-modal)
- `%Message{role: :assistant}` - AI responses (with optional tool calls)
- `%Message{role: :tool}` - Tool execution results
## Example
# Build conversation
conversation = [
Message.system("You are a helpful assistant"),
Message.user("What is 2+2?"),
Message.assistant("2+2 equals 4")
]
# Convert to provider format
openai_messages = Messages.to_openai_format(conversation)
anthropic_messages = Messages.to_anthropic_format(conversation)
# Parse provider response
response = Messages.from_openai_response(openai_response)
# => %Message{role: :assistant, content: "4"}
"""
require Logger
alias Nous.Message
alias Nous.Messages.{OpenAI, Anthropic, Gemini}
# Conversation utilities
@doc """
Extract text content from messages in a conversation.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.extract_text(conversation)
["Be helpful", "Hello"]
iex> message = Message.user("Hi there")
iex> Messages.extract_text(message)
"Hi there"
"""
@spec extract_text([Message.t()] | Message.t()) :: [String.t()] | String.t()
def extract_text(messages) when is_list(messages) do
Enum.map(messages, &Message.extract_text/1)
end
def extract_text(%Message{} = message) do
Message.extract_text(message)
end
@doc """
Extract tool calls from a conversation.
Returns all tool calls found in assistant messages.
## Examples
iex> conversation = [
...> Message.assistant("Let me search", tool_calls: [%{id: "call_1", name: "search", arguments: %{}}])
...> ]
iex> Messages.extract_tool_calls(conversation)
[%{id: "call_1", name: "search", arguments: %{}}]
"""
@spec extract_tool_calls([Message.t()]) :: [map()]
def extract_tool_calls(messages) when is_list(messages) do
messages
|> Enum.filter(&Message.from_assistant?/1)
|> Enum.flat_map(& &1.tool_calls)
end
@doc """
Find messages by role in a conversation.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.find_by_role(conversation, :system)
[%Message{role: :system, content: "Be helpful"}]
"""
@spec find_by_role([Message.t()], atom()) :: [Message.t()]
def find_by_role(messages, role) when is_list(messages) do
Enum.filter(messages, &(&1.role == role))
end
@doc """
Get the last message from a conversation.
## Examples
iex> conversation = [Message.user("Hi"), Message.assistant("Hello")]
iex> Messages.last_message(conversation)
%Message{role: :assistant, content: "Hello"}
"""
@spec last_message([Message.t()]) :: Message.t() | nil
def last_message([]), do: nil
def last_message(messages) when is_list(messages), do: List.last(messages)
@doc """
Count messages by role.
## Examples
iex> conversation = [Message.system("Hi"), Message.user("Hello"), Message.user("World")]
iex> Messages.count_by_role(conversation)
%{system: 1, user: 2, assistant: 0, tool: 0}
"""
@spec count_by_role([Message.t()]) :: map()
def count_by_role(messages) when is_list(messages) do
base_counts = %{system: 0, user: 0, assistant: 0, tool: 0}
messages
|> Enum.group_by(& &1.role)
|> Enum.map(fn {role, msgs} -> {role, length(msgs)} end)
|> Map.new()
|> then(&Map.merge(base_counts, &1))
end
# Provider format conversion
@doc """
Convert messages to OpenAI format.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.to_openai_format(conversation)
[
%{"role" => "system", "content" => "Be helpful"},
%{"role" => "user", "content" => "Hello"}
]
"""
@spec to_openai_format([Message.t()]) :: [map()]
defdelegate to_openai_format(messages), to: OpenAI, as: :to_format
@doc """
Convert messages to Anthropic format.
Returns `{system_prompt, messages}` where system prompt is extracted
and combined, and messages are converted to Anthropic format.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.to_anthropic_format(conversation)
{"Be helpful", [%{"role" => "user", "content" => "Hello"}]}
"""
@spec to_anthropic_format([Message.t()]) :: {String.t() | nil, [map()]}
defdelegate to_anthropic_format(messages), to: Anthropic, as: :to_format
@doc """
Convert messages to Gemini format.
Returns `{system_prompt, contents}` where system prompt is extracted
and messages are converted to Gemini contents format.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.to_gemini_format(conversation)
{"Be helpful", [%{"role" => "user", "parts" => [%{"text" => "Hello"}]}]}
"""
@spec to_gemini_format([Message.t()]) :: {String.t() | nil, [map()]}
defdelegate to_gemini_format(messages), to: Gemini, as: :to_format
@doc """
Convert messages to provider-specific format.
Dispatches to the appropriate provider-specific conversion function.
## Examples
iex> conversation = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.to_provider_format(conversation, :openai)
[%{"role" => "system", "content" => "Be helpful"}, %{"role" => "user", "content" => "Hello"}]
iex> Messages.to_provider_format(conversation, :anthropic)
{"Be helpful", [%{"role" => "user", "content" => "Hello"}]}
"""
@spec to_provider_format([Message.t()], atom()) :: any()
def to_provider_format(messages, provider) when is_list(messages) do
case provider do
:openai ->
to_openai_format(messages)
:openai_compatible ->
to_openai_format(messages)
:groq ->
to_openai_format(messages)
:lmstudio ->
to_openai_format(messages)
:ollama ->
to_openai_format(messages)
:openrouter ->
to_openai_format(messages)
:together ->
to_openai_format(messages)
:vllm ->
to_openai_format(messages)
:sglang ->
to_openai_format(messages)
:anthropic ->
to_anthropic_format(messages)
:gemini ->
to_gemini_format(messages)
:vertex_ai ->
to_gemini_format(messages)
:mistral ->
to_openai_format(messages)
:llamacpp ->
to_openai_format(messages)
:custom ->
to_openai_format(messages)
_ ->
raise ArgumentError, """
Unsupported provider: #{inspect(provider)}
Supported providers: :openai, :openai_compatible, :groq, :lmstudio, :llamacpp, :vllm, :sglang, :anthropic, :gemini, :vertex_ai, :mistral
"""
end
end
# Response parsing
@doc """
Parse OpenAI response into a Message.
## Examples
iex> openai_response = %{
...> "choices" => [%{"message" => %{"role" => "assistant", "content" => "Hello"}}],
...> "usage" => %{"total_tokens" => 10}
...> }
iex> Messages.from_openai_response(openai_response)
%Message{role: :assistant, content: "Hello"}
"""
@spec from_openai_response(map()) :: Message.t()
defdelegate from_openai_response(response), to: OpenAI, as: :from_response
@doc """
Parse Anthropic response into a Message.
## Examples
iex> anthropic_response = %{
...> "content" => [%{"type" => "text", "text" => "Hello"}],
...> "model" => "claude-3-sonnet"
...> }
iex> Messages.from_anthropic_response(anthropic_response)
%Message{role: :assistant, content: "Hello"}
"""
@spec from_anthropic_response(map()) :: Message.t()
defdelegate from_anthropic_response(response), to: Anthropic, as: :from_response
@doc """
Parse Gemini response into a Message.
## Examples
iex> gemini_response = %{
...> "candidates" => [%{"content" => %{"parts" => [%{"text" => "Hello"}]}}]
...> }
iex> Messages.from_gemini_response(gemini_response)
%Message{role: :assistant, content: "Hello"}
"""
@spec from_gemini_response(map()) :: Message.t()
defdelegate from_gemini_response(response), to: Gemini, as: :from_response
@doc """
Parse provider response into a Message.
Dispatches to appropriate provider-specific parser.
## Examples
iex> Messages.from_provider_response(openai_response, :openai)
%Message{role: :assistant, content: "Hello"}
"""
@spec from_provider_response(map(), atom()) :: Message.t()
def from_provider_response(response, provider) when is_map(response) do
case provider do
:openai ->
from_openai_response(response)
:openai_compatible ->
from_openai_response(response)
:groq ->
from_openai_response(response)
:lmstudio ->
from_openai_response(response)
:ollama ->
from_openai_response(response)
:openrouter ->
from_openai_response(response)
:together ->
from_openai_response(response)
:vllm ->
from_openai_response(response)
:sglang ->
from_openai_response(response)
:anthropic ->
from_anthropic_response(response)
:gemini ->
from_gemini_response(response)
:vertex_ai ->
from_gemini_response(response)
:mistral ->
from_openai_response(response)
:llamacpp ->
from_openai_response(response)
:custom ->
from_openai_response(response)
_ ->
raise ArgumentError, """
Unsupported provider: #{inspect(provider)}
Supported providers: :openai, :openai_compatible, :groq, :lmstudio, :llamacpp, :vllm, :sglang, :anthropic, :gemini, :vertex_ai, :mistral
"""
end
end
@doc """
Normalize any message format to internal Message representation.
Attempts to detect format and convert to Message structs.
## Examples
iex> Messages.normalize_format([%{"role" => "user", "content" => "Hi"}])
[%Message{role: :user, content: "Hi"}]
"""
@spec normalize_format(any()) :: [Message.t()]
def normalize_format(messages) when is_list(messages) do
case detect_format(messages) do
:message ->
messages
:legacy ->
Enum.map(messages, &Message.from_legacy/1)
:openai ->
OpenAI.from_messages(messages)
:anthropic ->
Anthropic.from_messages(messages)
:gemini ->
Gemini.from_messages(messages)
:unknown ->
Logger.warning("Unknown message format, attempting generic conversion")
attempt_generic_conversion(messages)
end
end
def normalize_format(single_message) do
normalize_format([single_message])
end
# Private helpers
defp detect_format([]), do: :message
defp detect_format([first | _rest]) do
cond do
is_struct(first, Message) ->
:message
match?({:system_prompt, _}, first) or match?({:user_prompt, _}, first) ->
:legacy
is_struct(first) and Map.has_key?(first, :role) ->
:openai
is_map(first) and Map.has_key?(first, "role") and Map.has_key?(first, "content") ->
:anthropic
is_map(first) and Map.has_key?(first, "role") and Map.has_key?(first, "parts") ->
:gemini
is_map(first) and Map.has_key?(first, :role) ->
:openai
true ->
:unknown
end
end
defp attempt_generic_conversion(messages) do
Enum.map(messages, fn
msg when is_binary(msg) -> Message.user(msg)
msg when is_map(msg) -> Message.user(inspect(msg))
msg -> Message.user(inspect(msg))
end)
end
end