signalwire-sdk 2.0.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/LICENSE +21 -0
- data/README.md +259 -0
- data/bin/swaig-test +872 -0
- data/lib/signalwire/agent/agent_base.rb +2134 -0
- data/lib/signalwire/contexts/context_builder.rb +861 -0
- data/lib/signalwire/core/logging_config.rb +54 -0
- data/lib/signalwire/datamap/data_map.rb +315 -0
- data/lib/signalwire/logging.rb +92 -0
- data/lib/signalwire/pom/prompt_object_model.rb +269 -0
- data/lib/signalwire/pom/section.rb +202 -0
- data/lib/signalwire/prefabs/concierge.rb +92 -0
- data/lib/signalwire/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire/prefabs/receptionist.rb +74 -0
- data/lib/signalwire/prefabs/survey.rb +75 -0
- data/lib/signalwire/relay/action.rb +291 -0
- data/lib/signalwire/relay/call.rb +523 -0
- data/lib/signalwire/relay/client.rb +789 -0
- data/lib/signalwire/relay/constants.rb +124 -0
- data/lib/signalwire/relay/message.rb +137 -0
- data/lib/signalwire/relay/relay_event.rb +670 -0
- data/lib/signalwire/rest/http_client.rb +159 -0
- data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
- data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
- data/lib/signalwire/rest/namespaces/project.rb +33 -0
- data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire/rest/namespaces/video.rb +129 -0
- data/lib/signalwire/rest/pagination.rb +89 -0
- data/lib/signalwire/rest/phone_call_handler.rb +56 -0
- data/lib/signalwire/rest/rest_client.rb +114 -0
- data/lib/signalwire/runtime.rb +98 -0
- data/lib/signalwire/security/session_manager.rb +124 -0
- data/lib/signalwire/security/webhook_middleware.rb +191 -0
- data/lib/signalwire/security/webhook_validator.rb +327 -0
- data/lib/signalwire/server/agent_server.rb +413 -0
- data/lib/signalwire/serverless/lambda_handler.rb +251 -0
- data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
- data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
- data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire/skills/builtin/joke.rb +65 -0
- data/lib/signalwire/skills/builtin/math.rb +176 -0
- data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire/skills/builtin/spider.rb +169 -0
- data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
- data/lib/signalwire/skills/builtin/web_search.rb +141 -0
- data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
- data/lib/signalwire/skills/skill_base.rb +82 -0
- data/lib/signalwire/skills/skill_manager.rb +97 -0
- data/lib/signalwire/skills/skill_registry.rb +258 -0
- data/lib/signalwire/swaig/function_result.rb +777 -0
- data/lib/signalwire/swml/document.rb +84 -0
- data/lib/signalwire/swml/schema.json +12250 -0
- data/lib/signalwire/swml/schema.rb +81 -0
- data/lib/signalwire/swml/service.rb +650 -0
- data/lib/signalwire/utils/schema_utils.rb +298 -0
- data/lib/signalwire/utils/serverless.rb +19 -0
- data/lib/signalwire/utils/url_validator.rb +138 -0
- data/lib/signalwire/version.rb +5 -0
- data/lib/signalwire.rb +114 -0
- metadata +225 -0
|
@@ -0,0 +1,861 @@
|
|
|
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 SignalWire
|
|
9
|
+
module Contexts
|
|
10
|
+
MAX_CONTEXTS = 50
|
|
11
|
+
MAX_STEPS_PER_CONTEXT = 100
|
|
12
|
+
|
|
13
|
+
# Reserved tool names auto-injected by the runtime when contexts/steps
|
|
14
|
+
# are present. User-defined SWAIG tools must not collide with these
|
|
15
|
+
# names.
|
|
16
|
+
#
|
|
17
|
+
# - next_step / change_context are injected when valid_steps or
|
|
18
|
+
# valid_contexts is set so the model can navigate the flow.
|
|
19
|
+
# - gather_submit is injected while a step's gather_info is
|
|
20
|
+
# collecting answers.
|
|
21
|
+
#
|
|
22
|
+
# ContextBuilder#validate! rejects any agent that registers a user
|
|
23
|
+
# tool sharing one of these names — the runtime would never call the
|
|
24
|
+
# user tool because the native one wins.
|
|
25
|
+
RESERVED_NATIVE_TOOL_NAMES = %w[next_step change_context gather_submit].freeze
|
|
26
|
+
|
|
27
|
+
# Represents a single question in a gather_info configuration.
|
|
28
|
+
class GatherQuestion
|
|
29
|
+
attr_accessor :key, :question, :type, :confirm, :prompt, :functions
|
|
30
|
+
|
|
31
|
+
def initialize(key:, question:, type: 'string', confirm: false, prompt: nil, functions: nil)
|
|
32
|
+
@key = key
|
|
33
|
+
@question = question
|
|
34
|
+
@type = type
|
|
35
|
+
@confirm = confirm
|
|
36
|
+
@prompt = prompt
|
|
37
|
+
@functions = functions
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
h = { "key" => @key, "question" => @question }
|
|
42
|
+
h["type"] = @type if @type != 'string'
|
|
43
|
+
h["confirm"] = true if @confirm
|
|
44
|
+
h["prompt"] = @prompt if @prompt
|
|
45
|
+
h["functions"] = @functions if @functions
|
|
46
|
+
h
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Configuration for gathering information in a step via the C-side gather_info system.
|
|
51
|
+
class GatherInfo
|
|
52
|
+
attr_accessor :output_key, :completion_action, :prompt
|
|
53
|
+
attr_reader :questions
|
|
54
|
+
|
|
55
|
+
def initialize(output_key: nil, completion_action: nil, prompt: nil)
|
|
56
|
+
@output_key = output_key
|
|
57
|
+
@completion_action = completion_action
|
|
58
|
+
@prompt = prompt
|
|
59
|
+
@questions = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Add a question. Returns +self+ for chaining.
|
|
63
|
+
def add_question(key:, question:, **opts)
|
|
64
|
+
@questions << GatherQuestion.new(
|
|
65
|
+
key: key,
|
|
66
|
+
question: question,
|
|
67
|
+
type: opts.fetch(:type, 'string'),
|
|
68
|
+
confirm: opts.fetch(:confirm, false),
|
|
69
|
+
prompt: opts[:prompt],
|
|
70
|
+
functions: opts[:functions]
|
|
71
|
+
)
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_h
|
|
76
|
+
raise ArgumentError, "gather_info must have at least one question" if @questions.empty?
|
|
77
|
+
|
|
78
|
+
h = { "questions" => @questions.map(&:to_h) }
|
|
79
|
+
h["prompt"] = @prompt if @prompt
|
|
80
|
+
h["output_key"] = @output_key if @output_key
|
|
81
|
+
h["completion_action"] = @completion_action if @completion_action
|
|
82
|
+
h
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Represents a single step within a context.
|
|
87
|
+
#
|
|
88
|
+
# All mutator methods return +self+ for fluent chaining.
|
|
89
|
+
class Step
|
|
90
|
+
attr_reader :name
|
|
91
|
+
|
|
92
|
+
def initialize(name)
|
|
93
|
+
@name = name
|
|
94
|
+
@text = nil
|
|
95
|
+
@step_criteria = nil
|
|
96
|
+
@functions = nil # nil | "none" | Array<String>
|
|
97
|
+
@valid_steps = nil
|
|
98
|
+
@valid_contexts = nil
|
|
99
|
+
@sections = []
|
|
100
|
+
@gather_info = nil
|
|
101
|
+
|
|
102
|
+
# Behavior flags
|
|
103
|
+
@end = false
|
|
104
|
+
@skip_user_turn = false
|
|
105
|
+
@skip_to_next_step = false
|
|
106
|
+
|
|
107
|
+
# Reset object for context-switching from steps
|
|
108
|
+
@reset_system_prompt = nil
|
|
109
|
+
@reset_user_prompt = nil
|
|
110
|
+
@reset_consolidate = false
|
|
111
|
+
@reset_full_reset = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Set the step's prompt text directly. Mutually exclusive with POM sections.
|
|
115
|
+
def set_text(text)
|
|
116
|
+
raise ArgumentError, "Cannot use set_text when POM sections have been added" if @sections.any?
|
|
117
|
+
|
|
118
|
+
@text = text
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Add a POM section (title + body). Mutually exclusive with +set_text+.
|
|
123
|
+
def add_section(title, body)
|
|
124
|
+
raise ArgumentError, "Cannot add POM sections when set_text has been used" unless @text.nil?
|
|
125
|
+
|
|
126
|
+
@sections << { "title" => title, "body" => body }
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Add a POM section with bullet points. Mutually exclusive with +set_text+.
|
|
131
|
+
def add_bullets(title, bullets)
|
|
132
|
+
raise ArgumentError, "Cannot add POM sections when set_text has been used" unless @text.nil?
|
|
133
|
+
|
|
134
|
+
@sections << { "title" => title, "bullets" => bullets }
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def set_step_criteria(criteria)
|
|
139
|
+
@step_criteria = criteria
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Set which non-internal functions are callable while this step is
|
|
144
|
+
# active.
|
|
145
|
+
#
|
|
146
|
+
# IMPORTANT — inheritance behavior:
|
|
147
|
+
# If you do NOT call this method, the step inherits whichever
|
|
148
|
+
# function set was active on the previous step (or the previous
|
|
149
|
+
# context's last step). The server-side runtime only resets the
|
|
150
|
+
# active set when a step explicitly declares its +functions+
|
|
151
|
+
# field. This is the most common source of bugs in multi-step
|
|
152
|
+
# agents: forgetting +set_functions+ on a later step lets the
|
|
153
|
+
# previous step's tools leak through. Best practice is to call
|
|
154
|
+
# +set_functions+ explicitly on every step that should differ
|
|
155
|
+
# from the previous one.
|
|
156
|
+
#
|
|
157
|
+
# Keep the per-step active set small: LLM tool selection accuracy
|
|
158
|
+
# degrades noticeably past ~7-8 simultaneously-active tools per
|
|
159
|
+
# call. Use per-step whitelisting to partition large tool
|
|
160
|
+
# collections.
|
|
161
|
+
#
|
|
162
|
+
# Internal functions (e.g. +gather_submit+, hangup hook) are
|
|
163
|
+
# ALWAYS protected and cannot be deactivated by this whitelist.
|
|
164
|
+
# The native navigation tools +next_step+ and +change_context+ are
|
|
165
|
+
# injected automatically when +set_valid_steps+/+set_valid_contexts+
|
|
166
|
+
# is used; they are not affected by this list and do not need to
|
|
167
|
+
# appear in it.
|
|
168
|
+
#
|
|
169
|
+
# @param functions [String, Array<String>] one of:
|
|
170
|
+
# - Array<String> — whitelist of function names allowed in this
|
|
171
|
+
# step. Functions not in the list become inactive.
|
|
172
|
+
# - [] — explicit disable-all (no user functions callable).
|
|
173
|
+
# - "none" — synonym for [], same effect.
|
|
174
|
+
def set_functions(functions)
|
|
175
|
+
@functions = functions
|
|
176
|
+
self
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def set_valid_steps(steps)
|
|
180
|
+
@valid_steps = steps
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def set_valid_contexts(contexts)
|
|
185
|
+
@valid_contexts = contexts
|
|
186
|
+
self
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Mark this step as terminal for the step flow.
|
|
190
|
+
#
|
|
191
|
+
# IMPORTANT: +is_end+ = true does NOT end the conversation or hang
|
|
192
|
+
# up the call. It exits step mode entirely after this step
|
|
193
|
+
# executes — clearing the steps list, current step index,
|
|
194
|
+
# valid_steps, and valid_contexts. The agent keeps running, but
|
|
195
|
+
# operates only under the base system prompt and the
|
|
196
|
+
# context-level prompt; no more step instructions are injected
|
|
197
|
+
# and no more +next_step+ tool is offered.
|
|
198
|
+
#
|
|
199
|
+
# To actually end the call, call a hangup tool or define a
|
|
200
|
+
# hangup hook.
|
|
201
|
+
def set_end(is_end)
|
|
202
|
+
@end = is_end
|
|
203
|
+
self
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def set_skip_user_turn(skip)
|
|
207
|
+
@skip_user_turn = skip
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def set_skip_to_next_step(skip)
|
|
212
|
+
@skip_to_next_step = skip
|
|
213
|
+
self
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Enable info gathering for this step. Returns +self+.
|
|
217
|
+
# After calling this, use +add_gather_question+ to define questions.
|
|
218
|
+
def set_gather_info(output_key: nil, completion_action: nil, prompt: nil)
|
|
219
|
+
@gather_info = GatherInfo.new(
|
|
220
|
+
output_key: output_key,
|
|
221
|
+
completion_action: completion_action,
|
|
222
|
+
prompt: prompt
|
|
223
|
+
)
|
|
224
|
+
self
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Add a question to this step's gather_info configuration.
|
|
228
|
+
# +set_gather_info+ must be called first.
|
|
229
|
+
#
|
|
230
|
+
# IMPORTANT — gather mode locks function access:
|
|
231
|
+
# While the model is asking gather questions, the runtime
|
|
232
|
+
# forcibly deactivates ALL of the step's other functions. The
|
|
233
|
+
# only callable tools during a gather question are:
|
|
234
|
+
#
|
|
235
|
+
# - +gather_submit+ (the native answer-submission tool)
|
|
236
|
+
# - Whatever names you pass in this question's +functions:+
|
|
237
|
+
# option
|
|
238
|
+
#
|
|
239
|
+
# +next_step+ and +change_context+ are also filtered out — the
|
|
240
|
+
# model cannot navigate away until the gather completes. This
|
|
241
|
+
# is by design: it forces a tight ask → submit → next-question
|
|
242
|
+
# loop.
|
|
243
|
+
#
|
|
244
|
+
# If a question needs to call out to a tool (e.g. validate an
|
|
245
|
+
# email, geocode a ZIP), pass that tool name in this question's
|
|
246
|
+
# +functions:+ option. Functions listed here are active ONLY for
|
|
247
|
+
# this question.
|
|
248
|
+
# Python parity: ``add_gather_question(key, question, type='string',
|
|
249
|
+
# confirm=False, prompt=None, functions=None)``. Ruby exposes the
|
|
250
|
+
# same parameter set as keyword args.
|
|
251
|
+
def add_gather_question(key:, question:, type: 'string', confirm: false,
|
|
252
|
+
prompt: nil, functions: nil)
|
|
253
|
+
raise ArgumentError, "Must call set_gather_info before add_gather_question" if @gather_info.nil?
|
|
254
|
+
|
|
255
|
+
@gather_info.add_question(
|
|
256
|
+
key: key,
|
|
257
|
+
question: question,
|
|
258
|
+
type: type,
|
|
259
|
+
confirm: confirm,
|
|
260
|
+
prompt: prompt,
|
|
261
|
+
functions: functions
|
|
262
|
+
)
|
|
263
|
+
self
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Remove all POM sections and direct text.
|
|
267
|
+
def clear_sections
|
|
268
|
+
@sections = []
|
|
269
|
+
@text = nil
|
|
270
|
+
self
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def set_reset_system_prompt(prompt)
|
|
274
|
+
@reset_system_prompt = prompt
|
|
275
|
+
self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def set_reset_user_prompt(prompt)
|
|
279
|
+
@reset_user_prompt = prompt
|
|
280
|
+
self
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def set_reset_consolidate(val)
|
|
284
|
+
@reset_consolidate = val
|
|
285
|
+
self
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def set_reset_full_reset(val)
|
|
289
|
+
@reset_full_reset = val
|
|
290
|
+
self
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def to_h
|
|
294
|
+
step_h = {
|
|
295
|
+
"name" => @name,
|
|
296
|
+
"text" => render_text
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
step_h["step_criteria"] = @step_criteria if @step_criteria
|
|
300
|
+
step_h["functions"] = @functions unless @functions.nil?
|
|
301
|
+
step_h["valid_steps"] = @valid_steps if @valid_steps
|
|
302
|
+
step_h["valid_contexts"] = @valid_contexts if @valid_contexts
|
|
303
|
+
step_h["end"] = true if @end
|
|
304
|
+
step_h["skip_user_turn"] = true if @skip_user_turn
|
|
305
|
+
step_h["skip_to_next_step"] = true if @skip_to_next_step
|
|
306
|
+
|
|
307
|
+
reset = {}
|
|
308
|
+
reset["system_prompt"] = @reset_system_prompt if @reset_system_prompt
|
|
309
|
+
reset["user_prompt"] = @reset_user_prompt if @reset_user_prompt
|
|
310
|
+
reset["consolidate"] = @reset_consolidate if @reset_consolidate
|
|
311
|
+
reset["full_reset"] = @reset_full_reset if @reset_full_reset
|
|
312
|
+
step_h["reset"] = reset if reset.any?
|
|
313
|
+
|
|
314
|
+
step_h["gather_info"] = @gather_info.to_h if @gather_info
|
|
315
|
+
|
|
316
|
+
step_h
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
def render_text
|
|
322
|
+
return @text if @text
|
|
323
|
+
|
|
324
|
+
raise ArgumentError, "Step '#{@name}' has no text or POM sections defined" if @sections.empty?
|
|
325
|
+
|
|
326
|
+
parts = []
|
|
327
|
+
@sections.each do |section|
|
|
328
|
+
if section.key?("bullets")
|
|
329
|
+
parts << "## #{section['title']}"
|
|
330
|
+
section["bullets"].each { |b| parts << "- #{b}" }
|
|
331
|
+
else
|
|
332
|
+
parts << "## #{section['title']}"
|
|
333
|
+
parts << section["body"]
|
|
334
|
+
end
|
|
335
|
+
parts << "" # spacing
|
|
336
|
+
end
|
|
337
|
+
parts.join("\n").strip
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Represents a single context containing multiple steps.
|
|
342
|
+
class Context
|
|
343
|
+
attr_reader :name
|
|
344
|
+
|
|
345
|
+
def initialize(name)
|
|
346
|
+
@name = name
|
|
347
|
+
@steps = {} # name => Step
|
|
348
|
+
@step_order = []
|
|
349
|
+
|
|
350
|
+
# Navigation
|
|
351
|
+
@valid_contexts = nil
|
|
352
|
+
@valid_steps = nil
|
|
353
|
+
@initial_step = nil
|
|
354
|
+
|
|
355
|
+
# Context entry parameters
|
|
356
|
+
@post_prompt = nil
|
|
357
|
+
@system_prompt = nil
|
|
358
|
+
@system_prompt_sections = []
|
|
359
|
+
@consolidate = false
|
|
360
|
+
@full_reset = false
|
|
361
|
+
@user_prompt = nil
|
|
362
|
+
@isolated = false
|
|
363
|
+
|
|
364
|
+
# Context prompt
|
|
365
|
+
@prompt_text = nil
|
|
366
|
+
@prompt_sections = []
|
|
367
|
+
|
|
368
|
+
# Fillers
|
|
369
|
+
@enter_fillers = nil
|
|
370
|
+
@exit_fillers = nil
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Add a new step. Returns the new Step object (not self).
|
|
374
|
+
#
|
|
375
|
+
# Python parity: ``Context.add_step(name, *, task=None, bullets=None,
|
|
376
|
+
# criteria=None, functions=None, valid_steps=None)``. The optional
|
|
377
|
+
# keyword arguments give a one-call configuration shortcut:
|
|
378
|
+
#
|
|
379
|
+
# ctx.add_step("greet",
|
|
380
|
+
# task: "Greet the caller",
|
|
381
|
+
# bullets: ["Say hi", "Ask how can I help"],
|
|
382
|
+
# criteria: "User has been greeted",
|
|
383
|
+
# functions: ["weather"],
|
|
384
|
+
# valid_steps: ["help"])
|
|
385
|
+
#
|
|
386
|
+
# Without the optional args this stays the bare ``add_step("greet")``
|
|
387
|
+
# form that returns a Step for further fluent configuration.
|
|
388
|
+
def add_step(name, task: nil, bullets: nil, criteria: nil,
|
|
389
|
+
functions: nil, valid_steps: nil)
|
|
390
|
+
raise ArgumentError, "Step '#{name}' already exists in context '#{@name}'" if @steps.key?(name)
|
|
391
|
+
raise ArgumentError, "Maximum steps per context (#{MAX_STEPS_PER_CONTEXT}) exceeded" if @steps.size >= MAX_STEPS_PER_CONTEXT
|
|
392
|
+
|
|
393
|
+
step = Step.new(name)
|
|
394
|
+
@steps[name] = step
|
|
395
|
+
@step_order << name
|
|
396
|
+
|
|
397
|
+
step.add_section('Task', task) unless task.nil?
|
|
398
|
+
step.add_bullets('Process', bullets) unless bullets.nil?
|
|
399
|
+
step.set_step_criteria(criteria) unless criteria.nil?
|
|
400
|
+
step.set_functions(functions) unless functions.nil?
|
|
401
|
+
step.set_valid_steps(valid_steps) unless valid_steps.nil?
|
|
402
|
+
|
|
403
|
+
step
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Get an existing step by name. Returns Step or nil.
|
|
407
|
+
def get_step(name)
|
|
408
|
+
@steps[name]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Remove a step by name. Returns self.
|
|
412
|
+
def remove_step(name)
|
|
413
|
+
if @steps.key?(name)
|
|
414
|
+
@steps.delete(name)
|
|
415
|
+
@step_order.delete(name)
|
|
416
|
+
end
|
|
417
|
+
self
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Move an existing step to a specific position. Returns self.
|
|
421
|
+
def move_step(name, position)
|
|
422
|
+
raise ArgumentError, "Step '#{name}' not found in context '#{@name}'" unless @steps.key?(name)
|
|
423
|
+
|
|
424
|
+
@step_order.delete(name)
|
|
425
|
+
@step_order.insert(position, name)
|
|
426
|
+
self
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Set which step the context starts on when entered.
|
|
430
|
+
#
|
|
431
|
+
# By default, a context starts on its first step (index 0). Use
|
|
432
|
+
# this to skip a preamble step on re-entry via +change_context+.
|
|
433
|
+
#
|
|
434
|
+
# @param step_name [String] name of the step to start on.
|
|
435
|
+
def set_initial_step(step_name)
|
|
436
|
+
@initial_step = step_name
|
|
437
|
+
self
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def set_valid_contexts(contexts)
|
|
441
|
+
@valid_contexts = contexts
|
|
442
|
+
self
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def set_valid_steps(steps)
|
|
446
|
+
@valid_steps = steps
|
|
447
|
+
self
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def set_post_prompt(prompt)
|
|
451
|
+
@post_prompt = prompt
|
|
452
|
+
self
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def set_system_prompt(prompt)
|
|
456
|
+
raise ArgumentError, "Cannot use set_system_prompt when POM system sections exist" if @system_prompt_sections.any?
|
|
457
|
+
|
|
458
|
+
@system_prompt = prompt
|
|
459
|
+
self
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def set_prompt(prompt)
|
|
463
|
+
raise ArgumentError, "Cannot use set_prompt when POM prompt sections exist" if @prompt_sections.any?
|
|
464
|
+
|
|
465
|
+
@prompt_text = prompt
|
|
466
|
+
self
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def set_consolidate(val)
|
|
470
|
+
@consolidate = val
|
|
471
|
+
self
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def set_full_reset(val)
|
|
475
|
+
@full_reset = val
|
|
476
|
+
self
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def set_user_prompt(prompt)
|
|
480
|
+
@user_prompt = prompt
|
|
481
|
+
self
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Mark this context as isolated — entering it wipes conversation
|
|
485
|
+
# history.
|
|
486
|
+
#
|
|
487
|
+
# When +val+ = true and the context is entered via change_context,
|
|
488
|
+
# the runtime wipes the conversation array. The model starts
|
|
489
|
+
# fresh with only the new context's system_prompt + step
|
|
490
|
+
# instructions, with no memory of prior turns.
|
|
491
|
+
#
|
|
492
|
+
# EXCEPTION — reset overrides the wipe:
|
|
493
|
+
# If the context also has a reset configuration (via
|
|
494
|
+
# +set_consolidate+ or +set_full_reset+), the wipe is skipped in
|
|
495
|
+
# favor of the reset behavior. Use reset with consolidate=true
|
|
496
|
+
# to summarize prior history into a single message instead of
|
|
497
|
+
# dropping it entirely.
|
|
498
|
+
#
|
|
499
|
+
# Use cases: switching to a sensitive billing flow that should
|
|
500
|
+
# not see prior small-talk; handing off to a different agent
|
|
501
|
+
# persona; resetting after a long off-topic detour.
|
|
502
|
+
def set_isolated(val)
|
|
503
|
+
@isolated = val
|
|
504
|
+
self
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Add a POM section to the context prompt.
|
|
508
|
+
def add_section(title, body)
|
|
509
|
+
raise ArgumentError, "Cannot add POM sections when set_prompt has been used" unless @prompt_text.nil?
|
|
510
|
+
|
|
511
|
+
@prompt_sections << { "title" => title, "body" => body }
|
|
512
|
+
self
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Add a POM section with bullets to the context prompt.
|
|
516
|
+
def add_bullets(title, bullets)
|
|
517
|
+
raise ArgumentError, "Cannot add POM sections when set_prompt has been used" unless @prompt_text.nil?
|
|
518
|
+
|
|
519
|
+
@prompt_sections << { "title" => title, "bullets" => bullets }
|
|
520
|
+
self
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Add a POM section to the system prompt.
|
|
524
|
+
def add_system_section(title, body)
|
|
525
|
+
raise ArgumentError, "Cannot add POM system sections when set_system_prompt has been used" unless @system_prompt.nil?
|
|
526
|
+
|
|
527
|
+
@system_prompt_sections << { "title" => title, "body" => body }
|
|
528
|
+
self
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Add a POM section with bullets to the system prompt.
|
|
532
|
+
def add_system_bullets(title, bullets)
|
|
533
|
+
raise ArgumentError, "Cannot add POM system sections when set_system_prompt has been used" unless @system_prompt.nil?
|
|
534
|
+
|
|
535
|
+
@system_prompt_sections << { "title" => title, "bullets" => bullets }
|
|
536
|
+
self
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def set_enter_fillers(fillers)
|
|
540
|
+
@enter_fillers = fillers if fillers.is_a?(Hash) && fillers.any?
|
|
541
|
+
self
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def set_exit_fillers(fillers)
|
|
545
|
+
@exit_fillers = fillers if fillers.is_a?(Hash) && fillers.any?
|
|
546
|
+
self
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def add_enter_filler(lang_code, fillers)
|
|
550
|
+
if lang_code && fillers.is_a?(Array) && fillers.any?
|
|
551
|
+
@enter_fillers ||= {}
|
|
552
|
+
@enter_fillers[lang_code] = fillers
|
|
553
|
+
end
|
|
554
|
+
self
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def add_exit_filler(lang_code, fillers)
|
|
558
|
+
if lang_code && fillers.is_a?(Array) && fillers.any?
|
|
559
|
+
@exit_fillers ||= {}
|
|
560
|
+
@exit_fillers[lang_code] = fillers
|
|
561
|
+
end
|
|
562
|
+
self
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def to_h
|
|
566
|
+
raise ArgumentError, "Context '#{@name}' has no steps defined" if @steps.empty?
|
|
567
|
+
|
|
568
|
+
ctx = {
|
|
569
|
+
"steps" => @step_order.map { |n| @steps[n].to_h }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
ctx["valid_contexts"] = @valid_contexts if @valid_contexts
|
|
573
|
+
ctx["valid_steps"] = @valid_steps if @valid_steps
|
|
574
|
+
ctx["initial_step"] = @initial_step if @initial_step
|
|
575
|
+
ctx["post_prompt"] = @post_prompt if @post_prompt
|
|
576
|
+
|
|
577
|
+
sys = render_system_prompt
|
|
578
|
+
ctx["system_prompt"] = sys if sys
|
|
579
|
+
|
|
580
|
+
ctx["consolidate"] = @consolidate if @consolidate
|
|
581
|
+
ctx["full_reset"] = @full_reset if @full_reset
|
|
582
|
+
ctx["user_prompt"] = @user_prompt if @user_prompt
|
|
583
|
+
ctx["isolated"] = @isolated if @isolated
|
|
584
|
+
|
|
585
|
+
if @prompt_sections.any?
|
|
586
|
+
ctx["pom"] = @prompt_sections
|
|
587
|
+
elsif @prompt_text
|
|
588
|
+
ctx["prompt"] = @prompt_text
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
ctx["enter_fillers"] = @enter_fillers if @enter_fillers
|
|
592
|
+
ctx["exit_fillers"] = @exit_fillers if @exit_fillers
|
|
593
|
+
|
|
594
|
+
ctx
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Expose internal state for validation
|
|
598
|
+
# @api private
|
|
599
|
+
def _steps; @steps; end
|
|
600
|
+
def _step_order; @step_order; end
|
|
601
|
+
def _initial_step; @initial_step; end
|
|
602
|
+
|
|
603
|
+
private
|
|
604
|
+
|
|
605
|
+
def render_system_prompt
|
|
606
|
+
return @system_prompt if @system_prompt
|
|
607
|
+
return nil if @system_prompt_sections.empty?
|
|
608
|
+
|
|
609
|
+
render_sections(@system_prompt_sections)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def render_sections(sections)
|
|
613
|
+
parts = []
|
|
614
|
+
sections.each do |s|
|
|
615
|
+
if s.key?("bullets")
|
|
616
|
+
parts << "## #{s['title']}"
|
|
617
|
+
s["bullets"].each { |b| parts << "- #{b}" }
|
|
618
|
+
else
|
|
619
|
+
parts << "## #{s['title']}"
|
|
620
|
+
parts << s["body"]
|
|
621
|
+
end
|
|
622
|
+
parts << ""
|
|
623
|
+
end
|
|
624
|
+
parts.join("\n").strip
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Builder for multi-step, multi-context AI agent workflows.
|
|
629
|
+
#
|
|
630
|
+
# A ContextBuilder owns one or more Contexts; each Context owns an
|
|
631
|
+
# ordered list of Steps. Only one context and one step is active at
|
|
632
|
+
# a time. Per chat turn, the runtime injects the current step's
|
|
633
|
+
# instructions as a system message, then asks the LLM for a
|
|
634
|
+
# response.
|
|
635
|
+
#
|
|
636
|
+
# == Native tools auto-injected by the runtime
|
|
637
|
+
#
|
|
638
|
+
# When a step (or its enclosing context) declares +valid_steps+ or
|
|
639
|
+
# +valid_contexts+, the runtime auto-injects two native tools so
|
|
640
|
+
# the model can navigate the flow:
|
|
641
|
+
#
|
|
642
|
+
# - +next_step(step: enum)+ — present when valid_steps is set
|
|
643
|
+
# - +change_context(context: enum)+ — present when valid_contexts is set
|
|
644
|
+
#
|
|
645
|
+
# Their +enum+ schemas are rewritten on every turn to match
|
|
646
|
+
# whatever valid_steps / valid_contexts apply to the current step.
|
|
647
|
+
# You do NOT need to define these tools yourself; they appear
|
|
648
|
+
# automatically.
|
|
649
|
+
#
|
|
650
|
+
# A third native tool — +gather_submit+ — is injected during
|
|
651
|
+
# gather_info questioning (see Step#set_gather_info /
|
|
652
|
+
# Step#add_gather_question).
|
|
653
|
+
#
|
|
654
|
+
# These three names — +next_step+, +change_context+,
|
|
655
|
+
# +gather_submit+ — are reserved. +validate!+ will reject any agent
|
|
656
|
+
# that defines a SWAIG tool with one of these names. See
|
|
657
|
+
# RESERVED_NATIVE_TOOL_NAMES.
|
|
658
|
+
#
|
|
659
|
+
# == Function whitelisting (Step#set_functions)
|
|
660
|
+
#
|
|
661
|
+
# Each step may declare a +functions+ whitelist. The whitelist is
|
|
662
|
+
# applied in-memory at the start of each LLM turn. CRITICALLY: if a
|
|
663
|
+
# step does NOT declare a +functions+ field, it INHERITS the
|
|
664
|
+
# previous step's active set. See Step#set_functions for details
|
|
665
|
+
# and examples.
|
|
666
|
+
class ContextBuilder
|
|
667
|
+
# Python parity: ``ContextBuilder.__init__(self, agent)`` accepts
|
|
668
|
+
# an owning agent so ``validate!`` can introspect registered
|
|
669
|
+
# SWAIG tools when checking for reserved-name collisions.
|
|
670
|
+
# Ruby allows nil for standalone use (tests, idiom of building
|
|
671
|
+
# a builder before attaching).
|
|
672
|
+
def initialize(agent = nil)
|
|
673
|
+
@contexts = {} # name => Context
|
|
674
|
+
@context_order = []
|
|
675
|
+
@agent = agent
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Attach an agent reference so +validate!+ can check
|
|
679
|
+
# user-defined tool names against RESERVED_NATIVE_TOOL_NAMES.
|
|
680
|
+
# Called internally by AgentBase#define_contexts.
|
|
681
|
+
def attach_agent(agent)
|
|
682
|
+
@agent = agent
|
|
683
|
+
self
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Remove all contexts, returning the builder to its initial state.
|
|
687
|
+
# Use this in a dynamic config callback when you need to rebuild
|
|
688
|
+
# contexts from scratch for a specific request.
|
|
689
|
+
def reset
|
|
690
|
+
@contexts.clear
|
|
691
|
+
@context_order.clear
|
|
692
|
+
self
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Add a new context. Returns the Context object.
|
|
696
|
+
def add_context(name)
|
|
697
|
+
raise ArgumentError, "Context '#{name}' already exists" if @contexts.key?(name)
|
|
698
|
+
raise ArgumentError, "Maximum number of contexts (#{MAX_CONTEXTS}) exceeded" if @contexts.size >= MAX_CONTEXTS
|
|
699
|
+
|
|
700
|
+
ctx = Context.new(name)
|
|
701
|
+
@contexts[name] = ctx
|
|
702
|
+
@context_order << name
|
|
703
|
+
ctx
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Get an existing context by name. Returns Context or nil.
|
|
707
|
+
def get_context(name)
|
|
708
|
+
@contexts[name]
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Validate the full configuration. Raises ArgumentError on problems.
|
|
712
|
+
def validate!
|
|
713
|
+
raise ArgumentError, "At least one context must be defined" if @contexts.empty?
|
|
714
|
+
|
|
715
|
+
# Single context must be named "default"
|
|
716
|
+
if @contexts.size == 1
|
|
717
|
+
ctx_name = @contexts.keys.first
|
|
718
|
+
raise ArgumentError, "When using a single context, it must be named 'default'" if ctx_name != 'default'
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Each context must have at least one step
|
|
722
|
+
@contexts.each do |ctx_name, ctx|
|
|
723
|
+
raise ArgumentError, "Context '#{ctx_name}' must have at least one step" if ctx._steps.empty?
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# Validate initial_step references a real step in the context
|
|
727
|
+
@contexts.each do |ctx_name, ctx|
|
|
728
|
+
is = ctx._initial_step
|
|
729
|
+
if is && !ctx._steps.key?(is)
|
|
730
|
+
available = ctx._steps.keys.sort
|
|
731
|
+
raise ArgumentError,
|
|
732
|
+
"Context '#{ctx_name}' has initial_step='#{is}' but that step does " \
|
|
733
|
+
"not exist. Available steps: #{available.inspect}"
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Validate step references in valid_steps
|
|
738
|
+
@contexts.each do |ctx_name, ctx|
|
|
739
|
+
ctx._steps.each do |step_name, step|
|
|
740
|
+
step_h = step.to_h
|
|
741
|
+
if step_h["valid_steps"]
|
|
742
|
+
step_h["valid_steps"].each do |vs|
|
|
743
|
+
next if vs == "next"
|
|
744
|
+
unless ctx._steps.key?(vs)
|
|
745
|
+
raise ArgumentError,
|
|
746
|
+
"Step '#{step_name}' in context '#{ctx_name}' references unknown step '#{vs}'"
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# Validate context references at context level
|
|
754
|
+
@contexts.each do |ctx_name, ctx|
|
|
755
|
+
ctx_h = ctx.to_h
|
|
756
|
+
if ctx_h["valid_contexts"]
|
|
757
|
+
ctx_h["valid_contexts"].each do |vc|
|
|
758
|
+
unless @contexts.key?(vc)
|
|
759
|
+
raise ArgumentError,
|
|
760
|
+
"Context '#{ctx_name}' references unknown context '#{vc}'"
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Validate context references at step level
|
|
767
|
+
@contexts.each do |ctx_name, ctx|
|
|
768
|
+
ctx._steps.each do |step_name, step|
|
|
769
|
+
step_h = step.to_h
|
|
770
|
+
if step_h["valid_contexts"]
|
|
771
|
+
step_h["valid_contexts"].each do |vc|
|
|
772
|
+
unless @contexts.key?(vc)
|
|
773
|
+
raise ArgumentError,
|
|
774
|
+
"Step '#{step_name}' in context '#{ctx_name}' references unknown context '#{vc}'"
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Validate gather_info configurations
|
|
782
|
+
@contexts.each do |ctx_name, ctx|
|
|
783
|
+
ctx._steps.each do |step_name, step|
|
|
784
|
+
step_h = step.to_h
|
|
785
|
+
next unless step_h.key?("gather_info")
|
|
786
|
+
|
|
787
|
+
gi = step_h["gather_info"]
|
|
788
|
+
questions = gi["questions"] || []
|
|
789
|
+
raise ArgumentError,
|
|
790
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info with no questions" if questions.empty?
|
|
791
|
+
|
|
792
|
+
keys_seen = Set.new
|
|
793
|
+
questions.each do |q|
|
|
794
|
+
raise ArgumentError,
|
|
795
|
+
"Step '#{step_name}' in context '#{ctx_name}' has duplicate gather_info question key '#{q['key']}'" if keys_seen.include?(q["key"])
|
|
796
|
+
keys_seen << q["key"]
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
action = gi["completion_action"]
|
|
800
|
+
if action
|
|
801
|
+
if action == "next_step"
|
|
802
|
+
idx = ctx._step_order.index(step_name)
|
|
803
|
+
if idx >= ctx._step_order.size - 1
|
|
804
|
+
raise ArgumentError,
|
|
805
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info " \
|
|
806
|
+
"completion_action='next_step' but it is the last step in the " \
|
|
807
|
+
"context. Either (1) add another step after '#{step_name}', " \
|
|
808
|
+
"(2) set completion_action to the name of an existing step in " \
|
|
809
|
+
"this context to jump to it, or (3) set completion_action=nil " \
|
|
810
|
+
"(default) to stay in '#{step_name}' after gathering completes."
|
|
811
|
+
end
|
|
812
|
+
elsif !ctx._steps.key?(action)
|
|
813
|
+
available = ctx._steps.keys.sort
|
|
814
|
+
raise ArgumentError,
|
|
815
|
+
"Step '#{step_name}' in context '#{ctx_name}' has gather_info " \
|
|
816
|
+
"completion_action='#{action}' but '#{action}' is not a step in " \
|
|
817
|
+
"this context. Valid options: 'next_step' (advance to the next " \
|
|
818
|
+
"sequential step), nil (stay in the current step), or one of " \
|
|
819
|
+
"#{available.inspect}."
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Validate that user-defined tools do not collide with reserved
|
|
826
|
+
# native tool names. The runtime auto-injects next_step /
|
|
827
|
+
# change_context / gather_submit when contexts/steps are
|
|
828
|
+
# present, so user tools sharing those names would never be
|
|
829
|
+
# called.
|
|
830
|
+
if @agent && @agent.respond_to?(:list_tool_names)
|
|
831
|
+
registered = @agent.list_tool_names.to_a
|
|
832
|
+
colliding = registered.select { |n| RESERVED_NATIVE_TOOL_NAMES.include?(n) }.sort.uniq
|
|
833
|
+
if colliding.any?
|
|
834
|
+
raise ArgumentError,
|
|
835
|
+
"Tool name(s) #{colliding.inspect} collide with reserved native " \
|
|
836
|
+
"tools auto-injected by contexts/steps. The names " \
|
|
837
|
+
"#{RESERVED_NATIVE_TOOL_NAMES.sort.inspect} are reserved and " \
|
|
838
|
+
"cannot be used for user-defined SWAIG tools when contexts/steps " \
|
|
839
|
+
"are in use. Rename your tool(s) to avoid the collision."
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
true
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def to_h
|
|
847
|
+
validate!
|
|
848
|
+
result = {}
|
|
849
|
+
@context_order.each do |name|
|
|
850
|
+
result[name] = @contexts[name].to_h
|
|
851
|
+
end
|
|
852
|
+
result
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Helper to create a standalone context (not via ContextBuilder).
|
|
857
|
+
def self.create_simple_context(name = 'default')
|
|
858
|
+
Context.new(name)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
end
|