lionherd-core 1.0.0a3__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 (64) hide show
  1. lionherd_core/__init__.py +84 -0
  2. lionherd_core/base/__init__.py +30 -0
  3. lionherd_core/base/_utils.py +295 -0
  4. lionherd_core/base/broadcaster.py +128 -0
  5. lionherd_core/base/element.py +300 -0
  6. lionherd_core/base/event.py +322 -0
  7. lionherd_core/base/eventbus.py +112 -0
  8. lionherd_core/base/flow.py +236 -0
  9. lionherd_core/base/graph.py +616 -0
  10. lionherd_core/base/node.py +212 -0
  11. lionherd_core/base/pile.py +811 -0
  12. lionherd_core/base/progression.py +261 -0
  13. lionherd_core/errors.py +104 -0
  14. lionherd_core/libs/__init__.py +2 -0
  15. lionherd_core/libs/concurrency/__init__.py +60 -0
  16. lionherd_core/libs/concurrency/_cancel.py +85 -0
  17. lionherd_core/libs/concurrency/_errors.py +80 -0
  18. lionherd_core/libs/concurrency/_patterns.py +238 -0
  19. lionherd_core/libs/concurrency/_primitives.py +253 -0
  20. lionherd_core/libs/concurrency/_priority_queue.py +135 -0
  21. lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
  22. lionherd_core/libs/concurrency/_task.py +58 -0
  23. lionherd_core/libs/concurrency/_utils.py +61 -0
  24. lionherd_core/libs/schema_handlers/__init__.py +35 -0
  25. lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
  26. lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
  27. lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
  28. lionherd_core/libs/schema_handlers/_typescript.py +153 -0
  29. lionherd_core/libs/string_handlers/__init__.py +15 -0
  30. lionherd_core/libs/string_handlers/_extract_json.py +65 -0
  31. lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
  32. lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
  33. lionherd_core/libs/string_handlers/_to_num.py +63 -0
  34. lionherd_core/ln/__init__.py +45 -0
  35. lionherd_core/ln/_async_call.py +314 -0
  36. lionherd_core/ln/_fuzzy_match.py +166 -0
  37. lionherd_core/ln/_fuzzy_validate.py +151 -0
  38. lionherd_core/ln/_hash.py +141 -0
  39. lionherd_core/ln/_json_dump.py +347 -0
  40. lionherd_core/ln/_list_call.py +110 -0
  41. lionherd_core/ln/_to_dict.py +373 -0
  42. lionherd_core/ln/_to_list.py +190 -0
  43. lionherd_core/ln/_utils.py +156 -0
  44. lionherd_core/lndl/__init__.py +62 -0
  45. lionherd_core/lndl/errors.py +30 -0
  46. lionherd_core/lndl/fuzzy.py +321 -0
  47. lionherd_core/lndl/parser.py +427 -0
  48. lionherd_core/lndl/prompt.py +137 -0
  49. lionherd_core/lndl/resolver.py +323 -0
  50. lionherd_core/lndl/types.py +287 -0
  51. lionherd_core/protocols.py +181 -0
  52. lionherd_core/py.typed +0 -0
  53. lionherd_core/types/__init__.py +46 -0
  54. lionherd_core/types/_sentinel.py +131 -0
  55. lionherd_core/types/base.py +341 -0
  56. lionherd_core/types/operable.py +133 -0
  57. lionherd_core/types/spec.py +313 -0
  58. lionherd_core/types/spec_adapters/__init__.py +10 -0
  59. lionherd_core/types/spec_adapters/_protocol.py +125 -0
  60. lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
  61. lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
  62. lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
  63. lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
  64. lionherd_core-1.0.0a3.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,427 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import ast
