Creating Custom Agents
View SourceThis guide explains how to create domain-specific LLM agents by extending the base functionality of LLMAgent. Custom agents can be specialized for particular tasks, domains, or interaction patterns.
Domain-Specific Agents
While the standard conversation flow works well for general-purpose assistants, domain-specific agents can provide more specialized capabilities:
- Financial advisors that understand investment concepts
- Technical support agents with product knowledge
- Medical assistants that can interpret symptoms
- Educational tutors with teaching methodologies
Extending Base Flows
To create a custom agent, start by extending one of the base flows:
defmodule MyApp.FinancialAdvisor do
alias LLMAgent.{Flows, Signals, Store}
def create_agent(options \\ []) do
# Define a domain-specific system prompt
system_prompt = """
You are a financial advisor specializing in retirement planning.
Follow these guidelines when advising clients:
- Always ask about their time horizon and risk tolerance
- Recommend diversified investment strategies
- Explain concepts in clear, non-technical language
- Disclose that you're not providing personalized financial advice
"""
# Define domain-specific tools
tools = [
%{
name: "calculate_compound_interest",
description: "Calculate compound interest growth. Parameters: principal, rate, time, compounds_per_year",
execute: &MyApp.FinancialTools.calculate_compound_interest/1
},
%{
name: "retirement_calculator",
description: "Estimate retirement savings needed. Parameters: current_age, retirement_age, life_expectancy, annual_expenses",
execute: &MyApp.FinancialTools.retirement_calculator/1
}
]
# Create the base conversation flow
{flow, initial_state} = Flows.conversation(system_prompt, tools, options)
# Add custom middleware if needed
custom_flow = fn signal, state ->
# Log all interactions for compliance
log_interaction(signal, state)
# Apply the base flow
flow.(signal, state)
end
{custom_flow, initial_state}
end
defp log_interaction(signal, state) do
# Implement compliance logging
# ...
end
end
Custom Handlers
You can create custom handlers for specialized signal processing:
defmodule MyApp.MedicalHandlers do
alias LLMAgent.{Signals, Store}
def symptom_handler(%{type: :symptom, data: symptoms}, state) do
# Process symptoms and generate potential diagnoses
# ...
# Create a thinking signal with medical context
thinking = Signals.thinking("Analyzing symptoms: #{symptoms}")
{{:emit, thinking}, state}
end
def diagnosis_handler(%{type: :diagnosis, data: diagnosis}, state) do
# Format and validate the diagnosis
# ...
# Return a response with recommendations
response = Signals.response(formatted_diagnosis)
{{:emit, response}, state}
end
end
Custom Signal Types
Define domain-specific signal types to represent specialized concepts:
defmodule MyApp.MedicalSignals do
alias AgentForge.Signal
def symptom(description) do
Signal.new(:symptom, description)
end
def diagnosis(condition, confidence, recommendations) do
Signal.new(:diagnosis, %{
condition: condition,
confidence: confidence,
recommendations: recommendations
})
end
def prescription(medication, dosage, instructions) do
Signal.new(:prescription, %{
medication: medication,
dosage: dosage,
instructions: instructions
})
end
end
Custom State Management
Extend the store to include domain-specific state:
defmodule MyApp.MedicalStore do
alias LLMAgent.Store
def new(initial_state \\ %{}) do
# Start with the base store
base_store = Store.new(initial_state)
# Add medical-specific state
Map.merge(base_store, %{
patient_info: %{},
medical_history: [],
current_symptoms: [],
previous_diagnoses: []
})
end
def add_patient_info(state, info) do
put_in(state, [:patient_info], info)
end
def add_symptom(state, symptom) do
update_in(state, [:current_symptoms], &([symptom | &1]))
end
def add_diagnosis(state, diagnosis) do
update_in(state, [:previous_diagnoses], &([diagnosis | &1]))
end
end
Composing Custom Flows
Create domain-specific flow compositions for specialized interaction patterns:
defmodule MyApp.MedicalFlows do
alias LLMAgent.{Flows, Handlers}
alias MyApp.{MedicalHandlers, MedicalStore}
def diagnosis_flow(system_prompt, tools \\ [], options \\ []) do
# Create base conversation flow
{base_flow, initial_state} = Flows.conversation(system_prompt, tools, options)
# Initialize medical-specific state
medical_state = MedicalStore.new(initial_state)
# Create a custom flow with specialized handlers
custom_flow = fn signal, state ->
state
|> handle_with(&Handlers.message_handler/2, signal)
|> handle_with(&MedicalHandlers.symptom_handler/2, signal)
|> handle_with(&Handlers.thinking_handler/2, signal)
|> handle_with(&Handlers.tool_handler/2, signal)
|> handle_with(&Handlers.tool_result_handler/2, signal)
|> handle_with(&MedicalHandlers.diagnosis_handler/2, signal)
|> handle_with(&Handlers.response_handler/2, signal)
|> handle_with(&Handlers.error_handler/2, signal)
end
{custom_flow, medical_state}
end
# Helper for handling signals, similar to LLMAgent.Flows implementation
defp handle_with({:halt, _} = result, _handler, _signal), do: result
defp handle_with({:skip, state}, handler, signal) do
# Implementation as in LLMAgent.Flows
end
# Other handle_with clauses...
end
Best Practices
Domain-Specific System Prompts
Create detailed system prompts that include:
- Role definition: Clearly state the specialized role and expertise
- Domain knowledge: Include essential facts and concepts
- Guidelines: Provide rules and constraints for the domain
- Examples: Demonstrate ideal responses for common scenarios
Example for a legal assistant:
system_prompt = """
You are a legal assistant specializing in contract law.
EXPERTISE:
- Contract formation and interpretation
- Common contractual clauses and their implications
- Basic legal principles (not legal advice)
GUIDELINES:
- Always clarify you are not providing legal advice
- Recommend consulting with a licensed attorney for specific legal questions
- Explain legal concepts in plain language
- Ask clarifying questions when details are ambiguous
EXAMPLE INTERACTIONS:
User: "What should be included in an NDA?"
Assistant: "A typical NDA (Non-Disclosure Agreement) generally includes: 1) Definition of confidential information, 2) Scope of confidentiality obligation, 3) Exclusions from confidential information, 4) Term of the agreement, and 5) Remedies for breach. However, please note that I'm not providing legal advice, and you should consult with an attorney to ensure your NDA meets your specific needs and complies with relevant laws."
"""
Configuration Options
Use options to make agents configurable:
def create_financial_advisor(options \\ []) do
# Extract customization options
risk_profile = Keyword.get(options, :risk_profile, :moderate)
timezone = Keyword.get(options, :timezone, "UTC")
include_market_data = Keyword.get(options, :include_market_data, true)
# Customize system prompt based on options
system_prompt = build_prompt(risk_profile, timezone)
# Select appropriate tools based on options
tools = select_tools(risk_profile, include_market_data)
# Create the flow
Flows.conversation(system_prompt, tools, options)
end
Testing Custom Agents
Create tests for domain-specific behavior:
defmodule MyApp.FinancialAdvisorTest do
use ExUnit.Case
test "provides retirement planning advice" do
{flow, state} = MyApp.FinancialAdvisor.create_agent()
signal = LLMAgent.Signals.user_message("I'm 30 years old and want to retire at 65. How much should I save?")
{:ok, result, _new_state} = AgentForge.process(flow, state, signal)
assert result.type == :response
assert String.contains?(result.data, "retirement")
assert String.contains?(result.data, "save")
end
test "uses retirement calculator tool" do
{flow, state} = MyApp.FinancialAdvisor.create_agent()
signal = LLMAgent.Signals.user_message("Calculate how much I need to retire if I'm currently 30, want to retire at 65, and need $50,000 per year.")
{:ok, tool_call, new_state} = AgentForge.process(flow, state, signal)
assert tool_call.type == :tool_call
assert tool_call.data.name == "retirement_calculator"
end
end
Real-World Examples
Educational Tutor
defmodule MyApp.MathTutor do
alias LLMAgent.{Flows, Signals}
def create_tutor(subject \\ :algebra, difficulty \\ :intermediate) do
# Build appropriate system prompt for subject and difficulty
system_prompt = """
You are a math tutor specializing in #{subject} at the #{difficulty} level.
Guide students through problems step-by-step, don't just give answers.
Ask questions to check understanding and provide hints when students are stuck.
"""
# Create appropriate tools
tools = [
%{
name: "equation_solver",
description: "Solve mathematical equations. Parameter: equation",
execute: &MyApp.MathTools.solve_equation/1
},
%{
name: "plot_function",
description: "Create a text representation of a function graph. Parameter: function",
execute: &MyApp.MathTools.plot_function/1
}
]
# Create the flow
Flows.conversation(system_prompt, tools)
end
end
Customer Support Agent
defmodule MyApp.SupportAgent do
alias LLMAgent.{Flows, Signals}
def create_agent(product, knowledge_base) do
# Build system prompt with product details
system_prompt = """
You are a customer support agent for #{product}.
Use the knowledge base to answer questions accurately.
For technical issues, ask for relevant details like error messages.
For billing or account issues, direct customers to appropriate resources.
"""
# Create support-specific tools
tools = [
%{
name: "search_knowledge_base",
description: "Search the knowledge base for articles. Parameter: query",
execute: fn args -> MyApp.Support.search_kb(knowledge_base, args["query"]) end
},
%{
name: "create_ticket",
description: "Create a support ticket. Parameters: issue, severity, description",
execute: &MyApp.Support.create_ticket/1
}
]
# Create the flow
Flows.conversation(system_prompt, tools)
end
end
Next Steps
- Explore advanced usage patterns
- Learn about LLM provider integration
- See the architecture overview for more extension points