tactus 0.34.1__py3-none-any.whl → 0.35.0__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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/tool.py CHANGED
@@ -9,7 +9,7 @@ Provides:
9
9
  """
10
10
 
11
11
  import logging
12
- from typing import Any, Optional, Dict, List, TYPE_CHECKING
12
+ from typing import Any, Optional, TYPE_CHECKING
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from tactus.primitives.tool_handle import ToolHandle
@@ -20,13 +20,13 @@ logger = logging.getLogger(__name__)
20
20
  class ToolCall:
21
21
  """Represents a single tool call with arguments and result."""
22
22
 
23
- def __init__(self, name: str, args: Dict[str, Any], result: Any):
23
+ def __init__(self, name: str, args: dict[str, Any], result: Any):
24
24
  self.name = name
25
25
  self.args = args
26
26
  self.result = result
27
27
  self.timestamp = None # Could add timestamp tracking
28
28
 
29
- def to_dict(self) -> Dict[str, Any]:
29
+ def to_dict(self) -> dict[str, Any]:
30
30
  """Convert to dictionary for Lua access."""
31
31
  return {"name": self.name, "args": self.args, "result": self.result}
32
32
 
@@ -47,19 +47,22 @@ class ToolPrimitive:
47
47
  """
48
48
 
49
49
  def __init__(
50
- self, log_handler=None, agent_name: Optional[str] = None, procedure_id: Optional[str] = None
50
+ self,
51
+ log_handler=None,
52
+ agent_name: Optional[str] = None,
53
+ procedure_id: Optional[str] = None,
51
54
  ):
52
55
  """Initialize tool tracking."""
53
- self._tool_calls: List[ToolCall] = []
54
- self._last_calls: Dict[str, ToolCall] = {} # name -> last call
56
+ self._tool_calls: list[ToolCall] = []
57
+ self._last_calls: dict[str, ToolCall] = {} # name -> last call
55
58
  self.log_handler = log_handler
56
59
  self.agent_name = agent_name
57
60
  self.procedure_id = procedure_id
58
61
  self._runtime = None # Will be set by runtime for Tool.get() support
59
- self._tool_registry: Dict[str, "ToolHandle"] = {} # For Tool("name") lookup
62
+ self._tool_registry: dict[str, "ToolHandle"] = {} # For Tool("name") lookup
60
63
  logger.debug("ToolPrimitive initialized")
61
64
 
62
- def set_tool_registry(self, registry: Dict[str, "ToolHandle"]) -> None:
65
+ def set_tool_registry(self, registry: dict[str, "ToolHandle"]) -> None:
63
66
  """
64
67
  Set the tool registry for Tool("name") lookup.
65
68
 
@@ -69,7 +72,7 @@ class ToolPrimitive:
69
72
  registry: Dict mapping tool names to ToolHandle instances
70
73
  """
71
74
  self._tool_registry = registry
72
- logger.debug(f"ToolPrimitive tool registry set with {len(registry)} tools")
75
+ logger.debug("ToolPrimitive tool registry set with %s tools", len(registry))
73
76
 
74
77
  def __call__(self, tool_name: str) -> "ToolHandle":
75
78
  """
@@ -132,7 +135,7 @@ class ToolPrimitive:
132
135
  """
133
136
  from tactus.primitives.tool_handle import ToolHandle
134
137
 
135
- logger.debug(f"Tool.get('{tool_name}') called")
138
+ logger.debug("Tool.get('%s') called", tool_name)
136
139
 
137
140
  # Look up toolset from runtime registry
138
141
  toolset = self._get_toolset(tool_name)
@@ -143,10 +146,10 @@ class ToolPrimitive:
143
146
  )
144
147
 
145
148
  # Extract the callable function from the toolset
146
- tool_fn = self._extract_tool_function(toolset, tool_name)
149
+ tool_function = self._extract_tool_function(toolset, tool_name)
147
150
 
148
- logger.debug(f"Tool.get('{tool_name}') returning ToolHandle")
149
- return ToolHandle(tool_name, tool_fn, self)
151
+ logger.debug("Tool.get('%s') returning ToolHandle", tool_name)
152
+ return ToolHandle(tool_name, tool_function, self)
150
153
 
