The Big One #43

Merged
k.marinus merged 93 commits from feat/reset-experiment-and-phase into dev 2026-01-26 19:20:45 +00:00
3 changed files with 143 additions and 45 deletions
Showing only changes of commit 756e1f0dc5 - Show all commits

View File

@@ -93,6 +93,33 @@ class Rule:
return f"{self.head} :- {self.body}." 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 @dataclass
class Plan: class Plan:
""" """
@@ -148,7 +175,7 @@ class AgentSpeakFile:
""" """
initial_beliefs: list[Rule] = field(default_factory=list) initial_beliefs: list[Rule] = field(default_factory=list)
inference_rules: list[Rule] = field(default_factory=list) inference_rules: list[Rule | PersistentRule] = field(default_factory=list)
plans: list[Plan] = field(default_factory=list) plans: list[Plan] = field(default_factory=list)
def __str__(self): def __str__(self):
@@ -161,7 +188,11 @@ class AgentSpeakFile:
if self.inference_rules: if self.inference_rules:
sections.append("// --- Inference Rules ---") sections.append("// --- Inference Rules ---")
sections.extend(str(rule) for rule in self.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("") sections.append("")
if self.plans: if self.plans:

View File

@@ -5,8 +5,6 @@ from functools import singledispatchmethod
from slugify import slugify from slugify import slugify
from control_backend.agents.bdi import BDICoreAgent from control_backend.agents.bdi import BDICoreAgent
# Import the AST we defined above
from control_backend.agents.bdi.asl_ast import ( from control_backend.agents.bdi.asl_ast import (
ActionLiteral, ActionLiteral,
AgentSpeakFile, AgentSpeakFile,
@@ -14,13 +12,13 @@ from control_backend.agents.bdi.asl_ast import (
BinaryOp, BinaryOp,
Expression, Expression,
GoalLiteral, GoalLiteral,
PersistentRule,
Plan, Plan,
Rule, Rule,
) )
from control_backend.agents.bdi.bdi_program_manager import test_program from control_backend.agents.bdi.bdi_program_manager import test_program
# Import your Pydantic models (adjust import based on your file structure)
from control_backend.schemas.program import ( from control_backend.schemas.program import (
BasicBelief,
Belief, Belief,
ConditionalNorm, ConditionalNorm,
GestureAction, GestureAction,
@@ -46,13 +44,17 @@ async def do_things():
f.write(program) f.write(program)
else: else:
# filename = "0test.asl" # filename = "0test.asl"
filename = "1766053943.asl" filename = "1766062491.asl"
bdi_agent = BDICoreAgent("BDICoreAgent", filename) bdi_agent = BDICoreAgent("BDICoreAgent", filename)
flag = asyncio.Event() flag = asyncio.Event()
await bdi_agent.start() await bdi_agent.start()
await flag.wait() await flag.wait()
def do_other_things():
print(AgentSpeakGenerator().generate(test_program))
class AgentSpeakGenerator: class AgentSpeakGenerator:
""" """
Converts a Pydantic Program behavior model into an AgentSpeak(L) AST, Converts a Pydantic Program behavior model into an AgentSpeak(L) AST,
@@ -118,6 +120,10 @@ class AgentSpeakGenerator:
# +user_said(Message) : phase(ID) <- !goal1; !goal2; !transition_phase. # +user_said(Message) : phase(ID) <- !goal1; !goal2; !transition_phase.
goal_actions = [ActionLiteral("-responded_this_turn")] goal_actions = [ActionLiteral("-responded_this_turn")]
goal_actions += [
ActionLiteral(f"!check_{self._slugify_str(keyword)}")
for keyword in self._get_keyword_conditionals(phase)
]
goal_actions += [ActionLiteral(f"!{self._slugify(g)}") for g in phase.goals] goal_actions += [ActionLiteral(f"!{self._slugify(g)}") for g in phase.goals]
goal_actions.append(ActionLiteral("!transition_phase")) goal_actions.append(ActionLiteral("!transition_phase"))
@@ -143,10 +149,20 @@ class AgentSpeakGenerator:
body=[ body=[
ActionLiteral(f'-phase("{phase.id}")'), ActionLiteral(f'-phase("{phase.id}")'),
ActionLiteral(f'+phase("{next_id}")'), ActionLiteral(f'+phase("{next_id}")'),
ActionLiteral("user_said(Anything)"),
ActionLiteral("-+user_said(Anything)"),
], ],
) )
) )
def _get_keyword_conditionals(self, phase: Phase) -> list[str]:
res = []
for belief in self._extract_basic_beliefs_from_phase(phase):
if isinstance(belief, KeywordBelief):
res.append(belief.keyword)
return res
# --- Section: Norms & Beliefs --- # --- Section: Norms & Beliefs ---
def _generate_norms(self, phase: Phase, asl: AgentSpeakFile): def _generate_norms(self, phase: Phase, asl: AgentSpeakFile):
@@ -174,21 +190,22 @@ class AgentSpeakGenerator:
though ASL engines often handle redefinition or we can use a set to track processed IDs. though ASL engines often handle redefinition or we can use a set to track processed IDs.
""" """
if isinstance(belief, KeywordBelief): if isinstance(belief, KeywordBelief):
# Rule: keyword_said("word") :- user_said(M) & .substring("word", M, P) & P >= 0. pass
kwd_slug = f'"{belief.keyword}"' # # Rule: keyword_said("word") :- user_said(M) & .substring("word", M, P) & P >= 0.
head = BeliefLiteral("keyword_said", [kwd_slug]) # kwd_slug = f'"{belief.keyword}"'
# head = BeliefLiteral("keyword_said", [kwd_slug])
# Avoid duplicates #
if any(str(r.head) == str(head) for r in asl.inference_rules): # # Avoid duplicates
return # if any(str(r.head) == str(head) for r in asl.inference_rules):
# return
body = BinaryOp( #
BeliefLiteral("user_said", ["Message"]), # body = BinaryOp(
"&", # BeliefLiteral("user_said", ["Message"]),
BinaryOp(f".substring({kwd_slug}, Message, Pos)", "&", "Pos >= 0"), # "&",
) # BinaryOp(f".substring({kwd_slug}, Message, Pos)", "&", "Pos >= 0"),
# )
asl.inference_rules.append(Rule(head=head, body=body)) #
# asl.inference_rules.append(Rule(head=head, body=body))
elif isinstance(belief, InferredBelief): elif isinstance(belief, InferredBelief):
self._ensure_belief_inference(belief.left, asl) self._ensure_belief_inference(belief.left, asl)
@@ -204,7 +221,7 @@ class AgentSpeakGenerator:
body = BinaryOp( body = BinaryOp(
self._belief_to_expr(belief.left), op_char, self._belief_to_expr(belief.right) self._belief_to_expr(belief.left), op_char, self._belief_to_expr(belief.right)
) )
asl.inference_rules.append(Rule(head=head, body=body)) asl.inference_rules.append(PersistentRule(head=head, body=body))
def _belief_to_expr(self, belief: Belief) -> Expression: def _belief_to_expr(self, belief: Belief) -> Expression:
if isinstance(belief, KeywordBelief): if isinstance(belief, KeywordBelief):
@@ -221,17 +238,26 @@ class AgentSpeakGenerator:
previous_goal = goal previous_goal = goal
def _generate_goal_plan_recursive( def _generate_goal_plan_recursive(
self, goal: Goal, phase_id: str, previous_goal: Goal | None, asl: AgentSpeakFile self,
goal: Goal,
phase_id: str,
previous_goal: Goal | None,
asl: AgentSpeakFile,
responded_needed: bool = True,
can_fail: bool = True,
): ):
goal_slug = self._slugify(goal) goal_slug = self._slugify(goal)
# phase(ID) & not responded_this_turn & not achieved_goal # phase(ID) & not responded_this_turn & not achieved_goal
context = [ context = [
BeliefLiteral("phase", [f'"{phase_id}"']), BeliefLiteral("phase", [f'"{phase_id}"']),
BeliefLiteral("responded_this_turn", negated=True),
BeliefLiteral(f"achieved_{goal_slug}", negated=True),
] ]
if responded_needed:
context.append(BeliefLiteral("responded_this_turn", negated=True))
if can_fail:
context.append(BeliefLiteral(f"achieved_{goal_slug}", negated=True))
if previous_goal: if previous_goal:
prev_slug = self._slugify(previous_goal) prev_slug = self._slugify(previous_goal)
context.append(BeliefLiteral(f"achieved_{prev_slug}")) context.append(BeliefLiteral(f"achieved_{prev_slug}"))
@@ -256,6 +282,9 @@ class AgentSpeakGenerator:
body_actions.append(ActionLiteral(f"+achieved_{goal_slug}")) body_actions.append(ActionLiteral(f"+achieved_{goal_slug}"))
asl.plans.append(Plan(trigger=GoalLiteral(goal_slug), context=context, body=body_actions)) asl.plans.append(Plan(trigger=GoalLiteral(goal_slug), context=context, body=body_actions))
asl.plans.append(
Plan(trigger=GoalLiteral(goal_slug), context=[], body=[ActionLiteral("true")])
)
prev_sub = None prev_sub = None
for sub_goal in sub_goals_to_process: for sub_goal in sub_goals_to_process:
@@ -265,6 +294,28 @@ class AgentSpeakGenerator:
# --- Section: Triggers --- # --- Section: Triggers ---
def _generate_triggers(self, phase: Phase, asl: AgentSpeakFile): def _generate_triggers(self, phase: Phase, asl: AgentSpeakFile):
for keyword in self._get_keyword_conditionals(phase):
asl.plans.append(
Plan(
trigger=GoalLiteral(f"check_{self._slugify_str(keyword)}"),
context=[
ActionLiteral(
f'user_said(Message) & .substring("{keyword}", Message, Pos) & Pos >= 0'
)
],
body=[
ActionLiteral(f'+keyword_said("{keyword}")'),
ActionLiteral(f'-keyword_said("{keyword}")'),
],
)
)
asl.plans.append(
Plan(
trigger=GoalLiteral(f"check_{self._slugify_str(keyword)}"),
body=[ActionLiteral("true")],
)
)
for trigger in phase.triggers: for trigger in phase.triggers:
self._ensure_belief_inference(trigger.condition, asl) self._ensure_belief_inference(trigger.condition, asl)
@@ -300,31 +351,18 @@ class AgentSpeakGenerator:
# Recurse for triggered goals # Recurse for triggered goals
prev_sub = None prev_sub = None
for sub_goal in sub_goals: for sub_goal in sub_goals:
self._generate_goal_plan_recursive(sub_goal, str(phase.id), prev_sub, asl) self._generate_goal_plan_recursive(
sub_goal, str(phase.id), prev_sub, asl, False, False
)
prev_sub = sub_goal prev_sub = sub_goal
# --- Section: Fallbacks --- # --- Section: Fallbacks ---
def _generate_fallbacks(self, program: Program, asl: AgentSpeakFile): def _generate_fallbacks(self, program: Program, asl: AgentSpeakFile):
for phase in program.phases:
for goal in phase.goals:
self._generate_goal_fallbacks_recursive(goal, asl)
asl.plans.append( asl.plans.append(
Plan(trigger=GoalLiteral("transition_phase"), context=[], body=[ActionLiteral("true")]) Plan(trigger=GoalLiteral("transition_phase"), context=[], body=[ActionLiteral("true")])
) )
def _generate_goal_fallbacks_recursive(self, goal: Goal, asl: AgentSpeakFile):
goal_slug = self._slugify(goal)
asl.plans.append(
Plan(trigger=GoalLiteral(goal_slug), context=[], body=[ActionLiteral("true")])
)
for step in goal.plan.steps:
if not isinstance(step, Goal):
continue
self._generate_goal_fallbacks_recursive(step, asl)
# --- Helpers --- # --- Helpers ---
@singledispatchmethod @singledispatchmethod
@@ -354,6 +392,34 @@ class AgentSpeakGenerator:
def _slugify_str(self, text: str) -> str: def _slugify_str(self, text: str) -> str:
return slugify(text, separator="_", stopwords=["a", "an", "the", "we", "you", "I"]) return slugify(text, separator="_", stopwords=["a", "an", "the", "we", "you", "I"])
def _extract_basic_beliefs_from_program(self, program: Program) -> list[BasicBelief]:
beliefs = []
for phase in program.phases:
beliefs.extend(self._extract_basic_beliefs_from_phase(phase))
return beliefs
def _extract_basic_beliefs_from_phase(self, phase: Phase) -> list[BasicBelief]:
beliefs = []
for norm in phase.norms:
if isinstance(norm, ConditionalNorm):
beliefs += self._extract_basic_beliefs_from_belief(norm.condition)
for trigger in phase.triggers:
beliefs += self._extract_basic_beliefs_from_belief(trigger.condition)
return beliefs
def _extract_basic_beliefs_from_belief(self, belief: Belief) -> list[BasicBelief]:
if isinstance(belief, InferredBelief):
return self._extract_basic_beliefs_from_belief(
belief.left
) + self._extract_basic_beliefs_from_belief(belief.right)
return [belief]
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(do_things()) asyncio.run(do_things())
# do_other_things()y

View File

@@ -89,9 +89,9 @@ class BDICoreAgent(BaseAgent):
the agent has deferred intentions (deadlines). the agent has deferred intentions (deadlines).
""" """
while self._running: while self._running:
await ( # await (
self._wake_bdi_loop.wait() # self._wake_bdi_loop.wait()
) # gets set whenever there's an update to the belief base # ) # gets set whenever there's an update to the belief base
# Agent knows when it's expected to have to do its next thing # Agent knows when it's expected to have to do its next thing
maybe_more_work = True maybe_more_work = True
@@ -168,6 +168,7 @@ class BDICoreAgent(BaseAgent):
:param args: Arguments for the belief. :param args: Arguments for the belief.
""" """
# new_args = (agentspeak.Literal(arg) for arg in args) # TODO: Eventually support multiple # new_args = (agentspeak.Literal(arg) for arg in args) # TODO: Eventually support multiple
args = args or []
if args: if args:
merged_args = DELIMITER.join(arg for arg in args) merged_args = DELIMITER.join(arg for arg in args)
new_args = (agentspeak.Literal(merged_args),) new_args = (agentspeak.Literal(merged_args),)