1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
# frozen_string_literal: true
module Dispatch
module Adapter
module Tester
# Immutable value object representing a single step in a playbook.
# Validated at parse time so errors surface early.
class Step
VALID_TYPES = %w[message tool_calls].freeze
attr_reader :step_id, :type, :content, :tool_calls
def initialize(data)
validate_and_assign!(data)
freeze
end
def message?
@type == "message"
end
def tool_calls?
@type == "tool_calls"
end
def to_s
"Step ##{@step_id} (#{@type})"
end
private
def validate_and_assign!(data)
validate_hash!(data)
@step_id = extract_step_id!(data)
@type = extract_type!(data)
@content = data["content"]
if tool_calls?
@tool_calls = extract_tool_calls!(data)
else
@tool_calls = []
validate_message_has_content!
end
end
def validate_hash!(data)
return if data.is_a?(Hash)
raise InvalidPlaybookError, "Each step must be a JSON object, got #{data.class}"
end
def extract_step_id!(data)
step_id = data["step"]
raise InvalidPlaybookError, "Step is missing required field 'step'" if step_id.nil?
unless step_id.is_a?(Integer)
raise InvalidPlaybookError, "Step 'step' field must be an integer, got #{step_id.inspect}"
end
step_id
end
def extract_type!(data)
type = data["type"]
raise InvalidPlaybookError, "Step ##{@step_id} is missing required field 'type'" if type.nil?
unless VALID_TYPES.include?(type)
raise InvalidPlaybookError,
"Step ##{@step_id} has invalid type #{type.inspect}. " \
"Must be one of: #{VALID_TYPES.join(", ")}"
end
type
end
def extract_tool_calls!(data)
raw = data["tool_calls"]
if raw.nil? || !raw.is_a?(Array) || raw.empty?
raise InvalidPlaybookError,
"Step ##{@step_id} (tool_calls) requires a non-empty 'tool_calls' array"
end
raw.map.with_index do |tc, idx|
validate_tool_call!(tc, idx)
end
end
def validate_tool_call!(tc, idx)
unless tc.is_a?(Hash)
raise InvalidPlaybookError,
"Step ##{@step_id}, tool_call[#{idx}] must be a JSON object"
end
%w[id name arguments].each do |field|
if tc[field].nil?
raise InvalidPlaybookError,
"Step ##{@step_id}, tool_call[#{idx}] is missing required field '#{field}'"
end
end
unless tc["id"].is_a?(String)
raise InvalidPlaybookError,
"Step ##{@step_id}, tool_call[#{idx}] 'id' must be a string"
end
unless tc["name"].is_a?(String)
raise InvalidPlaybookError,
"Step ##{@step_id}, tool_call[#{idx}] 'name' must be a string"
end
unless tc["arguments"].is_a?(Hash)
raise InvalidPlaybookError,
"Step ##{@step_id}, tool_call[#{idx}] 'arguments' must be a JSON object"
end
tc
end
def validate_message_has_content!
return if @content.is_a?(String) && [email protected]?
raise InvalidPlaybookError,
"Step ##{@step_id} (message) requires a non-empty 'content' string"
end
end
end
end
end
|