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
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DSL Handles - Lightweight placeholders returned by DSL declarations.
|
|
3
|
+
|
|
4
|
+
These handles are created during DSL parsing (before actual primitives exist)
|
|
5
|
+
and get connected to their real implementations at runtime via _enhance_handles().
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# During parsing:
|
|
9
|
+
Greeter = agent "greeter" { config } # Returns AgentHandle("greeter")
|
|
10
|
+
|
|
11
|
+
# During execution (callable syntax - preferred):
|
|
12
|
+
Greeter() # Direct call
|
|
13
|
+
Greeter({message = "Hello"}) # Call with options
|
|
14
|
+
|
|
15
|
+
# Lookup syntax also works:
|
|
16
|
+
Agent("greeter")() # Lookup + call
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Optional, Dict, TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from tactus.dspy.agent import DSPyAgentHandle
|
|
24
|
+
from tactus.primitives.model import ModelPrimitive
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _convert_lua_table(lua_table):
|
|
30
|
+
"""
|
|
31
|
+
Convert a lupa Lua table to a Python dict or list.
|
|
32
|
+
|
|
33
|
+
Used to convert opts passed from Lua to Python methods.
|
|
34
|
+
"""
|
|
35
|
+
if lua_table is None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Check if it's a lupa table by checking for the items method
|
|
39
|
+
if not hasattr(lua_table, "items"):
|
|
40
|
+
# It's a primitive value (string, number, bool), return as-is
|
|
41
|
+
return lua_table
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Get all keys
|
|
45
|
+
keys = list(lua_table.keys())
|
|
46
|
+
|
|
47
|
+
# Empty table - return empty dict for opts (different from dsl_stubs which returns [])
|
|
48
|
+
if not keys:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
# Check if it's an array (all keys are consecutive integers starting from 1)
|
|
52
|
+
if all(isinstance(k, int) for k in keys):
|
|
53
|
+
sorted_keys = sorted(keys)
|
|
54
|
+
if sorted_keys == list(range(1, len(keys) + 1)):
|
|
55
|
+
# It's an array
|
|
56
|
+
return [_convert_lua_table(lua_table[k]) for k in sorted_keys]
|
|
57
|
+
|
|
58
|
+
# It's a dictionary
|
|
59
|
+
result = {}
|
|
60
|
+
for key, value in lua_table.items():
|
|
61
|
+
# Recursively convert nested tables
|
|
62
|
+
result[key] = _convert_lua_table(value)
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
except (AttributeError, TypeError):
|
|
66
|
+
# Fallback: return as-is
|
|
67
|
+
return lua_table
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AgentHandle:
|
|
71
|
+
"""
|
|
72
|
+
Lightweight handle returned by agent() DSL function.
|
|
73
|
+
|
|
74
|
+
Created during DSL parsing, enhanced at runtime with actual AgentPrimitive.
|
|
75
|
+
Supports callable syntax: agent() or agent({message = "..."})
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, name: str):
|
|
79
|
+
"""
|
|
80
|
+
Initialize agent handle.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Agent name (string identifier)
|
|
84
|
+
"""
|
|
85
|
+
self.name = name
|
|
86
|
+
self._primitive: Optional["DSPyAgentHandle"] = None
|
|
87
|
+
self._execution_context: Optional[Any] = None
|
|
88
|
+
logger.debug(f"AgentHandle created for '{name}'")
|
|
89
|
+
|
|
90
|
+
def __call__(self, inputs=None):
|
|
91
|
+
"""
|
|
92
|
+
Execute an agent turn using the callable interface.
|
|
93
|
+
|
|
94
|
+
This is the unified callable interface that allows:
|
|
95
|
+
result = worker({message = "Hello"})
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
inputs: Input dict with fields matching input_schema.
|
|
99
|
+
Default field 'message' is used as the user message.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Result object with response and other fields
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
RuntimeError: If handle not connected to primitive
|
|
106
|
+
|
|
107
|
+
Example (Lua):
|
|
108
|
+
result = worker({message = "Process this task"})
|
|
109
|
+
print(result.response)
|
|
110
|
+
"""
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"[CHECKPOINT] AgentHandle '{self.name}'.__call__ invoked, _primitive={self._primitive is not None}, _execution_context={self._execution_context is not None}"
|
|
113
|
+
)
|
|
114
|
+
if self._primitive is None:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
f"Agent '{self.name}' initialization failed.\n"
|
|
117
|
+
f"This should not happen with immediate agent creation.\n"
|
|
118
|
+
f"Please report this as a bug with a minimal reproduction example."
|
|
119
|
+
)
|
|
120
|
+
# Convert Lua table to Python dict if needed
|
|
121
|
+
converted_inputs = _convert_lua_table(inputs) if inputs is not None else None
|
|
122
|
+
|
|
123
|
+
# Convenience: allow shorthand string calls in Lua:
|
|
124
|
+
# World("Hello") == World({message = "Hello"})
|
|
125
|
+
if isinstance(converted_inputs, str):
|
|
126
|
+
converted_inputs = {"message": converted_inputs}
|
|
127
|
+
|
|
128
|
+
# If we have an execution context, checkpoint the agent call
|
|
129
|
+
logger.debug(
|
|
130
|
+
f"[CHECKPOINT] AgentHandle '{self.name}' called, has_execution_context={self._execution_context is not None}"
|
|
131
|
+
)
|
|
132
|
+
if self._execution_context is not None:
|
|
133
|
+
|
|
134
|
+
def agent_call():
|
|
135
|
+
return self._primitive(converted_inputs)
|
|
136
|
+
|
|
137
|
+
# Capture source location from Lua if available
|
|
138
|
+
source_info = None
|
|
139
|
+
if (
|
|
140
|
+
hasattr(self._execution_context, "lua_sandbox")
|
|
141
|
+
and self._execution_context.lua_sandbox
|
|
142
|
+
):
|
|
143
|
+
try:
|
|
144
|
+
lua = self._execution_context.lua_sandbox.lua
|
|
145
|
+
info = lua.eval("debug.getinfo(2, 'Sl')")
|
|
146
|
+
if info:
|
|
147
|
+
source_info = {
|
|
148
|
+
"file": info.get("source", "unknown"),
|
|
149
|
+
"line": info.get("currentline", 0),
|
|
150
|
+
}
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.debug(f"Could not capture source location: {e}")
|
|
153
|
+
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"[CHECKPOINT] Creating checkpoint for agent '{self.name}', type=agent_turn, source_info={source_info}"
|
|
156
|
+
)
|
|
157
|
+
result = self._execution_context.checkpoint(
|
|
158
|
+
agent_call, checkpoint_type="agent_turn", source_info=source_info
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
# No execution context - call directly without checkpointing
|
|
162
|
+
result = self._primitive(converted_inputs)
|
|
163
|
+
|
|
164
|
+
# Convenience: expose the last agent output on the handle as `.output`
|
|
165
|
+
# for Lua patterns like `agent(); return agent.output`.
|
|
166
|
+
output_text = None
|
|
167
|
+
if result is not None:
|
|
168
|
+
for attr in ("response", "message"):
|
|
169
|
+
try:
|
|
170
|
+
value = getattr(result, attr, None)
|
|
171
|
+
except Exception:
|
|
172
|
+
value = None
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
output_text = value
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
if output_text is None and isinstance(result, dict):
|
|
178
|
+
for key in ("response", "message"):
|
|
179
|
+
value = result.get(key)
|
|
180
|
+
if isinstance(value, str):
|
|
181
|
+
output_text = value
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
if output_text is None:
|
|
185
|
+
output_text = str(result)
|
|
186
|
+
|
|
187
|
+
self.output = output_text
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def _set_primitive(
|
|
191
|
+
self, primitive: "DSPyAgentHandle", execution_context: Optional[Any] = None
|
|
192
|
+
) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Connect this handle to its actual primitive and execution context.
|
|
195
|
+
|
|
196
|
+
Called by runtime._enhance_handles() after primitives are created.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
primitive: The DSPyAgentHandle to delegate to
|
|
200
|
+
execution_context: Optional execution context for checkpointing
|
|
201
|
+
"""
|
|
202
|
+
self._primitive = primitive
|
|
203
|
+
self._execution_context = execution_context
|
|
204
|
+
logger.debug(
|
|
205
|
+
f"[CHECKPOINT] AgentHandle '{self.name}' connected to primitive (checkpointing={'enabled' if execution_context else 'disabled'}, execution_context={execution_context})"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def __repr__(self) -> str:
|
|
209
|
+
connected = "connected" if self._primitive else "disconnected"
|
|
210
|
+
return f"AgentHandle('{self.name}', {connected})"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ModelHandle:
|
|
214
|
+
"""
|
|
215
|
+
Lightweight handle returned by model() DSL function.
|
|
216
|
+
|
|
217
|
+
Created during DSL parsing, enhanced at runtime with actual ModelPrimitive.
|
|
218
|
+
Delegates .predict() calls to the real primitive.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, name: str):
|
|
222
|
+
"""
|
|
223
|
+
Initialize model handle.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
name: Model name (string identifier)
|
|
227
|
+
"""
|
|
228
|
+
self.name = name
|
|
229
|
+
self._primitive: Optional["ModelPrimitive"] = None
|
|
230
|
+
logger.debug(f"ModelHandle created for '{name}'")
|
|
231
|
+
|
|
232
|
+
def predict(self, data: Any) -> Any:
|
|
233
|
+
"""
|
|
234
|
+
Run model prediction (delegates to ModelPrimitive.predict()).
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
data: Input data for prediction
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Prediction result
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
RuntimeError: If handle not connected to primitive
|
|
244
|
+
"""
|
|
245
|
+
if self._primitive is None:
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
f"Model '{self.name}' initialization failed.\n"
|
|
248
|
+
f"This should not happen - please report this as a bug."
|
|
249
|
+
)
|
|
250
|
+
converted_data = _convert_lua_table(data) if data is not None else None
|
|
251
|
+
return self._primitive.predict(converted_data)
|
|
252
|
+
|
|
253
|
+
def __call__(self, data: Any = None) -> Any:
|
|
254
|
+
"""
|
|
255
|
+
Execute model prediction using the callable interface.
|
|
256
|
+
|
|
257
|
+
This is the unified callable interface that allows:
|
|
258
|
+
result = classifier({text = "Hello"})
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
data: Input data for prediction (format depends on model type)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Model prediction result
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
RuntimeError: If handle not connected to primitive
|
|
268
|
+
|
|
269
|
+
Example (Lua):
|
|
270
|
+
result = classifier({text = "This is great!"})
|
|
271
|
+
print(result.label) -- "positive"
|
|
272
|
+
"""
|
|
273
|
+
if self._primitive is None:
|
|
274
|
+
raise RuntimeError(
|
|
275
|
+
f"Model '{self.name}' initialization failed.\n"
|
|
276
|
+
f"This should not happen - please report this as a bug."
|
|
277
|
+
)
|
|
278
|
+
# Convert Lua table to Python dict if needed
|
|
279
|
+
converted_data = _convert_lua_table(data) if data is not None else None
|
|
280
|
+
return self._primitive(converted_data)
|
|
281
|
+
|
|
282
|
+
def _set_primitive(self, primitive: "ModelPrimitive") -> None:
|
|
283
|
+
"""
|
|
284
|
+
Connect this handle to its actual primitive.
|
|
285
|
+
|
|
286
|
+
Called by runtime._enhance_handles() after primitives are created.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
primitive: The ModelPrimitive to delegate to
|
|
290
|
+
"""
|
|
291
|
+
self._primitive = primitive
|
|
292
|
+
logger.debug(f"ModelHandle '{self.name}' connected to primitive")
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
connected = "connected" if self._primitive else "disconnected"
|
|
296
|
+
return f"ModelHandle('{self.name}', {connected})"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class AgentLookup:
|
|
300
|
+
"""
|
|
301
|
+
Agent lookup primitive - provides Agent("name") lookup functionality.
|
|
302
|
+
|
|
303
|
+
Injected into Lua as 'Agent'. Callable to look up agents by name.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(self, registry: Dict[str, AgentHandle]):
|
|
307
|
+
"""
|
|
308
|
+
Initialize with reference to the agent registry.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
registry: Dict mapping agent names to AgentHandle instances
|
|
312
|
+
"""
|
|
313
|
+
self._registry = registry
|
|
314
|
+
|
|
315
|
+
def __call__(self, name: str) -> AgentHandle:
|
|
316
|
+
"""
|
|
317
|
+
Look up an agent by name.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
name: Agent name to look up
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
AgentHandle for the named agent
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
ValueError: If agent not found
|
|
327
|
+
|
|
328
|
+
Example (Lua):
|
|
329
|
+
Agent("greeter")() -- or Agent("greeter")({message = "Hello"})
|
|
330
|
+
"""
|
|
331
|
+
if name not in self._registry:
|
|
332
|
+
available = list(self._registry.keys())
|
|
333
|
+
raise ValueError(f"Agent '{name}' not defined. Available agents: {available}")
|
|
334
|
+
return self._registry[name]
|
|
335
|
+
|
|
336
|
+
def __repr__(self) -> str:
|
|
337
|
+
return f"AgentLookup({len(self._registry)} agents)"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ModelLookup:
|
|
341
|
+
"""
|
|
342
|
+
Model lookup primitive - provides Model("name") lookup functionality.
|
|
343
|
+
|
|
344
|
+
Injected into Lua as 'Model'. Callable to look up models by name.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def __init__(self, registry: Dict[str, ModelHandle]):
|
|
348
|
+
"""
|
|
349
|
+
Initialize with reference to the model registry.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
registry: Dict mapping model names to ModelHandle instances
|
|
353
|
+
"""
|
|
354
|
+
self._registry = registry
|
|
355
|
+
|
|
356
|
+
def __call__(self, name: str) -> ModelHandle:
|
|
357
|
+
"""
|
|
358
|
+
Look up a model by name.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
name: Model name to look up
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
ModelHandle for the named model
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
ValueError: If model not found
|
|
368
|
+
|
|
369
|
+
Example (Lua):
|
|
370
|
+
Model("classifier").predict(data)
|
|
371
|
+
"""
|
|
372
|
+
if name not in self._registry:
|
|
373
|
+
available = list(self._registry.keys())
|
|
374
|
+
raise ValueError(f"Model '{name}' not defined. Available models: {available}")
|
|
375
|
+
return self._registry[name]
|
|
376
|
+
|
|
377
|
+
def __repr__(self) -> str:
|
|
378
|
+
return f"ModelLookup({len(self._registry)} models)"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Host Primitive - brokered host capabilities for the runtime container.
|
|
3
|
+
|
|
4
|
+
This primitive is intended to be used inside the sandboxed runtime container.
|
|
5
|
+
It delegates allowlisted operations to the trusted host-side broker via the
|
|
6
|
+
`TACTUS_BROKER_SOCKET` transport.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from tactus.broker.client import BrokerClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HostPrimitive:
|
|
18
|
+
"""Provides access to allowlisted host-side tools via the broker."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: Optional[BrokerClient] = None):
|
|
21
|
+
self._client = client or BrokerClient.from_environment()
|
|
22
|
+
self._registry = None
|
|
23
|
+
if self._client is None:
|
|
24
|
+
# Allow Host.call() to work in non-sandboxed runs (and in deterministic tests)
|
|
25
|
+
# without requiring a broker transport, while still staying deny-by-default.
|
|
26
|
+
from tactus.broker.server import HostToolRegistry
|
|
27
|
+
|
|
28
|
+
self._registry = HostToolRegistry.default()
|
|
29
|
+
|
|
30
|
+
def _run_coro(self, coro):
|
|
31
|
+
"""
|
|
32
|
+
Run an async coroutine from Lua's synchronous context.
|
|
33
|
+
|
|
34
|
+
Mirrors the approach used by `ToolHandle` for async tool handlers.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
asyncio.get_running_loop()
|
|
38
|
+
|
|
39
|
+
import threading
|
|
40
|
+
|
|
41
|
+
result_container = {"value": None, "exception": None}
|
|
42
|
+
|
|
43
|
+
def run_in_thread():
|
|
44
|
+
try:
|
|
45
|
+
result_container["value"] = asyncio.run(coro)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
result_container["exception"] = e
|
|
48
|
+
|
|
49
|
+
thread = threading.Thread(target=run_in_thread)
|
|
50
|
+
thread.start()
|
|
51
|
+
thread.join()
|
|
52
|
+
|
|
53
|
+
if result_container["exception"]:
|
|
54
|
+
raise result_container["exception"]
|
|
55
|
+
return result_container["value"]
|
|
56
|
+
|
|
57
|
+
except RuntimeError:
|
|
58
|
+
return asyncio.run(coro)
|
|
59
|
+
|
|
60
|
+
def _lua_to_python(self, obj: Any) -> Any:
|
|
61
|
+
if obj is None:
|
|
62
|
+
return None
|
|
63
|
+
if hasattr(obj, "items") and not isinstance(obj, dict):
|
|
64
|
+
return {k: self._lua_to_python(v) for k, v in obj.items()}
|
|
65
|
+
if isinstance(obj, dict):
|
|
66
|
+
return {k: self._lua_to_python(v) for k, v in obj.items()}
|
|
67
|
+
if isinstance(obj, (list, tuple)):
|
|
68
|
+
return [self._lua_to_python(v) for v in obj]
|
|
69
|
+
return obj
|
|
70
|
+
|
|
71
|
+
def call(self, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Call an allowlisted host tool via the broker.
|
|
74
|
+
|
|
75
|
+
Example (Lua):
|
|
76
|
+
local result = Host.call("host.ping", {value = 1})
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(name, str) or not name:
|
|
79
|
+
raise ValueError("Host.call requires a non-empty tool name string")
|
|
80
|
+
|
|
81
|
+
args_dict = self._lua_to_python(args) or {}
|
|
82
|
+
if not isinstance(args_dict, dict):
|
|
83
|
+
raise ValueError("Host.call args must be an object/table")
|
|
84
|
+
|
|
85
|
+
if self._client is not None:
|
|
86
|
+
return self._run_coro(self._client.call_tool(name=name, args=args_dict))
|
|
87
|
+
|
|
88
|
+
if self._registry is not None:
|
|
89
|
+
try:
|
|
90
|
+
return self._registry.call(name, args_dict)
|
|
91
|
+
except KeyError as e:
|
|
92
|
+
raise RuntimeError(f"Tool not allowlisted: {name}") from e
|
|
93
|
+
|
|
94
|
+
raise RuntimeError("Host.call requires TACTUS_BROKER_SOCKET to be set")
|