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.
- lionherd_core/__init__.py +84 -0
- lionherd_core/base/__init__.py +30 -0
- lionherd_core/base/_utils.py +295 -0
- lionherd_core/base/broadcaster.py +128 -0
- lionherd_core/base/element.py +300 -0
- lionherd_core/base/event.py +322 -0
- lionherd_core/base/eventbus.py +112 -0
- lionherd_core/base/flow.py +236 -0
- lionherd_core/base/graph.py +616 -0
- lionherd_core/base/node.py +212 -0
- lionherd_core/base/pile.py +811 -0
- lionherd_core/base/progression.py +261 -0
- lionherd_core/errors.py +104 -0
- lionherd_core/libs/__init__.py +2 -0
- lionherd_core/libs/concurrency/__init__.py +60 -0
- lionherd_core/libs/concurrency/_cancel.py +85 -0
- lionherd_core/libs/concurrency/_errors.py +80 -0
- lionherd_core/libs/concurrency/_patterns.py +238 -0
- lionherd_core/libs/concurrency/_primitives.py +253 -0
- lionherd_core/libs/concurrency/_priority_queue.py +135 -0
- lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
- lionherd_core/libs/concurrency/_task.py +58 -0
- lionherd_core/libs/concurrency/_utils.py +61 -0
- lionherd_core/libs/schema_handlers/__init__.py +35 -0
- lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
- lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
- lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
- lionherd_core/libs/schema_handlers/_typescript.py +153 -0
- lionherd_core/libs/string_handlers/__init__.py +15 -0
- lionherd_core/libs/string_handlers/_extract_json.py +65 -0
- lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
- lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
- lionherd_core/libs/string_handlers/_to_num.py +63 -0
- lionherd_core/ln/__init__.py +45 -0
- lionherd_core/ln/_async_call.py +314 -0
- lionherd_core/ln/_fuzzy_match.py +166 -0
- lionherd_core/ln/_fuzzy_validate.py +151 -0
- lionherd_core/ln/_hash.py +141 -0
- lionherd_core/ln/_json_dump.py +347 -0
- lionherd_core/ln/_list_call.py +110 -0
- lionherd_core/ln/_to_dict.py +373 -0
- lionherd_core/ln/_to_list.py +190 -0
- lionherd_core/ln/_utils.py +156 -0
- lionherd_core/lndl/__init__.py +62 -0
- lionherd_core/lndl/errors.py +30 -0
- lionherd_core/lndl/fuzzy.py +321 -0
- lionherd_core/lndl/parser.py +427 -0
- lionherd_core/lndl/prompt.py +137 -0
- lionherd_core/lndl/resolver.py +323 -0
- lionherd_core/lndl/types.py +287 -0
- lionherd_core/protocols.py +181 -0
- lionherd_core/py.typed +0 -0
- lionherd_core/types/__init__.py +46 -0
- lionherd_core/types/_sentinel.py +131 -0
- lionherd_core/types/base.py +341 -0
- lionherd_core/types/operable.py +133 -0
- lionherd_core/types/spec.py +313 -0
- lionherd_core/types/spec_adapters/__init__.py +10 -0
- lionherd_core/types/spec_adapters/_protocol.py +125 -0
- lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
- lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
- lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
- lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
- 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()
|