151
154
  def _get_toolset(self, name: str) -> Optional[Any]:
152
155
  """
@@ -226,7 +229,9 @@ class ToolPrimitive:
226
229
 
227
230
  # Fallback: assume the toolset itself contains tool functions
228
231
  logger.warning(
229
- f"Could not extract tool function for '{tool_name}' from toolset type {type(toolset)}"
232
+ "Could not extract tool function for '%s' from toolset type %s",
233
+ tool_name,
234
+ type(toolset),
230
235
  )
231
236
 
232
237
  # Return a wrapper that attempts to call through the toolset
@@ -252,9 +257,9 @@ class ToolPrimitive:
252
257
  Log.info("Done tool was called")
253
258
  end
254
259
  """
255
- called = tool_name in self._last_calls
256
- logger.debug(f"Tool.called('{tool_name}') = {called}")
257
- return called
260
+ was_called = tool_name in self._last_calls
261
+ logger.debug("Tool.called('%s') = %s", tool_name, was_called)
262
+ return was_called
258
263
 
259
264
  def last_result(self, tool_name: str) -> Any:
260
265
  """
@@ -273,14 +278,14 @@ class ToolPrimitive:
273
278
  end
274
279
  """
275
280
  if tool_name not in self._last_calls:
276
- logger.debug(f"Tool.last_result('{tool_name}') = None (never called)")
281
+ logger.debug("Tool.last_result('%s') = None (never called)", tool_name)
277
282
  return None
278
283
 
279
284
  result = self._last_calls[tool_name].result
280
- logger.debug(f"Tool.last_result('{tool_name}') = {result}")
285
+ logger.debug("Tool.last_result('%s') = %s", tool_name, result)
281
286
  return result
282
287
 
283
- def last_call(self, tool_name: str) -> Optional[Dict[str, Any]]:
288
+ def last_call(self, tool_name: str) -> Optional[dict[str, Any]]:
284
289
  """
285
290
  Get full information about the last call to a tool.
286
291
 
@@ -298,15 +303,19 @@ class ToolPrimitive:
298
303
  end
299
304
  """
300
305
  if tool_name not in self._last_calls:
301
- logger.debug(f"Tool.last_call('{tool_name}') = None (never called)")
306
+ logger.debug("Tool.last_call('%s') = None (never called)", tool_name)
302
307
  return None
303
308
 
304
309
  call_dict = self._last_calls[tool_name].to_dict()
305
- logger.debug(f"Tool.last_call('{tool_name}') = {call_dict}")
310
+ logger.debug("Tool.last_call('%s') = %s", tool_name, call_dict)
306
311
  return call_dict
307
312
 
308
313
  def record_call(
309
- self, tool_name: str, args: Dict[str, Any], result: Any, agent_name: Optional[str] = None
314
+ self,
315
+ tool_name: str,
316
+ args: dict[str, Any],
317
+ result: Any,
318
+ agent_name: Optional[str] = None,
310
319
  ) -> None:
311
320
  """
312
321
  Record a tool call (called by runtime after tool execution).
@@ -319,11 +328,15 @@ class ToolPrimitive:
319
328
 
320
329
  Note: This is called internally by the runtime, not from Lua
321
330
  """
322
- call = ToolCall(tool_name, args, result)
323
- self._tool_calls.append(call)
324
- self._last_calls[tool_name] = call
325
-
326
- logger.debug(f"Tool call recorded: {tool_name} -> {len(self._tool_calls)} total calls")
331
+ tool_call = ToolCall(tool_name, args, result)
332
+ self._tool_calls.append(tool_call)
333
+ self._last_calls[tool_name] = tool_call
334
+
335
+ logger.debug(
336
+ "Tool call recorded: %s -> %s total calls",
337
+ tool_name,
338
+ len(self._tool_calls),
339
+ )
327
340
 
328
341
  # Emit ToolCallEvent if we have a log handler
329
342
  if self.log_handler:
@@ -338,10 +351,10 @@ class ToolPrimitive:
338
351
  procedure_id=self.procedure_id,
