defmodule Nous.Agent do @moduledoc """ Primary interface for AI agents. An Agent is a stateless configuration object that defines how to interact with an AI model. It specifies the model, instructions, tools, and output format. ## Example # Simple agent agent = Agent.new("openai:gpt-4", instructions: "Be helpful and concise" ) {:ok, result} = Agent.run(agent, "What is 2+2?") IO.puts(result.output) # "4" # Agent with tools agent = Agent.new("groq:llama-3.1-70b-versatile", instructions: "Help users find information", tools: [&MyTools.search/2] ) {:ok, result} = Agent.run(agent, "Search for Elixir tutorials", deps: %{database: MyApp.DB} ) """ alias Nous.{Fallback, Hook, Model, Tool, Types} @type t :: %__MODULE__{ model: Model.t(), fallback: [Model.t()], output_type: Types.output_type(), structured_output: keyword(), instructions: String.t() | function() | nil, system_prompt: String.t() | function() | nil, deps_type: module() | nil, name: String.t(), model_settings: map(), retries: non_neg_integer(), tools: [Tool.t()], plugins: [module()], hooks: [Hook.t()], skills: [module() | String.t() | Nous.Skill.t() | {:group, atom()}], end_strategy: :early | :exhaustive, enable_todos: boolean(), behaviour_module: module() | nil } @enforce_keys [:model] defstruct [ :model, :deps_type, :behaviour_module, fallback: [], output_type: :string, structured_output: [], instructions: nil, system_prompt: nil, name: nil, model_settings: %{}, retries: 1, tools: [], plugins: [], hooks: [], skills: [], end_strategy: :early, enable_todos: false ] @doc """ Create a new agent. ## Parameters * `model_string` - Model in format "provider:model-name" * `opts` - Configuration options ## Options * `:output_type` - Expected output type (`:string`, Ecto schema, schemaless map, JSON schema, or guided mode tuple) * `:structured_output` - Structured output options (`mode:`, `max_retries:`) * `:instructions` - Static instructions or function returning instructions * `:system_prompt` - Static system prompt or function * `:deps_type` - Module defining dependency structure * `:name` - Agent name for logging * `:model_settings` - Model settings (temperature, max_tokens, etc.) * `:retries` - Default retry count for tools * `:enable_todos` - Enable automatic todo tracking (default: false) * `:tools` - List of tool functions or Tool structs * `:plugins` - List of plugin modules implementing `Nous.Plugin` behaviour * `:hooks` - List of `Nous.Hook` structs for lifecycle interception * `:skills` - List of skill modules, directory paths, `Nous.Skill` structs, or `{:group, atom()}` * `:skill_dirs` - List of directory paths to scan for `.md` skill files (convenience for `:skills`) * `:end_strategy` - How to handle tool calls (`:early` or `:exhaustive`) * `:behaviour_module` - Custom agent behaviour module (default: BasicAgent) * `:fallback` - Ordered list of fallback model strings or `Model` structs to try when the primary model fails with a provider/model error ## Examples # OpenAI GPT-4 agent = Agent.new("openai:gpt-4") # Groq Llama with settings agent = Agent.new("groq:llama-3.1-70b-versatile", instructions: "Be concise", model_settings: %{temperature: 0.7, max_tokens: 1000} ) # Local LM Studio agent = Agent.new("lmstudio:qwen3-vl-4b-thinking-mlx", instructions: "Always answer in rhymes" ) # With tools agent = Agent.new("openai:gpt-4", tools: [&MyTools.search/2, &MyTools.calculate/2] ) """ @spec new(String.t(), keyword()) :: t() def new(model_string, opts \\ []) do model = Model.parse(model_string, opts) %__MODULE__{ model: model, fallback: Fallback.parse_fallback_models(Keyword.get(opts, :fallback, [])), output_type: Keyword.get(opts, :output_type, :string), structured_output: Keyword.get(opts, :structured_output, []), instructions: Keyword.get(opts, :instructions), system_prompt: Keyword.get(opts, :system_prompt), deps_type: Keyword.get(opts, :deps_type), name: Keyword.get(opts, :name, generate_name()), model_settings: Keyword.get(opts, :model_settings, %{}), retries: Keyword.get(opts, :retries, 1), tools: parse_tools(Keyword.get(opts, :tools, [])), plugins: ensure_skills_plugin( Keyword.get(opts, :plugins, []), merge_skill_dirs(Keyword.get(opts, :skills, []), Keyword.get(opts, :skill_dirs, [])) ), hooks: Keyword.get(opts, :hooks, []), skills: merge_skill_dirs(Keyword.get(opts, :skills, []), Keyword.get(opts, :skill_dirs, [])), end_strategy: Keyword.get(opts, :end_strategy, :early), behaviour_module: Keyword.get(opts, :behaviour_module) } end @doc """ Run agent synchronously. ## Input Formats The second argument accepts multiple formats: - **String prompt**: Simple string message from user - **Keyword list**: Use `:messages` for custom message list, `:context` to continue from previous run ## Options * `:deps` - Dependencies to pass to tools and prompts * `:message_history` - Previous messages to continue conversation * `:usage_limits` - Usage limits for this run * `:model_settings` - Override model settings for this run * `:callbacks` - Map of callback functions for events * `:notify_pid` - PID to receive event messages * `:context` - Existing context to continue from * `:output_type` - Override the agent's `output_type` for this run * `:structured_output` - Override the agent's `structured_output` options for this run ## Examples # String prompt {:ok, result} = Agent.run(agent, "What is the capital of France?") IO.puts(result.output) # "Paris" # With dependencies {:ok, result} = Agent.run(agent, "Search for users", deps: %{database: MyApp.DB} ) # Message list directly {:ok, result} = Agent.run(agent, messages: [ Message.system("Be concise"), Message.user("What is 2+2?") ] ) # Continue from previous context {:ok, result1} = Agent.run(agent, "First question") {:ok, result2} = Agent.run(agent, "Follow up", context: result1.context ) # Continue conversation with message history {:ok, result2} = Agent.run(agent, "Tell me more", message_history: result1.new_messages ) # With callbacks {:ok, result} = Agent.run(agent, "Hello", callbacks: %{ on_llm_new_delta: fn _, text -> IO.write(text) end } ) ## Returns {:ok, %{ output: "Result text or structured output", usage: %Usage{...}, all_messages: [...], new_messages: [...], context: %Context{...} # Can be used for continuation }} {:error, reason} """ @spec run(t(), String.t() | keyword(), keyword()) :: {:ok, map()} | {:error, term()} def run(agent, input, opts \\ []) # String prompt def run(%__MODULE__{} = agent, prompt, opts) when is_binary(prompt) do Nous.AgentRunner.run(agent, prompt, opts) end # Keyword input with messages or context def run(%__MODULE__{} = agent, input, opts) when is_list(input) do merged_opts = Keyword.merge(input, opts) cond do Keyword.has_key?(merged_opts, :messages) -> # Build context from messages messages = Keyword.fetch!(merged_opts, :messages) run_with_messages(agent, messages, Keyword.delete(merged_opts, :messages)) Keyword.has_key?(merged_opts, :context) -> # Continue from existing context - need a prompt {:error, :prompt_required_with_context} true -> {:error, :invalid_input} end end defp run_with_messages(agent, messages, opts) do alias Nous.Agent.Context # Build initial context from messages ctx = Context.new( messages: messages, deps: Keyword.get(opts, :deps, %{}), max_iterations: Keyword.get(opts, :max_iterations, 10), callbacks: Keyword.get(opts, :callbacks, %{}), notify_pid: Keyword.get(opts, :notify_pid), agent_name: agent.name, cancellation_check: Keyword.get(opts, :cancellation_check), needs_response: true ) # Run with pre-built context Nous.AgentRunner.run_with_context(agent, ctx, opts) end @doc """ Run agent with streaming. Returns a stream that yields events as they occur. ## Events * `{:text_delta, text}` - Incremental text update * `{:tool_call, call}` - Tool is being called * `{:tool_result, result}` - Tool execution completed * `{:complete, result}` - Final result ## Example {:ok, stream} = Agent.run_stream(agent, "Tell me a story") stream |> Stream.each(fn {:text_delta, text} -> IO.write(text) {:complete, result} -> IO.puts("\\nDone!") end) |> Stream.run() """ @spec run_stream(t(), String.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, term()} def run_stream(%__MODULE__{} = agent, prompt, opts \\ []) do Nous.AgentRunner.run_stream(agent, prompt, opts) end @doc """ Register a tool with the agent. Returns a new agent with the tool added. ## Options * `:name` - Custom tool name (default: function name) * `:description` - Custom description * `:retries` - Retry count for this tool * `:requires_approval` - Whether tool needs human approval ## Example agent = Agent.new("openai:gpt-4") agent = Agent.tool(agent, &MyTools.search/2, description: "Search the database" ) """ @spec tool(t(), function(), keyword()) :: t() def tool(%__MODULE__{} = agent, tool_fun, opts \\ []) do tool = Tool.from_function(tool_fun, opts) %{agent | tools: [tool | agent.tools]} end # Private functions @spec parse_tools([function() | Tool.t()]) :: [Tool.t()] defp parse_tools(tools) do Enum.map(tools, fn %Tool{} = tool -> tool fun when is_function(fun) -> Tool.from_function(fun) end) end @spec generate_name() :: String.t() defp generate_name do "agent_#{:erlang.unique_integer([:positive])}" end # Auto-include Skills plugin when skills are configured defp ensure_skills_plugin(plugins, []), do: plugins defp ensure_skills_plugin(plugins, _skills) do if Nous.Plugins.Skills in plugins do plugins else plugins ++ [Nous.Plugins.Skills] end end # Merge skill_dirs into skills list (directories become string entries) defp merge_skill_dirs(skills, []), do: skills defp merge_skill_dirs(skills, dirs), do: skills ++ dirs end