Files
chatwoot/enterprise/lib/captain/agent.rb
Shivam Mishra 451c28a7a1 feat: add prompt suggestions and June events (#10726)
This PR adds the following two features

1. Prompt suggestions to get started with Copilot Chat
2. June events for each action

![CleanShot 2025-01-20 at 21 00
52@2x](https://github.com/user-attachments/assets/d73e7982-0f78-4d85-873e-da2c16762688)

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-01-21 22:52:42 +05:30

138 lines
4.0 KiB
Ruby

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