lex-transformer 0.2.1 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7f4f19cb531c6f7cb50302bb3f2aab973b55976bb05d1b870ac2ad4f7bdb9a0
4
- data.tar.gz: a445f592b23aae48a5510a7c6f17ee3cf0ea0a29dd3c9d67bc405b5b208a0c72
3
+ metadata.gz: 35166d2a367010e3ba45201b37487ba9467bb23e5f721d11ec441e2933a3f5f0
4
+ data.tar.gz: 1f7ec62283b945208b5f59a2f3a55503a9f54974237bd96f6820195d93ab33e4
5
5
  SHA512:
6
- metadata.gz: 21096cee7c407f8df2aa5307e3fe71e35008103a798bc7f9c9cd8dfd1724728af7d65fbeeada8b5df657c412bdcfd44c60fd100efb2a3f63815c6615e63f9917
7
- data.tar.gz: 4e6348dc31ae9217b34040010ecf8b613b8b0e97ae1995854a3f30c821bce8a5b6d190bb283ad9a06e305fe128b68bd8a3533772451d608c55912659c1970ec9
6
+ metadata.gz: 799874d1728143a2cbff5fdb56af95ae960fa9532684c6619ce6a0f85073794b16acae11ac51cb5403a0207e6b75743424de5c05f16be4dc1a8e0d7fd82d7311
7
+ data.tar.gz: 138c24347f12024dd32e46b068d2e1dd54313211d278424a016c0657da510abab31abc80b62f23e63fdd6aaa6ad78d256103bc333073299837a8e13e5e4622e4
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
 
13
13
  Gemfile.lock
14
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-03-22
4
+
5
+ ### Changed
6
+ - Add sub-gem runtime dependencies: legion-cache, legion-crypt, legion-data, legion-json, legion-logging, legion-settings, legion-transport
7
+ - Replace inline Legion::Logging and Legion::JSON stubs in spec_helper with real sub-gem helpers
8
+ - Build Helpers::Lex stub from real sub-gem helper modules in spec_helper
9
+ - Add Actors::Subscription and Helpers::Task stubs to spec_helper for actor load-time resolution
10
+ - Remove redundant inline Helpers::Lex/Task stubs from runner spec files
11
+
12
+ ## [0.3.0] - 2026-03-19
13
+
14
+ ### Added
15
+ - LLM engine error handling with categorized retry (timeout/network retry, auth errors raise, provider errors return failure)
16
+ - LLM engine model/provider/temperature/system_prompt kwargs via `engine_options`
17
+ - LLM engine structured output support (`structured: true` + `schema:`)
18
+ - LLM engine JSON response validation with correction prompt on retry
19
+ - Settings-based LLM defaults (`lex-transformer.llm.*`)
20
+ - Named transform definitions via Settings (`lex-transformer.definitions.*`)
21
+ - `Definitions` class for Settings-based definition lookup
22
+ - `name:` parameter on `Client#transform` for named definition execution
23
+ - `engine_options:` parameter on `Client#transform` and `Client#transform_chain`
24
+ - Conditioner integration for named definitions with conditions
25
+ - LLM failure hash passthrough (no dispatch on engine failure)
26
+ - Integration specs for LLM engine with client, chain, and named definitions
27
+
28
+ ### Changed
29
+ - All engine `render` signatures accept `**opts` (backward-compatible, non-LLM engines ignore)
30
+
3
31
  ## [0.2.1] - 2026-03-17
4
32
 
5
33
  ### Added
data/CLAUDE.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  ## Purpose
8
8
 
9
- Legion Extension that transforms task payloads between services in a relationship chain. Uses ERB template-based transformation (via the `tilt` gem) to map data from one task's output into the format expected by the next task's input. Supports single-hash output (1:1 dispatch) and array output (fan-out/multiply). Requires `legion-data` (`data_required? true`).
9
+ Legion Extension that transforms task payloads between services in a relationship chain. Uses pluggable template engines (ERB, Static, Liquid, JSONPath, LLM) to map data from one task's output into the format expected by the next task's input. Supports named transform definitions (Settings-based), `engine_options:` passthrough, single-hash output (1:1 dispatch), array output (fan-out/multiply), schema validation, and sequential transform chains. Requires `legion-data` (`data_required? true`).
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-transformer
12
12
  **License**: MIT
