Current section
Files
Jump to
Current section
Files
lib/llm_ex/provider_behaviour.ex
defmodule LlmEx.ProviderBehaviour do
@moduledoc """
Defines a common behaviour interface for all LLM providers.
This module establishes a contract that all LLM provider implementations must fulfill.
"""
@doc """
Invoked when a module uses LlmEx.ProviderBehaviour.
Implements default functionality for the provider.
"""
defmacro __using__(_opts) do
quote do
@behaviour LlmEx.ProviderBehaviour
import LlmEx.ProviderBehaviour
alias LlmEx.Types.{Message, Tool}
# Client state for building requests
defp initial_state do
%{
tools: [],
prompt: nil,
system: "",
messages: [],
options: []
}
end
@doc """
Add a tool to the client request.
"""
def with_tool(state \\ initial_state(), tool) do
Map.update(state, :tools, [tool], fn tools -> [tool | tools] end)
end
@doc """
Set the prompt for the client request.
"""
def with_prompt(state \\ initial_state(), prompt) do
Map.put(state, :prompt, prompt)
end
@doc """
Set the system message for the client request.
"""
def with_system(state, system) when is_binary(system) do
Map.put(state, :system, system)
end
@doc """
Add a message to the conversation history.
"""
def with_message(state, %Message{} = message) do
Map.update(state, :messages, [message], fn messages -> messages ++ [message] end)
end
@doc """
Add a user message to the conversation history.
"""
def with_user_message(state, content) when is_binary(content) do
message = %Message{role: :user, content: content}
with_message(state, message)
end
@doc """
Add an assistant message to the conversation history.
"""
def with_assistant_message(state, content) when is_binary(content) do
message = %Message{role: :assistant, content: content}
with_message(state, message)
end
@doc """
Set additional options for the client request.
"""
def with_options(state, options) when is_list(options) do
Map.update(state, :options, options, &Keyword.merge(&1, options))
end
@doc """
Stream the response from the LLM provider.
"""
def stream(state) do
# Initialize message list if needed
messages = prepare_messages(state)
# Start streaming with provided options and tools
message_id = "msg_#{System.unique_integer() |> abs()}"
opts =
[
tools: state.tools,
system: state.system
] ++ state.options
stream_chat(messages, message_id, self(), opts)
end
# Helper to prepare messages for the request
defp prepare_messages(state) do
messages = state.messages
# If no messages but prompt is set, create a user message
if messages == [] and state.prompt do
[%Message{role: :user, content: state.prompt}]
else
messages
end
end
# Default implementations that can be overridden
def message_to_provider_format(_message), do: raise("Not implemented")
def tool_to_provider_format(_tool), do: raise("Not implemented")
end
end
@doc """
Streams a chat response from the LLM to the given process.
## Parameters
* `messages` - List of Message structs representing the conversation history
* `message_id` - ID for the response message
* `pid` - The process ID to stream the response to
* `opts` - Options for the request (model, temperature, etc.)
"""
@callback stream_chat(list(), String.t(), pid(), keyword()) :: any()
@doc """
Converts a Message struct to the provider-specific format.
## Parameters
* `message` - The Message struct to convert
"""
@callback message_to_provider_format(struct()) :: map()
@doc """
Converts a Tool struct to the provider-specific format.
## Parameters
* `tool` - The Tool struct to convert
"""
@callback tool_to_provider_format(struct()) :: map()
end