signalwire_agents 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +79 -0
- data/bin/swaig-test +310 -0
- data/lib/signalwire_agents/agent/agent_base.rb +1171 -0
- data/lib/signalwire_agents/contexts/context_builder.rb +622 -0
- data/lib/signalwire_agents/datamap/data_map.rb +279 -0
- data/lib/signalwire_agents/logging.rb +92 -0
- data/lib/signalwire_agents/prefabs/concierge.rb +92 -0
- data/lib/signalwire_agents/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire_agents/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire_agents/prefabs/receptionist.rb +74 -0
- data/lib/signalwire_agents/prefabs/survey.rb +75 -0
- data/lib/signalwire_agents/relay/action.rb +291 -0
- data/lib/signalwire_agents/relay/call.rb +523 -0
- data/lib/signalwire_agents/relay/client.rb +671 -0
- data/lib/signalwire_agents/relay/constants.rb +124 -0
- data/lib/signalwire_agents/relay/message.rb +137 -0
- data/lib/signalwire_agents/relay/relay_event.rb +670 -0
- data/lib/signalwire_agents/rest/http_client.rb +147 -0
- data/lib/signalwire_agents/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire_agents/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire_agents/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire_agents/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire_agents/rest/namespaces/fabric.rb +175 -0
- data/lib/signalwire_agents/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire_agents/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire_agents/rest/namespaces/phone_numbers.rb +20 -0
- data/lib/signalwire_agents/rest/namespaces/project.rb +33 -0
- data/lib/signalwire_agents/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire_agents/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire_agents/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire_agents/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire_agents/rest/namespaces/video.rb +129 -0
- data/lib/signalwire_agents/rest/signalwire_client.rb +110 -0
- data/lib/signalwire_agents/security/session_manager.rb +124 -0
- data/lib/signalwire_agents/server/agent_server.rb +260 -0
- data/lib/signalwire_agents/skills/builtin/api_ninjas_trivia.rb +91 -0
- data/lib/signalwire_agents/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire_agents/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire_agents/skills/builtin/datasphere.rb +141 -0
- data/lib/signalwire_agents/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire_agents/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire_agents/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire_agents/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire_agents/skills/builtin/joke.rb +65 -0
- data/lib/signalwire_agents/skills/builtin/math.rb +176 -0
- data/lib/signalwire_agents/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire_agents/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire_agents/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire_agents/skills/builtin/spider.rb +142 -0
- data/lib/signalwire_agents/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire_agents/skills/builtin/weather_api.rb +85 -0
- data/lib/signalwire_agents/skills/builtin/web_search.rb +123 -0
- data/lib/signalwire_agents/skills/builtin/wikipedia_search.rb +109 -0
- data/lib/signalwire_agents/skills/skill_base.rb +58 -0
- data/lib/signalwire_agents/skills/skill_manager.rb +85 -0
- data/lib/signalwire_agents/skills/skill_registry.rb +76 -0
- data/lib/signalwire_agents/swaig/function_result.rb +777 -0
- data/lib/signalwire_agents/swml/document.rb +84 -0
- data/lib/signalwire_agents/swml/schema.json +12250 -0
- data/lib/signalwire_agents/swml/schema.rb +81 -0
- data/lib/signalwire_agents/swml/service.rb +304 -0
- data/lib/signalwire_agents/version.rb +5 -0
- data/lib/signalwire_agents.rb +19 -0
- metadata +212 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025 SignalWire
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the MIT License.
|
|
6
|
+
# See LICENSE file in the project root for full license information.
|
|
7
|
+
|
|
8
|
+
module SignalWireAgents
|
|
9
|
+
module Contexts
|
|
10
|
+
MAX_CONTEXTS = 50
|
|
11
|
+
MAX_STEPS_PER_CONTEXT = 100
|
|
12
|
+
|
|
13
|
+
# Represents a single question in a gather_info configuration.
|
|
14
|
+
class GatherQuestion
|
|
15
|
+
attr_accessor :key, :question, :type, :confirm, :prompt, :functions
|
|
16
|
+
|
|
17
|
+
def initialize(key:, question:, type: 'string', confirm: false, prompt: nil, functions: nil)
|
|
18
|
+
@key = key
|
|
19
|
+
@question = question
|
|
20
|
+
@type = type
|
|
21
|
+
@confirm = confirm
|
|
22
|
+
@prompt = prompt
|
|
23
|
+
@functions = functions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
h = { "key" => @key, "question" => @question }
|
|
28
|
+
h["type"] = @type if @type != 'string'
|
|
29
|
+
h["confirm"] = true if @confirm
|
|
30
|
+
h["prompt"] = @prompt if @prompt
|
|
31
|
+
h["functions"] = @functions if @functions
|
|
32
|
+
h
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Configuration for gathering information in a step via the C-side gather_info system.
|
|
37
|
+
class GatherInfo
|
|
38
|
+
attr_accessor :output_key, :completion_action, :prompt
|
|
39
|
+
attr_reader :questions
|
|
40
|
+
|
|
41
|
+
def initialize(output_key: nil, completion_action: nil, prompt: nil)
|
|
42
|
+
@output_key = output_key
|
|
43
|
+
@completion_action = completion_action
|
|
44
|
+
@prompt = prompt
|
|
45
|
+
@questions = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add a question. Returns +self+ for chaining.
|
|
49
|
+
def add_question(key:, question:, **opts)
|
|
50
|
+
@questions << GatherQuestion.new(
|
|
51
|
+
key: key,
|
|
52
|
+
question: question,
|
|
53
|
+
type: opts.fetch(:type, 'string'),
|
|
54
|
+
confirm: opts.fetch(:confirm, false),
|
|
55
|
+
prompt: opts[:prompt],
|
|
56
|
+
functions: opts[:functions]
|
|
57
|
+
)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
raise ArgumentError, "gather_info must have at least one question" if @questions.empty?
|
|
63
|
+
|
|
64
|
+
h = { "questions" => @questions.map(&:to_h) }
|
|
65
|
+
h["prompt"] = @prompt if @prompt
|
|
66
|
+
h["output_key"] = @output_key if @output_key
|
|
67
|
+
h["completion_action"] = @completion_action if @completion_action
|
|
68
|
+
h
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Represents a single step within a context.
|
|
73
|
+
#
|
|
74
|
+
# All mutator methods return +self+ for fluent chaining.
|
|
75
|
+
class Step
|
|
76
|
+
attr_reader :name
|
|
77
|
+
|
|
78
|
+
def initialize(name)
|
|
79
|
+
@name = name
|
|
80
|
+
@text = nil
|
|
81
|
+
@step_criteria = nil
|
|
82
|
+
@functions = nil # nil | "none" | Array<String>
|
|
83
|
+
@valid_steps = nil
|
|
84
|
+
@valid_contexts = nil
|
|
85
|
+
@sections = []
|
|
86
|
+
@gather_info = nil
|
|
87
|
+
|
|
88
|
+
# Behavior flags
|
|
89
|
+
@end = false
|
|
90
|
+
@skip_user_turn = false
|
|
91
|
+
@skip_to_next_step = false
|
|
92
|
+
|
|
93
|
+
# Reset object for context-switching from steps
|
|
94
|
+
@reset_system_prompt = nil
|
|
95
|
+
@reset_user_prompt = nil
|
|
96
|
+
@reset_consolidate = false
|
|
97
|
+
@reset_full_reset = false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Set the step's prompt text directly. Mutually exclusive with POM sections.
|
|
101
|
+
def set_text(text)
|
|
102
|
+
raise ArgumentError, "Cannot use set_text when POM sections have been added" if @sections.any?
|
|
103
|
+
|
|
104
|
+
@text = text
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Add a POM section (title + body). Mutually exclusive with +set_text+.
|
|
109
|
+
def add_section(title, body)
|
|
110
|
+
raise ArgumentError, "Cannot add POM sections when set_text has been used" unless @text.nil?
|
|
111
|
+
|
|
112
|
+
@sections << { "title" => title, "body" => body }
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Add a POM section with bullet points. Mutually exclusive with +set_text+.
|
|
117
|
+
def add_bullets(title, bullets)
|
|
118
|
+
raise ArgumentError, "Cannot add POM sections when set_text has been used" unless @text.nil?
|
|
119
|
+
|
|
120
|
+
@sections << { "title" => title, "bullets" => bullets }
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def set_step_criteria(criteria)
|
|
125
|
+
@step_criteria = criteria
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param functions [String, Array<String>] "none" to disable all, or list of names
|
|
130
|
+
def set_functions(functions)
|
|
131
|
+
@functions = functions
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def set_valid_steps(steps)
|
|
136
|
+
@valid_steps = steps
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def set_valid_contexts(contexts)
|
|
141
|
+
@valid_contexts = contexts
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def set_end(is_end)
|
|
146
|
+
@end = is_end
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def set_skip_user_turn(skip)
|
|
151
|
+
@skip_user_turn = skip
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def set_skip_to_next_step(skip)
|
|
156
|
+
@skip_to_next_step = skip
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Enable info gathering for this step. Returns +self+.
|
|
161
|
+
# After calling this, use +add_gather_question+ to define questions.
|
|
162
|
+
def set_gather_info(output_key: nil, completion_action: nil, prompt: nil)
|
|
163
|
+
@gather_info = GatherInfo.new(
|
|
164
|
+
output_key: output_key,
|
|
165
|
+
completion_action: completion_action,
|
|
166
|
+
prompt: prompt
|
|
167
|
+
)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Add a question to this step's gather_info configuration.
|
|
172
|
+
# +set_gather_info+ must be called first.
|
|
173
|
+
def add_gather_question(key:, question:, **opts)
|
|
174
|
+
raise ArgumentError, "Must call set_gather_info before add_gather_question" if @gather_info.nil?
|
|
175
|
+
|
|
176
|
+
@gather_info.add_question(key: key, question: question, **opts)
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Remove all POM sections and direct text.
|
|
181
|
+
def clear_sections
|
|
182
|
+
@sections = []
|
|
183
|
+
@text = nil
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def set_reset_system_prompt(prompt)
|
|
188
|
+
@reset_system_prompt = prompt
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def set_reset_user_prompt(prompt)
|
|
193
|
+
@reset_user_prompt = prompt
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def set_reset_consolidate(val)
|
|
198
|
+
@reset_consolidate = val
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def set_reset_full_reset(val)
|
|
203
|
+
@reset_full_reset = val
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_h
|
|
208
|
+
step_h = {
|
|
209
|
+
"name" => @name,
|
|
210
|
+
"text" => render_text
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
step_h["step_criteria"] = @step_criteria if @step_criteria
|
|
214
|
+
step_h["functions"] = @functions unless @functions.nil?
|
|
215
|
+
step_h["valid_steps"] = @valid_steps if @valid_steps
|
|
216
|
+
step_h["valid_contexts"] = @valid_contexts if @valid_contexts
|
|
217
|
+
step_h["end"] = true if @end
|
|
218
|
+
step_h["skip_user_turn"] = true if @skip_user_turn
|
|
219
|
+
step_h["skip_to_next_step"] = true if @skip_to_next_step
|
|
220
|
+
|
|
221
|
+
reset = {}
|
|
222
|
+
reset["system_prompt"] = @reset_system_prompt if @reset_system_prompt
|
|
223
|
+
reset["user_prompt"] = @reset_user_prompt if @reset_user_prompt
|
|
224
|
+
reset["consolidate"] = @reset_consolidate if @reset_consolidate
|
|
225
|
+
reset["full_reset"] = @reset_full_reset if @reset_full_reset
|
|
226
|
+
step_h["reset"] = reset if reset.any?
|
|
227
|
+
|
|
228
|
+
step_h["gather_info"] = @gather_info.to_h if @gather_info
|
|
229
|
+
|
|
230
|
+
step_h
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def render_text
|
|
236
|
+
return @text if @text
|
|
237
|
+
|
|
238
|
+
raise ArgumentError, "Step '#{@name}' has no text or POM sections defined" if @sections.empty?
|
|
239
|
+
|
|
240
|
+
parts = []
|
|
241
|
+
@sections.each do |section|
|
|
242
|
+
if section.key?("bullets")
|
|
243
|
+
parts << "## #{section['title']}"
|
|
244
|
+
section["bullets"].each { |b| parts << "- #{b}" }
|
|
245
|
+
else
|
|
246
|
+
parts << "## #{section['title']}"
|
|
247
|
+
parts << section["body"]
|
|
248
|
+
end
|
|
249
|
+
parts << "" # spacing
|
|
250
|
+
end
|
|
251
|
+
parts.join("\n").strip
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Represents a single context containing multiple steps.
|
|
256
|
+
class Context
|
|
257
|
+
attr_reader :name
|
|
258
|
+
|
|
259
|
+
def initialize(name)
|
|
260
|
+
@name = name
|
|
261
|
+
@steps = {} # name => Step
|
|
262
|
+
@step_order = []
|
|
263
|
+
|
|
264
|
+
# Navigation
|
|
265
|
+
@valid_contexts = nil
|
|
266
|
+
@valid_steps = nil
|
|
267
|
+
|
|
268
|
+
# Context entry parameters
|
|
269
|
+
@post_prompt = nil
|
|
270
|
+
@system_prompt = nil
|
|
271
|
+
@system_prompt_sections = []
|
|
272
|
+
@consolidate = false
|
|
273
|
+
@full_reset = false
|
|
274
|
+
@user_prompt = nil
|
|
275
|
+
@isolated = false
|
|
276
|
+
|
|
277
|
+
# Context prompt
|
|
278
|
+
@prompt_text = nil
|
|
279
|
+
@prompt_sections = []
|
|
280
|
+
|
|
281
|
+
# Fillers
|
|
282
|
+
@enter_fillers = nil
|
|
283
|
+
@exit_fillers = nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Add a new step. Returns the new Step object (not self).
|
|
287
|
+
def add_step(name)
|
|
288
|
+
raise ArgumentError, "Step '#{name}' already exists in context '#{@name}'" if @steps.key?(name)
|
|
289
|
+
raise ArgumentError, "Maximum steps per context (#{MAX_STEPS_PER_CONTEXT}) exceeded" if @steps.size >= MAX_STEPS_PER_CONTEXT
|
|
290
|
+
|
|
291
|
+
step = Step.new(name)
|
|
292
|
+
@steps[name] = step
|
|
293
|
+
@step_order << name
|
|
294
|
+
step
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Get an existing step by name. Returns Step or nil.
|
|
298
|
+
def get_step(name)
|
|
299
|
+
@steps[name]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Remove a step by name. Returns self.
|
|
303
|
+
def remove_step(name)
|
|
304
|
+
if @steps.key?(name)
|
|
305
|
+
@steps.delete(name)
|
|
306
|
+
@step_order.delete(name)
|
|
307
|
+
end
|
|
308
|
+
self
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Move an existing step to a specific position. Returns self.
|
|
312
|
+
def move_step(name, position)
|
|
313
|
+
raise ArgumentError, "Step '#{name}' not found in context '#{@name}'" unless @steps.key?(name)
|
|
314
|
+
|
|
315
|
+
@step_order.delete(name)
|
|
316
|
+
@step_order.insert(position, name)
|
|
317
|
+
self
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def set_valid_contexts(contexts)
|
|
321
|
+
@valid_contexts = contexts
|
|
322
|
+
self
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def set_valid_steps(steps)
|
|
326
|
+
@valid_steps = steps
|
|
327
|
+
self
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def set_post_prompt(prompt)
|
|
331
|
+
@post_prompt = prompt
|
|
332
|
+
self
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def set_system_prompt(prompt)
|
|
336
|
+
raise ArgumentError, "Cannot use set_system_prompt when POM system sections exist" if @system_prompt_sections.any?
|
|
337
|
+
|
|
338
|
+
@system_prompt = prompt
|
|
339
|
+
self
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def set_prompt(prompt)
|
|
343
|
+
raise ArgumentError, "Cannot use set_prompt when POM prompt sections exist" if @prompt_sections.any?
|
|
344
|
+
|
|
345
|
+
@prompt_text = prompt
|
|
346
|
+
self
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def set_consolidate(val)
|
|
350
|
+
@consolidate = val
|
|
351
|
+
self
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def set_full_reset(val)
|
|
355
|
+
@full_reset = val
|
|
356
|
+
self
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def set_user_prompt(prompt)
|
|
360
|
+
@user_prompt = prompt
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def set_isolated(val)
|
|
365
|
+
@isolated = val
|
|
366
|
+
self
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Add a POM section to the context prompt.
|
|
370
|
+
def add_section(title, body)
|
|
371
|
+
raise ArgumentError, "Cannot add POM sections when set_prompt has been used" unless @prompt_text.nil?
|
|
372
|
+
|
|
373
|
+
@prompt_sections << { "title" => title, "body" => body }
|
|
374
|
+
self
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Add a POM section with bullets to the context prompt.
|
|
378
|
+
def add_bullets(title, bullets)
|
|
379
|
+
raise ArgumentError, "Cannot add POM sections when set_prompt has been used" unless @prompt_text.nil?
|
|
380
|
+
|
|
381
|
+
@prompt_sections << { "title" => title, "bullets" => bullets }
|
|
382
|
+
self
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Add a POM section to the system prompt.
|
|
386
|
+
def add_system_section(title, body)
|
|
387
|
+
raise ArgumentError, "Cannot add POM system sections when set_system_prompt has been used" unless @system_prompt.nil?
|
|
388
|
+
|
|
389
|
+
@system_prompt_sections << { "title" => title, "body" => body }
|
|
390
|
+
self
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Add a POM section with bullets to the system prompt.
|
|
394
|
+
def add_system_bullets(title, bullets)
|
|
395
|
+
raise ArgumentError, "Cannot add POM system sections when set_system_prompt has been used" unless @system_prompt.nil?
|
|
396
|
+
|
|
397
|
+
@system_prompt_sections << { "title" => title, "bullets" => bullets }
|
|
398
|
+
self
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def set_enter_fillers(fillers)
|
|
402
|
+
@enter_fillers = fillers if fillers.is_a?(Hash) && fillers.any?
|
|
403
|
+
self
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def set_exit_fillers(fillers)
|
|
407
|
+
@exit_fillers = fillers if fillers.is_a?(Hash) && fillers.any?
|
|
408
|
+
self
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def add_enter_filler(lang_code, fillers)
|
|
412
|
+
if lang_code && fillers.is_a?(Array) && fillers.any?
|
|
413
|
+
@enter_fillers ||= {}
|
|
414
|
+
@enter_fillers[lang_code] = fillers
|
|
415
|
+
end
|
|
416
|
+
self
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def add_exit_filler(lang_code, fillers)
|
|
420
|
+
if lang_code && fillers.is_a?(Array) && fillers.any?
|
|
421
|
+
@exit_fillers ||= {}
|
|
422
|
+
@exit_fillers[lang_code] = fillers
|
|
423
|
+
end
|
|
424
|
+
self
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def to_h
|
|
428
|
+
raise ArgumentError, "Context '#{@name}' has no steps defined" if @steps.empty?
|
|
429
|
+
|
|
430
|
+
ctx = {
|
|
431
|
+
"steps" => @step_order.map { |n| @steps[n].to_h }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
ctx["valid_contexts"] = @valid_contexts if @valid_contexts
|
|
435
|
+
ctx["valid_steps"] = @valid_steps if @valid_steps
|
|
436
|
+
ctx["post_prompt"] = @post_prompt if @post_prompt
|
|
437
|
+
|
|
438
|
+
sys = render_system_prompt
|
|
439
|
+
ctx["system_prompt"] = sys if sys
|
|
440
|
+
|
|
441
|
+
ctx["consolidate"] = @consolidate if @consolidate
|
|
442
|
+
ctx["full_reset"] = @full_reset if @full_reset
|
|
443
|
+
ctx["user_prompt"] = @user_prompt if @user_prompt
|
|
444
|
+
ctx["isolated"] = @isolated if @isolated
|
|
445
|
+
|
|
446
|
+
if @prompt_sections.any?
|
|
447
|
+
ctx["pom"] = @prompt_sections
|
|
448
|
+
elsif @prompt_text
|
|
449
|
+
ctx["prompt"] = @prompt_text
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
ctx["enter_fillers"] = @enter_fillers if @enter_fillers
|
|
453
|
+
ctx["exit_fillers"] = @exit_fillers if @exit_fillers
|
|
454
|
+
|
|
455
|
+
ctx
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Expose internal state for validation
|
|
459
|
+
# @api private
|
|
460
|
+
def _steps; @steps; end
|
|
461
|
+
def _step_order; @step_order; end
|
|
462
|
+
|
|
463
|
+
private
|
|
464
|
+
|
|
465
|
+
def render_system_prompt
|
|
466
|
+
return @system_prompt if @system_prompt
|
|
467
|
+
return nil if @system_prompt_sections.empty?
|
|
468
|
+
|
|
469
|
+
render_sections(@system_prompt_sections)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def render_sections(sections)
|
|
473
|
+
parts = []
|
|
474
|
+
sections.each do |s|
|
|
475
|
+
if s.key?("bullets")
|
|
476
|
+
parts << "## #{s['title']}"
|
|
477
|
+
s["bullets"].each { |b| parts << "- #{b}" }
|
|
478
|
+
else
|
|
479
|
+
parts << "## #{s['title']}"
|
|
480
|
+
parts << s["body"]
|
|
481
|
+
end
|
|
482
|
+
parts << ""
|
|
483
|
+
end
|
|
484
|
+
parts.join("\n").strip
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Main builder that holds multiple contexts and validates the configuration.
|
|
489
|
+
class ContextBuilder
|
|
490
|
+
def initialize
|
|
491
|
+
@contexts = {} # name => Context
|
|
492
|
+
@context_order = []
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Add a new context. Returns the Context object.
|
|
496
|
+
def add_context(name)
|
|
497
|
+
raise ArgumentError, "Context '#{name}' already exists" if @contexts.key?(name)
|
|
498
|
+
raise ArgumentError, "Maximum number of contexts (#{MAX_CONTEXTS}) exceeded" if @contexts.size >= MAX_CONTEXTS
|
|
499
|
+
|
|
500
|
+
ctx = Context.new(name)
|
|
501
|
+
@contexts[name] = ctx
|
|
502
|
+
@context_order << name
|
|
503
|
+
ctx
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Get an existing context by name. Returns Context or nil.
|
|
507
|
+
def get_context(name)
|
|
508
|
+
@contexts[name]
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Validate the full configuration. Raises ArgumentError on problems.
|
|
512
|
+
def validate!
|
|
513
|
+
raise ArgumentError, "At least one context must be defined" if @contexts.empty?
|
|
514
|
+
|
|
515
|
+
# Single context must be named "default"
|
|
516
|
+
if @contexts.size == 1
|
|
517
|
+
ctx_name = @contexts.keys.first
|
|
518
|
+
raise ArgumentError, "When using a single context, it must be named 'default'" if ctx_name != 'default'
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Each context must have at least one step
|
|
522
|
+
@contexts.each do |ctx_name, ctx|
|
|
523
|
+
raise ArgumentError, "Context '#{ctx_name}' must have at least one step" if ctx._steps.empty?
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Validate step references in valid_steps
|
|
527
|
+
@contexts.each do |ctx_name, ctx|
|
|
528
|
+
ctx._steps.each do |step_name, step|
|
|
529
|
+
step_h = step.to_h
|
|
530
|
+
if step_h["valid_steps"]
|
|
531
|
+
step_h["valid_steps"].each do |vs|
|
|
532
|
+
next if vs == "next"
|
|
533
|
+
unless ctx._steps.key?(vs)
|
|
534
|
+
raise ArgumentError,
|
|
535
|
+
"Step '#{step_name}' in context '#{ctx_name}' references unknown step '#{vs}'"
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Validate context references at context level
|
|
543
|
+
@contexts.each do |ctx_name, ctx|
|
|
544
|
+
ctx_h = ctx.to_h
|
|
545
|
+
if ctx_h["valid_contexts"]
|
|
546
|
+
ctx_h["valid_contexts"].each do |vc|
|
|
547
|
+
unless @contexts.key?(vc)
|
|
548
|
+
raise ArgumentError,
|
|
549
|
+
"Context '#{ctx_name}' references unknown context '#{vc}'"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Validate context references at step level
|
|
556
|
+
@contexts.each do |ctx_name, ctx|
|
|
557
|
+
ctx._steps.each do |step_name, step|
|
|
558
|
+
step_h = step.to_h
|
|
559
|
+
if step_h["valid_contexts"]
|
|
560
|
+
step_h["valid_contexts"].each do |vc|
|
|
561
|
+
unless @contexts.key?(vc)
|
|
562
|
+
raise ArgumentError,
|
|
563
|
+
"Step '#{step_name}' in context '#{ctx_name}' references unknown context '#{vc}'"
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Validate gather_info configurations
|
|
571
|
+
@contexts.each do |ctx_name, ctx|
|
|
572
|
+
ctx._steps.each do |step_name, step|
|
|
573
|
+
step_h = step.to_h
|
|
574
|
+
next unless step_h.key?("gather_info")
|
|
575
|
+
|
|
576
|
+
gi = step_h["gather_info"]
|
|
577
|
+
questions = gi["questions"] || []
|
|
578
|
+
raise ArgumentError,
|
|
579
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info with no questions" if questions.empty?
|
|
580
|
+
|
|
581
|
+
keys_seen = Set.new
|
|
582
|
+
questions.each do |q|
|
|
583
|
+
raise ArgumentError,
|
|
584
|
+
"Step '#{step_name}' in context '#{ctx_name}' has duplicate gather_info question key '#{q['key']}'" if keys_seen.include?(q["key"])
|
|
585
|
+
keys_seen << q["key"]
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
action = gi["completion_action"]
|
|
589
|
+
if action
|
|
590
|
+
if action == "next_step"
|
|
591
|
+
idx = ctx._step_order.index(step_name)
|
|
592
|
+
if idx >= ctx._step_order.size - 1
|
|
593
|
+
raise ArgumentError,
|
|
594
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info completion_action='next_step' but it is the last step"
|
|
595
|
+
end
|
|
596
|
+
elsif !ctx._steps.key?(action)
|
|
597
|
+
raise ArgumentError,
|
|
598
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info completion_action='#{action}' but step '#{action}' does not exist"
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
true
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def to_h
|
|
608
|
+
validate!
|
|
609
|
+
result = {}
|
|
610
|
+
@context_order.each do |name|
|
|
611
|
+
result[name] = @contexts[name].to_h
|
|
612
|
+
end
|
|
613
|
+
result
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Helper to create a standalone context (not via ContextBuilder).
|
|
618
|
+
def self.create_simple_context(name = 'default')
|
|
619
|
+
Context.new(name)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
end
|