fix: Use handoff_key for scenarios (#13755)
# Pull Request Template
## Description
Ensure agent function names stay within OpenAI's 64-char limit
(ai-agents prepends "handoff_to_").
Add HANDOFF_TITLE_SLUG_MAX_LENGTH and
handoff_key generation: persisted records use `scenario_{id}_agent`; new
records use a truncated title slug.
Assistant scenario keys and agent_name now reference the generated
handoff key.
fixes :
`Invalid 'messages[9].tool_calls[0].function.name': string too long.
Expected a string with maximum length 64, but got a string with length
95 instead.`
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Tested locally
<img width="1806" height="1044" alt="image"
src="https://github.com/user-attachments/assets/40cd7a3d-3d97-43a8-bd56-d3f5d63abbda"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -106,7 +106,7 @@ class Captain::Assistant < ApplicationRecord
|
||||
scenarios: scenarios.enabled.map do |scenario|
|
||||
{
|
||||
title: scenario.title,
|
||||
key: scenario.title.parameterize.underscore,
|
||||
key: scenario.handoff_key,
|
||||
description: scenario.description
|
||||
}
|
||||
end,
|
||||
|
||||
@@ -24,6 +24,19 @@ class Captain::Scenario < ApplicationRecord
|
||||
include Concerns::CaptainToolsHelpers
|
||||
include Concerns::Agentable
|
||||
|
||||
# OpenAI enforces a 64-char limit on function names. The ai-agents gem
|
||||
# prepends "handoff_to_" (11 chars), so we keep a safety margin and cap
|
||||
# the full tool name to MAX_HANDOFF_TOOL_NAME_LENGTH (60 chars).
|
||||
# Format: "scenario_{id}_{slug}_agent" for persisted records (stable + readable),
|
||||
# and "scenario_draft_{slug}_agent" for unsaved records, with slug truncated
|
||||
# based on the available length budget.
|
||||
HANDOFF_TOOL_PREFIX = 'handoff_to_'.freeze
|
||||
HANDOFF_KEY_PREFIX = 'scenario'.freeze
|
||||
HANDOFF_KEY_SUFFIX = 'agent'.freeze
|
||||
MAX_HANDOFF_TOOL_NAME_LENGTH = 60
|
||||
MAX_AGENT_NAME_LENGTH = MAX_HANDOFF_TOOL_NAME_LENGTH - HANDOFF_TOOL_PREFIX.length
|
||||
MAX_HANDOFF_SLUG_LENGTH = 24
|
||||
|
||||
self.table_name = 'captain_scenarios'
|
||||
|
||||
belongs_to :assistant, class_name: 'Captain::Assistant'
|
||||
@@ -42,6 +55,10 @@ class Captain::Scenario < ApplicationRecord
|
||||
|
||||
before_save :resolve_tool_references
|
||||
|
||||
def handoff_key
|
||||
[handoff_id_key, compact_handoff_slug, HANDOFF_KEY_SUFFIX].compact.join('_')
|
||||
end
|
||||
|
||||
def prompt_context
|
||||
{
|
||||
title: title,
|
||||
@@ -56,7 +73,28 @@ class Captain::Scenario < ApplicationRecord
|
||||
private
|
||||
|
||||
def agent_name
|
||||
"#{title} Agent".parameterize(separator: '_')
|
||||
handoff_key
|
||||
end
|
||||
|
||||
def handoff_id_key
|
||||
return "#{HANDOFF_KEY_PREFIX}_#{id}" if id.present?
|
||||
|
||||
"#{HANDOFF_KEY_PREFIX}_draft"
|
||||
end
|
||||
|
||||
def compact_handoff_slug
|
||||
slug = title.to_s.parameterize(separator: '_').presence
|
||||
return nil if slug.blank?
|
||||
|
||||
max_slug_length = [MAX_HANDOFF_SLUG_LENGTH, dynamic_slug_max_length].min
|
||||
return nil if max_slug_length <= 0
|
||||
|
||||
slug.first(max_slug_length).sub(/_+\z/, '').presence
|
||||
end
|
||||
|
||||
def dynamic_slug_max_length
|
||||
# handoff_to_#{scenario_<id>_<slug>_agent}
|
||||
MAX_AGENT_NAME_LENGTH - handoff_id_key.length - HANDOFF_KEY_SUFFIX.length - 2
|
||||
end
|
||||
|
||||
def agent_tools
|
||||
|
||||
@@ -42,6 +42,45 @@ RSpec.describe Captain::Scenario, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#handoff_key' do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'uses id plus readable slug for persisted scenarios' do
|
||||
scenario = create(:captain_scenario, assistant: assistant, account: account,
|
||||
title: 'Handle complex refund requests requiring manager approval steps')
|
||||
|
||||
expect(scenario.handoff_key).to start_with("scenario_#{scenario.id}_")
|
||||
expect(scenario.handoff_key).to end_with('_agent')
|
||||
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
|
||||
end
|
||||
|
||||
it 'uses a truncated slug key for unsaved scenarios' do
|
||||
scenario = build(:captain_scenario, assistant: assistant, account: account,
|
||||
title: 'Troubleshoot payment gateway errors for recurring subscription charges')
|
||||
|
||||
expect(scenario.handoff_key).to match(/\Ascenario_draft_[a-z0-9_]+_agent\z/)
|
||||
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
|
||||
end
|
||||
|
||||
it 'stays within length budget even for large ids' do
|
||||
scenario = build(:captain_scenario, assistant: assistant, account: account,
|
||||
title: 'A very long scenario title used only for budget verification')
|
||||
allow(scenario).to receive(:id).and_return(1_234_567_890_123_456_789)
|
||||
|
||||
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
|
||||
end
|
||||
|
||||
it 'exposes handoff keys in assistant prompt context' do
|
||||
scenario = create(:captain_scenario, assistant: assistant, account: account)
|
||||
|
||||
prompt_context = assistant.send(:prompt_context)
|
||||
scenario_config = prompt_context[:scenarios].find { |entry| entry[:title] == scenario.title }
|
||||
|
||||
expect(scenario_config[:key]).to eq(scenario.handoff_key)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tool validation and population' do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
Reference in New Issue
Block a user