Packages

Elixir port of Microsoft's multi-agent AI framework Autogen

Current section

Files

Jump to
autogen lib autogen.ex
Raw

lib/autogen.ex

defmodule LLMRequest do
defstruct model: "gpt-4o", messages: [], api_key: System.get_env("OPENAI_API_KEY"), temperature: 0.0
def dispatch(request) do
# IO.puts "calling openai with: #{request.api_key}"
OpenAI.chat_completion(
[model: request.model, messages: request.messages, temperature: request.temperature],
%OpenAI.Config{api_key: request.api_key}
)
end
def populate_prompt(template, params) do
{result, _binding} = Code.eval_string(template, params)
result
end
end
defmodule XTool do
defstruct name: "", description: "", jsonschema: ""
end
defmodule XMessage do
defstruct content: "", sender: "", receiver: ""
end
defmodule XThread do
defstruct max_turns: nil, chat_history: []
end
defmodule XAgent do
# https://microsoft.github.io/autogen/docs/tutorial/introduction
# Agents are abstract entities that can send messages, receive messages
# and generate a reply using models, tools, human inputs or a mixture of them.
# An agent can be powered by LLMs or CodeExecutors, Humans or a combination of these.
defstruct name: "",
system_prompt: "",
type: :conversable_agent,
llm: %{temperature: 0.0},
human_input_mode: :terminate,
max_consecutive_auto_reply: nil,
is_termination_msg: nil,
is_code_executor: false
def initiate_chat(opts \\ []) do
# This wraps the message in XMessage and sends it as XThread
# Reset the consecutive auto reply counter.
# If `clear_history` is True, the chat history with the recipient agent will be cleared.
%{
from_agent: from_agent,
to_agent: to_agent,
message: message,
max_turns: max_turns
} = Enum.into(opts, %{from_agent: nil, to_agent: nil, message: nil, max_turns: nil})
thread = %XThread{
max_turns: max_turns,
chat_history: [
%XMessage{
content: message,
sender: from_agent.name,
receiver: to_agent.name
}
]
}
send_thread(from_agent, to_agent, thread)
# This will trigger a cascade of function calls. If chat is initiated from A to B:
# A sends -> B receives -> B optionally sends -> A receives -> A optionally sends and so on.
# One round trip is 4 calls: A Sends, B Receives, B Sends, A Receives.
end
def send_thread(from_agent, to_agent, thread) do
# Send and Receive are separate so that agents can do their own logging and pre/post processing.
receive_thread(from_agent, to_agent, thread)
end
def receive_thread(from_agent, to_agent, thread) do
message = List.first(thread.chat_history)
IO.puts "#{from_agent.name} (to #{to_agent.name}):"
IO.puts message.content
IO.puts "--------------------------------------------------------------------------------"
if !(err = should_stop_replying?(thread, message, to_agent)) do
reply_str = generate_reply(to_agent, thread, message)
reply_msg = %XMessage{content: reply_str, sender: to_agent.name, receiver: from_agent.name}
send_thread(to_agent, from_agent, %XThread{thread | chat_history: [reply_msg | thread.chat_history]})
else
IO.puts "\n\n#{err}"
end
end
def should_stop_replying?(thread, message, to_agent) do
# TODO: Handle max_consecutive_auto_reply
cond do
thread.max_turns != nil and length(thread.chat_history) == 2 * thread.max_turns -> "Max turns reached"
to_agent.type == :conversable_agent and to_agent.is_termination_msg != nil and to_agent.is_termination_msg.(message) -> "is_termination_msg matched"
true -> false
end
end
def generate_reply(%XAgent{type: :conversable_agent, is_code_executor: false} = agent, thread, _message) do
# Assemble message history correctly for the LLM
# Our thread is sorted in reverse chronological order.
# And we need to ask the LLM to behave like us (role=assistant), and we will play other agents' role (role=user)
# So we reverse thread.chat_history and assign roles accordingly.
# if message.sender is our name, set role to assistant, else set role to user.
messages = Enum.reverse(thread.chat_history)
|> Enum.map(fn msg -> %{role: (if msg.sender == agent.name, do: "assistant", else: "user"), content: msg.content} end)
llm_messages = [%{role: "system", content: agent.system_prompt} | messages]
# IO.puts "Sending to LLM: #{inspect(llm_messages)}"
{:ok, %{choices: [%{"message" => %{"content" => response}}]}} = LLMRequest.dispatch(
%LLMRequest{
messages: llm_messages,
temperature: agent.llm.temperature
}
)
response
end
def generate_reply(%XAgent{type: :conversable_agent, is_code_executor: true} = agent, _thread, message) do
code = message.content
|> String.split("```elixir")
|> List.last()
|> String.split("```")
|> List.first()
if agent.human_input_mode == :never or XUtils.get_confirmation("Are you sure you want to run this code?") do
{result, _binding} = Code.eval_string(code)
"Code execution result: " <> inspect(result)
end
end
def generate_reply(%XAgent{type: :user_proxy_agent} = _agent, _thread, _message) do
user_msg = String.trim(IO.gets("Your response: "))
%XMessage{content: user_msg}
end
def agent_with_updated_system_message(agent, system_message) do
struct(agent, config: %{agent.config | system_message: system_message})
end
end
defmodule Autogen do
def hello do
:world
end
end