Class: SignalWire::Contexts::ContextBuilder
- Inherits:
-
Object
- Object
- SignalWire::Contexts::ContextBuilder
- Defined in:
- lib/signalwire/contexts/context_builder.rb
Overview
Builder for multi-step, multi-context AI agent workflows.
A ContextBuilder owns one or more Contexts; each Context owns an ordered list of Steps. Only one context and one step is active at a time. Per chat turn, the runtime injects the current step’s instructions as a system message, then asks the LLM for a response.
Native tools auto-injected by the runtime
When a step (or its enclosing context) declares valid_steps or valid_contexts, the runtime auto-injects two native tools so the model can navigate the flow:
- +next_step(step: enum)+ — present when valid_steps is set
- +change_context(context: enum)+ — present when valid_contexts is set
Their enum schemas are rewritten on every turn to match whatever valid_steps / valid_contexts apply to the current step. You do NOT need to define these tools yourself; they appear automatically.
A third native tool — gather_submit — is injected during gather_info questioning (see Step#set_gather_info / Step#add_gather_question).
These three names — next_step, change_context, gather_submit — are reserved. validate! will reject any agent that defines a SWAIG tool with one of these names. See RESERVED_NATIVE_TOOL_NAMES.
Function whitelisting (Step#set_functions)
Each step may declare a functions whitelist. The whitelist is applied in-memory at the start of each LLM turn. CRITICALLY: if a step does NOT declare a functions field, it INHERITS the previous step’s active set. See Step#set_functions for details and examples.
Instance Method Summary collapse
-
#add_context(name) ⇒ Object
Add a new context.
-
#attach_agent(agent) ⇒ Object
Attach an agent reference so
validate!can check user-defined tool names against RESERVED_NATIVE_TOOL_NAMES. -
#get_context(name) ⇒ Object
Get an existing context by name.
-
#initialize(agent = nil) ⇒ ContextBuilder
constructor
Python parity: “ContextBuilder.__init__(self, agent)“ accepts an owning agent so “validate!“ can introspect registered SWAIG tools when checking for reserved-name collisions.
-
#reset ⇒ Object
Remove all contexts, returning the builder to its initial state.
- #to_h ⇒ Object
-
#validate! ⇒ Object
Validate the full configuration.
Constructor Details
#initialize(agent = nil) ⇒ ContextBuilder
Python parity: “ContextBuilder.__init__(self, agent)“ accepts an owning agent so “validate!“ can introspect registered SWAIG tools when checking for reserved-name collisions. Ruby allows nil for standalone use (tests, idiom of building a builder before attaching).
672 673 674 675 676 |
# File 'lib/signalwire/contexts/context_builder.rb', line 672 def initialize(agent = nil) @contexts = {} # name => Context @context_order = [] @agent = agent end |
Instance Method Details
#add_context(name) ⇒ Object
Add a new context. Returns the Context object.
696 697 698 699 700 701 702 703 704 |
# File 'lib/signalwire/contexts/context_builder.rb', line 696 def add_context(name) raise ArgumentError, "Context '#{name}' already exists" if @contexts.key?(name) raise ArgumentError, "Maximum number of contexts (#{MAX_CONTEXTS}) exceeded" if @contexts.size >= MAX_CONTEXTS ctx = Context.new(name) @contexts[name] = ctx @context_order << name ctx end |
#attach_agent(agent) ⇒ Object
Attach an agent reference so validate! can check user-defined tool names against RESERVED_NATIVE_TOOL_NAMES. Called internally by AgentBase#define_contexts.
681 682 683 684 |
# File 'lib/signalwire/contexts/context_builder.rb', line 681 def attach_agent(agent) @agent = agent self end |
#get_context(name) ⇒ Object
Get an existing context by name. Returns Context or nil.
707 708 709 |
# File 'lib/signalwire/contexts/context_builder.rb', line 707 def get_context(name) @contexts[name] end |
#reset ⇒ Object
Remove all contexts, returning the builder to its initial state. Use this in a dynamic config callback when you need to rebuild contexts from scratch for a specific request.
689 690 691 692 693 |
# File 'lib/signalwire/contexts/context_builder.rb', line 689 def reset @contexts.clear @context_order.clear self end |
#to_h ⇒ Object
846 847 848 849 850 851 852 853 |
# File 'lib/signalwire/contexts/context_builder.rb', line 846 def to_h validate! result = {} @context_order.each do |name| result[name] = @contexts[name].to_h end result end |
#validate! ⇒ Object
Validate the full configuration. Raises ArgumentError on problems.
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 |
# File 'lib/signalwire/contexts/context_builder.rb', line 712 def validate! raise ArgumentError, "At least one context must be defined" if @contexts.empty? # Single context must be named "default" if @contexts.size == 1 ctx_name = @contexts.keys.first raise ArgumentError, "When using a single context, it must be named 'default'" if ctx_name != 'default' end # Each context must have at least one step @contexts.each do |ctx_name, ctx| raise ArgumentError, "Context '#{ctx_name}' must have at least one step" if ctx._steps.empty? end # Validate initial_step references a real step in the context @contexts.each do |ctx_name, ctx| is = ctx._initial_step if is && !ctx._steps.key?(is) available = ctx._steps.keys.sort raise ArgumentError, "Context '#{ctx_name}' has initial_step='#{is}' but that step does " \ "not exist. Available steps: #{available.inspect}" end end # Validate step references in valid_steps @contexts.each do |ctx_name, ctx| ctx._steps.each do |step_name, step| step_h = step.to_h if step_h["valid_steps"] step_h["valid_steps"].each do |vs| next if vs == "next" unless ctx._steps.key?(vs) raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' references unknown step '#{vs}'" end end end end end # Validate context references at context level @contexts.each do |ctx_name, ctx| ctx_h = ctx.to_h if ctx_h["valid_contexts"] ctx_h["valid_contexts"].each do |vc| unless @contexts.key?(vc) raise ArgumentError, "Context '#{ctx_name}' references unknown context '#{vc}'" end end end end # Validate context references at step level @contexts.each do |ctx_name, ctx| ctx._steps.each do |step_name, step| step_h = step.to_h if step_h["valid_contexts"] step_h["valid_contexts"].each do |vc| unless @contexts.key?(vc) raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' references unknown context '#{vc}'" end end end end end # Validate gather_info configurations @contexts.each do |ctx_name, ctx| ctx._steps.each do |step_name, step| step_h = step.to_h next unless step_h.key?("gather_info") gi = step_h["gather_info"] questions = gi["questions"] || [] raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' has gather_info with no questions" if questions.empty? keys_seen = Set.new questions.each do |q| raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' has duplicate gather_info question key '#{q['key']}'" if keys_seen.include?(q["key"]) keys_seen << q["key"] end action = gi["completion_action"] if action if action == "next_step" idx = ctx._step_order.index(step_name) if idx >= ctx._step_order.size - 1 raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' has gather_info " \ "completion_action='next_step' but it is the last step in the " \ "context. Either (1) add another step after '#{step_name}', " \ "(2) set completion_action to the name of an existing step in " \ "this context to jump to it, or (3) set completion_action=nil " \ "(default) to stay in '#{step_name}' after gathering completes." end elsif !ctx._steps.key?(action) available = ctx._steps.keys.sort raise ArgumentError, "Step '#{step_name}' in context '#{ctx_name}' has gather_info " \ "completion_action='#{action}' but '#{action}' is not a step in " \ "this context. Valid options: 'next_step' (advance to the next " \ "sequential step), nil (stay in the current step), or one of " \ "#{available.inspect}." end end end end # Validate that user-defined tools do not collide with reserved # native tool names. The runtime auto-injects next_step / # change_context / gather_submit when contexts/steps are # present, so user tools sharing those names would never be # called. if @agent && @agent.respond_to?(:list_tool_names) registered = @agent.list_tool_names.to_a colliding = registered.select { |n| RESERVED_NATIVE_TOOL_NAMES.include?(n) }.sort.uniq if colliding.any? raise ArgumentError, "Tool name(s) #{colliding.inspect} collide with reserved native " \ "tools auto-injected by contexts/steps. The names " \ "#{RESERVED_NATIVE_TOOL_NAMES.sort.inspect} are reserved and " \ "cannot be used for user-defined SWAIG tools when contexts/steps " \ "are in use. Rename your tool(s) to avoid the collision." end end true end |