tactus 0.38.0__py3-none-any.whl → 0.39.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.
tactus/__init__.py CHANGED
@@ -5,7 +5,7 @@ Tactus provides a declarative workflow engine for AI agents with pluggable
5
5
  backends for storage, HITL, and chat recording.
6
6
  """
7
7
 
8
- __version__ = "0.38.0"
8
+ __version__ = "0.39.0"
9
9
 
10
10
  # Core exports
11
11
  from tactus.core.runtime import TactusRuntime
tactus/core/dsl_stubs.py CHANGED
@@ -413,6 +413,17 @@ def create_dsl_stubs(
413
413
  # Task "name" { ... }
414
414
  if isinstance(name_or_config, str):
415
415
  task_name = name_or_config
416
+ if config is None:
417
+
418
+ def _curried(task_config=None):
419
+ if task_config is None or not hasattr(task_config, "items"):
420
+ raise TypeError(
421
+ f"Task '{task_name}' requires a configuration table. "
422
+ 'Use: Task "name" { ... } or name = Task { ... }.'
423
+ )
424
+ return _task(task_name, task_config)
425
+
426
+ return _curried
416
427
  task_config = config or {}
417
428
  if hasattr(task_config, "__setitem__"):
418
429
  try:
tactus/core/runtime.py CHANGED
@@ -2499,6 +2499,7 @@ class TactusRuntime:
2499
2499
  """
2500
2500
  if not self.task_name and self.registry:
2501
2501
  explicit_tasks = getattr(self.registry, "tasks", {}) or {}
2502
+ named_procedures = getattr(self.registry, "named_procedures", {}) or {}
2502
2503
 
2503
2504
  def _flatten_tasks(task_map: dict, prefix: str = "") -> list[str]:
2504
2505
  names: list[str] = []
@@ -2528,10 +2529,12 @@ class TactusRuntime:
2528
2529
  implicit_tasks.append(f"{task}:{retriever_name}")
2529
2530
 
2530
2531
  if explicit_tasks:
2531
- if len(explicit_tasks) == 1:
2532
- self.task_name = next(iter(explicit_tasks.keys()))
2533
- elif "run" in explicit_tasks:
2532
+ if "run" in explicit_tasks:
2534
2533
  self.task_name = "run"
2534
+ elif "main" in named_procedures:
2535
+ self.task_name = None
2536
+ elif len(explicit_tasks) == 1:
2537
+ self.task_name = next(iter(explicit_tasks.keys()))
2535
2538
  else:
2536
2539
  from tactus.core.exceptions import TaskSelectionRequired
2537
2540
 
@@ -2892,7 +2895,11 @@ class TactusRuntime:
2892
2895
  r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*(?:"
2893
2896
  r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt|"
2894
2897
  r"Task|TaskFunction|Context|Corpus|Retriever|Compactor|function|"
2895
- r"[A-Za-z_][A-Za-z0-9_]*Retriever|[A-Za-z_][A-Za-z0-9_]*\.Retriever"
2898
+ r"[A-Za-z_][A-Za-z0-9_]*Retriever|"
2899
+ r"[A-Za-z_][A-Za-z0-9_]*\.Retriever|"
2900
+ r"[A-Za-z_][A-Za-z0-9_]*\.Corpus|"
2901
+ r"[A-Za-z_][A-Za-z0-9_]*\.Context|"
2902
+ r"[A-Za-z_][A-Za-z0-9_]*\.Compactor"
2896
2903
  r")\b"
2897
2904
  )
2898
2905
  # Match function definitions: function name() or local function name()
@@ -3167,6 +3174,9 @@ class TactusRuntime:
3167
3174
  except LuaSandboxError as e:
3168
3175
  raise TactusRuntimeError(f"Failed to parse DSL: {e}")
3169
3176
 
3177
+ lua_globals = sandbox.lua.globals()
3178
+ self._register_assignment_tasks(builder, lua_globals)
3179
+
3170
3180
  self._expand_inline_task_children(builder.registry)
3171
3181
 
3172
3182
  # Execute IncludeTasks files to register additional tasks
@@ -3269,7 +3279,6 @@ class TactusRuntime:
3269
3279
  # The script mode transformation (in _maybe_transform_script_mode_source)
3270
3280
  # is designed to skip files with named function definitions to avoid wrapping
3271
3281
  # them incorrectly.
3272
- lua_globals = sandbox.lua.globals()
3273
3282
  if "main" in lua_globals:
3274
3283
  main_func = lua_globals["main"]
3275
3284
  # Check if it's a function and not already registered
@@ -3297,6 +3306,49 @@ class TactusRuntime:
3297
3306
  logger.debug(f"Registry after parsing: lua_tools={list(result.registry.lua_tools.keys())}")
3298
3307
  return result.registry
3299
3308
 
