tactus 0.31.2__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 +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
tactus/core/registry.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry system for Lua DSL declarations.
|
|
3
|
+
|
|
4
|
+
This module provides Pydantic models for collecting and validating
|
|
5
|
+
procedure declarations from .tac files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, ValidationError, ConfigDict
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFieldDeclaration(BaseModel):
|
|
17
|
+
"""Output field declaration from DSL."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
field_type: str = Field(alias="type") # string, number, boolean, array, object
|
|
21
|
+
required: bool = False
|
|
22
|
+
description: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MessageHistoryConfiguration(BaseModel):
|
|
28
|
+
"""Message history configuration for agents.
|
|
29
|
+
|
|
30
|
+
Aligned with pydantic-ai's message_history concept.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
source: str = "own" # "own", "shared", or another agent's name
|
|
34
|
+
filter: Optional[Any] = None # Lua function reference or filter name
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentOutputSchema(BaseModel):
|
|
38
|
+
"""Maps to Pydantic AI's output."""
|
|
39
|
+
|
|
40
|
+
fields: dict[str, OutputFieldDeclaration] = Field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentDeclaration(BaseModel):
|
|
44
|
+
"""Agent declaration from DSL."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
provider: Optional[str] = None
|
|
48
|
+
model: Union[str, dict[str, Any]] = "gpt-4o"
|
|
49
|
+
system_prompt: Union[str, Any] # String with {markers} or Lua function
|
|
50
|
+
initial_message: Optional[str] = None
|
|
51
|
+
tools: list[Any] = Field(default_factory=list) # Tool/toolset references and expressions
|
|
52
|
+
inline_tools: list[dict[str, Any]] = Field(default_factory=list) # Inline tool definitions
|
|
53
|
+
output: Optional[AgentOutputSchema] = None # Aligned with pydantic-ai
|
|
54
|
+
message_history: Optional[MessageHistoryConfiguration] = None
|
|
55
|
+
max_turns: int = 50
|
|
56
|
+
disable_streaming: bool = (
|
|
57
|
+
False # Disable streaming for models that don't support tools in streaming mode
|
|
58
|
+
)
|
|
59
|
+
temperature: Optional[float] = None
|
|
60
|
+
max_tokens: Optional[int] = None
|
|
61
|
+
model_type: Optional[str] = None # e.g., "chat", "responses" for reasoning models
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="allow")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HITLDeclaration(BaseModel):
|
|
67
|
+
"""Human-in-the-loop interaction point declaration."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
hitl_type: str = Field(alias="type") # approval, input, review
|
|
71
|
+
message: str
|
|
72
|
+
timeout: Optional[int] = None
|
|
73
|
+
default: Any = None
|
|
74
|
+
options: Optional[list[dict[str, Any]]] = None
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ScenarioDeclaration(BaseModel):
|
|
80
|
+
"""BDD scenario declaration."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
given: dict[str, Any] = Field(default_factory=dict)
|
|
84
|
+
when: Optional[str] = None # defaults to "procedure_completes"
|
|
85
|
+
then_output: Optional[dict[str, Any]] = None
|
|
86
|
+
then_state: Optional[dict[str, Any]] = None
|
|
87
|
+
mocks: dict[str, Any] = Field(default_factory=dict) # tool_name -> response
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SpecificationDeclaration(BaseModel):
|
|
91
|
+
"""BDD specification declaration."""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
94
|
+
scenarios: list[ScenarioDeclaration] = Field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DependencyDeclaration(BaseModel):
|
|
98
|
+
"""Dependency declaration from DSL."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
dependency_type: str = Field(alias="type") # http_client, postgres, redis
|
|
102
|
+
config: dict[str, Any] = Field(default_factory=dict) # Configuration dict
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AgentMockConfig(BaseModel):
|
|
108
|
+
"""Mock configuration for an agent's behavior.
|
|
109
|
+
|
|
110
|
+
Specifies what tool calls the agent should simulate when mocking is enabled.
|
|
111
|
+
This allows agent-based tests to pass in CI without making real LLM calls.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
tool_calls: list[dict[str, Any]] = Field(default_factory=list)
|
|
115
|
+
# List of tool calls to simulate: [{"tool": "done", "args": {"reason": "..."}}, ...]
|
|
116
|
+
message: str = "" # The agent's final message response
|
|
117
|
+
data: dict[str, Any] = Field(
|
|
118
|
+
default_factory=dict,
|
|
119
|
+
description="Optional structured response payload (exposed as result.data in Lua)",
|
|
120
|
+
)
|
|
121
|
+
usage: dict[str, Any] = Field(
|
|
122
|
+
default_factory=dict,
|
|
123
|
+
description="Optional token usage payload (exposed as result.usage in Lua)",
|
|
124
|
+
)
|
|
125
|
+
temporal: list[dict[str, Any]] = Field(
|
|
126
|
+
default_factory=list,
|
|
127
|
+
description="Optional temporal mock turns (1-indexed by agent turn).",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ProcedureRegistry(BaseModel):
|
|
132
|
+
"""Collects all declarations from a .tac file."""
|
|
133
|
+
|
|
134
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
135
|
+
|
|
136
|
+
# Metadata
|
|
137
|
+
description: Optional[str] = None
|
|
138
|
+
|
|
139
|
+
# Declarations
|
|
140
|
+
input_schema: dict[str, Any] = Field(default_factory=dict)
|
|
141
|
+
output_schema: dict[str, Any] = Field(default_factory=dict)
|
|
142
|
+
state_schema: dict[str, Any] = Field(default_factory=dict)
|
|
143
|
+
agents: dict[str, AgentDeclaration] = Field(default_factory=dict)
|
|
144
|
+
models: dict[str, dict[str, Any]] = Field(default_factory=dict) # ML models
|
|
145
|
+
toolsets: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
146
|
+
lua_tools: dict[str, dict[str, Any]] = Field(default_factory=dict) # Lua function tools
|
|
147
|
+
hitl_points: dict[str, HITLDeclaration] = Field(default_factory=dict)
|
|
148
|
+
specifications: list[SpecificationDeclaration] = Field(default_factory=list)
|
|
149
|
+
dependencies: dict[str, DependencyDeclaration] = Field(default_factory=dict)
|
|
150
|
+
mocks: dict[str, dict[str, Any]] = Field(default_factory=dict) # Mock configurations
|
|
151
|
+
agent_mocks: dict[str, AgentMockConfig] = Field(default_factory=dict) # Agent mock configs
|
|
152
|
+
|
|
153
|
+
# Message history configuration (aligned with pydantic-ai)
|
|
154
|
+
message_history_config: dict[str, Any] = Field(default_factory=dict)
|
|
155
|
+
|
|
156
|
+
# Gherkin BDD Testing
|
|
157
|
+
gherkin_specifications: Optional[str] = None # Raw Gherkin text
|
|
158
|
+
custom_steps: dict[str, Any] = Field(default_factory=dict) # step_text -> lua_function
|
|
159
|
+
evaluation_config: dict[str, Any] = Field(default_factory=dict) # runs, parallel, etc.
|
|
160
|
+
|
|
161
|
+
# Pydantic Evals Integration
|
|
162
|
+
pydantic_evaluations: Optional[dict[str, Any]] = None # Pydantic Evals configuration
|
|
163
|
+
|
|
164
|
+
# Prompts
|
|
165
|
+
prompts: dict[str, str] = Field(default_factory=dict)
|
|
166
|
+
return_prompt: Optional[str] = None
|
|
167
|
+
error_prompt: Optional[str] = None
|
|
168
|
+
status_prompt: Optional[str] = None
|
|
169
|
+
|
|
170
|
+
# Execution settings
|
|
171
|
+
async_enabled: bool = False
|
|
172
|
+
max_depth: int = 5
|
|
173
|
+
max_turns: int = 50
|
|
174
|
+
default_provider: Optional[str] = None
|
|
175
|
+
default_model: Optional[str] = None
|
|
176
|
+
|
|
177
|
+
# Named procedures (for in-file sub-procedures)
|
|
178
|
+
named_procedures: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
179
|
+
# Structure: {"proc_name": {"function": <lua_ref>, "input_schema": {...}, ...}}
|
|
180
|
+
|
|
181
|
+
# Script mode support (top-level input/output without explicit main procedure)
|
|
182
|
+
script_mode: bool = False
|
|
183
|
+
top_level_input_schema: dict[str, Any] = Field(default_factory=dict)
|
|
184
|
+
top_level_output_schema: dict[str, Any] = Field(default_factory=dict)
|
|
185
|
+
|
|
186
|
+
# Source locations for error messages (declaration_name -> (line, col))
|
|
187
|
+
source_locations: dict[str, tuple[int, int]] = Field(default_factory=dict)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ValidationMessage(BaseModel):
|
|
191
|
+
"""Validation error or warning message."""
|
|
192
|
+
|
|
193
|
+
level: str # "error" or "warning"
|
|
194
|
+
message: str
|
|
195
|
+
location: Optional[tuple[int, int]] = None
|
|
196
|
+
declaration: Optional[str] = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ValidationResult(BaseModel):
|
|
200
|
+
"""Result of validation."""
|
|
201
|
+
|
|
202
|
+
valid: bool
|
|
203
|
+
errors: list[ValidationMessage] = Field(default_factory=list)
|
|
204
|
+
warnings: list[ValidationMessage] = Field(default_factory=list)
|
|
205
|
+
registry: Optional[ProcedureRegistry] = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class RegistryBuilder:
|
|
209
|
+
"""Builds ProcedureRegistry from DSL function calls."""
|
|
210
|
+
|
|
211
|
+
def __init__(self):
|
|
212
|
+
self.registry = ProcedureRegistry()
|
|
213
|
+
self.validation_messages: list[ValidationMessage] = []
|
|
214
|
+
|
|
215
|
+
def register_input_schema(self, schema: dict) -> None:
|
|
216
|
+
"""Register input schema declaration."""
|
|
217
|
+
self.registry.input_schema = schema
|
|
218
|
+
|
|
219
|
+
def register_output_schema(self, schema: dict) -> None:
|
|
220
|
+
"""Register output schema declaration."""
|
|
221
|
+
self.registry.output_schema = schema
|
|
222
|
+
|
|
223
|
+
def register_state_schema(self, schema: dict) -> None:
|
|
224
|
+
"""Register state schema declaration."""
|
|
225
|
+
self.registry.state_schema = schema
|
|
226
|
+
|
|
227
|
+
def register_agent(self, name: str, config: dict, output_schema: Optional[dict] = None) -> None:
|
|
228
|
+
"""Register an agent declaration."""
|
|
229
|
+
config["name"] = name
|
|
230
|
+
|
|
231
|
+
# Add output_schema to config if provided
|
|
232
|
+
if output_schema:
|
|
233
|
+
# Convert output_schema dict to AgentOutputSchema
|
|
234
|
+
fields = {}
|
|
235
|
+
for field_name, field_config in output_schema.items():
|
|
236
|
+
# Add the field name to the config (required by OutputFieldDeclaration)
|
|
237
|
+
field_config_with_name = dict(field_config)
|
|
238
|
+
field_config_with_name["name"] = field_name
|
|
239
|
+
fields[field_name] = OutputFieldDeclaration(**field_config_with_name)
|
|
240
|
+
config["output"] = AgentOutputSchema(fields=fields)
|
|
241
|
+
|
|
242
|
+
# Apply defaults
|
|
243
|
+
if "provider" not in config and self.registry.default_provider:
|
|
244
|
+
config["provider"] = self.registry.default_provider
|
|
245
|
+
if "model" not in config and self.registry.default_model:
|
|
246
|
+
config["model"] = self.registry.default_model
|
|
247
|
+
try:
|
|
248
|
+
self.registry.agents[name] = AgentDeclaration(**config)
|
|
249
|
+
except ValidationError as e:
|
|
250
|
+
self._add_error(f"Invalid agent '{name}': {e}")
|
|
251
|
+
|
|
252
|
+
def register_model(self, name: str, config: dict) -> None:
|
|
253
|
+
"""Register a model declaration."""
|
|
254
|
+
config["name"] = name
|
|
255
|
+
self.registry.models[name] = config
|
|
256
|
+
|
|
257
|
+
def register_hitl(self, name: str, config: dict) -> None:
|
|
258
|
+
"""Register a HITL interaction point."""
|
|
259
|
+
config["name"] = name
|
|
260
|
+
try:
|
|
261
|
+
self.registry.hitl_points[name] = HITLDeclaration(**config)
|
|
262
|
+
except ValidationError as e:
|
|
263
|
+
self._add_error(f"Invalid HITL point '{name}': {e}")
|
|
264
|
+
|
|
265
|
+
def register_dependency(self, name: str, config: dict) -> None:
|
|
266
|
+
"""Register a dependency declaration."""
|
|
267
|
+
# The config dict contains the type and all other configuration
|
|
268
|
+
dependency_config = {
|
|
269
|
+
"name": name,
|
|
270
|
+
"type": config.get("type"),
|
|
271
|
+
"config": config, # Store the entire config dict
|
|
272
|
+
}
|
|
273
|
+
try:
|
|
274
|
+
self.registry.dependencies[name] = DependencyDeclaration(**dependency_config)
|
|
275
|
+
except ValidationError as e:
|
|
276
|
+
self._add_error(f"Invalid dependency '{name}': {e}")
|
|
277
|
+
|
|
278
|
+
def register_prompt(self, name: str, content: str) -> None:
|
|
279
|
+
"""Register a prompt template."""
|
|
280
|
+
self.registry.prompts[name] = content
|
|
281
|
+
|
|
282
|
+
def register_toolset(self, name: str, config: dict) -> None:
|
|
283
|
+
"""Register a toolset definition from DSL."""
|
|
284
|
+
self.registry.toolsets[name] = config
|
|
285
|
+
|
|
286
|
+
def register_tool(self, name: str, config: dict, lua_handler: Any) -> None:
|
|
287
|
+
"""Register an individual Lua tool declaration.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
name: Tool name
|
|
291
|
+
config: Dict with description, input, output schemas, and source info
|
|
292
|
+
lua_handler: Lupa function reference (or placeholder for external sources)
|
|
293
|
+
"""
|
|
294
|
+
tool_def = {
|
|
295
|
+
"description": config.get("description", ""),
|
|
296
|
+
"input": config.get("input", {}), # Changed from parameters
|
|
297
|
+
"output": config.get("output", {}), # New: output schema
|
|
298
|
+
"handler": lua_handler,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# If this tool references an external source, store that info
|
|
302
|
+
if "source" in config:
|
|
303
|
+
tool_def["source"] = config["source"]
|
|
304
|
+
|
|
305
|
+
self.registry.lua_tools[name] = tool_def
|
|
306
|
+
|
|
307
|
+
def register_mock(self, tool_name: str, config: dict) -> None:
|
|
308
|
+
"""Register a mock configuration for a tool.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
tool_name: Name of the tool to mock
|
|
312
|
+
config: Mock configuration (output, temporal, conditional_mocks, error)
|
|
313
|
+
"""
|
|
314
|
+
self.registry.mocks[tool_name] = config
|
|
315
|
+
|
|
316
|
+
def register_agent_mock(self, agent_name: str, config: dict) -> None:
|
|
317
|
+
"""Register a mock configuration for an agent.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
agent_name: Name of the agent to mock
|
|
321
|
+
config: Mock configuration with tool_calls and message
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
self.registry.agent_mocks[agent_name] = AgentMockConfig(**config)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
self._add_error(f"Invalid agent mock config for '{agent_name}': {e}")
|
|
327
|
+
|
|
328
|
+
def register_specification(self, name: str, scenarios: list) -> None:
|
|
329
|
+
"""Register a BDD specification."""
|
|
330
|
+
try:
|
|
331
|
+
spec = SpecificationDeclaration(
|
|
332
|
+
name=name, scenarios=[ScenarioDeclaration(**s) for s in scenarios]
|
|
333
|
+
)
|
|
334
|
+
self.registry.specifications.append(spec)
|
|
335
|
+
except ValidationError as e:
|
|
336
|
+
self._add_error(f"Invalid specification '{name}': {e}")
|
|
337
|
+
|
|
338
|
+
def register_named_procedure(
|
|
339
|
+
self,
|
|
340
|
+
name: str,
|
|
341
|
+
lua_function: Any,
|
|
342
|
+
input_schema: dict[str, Any],
|
|
343
|
+
output_schema: dict[str, Any],
|
|
344
|
+
state_schema: dict[str, Any],
|
|
345
|
+
) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Register a named procedure for in-file calling.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
name: Procedure name
|
|
351
|
+
lua_function: Lua function reference
|
|
352
|
+
input_schema: Input validation schema
|
|
353
|
+
output_schema: Output validation schema
|
|
354
|
+
state_schema: State initialization schema
|
|
355
|
+
"""
|
|
356
|
+
self.registry.named_procedures[name] = {
|
|
357
|
+
"function": lua_function,
|
|
358
|
+
"input_schema": input_schema,
|
|
359
|
+
"output_schema": output_schema,
|
|
360
|
+
"state_schema": state_schema,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# If this is the main entry point, also populate the top-level schemas so
|
|
364
|
+
# runtime output validation and tooling use a single canonical `output`.
|
|
365
|
+
if name == "main":
|
|
366
|
+
self.registry.input_schema = input_schema
|
|
367
|
+
self.registry.output_schema = output_schema
|
|
368
|
+
self.registry.state_schema = state_schema
|
|
369
|
+
|
|
370
|
+
def register_top_level_input(self, schema: dict) -> None:
|
|
371
|
+
"""Register top-level input schema for script mode."""
|
|
372
|
+
self.registry.top_level_input_schema = schema
|
|
373
|
+
self.registry.script_mode = True
|
|
374
|
+
|
|
375
|
+
def register_top_level_output(self, schema: dict) -> None:
|
|
376
|
+
"""Register top-level output schema for script mode."""
|
|
377
|
+
self.registry.top_level_output_schema = schema
|
|
378
|
+
self.registry.script_mode = True
|
|
379
|
+
|
|
380
|
+
def set_default_provider(self, provider: str) -> None:
|
|
381
|
+
"""Set default provider for agents."""
|
|
382
|
+
self.registry.default_provider = provider
|
|
383
|
+
|
|
384
|
+
def set_default_model(self, model: str) -> None:
|
|
385
|
+
"""Set default model for agents."""
|
|
386
|
+
self.registry.default_model = model
|
|
387
|
+
|
|
388
|
+
def set_return_prompt(self, prompt: str) -> None:
|
|
389
|
+
"""Set return prompt."""
|
|
390
|
+
self.registry.return_prompt = prompt
|
|
391
|
+
|
|
392
|
+
def set_error_prompt(self, prompt: str) -> None:
|
|
393
|
+
"""Set error prompt."""
|
|
394
|
+
self.registry.error_prompt = prompt
|
|
395
|
+
|
|
396
|
+
def set_status_prompt(self, prompt: str) -> None:
|
|
397
|
+
"""Set status prompt."""
|
|
398
|
+
self.registry.status_prompt = prompt
|
|
399
|
+
|
|
400
|
+
def set_async(self, enabled: bool) -> None:
|
|
401
|
+
"""Set async execution flag."""
|
|
402
|
+
self.registry.async_enabled = enabled
|
|
403
|
+
|
|
404
|
+
def set_max_depth(self, depth: int) -> None:
|
|
405
|
+
"""Set maximum recursion depth."""
|
|
406
|
+
self.registry.max_depth = depth
|
|
407
|
+
|
|
408
|
+
def set_max_turns(self, turns: int) -> None:
|
|
409
|
+
"""Set maximum turns."""
|
|
410
|
+
self.registry.max_turns = turns
|
|
411
|
+
|
|
412
|
+
def register_specifications(self, gherkin_text: str) -> None:
|
|
413
|
+
"""Register Gherkin BDD specifications."""
|
|
414
|
+
self.registry.gherkin_specifications = gherkin_text
|
|
415
|
+
|
|
416
|
+
def register_custom_step(self, step_text: str, lua_function: Any) -> None:
|
|
417
|
+
"""Register a custom step definition."""
|
|
418
|
+
self.registry.custom_steps[step_text] = lua_function
|
|
419
|
+
|
|
420
|
+
def set_evaluation_config(self, config: dict) -> None:
|
|
421
|
+
"""Set evaluation configuration."""
|
|
422
|
+
self.registry.evaluation_config = config
|
|
423
|
+
|
|
424
|
+
def set_message_history_config(self, config: dict) -> None:
|
|
425
|
+
"""Set procedure-level message history configuration."""
|
|
426
|
+
self.registry.message_history_config = config
|
|
427
|
+
|
|
428
|
+
def register_evaluations(self, config: dict) -> None:
|
|
429
|
+
"""Register Pydantic Evals evaluation configuration."""
|
|
430
|
+
self.registry.pydantic_evaluations = config
|
|
431
|
+
|
|
432
|
+
def _add_error(self, message: str) -> None:
|
|
433
|
+
"""Add an error message."""
|
|
434
|
+
self.validation_messages.append(ValidationMessage(level="error", message=message))
|
|
435
|
+
|
|
436
|
+
def _add_warning(self, message: str) -> None:
|
|
437
|
+
"""Add a warning message."""
|
|
438
|
+
self.validation_messages.append(ValidationMessage(level="warning", message=message))
|
|
439
|
+
|
|
440
|
+
def validate(self) -> ValidationResult:
|
|
441
|
+
"""Run all validations after declarations collected."""
|
|
442
|
+
errors = []
|
|
443
|
+
warnings = []
|
|
444
|
+
|
|
445
|
+
# Script mode: merge top-level schemas into main procedure
|
|
446
|
+
if self.registry.script_mode and "main" in self.registry.named_procedures:
|
|
447
|
+
main_proc = self.registry.named_procedures["main"]
|
|
448
|
+
# Merge top-level input schema if main doesn't have one
|
|
449
|
+
if not main_proc["input_schema"] and self.registry.top_level_input_schema:
|
|
450
|
+
main_proc["input_schema"] = self.registry.top_level_input_schema
|
|
451
|
+
# Merge top-level output schema if main doesn't have one
|
|
452
|
+
if not main_proc["output_schema"] and self.registry.top_level_output_schema:
|
|
453
|
+
main_proc["output_schema"] = self.registry.top_level_output_schema
|
|
454
|
+
|
|
455
|
+
# Check for multiple unnamed Procedures (all would register as "main")
|
|
456
|
+
# Count how many times a procedure was registered as "main"
|
|
457
|
+
main_count = sum(1 for name in self.registry.named_procedures.keys() if name == "main")
|
|
458
|
+
if main_count > 1:
|
|
459
|
+
errors.append(
|
|
460
|
+
ValidationMessage(
|
|
461
|
+
level="error",
|
|
462
|
+
message="Multiple unnamed Procedures found. Only one unnamed Procedure is allowed as the main entry point. Use named Procedures (e.g., helper = Procedure {...}) for additional procedures.",
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Note: With immediate agent creation, Procedures are optional.
|
|
467
|
+
# Top-level code can execute directly without being wrapped in a Procedure.
|
|
468
|
+
|
|
469
|
+
# Agent validation
|
|
470
|
+
for agent in self.registry.agents.values():
|
|
471
|
+
# Check if agent has provider or if there's a default
|
|
472
|
+
if not agent.provider and not self.registry.default_provider:
|
|
473
|
+
errors.append(
|
|
474
|
+
ValidationMessage(
|
|
475
|
+
level="error",
|
|
476
|
+
message=f"Agent '{agent.name}' missing provider",
|
|
477
|
+
declaration=agent.name,
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Warnings for missing specifications
|
|
482
|
+
if not self.registry.specifications and not self.registry.gherkin_specifications:
|
|
483
|
+
warnings.append(
|
|
484
|
+
ValidationMessage(
|
|
485
|
+
level="warning",
|
|
486
|
+
message="No specifications defined - consider adding BDD tests using specifications([[...]])",
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Add any errors from registration
|
|
491
|
+
errors.extend([m for m in self.validation_messages if m.level == "error"])
|
|
492
|
+
warnings.extend([m for m in self.validation_messages if m.level == "warning"])
|
|
493
|
+
|
|
494
|
+
return ValidationResult(
|
|
495
|
+
valid=len(errors) == 0,
|
|
496
|
+
errors=errors,
|
|
497
|
+
warnings=warnings,
|
|
498
|
+
registry=self.registry if len(errors) == 0 else None,
|
|
499
|
+
)
|