From 3406e9ac2f468b9ad2e575378007d5e0736c0ef7 Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:26:44 +0100 Subject: [PATCH] feat: make the pipeline work with Program and AgentSpeak ref: N25B-429 --- .../agents/bdi/agentspeak_generator.py | 80 +++++++++++++++---- .../agents/bdi/bdi_core_agent.py | 31 +++---- .../agents/bdi/belief_collector_agent.py | 2 +- .../agents/bdi/default_behavior.asl | 5 ++ src/control_backend/agents/bdi/rules.asl | 6 -- src/control_backend/agents/bdi/test.asl | 0 src/control_backend/schemas/program.py | 2 +- 7 files changed, 88 insertions(+), 38 deletions(-) create mode 100644 src/control_backend/agents/bdi/default_behavior.asl delete mode 100644 src/control_backend/agents/bdi/rules.asl delete mode 100644 src/control_backend/agents/bdi/test.asl diff --git a/src/control_backend/agents/bdi/agentspeak_generator.py b/src/control_backend/agents/bdi/agentspeak_generator.py index 4f892e1..a446f13 100644 --- a/src/control_backend/agents/bdi/agentspeak_generator.py +++ b/src/control_backend/agents/bdi/agentspeak_generator.py @@ -42,9 +42,9 @@ class AgentSpeakGenerator: def generate(self, program: Program) -> str: self._asp = AstProgram() - self._asp.rules.append(AstRule(AstLiteral("phase", [AstString("start")]))) + self._asp.rules.append(AstRule(self._astify(program.phases[0]))) self._add_keyword_inference() - self._add_response_goal() + self._add_default_plans() self._process_phases(program.phases) @@ -66,11 +66,16 @@ class AgentSpeakGenerator: ) ) - def _add_response_goal(self): + def _add_default_plans(self): + self._add_reply_with_goal_plan() + self._add_say_plan() + self._add_reply_plan() + + def _add_reply_with_goal_plan(self): self._asp.plans.append( AstPlan( TriggerType.ADDED_GOAL, - AstLiteral("generate_response_with_goal", [AstVar("Goal")]), + AstLiteral("reply_with_goal", [AstVar("Goal")]), [AstLiteral("user_said", [AstVar("Message")])], [ AstStatement(StatementType.ADD_BELIEF, AstLiteral("responded_this_turn")), @@ -91,12 +96,59 @@ class AgentSpeakGenerator: ) ) + def _add_say_plan(self): + self._asp.plans.append( + AstPlan( + TriggerType.ADDED_GOAL, + AstLiteral("say", [AstVar("Text")]), + [], + [ + AstStatement(StatementType.ADD_BELIEF, AstLiteral("responded_this_turn")), + AstStatement(StatementType.DO_ACTION, AstLiteral("say", [AstVar("Text")])), + ], + ) + ) + + def _add_reply_plan(self): + self._asp.plans.append( + AstPlan( + TriggerType.ADDED_GOAL, + AstLiteral("reply"), + [AstLiteral("user_said", [AstVar("Message")])], + [ + AstStatement(StatementType.ADD_BELIEF, AstLiteral("responded_this_turn")), + AstStatement( + StatementType.DO_ACTION, + AstLiteral( + "findall", + [AstVar("Norm"), AstLiteral("norm", [AstVar("Norm")]), AstVar("Norms")], + ), + ), + AstStatement( + StatementType.DO_ACTION, + AstLiteral("reply", [AstVar("Message"), AstVar("Norms")]), + ), + ], + ) + ) + def _process_phases(self, phases: list[Phase]) -> None: for curr_phase, next_phase in zip([None] + phases, phases + [None], strict=True): if curr_phase: self._process_phase(curr_phase) self._add_phase_transition(curr_phase, next_phase) + # End phase behavior + # When deleting this, the entire `reply` plan and action can be deleted + self._asp.plans.append( + AstPlan( + type=TriggerType.ADDED_BELIEF, + trigger_literal=AstLiteral("user_said", [AstVar("Message")]), + context=[AstLiteral("phase", [AstString("end")])], + body=[AstStatement(StatementType.ACHIEVE_GOAL, AstLiteral("reply"))], + ) + ) + def _process_phase(self, phase: Phase) -> None: for norm in phase.norms: self._process_norm(norm, phase) @@ -112,14 +164,14 @@ class AgentSpeakGenerator: self._process_trigger(trigger, phase) def _add_phase_transition(self, from_phase: Phase | None, to_phase: Phase | None) -> None: - from_phase_ast = ( - self._astify(from_phase) if from_phase else AstLiteral("phase", [AstString("start")]) - ) + if from_phase is None: + return + from_phase_ast = self._astify(from_phase) to_phase_ast = ( self._astify(to_phase) if to_phase else AstLiteral("phase", [AstString("end")]) ) - context = [from_phase_ast] + context = [from_phase_ast, ~AstLiteral("responded_this_turn")] if from_phase and from_phase.goals: context.append(self._astify(from_phase.goals[-1], achieved=True)) @@ -221,14 +273,10 @@ class AgentSpeakGenerator: def _step_to_statement(self, step: PlanElement) -> AstStatement: match step: - case Goal() as g: - return AstStatement(StatementType.ACHIEVE_GOAL, self._astify(g)) - case SpeechAction() | GestureAction() as a: + case Goal() | SpeechAction() | LLMAction() as a: + return AstStatement(StatementType.ACHIEVE_GOAL, self._astify(a)) + case GestureAction() as a: return AstStatement(StatementType.DO_ACTION, self._astify(a)) - case LLMAction() as la: - return AstStatement( - StatementType.ACHIEVE_GOAL, self._astify(la) - ) # LLM action is a goal in ASL # TODO: separate handling of keyword and others def _process_trigger(self, trigger: Trigger, phase: Phase) -> None: @@ -318,7 +366,7 @@ class AgentSpeakGenerator: @_astify.register def _(self, la: LLMAction) -> AstExpression: - return AstLiteral("generate_response_with_goal", [AstString(la.goal)]) + return AstLiteral("reply_with_goal", [AstString(la.goal)]) @staticmethod def _slugify_str(text: str) -> str: diff --git a/src/control_backend/agents/bdi/bdi_core_agent.py b/src/control_backend/agents/bdi/bdi_core_agent.py index 7da6708..249b6ee 100644 --- a/src/control_backend/agents/bdi/bdi_core_agent.py +++ b/src/control_backend/agents/bdi/bdi_core_agent.py @@ -48,6 +48,7 @@ class BDICoreAgent(BaseAgent): # Deep copy because we don't actually want to modify the standard actions globally self.actions = copy.deepcopy(agentspeak.stdlib.actions) self._wake_bdi_loop = asyncio.Event() + self._bdi_loop_task = None async def setup(self) -> None: """ @@ -64,7 +65,7 @@ class BDICoreAgent(BaseAgent): await self._load_asl() # Start the BDI cycle loop - self.add_behavior(self._bdi_loop()) + self._bdi_loop_task = self.add_behavior(self._bdi_loop()) self._wake_bdi_loop.set() self.logger.debug("Setup complete.") @@ -72,7 +73,7 @@ class BDICoreAgent(BaseAgent): """ Load and parse the AgentSpeak source file. """ - file_name = file_name or "src/control_backend/agents/bdi/rules.asl" + file_name = file_name or "src/control_backend/agents/bdi/default_behavior.asl" try: with open(file_name) as source: @@ -135,7 +136,10 @@ class BDICoreAgent(BaseAgent): # New agentspeak file if msg.thread == "new_program": + if self._bdi_loop_task: + self._bdi_loop_task.cancel() await self._load_asl(msg.body) + self.add_behavior(self._bdi_loop()) # The message was not a belief, handle special cases based on sender match msg.sender: @@ -246,20 +250,18 @@ class BDICoreAgent(BaseAgent): the function expects (which will be located in `term.args`). """ - @self.actions.add(".reply", 3) + @self.actions.add(".reply", 2) def _reply(agent: "BDICoreAgent", term, intention): """ Let the LLM generate a response to a user's utterance with the current norms and goals. """ message_text = agentspeak.grounded(term.args[0], intention.scope) norms = agentspeak.grounded(term.args[1], intention.scope) - goals = agentspeak.grounded(term.args[2], intention.scope) self.logger.debug("Norms: %s", norms) - self.logger.debug("Goals: %s", goals) self.logger.debug("User text: %s", message_text) - asyncio.create_task(self._send_to_llm(str(message_text), str(norms), str(goals))) + self.add_behavior(self._send_to_llm(str(message_text), str(norms), "")) yield @self.actions.add(".reply_with_goal", 3) @@ -278,7 +280,7 @@ class BDICoreAgent(BaseAgent): norms, goal, ) - # asyncio.create_task(self._send_to_llm(str(message_text), norms, str(goal))) + self.add_behavior(self._send_to_llm(str(message_text), str(norms), str(goal))) yield @self.actions.add(".say", 1) @@ -290,13 +292,14 @@ class BDICoreAgent(BaseAgent): self.logger.debug('"say" action called with text=%s', message_text) - # speech_command = SpeechCommand(data=message_text) - # speech_message = InternalMessage( - # to=settings.agent_settings.robot_speech_name, - # sender=settings.agent_settings.bdi_core_name, - # body=speech_command.model_dump_json(), - # ) - # asyncio.create_task(agent.send(speech_message)) + speech_command = SpeechCommand(data=message_text) + speech_message = InternalMessage( + to=settings.agent_settings.robot_speech_name, + sender=settings.agent_settings.bdi_core_name, + body=speech_command.model_dump_json(), + ) + # TODO: add to conversation history + self.add_behavior(self.send(speech_message)) yield @self.actions.add(".gesture", 2) diff --git a/src/control_backend/agents/bdi/belief_collector_agent.py b/src/control_backend/agents/bdi/belief_collector_agent.py index 788cff1..81c5ab2 100644 --- a/src/control_backend/agents/bdi/belief_collector_agent.py +++ b/src/control_backend/agents/bdi/belief_collector_agent.py @@ -101,7 +101,7 @@ class BDIBeliefCollectorAgent(BaseAgent): :return: A Belief object if the input is valid or None. """ try: - return Belief(name=name, arguments=arguments) + return Belief(name=name, arguments=arguments, replace=name == "user_said") except ValidationError: return None diff --git a/src/control_backend/agents/bdi/default_behavior.asl b/src/control_backend/agents/bdi/default_behavior.asl new file mode 100644 index 0000000..249689a --- /dev/null +++ b/src/control_backend/agents/bdi/default_behavior.asl @@ -0,0 +1,5 @@ +norms(""). + ++user_said(Message) : norms(Norms) <- + -user_said(Message); + .reply(Message, Norms). diff --git a/src/control_backend/agents/bdi/rules.asl b/src/control_backend/agents/bdi/rules.asl deleted file mode 100644 index cc9b4ef..0000000 --- a/src/control_backend/agents/bdi/rules.asl +++ /dev/null @@ -1,6 +0,0 @@ -norms(""). -goals(""). - -+user_said(Message) : norms(Norms) & goals(Goals) <- - -user_said(Message); - .reply(Message, Norms, Goals). diff --git a/src/control_backend/agents/bdi/test.asl b/src/control_backend/agents/bdi/test.asl deleted file mode 100644 index e69de29..0000000 diff --git a/src/control_backend/schemas/program.py b/src/control_backend/schemas/program.py index 5a8caa9..be538b0 100644 --- a/src/control_backend/schemas/program.py +++ b/src/control_backend/schemas/program.py @@ -194,7 +194,7 @@ class Phase(ProgramElement): """ name: str = "" - norms: list[Norm] + norms: list[BasicNorm | ConditionalNorm] goals: list[Goal] triggers: list[Trigger]