339
352
  )
340
353
  self.log_handler.log(event)
341
- except Exception as e:
342
- logger.warning(f"Failed to log tool call event: {e}")
354
+ except Exception as error:
355
+ logger.warning("Failed to log tool call event: %s", error)
343
356
 
344
- def get_all_calls(self) -> List[ToolCall]:
357
+ def get_all_calls(self) -> list[ToolCall]:
345
358
  """
346
359
  Get all tool calls (for debugging/logging).
347
360
 
@@ -7,7 +7,9 @@ that can be invoked directly without going through an agent.
7
7
 
8
8
  import asyncio
9
9
  import logging
10
- from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
10
+ from typing import Any, Callable, Optional, TYPE_CHECKING
11
+
12
+ from tactus.utils.asyncio_helpers import clear_closed_event_loop
11
13
 
12
14
  if TYPE_CHECKING:
13
15
  from tactus.primitives.tool import ToolPrimitive
@@ -30,7 +32,7 @@ class ToolHandle:
30
32
  def __init__(
31
33
  self,
32
34
  name: str,
33
- impl_fn: Callable,
35
+ implementation_function: Callable,
34
36
  tool_primitive: Optional["ToolPrimitive"] = None,
35
37
  is_async: bool = False,
36
38
  record_calls: bool = True,
@@ -40,19 +42,34 @@ class ToolHandle:
40
42
 
41
43
  Args:
42
44
  name: Tool name for tracking/logging
43
- impl_fn: The actual function to execute
45
+ implementation_function: The actual function to execute
44
46
  tool_primitive: Optional ToolPrimitive for call recording
45
- is_async: Whether impl_fn is async (for MCP tools)
47
+ is_async: Whether the implementation function is async (for MCP tools)
46
48
  """
47
49
  self.name = name
48
- self.impl_fn = impl_fn
50
+ self.implementation_function = implementation_function
49
51
  self.tool_primitive = tool_primitive
50
52
  self.is_async = is_async
51
53
  self.record_calls = record_calls
52
54
 
53
- logger.debug(f"ToolHandle created for '{name}' (async={is_async})")
55
+ logger.debug("ToolHandle created for '%s' (async=%s)", name, is_async)
56
+
57
+ def _has_tool_primitive(self) -> bool:
58
+ return self.tool_primitive is not None
59
+
60
+ def _normalize_tool_arguments(self, tool_arguments: Any) -> Any:
61
+ """
62
+ Convert a Lua table or mapping-like input to a plain Python dict when possible.
63
+ """
64
+ if tool_arguments is None:
65
+ return {}
66
+
67
+ if hasattr(tool_arguments, "items"):
68
+ return self._lua_table_to_dict(tool_arguments)
69
+
70
+ return tool_arguments
54
71
 
55
- def call(self, args: Dict[str, Any]) -> Any:
72
+ def call(self, args: dict[str, Any]) -> Any:
56
73
  """
57
74
  Execute the tool with given arguments.
58
75
 
@@ -65,31 +82,35 @@ class ToolHandle:
65
82
  Example (Lua):
66
83
  local result = my_tool:call({arg1 = "value"})
67
84
  """
68
- logger.debug(f"ToolHandle.call('{self.name}') with args: {args}")
85
+ logger.debug("ToolHandle.call('%s') with args: %s", self.name, args)
69
86
 
70
87
  try:
71
88
  # Convert Lua table to Python dict if needed
72
- if hasattr(args, "items"):
73
- args = self._lua_table_to_dict(args)
89
+ normalized_arguments = self._normalize_tool_arguments(args)
74
90
 
75
91
  # Execute the implementation
76
- if self.is_async or asyncio.iscoroutinefunction(self.impl_fn):
77
- result = self._run_async(args)
92
+ if self.is_async or asyncio.iscoroutinefunction(self.implementation_function):
93
+ result = self._run_async(normalized_arguments)
78
94
  else:
79
- result = self.impl_fn(args)
95
+ result = self.implementation_function(normalized_arguments)
80
96
 
81
97
  # Record the call for tracking
