a2ui-rails 0.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +373 -0
  3. data/lib/a2ui/modules/action_handler.rb +25 -0
  4. data/lib/a2ui/modules/data_validator.rb +22 -0
  5. data/lib/a2ui/modules/input_parser.rb +25 -0
  6. data/lib/a2ui/modules/layout_adapter.rb +25 -0
  7. data/lib/a2ui/modules/surface.rb +66 -0
  8. data/lib/a2ui/modules/surface_manager.rb +64 -0
  9. data/lib/a2ui/modules/ui_generator.rb +25 -0
  10. data/lib/a2ui/modules/ui_updater.rb +26 -0
  11. data/lib/a2ui/modules.rb +17 -0
  12. data/lib/a2ui/signatures/adapt_layout.rb +19 -0
  13. data/lib/a2ui/signatures/generate_ui.rb +20 -0
  14. data/lib/a2ui/signatures/handle_action.rb +23 -0
  15. data/lib/a2ui/signatures/parse_input.rb +21 -0
  16. data/lib/a2ui/signatures/update_ui.rb +21 -0
  17. data/lib/a2ui/signatures/validate_data.rb +18 -0
  18. data/lib/a2ui/signatures.rb +12 -0
  19. data/lib/a2ui/types/action.rb +9 -0
  20. data/lib/a2ui/types/action_response_type.rb +13 -0
  21. data/lib/a2ui/types/alignment.rb +13 -0
  22. data/lib/a2ui/types/boolean_value.rb +9 -0
  23. data/lib/a2ui/types/button_component.rb +12 -0
  24. data/lib/a2ui/types/card_component.rb +11 -0
  25. data/lib/a2ui/types/check_box_component.rb +10 -0
  26. data/lib/a2ui/types/column_component.rb +12 -0
  27. data/lib/a2ui/types/context_binding.rb +9 -0
  28. data/lib/a2ui/types/data_driven_children.rb +9 -0
  29. data/lib/a2ui/types/data_update.rb +9 -0
  30. data/lib/a2ui/types/distribution.rb +15 -0
  31. data/lib/a2ui/types/divider_component.rb +9 -0
  32. data/lib/a2ui/types/explicit_children.rb +8 -0
  33. data/lib/a2ui/types/icon_component.rb +10 -0
  34. data/lib/a2ui/types/image_component.rb +11 -0
  35. data/lib/a2ui/types/image_fit.rb +14 -0
  36. data/lib/a2ui/types/input_type.rb +16 -0
  37. data/lib/a2ui/types/list_component.rb +10 -0
  38. data/lib/a2ui/types/literal_value.rb +8 -0
  39. data/lib/a2ui/types/number_value.rb +9 -0
  40. data/lib/a2ui/types/object_value.rb +9 -0
  41. data/lib/a2ui/types/orientation.rb +11 -0
  42. data/lib/a2ui/types/path_reference.rb +8 -0
  43. data/lib/a2ui/types/row_component.rb +12 -0
  44. data/lib/a2ui/types/screen_size.rb +12 -0
  45. data/lib/a2ui/types/select_component.rb +12 -0
  46. data/lib/a2ui/types/slider_component.rb +13 -0
  47. data/lib/a2ui/types/stream_action.rb +16 -0
  48. data/lib/a2ui/types/stream_op.rb +10 -0
  49. data/lib/a2ui/types/string_value.rb +9 -0
  50. data/lib/a2ui/types/text_component.rb +10 -0
  51. data/lib/a2ui/types/text_field_component.rb +13 -0
  52. data/lib/a2ui/types/text_usage_hint.rb +16 -0
  53. data/lib/a2ui/types/user_action.rb +11 -0
  54. data/lib/a2ui/types/validation_issue.rb +10 -0
  55. data/lib/a2ui/types.rb +91 -0
  56. data/lib/a2ui/version.rb +6 -0
  57. data/lib/a2ui.rb +7 -0
  58. metadata +130 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 35be118652b4117d296da399cf1c3cd845d479b98353dd3ce2b449ef9d7d9886