13
- **Version**: 0.2.1
13
+ **Version**: 0.3.0
14
14
 
15
15
  ## Architecture
16
16
 
@@ -18,18 +18,31 @@ Legion Extension that transforms task payloads between services in a relationshi
18
18
  Legion::Extensions::Transformer
19
19
  ├── Actors/
20
20
  │ └── Transform # Subscription actor consuming transform requests
21
+ ├── Engines/
22
+ │ ├── Base # Abstract engine interface
23
+ │ ├── Registry # Maps engine name symbols to engine classes
24
+ │ ├── Erb # ERB rendering via tilt
25
+ │ ├── Static # JSON passthrough (no templating)
26
+ │ ├── Liquid # Liquid template rendering
27
+ │ ├── Jsonpath # Dot-notation value extraction from payload
28
+ │ └── Llm # Natural language transformation via Legion::LLM
29
+ ├── Helpers/
30
+ │ └── SchemaValidator # Validates transform output against required_keys/types schema
21
31
  ├── Runners/
22
32
  │ └── Transform # Executes template-based payload transformation
23
- │ ├── transform # Entry point: render + dispatch
24
- │ ├── render_transformation # ERB rendering via tilt, or plain JSON if no ERB tags
25
- │ ├── build_template_variables # Inject crypt/settings/cache/task into ERB scope
33
+ │ ├── transform # Entry point: render + dispatch or validate; accepts engine_options:
34
+ │ ├── transform_chain # Sequential pipeline: N steps, output feeds next; per-step engine_options
35
+ │ ├── render_transformation # Engine dispatch (ERB/Static/Liquid/JSONPath/LLM) with engine_options
36
+ │ ├── build_template_variables # Inject crypt/settings/cache/task into scope
26
37
  │ ├── dispatch_transformed # Route Hash (single) or Array (fan-out) results
27
38
  │ ├── dispatch_multiplied # Fan-out: create a new task per array element
28
39
  │ └── send_task # Publish transformed payload to next runner