82
98
  if self.tool_primitive and self.record_calls:
83
- self.tool_primitive.record_call(self.name, args, result)
99
+ self.tool_primitive.record_call(self.name, normalized_arguments, result)
84
100
 
85
- logger.debug(f"ToolHandle.call('{self.name}') returned: {result}")
101
+ logger.debug("ToolHandle.call('%s') returned: %s", self.name, result)
86
102
  return result
87
103
 
88
- except Exception as e:
89
- logger.error(f"ToolHandle.call('{self.name}') failed: {e}", exc_info=True)
104
+ except Exception as error:
105
+ logger.error(
106
+ "ToolHandle.call('%s') failed: %s",
107
+ self.name,
108
+ error,
109
+ exc_info=True,
110
+ )
90
111
  raise
91
112
 
92
- def __call__(self, args: Dict[str, Any]) -> Any:
113
+ def __call__(self, args: dict[str, Any]) -> Any:
93
114
  """
94
115
  Make handle callable for shorthand syntax.
95
116
 
@@ -110,15 +131,15 @@ class ToolHandle:
110
131
  Log.info("Task completed!")
111
132
  end
112
133
  """
113
- if not self.tool_primitive:
114
- logger.warning(f"ToolHandle.called('{self.name}'): No tool_primitive attached")
134
+ if not self._has_tool_primitive():
135
+ logger.warning("ToolHandle.called('%s'): No tool_primitive attached", self.name)
115
136
  return False
116
137
 
117
138
  result = self.tool_primitive.called(self.name)
118
- logger.debug(f"ToolHandle.called('{self.name}') = {result}")
139
+ logger.debug("ToolHandle.called('%s') = %s", self.name, result)
119
140
  return result
120
141
 
121
- def last_call(self) -> Optional[Dict[str, Any]]:
142
+ def last_call(self) -> Optional[dict[str, Any]]:
122
143
  """
123
144
  Get the last call record for this tool.
124
145
 
@@ -131,12 +152,12 @@ class ToolHandle:
131
152
  Log.info("Last multiply: " .. call.args.a .. " * " .. call.args.b)
132
153
  end
133
154
  """
134
- if not self.tool_primitive:
135
- logger.warning(f"ToolHandle.last_call('{self.name}'): No tool_primitive attached")
155
+ if not self._has_tool_primitive():
156
+ logger.warning("ToolHandle.last_call('%s'): No tool_primitive attached", self.name)
136
157
  return None
137
158
 
138
159
  result = self.tool_primitive.last_call(self.name)
139
- logger.debug(f"ToolHandle.last_call('{self.name}') = {result}")
160
+ logger.debug("ToolHandle.last_call('%s') = %s", self.name, result)
140
161
  return result
141
162
 
142
163
  def last_result(self) -> Any:
@@ -150,12 +171,12 @@ class ToolHandle:
150
171
  local answer = done.last_result()
151
172
  return { result = answer }
152
173
  """
153
- if not self.tool_primitive:
154
- logger.warning(f"ToolHandle.last_result('{self.name}'): No tool_primitive attached")
174
+ if not self._has_tool_primitive():
175
+ logger.warning("ToolHandle.last_result('%s'): No tool_primitive attached", self.name)
155
176
  return None
156
177
 
157
178
  result = self.tool_primitive.last_result(self.name)
158
- logger.debug(f"ToolHandle.last_result('{self.name}') = {result}")
179
+ logger.debug("ToolHandle.last_result('%s') = %s", self.name, result)
159
180
  return result
160
181
 
161
182
  def call_count(self) -> int:
@@ -169,13 +190,13 @@ class ToolHandle:
169
190
  local count = multiply.call_count()
170
191
  Log.info("Multiply was called " .. count .. " times")
171
192
  """
172
- if not self.tool_primitive:
173
- logger.warning(f"ToolHandle.call_count('{self.name}'): No tool_primitive attached")
193
+ if not self._has_tool_primitive():
194
+ logger.warning("ToolHandle.call_count('%s'): No tool_primitive attached", self.name)
174
195
  return 0
175
196
 
176
197
  # Count all calls with this tool name