4
+ data.tar.gz: 32d8a2385bf14245720a82ffaa6cfedc8e63249d0b79ab5891c9f530891be772
5
+ SHA512:
6
+ metadata.gz: 9e53301459ba5aa690c88057806b667facd5ef09d6a702cbdfbd31071f725970e3a31c0a7c9edbe0847f6464e0904eaaf8a06fa4b0356a0e6d859054bc9f81f6
7
+ data.tar.gz: 3195cebf0c869b6fdf36639cb91ab19819d37ef85623bae1086f4dc95752e8e574033373a80a27910f6b297e3bf6b0cf4d81310e2ca6e944ce854e1bdf46934f
data/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # A2UI Rails
2
+
3
+ A Ruby port of [Google's A2UI](https://github.com/google/A2UI) (Agent-to-User Interface) for Rails, using Turbo Streams and DSPy.rb for LLM-driven UI generation.
4
+
5
+ > **Status**: Early development. APIs will change.
6
+
7
+ ## What is A2UI?
8
+
9
+ A2UI lets AI agents generate rich, interactive UIs by shipping **data and UI descriptions together** as structured output, rather than executable code. The client maintains a catalog of trusted components that the agent references by type.
10
+
11
+ This port maps A2UI concepts to Rails + Turbo:
12
+
13
+ | A2UI | Rails + Turbo |
14
+ |------|---------------|
15
+ | Surface | `<turbo-frame>` |
16
+ | `surfaceUpdate` | `<turbo-stream>` |
17
+ | `dataModelUpdate` | Stimulus controller values |
18
+ | Component catalog | ViewComponent library |
19
+ | JSON adjacency list | Rendered HTML fragments |
20
+
21
+ ## DSPy Pipeline
22
+
23
+ ```
24
+ ┌─────────────────────────────────────────────────────────────────────────────┐
25
+ │ A2UI DSPy Pipelines │
26
+ ├─────────────────────────────────────────────────────────────────────────────┤
27
+ │ │
28
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
29
+ │ │ CREATE SURFACE │ │
30
+ │ │ │ │
31
+ │ │ "Create a booking form" ───▶ GenerateUI (ChainOfThought) │ │
32
+ │ │ + surface_id │ │ │
33
+ │ │ + available_data ┌──────┴──────┐ │ │
34
+ │ │ ▼ ▼ │ │
35
+ │ │ root_id components[] │ │
36
+ │ │ "form-1" [Column, TextField, │ │
37
+ │ │ TextField, Button] │ │
38
+ │ └─────────────────────────────────────────────────────────────────────┘ │
39
+ │ │
40
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
41
+ │ │ UPDATE SURFACE │ │
42
+ │ │ │ │
43
+ │ │ "Add phone field" ───▶ UpdateUI (ChainOfThought) │ │
44
+ │ │ + current_components │ │ │
45
+ │ │ + current_data ┌──────┴──────┐ │ │
46
+ │ │ ▼ ▼ │ │
47
+ │ │ streams[] new_components[] │ │
48
+ │ │ [{action: [TextFieldComponent] │ │
49
+ │ │ "after", │ │
50
+ │ │ target: │ │
51
+ │ │ "email"}] │ │
52
+ │ └─────────────────────────────────────────────────────────────────────┘ │
53
+ │ │
54
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
55
+ │ │ HANDLE ACTION │ │
56
+ │ │ │ │
57
+ │ │ UserAction{name,context} ───▶ HandleAction (ChainOfThought) │ │
58
+ │ │ + business_rules │ │ │
59
+ │ │ + current_data ┌───────┼───────┐ │ │
60
+ │ │ ▼ ▼ ▼ │ │
61
+ │ │ response streams data_updates │ │
62
+ │ │ _type [] [{path: "/booking", │ │
63
+ │ │ :update_ui entries: [...]}] │ │
64
+ │ └─────────────────────────────────────────────────────────────────────┘ │
65
+ │ │
66
+ └─────────────────────────────────────────────────────────────────────────────┘
67
+ ```
68
+
69
+ ### How It Works
70
+
71
+ **Signals (Inputs):**
72
+ - `request` — Natural language describing what to build/change
73
+ - `available_data` / `current_data` — JSON data model the UI binds to
74
+ - `current_components` — Existing component tree for incremental updates
75
+ - `business_rules` — Domain constraints for action handling
76
+
77
+ **Decisions (LLM Reasoning via ChainOfThought):**
78
+ 1. **Component Selection** — Which component types fit the request?
79
+ 2. **Layout Structure** — How to arrange components (Row vs Column, nesting)?
80
+ 3. **Data Binding** — Which JSON Pointer paths connect to which fields?
81
+ 4. **Action Mapping** — What context to capture when buttons are clicked?
82
+ 5. **Stream Operations** — For updates: append, replace, or remove?
83
+
84
+ **Outputs (Structured):**
85
+ - `components[]` — Flat adjacency list of typed component structs
86
+ - `root_id` — Entry point for rendering the tree
87
+ - `streams[]` — Turbo Stream operations (action + target + content)
88
+ - `data_updates[]` — Mutations to apply to the data model
89
+
90
+ The LLM never generates code—only typed data structures that map to trusted ViewComponents.
91
+
92
+ ## Installation
93
+
94
+ Add to your Gemfile:
95
+
96
+ ```ruby
97
+ gem 'dspy', '~> 0.34'
98
+ gem 'sorbet-runtime'
99
+
100
+ # Choose your LLM provider:
101
+ gem 'dspy-openai' # OpenAI, OpenRouter, Ollama
102
+ # gem 'dspy-anthropic' # Claude
103
+ # gem 'dspy-gemini' # Gemini
104
+ ```
105
+
106
+ Copy the `lib/a2ui/` and `app/` directories to your Rails app.
107
+
108
+ Configure DSPy in an initializer:
109
+
110
+ ```ruby
111
+ # config/initializers/dspy.rb
112
+ DSPy.configure do |c|
113
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
114
+ end
115
+ ```
116
+
117
+ Add the inflection for proper constant loading:
118
+
119
+ ```ruby
120
+ # config/initializers/inflections.rb
121
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
122
+ inflect.acronym 'A2UI'
123
+ end
124
+ ```
125
+
126
+ ## Quick Start
127
+
128
+ ### Generate UI from Natural Language
129
+
130
+ ```ruby
131
+ manager = A2UI::SurfaceManager.new
132
+
133
+ # Create a surface from a natural language request
134
+ surface = manager.create(
135
+ surface_id: 'booking-form',
136
+ request: 'Create a booking form with guest count, date picker, and submit button',
137
+ data: '{"booking": {"guests": 2}}'
138
+ )
139
+
140
+ # Render in your view
141
+ render partial: 'a2ui/surface', locals: { surface: surface }
142
+ ```
143
+
144
+ ### Handle User Actions
145
+
146
+ ```ruby
147
+ action = A2UI::UserAction.new(
148
+ name: 'submit_booking',
149
+ surface_id: 'booking-form',
150
+ source_id: 'submit-btn',
151
+ context: { 'guests' => '3', 'date' => '2025-01-15' }
152
+ )
153
+
154
+ result = manager.handle_action(
155
+ action: action,
156
+ business_rules: 'Maximum 10 guests per booking'
157
+ )
158
+
159
+ # result.response_type => A2UI::ActionResponseType::UpdateUI
160
+ # result.streams => [A2UI::StreamOp, ...]
161
+ # result.components => [A2UI::Component, ...]
162
+ ```
163
+
164
+ ### Update Existing UI
165
+
166
+ ```ruby
167
+ result = manager.update(
168
+ surface_id: 'booking-form',
169
+ request: 'Add a phone number field after the email'
170
+ )
171
+
172
+ # Returns Turbo Stream operations to apply
173
+ ```
174
+
175
+ ## Architecture
176
+
177
+ ```
178
+ ┌─────────────────────────────────────────────────────────────┐
179
+ │ Your Rails App │
180
+ ├─────────────────────────────────────────────────────────────┤
181
+ │ │
182
+ │ DSPy Signatures DSPy Modules Controllers │
183
+ │ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
184
+ │ │ GenerateUI │────────▶│ UIGenerator │─────▶│ Surfaces │ │
185
+ │ │ UpdateUI │ │ UIUpdater │ │ Actions │ │
186
+ │ │ HandleAction│ │ ActionHndlr │ └───────────┘ │
187
+ │ └─────────────┘ └─────────────┘ │ │
188
+ │ │ │ │ │
189
+ │ ▼ ▼ ▼ │
190
+ │ ┌─────────────────────────────────────────────────────────┐│
191
+ │ │ A2UI::Components::Renderer ││
192
+ │ │ Maps Component structs → ViewComponents ││
193
+ │ └─────────────────────────────────────────────────────────┘│
194
+ │ │ │
195
+ │ ▼ │
196
+ │ ┌─────────────────────────────────────────────────────────┐│
197
+ │ │ Turbo Streams / Frames ││
198
+ │ └─────────────────────────────────────────────────────────┘│
199
+ └─────────────────────────────────────────────────────────────┘
200
+ ```
201
+
202
+ ## Core Concepts
203
+
204
+ ### Signatures (DSPy)
205
+
206
+ Type-safe interfaces for LLM calls using Sorbet types:
207
+
208
+ ```ruby
209
+ class A2UI::GenerateUI < DSPy::Signature
210
+ description 'Generate UI components from natural language.'
211
+
212
+ input do
213
+ const :request, String
214
+ const :surface_id, String
215
+ const :available_data, String, default: '{}'
216
+ end
217
+
218
+ output do
219
+ const :root_id, String
220
+ const :components, T::Array[Component] # Union type
221
+ const :initial_data, T::Array[DataUpdate], default: []
222
+ end
223
+ end
224
+ ```
225
+
226
+ ### Union Types
227
+
228
+ Components and values use discriminated unions for type safety:
229
+
230
+ ```ruby
231
+ # Value is either literal or a path reference
232
+ A2UI::Value = T.any(A2UI::LiteralValue, A2UI::PathReference)
233
+
234
+ # Children are either explicit IDs or data-driven
235
+ A2UI::Children = T.any(A2UI::ExplicitChildren, A2UI::DataDrivenChildren)
236
+
237
+ # Component is a union of all component types
238
+ A2UI::Component = T.any(
239
+ A2UI::TextComponent,
240
+ A2UI::ButtonComponent,
241
+ A2UI::TextFieldComponent,
242
+ A2UI::RowComponent,
243
+ A2UI::ColumnComponent,
244
+ # ... 13 total
245
+ )
246
+ ```
247
+
248
+ DSPy automatically handles `_type` discrimination in LLM responses.
249
+
250
+ ### Components
251
+
252
+ Each component type maps to a ViewComponent:
253
+
254
+ | Struct | ViewComponent | Purpose |
255
+ |--------|---------------|---------|
256
+ | `TextComponent` | `A2UI::Components::Text` | Display text with semantic hints |
257
+ | `ButtonComponent` | `A2UI::Components::Button` | Trigger actions |
258
+ | `TextFieldComponent` | `A2UI::Components::TextField` | Text input with data binding |
259
+ | `RowComponent` | `A2UI::Components::Row` | Horizontal flex layout |
260
+ | `ColumnComponent` | `A2UI::Components::Column` | Vertical flex layout |
261
+ | `CardComponent` | `A2UI::Components::Card` | Container with elevation |
262
+ | `CheckBoxComponent` | `A2UI::Components::CheckBox` | Boolean input |
263
+ | `DividerComponent` | `A2UI::Components::Divider` | Visual separator |
264
+
265
+ ### Data Binding
266
+
267
+ Form inputs bind to the data model via JSON Pointer paths:
268
+
269
+ ```ruby
270
+ # Component definition
271
+ A2UI::TextFieldComponent.new(
272
+ id: 'guest-count',
273
+ value: A2UI::PathReference.new(path: '/booking/guests'),
274
+ input_type: A2UI::InputType::Number
275
+ )
276
+
277
+ # Renders with Stimulus binding
278
+ # <input data-controller="a2ui-binding"
279
+ # data-a2ui-binding-path-value="/booking/guests" ...>
280
+ ```
281
+
282
+ ### Actions
283
+
284
+ Buttons dispatch actions with context from the data model:
285
+
286
+ ```ruby
287
+ A2UI::ButtonComponent.new(
288
+ id: 'submit',
289
+ label: A2UI::LiteralValue.new(value: 'Book Now'),
290
+ action: A2UI::Action.new(
291
+ name: 'submit_booking',
292
+ context: [
293
+ A2UI::ContextBinding.new(key: 'booking', path: '/booking')
294
+ ]
295
+ )
296
+ )
297
+ ```
298
+
299
+ ## Stimulus Controllers
300
+
301
+ Three controllers handle client-side behavior:
302
+
303
+ - **`a2ui-data`** - Manages surface data model (JSON Pointer get/set)
304
+ - **`a2ui-binding`** - Two-way binding between inputs and data model
305
+ - **`a2ui-action`** - Dispatches user actions to server via fetch
306
+
307
+ ## Routes
308
+
309
+ ```ruby
310
+ namespace :a2ui do
311
+ resources :surfaces, only: [:create, :show, :update, :destroy]
312
+ resources :actions, only: [:create]
313
+ end
314
+ ```
315
+
316
+ ## Testing
317
+
318
+ ```bash
319
+ bundle exec rspec spec/a2ui/types_spec.rb # Unit tests (no API)
320
+ bundle exec rspec spec/a2ui/ # All tests (needs API key + VCR)
321
+ ```
322
+
323
+ Integration tests use VCR to record LLM responses:
324
+
325
+ ```ruby
326
+ RSpec.describe A2UI::GenerateUI, :vcr do
327
+ it 'generates a booking form' do
328
+ generator = A2UI::UIGenerator.new
329
+ result = generator.call(
330
+ request: 'Create a booking form',
331
+ surface_id: 'booking'
332
+ )
333
+
334
+ expect(result.components).not_to be_empty
335
+ end
336
+ end
337
+ ```
338
+
339
+ ## Development
340
+
341
+ ```bash
342
+ # Install dependencies
343
+ bundle install
344
+
345
+ # Run tests
346
+ bundle exec rspec
347
+
348
+ # Type check (optional)
349
+ bundle exec srb tc
350
+ ```
351
+
352
+ ## Next Steps
353
+
354
+ - [ ] **Add LayoutEvidenceSteps** — Track layout decisions (why Column vs Row? why this nesting?) as structured reasoning evidence for debugging and optimization
355
+ - [ ] **Build a demo app** — Interactive playground showing surface creation, updates, and action handling in real-time
356
+
357
+ ## Roadmap
358
+
359
+ - [ ] More components (Select, Slider, Tabs, Modal)
360
+ - [ ] Data-driven children (repeat templates from array)
361
+ - [ ] Optimizers for prompt tuning (MIPROv2 for signature optimization)
362
+ - [ ] Rails generator for scaffolding
363
+ - [ ] JavaScript package for standalone use
364
+
365
+ ## License
366
+
367
+ MIT
368
+
369
+ ## See Also
370
+
371
+ - [Google A2UI](https://github.com/google/A2UI) - Original specification
372
+ - [DSPy.rb](https://github.com/vicentereig/dspy.rb) - Ruby DSPy framework
373
+ - [Hotwired Turbo](https://github.com/hotwired/turbo) - Turbo Streams/Frames
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class ActionHandler < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::ChainOfThought.new(HandleAction)
12
+ end
13
+
14
+ sig do
15
+ params(input_values: T.untyped).returns(T.untyped)
16
+ end
17
+ def forward(**input_values)
18
+ @predictor.call(
19
+ action: input_values.fetch(:action),
20
+ current_data: input_values.fetch(:current_data, '{}'),
21
+ business_rules: input_values.fetch(:business_rules, '')
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class DataValidator < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::Predict.new(ValidateData)
12
+ end
13
+
14
+ sig { params(input_values: T.untyped).returns(T.untyped) }
15
+ def forward(**input_values)
16
+ @predictor.call(
17
+ data: input_values.fetch(:data),
18
+ rules: input_values.fetch(:rules)
19
+ )
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class InputParser < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::Predict.new(ParseInput)
12
+ end
13
+
14
+ sig do
15
+ params(input_values: T.untyped).returns(T.untyped)
16
+ end
17
+ def forward(**input_values)
18
+ @predictor.call(
19
+ text: input_values.fetch(:text),
20
+ target_path: input_values.fetch(:target_path),
21
+ expected_schema: input_values.fetch(:expected_schema, '')
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class LayoutAdapter < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::Predict.new(AdaptLayout)
12
+ end
13
+
14
+ sig do
15
+ params(input_values: T.untyped).returns(T.untyped)
16
+ end
17
+ def forward(**input_values)
18
+ @predictor.call(
19
+ components: input_values.fetch(:components),
20
+ root_id: input_values.fetch(:root_id),
21
+ screen: input_values.fetch(:screen)
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class Surface
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ attr_reader :id
10
+
11
+ sig { returns(T.nilable(String)) }
12
+ attr_reader :root_id
13
+
14
+ sig { returns(T::Hash[String, Component]) }
15
+ attr_reader :components
16
+
17
+ sig { returns(T::Hash[String, T.untyped]) }
18
+ attr_reader :data
19
+
20
+ sig { params(id: String).void }
21
+ def initialize(id)
22
+ @id = id
23
+ @root_id = T.let(nil, T.nilable(String))
24
+ @components = T.let({}, T::Hash[String, Component])
25
+ @data = T.let({}, T::Hash[String, T.untyped])
26
+ end
27
+
28
+ sig { params(root_id: String, components: T::Array[Component]).void }
29
+ def set_components(root_id, components)
30
+ @root_id = root_id
31
+ components.each { |c| @components[c.id] = c }
32
+ end
33
+
34
+ sig { params(updates: T::Array[DataUpdate]).void }
35
+ def apply_data_updates(updates)
36
+ updates.each do |update|
37
+ @data[update.path] = entries_to_hash(update.entries)
38
+ end
39
+ end
40
+
41
+ sig { params(path: String).returns(T.untyped) }
42
+ def get_data(path)
43
+ @data[path]
44
+ end
45
+
46
+ sig { returns(String) }
47
+ def to_json
48
+ @data.to_json
49
+ end
50
+
51
+ private
52
+
53
+ sig { params(entries: T::Array[DataValue]).returns(T::Hash[String, T.untyped]) }
54
+ def entries_to_hash(entries)
55
+ entries.to_h do |entry|
56
+ value = case entry
57
+ when StringValue then entry.string
58
+ when NumberValue then entry.number
59
+ when BooleanValue then entry.boolean
60
+ when ObjectValue then entry.entries # Already a hash
61
+ end
62
+ [entry.key, value]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class SurfaceManager
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ @surfaces = T.let({}, T::Hash[String, Surface])
11
+ @generator = T.let(UIGenerator.new, UIGenerator)
12
+ @updater = T.let(UIUpdater.new, UIUpdater)
13
+ @action_handler = T.let(ActionHandler.new, ActionHandler)
14
+ end
15
+
16
+ sig { params(surface_id: String, request: String, data: String).returns(Surface) }
17
+ def create(surface_id:, request:, data: '{}')
18
+ result = @generator.call(
19
+ request: request,
20
+ surface_id: surface_id,
21
+ available_data: data
22
+ )
23
+
24
+ surface = Surface.new(surface_id)
25
+ surface.set_components(result.root_id, result.components)
26
+ surface.apply_data_updates(result.initial_data)
27
+
28
+ @surfaces[surface_id] = surface
29
+ end
30
+
31
+ sig { params(surface_id: String, request: String).returns(T.untyped) }
32
+ def update(surface_id:, request:)
33
+ surface = @surfaces.fetch(surface_id)
34
+
35
+ @updater.call(
36
+ request: request,
37
+ surface_id: surface_id,
38
+ current_components: surface.components.values,
39
+ current_data: surface.to_json
40
+ )
41
+ end
42
+
43
+ sig { params(action: UserAction, business_rules: String).returns(T.untyped) }
44
+ def handle_action(action:, business_rules: '')
45
+ surface = @surfaces.fetch(action.surface_id)
46
+
47
+ @action_handler.call(
48
+ action: action,
49
+ current_data: surface.to_json,
50
+ business_rules: business_rules
51
+ )
52
+ end
53
+
54
+ sig { params(surface_id: String).returns(T.nilable(Surface)) }
55
+ def get(surface_id)
56
+ @surfaces[surface_id]
57
+ end
58
+
59
+ sig { params(surface_id: String).void }
60
+ def delete(surface_id)
61
+ @surfaces.delete(surface_id)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class UIGenerator < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::ChainOfThought.new(GenerateUI)
12
+ end
13
+
14
+ sig do
15
+ params(input_values: T.untyped).returns(T.untyped)
16
+ end
17
+ def forward(**input_values)
18
+ @predictor.call(
19
+ request: input_values.fetch(:request),
20
+ surface_id: input_values.fetch(:surface_id),
21
+ available_data: input_values.fetch(:available_data, '{}')
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module A2UI
5
+ class UIUpdater < DSPy::Module
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ super
11
+ @predictor = DSPy::ChainOfThought.new(UpdateUI)
12
+ end
13
+
14
+ sig do
15
+ params(input_values: T.untyped).returns(T.untyped)
16
+ end
17
+ def forward(**input_values)
18
+ @predictor.call(
19
+ request: input_values.fetch(:request),
20
+ surface_id: input_values.fetch(:surface_id),
21
+ current_components: input_values.fetch(:current_components, []),
22
+ current_data: input_values.fetch(:current_data, '{}')
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'dspy'
5
+ require_relative 'signatures'
6
+
7
+ # DSPy modules
8
+ require_relative 'modules/ui_generator'
9
+ require_relative 'modules/ui_updater'
10
+ require_relative 'modules/action_handler'
11
+ require_relative 'modules/data_validator'
12
+ require_relative 'modules/input_parser'
13
+ require_relative 'modules/layout_adapter'
14
+
15
+ # State management
16
+ require_relative 'modules/surface'
17
+ require_relative 'modules/surface_manager'