No release in over 3 years
Provides a way to create a command without an execute method that is instead executed by a Foobara::Agent
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

>= 0.0.1, < 2.0.0
 Project Readme

Foobara::AgentBackedCommand

Allows a quick, easy way to have a command's execute method handled by a Foobara::Agent. Similar to the Foobara::LlmBackedCommand, you can just specify whatever parts of the command's anatomy other than the execute method, and it will be handled by the Agent.

Installation

Typical stuff: add gem "foobara-agent-backed-command" to your Gemfile or .gemspec file. Or even just gem install foobara-agent-backed-command if just playing with it directly in scripts.

Usage

You can make an AgentBackedCommand by subclassing Foobara::AgentBackedCommand. You can specify whatever inputs/result types you want or you can omit them.

Here's a very short example from the loan-origination foobara demo domain:

class FoobaraDemo::LoanOrigination::ReviewAllLoanFilesNeedingReview < Foobara::AgentBackedCommand
end

A more fleshed-out version might be:

class FoobaraDemo::LoanOrigination::ReviewAllLoanFilesNeedingReview < Foobara::AgentBackedCommand
  description "Checks each LoanFile needing review against all requirements in its CreditPolicy. " \
                "If any requirement is not satisfied, it will be denied. Otherwise, approved"
  verbose

  result do
    approved [{ applicant_name: :string }]
    denied [{ applicant_name: :string, denied_reasons: [:denied_reason] }]
  end
end

Notice how we don't need to specify an execute method. The AgentBackedCommand choose which commands from its domain to execute. Here's some example output of running the above command:

$ loan-origination ReviewAllLoanFilesNeedingReview --agent-options-verbose
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview")
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 64)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::DenyLoanFile")
FoobaraDemo::LoanOrigination::DenyLoanFile.run(loan_file: 64, credit_score_used: 650, denied_reasons: ["low_credit_score"])
Command FoobaraDemo::LoanOrigination::DenyLoanFile failed {"data.loan_file.cannot_transition_state" => {key: "data.loan_file.cannot_transition_state", path: [:loan_file], runtime_path: [], category: :data, symbol: :cannot_transition_state, message: "Cannot perform deny transition for loan file 64 from needs_review. Expected state to be one of: [:in_review]. Did you forget to start the review?", context: {loan_file_id: 64, current_state: :needs_review, required_states: [:in_review], attempted_transition: :deny}, is_fatal: false}}
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::StartUnderwriterReview")
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 64)
FoobaraDemo::LoanOrigination::DenyLoanFile.run(loan_file: 64, credit_score_used: 650, denied_reasons: ["low_credit_score"])
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 65)
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 65)
FoobaraDemo::LoanOrigination::DenyLoanFile.run(loan_file: 65, credit_score_used: 750, denied_reasons: ["insufficient_pay_stubs_provided"])
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 66)
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 66)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::ApproveLoanFile")
FoobaraDemo::LoanOrigination::ApproveLoanFile.run(loan_file: 66, credit_score_used: 750)
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewAllLoanFilesNeedingReviewAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewAllLoanFilesNeedingReviewAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(approved: [{"applicant_name" => "Fumiko"}], denied: [{"applicant_name" => "Barbara", "denied_reasons" => ["low_credit_score"]}, {"applicant_name" => "Basil", "denied_reasons" => ["insufficient_pay_stubs_provided"]}])
approved: [
  {
    applicant_name: "Fumiko"
  }
],
denied: [
  {
    applicant_name: "Barbara",
    denied_reasons: [
      "low_credit_score"
    ]
  },
  {
    applicant_name: "Basil",
    denied_reasons: [
      "insufficient_pay_stubs_provided"
    ]
  }
]

Here, we used a CLI connector to let us run it from the command line. We ran it with the --agent-options-verbose flag to get verbose output from command. We can see all of the commands from the domain it ran to accomplish its goal. Also, it gave us its result in the format we wanted. We can connect this command in any way we'd connect any other Foobara command.

Here's an example with inputs:

module FoobaraDemo
  module LoanOrigination
    class ReviewLoanFile < Foobara::AgentBackedCommand
      description "Performs a review of the given LoanFile by checking it " \
                  "against all requirements in its CreditPolicy. " \
                  "If any requirement is not satisfied, it will be denied. Otherwise, approved"
      verbose

      add_inputs do
        loan_file LoanFile, :required
      end

      result LoanFile::UnderwriterDecision
    end
  end
end

running this command in a CLI connector gives the following output:

╚loan-origination ReviewLoanFile --agent-options-verbose --loan-file 72
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 72)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::ApproveLoanFile")
FoobaraDemo::LoanOrigination::ApproveLoanFile.run(loan_file: 72, credit_score_used: 750)
Command FoobaraDemo::LoanOrigination::ApproveLoanFile failed {"data.loan_file.cannot_transition_state" => {key: "data.loan_file.cannot_transition_state", path: [:loan_file], runtime_path: [], category: :data, symbol: :cannot_transition_state, message: "Cannot perform approve transition for loan file 72 from needs_review. Expected state to be one of: [:in_review]. Did you forget to start the review?", context: {loan_file_id: 72, current_state: :needs_review, required_states: [:in_review], attempted_transition: :approve}, is_fatal: false}}
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 72)
FoobaraDemo::LoanOrigination::ApproveLoanFile.run(loan_file: 72, credit_score_used: 750)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(result: {"decision" => "approved", "credit_score_used" => 750})
decision: "approved",
credit_score_used: 750