177
198
  count = sum(1 for call in self.tool_primitive._tool_calls if call.name == self.name)
178
- logger.debug(f"ToolHandle.call_count('{self.name}') = {count}")
199
+ logger.debug("ToolHandle.call_count('%s') = %s", self.name, count)
179
200
  return count
180
201
 
181
202
  def reset(self) -> None:
@@ -202,17 +223,17 @@ class ToolHandle:
202
223
  Log.info("Agent 2 completed")
203
224
  end
204
225
  """
205
- if not self.tool_primitive:
206
- logger.warning(f"ToolHandle.reset('{self.name}'): No tool_primitive attached")
226
+ if not self._has_tool_primitive():
227
+ logger.warning("ToolHandle.reset('%s'): No tool_primitive attached", self.name)
207
228
  return
208
229
 
209
230
  # Remove all calls for this tool
210
231
  self.tool_primitive._tool_calls = [
211
232
  call for call in self.tool_primitive._tool_calls if call.name != self.name
212
233
  ]
213
- logger.debug(f"ToolHandle.reset('{self.name}'): Cleared all call records")
234
+ logger.debug("ToolHandle.reset('%s'): Cleared all call records", self.name)
214
235
 
215
- def _run_async(self, args: Dict[str, Any]) -> Any:
236
+ def _run_async(self, args: dict[str, Any]) -> Any:
216
237
  """
217
238
  Run async function from sync context.
218
239
 
@@ -220,46 +241,47 @@ class ToolHandle:
220
241
  """
221
242
  try:
222
243
  # Try to get a running event loop
223
- loop = asyncio.get_running_loop()
244
+ running_loop = asyncio.get_running_loop()
224
245
 
225
246
  # We're in an async context - use nest_asyncio if available
226
247
  try:
227
248
  import nest_asyncio
228
249
 
229
- nest_asyncio.apply(loop)
230
- return asyncio.run(self.impl_fn(args))
250
+ nest_asyncio.apply(running_loop)
251
+ return asyncio.run(self.implementation_function(args))
231
252
  except ImportError:
232
253
  # nest_asyncio not available, fall back to threading
233
254
  import threading
234
255
 
235
- result_container = {"value": None, "exception": None}
256
+ async_result = {"value": None, "exception": None}
236
257
 
237
258
  def run_in_thread():
238
259
  try:
239
- new_loop = asyncio.new_event_loop()
240
- asyncio.set_event_loop(new_loop)
260
+ thread_event_loop = asyncio.new_event_loop()
261
+ asyncio.set_event_loop(thread_event_loop)
241
262
  try:
242
- result_container["value"] = new_loop.run_until_complete(
243
- self.impl_fn(args)
263
+ async_result["value"] = thread_event_loop.run_until_complete(
264
+ self.implementation_function(args)
244
265
  )
245
266
  finally:
246
- new_loop.close()
247
- except Exception as e:
248
- result_container["exception"] = e
267
+ thread_event_loop.close()
268
+ except Exception as error:
269
+ async_result["exception"] = error
249
270
 
250
- thread = threading.Thread(target=run_in_thread)
251
- thread.start()
252
- thread.join()
271
+ worker_thread = threading.Thread(target=run_in_thread)
272
+ worker_thread.start()
273
+ worker_thread.join()
253
274
 
254
- if result_container["exception"]:
255
- raise result_container["exception"]
256
- return result_container["value"]
275
+ if async_result["exception"]:
276
+ raise async_result["exception"]
277
+ return async_result["value"]
257
278
 
258
279
  except RuntimeError:
259
280
  # No event loop running - safe to use asyncio.run()
260
- return asyncio.run(self.impl_fn(args))
281
+ clear_closed_event_loop()
282
+ return asyncio.run(self.implementation_function(args))
261
283
 
262
- def _lua_table_to_dict(self, lua_table) -> Dict[str, Any]:
284
+ def _lua_table_to_dict(self, lua_table: Any) -> Any:
263
285
  """Convert a Lua table to Python dict recursively."""
264
286
  if lua_table is None:
265
287
  return {}
@@ -5,7 +5,7 @@ Provides first-class support for Pydantic AI's composable toolset architecture.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Any, Dict, Callable
8
+ from typing import Any, Callable
9
9
  from pydantic_ai.toolsets import AbstractToolset, CombinedToolset, FilteredToolset
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -36,7 +36,7 @@ class ToolsetPrimitive:
36
36
  self.definitions = {} # name -> toolset config (from DSL)
37
37
  logger.debug("ToolsetPrimitive initialized")
38
38
 
39
- def define(self, name: str, config: Dict[str, Any]) -> None:
39
+ def define(self, name: str, config: dict[str, Any]) -> None:
40
40
  """