29
- └── Transport/
30
- ├── Exchanges/Task # Publishes to the task exchange
31
- ├── Queues/Transform # Subscribes to transformation queue
32
- └── Messages/Message # Transform request message format
40
+ ├── Definitions # Named transform definitions loaded from Settings (lex-transformer.definitions.*)
41
+ ├── Transport/
42
+ ├── Exchanges/Task # Publishes to the task exchange
43
+ │ ├── Queues/Transform # Subscribes to transformation queue
44
+ │ └── Messages/Message # Transform request message format
45
+ └── Client # Standalone client: transform + transform_chain + transform_by_name
33
46
  ```
34
47
 
35
48
  ## Key Files
@@ -39,8 +52,26 @@ Legion::Extensions::Transformer
39
52
  | `lib/legion/extensions/transformer.rb` | Entry point (`data_required? true`) |
40
53
  | `lib/legion/extensions/transformer/runners/transform.rb` | Core transformation logic |
41
54
  | `lib/legion/extensions/transformer/actors/transform.rb` | AMQP subscription actor |
55
+ | `lib/legion/extensions/transformer/client.rb` | Standalone client (transform, transform_chain, transform_by_name) |
56
+ | `lib/legion/extensions/transformer/definitions.rb` | Named definition loader from Settings (fetch, names, merge_options) |
57
+ | `lib/legion/extensions/transformer/engines/registry.rb` | Engine name -> class lookup + auto-detection |
58
+ | `lib/legion/extensions/transformer/helpers/schema_validator.rb` | Output schema validation |
42
59
  | `lib/legion/extensions/transformer/transport.rb` | Transport setup |
43
60
 
61
+ ## Template Engines
62
+
63
+ | Engine | Name | Detection | Description |
64
+ |--------|------|-----------|-------------|
65
+ | ERB | `:erb` | `<%` or `%>` in template | Full ERB template rendering via `tilt` |
66
+ | Static | `:static` | Default (no ERB markers) | Plain JSON passthrough |
67
+ | Liquid | `:liquid` | Explicit only | Liquid template rendering (`{{ var }}`) |
68
+ | JSONPath | `:jsonpath` | Explicit only | Dot-notation value extraction from payload |
69
+ | LLM | `:llm` | Explicit only | Natural language transformation via `Legion::LLM` |
70
+
71
+ Auto-detection: ERB when template contains `<%` or `%>`, otherwise Static. Pass `engine:` to force a specific engine.
72
+
73
+ The LLM engine requires `legion-llm` to be started; it is provider-agnostic (Ollama, Bedrock, Anthropic, OpenAI, Gemini).
74
+
44
75
  ## Template Variables Available in ERB
45
76
 
46
77
  | Variable | Available when |
@@ -51,28 +82,72 @@ Legion::Extensions::Transformer
51
82
  | `cache` | Template string contains `'cache'` |
52
83
  | `task` | Template string contains `'task'` and payload has `task_id` |
53
84
 
85
+ ## Named Transform Definitions
86
+
87
+ `Definitions` loads named transform configurations from `Legion::Settings[:lex_transformer][:definitions]`. Each definition is a hash with `:transformation`, optional `:engine`, `:schema`, `:engine_options`, and `:conditions` keys.
88
+
89
+ **`Definitions.fetch(name)`** — returns the named definition hash (symbolized) or `nil`.
90
+ **`Definitions.names`** — returns array of defined names.
91
+ **`Definitions.merge_options(definition, **overrides)`** — merges caller engine_options over definition defaults.
92
+
93
+ Client usage:
94
+
95
+ ```ruby
96
+ # Define in settings: lex-transformer.definitions.my_template: { transformation: "...", engine: "erb" }
97
+ result = client.transform(name: 'my_template', payload: { foo: 'bar' })
98
+ ```
99
+
100
+ If a definition includes `conditions:` (a lex-conditioner condition hash), the conditioner client is called first — the transform is skipped if conditions fail.
101
+
102
+ ## Schema Validation
103
+
104
+ ```ruby
105
+ schema = {
106
+ required_keys: [:name, :email],
107
+ types: { name: String, email: String, age: Integer }
108
+ }
109
+ ```
110
+
111
+ On failure: `{ success: false, status: 'transformer.validation_failed', errors: [...] }` — no dispatch.
112
+
54
113
  ## Dispatch Behavior
55
114
 
56
115
  - **Hash result**: Updates task to `transformer.succeeded`, dispatches single task, updates to `task.queued`
57
- - **Array result**: Fan-out via `dispatch_multiplied` - creates a new task record per element, dispatches each, marks original as `task.multiplied`
58
- - **Plain JSON** (no ERB tags): Parsed directly without template rendering
116
+ - **Array result**: Fan-out via `dispatch_multiplied` creates a new task record per element, dispatches each, marks original as `task.multiplied`
117
+ - **Schema failure**: `transformer.validation_failed`, no dispatch
118
+
119
+ ## Transform Chains
120
+
121
+ `transform_chain(steps:, **payload)` runs steps sequentially. Each step specifies `:transformation`, optional `:engine`, and optional `:schema`. Output of step N is merged into the running payload for step N+1. Stops on first schema failure.
122
+
123
+ ## Standalone Client
124
+
125
+ `Legion::Extensions::Transformer::Client` includes the Transform runner:
126
+
127
+ ```ruby
128
+ require 'legion/extensions/transformer/client'
129
+ client = Legion::Extensions::Transformer::Client.new
130
+ result = client.transform(transformation: '{"x":"<%= y %>"}', payload: { y: 'hello' })
131
+ result[:success] # => true
132
+ result[:result] # => { x: "hello" }
133
+ ```
59
134
 
60
135
  ## Dependencies
61
136
 
62
137
  | Gem | Purpose |
63
138
  |-----|---------|
64
139
  | `tilt` (>= 2.3) | Template engine abstraction for ERB rendering |
65
- | `legion-data` | Required - task record creation for fan-out |
140
+ | `legion-data` | Required task record creation for fan-out |
66
141
 
67
142
  ## Testing
68
143
 
69
144
  ```bash
