tactus 0.31.0__py3-none-any.whl → 0.34.1__py3-none-any.whl
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.
- tactus/__init__.py +1 -1
- tactus/adapters/__init__.py +18 -1
- tactus/adapters/broker_log.py +127 -34
- tactus/adapters/channels/__init__.py +153 -0
- tactus/adapters/channels/base.py +174 -0
- tactus/adapters/channels/broker.py +179 -0
- tactus/adapters/channels/cli.py +448 -0
- tactus/adapters/channels/host.py +225 -0
- tactus/adapters/channels/ipc.py +297 -0
- tactus/adapters/channels/sse.py +305 -0
- tactus/adapters/cli_hitl.py +223 -1
- tactus/adapters/control_loop.py +879 -0
- tactus/adapters/file_storage.py +35 -2
- tactus/adapters/ide_log.py +7 -1
- tactus/backends/http_backend.py +0 -1
- tactus/broker/client.py +31 -1
- tactus/broker/server.py +416 -92
- tactus/cli/app.py +270 -7
- tactus/cli/control.py +393 -0
- tactus/core/config_manager.py +33 -6
- tactus/core/dsl_stubs.py +102 -18
- tactus/core/execution_context.py +265 -8
- tactus/core/lua_sandbox.py +8 -9
- tactus/core/registry.py +19 -2
- tactus/core/runtime.py +235 -27
- tactus/docker/Dockerfile.pypi +49 -0
- tactus/docs/__init__.py +33 -0
- tactus/docs/extractor.py +326 -0
- tactus/docs/html_renderer.py +72 -0
- tactus/docs/models.py +121 -0
- tactus/docs/templates/base.html +204 -0
- tactus/docs/templates/index.html +58 -0
- tactus/docs/templates/module.html +96 -0
- tactus/dspy/agent.py +403 -22
- tactus/dspy/broker_lm.py +57 -6
- tactus/dspy/config.py +14 -3
- tactus/dspy/history.py +2 -1
- tactus/dspy/module.py +136 -11
- tactus/dspy/signature.py +0 -1
- tactus/ide/config_server.py +536 -0
- tactus/ide/server.py +345 -21
- tactus/primitives/human.py +619 -47
- tactus/primitives/system.py +0 -1
- tactus/protocols/__init__.py +25 -0
- tactus/protocols/control.py +427 -0
- tactus/protocols/notification.py +207 -0
- tactus/sandbox/container_runner.py +79 -11
- tactus/sandbox/docker_manager.py +23 -0
- tactus/sandbox/entrypoint.py +26 -0
- tactus/sandbox/protocol.py +3 -0
- tactus/stdlib/README.md +77 -0
- tactus/stdlib/__init__.py +27 -1
- tactus/stdlib/classify/__init__.py +165 -0
- tactus/stdlib/classify/classify.spec.tac +195 -0
- tactus/stdlib/classify/classify.tac +257 -0
- tactus/stdlib/classify/fuzzy.py +282 -0
- tactus/stdlib/classify/llm.py +319 -0
- tactus/stdlib/classify/primitive.py +287 -0
- tactus/stdlib/core/__init__.py +57 -0
- tactus/stdlib/core/base.py +320 -0
- tactus/stdlib/core/confidence.py +211 -0
- tactus/stdlib/core/models.py +161 -0
- tactus/stdlib/core/retry.py +171 -0
- tactus/stdlib/core/validation.py +274 -0
- tactus/stdlib/extract/__init__.py +125 -0
- tactus/stdlib/extract/llm.py +330 -0
- tactus/stdlib/extract/primitive.py +256 -0
- tactus/stdlib/tac/tactus/classify/base.tac +51 -0
- tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
- tactus/stdlib/tac/tactus/classify/index.md +77 -0
- tactus/stdlib/tac/tactus/classify/init.tac +29 -0
- tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
- tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
- tactus/stdlib/tac/tactus/extract/base.tac +138 -0
- tactus/stdlib/tac/tactus/extract/index.md +96 -0
- tactus/stdlib/tac/tactus/extract/init.tac +27 -0
- tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
- tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
- tactus/stdlib/tac/tactus/generate/base.tac +142 -0
- tactus/stdlib/tac/tactus/generate/index.md +195 -0
- tactus/stdlib/tac/tactus/generate/init.tac +28 -0
- tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
- tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
- tactus/testing/behave_integration.py +171 -7
- tactus/testing/context.py +0 -1
- tactus/testing/evaluation_runner.py +0 -1
- tactus/testing/gherkin_parser.py +0 -1
- tactus/testing/mock_hitl.py +0 -1
- tactus/testing/mock_tools.py +0 -1
- tactus/testing/models.py +0 -1
- tactus/testing/steps/builtin.py +0 -1
- tactus/testing/steps/custom.py +81 -22
- tactus/testing/steps/registry.py +0 -1
- tactus/testing/test_runner.py +7 -1
- tactus/validation/semantic_visitor.py +11 -5
- tactus/validation/validator.py +0 -1
- {tactus-0.31.0.dist-info → tactus-0.34.1.dist-info}/METADATA +16 -2
- {tactus-0.31.0.dist-info → tactus-0.34.1.dist-info}/RECORD +101 -49
- {tactus-0.31.0.dist-info → tactus-0.34.1.dist-info}/WHEEL +0 -0
- {tactus-0.31.0.dist-info → tactus-0.34.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.31.0.dist-info → tactus-0.34.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Generate Module
|
|
2
|
+
|
|
3
|
+
The `tactus.generate` module provides flexible text generation built on DSPy's modular architecture.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
All generators extend `BaseGenerator` and share a common interface. The module supports multiple output formats and optional chain-of-thought reasoning via DSPy's native modules.
|
|
8
|
+
|
|
9
|
+
## DSPy Module Integration
|
|
10
|
+
|
|
11
|
+
The generator uses DSPy's module system under the hood:
|
|
12
|
+
|
|
13
|
+
| Mode | DSPy Module | Behavior |
|
|
14
|
+
|------|------------|----------|
|
|
15
|
+
| **Default** | `Raw` | No prompt modifications - passes your system prompt and user message directly to the LLM without any DSPy formatting |
|
|
16
|
+
| **`reasoning = true`** | `ChainOfThought` | Uses DSPy's native reasoning module - automatically adds step-by-step thinking |
|
|
17
|
+
|
|
18
|
+
### Why Raw Mode by Default?
|
|
19
|
+
|
|
20
|
+
Even DSPy's basic `Predict` module adds formatting delimiters (like `[[ ## response ## ]]`) to prompts. The `Raw` module bypasses all DSPy prompt modifications, giving you:
|
|
21
|
+
|
|
22
|
+
- **Clean prompts**: Your system prompt goes to the LLM exactly as written
|
|
23
|
+
- **Predictable output**: No unexpected formatting in responses
|
|
24
|
+
- **Full control**: You decide what goes in the prompt
|
|
25
|
+
|
|
26
|
+
### When to Use ChainOfThought
|
|
27
|
+
|
|
28
|
+
Enable `reasoning = true` when you want the model to:
|
|
29
|
+
- Show its work on math problems
|
|
30
|
+
- Explain multi-step reasoning
|
|
31
|
+
- Provide transparent decision-making
|
|
32
|
+
|
|
33
|
+
The reasoning is captured separately from the final answer, so you can access both.
|
|
34
|
+
|
|
35
|
+
## Output Formats
|
|
36
|
+
|
|
37
|
+
| Format | Description |
|
|
38
|
+
|--------|-------------|
|
|
39
|
+
| `text` | Plain text output (default) |
|
|
40
|
+
| `json` | JSON-formatted response with validation |
|
|
41
|
+
| `markdown` | Markdown-formatted response |
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
The module uses a proper Lua class hierarchy:
|
|
46
|
+
|
|
47
|
+
- `BaseGenerator` - Abstract base with common interface
|
|
48
|
+
- `LLMGenerator` - LLM-powered generation with all options
|
|
49
|
+
|
|
50
|
+
All generators return a consistent result format:
|
|
51
|
+
|
|
52
|
+
```lua
|
|
53
|
+
{
|
|
54
|
+
output = "The generated text...", -- Main output (final answer)
|
|
55
|
+
reasoning = "Step-by-step...", -- Reasoning steps (only if reasoning=true)
|
|
56
|
+
format = "text", -- Format used
|
|
57
|
+
retry_count = 0, -- Number of retries needed
|
|
58
|
+
raw_response = "...", -- Raw LLM response
|
|
59
|
+
error = nil -- Error message if failed
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Loading the Module
|
|
64
|
+
|
|
65
|
+
```lua
|
|
66
|
+
-- Load the main module
|
|
67
|
+
local generate = require("tactus.generate")
|
|
68
|
+
|
|
69
|
+
-- Or load specific generators (dependencies auto-load)
|
|
70
|
+
local LLMGenerator = require("tactus.generate.llm")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Basic Text Generation (Raw Mode)
|
|
76
|
+
|
|
77
|
+
By default, your prompt goes directly to the LLM without modification:
|
|
78
|
+
|
|
79
|
+
```lua
|
|
80
|
+
local generator = LLMGenerator:new {
|
|
81
|
+
model = "openai/gpt-4o-mini"
|
|
82
|
+
}
|
|
83
|
+
local result = generator:generate("Write a haiku about programming")
|
|
84
|
+
print(result.output)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Chain-of-Thought Reasoning
|
|
88
|
+
|
|
89
|
+
Enable `reasoning = true` to use DSPy's `ChainOfThought` module. The reasoning is captured in a separate field:
|
|
90
|
+
|
|
91
|
+
```lua
|
|
92
|
+
local generator = LLMGenerator:new {
|
|
93
|
+
model = "openai/gpt-4o-mini",
|
|
94
|
+
reasoning = true
|
|
95
|
+
}
|
|
96
|
+
local result = generator:generate("What is 15% of 240?")
|
|
97
|
+
|
|
98
|
+
-- Access both the reasoning and the final answer
|
|
99
|
+
print("Reasoning:", result.reasoning) -- "15% means 15/100 = 0.15. So 0.15 × 240 = 36"
|
|
100
|
+
print("Answer:", result.output) -- "36"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### JSON Output Format
|
|
104
|
+
|
|
105
|
+
```lua
|
|
106
|
+
local generator = LLMGenerator:new {
|
|
107
|
+
model = "openai/gpt-4o-mini",
|
|
108
|
+
output_format = "json"
|
|
109
|
+
}
|
|
110
|
+
local result = generator:generate("Return a JSON object with name, age, and city for a fictional person")
|
|
111
|
+
-- result.output will be valid JSON like: {"name": "Alice", "age": 28, "city": "Portland"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Custom System Prompt
|
|
115
|
+
|
|
116
|
+
Your system prompt is passed directly to the LLM (no DSPy modifications):
|
|
117
|
+
|
|
118
|
+
```lua
|
|
119
|
+
local generator = LLMGenerator:new {
|
|
120
|
+
model = "openai/gpt-4o-mini",
|
|
121
|
+
system_prompt = "You are a helpful coding assistant specializing in Lua.",
|
|
122
|
+
temperature = 0.3
|
|
123
|
+
}
|
|
124
|
+
local result = generator:generate("How do I iterate over a table in Lua?")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### With Constraints
|
|
128
|
+
|
|
129
|
+
```lua
|
|
130
|
+
local generator = LLMGenerator:new {
|
|
131
|
+
model = "openai/gpt-4o-mini",
|
|
132
|
+
constraints = {"Keep response under 50 words", "Use simple language"}
|
|
133
|
+
}
|
|
134
|
+
local result = generator:generate("Explain quantum computing")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Parameters Reference
|
|
138
|
+
|
|
139
|
+
| Parameter | Type | Default | Description |
|
|
140
|
+
|-----------|------|---------|-------------|
|
|
141
|
+
| `model` | `string` | required | Model identifier (e.g., "openai/gpt-4o-mini") |
|
|
142
|
+
| `temperature` | `number` | 0.7 | Generation randomness (0.0-1.0) |
|
|
143
|
+
| `max_tokens` | `number` | nil | Maximum output tokens |
|
|
144
|
+
| `reasoning` | `boolean` | false | Enable ChainOfThought mode (captures reasoning separately) |
|
|
145
|
+
| `output_format` | `string` | "text" | Output format: "text", "json", "markdown" |
|
|
146
|
+
| `system_prompt` | `string` | nil | Custom system prompt (passed directly, no modifications) |
|
|
147
|
+
| `instructions` | `string` | nil | Additional generation instructions |
|
|
148
|
+
| `constraints` | `string|table` | nil | Output constraints |
|
|
149
|
+
| `max_retries` | `number` | 2 | Maximum retry attempts |
|
|
150
|
+
|
|
151
|
+
## Result Fields
|
|
152
|
+
|
|
153
|
+
| Field | Type | Description |
|
|
154
|
+
|-------|------|-------------|
|
|
155
|
+
| `output` | `string` | The main generated output (final answer) |
|
|
156
|
+
| `reasoning` | `string?` | Step-by-step reasoning (only when `reasoning = true`) |
|
|
157
|
+
| `format` | `string` | The output format used |
|
|
158
|
+
| `retry_count` | `number` | Number of retries that were needed |
|
|
159
|
+
| `raw_response` | `string` | The raw response from the LLM |
|
|
160
|
+
| `error` | `string?` | Error message if generation failed |
|
|
161
|
+
|
|
162
|
+
## Future Enhancements
|
|
163
|
+
|
|
164
|
+
Planned DSPy-inspired features:
|
|
165
|
+
|
|
166
|
+
- **Few-shot examples**: Pass examples for in-context learning
|
|
167
|
+
- **Optimizers**: Automatic prompt optimization with training data
|
|
168
|
+
- **Assertions**: Output validation with automatic retry
|
|
169
|
+
- **Parallel generation**: Multiple completions with selection
|
|
170
|
+
|
|
171
|
+
## Extending Generators
|
|
172
|
+
|
|
173
|
+
You can extend `BaseGenerator` to create custom generators:
|
|
174
|
+
|
|
175
|
+
```lua
|
|
176
|
+
local base = require("tactus.generate.base")
|
|
177
|
+
local class = base.class
|
|
178
|
+
local BaseGenerator = base.BaseGenerator
|
|
179
|
+
|
|
180
|
+
MyGenerator = class(BaseGenerator)
|
|
181
|
+
|
|
182
|
+
function MyGenerator:init(config)
|
|
183
|
+
BaseGenerator.init(self, config)
|
|
184
|
+
-- Your initialization
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
function MyGenerator:generate(prompt)
|
|
188
|
+
-- Your generation logic
|
|
189
|
+
return {
|
|
190
|
+
output = "...",
|
|
191
|
+
format = self.output_format,
|
|
192
|
+
retry_count = 0
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Tactus Generate Module
|
|
2
|
+
--
|
|
3
|
+
-- Provides flexible text generation with DSPy-inspired features:
|
|
4
|
+
-- - LLM-based generation (tactus.generate.llm)
|
|
5
|
+
-- - Optional chain-of-thought reasoning
|
|
6
|
+
-- - Output format control (text, JSON, markdown)
|
|
7
|
+
-- - Extensible base class (tactus.generate.base)
|
|
8
|
+
--
|
|
9
|
+
-- Usage:
|
|
10
|
+
-- local generate = require("tactus.generate")
|
|
11
|
+
-- local generator = generate.LLMGenerator:new{...}
|
|
12
|
+
--
|
|
13
|
+
-- Or load specific generators:
|
|
14
|
+
-- local LLMGenerator = require("tactus.generate.llm")
|
|
15
|
+
|
|
16
|
+
-- Load all submodules
|
|
17
|
+
local base = require("tactus.generate.base")
|
|
18
|
+
local llm = require("tactus.generate.llm")
|
|
19
|
+
|
|
20
|
+
-- Re-export all classes
|
|
21
|
+
return {
|
|
22
|
+
-- Core classes
|
|
23
|
+
BaseGenerator = base.BaseGenerator,
|
|
24
|
+
LLMGenerator = llm.LLMGenerator,
|
|
25
|
+
|
|
26
|
+
-- Helper for users who want to extend
|
|
27
|
+
class = base.class,
|
|
28
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
-- LLM-Based Text Generation
|
|
2
|
+
--
|
|
3
|
+
-- Provides flexible text generation with DSPy-inspired features:
|
|
4
|
+
-- - Configurable prompts and system instructions
|
|
5
|
+
-- - Optional chain-of-thought reasoning
|
|
6
|
+
-- - Output format control (text, JSON, markdown)
|
|
7
|
+
-- - Retry logic for invalid responses
|
|
8
|
+
-- - Few-shot examples support (future optimization)
|
|
9
|
+
|
|
10
|
+
-- Load dependencies
|
|
11
|
+
local base = require("tactus.generate.base")
|
|
12
|
+
local BaseGenerator = base.BaseGenerator
|
|
13
|
+
local class = base.class
|
|
14
|
+
|
|
15
|
+
-- ============================================================================
|
|
16
|
+
-- LLMGenerator
|
|
17
|
+
-- ============================================================================
|
|
18
|
+
|
|
19
|
+
local LLMGenerator = class(BaseGenerator)
|
|
20
|
+
|
|
21
|
+
function LLMGenerator:init(config)
|
|
22
|
+
BaseGenerator.init(self, config)
|
|
23
|
+
|
|
24
|
+
-- Build system prompt (without reasoning - ChainOfThought handles that)
|
|
25
|
+
local full_system_prompt = self:build_system_prompt()
|
|
26
|
+
|
|
27
|
+
-- Create agent configuration
|
|
28
|
+
-- Default: "Raw" module (no prompt modifications)
|
|
29
|
+
-- With reasoning: "ChainOfThought" module (DSPy's reasoning)
|
|
30
|
+
local agent_config = {
|
|
31
|
+
system_prompt = full_system_prompt,
|
|
32
|
+
temperature = self.temperature,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
-- Use ChainOfThought module when reasoning is enabled
|
|
36
|
+
-- Otherwise, Agent defaults to "Raw" (no prompt modifications)
|
|
37
|
+
if self.reasoning then
|
|
38
|
+
agent_config.module = "ChainOfThought"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
-- Parse model string (e.g., "openai/gpt-4o-mini")
|
|
42
|
+
if self.model then
|
|
43
|
+
local provider, model_id = self.model:match("([^/]+)/(.+)")
|
|
44
|
+
if provider and model_id then
|
|
45
|
+
agent_config.provider = provider
|
|
46
|
+
agent_config.model = model_id
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
-- Add max_tokens if specified
|
|
51
|
+
if self.max_tokens then
|
|
52
|
+
agent_config.max_tokens = self.max_tokens
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if self.name then
|
|
56
|
+
self.agent = Agent(self.name)(agent_config)
|
|
57
|
+
else
|
|
58
|
+
self.agent = Agent(agent_config)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
function LLMGenerator:generate(prompt)
|
|
63
|
+
local retry_count = 0
|
|
64
|
+
local last_response = nil
|
|
65
|
+
local last_error = nil
|
|
66
|
+
|
|
67
|
+
for attempt = 1, self.max_retries + 1 do
|
|
68
|
+
-- Call agent
|
|
69
|
+
local ok, agent_result = pcall(function()
|
|
70
|
+
return self.agent({message = prompt})
|
|
71
|
+
end)
|
|
72
|
+
|
|
73
|
+
if not ok then
|
|
74
|
+
last_error = agent_result
|
|
75
|
+
retry_count = retry_count + 1
|
|
76
|
+
else
|
|
77
|
+
local output = agent_result.output
|
|
78
|
+
local response_text = nil
|
|
79
|
+
local reasoning_text = nil
|
|
80
|
+
|
|
81
|
+
-- Handle different output formats from DSPy modules
|
|
82
|
+
if type(output) == "table" then
|
|
83
|
+
-- ChainOfThought returns {reasoning: ..., response: ...}
|
|
84
|
+
response_text = tostring(output.response or "")
|
|
85
|
+
reasoning_text = output.reasoning and tostring(output.reasoning) or nil
|
|
86
|
+
last_response = response_text
|
|
87
|
+
else
|
|
88
|
+
-- Raw module returns plain text
|
|
89
|
+
response_text = tostring(output or "")
|
|
90
|
+
last_response = response_text
|
|
91
|
+
|
|
92
|
+
-- For raw mode with reasoning enabled, try to parse structured output
|
|
93
|
+
-- (This handles edge cases where manual reasoning format was used)
|
|
94
|
+
if self.reasoning and type(response_text) == "string" then
|
|
95
|
+
local parsed = self:parse_reasoning_response(response_text)
|
|
96
|
+
response_text = parsed.response
|
|
97
|
+
reasoning_text = parsed.reasoning
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
-- Validate response based on format
|
|
102
|
+
local valid = self:validate_response(response_text)
|
|
103
|
+
|
|
104
|
+
if valid then
|
|
105
|
+
return {
|
|
106
|
+
output = response_text,
|
|
107
|
+
reasoning = reasoning_text,
|
|
108
|
+
format = self.output_format,
|
|
109
|
+
retry_count = retry_count,
|
|
110
|
+
raw_response = last_response
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
retry_count = retry_count + 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
-- All retries exhausted
|
|
119
|
+
return {
|
|
120
|
+
output = last_response or "",
|
|
121
|
+
reasoning = nil,
|
|
122
|
+
format = self.output_format,
|
|
123
|
+
retry_count = retry_count,
|
|
124
|
+
error = last_error or "Failed to generate valid response after " .. self.max_retries .. " retries",
|
|
125
|
+
raw_response = last_response
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
function LLMGenerator:validate_response(response)
|
|
130
|
+
-- Ensure response is a string
|
|
131
|
+
if response == nil then
|
|
132
|
+
return false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
-- Convert to string if needed (handles Python objects)
|
|
136
|
+
local response_str = tostring(response)
|
|
137
|
+
|
|
138
|
+
-- Basic validation - ensure we got something
|
|
139
|
+
if #response_str == 0 then
|
|
140
|
+
return false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
-- JSON format validation
|
|
144
|
+
if self.output_format == "json" then
|
|
145
|
+
-- Try to detect valid JSON (basic check)
|
|
146
|
+
local trimmed = response_str:gsub("^%s+", ""):gsub("%s+$", "")
|
|
147
|
+
|
|
148
|
+
-- Should start with { or [
|
|
149
|
+
if not (trimmed:match("^%{") or trimmed:match("^%[")) then
|
|
150
|
+
return false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
-- Should end with } or ]
|
|
154
|
+
if not (trimmed:match("%}$") or trimmed:match("%]$")) then
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
function LLMGenerator:__call(prompt)
|
|
163
|
+
return self:generate(prompt)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
-- Export LLMGenerator
|
|
167
|
+
return {
|
|
168
|
+
LLMGenerator = LLMGenerator,
|
|
169
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
--[[doc
|
|
2
|
+
# Generate Classes
|
|
3
|
+
|
|
4
|
+
Flexible text generation with DSPy-inspired features:
|
|
5
|
+
|
|
6
|
+
- **BaseGenerator**: Abstract base class for custom generators
|
|
7
|
+
- **LLMGenerator**: LLM-based generation with configurable options
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```lua
|
|
12
|
+
-- Import generate classes
|
|
13
|
+
local generate = require("tactus.generate")
|
|
14
|
+
local LLMGenerator = generate.LLMGenerator
|
|
15
|
+
|
|
16
|
+
-- Or load directly:
|
|
17
|
+
local LLMGenerator = require("tactus.generate.llm")
|
|
18
|
+
|
|
19
|
+
-- Basic generation
|
|
20
|
+
local generator = LLMGenerator:new {
|
|
21
|
+
model = "openai/gpt-4o-mini"
|
|
22
|
+
}
|
|
23
|
+
local result = generator:generate("Write a haiku about coding")
|
|
24
|
+
|
|
25
|
+
-- With chain-of-thought reasoning (DSPy-inspired)
|
|
26
|
+
local reasoning_generator = LLMGenerator:new {
|
|
27
|
+
model = "openai/gpt-4o-mini",
|
|
28
|
+
reasoning = true
|
|
29
|
+
}
|
|
30
|
+
local result = reasoning_generator:generate("Solve: What is 15% of 80?")
|
|
31
|
+
-- result.reasoning contains step-by-step thinking
|
|
32
|
+
-- result.output contains final answer
|
|
33
|
+
|
|
34
|
+
-- JSON output format
|
|
35
|
+
local json_generator = LLMGenerator:new {
|
|
36
|
+
model = "openai/gpt-4o-mini",
|
|
37
|
+
output_format = "json"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## LLMGenerator Parameters
|
|
42
|
+
|
|
43
|
+
- `model`: Model identifier (e.g., "openai/gpt-4o-mini")
|
|
44
|
+
- `temperature`: Generation randomness (default: 0.7)
|
|
45
|
+
- `max_tokens`: Maximum output tokens (optional)
|
|
46
|
+
- `reasoning`: Enable chain-of-thought mode (default: false)
|
|
47
|
+
- `output_format`: Output format - "text" (default), "json", "markdown"
|
|
48
|
+
- `system_prompt`: Custom system prompt (optional)
|
|
49
|
+
- `instructions`: Additional instructions (optional)
|
|
50
|
+
- `constraints`: Output constraints (optional)
|
|
51
|
+
- `max_retries`: Maximum retry attempts (default: 2)
|
|
52
|
+
]]
|
|
53
|
+
|
|
54
|
+
-- Load generate classes
|
|
55
|
+
local generate = require("tactus.generate")
|
|
56
|
+
local LLMGenerator = generate.LLMGenerator
|
|
57
|
+
|
|
58
|
+
-- Local state for test context
|
|
59
|
+
local test_state = {}
|
|
60
|
+
|
|
61
|
+
-- Custom step definitions
|
|
62
|
+
Step("an LLM generator", function(ctx)
|
|
63
|
+
test_state.generator_config = {
|
|
64
|
+
name = "stdlib_generate_llm",
|
|
65
|
+
model = "openai/gpt-4o-mini"
|
|
66
|
+
}
|
|
67
|
+
end)
|
|
68
|
+
|
|
69
|
+
Step("an LLM generator with reasoning enabled", function(ctx)
|
|
70
|
+
test_state.generator_config = {
|
|
71
|
+
name = "stdlib_generate_llm",
|
|
72
|
+
model = "openai/gpt-4o-mini",
|
|
73
|
+
reasoning = true
|
|
74
|
+
}
|
|
75
|
+
end)
|
|
76
|
+
|
|
77
|
+
Step("an LLM generator with JSON output format", function(ctx)
|
|
78
|
+
test_state.generator_config = {
|
|
79
|
+
name = "stdlib_generate_llm",
|
|
80
|
+
model = "openai/gpt-4o-mini",
|
|
81
|
+
output_format = "json"
|
|
82
|
+
}
|
|
83
|
+
end)
|
|
84
|
+
|
|
85
|
+
Step("an LLM generator with markdown output format", function(ctx)
|
|
86
|
+
test_state.generator_config = {
|
|
87
|
+
name = "stdlib_generate_llm",
|
|
88
|
+
model = "openai/gpt-4o-mini",
|
|
89
|
+
output_format = "markdown"
|
|
90
|
+
}
|
|
91
|
+
end)
|
|
92
|
+
|
|
93
|
+
Step("temperature of (.+)", function(ctx, temp)
|
|
94
|
+
test_state.generator_config.temperature = tonumber(temp)
|
|
95
|
+
end)
|
|
96
|
+
|
|
97
|
+
Step("system prompt \"(.+)\"", function(ctx, prompt)
|
|
98
|
+
test_state.generator_config.system_prompt = prompt
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
Step("I generate text for prompt \"(.+)\"", function(ctx, prompt)
|
|
102
|
+
if not test_state.generator then
|
|
103
|
+
test_state.generator = LLMGenerator:new(test_state.generator_config)
|
|
104
|
+
end
|
|
105
|
+
test_state.result = test_state.generator:generate(prompt)
|
|
106
|
+
end)
|
|
107
|
+
|
|
108
|
+
Step("the generation should succeed", function(ctx)
|
|
109
|
+
assert(test_state.result, "No generation result found")
|
|
110
|
+
assert(not test_state.result.error,
|
|
111
|
+
"Generation failed with error: " .. tostring(test_state.result.error))
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
Step("the output should not be empty", function(ctx)
|
|
115
|
+
assert(test_state.result, "No generation result found")
|
|
116
|
+
assert(test_state.result.output, "No output in result")
|
|
117
|
+
assert(#test_state.result.output > 0, "Output is empty")
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
Step("the result format should be \"(.+)\"", function(ctx, expected_format)
|
|
121
|
+
assert(test_state.result, "No generation result found")
|
|
122
|
+
assert(test_state.result.format == expected_format,
|
|
123
|
+
"Expected format '" .. expected_format .. "' but got '" .. tostring(test_state.result.format) .. "'")
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
Step("the result should include reasoning", function(ctx)
|
|
127
|
+
assert(test_state.result, "No generation result found")
|
|
128
|
+
-- Note: reasoning may or may not be parsed depending on LLM response format
|
|
129
|
+
-- We check that the result structure supports reasoning
|
|
130
|
+
assert(test_state.result.output ~= nil, "Output should be present")
|
|
131
|
+
end)
|
|
132
|
+
|
|
133
|
+
Step("the output should look like JSON", function(ctx)
|
|
134
|
+
assert(test_state.result, "No generation result found")
|
|
135
|
+
local output = test_state.result.output or ""
|
|
136
|
+
local trimmed = output:gsub("^%s+", ""):gsub("%s+$", "")
|
|
137
|
+
assert(trimmed:match("^%{") or trimmed:match("^%["),
|
|
138
|
+
"Output does not appear to be JSON: " .. output:sub(1, 100))
|
|
139
|
+
end)
|
|
140
|
+
|
|
141
|
+
Mocks {
|
|
142
|
+
stdlib_generate_llm = {
|
|
143
|
+
message = "Mocked response",
|
|
144
|
+
temporal = {
|
|
145
|
+
{
|
|
146
|
+
when_message = "Write a one-sentence description of the color blue.",
|
|
147
|
+
message = "Blue is a calm, cool color that often symbolizes clarity and depth."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
when_message = "Generate a creative name for a coffee shop.",
|
|
151
|
+
message = "Amber Bean Cafe"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
when_message = "What is 25% of 120? Explain your calculation.",
|
|
155
|
+
message = "REASONING: 25% is one quarter. 120 divided by 4 is 30. RESPONSE: 30"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
when_message = "Return a JSON object with keys 'name' and 'age' for a fictional person.",
|
|
159
|
+
message = [[{"name":"Ava","age":28}]]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
-- BDD Specifications
|
|
166
|
+
Specification([[
|
|
167
|
+
Feature: Generate Class Hierarchy
|
|
168
|
+
As a Tactus developer
|
|
169
|
+
I want to generate text with various options
|
|
170
|
+
So that I can create content flexibly
|
|
171
|
+
|
|
172
|
+
Scenario: Basic text generation
|
|
173
|
+
Given an LLM generator
|
|
174
|
+
When I generate text for prompt "Write a one-sentence description of the color blue."
|
|
175
|
+
Then the generation should succeed
|
|
176
|
+
And the output should not be empty
|
|
177
|
+
And the result format should be "text"
|
|
178
|
+
|
|
179
|
+
Scenario: Generation with custom temperature
|
|
180
|
+
Given an LLM generator
|
|
181
|
+
And temperature of 0.9
|
|
182
|
+
When I generate text for prompt "Generate a creative name for a coffee shop."
|
|
183
|
+
Then the generation should succeed
|
|
184
|
+
And the output should not be empty
|
|
185
|
+
|
|
186
|
+
Scenario: Generation with reasoning mode
|
|
187
|
+
Given an LLM generator with reasoning enabled
|
|
188
|
+
When I generate text for prompt "What is 25% of 120? Explain your calculation."
|
|
189
|
+
Then the generation should succeed
|
|
190
|
+
And the output should not be empty
|
|
191
|
+
And the result should include reasoning
|
|
192
|
+
|
|
193
|
+
Scenario: JSON output format
|
|
194
|
+
Given an LLM generator with JSON output format
|
|
195
|
+
When I generate text for prompt "Return a JSON object with keys 'name' and 'age' for a fictional person."
|
|
196
|
+
Then the generation should succeed
|
|
197
|
+
And the output should not be empty
|
|
198
|
+
And the result format should be "json"
|
|
199
|
+
And the output should look like JSON
|
|
200
|
+
]])
|
|
201
|
+
|
|
202
|
+
-- Minimal procedure
|
|
203
|
+
Procedure {
|
|
204
|
+
output = {
|
|
205
|
+
result = field.string{required = true}
|
|
206
|
+
},
|
|
207
|
+
function(input)
|
|
208
|
+
return {result = "Generate class hierarchy specs executed"}
|
|
209
|
+
end
|
|
210
|
+
}
|