lmdl 0.1.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.
- lmdl/__init__.py +80 -0
- lmdl/convert.py +134 -0
- lmdl/nl.py +118 -0
- lmdl/pddl.py +654 -0
- lmdl/prompts.py +90 -0
- lmdl/schema.py +271 -0
- lmdl-0.1.0.dist-info/METADATA +71 -0
- lmdl-0.1.0.dist-info/RECORD +12 -0
- lmdl-0.1.0.dist-info/WHEEL +5 -0
- lmdl-0.1.0.dist-info/entry_points.txt +2 -0
- lmdl-0.1.0.dist-info/licenses/LICENSE +396 -0
- lmdl-0.1.0.dist-info/top_level.txt +1 -0
lmdl/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LMDL: Language-Model Domain Language.
|
|
3
|
+
|
|
4
|
+
The base planning schema: sorts, predicates, actions, problem instances,
|
|
5
|
+
PDDL compilation, and template-driven NL rendering. The pydantic models
|
|
6
|
+
ignore unknown JSON fields and subclass cleanly, so downstream projects can
|
|
7
|
+
extend the schema without changes here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from lmdl.schema import (
|
|
11
|
+
ActionParam,
|
|
12
|
+
ActionSpec,
|
|
13
|
+
AtomTemplate,
|
|
14
|
+
DomainSchema,
|
|
15
|
+
GroundAtom,
|
|
16
|
+
PredicateArg,
|
|
17
|
+
PredicateSpec,
|
|
18
|
+
PrimitiveRegistry,
|
|
19
|
+
ProblemSpec,
|
|
20
|
+
domain_profile_json_schema,
|
|
21
|
+
dumps_domain_schema,
|
|
22
|
+
load_domain_schema,
|
|
23
|
+
load_problem_spec,
|
|
24
|
+
save_domain_schema,
|
|
25
|
+
save_problem_spec,
|
|
26
|
+
)
|
|
27
|
+
from lmdl.nl import domain_nl, format_atom, problem_nl, render_atom
|
|
28
|
+
from lmdl import prompts
|
|
29
|
+
from lmdl.pddl import (
|
|
30
|
+
Atom,
|
|
31
|
+
action_from_pddl,
|
|
32
|
+
build_domain,
|
|
33
|
+
build_problem,
|
|
34
|
+
build_problem_from_spec,
|
|
35
|
+
domain_from_pddl,
|
|
36
|
+
domain_types_from_pddl,
|
|
37
|
+
extract_action_pddl,
|
|
38
|
+
load_problem,
|
|
39
|
+
minimal_problem,
|
|
40
|
+
predicates_from_pddl,
|
|
41
|
+
problem_from_pddl,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# base planning schema
|
|
46
|
+
"ActionParam",
|
|
47
|
+
"ActionSpec",
|
|
48
|
+
"AtomTemplate",
|
|
49
|
+
"DomainSchema",
|
|
50
|
+
"GroundAtom",
|
|
51
|
+
"PredicateArg",
|
|
52
|
+
"PredicateSpec",
|
|
53
|
+
"PrimitiveRegistry",
|
|
54
|
+
"ProblemSpec",
|
|
55
|
+
"domain_profile_json_schema",
|
|
56
|
+
"dumps_domain_schema",
|
|
57
|
+
"load_domain_schema",
|
|
58
|
+
"load_problem_spec",
|
|
59
|
+
"save_domain_schema",
|
|
60
|
+
"save_problem_spec",
|
|
61
|
+
# natural-language rendering and prompt snippets
|
|
62
|
+
"prompts",
|
|
63
|
+
"domain_nl",
|
|
64
|
+
"format_atom",
|
|
65
|
+
"problem_nl",
|
|
66
|
+
"render_atom",
|
|
67
|
+
# PDDL compilation + ingestion
|
|
68
|
+
"Atom",
|
|
69
|
+
"action_from_pddl",
|
|
70
|
+
"build_domain",
|
|
71
|
+
"build_problem",
|
|
72
|
+
"build_problem_from_spec",
|
|
73
|
+
"domain_from_pddl",
|
|
74
|
+
"domain_types_from_pddl",
|
|
75
|
+
"extract_action_pddl",
|
|
76
|
+
"load_problem",
|
|
77
|
+
"minimal_problem",
|
|
78
|
+
"predicates_from_pddl",
|
|
79
|
+
"problem_from_pddl",
|
|
80
|
+
]
|
lmdl/convert.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``lmdl-convert``: convert between PDDL files and canonical LMDL JSON.
|
|
3
|
+
|
|
4
|
+
The input kind is detected from its content and converted to the other
|
|
5
|
+
representation:
|
|
6
|
+
|
|
7
|
+
PDDL domain (define (domain ...)) -> LMDL domain JSON
|
|
8
|
+
PDDL problem (define (problem ...)) -> LMDL problem JSON
|
|
9
|
+
LMDL domain JSON with sorts/predicates/actions -> PDDL domain
|
|
10
|
+
LMDL problem JSON with objects/init/goal -> PDDL problem
|
|
11
|
+
|
|
12
|
+
PDDL -> LMDL is lossy upward: PDDL carries no NL, so the ``description`` and
|
|
13
|
+
``template`` fields come out empty for authors to fill in. LMDL -> PDDL is
|
|
14
|
+
the standard compilation path (``build_domain`` / ``build_problem_from_spec``).
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
lmdl-convert domain.pddl # LMDL JSON to stdout
|
|
18
|
+
lmdl-convert domain.lmdl.json --name blocksworld # PDDL to stdout
|
|
19
|
+
lmdl-convert instance-0.pddl -o instance-0.problem.lmdl.json
|
|
20
|
+
python -m lmdl.convert ... # equivalent invocation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
from lmdl.pddl import (
|
|
33
|
+
build_domain,
|
|
34
|
+
build_problem_from_spec,
|
|
35
|
+
domain_from_pddl,
|
|
36
|
+
problem_from_pddl,
|
|
37
|
+
)
|
|
38
|
+
from lmdl.schema import ProblemSpec, dumps_domain_schema, load_domain_schema
|
|
39
|
+
|
|
40
|
+
# (format, kind) pairs returned by _detect
|
|
41
|
+
_PDDL_DOMAIN_RE = r"\(\s*define\s*\(\s*domain\b"
|
|
42
|
+
_PDDL_PROBLEM_RE = r"\(\s*define\s*\(\s*problem\b"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _detect(text: str) -> Tuple[str, str]:
|
|
46
|
+
"""Detect an input file's representation and kind from its content.
|
|
47
|
+
|
|
48
|
+
:param text: The file's full text.
|
|
49
|
+
:returns: ``(format, kind)`` with format ``"pddl"`` | ``"lmdl"`` and kind
|
|
50
|
+
``"domain"`` | ``"problem"``.
|
|
51
|
+
:raises ValueError: If the content is neither recognizable PDDL nor
|
|
52
|
+
recognizable LMDL JSON.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
payload = json.loads(text)
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
if re.search(_PDDL_DOMAIN_RE, text, re.IGNORECASE):
|
|
58
|
+
return "pddl", "domain"
|
|
59
|
+
if re.search(_PDDL_PROBLEM_RE, text, re.IGNORECASE):
|
|
60
|
+
return "pddl", "problem"
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"input is neither LMDL JSON nor a PDDL (define (domain|problem ...))"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not isinstance(payload, dict):
|
|
66
|
+
raise ValueError("LMDL JSON input must be an object")
|
|
67
|
+
if {"init", "goal", "objects"} & set(payload):
|
|
68
|
+
return "lmdl", "problem"
|
|
69
|
+
if {"predicates", "actions", "sorts"} & set(payload):
|
|
70
|
+
return "lmdl", "domain"
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"JSON input has neither problem keys (objects/init/goal) nor "
|
|
73
|
+
"domain keys (sorts/predicates/actions)"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def convert(path: Path, name: str = "domain") -> str:
|
|
78
|
+
"""Convert one file to the opposite representation.
|
|
79
|
+
|
|
80
|
+
:param path: The PDDL or LMDL JSON file to convert.
|
|
81
|
+
:param name: The domain name to write into compiled PDDL headers
|
|
82
|
+
(LMDL carries no domain name; ignored for other conversions).
|
|
83
|
+
:returns: The converted text.
|
|
84
|
+
:raises ValueError: If the input kind cannot be detected or parsed.
|
|
85
|
+
"""
|
|
86
|
+
text = path.read_text()
|
|
87
|
+
fmt, kind = _detect(text)
|
|
88
|
+
|
|
89
|
+
if fmt == "pddl" and kind == "domain":
|
|
90
|
+
return dumps_domain_schema(domain_from_pddl(text))
|
|
91
|
+
if fmt == "pddl" and kind == "problem":
|
|
92
|
+
return problem_from_pddl(text).model_dump_json(indent=2)
|
|
93
|
+
if kind == "domain":
|
|
94
|
+
return build_domain(load_domain_schema(path), name)
|
|
95
|
+
return build_problem_from_spec(ProblemSpec.model_validate_json(text))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
99
|
+
"""Parse command-line arguments and run one conversion.
|
|
100
|
+
|
|
101
|
+
:param argv: Arguments to parse instead of ``sys.argv`` (for tests).
|
|
102
|
+
"""
|
|
103
|
+
parser = argparse.ArgumentParser(
|
|
104
|
+
prog="lmdl-convert",
|
|
105
|
+
description="Convert a PDDL domain/problem file to LMDL JSON, or back.",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument("input", type=Path, help="PDDL or LMDL JSON file")
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"-o",
|
|
110
|
+
"--output",
|
|
111
|
+
type=Path,
|
|
112
|
+
default=None,
|
|
113
|
+
help="Write here instead of stdout",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--name",
|
|
117
|
+
default="domain",
|
|
118
|
+
help="Domain name for compiled PDDL output (default: domain)",
|
|
119
|
+
)
|
|
120
|
+
args = parser.parse_args(argv)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
result = convert(args.input, name=args.name)
|
|
124
|
+
except (ValueError, FileNotFoundError) as e:
|
|
125
|
+
sys.exit(f"lmdl-convert: {e}")
|
|
126
|
+
|
|
127
|
+
if args.output is not None:
|
|
128
|
+
args.output.write_text(result + "\n")
|
|
129
|
+
else:
|
|
130
|
+
print(result)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|
lmdl/nl.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Natural-language rendering of domains, ground atoms, and problem instances.
|
|
3
|
+
|
|
4
|
+
Each predicate in a domain profile carries an NL ``template`` with ``{arg}``
|
|
5
|
+
placeholders (e.g. ``"block {x} is on top of block {y}"``); rendering a state
|
|
6
|
+
or goal is placeholder substitution -- no LLM required. ``domain_nl`` is the
|
|
7
|
+
domain-side counterpart: it renders the predicate and action vocabulary from
|
|
8
|
+
the schema's authored ``description`` fields, with the formal signatures and
|
|
9
|
+
pre/add/delete structure kept symbolic.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from typing import Dict, List, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
from lmdl.schema import DomainSchema, ProblemSpec
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_atom(atom: Sequence[str]) -> str:
|
|
21
|
+
"""Format an atom as compact ``pred(a, b)`` text (bare ``pred`` when
|
|
22
|
+
zero-arity).
|
|
23
|
+
|
|
24
|
+
Works on both ground atoms (object arguments) and atom templates
|
|
25
|
+
(variable arguments).
|
|
26
|
+
|
|
27
|
+
:param atom: The atom as ``(name, *args)``.
|
|
28
|
+
:returns: The atom as compact symbolic text.
|
|
29
|
+
"""
|
|
30
|
+
name, args = atom[0], atom[1:]
|
|
31
|
+
return name if not args else f"{name}({', '.join(args)})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_atom(atom: Sequence[str], schema: DomainSchema) -> str:
|
|
35
|
+
"""Render a ground atom ``(pred, obj1, obj2)`` as an NL sentence via its
|
|
36
|
+
predicate's template.
|
|
37
|
+
|
|
38
|
+
Falls back to the symbolic form ``pred(obj1, obj2)`` when the predicate is
|
|
39
|
+
unknown, has no template, or the atom's arity doesn't match.
|
|
40
|
+
|
|
41
|
+
:param atom: The ground atom as ``(predicate_name, *object_names)``.
|
|
42
|
+
:param schema: The domain schema supplying the predicate templates.
|
|
43
|
+
:returns: One NL sentence (or the symbolic fallback form).
|
|
44
|
+
"""
|
|
45
|
+
name, objs = atom[0], list(atom[1:])
|
|
46
|
+
pred = schema.predicates.get(name)
|
|
47
|
+
if pred is None or not pred.template or len(pred.args) != len(objs):
|
|
48
|
+
return format_atom(atom)
|
|
49
|
+
# Substitute by whole placeholder token rather than str.format so that
|
|
50
|
+
# hyphenated arg names (illegal as format fields) still work.
|
|
51
|
+
text = pred.template
|
|
52
|
+
for arg, obj in zip(pred.args, objs):
|
|
53
|
+
text = text.replace("{" + arg.name + "}", obj)
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def domain_nl(schema: DomainSchema, exclude_action: Optional[str] = None) -> str:
|
|
58
|
+
"""Render a domain's vocabulary as text from its authored descriptions.
|
|
59
|
+
|
|
60
|
+
Predicates and actions are listed with their typed signatures and NL
|
|
61
|
+
``description`` fields; each action's pre/add/delete atoms stay symbolic
|
|
62
|
+
(``clear(underob), holding(ob)``). No LLM required.
|
|
63
|
+
|
|
64
|
+
:param schema: The domain schema to render.
|
|
65
|
+
:param exclude_action: Optionally omit one action, e.g. so it can serve
|
|
66
|
+
as a held-out generation target.
|
|
67
|
+
:returns: The domain description, one predicate/action per block.
|
|
68
|
+
"""
|
|
69
|
+
lines = ["Predicates:"]
|
|
70
|
+
for name, pred in schema.predicates.items():
|
|
71
|
+
sig = ", ".join(f"{a.name}: {a.type}" for a in pred.args) if pred.args else ""
|
|
72
|
+
desc = f" — {pred.description}" if pred.description else ""
|
|
73
|
+
lines.append(f" {name}({sig}){desc}")
|
|
74
|
+
|
|
75
|
+
lines.append("")
|
|
76
|
+
lines.append("Actions:")
|
|
77
|
+
for name, action in schema.actions.items():
|
|
78
|
+
if name == exclude_action:
|
|
79
|
+
continue
|
|
80
|
+
sig = ", ".join(f"{p.name}: {p.type}" for p in action.params)
|
|
81
|
+
desc = f" — {action.description}" if action.description else ""
|
|
82
|
+
lines.append(f" {name}({sig}){desc}")
|
|
83
|
+
|
|
84
|
+
pre_str = ", ".join(format_atom(t) for t in action.pre) or "—"
|
|
85
|
+
add_str = ", ".join(format_atom(t) for t in action.add) or "—"
|
|
86
|
+
del_str = ", ".join(format_atom(t) for t in action.delete) or "—"
|
|
87
|
+
lines.append(f" preconditions: {pre_str}")
|
|
88
|
+
lines.append(f" add: {add_str}")
|
|
89
|
+
lines.append(f" delete: {del_str}")
|
|
90
|
+
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def problem_nl(problem: ProblemSpec, schema: DomainSchema) -> str:
|
|
95
|
+
"""Render a problem instance as NL: objects grouped by sort, then the
|
|
96
|
+
initial state and goal as one templated sentence per ground atom.
|
|
97
|
+
|
|
98
|
+
:param problem: The problem instance to render.
|
|
99
|
+
:param schema: The domain schema supplying the predicate templates.
|
|
100
|
+
:returns: The full NL problem statement (objects, initial state, goal).
|
|
101
|
+
"""
|
|
102
|
+
by_sort: Dict[str, List[str]] = defaultdict(list)
|
|
103
|
+
for obj, sort in problem.objects.items():
|
|
104
|
+
by_sort[sort].append(obj)
|
|
105
|
+
obj_lines = [
|
|
106
|
+
f" {sort}: {', '.join(sorted(objs))}" for sort, objs in sorted(by_sort.items())
|
|
107
|
+
]
|
|
108
|
+
init_lines = [f" - {render_atom(a, schema)}" for a in sorted(problem.init)]
|
|
109
|
+
goal_lines = [f" - {render_atom(a, schema)}" for a in problem.goal]
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
"Objects:\n"
|
|
113
|
+
+ "\n".join(obj_lines)
|
|
114
|
+
+ "\n\nInitial state:\n"
|
|
115
|
+
+ "\n".join(init_lines)
|
|
116
|
+
+ "\n\nGoal:\n"
|
|
117
|
+
+ "\n".join(goal_lines)
|
|
118
|
+
)
|