lex-transformer 0.2.1 → 0.3.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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +70 -14
- data/lib/legion/extensions/transformer/client.rb +43 -3
- data/lib/legion/extensions/transformer/definitions.rb +56 -0
- data/lib/legion/extensions/transformer/engines/base.rb +1 -1
- data/lib/legion/extensions/transformer/engines/erb.rb +1 -1
- data/lib/legion/extensions/transformer/engines/jsonpath.rb +1 -1
- data/lib/legion/extensions/transformer/engines/liquid.rb +1 -1
- data/lib/legion/extensions/transformer/engines/llm.rb +123 -6
- data/lib/legion/extensions/transformer/engines/static.rb +1 -1
- data/lib/legion/extensions/transformer/runners/transform.rb +14 -5
- data/lib/legion/extensions/transformer/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 111582b1dfcbe44b2cf91d3b316bd6394eb09e4d750a7b19be8844e91257e2c5
|
|
4
|
+
data.tar.gz: 6efebcb5ea769c9426e17e6be93aa1cc0ca1bf5093e02d9f5ea0fe5254809760
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8af3daa3467d9e37e571319c876a17ff772e3558c0510fcca75a633e970ed32b0497c1e31f63faa047e0eb2f6e8488d8a55820d0d5e46394450957228f4ec3af
|
|
7
|
+
data.tar.gz: a8dc1caf0ce16261c8eecf6d6ed37b2af4ff1cf4736465047a67cfd96eeb431a1454d4a31f2c34861219fea481b8d79b42d13b860a3935d240f81b89669c870d
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- LLM engine error handling with categorized retry (timeout/network retry, auth errors raise, provider errors return failure)
|
|
7
|
+
- LLM engine model/provider/temperature/system_prompt kwargs via `engine_options`
|
|
8
|
+
- LLM engine structured output support (`structured: true` + `schema:`)
|
|
9
|
+
- LLM engine JSON response validation with correction prompt on retry
|
|
10
|
+
- Settings-based LLM defaults (`lex-transformer.llm.*`)
|
|
11
|
+
- Named transform definitions via Settings (`lex-transformer.definitions.*`)
|
|
12
|
+
- `Definitions` class for Settings-based definition lookup
|
|
13
|
+
- `name:` parameter on `Client#transform` for named definition execution
|
|
14
|
+
- `engine_options:` parameter on `Client#transform` and `Client#transform_chain`
|
|
15
|
+
- Conditioner integration for named definitions with conditions
|
|
16
|
+
- LLM failure hash passthrough (no dispatch on engine failure)
|
|
17
|
+
- Integration specs for LLM engine with client, chain, and named definitions
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- All engine `render` signatures accept `**opts` (backward-compatible, non-LLM engines ignore)
|
|
21
|
+
|
|
3
22
|
## [0.2.1] - 2026-03-17
|
|
4
23
|
|
|
5
24
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Purpose
|
|
8
8
|
|
|
9
|
-
Legion Extension that transforms task payloads between services in a relationship chain. Uses
|
|
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 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
|
|
@@ -18,18 +18,30 @@ 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
|
-
│ ├──
|
|
25
|
-
│ ├──
|
|
33
|
+
│ ├── transform # Entry point: render + dispatch or validate
|
|
34
|
+
│ ├── transform_chain # Sequential pipeline: N steps, output feeds next
|
|
35
|
+
│ ├── render_transformation # Engine dispatch (ERB/Static/Liquid/JSONPath/LLM)
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
├── Transport/
|
|
41
|
+
│ ├── Exchanges/Task # Publishes to the task exchange
|
|
42
|
+
│ ├── Queues/Transform # Subscribes to transformation queue
|
|
43
|
+
│ └── Messages/Message # Transform request message format
|
|
44
|
+
└── Client # Standalone client: transform + transform_chain (uses Engines::Registry directly)
|
|
33
45
|
```
|
|
34
46
|
|
|
35
47
|
## Key Files
|
|
@@ -39,8 +51,25 @@ Legion::Extensions::Transformer
|
|
|
39
51
|
| `lib/legion/extensions/transformer.rb` | Entry point (`data_required? true`) |
|
|
40
52
|
| `lib/legion/extensions/transformer/runners/transform.rb` | Core transformation logic |
|
|
41
53
|
| `lib/legion/extensions/transformer/actors/transform.rb` | AMQP subscription actor |
|
|
54
|
+
| `lib/legion/extensions/transformer/client.rb` | Standalone client (transform, transform_chain) |
|
|
55
|
+
| `lib/legion/extensions/transformer/engines/registry.rb` | Engine name -> class lookup + auto-detection |
|
|
56
|
+
| `lib/legion/extensions/transformer/helpers/schema_validator.rb` | Output schema validation |
|
|
42
57
|
| `lib/legion/extensions/transformer/transport.rb` | Transport setup |
|
|
43
58
|
|
|
59
|
+
## Template Engines
|
|
60
|
+
|
|
61
|
+
| Engine | Name | Detection | Description |
|
|
62
|
+
|--------|------|-----------|-------------|
|
|
63
|
+
| ERB | `:erb` | `<%` or `%>` in template | Full ERB template rendering via `tilt` |
|
|
64
|
+
| Static | `:static` | Default (no ERB markers) | Plain JSON passthrough |
|
|
65
|
+
| Liquid | `:liquid` | Explicit only | Liquid template rendering (`{{ var }}`) |
|
|
66
|
+
| JSONPath | `:jsonpath` | Explicit only | Dot-notation value extraction from payload |
|
|
67
|
+
| LLM | `:llm` | Explicit only | Natural language transformation via `Legion::LLM` |
|
|
68
|
+
|
|
69
|
+
Auto-detection: ERB when template contains `<%` or `%>`, otherwise Static. Pass `engine:` to force a specific engine.
|
|
70
|
+
|
|
71
|
+
The LLM engine requires `legion-llm` to be started; it is provider-agnostic (Ollama, Bedrock, Anthropic, OpenAI, Gemini).
|
|
72
|
+
|
|
44
73
|
## Template Variables Available in ERB
|
|
45
74
|
|
|
46
75
|
| Variable | Available when |
|
|
@@ -51,28 +80,55 @@ Legion::Extensions::Transformer
|
|
|
51
80
|
| `cache` | Template string contains `'cache'` |
|
|
52
81
|
| `task` | Template string contains `'task'` and payload has `task_id` |
|
|
53
82
|
|
|
83
|
+
## Schema Validation
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
schema = {
|
|
87
|
+
required_keys: [:name, :email],
|
|
88
|
+
types: { name: String, email: String, age: Integer }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
On failure: `{ success: false, status: 'transformer.validation_failed', errors: [...] }` — no dispatch.
|
|
93
|
+
|
|
54
94
|
## Dispatch Behavior
|
|
55
95
|
|
|
56
96
|
- **Hash result**: Updates task to `transformer.succeeded`, dispatches single task, updates to `task.queued`
|
|
57
|
-
- **Array result**: Fan-out via `dispatch_multiplied`
|
|
58
|
-
- **
|
|
97
|
+
- **Array result**: Fan-out via `dispatch_multiplied` — creates a new task record per element, dispatches each, marks original as `task.multiplied`
|
|
98
|
+
- **Schema failure**: `transformer.validation_failed`, no dispatch
|
|
99
|
+
|
|
100
|
+
## Transform Chains
|
|
101
|
+
|
|
102
|
+
`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.
|
|
103
|
+
|
|
104
|
+
## Standalone Client
|
|
105
|
+
|
|
106
|
+
`Legion::Extensions::Transformer::Client` includes the Transform runner:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require 'legion/extensions/transformer/client'
|
|
110
|
+
client = Legion::Extensions::Transformer::Client.new
|
|
111
|
+
result = client.transform(transformation: '{"x":"<%= y %>"}', payload: { y: 'hello' })
|
|
112
|
+
result[:success] # => true
|
|
113
|
+
result[:result] # => { x: "hello" }
|
|
114
|
+
```
|
|
59
115
|
|
|
60
116
|
## Dependencies
|
|
61
117
|
|
|
62
118
|
| Gem | Purpose |
|
|
63
119
|
|-----|---------|
|
|
64
120
|
| `tilt` (>= 2.3) | Template engine abstraction for ERB rendering |
|
|
65
|
-
| `legion-data` | Required
|
|
121
|
+
| `legion-data` | Required — task record creation for fan-out |
|
|
66
122
|
|
|
67
123
|
## Testing
|
|
68
124
|
|
|
69
125
|
```bash
|
|
70
126
|
bundle install
|
|
71
|
-
bundle exec rspec
|
|
72
|
-
bundle exec rubocop
|
|
127
|
+
bundle exec rspec # 86 examples, 0 failures
|
|
128
|
+
bundle exec rubocop # 0 offenses
|
|
73
129
|
```
|
|
74
130
|
|
|
75
|
-
Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename), `spec/legion/extensions/transform_runner_spec.rb`
|
|
131
|
+
Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename is intentional in repo), `spec/legion/extensions/transform_runner_spec.rb`
|
|
76
132
|
|
|
77
133
|
---
|
|
78
134
|
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
18
|
+
def render(prompt, payload, **opts)
|
|
19
|
+
raise 'Legion::LLM is not available or not started' unless llm_available?
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/legion/extensions/transformer.rb
|
|
48
48
|
- lib/legion/extensions/transformer/actors/transform.rb
|
|
49
49
|
- lib/legion/extensions/transformer/client.rb
|
|
50
|
+
- lib/legion/extensions/transformer/definitions.rb
|
|
50
51
|
- lib/legion/extensions/transformer/engines/base.rb
|
|
51
52
|
- lib/legion/extensions/transformer/engines/erb.rb
|
|
52
53
|
- lib/legion/extensions/transformer/engines/jsonpath.rb
|