Class: GenerativeUI::Catalog

Inherits:
Object
  • Object
show all
Defined in:
lib/generative_ui/catalog.rb

Defined Under Namespace

Classes: InvalidCatalogError

Constant Summary collapse

REF_DEFS =
{
  ComponentId: {
    type: 'string',
    description: 'Reference to another component id in this UI tree.'
  },
  ComponentIdList: {
    type: 'array',
    description: 'Ordered list of referenced component ids.',
    items: { "$ref": '#/$defs/ComponentId' }
  }
}.freeze
COMPONENT_NAME_REGEX =
/\A[A-Z][a-zA-Z0-9]*\z/
PROPERTY_NAME_REGEX =
/\A[a-z][a-zA-Z0-9_]*\z/
ACRONYM_RUN_REGEX =
/[A-Z]{2,}/
UNDERSCORE_CAP_REGEX =
/_[A-Z]/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCatalog

Returns a new instance of Catalog.



84
85
86
87
# File 'lib/generative_ui/catalog.rb', line 84

def initialize
  @definitions = self.class.component_definitions.dup
  validate!
end

Instance Attribute Details

#definitionsObject (readonly)

Returns the value of attribute definitions.



19
20
21
# File 'lib/generative_ui/catalog.rb', line 19

def definitions
  @definitions
end

Class Method Details

.coerce(value) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/generative_ui/catalog.rb', line 49

def self.coerce(value)
  case value
  when Catalog
    value
  when Class
    raise ArgumentError, "expected Catalog subclass, got #{value}" unless value <= Catalog

    value.new
  when Symbol, String
    name = value.to_sym
    configured = GenerativeUI.configuration.catalog(name)
    return instantiate(configured) if configured

    if name == :default
      raise ArgumentError,
            'Default generative UI catalog is not configured. Configure ' \
            '`config.catalog :default, "ApplicationGenerativeCatalog"` in an initializer.'
    end

    raise ArgumentError, "Unknown generative UI catalog: #{name.inspect}"
  else
    raise ArgumentError, 'expected Catalog, catalog class, or catalog name'
  end
end

.component(name, &block) ⇒ Object



22
23
24
25
26
27
28
29
30
31
# File 'lib/generative_ui/catalog.rb', line 22

def component(name, &block)
  definition = ComponentDefinition.build(name, &block)

  if component_definitions.any? { |existing| existing.component == definition.component }
    warn "GenerativeUI: component #{definition.component.inspect} was already declared; replacing previous declaration"
    component_definitions.delete_if { |existing| existing.component == definition.component }
  end

  component_definitions << definition
end

.component_definitionsObject



40
41
42
# File 'lib/generative_ui/catalog.rb', line 40

def component_definitions
  @component_definitions ||= []
end

.default_targetsObject



44
45
46
# File 'lib/generative_ui/catalog.rb', line 44

def default_targets
  @default_targets ||= {}
end

.instantiate(value) ⇒ Object



74
75
76
77
78
79
80
81
82
# File 'lib/generative_ui/catalog.rb', line 74

def self.instantiate(value)
  case value
  when Catalog then value
  when Class then value.new
  when String then value.constantize.new
  else
    raise ArgumentError, "cannot instantiate catalog from #{value.inspect}"
  end
end

.present_with(adapter, target = nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


33
34
35
36
37
38
# File 'lib/generative_ui/catalog.rb', line 33

def present_with(adapter, target = nil, &block)
  raise ArgumentError, 'present_with at catalog scope requires a block' unless block
  raise ArgumentError, 'present_with at catalog scope does not take a positional target' unless target.nil?

  default_targets[adapter.to_sym] = block
end

Instance Method Details

#component_schema(component_name) ⇒ Object

Raises:

  • (ArgumentError)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/generative_ui/catalog.rb', line 138

def component_schema(component_name)
  definition = fetch(component_name)
  raise ArgumentError, "Unknown generative component: #{component_name}" unless definition

  attributes_schema = definition.attributes_json_schema
  properties = attributes_schema.fetch(:properties, {}).transform_values do |schema|
    compile_provider_schema(schema)
  end
  required = Array(attributes_schema[:required]).map(&:to_sym)

  {
    type: 'object',
    "$defs": REF_DEFS,
    properties: {
      id: { type: 'string' },
      component: { const: definition.component },
      **properties
    },
    required: [:id, :component, *required],
    additionalProperties: attributes_schema.fetch(:additionalProperties, false)
  }
end

#empty?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/generative_ui/catalog.rb', line 93

def empty?
  definitions.empty?
end

#fetch(component) ⇒ Object



97
98
99
# File 'lib/generative_ui/catalog.rb', line 97

def fetch(component)
  definitions.find { |definition| definition.component == component.to_s }
end

#namesObject



89
90
91
# File 'lib/generative_ui/catalog.rb', line 89

def names
  definitions.map(&:component).sort
end

#target_for(definition, adapter) ⇒ Object



181
182
183
184
185
186
187
188
# File 'lib/generative_ui/catalog.rb', line 181

def target_for(definition, adapter)
  adapter = adapter.to_sym
  name = definition.respond_to?(:component) ? definition.component : definition.name

  definition.render_target_for(adapter) \
    || resolve_catalog_default(adapter, name) \
    || Conventions.fetch(adapter).call(name)
end

#to_promptObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/generative_ui/catalog.rb', line 120

def to_prompt
  lines = ['Components:']

  to_prompt_entries.each do |entry|
    description = entry.fetch(:description)
    lines << (description.nil? ? "- #{entry.fetch(:component)}" : "- #{entry.fetch(:component)}: #{description}")

    if entry.fetch(:required).empty? && entry.fetch(:optional).empty?
      lines << '  attributes: none'
    else
      lines << "  required: #{entry.fetch(:required).join(', ')}" if entry.fetch(:required).any?
      lines << "  optional: #{entry.fetch(:optional).join(', ')}" if entry.fetch(:optional).any?
    end
  end

  lines.join("\n")
end

#to_prompt_entriesObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/generative_ui/catalog.rb', line 101

def to_prompt_entries
  definitions.sort_by(&:component).map do |definition|
    schema = definition.attributes_json_schema
    required = Array(schema[:required] || schema['required']).map(&:to_s)
    properties = schema[:properties] || schema['properties'] || {}

    props = properties.flat_map do |name, property_schema|
      prompt_properties(name.to_s, property_schema).map { |prop| [name.to_s, prop] }
    end

    {
      component: definition.component,
      description: definition.description_text,
      required: props.select { |name, _| required.include?(name) }.map(&:last),
      optional: props.reject { |name, _| required.include?(name) }.map(&:last)
    }
  end
end

#tool_arguments_schemaObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/generative_ui/catalog.rb', line 161

def tool_arguments_schema
  {
    type: 'object',
    "$defs": REF_DEFS,
    properties: {
      components: {
        type: 'array',
        items: { anyOf: names.map { |name| component_schema(name) } }
      }
    },
    required: [:components],
    additionalProperties: false
  }
end