70
145
  bundle install
71
- bundle exec rspec
72
- bundle exec rubocop
146
+ bundle exec rspec # 121 examples, 0 failures
147
+ bundle exec rubocop # 0 offenses
73
148
  ```
74
149
 
75
- Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename), `spec/legion/extensions/transform_runner_spec.rb`
150
+ Spec files include: `tranformer_spec.rb` (note: typo in filename), `transform_runner_spec.rb`, `definitions_spec.rb`, `client_spec.rb`, per-engine specs, `transform_chain_spec.rb`, `transform_schema_spec.rb`, and `llm_integration_spec.rb`.
76
151
 
77
152
  ---
78
153
 
data/README.md CHANGED
@@ -38,6 +38,14 @@ result = client.transform(
38
38
  engine: :liquid
39
39
  )
40
40
 
41
+ # With engine options (passed through to the engine)
42
+ result = client.transform(
43
+ transformation: 'Summarize this in one sentence',
44
+ payload: { text: 'Long article...' },
45
+ engine: :llm,
46
+ engine_options: { model: 'claude-opus-4-6', temperature: 0.3 }
47
+ )
48
+
41
49
  # With schema validation
42
50
  result = client.transform(
43
51
  transformation: '{"name":"test"}',
@@ -47,6 +55,9 @@ result = client.transform(
47
55
  result[:success] # => false
48
56
  result[:status] # => "transformer.validation_failed"
49
57
  result[:errors] # => ["missing required key: email"]
58
+
59
+ # By named definition (loaded from Legion::Settings)
60
+ result = client.transform(name: 'my_template', payload: { foo: 'bar' })
50
61
  ```
51
62
 
52
63
  ### Transform Chains
