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 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
+ )