3309
+ def _register_assignment_tasks(self, builder: RegistryBuilder, lua_globals: Any) -> None:
3310
+ if not lua_globals:
3311
+ return
3312
+
3313
+ for key, value in lua_globals.items():
3314
+ if not isinstance(key, str):
3315
+ continue
3316
+ if not hasattr(value, "items"):
3317
+ continue
3318
+ try:
3319
+ marker = value["__tactus_task_config"]
3320
+ except Exception:
3321
+ continue
3322
+ if not marker:
3323
+ continue
3324
+ task_config = lua_table_to_dict(value)
3325
+ if not isinstance(task_config, dict):
3326
+ continue
3327
+ if key in builder.registry.tasks:
3328
+ continue
3329
+ if "entry" in task_config and not callable(task_config["entry"]):
3330
+ raise TactusRuntimeError(f"Task '{key}' entry must be a function")
3331
+ builder.register_task(key, task_config)
3332
+
3333
+ child_sources = task_config.get("__tactus_child_tasks")
3334
+ if isinstance(child_sources, dict):
3335
+ child_iter = child_sources.items()
3336
+ else:
3337
+ child_iter = value.items()
3338
+ for child_key, child_value in child_iter:
3339
+ child_name = child_key if isinstance(child_key, str) else None
3340
+ if not hasattr(child_value, "items"):
3341
+ continue
3342
+ child_config = lua_table_to_dict(child_value)
3343
+ if not child_name and isinstance(child_config, dict):
3344
+ child_name = child_config.get("__task_name")
3345
+ if child_name and isinstance(child_config, dict):
3346
+ if "entry" in child_config and not callable(child_config["entry"]):
3347
+ raise TactusRuntimeError(
3348
+ f"Task '{key}:{child_name}' entry must be a function"
3349
+ )
3350
+ builder.register_task(child_name, child_config, parent=key)
3351
+
3300
3352
  def _registry_to_config(self, registry: ProcedureRegistry) -> dict[str, Any]:
3301
3353
  """
3302
3354
  Convert registry to config dict format.
@@ -0,0 +1,3 @@
1
+ """
2
+ Local plugin tools for Tactus.
3
+ """
tactus/plugins/noaa.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ NOAA AFD helper tools for Tactus demos.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, Dict
11
+
12
+
13
+ def fetch_noaa_afd(
14
+ *,
15
+ wfo: str,
16
+ max_items: int = 5,
17
+ output_root: str = "tests/fixtures/noaa_afd",
18
+ corpus_root: str = "tests/fixtures/noaa_afd_corpus",
19
+ force: bool = True,
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Fetch NOAA AFD fixtures and import them into a Biblicus corpus (no index).
23
+
24
+ :param wfo: Weather Forecast Office code (e.g., MFL).
25
+ :type wfo: str
26
+ :param max_items: Maximum number of items to fetch.
27
+ :type max_items: int
28
+ :param output_root: Directory for raw fixture output.
29
+ :type output_root: str
30
+ :param corpus_root: Directory for Biblicus corpus output.
31
+ :type corpus_root: str
32
+ :param force: Whether to recreate the corpus directory.
33
+ :type force: bool
34
+ :return: Summary of the fetch/import operation.
35
+ :rtype: dict[str, Any]
36
+ """
37
+ repo_root = Path(__file__).resolve().parents[2]
38
+ fetch_script = repo_root / "scripts" / "fetch_noaa_afd_corpus.py"
39
+ prepare_script = repo_root / "scripts" / "prepare_noaa_afd_biblicus_corpus.py"
40
+
41
+ output_root_path = Path(output_root)
42
+ corpus_root_path = Path(corpus_root) / wfo.upper()
43
+
44
+ fetch_args = [
45
+ sys.executable,
46
+ str(fetch_script),
47
+ "--wfo",
48
+ wfo,
49
+ "--max-items",
50
+ str(max_items),
51
+ "--output",
52
+ str(output_root_path),
53
+ ]
54
+ subprocess.run(fetch_args, check=True)
55
+
56
+ prepare_args = [
57
+ sys.executable,
58
+ str(prepare_script),
59
+ "--wfo",
60
+ wfo,
61
+ "--corpus",
62
+ str(corpus_root_path),
63
+ "--no-index",
64
+ ]
65
+ if force:
66
+ prepare_args.append("--force")
67
+ subprocess.run(prepare_args, check=True)
68
+
69
+ return {
70
+ "status": "ok",
71
+ "wfo": wfo,
72
+ "max_items": max_items,
73
+ "output_root": str(output_root_path),
74
+ "corpus_root": str(corpus_root_path),
75
+ "indexed": False,
76
+ }
@@ -62,10 +62,18 @@ def _normalize_client_config(client: Any) -> Any:
62
62
 
63
63
  payload = dict(client)
64
64
  model = payload.get("model")
65
- provider = payload.get("provider")
66
65
  if not model:
67
66
  raise ValueError("client.model is required")
68
67
 