And we can also just run this command directly and use its result programmatically if we want:

irb(main):001> outcome = FoobaraDemo::LoanOrigination::ReviewLoanFile.run(loan_file: 71, agent_options: {verbose: true})
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 71)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::StartUnderwriterReview")
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 71)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::DenyLoanFile")
FoobaraDemo::LoanOrigination::DenyLoanFile.run(loan_file: 71, credit_score_used: 750, denied_reasons: ["insufficient_pay_stubs_provided"])
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(result: {"decision" => "denied", "credit_score_used" => 750, "denied_reasons" => ["insufficient_pay_stubs_provided"]})
=> 
#<Foobara::Outcome:0x00007f9a989fe8b0
...
irb(main):002> outcome.success?
=> true
irb(main):003> outcome.result
=> 
#<FoobaraDemo::LoanOrigination::LoanFile::UnderwriterDecision:0x00007f9a981f24a0
 @attributes={decision: "denied", credit_score_used: 750, denied_reasons: ["insufficient_pay_stubs_provided"]},
 @mutable=true,
 @skip_validations=nil>
irb(main):004> underwriter_decision = outcome.result
=> 
#<FoobaraDemo::LoanOrigination::LoanFile::UnderwriterDecision:0x00007f9a981f24a0
...
irb(main):005> underwriter_decision.denied_reasons.first
=> "insufficient_pay_stubs_provided"

We could now let our original AgentBackedCommand call this new one we wrote just like any other command. This means we'd have one agent orchestrating another without even knowing that that's what its doing:

module FoobaraDemo
  module LoanOrigination
    class ReviewLoanFile < Foobara::AgentBackedCommand
      add_inputs do
        loan_file LoanFile, :required
      end

      result LoanFile::UnderwriterDecision

      depends_on StartUnderwriterReview,
                 FindCreditPolicy,
                 DenyLoanFile,
                 ApproveLoanFile

      verbose
    end

    class ReviewAllLoanFilesNeedingReview < Foobara::AgentBackedCommand
      result do
        approved_count :integer, :required
        denied_count :integer, :required
      end
      depends_on FindALoanFileThatNeedsReview, ReviewLoanFile
      verbose
    end
  end
end

outcome = FoobaraDemo::LoanOrigination::ReviewAllLoanFilesNeedingReview.run

if outcome.success?
  result = outcome.result

  puts "Great success!!"
  puts "Approved: #{result[:approved_count]}"
  puts "Denied: #{result[:denied_count]}"
else
  warn "Error: #{outcome.errors_hash}"
end

Which gives the following output:

Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview")
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::ReviewLoanFile")
FoobaraDemo::LoanOrigination::ReviewLoanFile.run(loan_file: 79)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::StartUnderwriterReview")
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 79)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 79)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::DenyLoanFile")
FoobaraDemo::LoanOrigination::DenyLoanFile.run(loan_file: 79, credit_score_used: 650, denied_reasons: ["low_credit_score"])
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(result: {"decision" => "denied", "credit_score_used" => 650, "denied_reasons" => ["low_credit_score"]})
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
FoobaraDemo::LoanOrigination::ReviewLoanFile.run(loan_file: 80)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::StartUnderwriterReview")
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 80)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 80)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::ApproveLoanFile")
FoobaraDemo::LoanOrigination::ApproveLoanFile.run(loan_file: 80, credit_score_used: 750)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(result: {"decision" => "approved", "credit_score_used" => 750})
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
FoobaraDemo::LoanOrigination::ReviewLoanFile.run(loan_file: 81)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ListCommands")
Foobara::Agent::ListCommands.run
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::StartUnderwriterReview")
FoobaraDemo::LoanOrigination::StartUnderwriterReview.run(loan_file: 81)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::FindCreditPolicy")
FoobaraDemo::LoanOrigination::FindCreditPolicy.run(credit_policy: 81)
Foobara::Agent::DescribeCommand.run(command_name: "FoobaraDemo::LoanOrigination::ApproveLoanFile")
FoobaraDemo::LoanOrigination::ApproveLoanFile.run(loan_file: 81, credit_score_used: 750)
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewLoanFileAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(result: {"decision" => "approved", "credit_score_used" => 750})
FoobaraDemo::LoanOrigination::FindALoanFileThatNeedsReview.run
Foobara::Agent::DescribeCommand.run(command_name: "Foobara::Agent::ReviewAllLoanFilesNeedingReviewAgent::NotifyUserThatCurrentGoalHasBeenAccomplished")
Foobara::Agent::ReviewAllLoanFilesNeedingReviewAgent::NotifyUserThatCurrentGoalHasBeenAccomplished.run(approved_count: 2, denied_count: 1)
Great success!!
Approved: 2
Denied: 1

Here you can see two agents working together to solve the problem without either knowing it.

Contributing

Helllllp! If this project seems interesting and you want to try using it or you want to help, please get in touch! There's no shortage of work to do of any experience level.

Bug reports and pull requests are welcome on GitHub at https://github.com/foobara/agent-backed-command

License

This project is licensed under the MPL-2.0 license. Please see LICENSE.txt for more info.