import typing from dataclasses import dataclass, field # --- Types --- @dataclass class BeliefLiteral: """ Represents a literal or atom. Example: phase(1), user_said("hello"), ~started """ functor: str args: list[str] = field(default_factory=list) negated: bool = False def __str__(self): # In ASL, 'not' is usually for closed-world assumption (prolog style), # '~' is for explicit negation in beliefs. # For simplicity in behavior trees, we often use 'not' for conditions. prefix = "not " if self.negated else "" if not self.args: return f"{prefix}{self.functor}" # Clean args to ensure strings are quoted if they look like strings, # but usually the converter handles the quoting of string literals. args_str = ", ".join(self.args) return f"{prefix}{self.functor}({args_str})" @dataclass class GoalLiteral: name: str def __str__(self): return f"!{self.name}" @dataclass class ActionLiteral: """ Represents a step in a plan body. Example: .say("Hello") or !achieve_goal """ code: str def __str__(self): return self.code @dataclass class BinaryOp: """ Represents logical operations. Example: (A & B) | C """ left: "Expression | str" operator: typing.Literal["&", "|"] right: "Expression | str" def __str__(self): l_str = str(self.left) r_str = str(self.right) if isinstance(self.left, BinaryOp): l_str = f"({l_str})" if isinstance(self.right, BinaryOp): r_str = f"({r_str})" return f"{l_str} {self.operator} {r_str}" Literal = BeliefLiteral | GoalLiteral | ActionLiteral Expression = Literal | BinaryOp | str @dataclass class Rule: """ Represents an inference rule. Example: head :- body. """ head: Expression body: Expression | None = None def __str__(self): if not self.body: return f"{self.head}." return f"{self.head} :- {self.body}." @dataclass class PersistentRule: """ Represents an inference rule, where the inferred belief is persistent when formed. """ head: Expression body: Expression def __str__(self): if not self.body: raise Exception("Rule without body should not be persistent.") lines = [] if isinstance(self.body, BinaryOp): lines.append(f"+{self.body.left}") if self.body.operator == "&": lines.append(f" : {self.body.right}") lines.append(f" <- +{self.head}.") if self.body.operator == "|": lines.append(f"+{self.body.right}") lines.append(f" <- +{self.head}.") return "\n".join(lines) @dataclass class Plan: """ Represents a plan. Syntax: +trigger : context <- body. """ trigger: BeliefLiteral | GoalLiteral context: list[Expression] = field(default_factory=list) body: list[ActionLiteral] = field(default_factory=list) def __str__(self): # Indentation settings INDENT = " " ARROW = "\n <- " COLON = "\n : " # Build Header header = f"+{self.trigger}" if self.context: ctx_str = f" &\n{INDENT}".join(str(c) for c in self.context) header += f"{COLON}{ctx_str}" # Case 1: Empty body if not self.body: return f"{header}." # Case 2: Short body (optional optimization, keeping it uniform usually better) header += ARROW lines = [] # We start the first action on the same line or next line. # Let's put it on the next line for readability if there are multiple. if len(self.body) == 1: return f"{header}{self.body[0]}." # First item lines.append(f"{header}{self.body[0]};") # Middle items for item in self.body[1:-1]: lines.append(f"{INDENT}{item};") # Last item lines.append(f"{INDENT}{self.body[-1]}.") return "\n".join(lines) @dataclass class AgentSpeakFile: """ Root element representing the entire generated file. """ initial_beliefs: list[Rule] = field(default_factory=list) inference_rules: list[Rule | PersistentRule] = field(default_factory=list) plans: list[Plan] = field(default_factory=list) def __str__(self): sections = [] if self.initial_beliefs: sections.append("// --- Initial Beliefs & Facts ---") sections.extend(str(rule) for rule in self.initial_beliefs) sections.append("") if self.inference_rules: sections.append("// --- Inference Rules ---") sections.extend(str(rule) for rule in self.inference_rules if isinstance(rule, Rule)) sections.append("") sections.extend( str(rule) for rule in self.inference_rules if isinstance(rule, PersistentRule) ) sections.append("") if self.plans: sections.append("// --- Plans ---") # Separate plans by a newline for readability sections.extend(str(plan) + "\n" for plan in self.plans) return "\n".join(sections)