68
+ provider, model = _resolve_provider_and_model(payload.get("provider"), model)
69
+ payload["provider"] = provider
70
+ payload["model"] = model
71
+
72
+ biblicus = _require_biblicus_text()
73
+ return biblicus["LlmClientConfig"](**payload)
74
+
75
+
76
+ def _resolve_provider_and_model(provider: str | None, model: Any) -> tuple[str, Any]:
69
77
  if provider is None and isinstance(model, str) and "/" in model:
70
78
  provider, model = model.split("/", 1)
71
79
  elif provider is not None and isinstance(model, str) and model.startswith(f"{provider}/"):
@@ -74,11 +82,7 @@ def _normalize_client_config(client: Any) -> Any:
74
82
  if provider is None:
75
83
  raise ValueError("client.provider is required when model lacks a provider prefix")
76
84
 
77
- payload["provider"] = provider
78
- payload["model"] = model
79
-
80
- biblicus = _require_biblicus_text()
81
- return biblicus["LlmClientConfig"](**payload)
85
+ return provider, model
82
86
 
83
87
 
84
88
  def _prepare_request(request: Dict[str, Any]) -> Dict[str, Any]:
@@ -97,11 +101,7 @@ def _prepare_request(request: Dict[str, Any]) -> Dict[str, Any]:
97
101
 
98
102
 
99
103
  def _maybe_mock(tool_name: str, payload: Dict[str, Any]) -> Dict[str, Any] | None:
100
- try:
101
- from tactus.core.mocking import get_current_mock_manager
102
- except Exception:
103
- return None
104
- mock_manager = get_current_mock_manager()
104
+ mock_manager = _get_mock_manager()
105
105
  if mock_manager is None:
106
106
  return None
107
107
  mock_result = mock_manager.get_mock_response(tool_name, payload)
@@ -111,54 +111,73 @@ def _maybe_mock(tool_name: str, payload: Dict[str, Any]) -> Dict[str, Any] | Non
111
111
  return mock_result
112
112
 
113
113
 
114
- def extract(request: Dict[str, Any]) -> Dict[str, Any]:
114
+ def _get_mock_manager() -> Any | None:
115
+ try:
116
+ from tactus.core.mocking import get_current_mock_manager
117
+ except Exception:
118
+ return None
119
+ return get_current_mock_manager()
120
+
121
+
122
+ def _run_text_tool(
123
+ request: Dict[str, Any],
124
+ *,
125
+ tool_name: str,
126
+ request_model_key: str,
127
+ apply_key: str,
128
+ ) -> Dict[str, Any]:
115
129
  payload = _prepare_request(request)
116
- mock_result = _maybe_mock("biblicus.text.extract", payload)
130
+ mock_result = _maybe_mock(tool_name, payload)
117
131
  if mock_result is not None:
118
132
  return mock_result
119
133
  biblicus = _require_biblicus_text()
120
- result = biblicus["apply_text_extract"](biblicus["TextExtractRequest"](**payload))
134
+ result = biblicus[apply_key](biblicus[request_model_key](**payload))
121
135
  return result.model_dump()
122
136
 
123
137
 
138
+ def extract(request: Dict[str, Any]) -> Dict[str, Any]:
139
+ return _run_text_tool(
140
+ request,
141
+ tool_name="biblicus.text.extract",
142
+ request_model_key="TextExtractRequest",
143
+ apply_key="apply_text_extract",
144
+ )
145
+
146
+
124
147
  def slice(request: Dict[str, Any]) -> Dict[str, Any]:
125
- payload = _prepare_request(request)
126
- mock_result = _maybe_mock("biblicus.text.slice", payload)
127
- if mock_result is not None:
128
- return mock_result
129
- biblicus = _require_biblicus_text()
130
- result = biblicus["apply_text_slice"](biblicus["TextSliceRequest"](**payload))
131
- return result.model_dump()
148
+ return _run_text_tool(
149
+ request,
150
+ tool_name="biblicus.text.slice",
151
+ request_model_key="TextSliceRequest",
152
+ apply_key="apply_text_slice",
153
+ )
132
154
 
133
155
 
134
156
  def annotate(request: Dict[str, Any]) -> Dict[str, Any]:
135
- payload = _prepare_request(request)
136
- mock_result = _maybe_mock("biblicus.text.annotate", payload)
137
- if mock_result is not None:
138
- return mock_result
139
- biblicus = _require_biblicus_text()
140
- result = biblicus["apply_text_annotate"](biblicus["TextAnnotateRequest"](**payload))
141
- return result.model_dump()
157
+ return _run_text_tool(
158
+ request,
159
+ tool_name="biblicus.text.annotate",
160
+ request_model_key="TextAnnotateRequest",
161
+ apply_key="apply_text_annotate",
162
+ )
142
163
 
143
164
 
144
165
  def redact(request: Dict[str, Any]) -> Dict[str, Any]:
145
- payload = _prepare_request(request)
146
- mock_result = _maybe_mock("biblicus.text.redact", payload)
147
- if mock_result is not None:
148
- return mock_result
149
- biblicus = _require_biblicus_text()
150
- result = biblicus["apply_text_redact"](biblicus["TextRedactRequest"](**payload))
151
- return result.model_dump()
166
+ return _run_text_tool(
167
+ request,
168
+ tool_name="biblicus.text.redact",
169
+ request_model_key="TextRedactRequest",
170
+ apply_key="apply_text_redact",
171
+ )
152
172
 
153
173
 
154
174
  def link(request: Dict[str, Any]) -> Dict[str, Any]:
155
- payload = _prepare_request(request)
156
- mock_result = _maybe_mock("biblicus.text.link", payload)
157
- if mock_result is not None:
158
- return mock_result
159
- biblicus = _require_biblicus_text()
160
- result = biblicus["apply_text_link"](biblicus["TextLinkRequest"](**payload))
161
- return result.model_dump()
175
+ return _run_text_tool(
176
+ request,
177
+ tool_name="biblicus.text.link",
178
+ request_model_key="TextLinkRequest",
179
+ apply_key="apply_text_link",
180
+ )
162
181
 
163
182
 
164
183
  def strip_span_tags(marked_up_text: str) -> str:
@@ -131,29 +131,9 @@ class TactusDSLVisitor(LuaParserVisitor):
131
131
  assignment_target_name = assignment_target_node.NAME().getText()
132
132
  self._track_retriever_alias(assignment_target_name, expression_list)
133
133
 
134
- # Check if this is a DSL setting assignment
135
- setting_handlers_by_name = {
136
- "default_provider": self.builder.set_default_provider,
137
- "default_model": self.builder.set_default_model,
138
- "return_prompt": self.builder.set_return_prompt,
139
- "error_prompt": self.builder.set_error_prompt,
140
- "status_prompt": self.builder.set_status_prompt,
141
- "async": self.builder.set_async,
142
- "max_depth": self.builder.set_max_depth,
143
- "max_turns": self.builder.set_max_turns,
144
- }
145
- if assignment_target_name in setting_handlers_by_name:
146
- # Get the value from explist
147
- if expression_list.exp() and len(expression_list.exp()) > 0:
148
- first_expression = expression_list.exp()[0]
149
- literal_value = self._extract_literal_value(first_expression)
150
- # Process the assignment like a function call
151
- setting_handlers_by_name[assignment_target_name](literal_value)
152
- else:
153
- # Check for assignment-based DSL declarations
154
- # e.g., greeter = Agent {...}, done = Tool {...}
155
- if expression_list.exp() and len(expression_list.exp()) > 0:
156
- first_expression = expression_list.exp()[0]
134
+ if not self._apply_setting_assignment(assignment_target_name, expression_list):
135
+ first_expression = self._extract_first_expression(expression_list)
136
+ if first_expression:
157
137
  self._check_assignment_based_declaration(
158
138
  assignment_target_name, first_expression
159
139
  )
@@ -219,105 +199,116 @@ class TactusDSLVisitor(LuaParserVisitor):
219
199
  )
220
200
  return
221
201
 
222
- if function_name == "Agent" and not is_chained_method_call:
223
- # Extract config from Agent {...}
224
- declaration_config = self._extract_single_table_arg(function_call)
225
- # Filter out None values from tools list (variable refs can't be resolved)
226
- if declaration_config and "tools" in declaration_config:
227
- tool_name_list = declaration_config["tools"]
228
- if isinstance(tool_name_list, list):
229
- declaration_config["tools"] = [
230
- tool_name for tool_name in tool_name_list if tool_name is not None
231
- ]
232
- self.builder.register_agent(
233
- assignment_target_name,
234
- declaration_config if declaration_config else {},
235
- None,
236
- )
237
- elif function_name == "Tool":
238
- # Extract config from Tool {...}
239
- declaration_config = self._extract_single_table_arg(function_call)
240
- if (
241
- declaration_config
242
- and isinstance(declaration_config, dict)
243
- and isinstance(declaration_config.get("name"), str)
244
- and declaration_config.get("name") != assignment_target_name
245
- ):
246
- self._record_error(
247
- message=(
248
- f"Tool name mismatch: '{assignment_target_name} = Tool {{ name = \"{declaration_config.get('name')}\" }}'. "
249
- f"Remove the 'name' field or set it to '{assignment_target_name}'."
250
- ),
251
- declaration="Tool",
252
- )
253
- self.builder.register_tool(
254
- assignment_target_name,
255
- declaration_config if declaration_config else {},
256
- None,
257
- )
258
- elif function_name == "Toolset":
259
- # Extract config from Toolset {...}
260
- declaration_config = self._extract_single_table_arg(function_call)
261
- self.builder.register_toolset(
262
- assignment_target_name,
263
- declaration_config if declaration_config else {},
264
- )
265
- elif function_name == "Context":
266
- declaration_config = self._extract_single_table_arg(function_call)
267
- self.builder.register_context(
268
- assignment_target_name,
269
- declaration_config if declaration_config else {},
270
- )
271
- elif function_name == "Corpus":
272
- declaration_config = self._extract_single_table_arg(function_call)
273
- self.builder.register_corpus(
274
- assignment_target_name,
275
- declaration_config if declaration_config else {},
276
- )
277
- elif function_name == "Retriever":
278
- declaration_config = self._extract_single_table_arg(function_call)
202
+ if function_name == "Agent" and is_chained_method_call:
203
+ return
204
+ handled = self._register_assignment_based_declaration(
205
+ assignment_target_name, function_name, function_call
206
+ )
207
+ if handled:
208
+ return
209
+ declaration_config = self._extract_single_table_arg(function_call)
210
+ if isinstance(declaration_config, dict) and "corpus" in declaration_config:
279
211
  retriever_id = self._resolve_retriever_id_from_call(function_call)
