feat: make the pipeline work with Program and AgentSpeak

ref: N25B-429
This commit is contained in:
Twirre Meulenbelt
2026-01-06 15:26:44 +01:00
parent a357b6990b
commit 3406e9ac2f
7 changed files with 88 additions and 38 deletions

View File

@@ -42,9 +42,9 @@ class AgentSpeakGenerator:
def generate(self, program: Program) -> str: def generate(self, program: Program) -> str:
self._asp = AstProgram() 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_keyword_inference()
self._add_response_goal() self._add_default_plans()
self._process_phases(program.phases) 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( self._asp.plans.append(
AstPlan( AstPlan(
TriggerType.ADDED_GOAL, TriggerType.ADDED_GOAL,
AstLiteral("generate_response_with_goal", [AstVar("Goal")]), AstLiteral("reply_with_goal", [AstVar("Goal")]),
[AstLiteral("user_said", [AstVar("Message")])], [AstLiteral("user_said", [AstVar("Message")])],
[ [
AstStatement(StatementType.ADD_BELIEF, AstLiteral("responded_this_turn")), 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: def _process_phases(self, phases: list[Phase]) -> None:
for curr_phase, next_phase in zip([None] + phases, phases + [None], strict=True): for curr_phase, next_phase in zip([None] + phases, phases + [None], strict=True):
if curr_phase: if curr_phase:
self._process_phase(curr_phase) self._process_phase(curr_phase)
self._add_phase_transition(curr_phase, next_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: def _process_phase(self, phase: Phase) -> None:
for norm in phase.norms: for norm in phase.norms:
self._process_norm(norm, phase) self._process_norm(norm, phase)
@@ -112,14 +164,14 @@ class AgentSpeakGenerator:
self._process_trigger(trigger, phase) self._process_trigger(trigger, phase)
def _add_phase_transition(self, from_phase: Phase | None, to_phase: Phase | None) -> None: def _add_phase_transition(self, from_phase: Phase | None, to_phase: Phase | None) -> None:
from_phase_ast = ( if from_phase is None:
self._astify(from_phase) if from_phase else AstLiteral("phase", [AstString("start")]) return
) from_phase_ast = self._astify(from_phase)
to_phase_ast = ( to_phase_ast = (
self._astify(to_phase) if to_phase else AstLiteral("phase", [AstString("end")]) 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: if from_phase and from_phase.goals:
context.append(self._astify(from_phase.goals[-1], achieved=True)) context.append(self._astify(from_phase.goals[-1], achieved=True))
@@ -221,14 +273,10 @@ class AgentSpeakGenerator:
def _step_to_statement(self, step: PlanElement) -> AstStatement: def _step_to_statement(self, step: PlanElement) -> AstStatement:
match step: match step:
case Goal() as g: case Goal() | SpeechAction() | LLMAction() as a:
return AstStatement(StatementType.ACHIEVE_GOAL, self._astify(g)) return AstStatement(StatementType.ACHIEVE_GOAL, self._astify(a))
case SpeechAction() | GestureAction() as a: case GestureAction() as a:
return AstStatement(StatementType.DO_ACTION, self._astify(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 # TODO: separate handling of keyword and others
def _process_trigger(self, trigger: Trigger, phase: Phase) -> None: def _process_trigger(self, trigger: Trigger, phase: Phase) -> None:
@@ -318,7 +366,7 @@ class AgentSpeakGenerator:
@_astify.register @_astify.register
def _(self, la: LLMAction) -> AstExpression: def _(self, la: LLMAction) -> AstExpression:
return AstLiteral("generate_response_with_goal", [AstString(la.goal)]) return AstLiteral("reply_with_goal", [AstString(la.goal)])
@staticmethod @staticmethod
def _slugify_str(text: str) -> str: def _slugify_str(text: str) -> str:

View File

@@ -48,6 +48,7 @@ class BDICoreAgent(BaseAgent):
# Deep copy because we don't actually want to modify the standard actions globally # Deep copy because we don't actually want to modify the standard actions globally
self.actions = copy.deepcopy(agentspeak.stdlib.actions) self.actions = copy.deepcopy(agentspeak.stdlib.actions)
self._wake_bdi_loop = asyncio.Event() self._wake_bdi_loop = asyncio.Event()
self._bdi_loop_task = None
async def setup(self) -> None: async def setup(self) -> None:
""" """
@@ -64,7 +65,7 @@ class BDICoreAgent(BaseAgent):
await self._load_asl() await self._load_asl()
# Start the BDI cycle loop # 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._wake_bdi_loop.set()
self.logger.debug("Setup complete.") self.logger.debug("Setup complete.")
@@ -72,7 +73,7 @@ class BDICoreAgent(BaseAgent):
""" """
Load and parse the AgentSpeak source file. 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: try:
with open(file_name) as source: with open(file_name) as source:
@@ -135,7 +136,10 @@ class BDICoreAgent(BaseAgent):
# New agentspeak file # New agentspeak file
if msg.thread == "new_program": if msg.thread == "new_program":
if self._bdi_loop_task:
self._bdi_loop_task.cancel()
await self._load_asl(msg.body) await self._load_asl(msg.body)
self.add_behavior(self._bdi_loop())
# The message was not a belief, handle special cases based on sender # The message was not a belief, handle special cases based on sender
match msg.sender: match msg.sender:
@@ -246,20 +250,18 @@ class BDICoreAgent(BaseAgent):
the function expects (which will be located in `term.args`). 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): def _reply(agent: "BDICoreAgent", term, intention):
""" """
Let the LLM generate a response to a user's utterance with the current norms and goals. 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) message_text = agentspeak.grounded(term.args[0], intention.scope)
norms = agentspeak.grounded(term.args[1], 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("Norms: %s", norms)
self.logger.debug("Goals: %s", goals)
self.logger.debug("User text: %s", message_text) 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 yield
@self.actions.add(".reply_with_goal", 3) @self.actions.add(".reply_with_goal", 3)
@@ -278,7 +280,7 @@ class BDICoreAgent(BaseAgent):
norms, norms,
goal, 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 yield
@self.actions.add(".say", 1) @self.actions.add(".say", 1)
@@ -290,13 +292,14 @@ class BDICoreAgent(BaseAgent):
self.logger.debug('"say" action called with text=%s', message_text) self.logger.debug('"say" action called with text=%s', message_text)
# speech_command = SpeechCommand(data=message_text) speech_command = SpeechCommand(data=message_text)
# speech_message = InternalMessage( speech_message = InternalMessage(
# to=settings.agent_settings.robot_speech_name, to=settings.agent_settings.robot_speech_name,
# sender=settings.agent_settings.bdi_core_name, sender=settings.agent_settings.bdi_core_name,
# body=speech_command.model_dump_json(), body=speech_command.model_dump_json(),
# ) )
# asyncio.create_task(agent.send(speech_message)) # TODO: add to conversation history
self.add_behavior(self.send(speech_message))
yield yield
@self.actions.add(".gesture", 2) @self.actions.add(".gesture", 2)

View File

@@ -101,7 +101,7 @@ class BDIBeliefCollectorAgent(BaseAgent):
:return: A Belief object if the input is valid or None. :return: A Belief object if the input is valid or None.
""" """
try: try:
return Belief(name=name, arguments=arguments) return Belief(name=name, arguments=arguments, replace=name == "user_said")
except ValidationError: except ValidationError:
return None return None

View File

@@ -0,0 +1,5 @@
norms("").
+user_said(Message) : norms(Norms) <-
-user_said(Message);
.reply(Message, Norms).

View File

@@ -1,6 +0,0 @@
norms("").
goals("").
+user_said(Message) : norms(Norms) & goals(Goals) <-
-user_said(Message);
.reply(Message, Norms, Goals).

View File

@@ -194,7 +194,7 @@ class Phase(ProgramElement):
""" """
name: str = "" name: str = ""
norms: list[Norm] norms: list[BasicNorm | ConditionalNorm]
goals: list[Goal] goals: list[Goal]
triggers: list[Trigger] triggers: list[Trigger]