41
41
  Register a toolset definition from the DSL.
42
42
 
@@ -61,7 +61,11 @@ class ToolsetPrimitive:
61
61
  }
62
62
  """
63
63
  self.definitions[name] = config
64
- logger.info(f"Defined toolset '{name}' of type '{config.get('type')}'")
64
+ logger.info(
65
+ "Defined toolset '%s' of type '%s'",
66
+ name,
67
+ config.get("type"),
68
+ )
65
69
 
66
70
  def get(self, name: str) -> AbstractToolset:
67
71
  """
@@ -80,9 +84,9 @@ class ToolsetPrimitive:
80
84
  ValueError: If toolset not found
81
85
  """
82
86
  # Try to resolve from runtime first (config-defined toolsets)
83
- toolset = self.runtime.resolve_toolset(name)
84
- if toolset:
85
- return toolset
87
+ toolset_from_runtime = self.runtime.resolve_toolset(name)
88
+ if toolset_from_runtime:
89
+ return toolset_from_runtime
86
90
 
87
91
  # Try DSL definitions
88
92
  if name in self.definitions:
@@ -101,7 +105,7 @@ class ToolsetPrimitive:
101
105
  CombinedToolset containing all input toolsets
102
106
  """
103
107
  toolset_list = list(toolsets)
104
- logger.debug(f"Combining {len(toolset_list)} toolsets")
108
+ logger.debug("Combining %s toolsets", len(toolset_list))
105
109
  return CombinedToolset(toolset_list)
106
110
 
107
111
  def filter(self, toolset: AbstractToolset, predicate: Callable[[str], bool]) -> FilteredToolset:
@@ -123,14 +127,14 @@ class ToolsetPrimitive:
123
127
 
124
128
  # Wrap Lua function for Pydantic AI's filter API
125
129
  # Pydantic AI's filtered() expects: lambda ctx, tool: bool
126
- def pydantic_filter(ctx, tool):
130
+ def pydantic_filter(_context, tool):
127
131
  # Call Lua predicate with just the tool name
128
132
  return predicate(tool.name)
129
133
 
130
134
  logger.debug("Creating filtered toolset")
131
135
  return toolset.filtered(pydantic_filter)
132
136
 