280
- if (
281
- isinstance(declaration_config, dict)
282
- and retriever_id
283
- and "retriever_id" not in declaration_config
284
- ):
212
+ if retriever_id and "retriever_id" not in declaration_config:
285
213
  declaration_config["retriever_id"] = retriever_id
286
214
  self.builder.register_retriever(
287
215
  assignment_target_name,
288
- declaration_config if declaration_config else {},
289
- )
290
- elif function_name == "Compactor":
291
- declaration_config = self._extract_single_table_arg(function_call)
292
- self.builder.register_compactor(
293
- assignment_target_name,
294
- declaration_config if declaration_config else {},
295
- )
296
- elif function_name == "Procedure":
297
- # New assignment syntax: main = Procedure { function(input) ... }
298
- # Register as a named procedure
299
- self.builder.register_named_procedure(
300
- assignment_target_name,
301
- None, # Function not available during validation
302
- {}, # Input schema will be extracted from top-level input {}
303
- {}, # Output schema will be extracted from top-level output {}
304
- {}, # State schema
216
+ declaration_config,
305
217
  )
306
- elif function_name == "Task":
307
- # Assignment-based task declaration: name = Task { ... }
308
- self._processed_task_calls.add(id(function_call))
309
- self._register_task_declaration(assignment_target_name, function_call)
310
- else:
311
- # Heuristic: treat any assignment-based call with a 'corpus' field as a retriever.
312
- declaration_config = self._extract_single_table_arg(function_call)
313
- if isinstance(declaration_config, dict) and "corpus" in declaration_config:
314
- retriever_id = self._resolve_retriever_id_from_call(function_call)
315
- if retriever_id and "retriever_id" not in declaration_config:
316
- declaration_config["retriever_id"] = retriever_id
317
- self.builder.register_retriever(
318
- assignment_target_name,
319
- declaration_config,
320
- )
218
+
219
+ def _apply_setting_assignment(self, assignment_target_name: str, expression_list) -> bool:
220
+ setting_handlers_by_name = {
221
+ "default_provider": self.builder.set_default_provider,
222
+ "default_model": self.builder.set_default_model,
223
+ "return_prompt": self.builder.set_return_prompt,
224
+ "error_prompt": self.builder.set_error_prompt,
225
+ "status_prompt": self.builder.set_status_prompt,
226
+ "async": self.builder.set_async,
227
+ "max_depth": self.builder.set_max_depth,
228
+ "max_turns": self.builder.set_max_turns,
229
+ }
230
+ if assignment_target_name not in setting_handlers_by_name:
231
+ return False
232
+ first_expression = self._extract_first_expression(expression_list)
233
+ if not first_expression:
234
+ return True
235
+ literal_value = self._extract_literal_value(first_expression)
236
+ setting_handlers_by_name[assignment_target_name](literal_value)
237
+ return True
238
+
239
+ def _extract_first_expression(self, expression_list):
240
+ exp_list = getattr(expression_list, "exp", None)
241
+ if not callable(exp_list):
242
+ return None
243
+ expressions = exp_list()
244
+ if expressions:
245
+ return expressions[0]
246
+ return None
247
+
248
+ def _register_assignment_based_declaration(
249
+ self,
250
+ assignment_target_name: str,
251
+ function_name: str,
252
+ function_call,
253
+ ) -> bool:
254
+ declaration_config = self._extract_single_table_arg(function_call)
255
+ normalized_config = declaration_config if declaration_config else {}
256
+ if function_name == "Agent":
257
+ if "tools" in normalized_config:
258
+ tool_name_list = normalized_config["tools"]
259
+ if isinstance(tool_name_list, list):
260
+ normalized_config["tools"] = [
261
+ tool_name for tool_name in tool_name_list if tool_name is not None
262
+ ]
263
+ self.builder.register_agent(assignment_target_name, normalized_config, None)
264
+ return True
265
+ if function_name == "Tool":
266
+ if (
267
+ isinstance(normalized_config, dict)
268
+ and isinstance(normalized_config.get("name"), str)
269
+ and normalized_config.get("name") != assignment_target_name
270
+ ):
271
+ self._record_error(
272
+ message=(
273
+ f"Tool name mismatch: '{assignment_target_name} = Tool {{ name = \"{normalized_config.get('name')}\" }}'. "
274
+ f"Remove the 'name' field or set it to '{assignment_target_name}'."
275
+ ),
276
+ declaration="Tool",
277
+ )
278
+ self.builder.register_tool(assignment_target_name, normalized_config, None)
279
+ return True
280
+ if function_name == "Toolset":
281
+ self.builder.register_toolset(assignment_target_name, normalized_config)
282
+ return True
283
+ if function_name == "Context":
284
+ self.builder.register_context(assignment_target_name, normalized_config)
285
+ return True
286
+ if function_name == "Corpus":
287
+ self.builder.register_corpus(assignment_target_name, normalized_config)
288
+ return True
289
+ if function_name == "Retriever":
290
+ retriever_id = self._resolve_retriever_id_from_call(function_call)
291
+ if retriever_id and "retriever_id" not in normalized_config:
292
+ normalized_config["retriever_id"] = retriever_id
293
+ self.builder.register_retriever(assignment_target_name, normalized_config)
294
+ return True
295
+ if function_name == "Compactor":
296
+ self.builder.register_compactor(assignment_target_name, normalized_config)
297
+ return True
298
+ if function_name == "Procedure":
299
+ self.builder.register_named_procedure(
300
+ assignment_target_name,
301
+ None,
302
+ {},
303
+ {},
304
+ {},
305
+ )
306
+ return True
307
+ if function_name == "Task":
308
+ self._processed_task_calls.add(id(function_call))
309
+ self._register_task_declaration(assignment_target_name, function_call)
310
+ return True
311
+ return False
321
312
 