@@ -106,11 +117,34 @@ schema = {
106
117
 
107
118
  When validation fails, the transform returns `{ success: false, status: 'transformer.validation_failed', errors: [...] }`.
108
119
 
120
+ ## Named Definitions
121
+
122
+ Transform definitions can be registered in settings under `lex-transformer.definitions.<name>` and referenced by name:
123
+
124
+ ```json
125
+ {
126
+ "lex-transformer": {
127
+ "definitions": {
128
+ "slack_notify": {
129
+ "transformation": "{\"text\":\"<%= title %> by <%= author %>\"}",
130
+ "engine": "erb"
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ```ruby
138
+ result = client.transform(name: 'slack_notify', payload: { title: 'PR merged', author: 'alice' })
139
+ ```
140
+
141
+ If a definition includes `conditions:`, the conditioner client is evaluated first and the transform is skipped on failure.
142
+
109
143
  ## Runners
110
144
 
111
145
  ### Transform
112
146
 
113
- #### `transform(transformation:, engine: nil, schema: nil, **payload)`
147
+ #### `transform(transformation:, engine: nil, schema: nil, engine_options: {}, name: nil, **payload)`
114
148
 
115
149
  Renders the transformation template against the payload, optionally validates the result, then dispatches:
116
150
 
@@ -26,5 +26,12 @@ Gem::Specification.new do |spec|
26
26
  end
27
27
  spec.require_paths = ['lib']
28
28
 
29
- spec.add_dependency 'tilt', '>= 2.3'
29
+ spec.add_dependency 'legion-cache', '>= 1.3.11'
30
+ spec.add_dependency 'legion-crypt', '>= 1.4.9'
31
+ spec.add_dependency 'legion-data', '>= 1.4.17'
32
+ spec.add_dependency 'legion-json', '>= 1.2.1'
33
+ spec.add_dependency 'legion-logging', '>= 1.3.2'
34
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
35
+ spec.add_dependency 'legion-transport', '>= 1.3.9'
36
+ spec.add_dependency 'tilt', '>= 2.3'
30
37
  end
@@ -2,16 +2,21 @@
2
2
 
3
3
  require_relative 'engines/registry'
4
4
  require_relative 'helpers/schema_validator'
5
+ require_relative 'definitions'
5
6
 
6
7
  module Legion
7
8
  module Extensions
8
9
  module Transformer
9
10
  class Client
10
- def transform(transformation:, payload:, engine: nil, schema: nil)
11
+ def transform(payload:, transformation: nil, engine: nil, schema: nil, engine_options: {}, name: nil)
12
+ return transform_by_name(name: name, payload: payload, engine_options: engine_options) if transformation.nil? && name
13
+
11
14
  eng = resolve_engine(engine, transformation)
12
- rendered = eng.render(transformation, payload)
15
+ rendered = eng.render(transformation, payload, **engine_options)
13
16
  rendered = parse_rendered(rendered)
14
17
 
18
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
19
+
15
20
  if schema
16
21
  validation = Helpers::SchemaValidator.validate(schema: schema, data: rendered)
17
22
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -24,9 +29,12 @@ module Legion
24
29
  result = payload.dup
25
30
  steps.each do |step|
26
31
  eng = resolve_engine(step[:engine], step[:transformation])
27
- rendered = eng.render(step[:transformation], result)
32
+ step_opts = step[:engine_options] || {}
33
+ rendered = eng.render(step[:transformation], result, **step_opts)
28
34
  rendered = parse_rendered(rendered)
29
35
 
36
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
37
+
30
38
  if step[:schema]
31
39
  validation = Helpers::SchemaValidator.validate(schema: step[:schema], data: rendered)
32
40
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -43,6 +51,38 @@ module Legion
43
51
 
44
52
  private
45
53
 
54
+ def transform_by_name(name:, payload:, engine_options: {})
55
+ definition = Definitions.fetch(name)
56
+ return { success: false, error: 'definition_not_found' } unless definition
57
+
58
+ if definition[:conditions] && conditioner_available?
59
+ cond_result = evaluate_conditions(definition[:conditions], payload)
60
+ return { success: false, reason: 'conditions_not_met' } unless cond_result
61
+ end
62
+
63
+ merged_opts = Definitions.merge_options(definition, **engine_options)
64
+
65
+ transform(
66
+ transformation: definition[:transformation],
67
+ payload: payload,
68
+ engine: definition[:engine],
69
+ schema: definition[:schema],
70
+ engine_options: merged_opts
71
+ )
72
+ end
73
+
74
+ def conditioner_available?
75
+ defined?(Legion::Extensions::Conditioner::Client)
76
+ end
77
+
78
+ def evaluate_conditions(conditions, payload)
79
+ client = Legion::Extensions::Conditioner::Client.new
80
+ result = client.evaluate(conditions: conditions, values: payload)
81
+ result[:passed]
82
+ rescue StandardError
83
+ true
84
+ end
85
+
46
86
  def resolve_engine(engine_name, transformation)
47
87
  if engine_name
48
88
  Engines::Registry.fetch(engine_name)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Transformer
6
+ class Definitions
7
+ class << self
8
+ def fetch(name)
9
+ defns = load_definitions
10
+ return nil unless defns&.key?(name)
11
+
12
+ symbolize_definition(defns[name])
13
+ end
14
+
15
+ def names
16
+ defns = load_definitions
17
+ return [] unless defns
18
+
19
+ defns.keys
20
+ end
21
+
22
+ def merge_options(definition, **overrides)
23
+ base = definition[:engine_options] || {}
24
+ base.merge(overrides)
25
+ end
26
+
27
+ private
28
+
29
+ def load_definitions
30
+ return nil unless defined?(Legion::Settings)
31
+
32
+ Legion::Settings.dig('lex-transformer', 'definitions')
33
+ rescue StandardError
34
+ nil
35
+ end
36
+
37
+ def symbolize_definition(raw)
38
+ defn = {}
39
+ defn[:transformation] = raw['transformation'] || raw[:transformation]
40
+ defn[:engine] = (raw['engine'] || raw[:engine])&.to_sym
41
+ defn[:engine_options] = symbolize_hash(raw['engine_options'] || raw[:engine_options] || {})
42
+ defn[:schema] = raw['schema'] || raw[:schema]
43
+ defn[:conditions] = raw['conditions'] || raw[:conditions]
44
+ defn
45
+ end
46
+
47
+ def symbolize_hash(hash)
48
+ return {} unless hash.is_a?(Hash)
49
+
50
+ hash.transform_keys(&:to_sym)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -5,7 +5,7 @@ module Legion
5
5
  module Transformer
6
6
  module Engines
7
7
  class Base
8
- def render(template, payload)
8
+ def render(template, payload, **_opts)
9
9
  raise NotImplementedError, "#{self.class}#render must be implemented"
10
10
  end
11
11
 
@@ -12,7 +12,7 @@ module Legion
12
12
  :erb
13
13
  end
14
14
 
15
- def render(template, payload)
15
+ def render(template, payload, **_opts)
16
16
  tilt_template = Tilt['erb'].new { template }
17
17
  variables = build_variables(template, payload)
18
18
  tilt_template.render(Object.new, variables)
@@ -11,7 +11,7 @@ module Legion
11
11
  :jsonpath
12
12
  end
13
13
 
14
- def render(expression, payload)
14
+ def render(expression, payload, **_opts)
15
15
  extract(expression, payload)
16
16
  end
17
17
 
@@ -11,7 +11,7 @@ module Legion
11
11
  :liquid
12
12
  end
13
13
 
14
- def render(template, payload)
14
+ def render(template, payload, **_opts)
15
15
  require 'liquid'
16
16
  liquid_template = ::Liquid::Template.parse(template)
17
17
  liquid_template.render(stringify_keys(payload))
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
3
4
  require_relative 'base'
4
5
 
5
6
  module Legion
@@ -7,28 +8,144 @@ module Legion
7
8
  module Transformer
8
9
  module Engines
9
10
  class Llm < Base
11
+ AUTH_ERRORS = /auth|credentials|forbidden|api.key|unauthorized/i
12
+ RETRY_SLEEP = 1
13
+
10
14
  def name
11
15
  :llm
12
16
  end
13
17
 
14
- def render(prompt, payload)
15
- raise 'Legion::LLM is not available or not started' unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
18
+ def render(prompt, payload, **opts)
19
+ raise 'Legion::LLM is not available or not started' unless llm_available?
16
20
 
17
- context = Legion::JSON.dump(payload)
18
- full_prompt = "#{prompt}\n\nPayload:\n```json\n#{context}\n```\n\nRespond with valid JSON only. No explanation, no markdown fences."
21
+ @last_raw = nil
22
+ resolved = resolve_options(opts)
23
+ max_retries = resolved.delete(:max_retries)
24
+ attempts = 0
25
+
26
+ loop do
27
+ result = attempt_render(prompt, payload, resolved, attempts)
28
+ return result unless result == :retry
19
29
 
20
- chat = Legion::LLM.chat(message: full_prompt)
21
- extract_response(chat)
30
+ attempts += 1
31
+ return build_failure if attempts > max_retries
32
+
33
+ sleep(RETRY_SLEEP)
34
+ end
22
35
  end
23
36
 
24
37
  private
25
38
 
39
+ def llm_available?
40
+ defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
41
+ end
42
+
43
+ def resolve_options(opts)
44
+ defaults = settings_defaults
45
+ resolved = defaults.merge(opts)
46
+ resolved[:max_retries] = (resolved[:max_retries] || 1).to_i
47
+ resolved
48
+ end
49
+
50
+ def settings_defaults
51
+ return {} unless defined?(Legion::Settings)
52
+
53
+ settings = begin
54
+ Legion::Settings.dig('lex-transformer', 'llm')
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ return {} unless settings.is_a?(Hash)
59
+
60
+ settings.transform_keys(&:to_sym)
61
+ end
62
+
63
+ def attempt_render(prompt, payload, opts, attempt)
64
+ chat = call_llm(prompt, payload, opts, attempt)
65
+ content = extract_response(chat)
66
+ validate_json(content)
67
+ content
68
+ rescue Timeout::Error, IOError, Errno::ECONNREFUSED, Errno::ECONNRESET
69
+ :retry
70
+ rescue ::JSON::ParserError
71
+ @last_raw = content
72
+ :retry
73
+ rescue RuntimeError => e
74
+ raise if auth_error?(e)
75
+
76
+ { success: false, error: e.class.to_s, message: e.message }
77
+ rescue StandardError => e
78
+ { success: false, error: e.class.to_s, message: e.message }
79
+ end
80
+
81
+ def call_llm(prompt, payload, opts, attempt)
82
+ context = Legion::JSON.dump(payload)
83
+
84
+ return call_structured(prompt, context, opts) if opts[:structured] && opts[:schema] && structured_available?
85
+
86
+ full_prompt = build_prompt(prompt, context, attempt)
87
+ full_prompt = inject_schema_into_prompt(full_prompt, opts[:schema]) if opts[:structured] && opts[:schema]
88
+
89
+ llm_opts = build_llm_opts(opts)
90
+ Legion::LLM.chat(message: full_prompt, **llm_opts)
91
+ end
92
+
93
+ def structured_available?
94
+ Legion::LLM.respond_to?(:structured)
95
+ end
96
+
97
+ def call_structured(prompt, context, opts)
98
+ llm_opts = build_llm_opts(opts)
99
+ Legion::LLM.structured(
100
+ message: "#{prompt}\n\nPayload:\n```json\n#{context}\n```",
101
+ schema: opts[:schema],
102
+ **llm_opts
103
+ )
104
+ end
105
+
106
+ def inject_schema_into_prompt(prompt, schema)
107
+ schema_json = Legion::JSON.dump(schema)
108
+ "#{prompt}\n\nYour response MUST conform to this JSON schema:\n```json\n#{schema_json}\n```"
109
+ end
110
+
111
+ def build_prompt(prompt, context, attempt)
112
+ base = "#{prompt}\n\nPayload:\n```json\n#{context}\n```\n\nRespond with valid JSON only. No explanation, no markdown fences."
113
+ return base unless attempt.positive?
114
+
115
+ "#{base}\n\nIMPORTANT: Your previous response was not valid JSON. Return ONLY a valid JSON object or array, nothing else."
116
+ end
117
+
26
118
  def extract_response(chat)
27
119
  content = chat.respond_to?(:content) ? chat.content : chat.to_s
28
120
  content = content.strip
29
121
  content = content.sub(/\A```(?:json)?\n?/, '').sub(/\n?```\z/, '') if content.start_with?('```')
30
122
  content
31
123
  end
124
+
125
+ def validate_json(content)
126
+ ::JSON.parse(content)
127
+ end
128
+
129
+ def auth_error?(error)
130
+ AUTH_ERRORS.match?(error.message)
131
+ end
132
+
133
+ def build_llm_opts(opts)
134
+ llm_opts = {}
135
+ llm_opts[:model] = opts[:model] if opts[:model]
136
+ llm_opts[:provider] = opts[:provider] if opts[:provider]
137
+ llm_opts[:temperature] = opts[:temperature] if opts[:temperature]
138
+ llm_opts[:system_prompt] = opts[:system_prompt] if opts[:system_prompt]
139
+ llm_opts
140
+ end
141
+
142
+ def build_failure
143
+ if @last_raw
144
+ { success: false, error: 'invalid_json', message: 'max retries exhausted', raw: @last_raw }
145
+ else
146
+ { success: false, error: 'timeout', message: 'max retries exhausted' }
147
+ end
148
+ end
32
149
  end
33
150
  end
34
151
  end
@@ -11,7 +11,7 @@ module Legion
11
11
  :static
12
12
  end
13
13
 
14
- def render(template, _payload)
14
+ def render(template, _payload, **_opts)
15
15
  template
16
16
  end
17
17
  end
@@ -8,8 +8,11 @@ module Legion
8
8
  module Transformer
9
9
  module Runners
10
10
  module Transform
11
- def transform(transformation:, engine: nil, schema: nil, **payload)
12
- payload[:args] = render_transformation(transformation, payload, engine: engine)
11
+ def transform(transformation:, engine: nil, schema: nil, engine_options: {}, **payload)
12
+ payload[:args] = render_transformation(transformation, payload, engine: engine, engine_options: engine_options)
13
+
14
+ return payload[:args] if payload[:args].is_a?(Hash) && payload[:args][:success] == false
15
+
13
16
  if schema
14
17
  validation = Helpers::SchemaValidator.validate(schema: schema, data: payload[:args])
15
18
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -22,8 +25,12 @@ module Legion
22
25
  def transform_chain(steps:, **payload)
23
26
  result = payload
24
27
  steps.each do |step|
25
- rendered = render_transformation(step[:transformation], result, engine: step[:engine])
28
+ step_opts = step[:engine_options] || {}
29
+ rendered = render_transformation(step[:transformation], result, engine: step[:engine], engine_options: step_opts)
26
30
  rendered = from_json(rendered) if rendered.is_a?(String)
31
+
32
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
33
+
27
34
  if step[:schema]
28
35
  validation = Helpers::SchemaValidator.validate(schema: step[:schema], data: rendered)
29
36
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -33,14 +40,16 @@ module Legion
33
40
  { success: true, **result }
34
41
  end
35
42
 
36
- def render_transformation(transformation, payload, engine: nil)
43
+ def render_transformation(transformation, payload, engine: nil, engine_options: {})
37
44
  eng = if engine
38
45
  Engines::Registry.fetch(engine)
39
46
  else
40
47
  Engines::Registry.detect(transformation)
41
48
  end
42
49
 
43
- rendered = eng.render(transformation, payload)
50
+ rendered = eng.render(transformation, payload, **engine_options)
51
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
52
+
44
53
  rendered.is_a?(String) ? from_json(rendered) : rendered
45
54
  end
46
55
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Transformer
6
- VERSION = '0.2.1'
6
+ VERSION = '0.3.1'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-transformer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,104 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-cache
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.3.11
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.3.11
26
+ - !ruby/object:Gem::Dependency
27
+ name: legion-crypt
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.9
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.4.9
40
+ - !ruby/object:Gem::Dependency
41
+ name: legion-data
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.4.17
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.4.17
54
+ - !ruby/object:Gem::Dependency
55
+ name: legion-json
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.2.1
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 1.2.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: legion-logging
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.3.2
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.2
82
+ - !ruby/object:Gem::Dependency
83
+ name: legion-settings
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.14
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 1.3.14
96
+ - !ruby/object:Gem::Dependency
97
+ name: legion-transport
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.3.9
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.3.9
12
110
  - !ruby/object:Gem::Dependency
13
111
  name: tilt
14
112
  requirement: !ruby/object:Gem::Requirement
@@ -47,6 +145,7 @@ files:
47
145
  - lib/legion/extensions/transformer.rb
48
146
  - lib/legion/extensions/transformer/actors/transform.rb
49
147
  - lib/legion/extensions/transformer/client.rb
148
+ - lib/legion/extensions/transformer/definitions.rb
50
149
  - lib/legion/extensions/transformer/engines/base.rb
51
150
  - lib/legion/extensions/transformer/engines/erb.rb
52
151
  - lib/legion/extensions/transformer/engines/jsonpath.rb