133
- def _create_toolset_from_definition(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
137
+ def _create_toolset_from_definition(self, name: str, config: dict[str, Any]) -> AbstractToolset:
134
138
  """
135
139
  Create a toolset from a DSL definition.
136
140
 
@@ -148,16 +152,15 @@ class ToolsetPrimitive:
148
152
 
149
153
  if toolset_type == "plugin":
150
154
  return self._create_plugin_toolset(name, config)
151
- elif toolset_type == "mcp":
155
+ if toolset_type == "mcp":
152
156
  return self._create_mcp_toolset_reference(name, config)
153
- elif toolset_type == "combined":
157
+ if toolset_type == "combined":
154
158
  return self._create_combined_toolset(name, config)
155
- elif toolset_type == "filtered":
159
+ if toolset_type == "filtered":
156
160
  return self._create_filtered_toolset(name, config)
157
- else:
158
- raise ValueError(f"Unknown toolset type: {toolset_type}")
161
+ raise ValueError(f"Unknown toolset type: {toolset_type}")
159
162
 
160
- def _create_plugin_toolset(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
163
+ def _create_plugin_toolset(self, name: str, config: dict[str, Any]) -> AbstractToolset:
161
164
  """Create a plugin toolset from paths."""
162
165
  from tactus.adapters.plugins import PluginLoader
163
166
 
@@ -169,7 +172,7 @@ class ToolsetPrimitive:
169
172
  toolset = loader.create_toolset(paths, name=name)
170
173
  return toolset
171
174
 
172
- def _create_mcp_toolset_reference(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
175
+ def _create_mcp_toolset_reference(self, name: str, config: dict[str, Any]) -> AbstractToolset:
173
176
  """Get reference to an MCP toolset."""
174
177
  server_name = config.get("server")
175
178
  if not server_name:
@@ -183,12 +186,12 @@ class ToolsetPrimitive:
183
186
  # Get the toolset by server name
184
187
  toolset = self.runtime.mcp_manager.get_toolset_by_name(server_name)
185
188
  if toolset:
186
- logger.info(f"Found MCP toolset for server '{server_name}'")
189
+ logger.info("Found MCP toolset for server '%s'", server_name)
187
190
  return toolset
188
191
 
189
192
  raise ValueError(f"MCP server toolset '{server_name}' not found")
190
193
 
191
- def _create_combined_toolset(self, name: str, config: Dict[str, Any]) -> CombinedToolset:
194
+ def _create_combined_toolset(self, name: str, config: dict[str, Any]) -> CombinedToolset:
192
195
  """Create a combined toolset from sources."""
193
196
  sources = config.get("sources", [])
194
197
  if not sources:
@@ -197,12 +200,12 @@ class ToolsetPrimitive:
197
200
  # Resolve each source toolset
198
201
  resolved_toolsets = []
199
202
  for source_name in sources:
200
- toolset = self.get(source_name)
201
- resolved_toolsets.append(toolset)
203
+ resolved_toolset = self.get(source_name)
204
+ resolved_toolsets.append(resolved_toolset)
202
205
 
203
206
  return CombinedToolset(resolved_toolsets)
204
207
 
205
- def _create_filtered_toolset(self, name: str, config: Dict[str, Any]) -> FilteredToolset:
208
+ def _create_filtered_toolset(self, name: str, config: dict[str, Any]) -> FilteredToolset:
206
209
  """Create a filtered toolset."""
207
210
  source = config.get("source")
208
211
  filter_pattern = config.get("filter")
@@ -220,7 +223,7 @@ class ToolsetPrimitive:
220
223
 
221
224
  pattern = re.compile(filter_pattern)
222
225
 
223
- def filter_func(ctx, tool):
226
+ def filter_func(_context, tool):
224
227
  return pattern.match(tool.name) is not None
225
228
 
226
229
  return source_toolset.filtered(filter_func)
tactus/sandbox/config.py CHANGED
@@ -5,7 +5,7 @@ Defines the SandboxConfig Pydantic model for controlling container execution.
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
- from typing import Dict, List, Optional
8
+ from typing import Optional
9
9
 
10
10
  from pydantic import BaseModel, Field, model_validator
11
11
 
@@ -47,7 +47,7 @@ class SandboxConfig(BaseModel):
47
47
  )
48
48
 
49
49
  # Additional environment variables to pass to container
50
- env: Dict[str, str] = Field(
50
+ env: dict[str, str] = Field(
51
51
  default_factory=dict,
52
52
  description="Additional environment variables to pass to the container",
53
53
  )
@@ -59,7 +59,7 @@ class SandboxConfig(BaseModel):
59
59
  )
60
60
 
61
61
  # Additional volume mounts
62
- volumes: List[str] = Field(
62
+ volumes: list[str] = Field(
63
63
  default_factory=list,
64
64
  description="Additional volume mounts in 'host:container:mode' format",
65
65
  )
@@ -157,7 +157,7 @@ class SandboxConfig(BaseModel):
157
157
  model_config = {"arbitrary_types_allowed": True}
158
158
 
159
159
  @model_validator(mode="after")
160
- def add_default_volumes(self):
160
+ def add_default_volumes(self) -> "SandboxConfig":
161
161
  """Add default volume mounts based on config flags."""
162
162
  if self.mount_current_dir:
163
163
  # Insert at beginning so user volumes can override