322
313
  def _extract_single_table_arg(self, function_call) -> dict:
323
314
  """Extract a single table argument from a function call like Agent {...}."""
@@ -1122,23 +1113,23 @@ class TactusDSLVisitor(LuaParserVisitor):
1122
1113
  return token_text[2:-2]
1123
1114
  elif token_text.startswith('"') and token_text.endswith('"'):
1124
1115
  # Double-quoted string
1125
- content = token_text[1:-1]
1126
- content = content.replace("\\n", "\n")
1127
- content = content.replace("\\t", "\t")
1128
- content = content.replace('\\"', '"')
1129
- content = content.replace("\\\\", "\\")
1130
- return content
1116
+ return self._unescape_basic_string(token_text[1:-1], '"')
1131
1117
  elif token_text.startswith("'") and token_text.endswith("'"):
1132
1118
  # Single-quoted string
1133
- content = token_text[1:-1]
1134
- content = content.replace("\\n", "\n")
1135
- content = content.replace("\\t", "\t")
1136
- content = content.replace("\\'", "'")
1137
- content = content.replace("\\\\", "\\")
1138
- return content
1119
+ return self._unescape_basic_string(token_text[1:-1], "'")
1139
1120
 
1140
1121
  return token_text
1141
1122
 
1123
+ def _unescape_basic_string(self, content: str, quote_char: str) -> str:
1124
+ content = content.replace("\\n", "\n")
1125
+ content = content.replace("\\t", "\t")
1126
+ if quote_char == '"':
1127
+ content = content.replace('\\"', '"')
1128
+ elif quote_char == "'":
1129
+ content = content.replace("\\'", "'")
1130
+ content = content.replace("\\\\", "\\")
1131
+ return content
1132
+
1142
1133
  def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> Any:
1143
1134
  """Parse Lua table constructor to Python dict."""
1144
1135
  parsed_table = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tactus
3
- Version: 0.38.0
3
+ Version: 0.39.0
4
4
  Summary: Tactus: Lua-based DSL for agentic workflows
5
5
  Project-URL: Homepage, https://github.com/AnthusAI/Tactus
6
6
  Project-URL: Documentation, https://github.com/AnthusAI/Tactus/tree/main/docs
@@ -1,4 +1,4 @@
1
- tactus/__init__.py,sha256=g5cqIhFTf2v9_3EjAdhFDjXJWMvCrtSaYVs7B7LpU7M,1245
1
+ tactus/__init__.py,sha256=FbndoB7McjD-smRU9tGDLvNIw7o7-dCqiKRP3LSD23c,1245
2
2
  tactus/adapters/__init__.py,sha256=47Y8kGBR4QGxqEGvjA1mneOSACb2L7oELnj6P2uI7uk,759
3
3
  tactus/adapters/broker_log.py,sha256=9ZR-rJdyW6bMNZx3OfXoQEnDxcAzNsiJ8aPxZGqJYrM,6019
4
4
  tactus/adapters/cli_hitl.py,sha256=Nrfoi35Ei9fTMReLG2QxKkhKyIvl3pYcAUdQCAUOZDk,17361
