204 lines
5.2 KiB
Python
204 lines
5.2 KiB
Python
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)
|