feat: legacy features to ruby llm (#12994)
This commit is contained in:
@@ -1,137 +0,0 @@
|
||||
require 'openai'
|
||||
class Captain::Agent
|
||||
attr_reader :name, :tools, :prompt, :persona, :goal, :secrets
|
||||
|
||||
def initialize(name:, config:)
|
||||
@name = name
|
||||
@prompt = construct_prompt(config)
|
||||
@tools = prepare_tools(config[:tools] || [])
|
||||
@messages = config[:messages] || []
|
||||
@max_iterations = config[:max_iterations] || 10
|
||||
@llm = Captain::LlmService.new(api_key: config[:secrets][:OPENAI_API_KEY])
|
||||
@logger = Rails.logger
|
||||
|
||||
@logger.info(@prompt)
|
||||
end
|
||||
|
||||
def execute(input, context)
|
||||
setup_messages(input, context)
|
||||
result = {}
|
||||
@max_iterations.times do |iteration|
|
||||
push_to_messages(role: 'system', content: 'Provide a final answer') if iteration == @max_iterations - 1
|
||||
|
||||
result = @llm.call(@messages, functions)
|
||||
handle_llm_result(result)
|
||||
|
||||
break if result[:stop]
|
||||
end
|
||||
|
||||
result[:output]
|
||||
end
|
||||
|
||||
def register_tool(tool)
|
||||
@tools << tool
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_messages(input, context)
|
||||
if @messages.empty?
|
||||
push_to_messages({ role: 'system', content: @prompt })
|
||||
push_to_messages({ role: 'assistant', content: context }) if context.present?
|
||||
end
|
||||
push_to_messages({ role: 'user', content: input })
|
||||
end
|
||||
|
||||
def handle_llm_result(result)
|
||||
if result[:tool_call]
|
||||
tool_result = execute_tool(result[:tool_call])
|
||||
push_to_messages({ role: 'assistant', content: tool_result })
|
||||
else
|
||||
push_to_messages({ role: 'assistant', content: result[:output] })
|
||||
end
|
||||
result[:output]
|
||||
end
|
||||
|
||||
def execute_tool(tool_call)
|
||||
function_name = tool_call['function']['name']
|
||||
arguments = JSON.parse(tool_call['function']['arguments'])
|
||||
|
||||
tool = @tools.find { |t| t.name == function_name }
|
||||
tool.execute(arguments, {})
|
||||
rescue StandardError => e
|
||||
"Tool execution failed: #{e.message}"
|
||||
end
|
||||
|
||||
def construct_prompt(config)
|
||||
return config[:prompt] if config[:prompt]
|
||||
|
||||
<<~PROMPT
|
||||
Persona: #{config[:persona]}
|
||||
Objective: #{config[:goal]}
|
||||
|
||||
Guidelines:
|
||||
- Persistently work towards achieving the stated objective without deviation.
|
||||
- Use only the provided tools to complete the task. Avoid inventing or assuming function names.
|
||||
- Set `'stop': true` once the objective is fully achieved.
|
||||
- DO NOT return tool usage as the final result.
|
||||
- If sufficient information is available to deliver result, compile and present it to the user.
|
||||
- Always return a final result and ENSURE the final result is formatted in Markdown.
|
||||
|
||||
Output Structure:
|
||||
|
||||
1. **Tool Usage:**
|
||||
- If a relevant function is identified, call it directly without unnecessary explanations.
|
||||
|
||||
2. **Final Answer:**
|
||||
When ready to provide a complete response, follow this JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"thought_process": "Explain the reasoning and steps taken to arrive at the final result.",
|
||||
"result": "Provide the complete response in clear, structured text.",
|
||||
"stop": true
|
||||
}
|
||||
PROMPT
|
||||
end
|
||||
|
||||
def prepare_tools(tools = [])
|
||||
tools.map do |_, tool|
|
||||
Captain::Tool.new(
|
||||
name: tool['name'],
|
||||
config: {
|
||||
description: tool['description'],
|
||||
properties: tool['properties'],
|
||||
secrets: tool['secrets'],
|
||||
implementation: tool['implementation']
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def functions
|
||||
@tools.map do |tool|
|
||||
properties = {}
|
||||
tool.properties.each do |property_name, property_details|
|
||||
properties[property_name] = {
|
||||
type: property_details[:type],
|
||||
description: property_details[:description]
|
||||
}
|
||||
end
|
||||
required = tool.properties.select { |_, details| details[:required] == true }.keys
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: { type: 'object', properties: properties, required: required }
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def push_to_messages(message)
|
||||
@logger.info("\n\n\nMessage: #{message}\n\n\n")
|
||||
@messages << message
|
||||
end
|
||||
end
|
||||
@@ -1,64 +0,0 @@
|
||||
require 'openai'
|
||||
|
||||
class Captain::LlmService
|
||||
def initialize(config)
|
||||
@client = OpenAI::Client.new(
|
||||
access_token: config[:api_key],
|
||||
log_errors: Rails.env.development?
|
||||
)
|
||||
@logger = Rails.logger
|
||||
end
|
||||
|
||||
def call(messages, functions = [])
|
||||
openai_params = {
|
||||
model: 'gpt-4o',
|
||||
response_format: { type: 'json_object' },
|
||||
messages: messages
|
||||
}
|
||||
openai_params[:tools] = functions if functions.any?
|
||||
|
||||
response = @client.chat(parameters: openai_params)
|
||||
handle_response(response)
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response(response)
|
||||
if response['choices'][0]['message']['tool_calls']
|
||||
handle_tool_calls(response)
|
||||
else
|
||||
handle_direct_response(response)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_tool_calls(response)
|
||||
tool_call = response['choices'][0]['message']['tool_calls'][0]
|
||||
{
|
||||
tool_call: tool_call,
|
||||
output: nil,
|
||||
stop: false
|
||||
}
|
||||
end
|
||||
|
||||
def handle_direct_response(response)
|
||||
content = response.dig('choices', 0, 'message', 'content').strip
|
||||
parsed = JSON.parse(content)
|
||||
|
||||
{
|
||||
output: parsed['result'] || parsed['thought_process'],
|
||||
stop: parsed['stop'] || false
|
||||
}
|
||||
rescue JSON::ParserError => e
|
||||
handle_error(e, content)
|
||||
end
|
||||
|
||||
def handle_error(error, content = nil)
|
||||
@logger.error("LLM call failed: #{error.message}")
|
||||
@logger.error(error.backtrace.join("\n"))
|
||||
@logger.error("Content: #{content}") if content
|
||||
|
||||
{ output: 'Error occurred, retrying', stop: false }
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
class Captain::Tool
|
||||
class InvalidImplementationError < StandardError; end
|
||||
class InvalidSecretsError < StandardError; end
|
||||
class ExecutionError < StandardError; end
|
||||
|
||||
REQUIRED_PROPERTIES = %w[name description properties secrets].freeze
|
||||
|
||||
attr_reader :name, :description, :properties, :secrets, :implementation, :memory
|
||||
|
||||
def initialize(name:, config:)
|
||||
@name = name
|
||||
@description = config[:description]
|
||||
@properties = config[:properties]
|
||||
@secrets = config[:secrets] || []
|
||||
@implementation = config[:implementation]
|
||||
@memory = config[:memory] || {}
|
||||
end
|
||||
|
||||
def register_method(&block)
|
||||
@implementation = block
|
||||
end
|
||||
|
||||
def execute(input, provided_secrets = {})
|
||||
validate_secrets!(provided_secrets)
|
||||
validate_input!(input)
|
||||
|
||||
raise ExecutionError, 'No implementation registered' unless @implementation
|
||||
|
||||
instance_exec(input, provided_secrets, memory, &@implementation)
|
||||
rescue StandardError => e
|
||||
raise ExecutionError, "Execution failed: #{e.message}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_config!(config)
|
||||
missing_keys = REQUIRED_PROPERTIES - config.keys
|
||||
return if missing_keys.empty?
|
||||
|
||||
raise InvalidImplementationError,
|
||||
"Missing required properties: #{missing_keys.join(', ')}"
|
||||
end
|
||||
|
||||
def validate_secrets!(provided_secrets)
|
||||
required_secrets = secrets.map!(&:to_sym)
|
||||
missing_secrets = required_secrets - provided_secrets.keys
|
||||
|
||||
return if missing_secrets.empty?
|
||||
|
||||
raise InvalidSecretsError, "Missing required secrets: #{missing_secrets.join(', ')}"
|
||||
end
|
||||
|
||||
def validate_input!(input)
|
||||
properties.each do |property, constraints|
|
||||
validate_property!(input, property, constraints)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_property!(input, property, constraints)
|
||||
value = input[property.to_sym]
|
||||
|
||||
raise ArgumentError, "Missing required property: #{property}" if constraints['required'] && value.nil?
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user