@@ -37,7 +37,7 @@ tactus/core/compaction.py,sha256=mVwFsEb9FEc-bMPFcKgXyAO-pVnAXeQGZka7RWtjVsY,397
37
37
  tactus/core/config_manager.py,sha256=kxz853j4Nx97SBh8-fAar_OfmfWZvvcfafLyxjTQG1A,35131
38
38
  tactus/core/context_assembler.py,sha256=tbd-XACBvAFkBlPrzAAZ_L-2JGnO4meS-GL4ilE7XHw,2406
39
39
  tactus/core/context_models.py,sha256=YHKjHKEzLk3fgpkmXYQ9RWDPKRNStt92ve40xcMv3p0,975
40
- tactus/core/dsl_stubs.py,sha256=3yg7yWcflDPOOzbLG7VmHh7yZPXf1G_q6sdYt5DWPOw,109777
40
+ tactus/core/dsl_stubs.py,sha256=l1icTqTM092Z2AsrdLAo3WjZtDDVggMBG662KdWUIIU,110257
41
41
  tactus/core/exceptions.py,sha256=r-4IrZw_WrioBkpMR42Q3LNwEqimJ6jxXgfOo2wANTM,1962
42
42
  tactus/core/execution_context.py,sha256=OgTe9E0xc3nTQbCTEaaBfI11dVA1-J0eFz0eQR4XMZY,29402
43
43
  tactus/core/lua_sandbox.py,sha256=Ln2P1gdxVl396HLvEw7FmDKV3eVdVdbDzYHMbDSEciY,19106
@@ -47,7 +47,7 @@ tactus/core/output_validator.py,sha256=LcSjgAiDRvzsj2uWasQihengQRt7R-ZYaPiLQPbZy
47
47
  tactus/core/registry.py,sha256=z5HFoi1zb81-TJ_6qeTWSte4J0Zw-NYmN19VjjBjJE8,26577
48
48
  tactus/core/retrieval.py,sha256=AMDa4X4YWaSDNdf3T3hRUzgM4W1l_sGqxAX5Jki5PH0,12083
49
49
  tactus/core/retriever_tasks.py,sha256=i2wsoZN9cZOypkdPAJKqlrRP_jwtqpHVCFoxPL0Iirk,931
50
- tactus/core/runtime.py,sha256=kwDM88Ell2RkOkEbwmI5v92fQOLeruod-dpbCTq6hM0,155443
50
+ tactus/core/runtime.py,sha256=YMZKfUj2sfyGyBFdKvXWjbt2dbf-mckz4CefeMm_7sw,157790
51
51
  tactus/core/template_resolver.py,sha256=r97KzFNaK4nFSoWtIFZeSKyuUWgbp-ay1_BGrb-BgUY,4179
52
52
  tactus/core/yaml_parser.py,sha256=JD7Nehaxw3uP1KV_uTU_xiXTbEWqoKOceU5tAJ4lcH8,13985
53
53
  tactus/core/dependencies/__init__.py,sha256=28-TM7_i-JqTD3hvkq1kMzr__A8VjfIKXymdW9mn5NM,362
@@ -76,6 +76,8 @@ tactus/ide/__init__.py,sha256=1fSC0xWP-Lq5wl4FgDq7SMnkvZ0DxXupreTl3ZRX1zw,143
76
76
  tactus/ide/coding_assistant.py,sha256=i2cfT6uMSM7TEFw-9p9Ed_BWSbAMqgokoaFdbjKAQcg,12187
77
77
  tactus/ide/config_server.py,sha256=U8OWxi5l24GH1lUHIAQ8WB8j0cJ5ofLX9iVecW1O2vc,18862
78
78
  tactus/ide/server.py,sha256=nE_UDiXJZN7G-RzPD-guZ_4qPxPl722qcrv4UY6bjII,111151
79
+ tactus/plugins/__init__.py,sha256=MOSTLvPnXeDHKNcGzTv26bRxsK62pVRmTjEwbuoaWHk,39
80
+ tactus/plugins/noaa.py,sha256=BTBxmbI55AFgDNB4ZhNR7NaKqJECRvRJRua4gahaSvU,2022
79
81
  tactus/primitives/__init__.py,sha256=x6bGwoa9DizKUwqsg7SqURfJxisEdctTCv1XnSAZxIk,1709
80
82
  tactus/primitives/control.py,sha256=jw-7ggHtNLfFL5aTUUs6Fo5y4xsxEG8OIRe0RyIjVnc,4783
81
83
  tactus/primitives/file.py,sha256=GFHmXOADRllfJw6gHpIdVMmZ_ZS7DVgresQ0F71nqJE,7458
@@ -122,7 +124,7 @@ tactus/stdlib/README.md,sha256=AqKYj7JxtphfKJlhq_sNezIUPfblzzubzdxE3PheiQE,2576
122
124
  tactus/stdlib/__init__.py,sha256=NkRsL413VXr0rLAadbb3meP5TelwcrEFVJd1u39XCbk,1047
