### Quick Start Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/index.md A basic example demonstrating how to configure API keys, create agents, set up handoffs, and run a conversation. ```ruby require 'agents' # Configure your API keys Agents.configure do |config| config.openai_api_key = ENV['OPENAI_API_KEY'] # config.azure_api_base = ENV['AZURE_API_BASE'] # config.azure_api_key = ENV['AZURE_API_KEY'] # config.anthropic_api_key = ENV['ANTHROPIC_API_KEY'] # config.gemini_api_key = ENV['GEMINI_API_KEY'] config.default_model = 'gpt-4o-mini' end # Create agents triage = Agents::Agent.new( name: "Triage", instructions: "You help route customer inquiries to the right department." ) support = Agents::Agent.new( name: "Support", instructions: "You provide technical support for our products." ) # Set up handoffs triage.register_handoffs(support) # Create runner and start conversation runner = Agents::Runner.with_agents(triage, support) result = runner.run("I need help with a technical issue") puts result.output ``` -------------------------------- ### Install Dependencies Source: https://github.com/chatwoot/ai-agents/blob/main/examples/README.md Install the necessary Ruby dependencies using Bundler. ```bash bundle install ``` -------------------------------- ### OpenTelemetry Installation Example Source: https://github.com/chatwoot/ai-agents/blob/main/CLAUDE.md Example of how to install OpenTelemetry instrumentation with a runner and tracer. ```ruby Agents::Instrumentation.install(runner, tracer: tracer) ``` -------------------------------- ### Best Practice: Multi-Step Workflow Instructions Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Provides an example of orchestrator instructions that guide the agent on how to handle complex queries by planning, using tools sequentially, and extracting specific values for subsequent calls. ```ruby orchestrator = Agent.new( instructions: <<~PROMPT **For complex queries requiring multiple pieces of information:** 1. Plan what information you need to gather 2. Use tools sequentially, building on previous results 3. Extract specific values from tool outputs for subsequent calls 4. Don't pass original parameters - use discovered values **Example:** To check billing for CONTACT-123: Step 1: research_customer("Get details for CONTACT-123") → finds email Step 2: check_billing("Check billing for [discovered email]") → not original ID PROMPT ) ``` -------------------------------- ### Installation with Bundler Source: https://github.com/chatwoot/ai-agents/blob/main/docs/index.md Add the gem to your application's Gemfile and install it. ```ruby gem 'ai-agents' ``` ```bash bundle install ``` -------------------------------- ### Best Practice: Clear Tool Descriptions Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Illustrates good and bad examples of tool descriptions, emphasizing the importance of specifying tool requirements and outputs clearly. ```ruby # Good: Clear requirements billing_agent.as_tool( name: "check_stripe_billing", description: "Check Stripe billing info. Requires customer email (not contact ID)." ) research_agent.as_tool( name: "research_customer", description: "Research customer details. Returns email address and contact info." ) # Avoid: Vague descriptions agent.as_tool(name: "process", description: "Do stuff") ``` -------------------------------- ### Complete Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/instrumentation.md A comprehensive Ruby example demonstrating how to configure Agents, OpenTelemetry with Langfuse, build agents, and install instrumentation for tracing. ```ruby require "agents" require "agents/instrumentation" require "opentelemetry-sdk" require "opentelemetry-exporter-otlp" require "base64" # --- Configure Agents --- Agents.configure do |config| config.openai_api_key = ENV["OPENAI_API_KEY"] config.default_model = "gpt-4o-mini" end # --- Configure OTel with Langfuse --- langfuse_host = ENV.fetch("LANGFUSE_HOST", "https://cloud.langfuse.com") auth_token = Base64.strict_encode64( "#{ENV["LANGFUSE_PUBLIC_KEY"]}:#{ENV["LANGFUSE_SECRET_KEY"]}" ) OpenTelemetry::SDK.configure do |c| c.add_span_processor( OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new( OpenTelemetry::Exporter::OTLP::Exporter.new( endpoint: "#{langfuse_host}/api/public/otel/v1/traces", headers: { "Authorization" => "Basic #{auth_token}" } ) ) ) end tracer = OpenTelemetry.tracer_provider.tracer("my-app") # --- Build agents --- triage = Agents::Agent.new(name: "Triage", instructions: "Route users...") billing = Agents::Agent.new(name: "Billing", instructions: "Handle billing...") support = Agents::Agent.new(name: "Support", instructions: "Technical support...") triage.register_handoffs(billing, support) billing.register_handoffs(triage) support.register_handoffs(triage) # --- Create runner with instrumentation --- runner = Agents::Runner.with_agents(triage, billing, support) Agents::Instrumentation.install(runner, tracer: tracer, trace_name: "customer_support", attribute_provider: ->(ctx) { { "langfuse.user.id" => ctx.context[:user_id].to_s, "langfuse.session.id" => ctx.context[:session_id].to_s } } ) # --- Run conversations --- result = runner.run("I have a billing question", context: { user_id: "user_123", session_id: "sess_456" }) puts result.output # Ensure spans are flushed before exit at_exit { OpenTelemetry.tracer_provider.force_flush } ``` -------------------------------- ### Run Basic Usage Example Source: https://github.com/chatwoot/ai-agents/blob/main/examples/README.md Execute the basic_usage.rb script to see the gem in action. ```bash ruby examples/basic_usage.rb ``` -------------------------------- ### RubyLLM::Schema (Recommended) Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/structured-output.md Example using RubyLLM::Schema for more complex schemas with a Ruby DSL, demonstrating schema definition, agent creation, runner setup, and the structured output. ```ruby class ContactSchema < RubyLLM::Schema string :name, description: "Full name of the person" string :email, description: "Email address" string :phone, description: "Phone number", required: false string :company, description: "Company name", required: false array :interests, description: "List of interests or topics mentioned" do string description: "Individual interest or topic" end end contact_agent = Agents::Agent.new( name: "ContactExtractor", instructions: "Extract contact information from business communications", response_schema: ContactSchema ) runner = Agents::Runner.with_agents(contact_agent) result = runner.run("Hi, I'm Sarah Johnson from TechCorp. You can reach me at sarah@techcorp.com or 555-0123. I'm interested in AI and automation solutions.") # Returns structured contact data: # { # "name": "Sarah Johnson", # "email": "sarah@techcorp.com", # "phone": "555-0123", # "company": "TechCorp", # "interests": ["AI", "automation solutions"] # } ``` -------------------------------- ### Installation with RubyGems Source: https://github.com/chatwoot/ai-agents/blob/main/docs/index.md Install the gem directly using RubyGems. ```bash gem install ai-agents ``` -------------------------------- ### Example Agent Creation Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/agents.md This example demonstrates how to create a simple agent and then a specialized agent by cloning the base agent and adding a new tool. This approach allows for easy composition and reuse of agent configurations. ```ruby # Create a simple agent assistant_agent = Agents::Agent.new( name: "Assistant", instructions: "You are a helpful assistant.", model: "gpt-4.1-mini", tools: [CalculatorTool.new], temperature: 0.3 ) # Create a specialized agent by cloning the base agent specialized_agent = assistant_agent.clone( instructions: "You are a specialized assistant for financial calculations.", tools: assistant_agent.tools + [FinancialDataTool.new] ) ``` -------------------------------- ### Running the Interactive Demo Source: https://github.com/chatwoot/ai-agents/blob/main/examples/collaborative-copilot/README.md Commands to set the API key and run the interactive demo. ```bash # Set your API key export OPENAI_API_KEY="your-key-here" # Run the interactive demo ruby examples/collaborative-copilot/interactive.rb ``` -------------------------------- ### Set up API Key Source: https://github.com/chatwoot/ai-agents/blob/main/examples/README.md Set your OpenAI API key as an environment variable. ```bash export OPENAI_API_KEY=your_api_key_here ``` -------------------------------- ### JSON Schema approach Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/structured-output.md Example of creating an agent with a JSON schema for structured responses, including the agent definition, runner setup, and expected output. ```ruby # JSON Schema approach extraction_agent = Agents::Agent.new( name: "DataExtractor", instructions: "Extract key information from user messages", response_schema: { type: 'object', properties: { entities: { type: 'array', items: { type: 'string' } }, sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, summary: { type: 'string' } }, required: ['entities', 'sentiment'], additionalProperties: false } ) runner = Agents::Runner.with_agents(extraction_agent) result = runner.run("I love the new product features, especially the API and dashboard!") # Response will be valid JSON matching the schema: # { # "entities": ["API", "dashboard"], # "sentiment": "positive", # "summary": "User expresses enthusiasm for new product features" # } ``` -------------------------------- ### Clear Boundaries Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Illustrates the principle of clear agent responsibilities, showing a 'good' example where sales and support agents have distinct, non-overlapping roles and know when to transfer questions to the other. ```ruby # GOOD: Clear specialization sales_agent = Agents::Agent.new( name: "Sales", instructions: "Handle product inquiries, pricing, and purchase decisions. Transfer technical questions to support." ) support_agent = Agents::Agent.new( name: "Support", instructions: "Handle technical issues and product troubleshooting. Transfer sales questions to sales team." ) ``` -------------------------------- ### Example of Handoffs Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/handoffs.md This example demonstrates how to create specialized agents and a triage agent with handoff capabilities, and then run the triage agent with a user query that triggers a handoff. ```ruby # Create the specialized agents billing_agent = Agents::Agent.new(name: "Billing", instructions: "Handle billing and payment issues.") support_agent = Agents::Agent.new(name: "Support", instructions: "Provide technical support.") # Create the triage agent with handoff agents triage_agent = Agents::Agent.new( name: "Triage", instructions: "You are a triage agent. Your job is to route users to the correct department.", handoff_agents: [billing_agent, support_agent] ) # Run the triage agent result = Agents::Runner.run(triage_agent, "I have a problem with my bill.") # The runner will automatically hand off to the billing agent ``` -------------------------------- ### Avoiding Handoff Loops Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Provides examples of 'bad' and 'good' agent designs to prevent infinite handoff loops. The 'good' example shows a clear escalation hierarchy. ```ruby # BAD: Conflicting handoff criteria agent_a = Agents::Agent.new( instructions: "Handle X. If you need Y info, hand off to Agent B." ) agent_b = Agents::Agent.new( instructions: "Handle Y. If you need X context, hand off to Agent A." ) # GOOD: Clear escalation hierarchy specialist = Agents::Agent.new( instructions: "Handle specialized requests. Ask users for needed info directly." ) triage = Agents::Agent.new( instructions: "Route users to specialists. Don't solve problems yourself." ) ``` -------------------------------- ### Install on a runner Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/instrumentation.md Install the instrumentation on a runner instance, passing the tracer and the runner configuration. ```ruby require "agents/instrumentation" tracer = OpenTelemetry.tracer_provider.tracer("my-app") runner = Agents::Runner.with_agents(triage, billing, support) Agents::Instrumentation.install(runner, tracer: tracer) ``` -------------------------------- ### Entry Point Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Demonstrates how the first agent defined in `AgentRunner.with_agents()` becomes the default entry point for conversations. ```ruby # Triage agent handles all initial conversations runner = Agents::Runner.with_agents(triage_agent, billing_agent, support_agent) # Start conversation result = runner.run("I need help with my account") # -> Automatically starts with triage_agent ``` -------------------------------- ### Check Ruby Version Source: https://github.com/chatwoot/ai-agents/blob/main/examples/README.md Verify that your Ruby version is 3.1 or later. ```bash ruby --version ``` -------------------------------- ### Agent Initialization Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Example of initializing an AI agent with a custom model. ```ruby agent = Agents::Agent.new( name: "Assistant", model: CustomProvider.new.model("custom-model") ) ``` -------------------------------- ### Troubleshooting: No API keys configured Error Source: https://github.com/chatwoot/ai-agents/blob/main/examples/README.md Ensure the OPENAI_API_KEY environment variable is set correctly. ```bash export OPENAI_API_KEY=sk-your-key-here ``` -------------------------------- ### OpenTelemetry Instrumentation Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Example of installing OpenTelemetry instrumentation for tracing agent execution. ```ruby require 'agents/instrumentation' tracer = OpenTelemetry.tracer_provider.tracer('my-app') runner = Agents::Runner.with_agents(triage, billing, support) Agents::Instrumentation.install(runner, tracer: tracer) ``` -------------------------------- ### Design Focused Agents Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Contrasts a good example of a focused agent with clear responsibilities against a bad example of an agent with overly broad responsibilities. ```ruby # Good: Focused responsibility customer_agent = Agent.new( name: "CustomerAgent", instructions: "Handle customer data lookups and history research" ) # Avoid: Too broad everything_agent = Agent.new( name: "EverythingAgent", instructions: "Handle all customer operations, billing, support, and analysis" ) ``` -------------------------------- ### Handle Errors with Guidance Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Shows how to provide helpful error messages in orchestrator instructions that guide the user on next steps when tools fail, such as missing email for billing or if a contact is not found. ```ruby # In orchestrator instructions instructions = <<~PROMPT **Error Handling:** - If billing fails due to missing email: Use research_customer first - If contact not found: Ask for more identifying information - Always provide helpful responses even if tools fail PROMPT ``` -------------------------------- ### Azure Custom Deployment Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/rails-integration.md Example of creating an agent with a specific Azure custom deployment name. ```ruby support = Agents::Agent.new( name: "Support", model: "my-azure-deployment", provider: :azure, assume_model_exists: true ) ``` -------------------------------- ### Install dependencies Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/instrumentation.md Add the necessary OpenTelemetry SDK and OTLP exporter gems to your Gemfile. ```ruby gem "opentelemetry-sdk" gem "opentelemetry-exporter-otlp" ``` -------------------------------- ### Creating a Tool Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/tools.md Example of creating a custom tool by inheriting from Agents::Tool and implementing the perform method. ```ruby class WeatherTool < Agents::Tool name "get_weather" description "Get the current weather for a location." param :location, type: "string", desc: "The city and state, e.g., San Francisco, CA" def perform(tool_context, location:) # Access the API key from the shared context api_key = tool_context.context[:weather_api_key] # Call the weather API and return the result WeatherApi.get(location, api_key) end end ``` -------------------------------- ### Dynamic Instructions Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Shows how to use Proc-based instructions to create context-aware agents. The agent's instructions can change based on provided context, such as customer tier. ```ruby support_agent = Agents::Agent.new( name: "Support", instructions: ->(context) { customer_tier = context[:customer_tier] || "standard" <<~INSTRUCTIONS You are a technical support specialist for #{customer_tier} tier customers. #{customer_tier == "premium" ? "Provide priority white-glove service." : ""} Available tools: diagnostics, escalation INSTRUCTIONS } ) ``` -------------------------------- ### AgentTool Architecture Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/agent-tool.md Demonstrates how to define specialized agents and an orchestrator agent that uses them as tools. ```ruby # Specialized agents as tools research_agent = Agent.new( name: "Research Agent", instructions: "Research topics and extract key information" ) analysis_agent = Agent.new( name: "Analysis Agent", instructions: "Analyze data and provide insights" ) # Main orchestrator uses other agents as tools orchestrator = Agent.new( name: "Orchestrator", instructions: "Coordinate research and analysis", tools: [ research_agent.as_tool( name: "research_topic", description: "Research a specific topic" ), analysis_agent.as_tool( name: "analyze_data", description: "Analyze research findings" ) ] ) ``` -------------------------------- ### Customer Support Copilot Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/agent-tool.md Shows how to build a support copilot using specialized agents for conversation analysis and Shopify operations. ```ruby # Specialized agents for different domains conversation_analyzer = Agent.new( name: "ConversationAnalyzer", instructions: "Extract order IDs and customer intent" ) shopify_agent = Agent.new( name: "ShopifyAgent", instructions: "Perform Shopify operations", tools: [shopify_refund_tool, shopify_lookup_tool] ) # Main copilot coordinates specialized agents copilot = Agent.new( name: "SupportCopilot", instructions: "Help support agents with customer requests", tools: [ conversation_analyzer.as_tool( name: "analyze_conversation", description: "Extract key information from conversation" ), shopify_agent.as_tool( name: "shopify_action", description: "Perform Shopify operations" ) ] ) ``` -------------------------------- ### Parameter Precedence Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/provider-params.md Demonstrates how runtime parameters override agent-level parameters. ```ruby agent = Agents::Agent.new( name: "Assistant", instructions: "You are a helpful assistant", params: { service_tier: "flex", top_p: 0.9 } ) runner = Agents::Runner.with_agents(agent) result = runner.run( "Hello!", params: { service_tier: "default", # Overrides agent's flex value max_completion_tokens: 1000 # Additional param } ) # Final params sent to LLM API: # { # service_tier: "default", # Runtime value wins # top_p: 0.9, # From agent # max_completion_tokens: 1000 # From runtime # } ``` -------------------------------- ### File Structure Source: https://github.com/chatwoot/ai-agents/blob/main/examples/isp-support/README.md The directory structure for the ISP customer support demo example. ```tree examples/isp-support/ ├── README.md # This documentation ├── interactive.rb # Interactive CLI demo ├── agents_factory.rb # Agent creation and configuration ├── tools/ │ ├── crm_lookup_tool.rb # Customer data retrieval │ ├── create_lead_tool.rb # Sales lead generation │ ├── create_checkout_tool.rb # Payment link generation │ ├── search_docs_tool.rb # Knowledge base search │ └── escalate_to_human_tool.rb # Human handoff └── data/ ├── customers.json # Dummy customer database ├── plans.json # Available service plans └── docs.json # Troubleshooting knowledge base ``` -------------------------------- ### Installation Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Add the gem to your Gemfile. ```ruby gem 'ai-agents' ``` -------------------------------- ### Custom Tool Implementation Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Example of how to define and implement a custom tool in Ruby, including defining its name, parameters, and the 'perform' method logic. ```ruby class CustomTool < Agents::Tool name "custom_action" description "Perform custom business logic" param :input, type: "string" def perform(tool_context, input:) # Access context for state user_id = tool_context.context[:user_id] # Perform custom logic result = BusinessLogic.process(input, user_id) # Update shared state tool_context.state[:last_action] = result result end end ``` -------------------------------- ### Context Isolation Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/agent-tool.md Illustrates the difference between parent context and isolated context when using AgentTool. ```ruby # Parent context (full conversation state) parent_context = { state: { user_id: 123, session: "abc" }, conversation_history: [...], current_agent: "MainAgent", turn_count: 5 } # Isolated context (only state) isolated_context = { state: { user_id: 123, session: "abc" } } ``` -------------------------------- ### Integration Testing Handoffs Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Example of integration testing complete workflows, including handoffs between agents. ```ruby RSpec.describe "Customer Support Workflow" do let(:runner) { create_support_runner } # Creates triage + specialists it "routes billing questions correctly" do result = runner.run("I have a billing question") # Verify handoff occurred expect(result.context[:current_agent]).to eq("Billing") # Test continued conversation followup = runner.run("What are your payment terms?", context: result.context) expect(followup.output).to include("payment terms") end end ``` -------------------------------- ### Tool-Specific Agents Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Demonstrates creating agents specialized for particular tool categories, such as a DataAnalyst agent with tools for data analysis and a Communications agent for notifications. ```ruby data_agent = Agents::Agent.new( name: "DataAnalyst", instructions: "Analyze data and generate reports using available analytics tools.", tools: [DatabaseTool.new, ChartGeneratorTool.new, ReportTool.new] ) communication_agent = Agents::Agent.new( name: "Communications", instructions: "Handle notifications and external communications.", tools: [EmailTool.new, SlackTool.new, SMSTool.new] ) ``` -------------------------------- ### Hub-and-Spoke Architecture Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Demonstrates setting up specialized agents (Billing, Support) and a central triage agent that routes conversations to them. It also shows how to register handoffs and create a runner with the triage agent as the entry point. ```ruby # Specialized agents billing_agent = Agents::Agent.new( name: "Billing", instructions: "Handle billing inquiries, payment processing, and account issues." ) support_agent = Agents::Agent.new( name: "Support", instructions: "Provide technical troubleshooting and product support." ) # Central hub agent triage_agent = Agents::Agent.new( name: "Triage", instructions: "Route users to the appropriate specialist. Only hand off, don't solve problems yourself." ) # Register handoffs (one-way: triage -> specialists) triage_agent.register_handoffs(billing_agent, support_agent) # Create runner with triage as entry point runner = Agents::Runner.with_agents(triage_agent, billing_agent, support_agent) ``` -------------------------------- ### Custom Output Extractor Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/agent-tool.md Demonstrates how to define a custom output extractor for transforming agent results. ```ruby # Custom output extraction summarizer = Agent.new( name: "Summarizer", instructions: "Summarize long content" ) summarizer_tool = summarizer.as_tool( name: "summarize", description: "Create a summary", output_extractor: ->(result) { "SUMMARY: #{result.output&.first(200)}..." } ) ``` -------------------------------- ### Main Answer Suggestion Agent Source: https://github.com/chatwoot/ai-agents/blob/main/examples/collaborative-copilot/README.md Defines the main answer suggestion agent with specialized sub-agents as tools. ```ruby answer_agent = Agents::Agent.new( name: "AnswerSuggestionAgent", instructions: "You help support agents by providing answers and suggestions...", tools: [ research_agent.as_tool( name: "research_customer_history", description: "Research customer history and similar cases" ), analysis_agent.as_tool( name: "analyze_conversation", description: "Analyze conversation tone and sentiment" ), integrations_agent.as_tool( name: "check_technical_context", description: "Check for technical issues and development context" ) ] ) # Support agent asks for help result = runner.run(answer_agent, "How should I respond to this angry customer about login issues?") ``` -------------------------------- ### Concurrent Execution Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Example demonstrating the thread-safety of AgentRunner for concurrent conversations. ```ruby # Safe to use same runner across threads threads = users.map do |user| Thread.new do user_result = runner.run(user.message, context: user.context) # Handle result... end end ``` -------------------------------- ### Configuration Source: https://github.com/chatwoot/ai-agents/blob/main/CLAUDE.md Example of how to configure the AI Agents SDK with LLM provider API keys and default model. ```ruby Agents.configure do |config| config.openai_api_key = ENV['OPENAI_API_KEY'] config.anthropic_api_key = ENV['ANTHROPIC_API_KEY'] config.gemini_api_key = ENV['GEMINI_API_KEY'] config.default_model = 'gpt-4o-mini' config.debug = true end ``` -------------------------------- ### Agent Architecture Overview Source: https://github.com/chatwoot/ai-agents/blob/main/examples/collaborative-copilot/README.md A diagram illustrating the architecture of the Chatwoot Copilot system, showing the interaction between specialized AI agents. ```text ┌─────────────────────────────────────────┐ │ Answer Suggestion Agent │ │ (Primary Entry) │ └─────────────┬───────────────────────────┘ │ ┌─────────┴─────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Research │ │ Analysis │ │Integration │ │ Agent │ │ Agent │ │ Agent │ └─────────────┘ └─────────────┘ └─────────────┘ ``` -------------------------------- ### Agent Definition Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Example of how to define an agent with a name, instructions, model, and tools, and then register handoffs. ```ruby # Create agents as instances agent = Agents::Agent.new( name: "Customer Service", instructions: "You are a helpful customer service agent.", model: "gpt-4o", tools: [EmailTool.new, TicketTool.new] ) # Register handoffs after creation agent.register_handoffs(technical_support, billing) ``` -------------------------------- ### Runner with Context Persistence Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Sets up an agent runner with context persistence and demonstrates an interactive loop where user input is processed, and the context is maintained and updated across turns. ```ruby # Set up runner with context persistence runner = Agents::Runner.with_agents(orchestrator) context = {} # Interactive loop maintains context loop do user_input = gets.chomp break if user_input == "exit" # Pass and update context each turn result = runner.run(user_input, context: context) context = result.context if result.context puts result.output end ``` -------------------------------- ### First-Call-Wins Handoff Resolution Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Example demonstrating how only the first handoff call is processed when multiple are present in an LLM response. ```text LLM Response with Multiple Handoffs: ┌─────────────────────────────────────┐ │ Call 1: handoff_to_support() │ ✓ Processed │ Call 2: handoff_to_billing() │ ✗ Ignored │ Call 3: handoff_to_support() │ ✗ Ignored └─────────────────────────────────────┘ Result: Transfer to Support Agent only ``` -------------------------------- ### Debug Hooks Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Examples of enabling debug logging and setting custom callbacks for agent interactions. ```ruby # Enable debug logging ENV["RUBYLLM_DEBUG"] = "true" # Custom callbacks chat.on(:new_message) { puts "Sending to LLM..." } chat.on(:end_message) { |response| puts "Received: #{response.content}" } ``` -------------------------------- ### Conditional Handoffs Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Example of an agent with dynamic instructions that control handoff behavior based on business hours. ```ruby triage_agent = Agents::Agent.new( name: "Triage", instructions: ->(context) { business_hours = context[:business_hours] || false base_instructions = "Route users to appropriate departments." if business_hours base_instructions + " All departments are available." else base_instructions + " Only hand off urgent technical issues to support. Others should wait for business hours." end } ) ``` -------------------------------- ### Explicit Parameter Requirements in Agent Instructions Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Demonstrates how to make tool parameter needs clear in agent instructions, specifically requiring customer email addresses for Stripe billing lookups. ```ruby billing_agent = Agent.new( instructions: <<~PROMPT **CRITICAL: Billing Requirements** - Stripe billing lookups REQUIRE customer email addresses - Contact IDs, names, phone numbers will NOT work - If you don't have email, clearly state you need it PROMPT ) ``` -------------------------------- ### Context Persistence for Multi-Turn Conversations Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Illustrates how to maintain context across conversation turns using an `Agents::Runner` and a `context` object, where each turn builds upon the previous context. ```ruby # Maintain context between interactions runner = Agents::Runner.with_agents(orchestrator) context = {} # Each turn builds on previous context result = runner.run(user_input, context: context) context = result.context if result.context ``` -------------------------------- ### Sales Inquiry Workflow Source: https://github.com/chatwoot/ai-agents/blob/main/examples/isp-support/README.md Example of a user request for a sales inquiry, demonstrating the routing from Triage Agent to Sales Agent and the actions taken by the Sales Agent. ```text User: "I want to upgrade my plan" Triage Agent → Sales Agent Sales Agent: Creates lead and checkout link ``` -------------------------------- ### Handoff Detection Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Shows that the AgentRunner automatically selects the correct agent based on the conversation state and history, eliminating the need for manual agent specification during subsequent turns. ```ruby # No need to manually specify which agent to use result = runner.run("Follow up question", context: previous_result.context) # -> AgentRunner automatically selects correct agent based on conversation state ``` -------------------------------- ### Conversation Continuity Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/runner.md Illustrates how the AgentRunner maintains conversation continuity by analyzing message history to determine the correct agent for each turn. ```ruby result1 = runner.run("I need help with my bill") result2 = runner.run("What payment methods do you accept?", context: result1.context) ``` -------------------------------- ### Custom Output Extraction for Specific Information Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Demonstrates how to use an `output_extractor` with an agent tool to extract specific information, such as an email address, from the tool's result instead of the full response. ```ruby research_agent.as_tool( name: "get_customer_email", description: "Get customer email address", output_extractor: ->(result) { # Extract just the email instead of full response email_match = result.output.match(/Email:\s*([^\s]+)/i) email_match&.captures&.first || "Email not found" } ) ``` -------------------------------- ### Context Cleanup Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Example of cleaning up context by removing old conversation history to reduce token usage. ```ruby # Remove old conversation history cleaned_context = result.context.dup cleaned_context[:conversation_history] = cleaned_context[:conversation_history].last(10) ``` -------------------------------- ### Context Preservation Example Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Illustrates how the AgentRunner automatically maintains conversation history and context across agent handoffs, allowing subsequent agents to access previous information. ```ruby # First interaction result1 = runner.run("My name is John and I have a billing question") # -> Triage agent hands off to billing agent # Continue conversation result2 = runner.run("What payment methods do you accept?", context: result1.context) # -> Billing agent remembers John's name and previous context ``` -------------------------------- ### Unit Testing Individual Agents Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/multi-agent-systems.md Example of unit testing a single agent in isolation using RSpec. ```ruby RSpec.describe "BillingAgent" do let(:agent) { create_billing_agent } let(:runner) { Agents::Runner.with_agents(agent) } it "handles payment inquiries" do result = runner.run("What payment methods do you accept?") expect(result.output).to include("credit card", "bank transfer") end end ``` -------------------------------- ### Custom Tools Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Example of defining a custom tool by inheriting from `Agents::Tool`, specifying its description, parameters, and implementing the `perform` method. ```ruby class EmailTool < Agents::Tool description "Send emails to customers" param :to, type: "string", desc: "Email address" param :subject, type: "string", desc: "Email subject" param :body, type: "string", desc: "Email body" def perform(tool_context, to:, subject:, body:) # Send email logic here "Email sent to #{to}" end end ``` -------------------------------- ### Interactive Ruby console with gem loaded Source: https://github.com/chatwoot/ai-agents/blob/main/CLAUDE.md Command to start an interactive Ruby console with the AI Agents gem loaded. ```bash bundle exec irb -r ./lib/agents ``` -------------------------------- ### Stateless Tool Design Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/state-persistence.md Example of a stateless tool that relies on context for data, demonstrating how to access database configuration and establish connections without instance variables. ```ruby class DatabaseTool < Agents::Tool name "query_database" description "Query the application database" param :query, type: "string", desc: "SQL query to execute" def perform(tool_context, query:) # Get database connection from context, not instance variables db_config = tool_context.context[:database_config] connection = establish_connection(db_config) # Execute query and return results connection.execute(query) end private def establish_connection(config) # Create connection based on config ActiveRecord::Base.establish_connection(config) end end ``` -------------------------------- ### Tool State Persistence Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/state-persistence.md Example of a tool that persists its state within the context, showing how to initialize, update, and utilize tool-specific data like processed files and their statuses. ```ruby class FileProcessorTool < Agents::Tool name "process_file" description "Process uploaded files" param :file_path, type: "string", desc: "Path to file" def perform(tool_context, file_path:) # Initialize tool state in context if needed tool_context.context[:file_processor] ||= { processed_files: [], processing_status: {} } # Process file result = process_file(file_path) # Update tool state in context tool_context.context[:file_processor][:processed_files] << file_path tool_context.context[:file_processor][:processing_status][file_path] = result[:status] result end end ``` -------------------------------- ### Technical Support Only Workflow Source: https://github.com/chatwoot/ai-agents/blob/main/examples/isp-support/README.md Example of a user request for technical support only, illustrating the routing from Triage Agent to Support Agent and the troubleshooting steps provided. ```text User: "My internet is slow" Triage Agent → Support Agent Support Agent: Provides troubleshooting steps from docs ``` -------------------------------- ### Orchestrator Agent with Specialized Tools Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/agent-as-tool-pattern.md Creates an orchestrator agent, SupportCopilot, designed to coordinate specialist agents. It includes instructions for a multi-step workflow approach and defines tools for researching customers and checking billing, emphasizing the need for customer email addresses. ```ruby # Main agent coordinates specialists orchestrator = Agents::Agent.new( name: "SupportCopilot", instructions: <<~PROMPT You help support agents by coordinating specialist agents. **CRITICAL: Multi-Step Workflow Approach** For complex queries, break them into steps and use tools sequentially: 1. Plan your approach: What information do you need? 2. Execute sequentially: Use EXACT results from previous tools 3. Build context progressively: Each tool builds on previous findings **Tool Requirements:** - research_customer: Returns contact details including emails - check_billing: Requires customer email (not contact ID) Always think: "What did I learn and how do I use it next?" PROMPT, tools: [ research_agent.as_tool( name: "research_customer", description: "Research customer details. Returns contact info including email." ), billing_agent.as_tool( name: "check_billing", description: "Check billing status. Requires customer email address." ) ] ) ``` -------------------------------- ### Account Information + Technical Issue Workflow Source: https://github.com/chatwoot/ai-agents/blob/main/examples/isp-support/README.md Example of a user request for account information and a technical issue, showing the routing from Triage Agent to Support Agent and the subsequent steps taken by the Support Agent. ```text User: "My account shows active but internet isn't working" Triage Agent → Support Agent Support Agent: 1. Asks for account ID and looks up account info 2. Provides technical troubleshooting steps 3. Handles complete conversation without handoffs ``` -------------------------------- ### Enqueue job from controller Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/rails-integration.md Example of enqueuing the AgentConversationJob from a controller. ```ruby # Enqueue job from controller def create_async job_id = AgentConversationJob.perform_later( current_user.id, params[:message], params[:conversation_id] ) render json: { job_id: job_id } end ``` -------------------------------- ### Basic Usage of Callbacks Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/callbacks.md Demonstrates how to register various callbacks on an AgentRunner instance using chainable methods. ```ruby runner = Agents::Runner.with_agents(triage, support) .on_run_start { |agent, input, ctx| puts "Starting: #{agent}" } .on_run_complete { |agent, result, ctx| puts "Completed: #{agent}" } .on_agent_complete { |agent, result, error, ctx| puts "Agent done: #{agent}" } .on_agent_thinking { |agent, input| puts "#{agent} thinking..." } .on_tool_start { |tool, args| puts "Using #{tool}" } .on_tool_complete { |tool, result| puts "#{tool} completed" } .on_agent_handoff { |from, to, reason| puts "#{from} → #{to}" } ``` -------------------------------- ### Azure and Custom Deployments Configuration Source: https://github.com/chatwoot/ai-agents/blob/main/docs/index.md Configure the SDK to use Azure OpenAI or other custom deployments. ```ruby Agents.configure do |config| config.azure_api_base = ENV["AZURE_API_BASE"] config.azure_api_key = ENV["AZURE_API_KEY"] end agent = Agents::Agent.new( name: "Support", model: "my-azure-deployment", provider: :azure, assume_model_exists: true ) ``` -------------------------------- ### Context Encryption Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/state-persistence.md A class `EncryptedContextStorage` for encrypting and decrypting sensitive context data using AES-256-CBC, with an example of its usage. ```ruby class EncryptedContextStorage def initialize(encryption_key) @cipher = OpenSSL::Cipher.new('AES-256-CBC') @key = encryption_key end def encrypt_context(context) @cipher.encrypt @cipher.key = @key encrypted_data = @cipher.update(context.to_json) encrypted_data << @cipher.final Base64.encode64(encrypted_data) end def decrypt_context(encrypted_data) @cipher.decrypt @cipher.key = @key decoded_data = Base64.decode64(encrypted_data) decrypted_data = @cipher.update(decoded_data) decrypted_data << @cipher.final JSON.parse(decrypted_data, symbolize_names: true) end end # Usage storage = EncryptedContextStorage.new(Rails.application.secret_key_base) # Encrypt before storing encrypted_context = storage.encrypt_context(result.context.to_h) database_record.update(encrypted_context: encrypted_context) # Decrypt when loading encrypted_data = database_record.encrypted_context context = storage.decrypt_context(encrypted_data) ``` -------------------------------- ### AgentRunner Usage Pattern Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/runner.md Demonstrates how to create agents, register handoffs, and initialize a thread-safe AgentRunner for concurrent conversations. ```ruby triage_agent = Agents::Agent.new( name: "Triage", instructions: "Route users to appropriate specialists" ) billing_agent = Agents::Agent.new( name: "Billing", instructions: "Handle billing inquiries" ) triage_agent.register_handoffs(billing_agent) runner = Agents::Runner.with_agents(triage_agent, billing_agent) result1 = runner.run("I have a billing question") result2 = runner.run("Follow up", context: result1.context) ``` -------------------------------- ### Basic Usage Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Configure the SDK with your API key and create a simple agent. Then, use a runner to interact with the agent. ```ruby require 'agents' # Configure with your API key Agents.configure do |config| config.openai_api_key = ENV['OPENAI_API_KEY'] end # Create agents weather_agent = Agents::Agent.new( name: "Weather Assistant", instructions: "Help users get weather information", tools: [WeatherTool.new] ) # Create a thread-safe runner (reusable across conversations) runner = Agents::Runner.with_agents(weather_agent) # Use the runner for conversations result = runner.run("What's the weather like today?") puts result.output ``` -------------------------------- ### Conversation Continuity Message Attribution Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Example of a message object with agent attribution, enabling conversation continuity by identifying the agent responsible for the message. ```ruby # Messages include agent attribution { role: :assistant, content: "I can help with billing", agent_name: "Billing" # Enables conversation continuity } ``` -------------------------------- ### Basic Usage Pattern Source: https://github.com/chatwoot/ai-agents/blob/main/CLAUDE.md Demonstrates how to create agents, register handoffs, set up a thread-safe runner, and use it for conversations. ```ruby # Create agents with handoff relationships triage = Agent.new(name: "Triage", instructions: "Route users...") billing = Agent.new(name: "Billing", instructions: "Handle billing...") support = Agent.new(name: "Support", instructions: "Technical support...") triage.register_handoffs(billing, support) # Create thread-safe runner (first agent is default entry point) runner = Agents::Runner.with_agents(triage, billing, support) # Add real-time callbacks for monitoring runner.on_agent_thinking { |agent_name, input| puts "🧠 #{agent_name} is thinking..." } runner.on_tool_start { |tool_name, args| puts "🔧 Using #{tool_name}..." } runner.on_tool_complete { |tool_name, result| puts "✅ #{tool_name} completed" } runner.on_agent_handoff { |from, to, reason| puts "🔄 Handoff: #{from} → #{to}" } # Use for conversations - automatically handles agent selection and persistence result = runner.run("I have a billing question") result = runner.run("What about technical support?", context: result.context) ``` -------------------------------- ### Run ISP Support Demo Source: https://github.com/chatwoot/ai-agents/blob/main/README.md Command to run a complete working demo showcasing multi-agent workflows. ```bash ruby examples/isp-support/interactive.rb ``` -------------------------------- ### Context Migration Source: https://github.com/chatwoot/ai-agents/blob/main/docs/guides/state-persistence.md A `ContextMigrator` class to handle format changes in context across different application versions, with a specific example for migrating from version 1 to version 2. ```ruby class ContextMigrator CURRENT_VERSION = 2 def self.migrate_context(context) version = context[:_version] || 1 case version when 1 migrate_v1_to_v2(context) when 2 context # Already current else raise "Unknown context version: #{version}" end end private def self.migrate_v1_to_v2(context) # V1 -> V2: Rename 'current_agent' to 'current_agent' (no change needed) migrated = context.deep_dup # No migration needed - current_agent is already the correct field name migrated[:_version] = 2 migrated end end # Use in context loading def load_context(user_id) raw_context = storage.load_context(user_id) ContextMigrator.migrate_context(raw_context) end ``` -------------------------------- ### Logging & Metrics Integration Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/callbacks.md Illustrates using callbacks for structured logging and metrics collection. ```ruby runner = Agents::Runner.with_agents(agent) .on_run_start { |agent, input, ctx| logger.info("Run started", agent: agent) } .on_tool_start { |tool, args| metrics.increment("tool.calls", tags: ["tool:#{tool}"]) .on_agent_complete do |agent, result, error, ctx| logger.error("Agent failed", agent: agent, error: error) if error end ``` -------------------------------- ### UI Feedback Integration Source: https://github.com/chatwoot/ai-agents/blob/main/docs/concepts/callbacks.md Shows how to use callbacks with ActionCable to stream agent status updates to browser clients for UI feedback. ```ruby runner = Agents::Runner.with_agents(agent) .on_agent_thinking { |agent, input| ActionCable.server.broadcast("agent_#{user_id}", { type: 'thinking', agent: agent }) } .on_tool_start { |tool, args| ActionCable.server.broadcast("agent_$_user_id", { type: 'tool', name: tool }) } ``` -------------------------------- ### Context Management Architecture Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Illustrates the layers of context: User Context, RunContext, and ToolContext. ```text ┌─────────────────┐ │ User Context │ (Serializable across sessions) │ │ ├─────────────────┤ │ RunContext │ (Execution isolation) │ │ ├─────────────────┤ │ ToolContext │ (Tool-specific state) │ │ └─────────────────┘ ``` -------------------------------- ### Tool Wrapper Pattern Source: https://github.com/chatwoot/ai-agents/blob/main/docs/architecture.md Ruby code illustrating how tools remain stateless by injecting context via a wrapper. ```ruby # Tool Definition (Stateless) class WeatherTool < Agents::Tool def perform(tool_context, city:) api_key = tool_context.context[:weather_api_key] WeatherAPI.get(city, api_key) end end # Runtime Wrapping (Context Injection) wrapped_tool = ToolWrapper.new(weather_tool, context_wrapper) ```