5
+ import re
6
+ import warnings
7
+ from typing import Any
8
+
9
+ from .errors import MissingOutBlockError
10
+ from .types import LactMetadata, LvarMetadata
11
+
12
+ # Track warned action names to prevent duplicate warnings
13
+ _warned_action_names: set[str] = set()
14
+
15
+ # Python reserved keywords and common builtins
16
+ # Action names matching these will trigger warnings (not errors)
17
+ PYTHON_RESERVED = {
18
+ # Keywords
19
+ "and",
20
+ "as",
21
+ "assert",
22
+ "async",
23
+ "await",
24
+ "break",
25
+ "class",
26
+ "continue",
27
+ "def",
28
+ "del",
29
+ "elif",
30
+ "else",
31
+ "except",
32
+ "finally",
33
+ "for",
34
+ "from",
35
+ "global",
36
+ "if",
37
+ "import",
38
+ "in",
39
+ "is",
40
+ "lambda",
41
+ "nonlocal",
42
+ "not",
43
+ "or",
44
+ "pass",
45
+ "raise",
46
+ "return",
47
+ "try",
48
+ "while",
49
+ "with",
50
+ "yield",
51
+ # Common builtins that might cause confusion
52
+ "print",
53
+ "input",
54
+ "open",
55
+ "len",
56
+ "range",
57
+ "list",
58
+ "dict",
59
+ "set",
60
+ "tuple",
61
+ "str",
62
+ "int",
63
+ "float",
64
+ "bool",
65
+ "type",
66
+ }
67
+
68
+
69
+ def extract_lvars(text: str) -> dict[str, str]:
70
+ """Extract <lvar name>content</lvar> declarations (legacy format).
71
+
72
+ Args:
73
+ text: Response text containing lvar declarations
74
+
75
+ Returns:
76
+ Dict mapping lvar names to their content
77
+ """
78
+ pattern = r"<lvar\s+(\w+)>(.*?)</lvar>"
79
+ matches = re.findall(pattern, text, re.DOTALL)
80
+
81
+ lvars = {}
82
+ for name, content in matches:
83
+ # Strip whitespace but preserve internal structure
84
+ lvars[name] = content.strip()
85
+
86
+ return lvars
87
+
88
+
89
+ def extract_lvars_prefixed(text: str) -> dict[str, LvarMetadata]:
90
+ """Extract namespace-prefixed lvar declarations.
91
+
92
+ Args:
93
+ text: Response text with <lvar Model.field alias>value</lvar> declarations
94
+
95
+ Returns:
96
+ Dict mapping local names to LvarMetadata
97
+ """
98
+ # Pattern: <lvar Model.field optional_local_name>value</lvar>
99
+ # Groups: (1) model, (2) field, (3) optional local_name, (4) value
100
+ pattern = r"<lvar\s+(\w+)\.(\w+)(?:\s+(\w+))?\s*>(.*?)</lvar>"
101
+ matches = re.findall(pattern, text, re.DOTALL)
102
+
103
+ lvars = {}
104
+ for model, field, local_name, value in matches:
105
+ # If no local_name provided, use field name
106
+ local = local_name if local_name else field
107
+
108
+ lvars[local] = LvarMetadata(model=model, field=field, local_name=local, value=value.strip())
109
+
110
+ return lvars
111
+
112
+
113
+ def extract_lacts(text: str) -> dict[str, str]:
114
+ """Extract <lact name>function_call</lact> action declarations (legacy, non-namespaced).
115
+
116
+ DEPRECATED: Use extract_lacts_prefixed() for namespace support.
117
+
118
+ Actions represent tool/function invocations using pythonic syntax.
119
+ They are only executed if referenced in the OUT{} block.
120
+
121
+ Args:
122
+ text: Response text containing <lact> declarations
123
+
124
+ Returns:
125
+ Dict mapping action names to Python function call strings
126
+
127
+ Examples:
128
+ >>> text = '<lact search>search(query="AI", limit=5)</lact>'
129
+ >>> extract_lacts(text)
130
+ {'search': 'search(query="AI", limit=5)'}
131
+ """
132
+ pattern = r"<lact\s+(\w+)>(.*?)</lact>"
133
+ matches = re.findall(pattern, text, re.DOTALL)
134
+
135
+ lacts = {}
136
+ for name, call_str in matches:
137
+ # Strip whitespace but preserve the function call structure
138
+ lacts[name] = call_str.strip()
139
+
140
+ return lacts
141
+
142
+
143
+ def extract_lacts_prefixed(text: str) -> dict[str, LactMetadata]:
144
+ """Extract <lact> action declarations with optional namespace prefix.
145
+
146
+ Supports two patterns:
147
+ Namespaced: <lact Model.field alias>function_call()</lact>
148
+ Direct: <lact name>function_call()</lact>
149
+
150
+ Args:
151
+ text: Response text containing <lact> declarations
152
+
153
+ Returns:
154
+ Dict mapping local names to LactMetadata
155
+
156
+ Note:
157
+ Performance: The regex pattern uses (.*?) with DOTALL for action body extraction.
158
+ For very large responses (>100KB), parsing may be slow. Recommended maximum
159
+ response size: 50KB. For larger responses, consider streaming parsers.
160
+
161
+ Examples:
162
+ >>> text = "<lact Report.summary s>generate_summary(...)</lact>"
163
+ >>> extract_lacts_prefixed(text)
164
+ {'s': LactMetadata(model="Report", field="summary", local_name="s", call="generate_summary(...)")}
165
+
166
+ >>> text = '<lact search>search(query="AI")</lact>'
167
+ >>> extract_lacts_prefixed(text)
168
+ {'search': LactMetadata(model=None, field=None, local_name="search", call='search(query="AI")')}
169
+ """
170
+ # Pattern matches both forms with strict identifier validation:
171
+ # <lact Model.field alias>call</lact> OR <lact name>call</lact>
172
+ # Groups: (1) identifier (Model or name), (2) optional .field, (3) optional alias, (4) call
173
+ # Rejects: multiple dots, leading/trailing dots, numeric prefixes
174
+ # Note: \w* allows single-character identifiers (e.g., alias="t")
175
+ pattern = r"<lact\s+([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?(?:\s+([A-Za-z_]\w*))?>(.*?)</lact>"
176
+ matches = re.findall(pattern, text, re.DOTALL)
177
+
178
+ lacts = {}
179
+ for identifier, field, alias, call_str in matches:
180
+ # Check if field group is present (namespaced pattern)
181
+ if field:
182
+ # Namespaced: <lact Model.field alias>
183
+ model = identifier
184
+ local_name = alias if alias else field # Use alias or default to field name
185
+ else:
186
+ # Direct: <lact name>
187
+ model = None
188
+ field = None
189
+ local_name = identifier # identifier is the name
190
+
191
+ # Warn if action name conflicts with Python reserved keywords (deduplicated)
192
+ if local_name in PYTHON_RESERVED and local_name not in _warned_action_names:
193
+ _warned_action_names.add(local_name)
194
+ warnings.warn(
195
+ f"Action name '{local_name}' is a Python reserved keyword or builtin. "
196
+ f"While this works in LNDL (string keys), it may cause confusion.",
197
+ UserWarning,
198
+ stacklevel=2,
199
+ )
200
+
201
+ lacts[local_name] = LactMetadata(
202
+ model=model,
203
+ field=field,
204
+ local_name=local_name,
205
+ call=call_str.strip(),
206
+ )
207
+
208
+ return lacts
209
+
210
+
211
+ def extract_out_block(text: str) -> str:
212
+ """Extract OUT{...} block content with balanced brace scanning.
213
+
214
+ Args:
215
+ text: Response text containing OUT{} block
216
+
217
+ Returns:
218
+ Content inside OUT{} block (without outer braces)
219
+
220
+ Raises:
221
+ MissingOutBlockError: If no OUT{} block found or unbalanced
222
+ """
223
+ # First try to extract from ```lndl code fence
224
+ lndl_fence_pattern = r"```lndl\s*(.*?)```"
225
+ lndl_match = re.search(lndl_fence_pattern, text, re.DOTALL | re.IGNORECASE)
226
+
227
+ if lndl_match:
228
+ # Extract from code fence content using balanced scanner
229
+ fence_content = lndl_match.group(1)
230
+ out_match = re.search(r"OUT\s*\{", fence_content, re.IGNORECASE)
231
+ if out_match:
232
+ return _extract_balanced_curly(fence_content, out_match.end() - 1).strip()
233
+
234
+ # Fallback: try to find OUT{} anywhere in text
235
+ out_match = re.search(r"OUT\s*\{", text, re.IGNORECASE)
236
+
237
+ if not out_match:
238
+ raise MissingOutBlockError("No OUT{} block found in response")
239
+
240
+ return _extract_balanced_curly(text, out_match.end() - 1).strip()
241
+
242
+
243
+ def _extract_balanced_curly(text: str, open_idx: int) -> str:
244
+ """Extract balanced curly brace content, ignoring braces in strings.
245
+
246
+ Args:
247
+ text: Full text containing the opening brace
248
+ open_idx: Index of the opening '{'
249
+
250
+ Returns:
251
+ Content between balanced braces (without outer braces)
252
+
253
+ Raises:
254
+ MissingOutBlockError: If braces are unbalanced
255
+ """
256
+ depth = 1
257
+ i = open_idx + 1
258
+ in_str = False
259
+ quote = ""
260
+ esc = False
261
+
262
+ while i < len(text):
263
+ ch = text[i]
264
+
265
+ if in_str:
266
+ # Inside string: handle escapes and track quote end
267
+ if esc:
268
+ esc = False
269
+ elif ch == "\\":
270
+ esc = True
271
+ elif ch == quote:
272
+ in_str = False
273
+ else:
274
+ # Outside string: track quotes and braces
275
+ if ch in ('"', "'"):
276
+ in_str = True
277
+ quote = ch
278
+ elif ch == "{":
279
+ depth += 1
280
+ elif ch == "}":
281
+ depth -= 1
282
+ if depth == 0:
283
+ # Found matching closing brace
284
+ return text[open_idx + 1 : i]
285
+
286
+ i += 1
287
+
288
+ raise MissingOutBlockError("Unbalanced OUT{} block")
289
+
290
+
291
+ def parse_out_block_array(out_content: str) -> dict[str, list[str] | str]:
292
+ """Parse OUT{} block with array syntax and literal values.
293
+
294
+ Args:
295
+ out_content: Content inside OUT{} block
296
+
297
+ Returns:
298
+ Dict mapping field names to lists of variable names or literal values
299
+ """
300
+ fields: dict[str, list[str] | str] = {}
301
+
302
+ # Pattern: field_name:[var1, var2, ...] or field_name:value
303
+ # Split by comma at top level (not inside brackets or quotes)
304
+ i = 0
305
+ while i < len(out_content):
306
+ # Skip whitespace
307
+ while i < len(out_content) and out_content[i].isspace():
308
+ i += 1
309
+
310
+ if i >= len(out_content):
311
+ break
312
+
313
+ # Extract field name
314
+ field_start = i
315
+ while i < len(out_content) and (out_content[i].isalnum() or out_content[i] == "_"):
316
+ i += 1
317
+
318
+ if i >= len(out_content):
319
+ break
320
+
321
+ field_name = out_content[field_start:i].strip()
322
+
323
+ # Skip whitespace and colon
324
+ while i < len(out_content) and out_content[i].isspace():
325
+ i += 1
326
+
327
+ if i >= len(out_content) or out_content[i] != ":":
328
+ break
329
+
330
+ i += 1 # Skip colon
331
+
332
+ # Skip whitespace
333
+ while i < len(out_content) and out_content[i].isspace():
334
+ i += 1
335
+
336
+ # Check if array syntax [var1, var2] or value
337
+ if i < len(out_content) and out_content[i] == "[":
338
+ # Array syntax
339
+ i += 1 # Skip opening bracket
340
+ bracket_start = i
341
+
342
+ # Find matching closing bracket
343
+ depth = 1
344
+ while i < len(out_content) and depth > 0:
345
+ if out_content[i] == "[":
346
+ depth += 1
347
+ elif out_content[i] == "]":
348
+ depth -= 1
349
+ i += 1
350
+
351
+ # Extract variable names from inside brackets
352
+ vars_str = out_content[bracket_start : i - 1].strip()
353
+ var_names = [v.strip() for v in vars_str.split(",") if v.strip()]
354
+ fields[field_name] = var_names
355
+
356
+ else:
357
+ # Single value (variable or literal)
358
+ value_start = i
359
+
360
+ # Handle quoted strings
361
+ if i < len(out_content) and out_content[i] in ('"', "'"):
362
+ quote = out_content[i]
363
+ i += 1
364
+ while i < len(out_content) and out_content[i] != quote:
365
+ if out_content[i] == "\\":
366
+ i += 2 # Skip escaped character
367
+ else:
368
+ i += 1
369
+ if i < len(out_content):
370
+ i += 1 # Skip closing quote
371
+ else:
372
+ # Read until comma or newline
373
+ while i < len(out_content) and out_content[i] not in ",\n":
374
+ i += 1
375
+
376
+ value = out_content[value_start:i].strip()
377
+ if value:
378
+ # Detect if this is a literal scalar (number, boolean) or variable name
379
+ # Heuristic: literals contain non-alphanumeric chars or are numbers/booleans
380
+ is_likely_literal = (
381
+ value.startswith('"')
382
+ or value.startswith("'")
383
+ or value.replace(".", "", 1).replace("-", "", 1).isdigit() # number
384
+ or value.lower() in ("true", "false", "null") # boolean/null
385
+ )
386
+
387
+ if is_likely_literal:
388
+ # Literal value (scalar)
389
+ fields[field_name] = value
390
+ else:
391
+ # Variable reference - wrap in list for consistency
392
+ fields[field_name] = [value]
393
+
394
+ # Skip optional comma
395
+ while i < len(out_content) and out_content[i].isspace():
396
+ i += 1
397
+ if i < len(out_content) and out_content[i] == ",":
398
+ i += 1
399
+
400
+ return fields
401
+
402
+
403
+ def parse_value(value_str: str) -> Any:
404
+ """Parse string value to Python object (numbers, booleans, lists, dicts, strings).
405
+
406
+ Args:
407
+ value_str: String representation of value
408
+
409
+ Returns:
410
+ Parsed Python object
411
+ """
412
+ value_str = value_str.strip()
413
+
414
+ # Handle lowercase boolean literals
415
+ if value_str.lower() == "true":
416
+ return True
417
+ if value_str.lower() == "false":
418
+ return False
419
+ if value_str.lower() == "null":
420
+ return None
421
+
422
+ # Try literal_eval for numbers, lists, dicts
423
+ try:
424
+ return ast.literal_eval(value_str)
425
+ except (ValueError, SyntaxError):
426
+ # Return as string
427
+ return value_str
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ LNDL_SYSTEM_PROMPT = """LNDL - Structured Output with Natural Thinking
5
+
6
+ SYNTAX
7
+
8
+ Variables:
9
+ <lvar Model.field alias>value</lvar>
10
+
11
+ - Model.field: Explicit mapping (Report.title, Reason.confidence)
12
+ - alias: Short name for OUT{} reference (optional, defaults to field name)
13
+ - Declare anywhere, revise anytime, think naturally
14
+
15
+ Actions (two patterns):
16
+
17
+ Namespaced (for mixing with lvars):
18
+ <lact Model.field alias>function_call(args)</lact>
19
+
20
+ - Model.field: Explicit mapping (like lvars)
21
+ - alias: Short name for OUT{} reference (optional, defaults to field name)
22
+ - Enables mixing with lvars in same model
23
+
24
+ Direct (entire output):
25
+ <lact name>function_call(args)</lact>
26
+
27
+ - name: Local reference for OUT{} block
28
+ - Result becomes the entire field value
29
+ - Cannot mix with lvars (use namespaced pattern instead)
30
+
31
+ Both patterns:
32
+ - Only executed if referenced in OUT{}
33
+ - Not in OUT{} = scratch work (thinking, not executed)
34
+ - Pythonic function syntax with arguments
35
+
36
+ Output:
37
+ ```lndl
38
+ OUT{field1:[var1, var2], field2:[action], scalar:literal}
39
+ ```
40
+
41
+ Arrays for models, action references for tool results, literals for scalars (float, str, int, bool)
42
+
43
+ EXAMPLE 1: Direct Actions (entire output)
44
+
45
+ Specs: report(Report: title, summary), search_data(SearchResults: items, count), quality_score(float)
46
+
47
+ Let me search first...
48
+ <lact broad>search(query="AI", limit=100)</lact>
49
+ Too much noise. Let me refine:
50
+ <lact focused>search(query="AI safety", limit=20)</lact>
51
+
52
+ Now I'll analyze the results:
53
+ <lvar Report.title t>AI Safety Analysis</lvar>
54
+ <lvar Report.summary s>Based on search results...</lvar>
55
+
56
+ ```lndl
57
+ OUT{report:[t, s], search_data:[focused], quality_score:0.85}
58
+ ```
59
+
60
+ Note: Only "focused" action executes (in OUT{}). "broad" was scratch work.
61
+
62
+ EXAMPLE 2: Mixing lvars and namespaced actions
63
+
64
+ Specs: report(Report: title, summary, footer)
65
+
66
+ <lvar Report.title t>Analysis Report</lvar>
67
+ <lact Report.summary summarize>generate_summary(data="metrics")</lact>
68
+ <lvar Report.footer f>End of Report</lvar>
69
+
70
+ ```lndl
71
+ OUT{report:[t, summarize, f]}
72
+ ```
73
+
74
+ Note: "summarize" action result fills Report.summary field, mixed with lvars for title and footer.
75
+
76
+ KEY POINTS
77
+
78
+ - Model.field provides explicit mapping (no ambiguity)
79
+ - Declare multiple versions (s1, s2), select final in OUT{}
80
+ - Think naturally: prose + variables intermixed
81
+ - Array syntax: field:[var1, var2] maps to model fields
82
+ - Scalar literals: field:0.8 or field:true for simple types
83
+ - Unused variables ignored but preserved for debugging
84
+
85
+ SCALARS vs MODELS vs ACTIONS
86
+
87
+ Scalars (float, str, int, bool):
88
+ - Can use direct literals: quality:0.8, is_draft:false
89
+ - Or single variable: quality:[q]
90
+ - Or single action: score:[calculate]
91
+
92
+ Models (Pydantic classes):
93
+ - Must use array syntax: report:[title, summary]
94
+ - Can mix lvars and namespaced actions: data:[title, api_call, summary]
95
+ - Direct action for entire model: data:[fetch_data] (single action, no lvars)
96
+ - Actions referenced are executed, results used as field values
97
+
98
+ Actions (tool/function calls):
99
+ - Namespaced: <lact Model.field name>function(args)</lact> (for mixing)
100
+ - Direct: <lact name>function(args)</lact> (entire output)
101
+ - Referenced by name in OUT{}: field:[action_name]
102
+ - Only executed if in OUT{} (scratch actions ignored)
103
+ - Use pythonic call syntax: search(query="text", limit=10)
104
+
105
+ ERRORS TO AVOID
106
+
107
+ <lvar title>value</lvar> # WRONG: Missing Model.field prefix
108
+ <lvar Report.title>val</var> # WRONG: Mismatched tags
109
+ <lact search>search(...)</lvar> # WRONG: Mismatched tags (should be </lact>)
110
+ OUT{report:Report(title=t)} # WRONG: No constructors, use arrays
111
+ OUT{report:[t, s2], reason:[c, a]} # WRONG: field name must match spec
112
+ OUT{quality_score:[x, y]} # WRONG: scalars need single var or literal
113
+ <lact Report.field data>search(...)</lact>
114
+ <lvar Report.field data>value</lvar>
115
+ OUT{field:[data]} # WRONG: name collision (both lvar and lact named "data")
116
+
117
+ CORRECT
118
+
119
+ <lvar Model.field alias>value</lvar> # Proper namespace for variables
120
+ <lact Model.field alias>function(args)</lact> # Namespaced action (for mixing)
121
+ <lact name>function(args)</lact> # Direct action (entire output)
122
+ OUT{report:[var1, var2]} # Array maps to model fields (lvars)
123
+ OUT{report:[var1, action1, var2]} # Mixing lvars and namespaced actions
124
+ OUT{data:[action_name]} # Direct action for entire field
125
+ OUT{quality_score:0.8} # Scalar literal
126
+ OUT{quality_score:[q]} # Scalar from variable
127
+ OUT{result:[action]} # Scalar from action result
128
+ """
129
+
130
+
131
+ def get_lndl_system_prompt() -> str:
132
+ """Get the LNDL system prompt for LLM guidance.
133
+
134
+ Returns:
135
+ LNDL system prompt string
136
+ """
137
+ return LNDL_SYSTEM_PROMPT.strip()