123
125
  tactus/stdlib/loader.py,sha256=qjVnz5mn3Uu7g1O4vjSREHkR-YdRoON1vqJQq-oiFIE,8679
124
126
  tactus/stdlib/biblicus/__init__.py,sha256=Y6Nb-wp33KeDtkjBccZrGYlyR98yhZBm1RfmiKIJHm8,50
125
- tactus/stdlib/biblicus/text.py,sha256=KMbMlaGQsrAwXgCShoLs54xYDoLm-s0Ymob5zZOhOeY,6285
127
+ tactus/stdlib/biblicus/text.py,sha256=aL7FF5MmX0aXskGwSTD970kGP4C0bAGHm0vJe7MThPU,6255
126
128
  tactus/stdlib/classify/__init__.py,sha256=51Lqge0g0Q6GWXkmw42HwuqkkDCsww9VBcoreYId374,5623
127
129
  tactus/stdlib/classify/classify.spec.tac,sha256=0yuRD_2dbPKTuhyqwk3vtsj_R3kwGoSEiEF4OY-ARqA,6475
128
130
  tactus/stdlib/classify/classify.tac,sha256=KvOXLihspPK1_g2GcT9wnLkynDubglp1S_JUfZlo-88,6850
@@ -210,7 +212,7 @@ tactus/validation/LuaParserBase.py,sha256=o3klCIY0ANkVCU0VHml0IOYE4CdEledeoyoIAP
210
212
  tactus/validation/README.md,sha256=AS6vr4blY7IKWRsj4wuvWBHVMTc5fto7IgNmv-Rjkdo,5366
211
213
  tactus/validation/__init__.py,sha256=rnap-YvNievWigYYUewuXBcLtAdjZ8YpeJDYS1T7XZM,153
212
214
  tactus/validation/error_listener.py,sha256=MPkvsVbojwYjNA8MpapG_GNtR6ZDyb3cTt7aLwekCtM,1010
213
- tactus/validation/semantic_visitor.py,sha256=frs7zc66rQkAKtPwshMSXYxXyqyYxEV9TokOiRY5lxo,57135
215
+ tactus/validation/semantic_visitor.py,sha256=YLX54qXpGNrJSANr15UfbGmxT017vGE10dBtBZTphIQ,55149
214
216
  tactus/validation/validator.py,sha256=JSQHmI3EkNjJsPxrztTszJGuNuHyyaDdRsV62I4P0QU,11911
215
217
  tactus/validation/generated/LuaLexer.interp,sha256=B-Xb6HNXS7YYYQB_cvsWzf8OQLFnEhZHDN5vCOyP3yw,20444
216
218
  tactus/validation/generated/LuaLexer.py,sha256=6B-HNB_vAp3bA5iACLvMWw0R4KFENsuiG7bccysxbRQ,67252
@@ -224,8 +226,8 @@ tactus/validation/generated/LuaParserVisitor.py,sha256=ageKSmHPxnO3jBS2fBtkmYBOd
224
226
  tactus/validation/generated/__init__.py,sha256=5gWlwRI0UvmHw2fnBpj_IG6N8oZeabr5tbj1AODDvjc,196
225
227
  tactus/validation/grammar/LuaLexer.g4,sha256=t2MXiTCr127RWAyQGvamkcU_m4veqPzSuHUtAKwalw4,2771
226
228
  tactus/validation/grammar/LuaParser.g4,sha256=ceZenb90BdiZmVdOxMGj9qJk3QbbWVZe5HUqPgoePfY,3202
227
- tactus-0.38.0.dist-info/METADATA,sha256=aWNJ9IXllKc79gX9ed4X-MSGqjGSiEtGT8xTdYHQzjA,60383
228
- tactus-0.38.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
229
- tactus-0.38.0.dist-info/entry_points.txt,sha256=vWseqty8m3z-Worje0IYxlioMjPDCoSsm0AtY4GghBY,47
230
- tactus-0.38.0.dist-info/licenses/LICENSE,sha256=ivohBcAIYnaLPQ-lKEeCXSMvQUVISpQfKyxHBHoa4GA,1066
231
- tactus-0.38.0.dist-info/RECORD,,
229
+ tactus-0.39.0.dist-info/METADATA,sha256=5Axn0cmSvHUPhqcpvlbMouG27Gu2YkmxHfINh3ucnig,60383
230
+ tactus-0.39.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
231
+ tactus-0.39.0.dist-info/entry_points.txt,sha256=vWseqty8m3z-Worje0IYxlioMjPDCoSsm0AtY4GghBY,47
232
+ tactus-0.39.0.dist-info/licenses/LICENSE,sha256=ivohBcAIYnaLPQ-lKEeCXSMvQUVISpQfKyxHBHoa4GA,1066
233
+ tactus-0.39.0.dist-info/RECORD,,