feat: make the pipeline work with Program and AgentSpeak
ref: N25B-429
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
5
src/control_backend/agents/bdi/default_behavior.asl
Normal file
5
src/control_backend/agents/bdi/default_behavior.asl
Normal file
@@ -0,0 +1,5 @@
|
||||
norms("").
|
||||
|
||||
+user_said(Message) : norms(Norms) <-
|
||||
-user_said(Message);
|
||||
.reply(Message, Norms).
|
||||
@@ -1,6 +0,0 @@
|
||||
norms("").
|
||||
goals("").
|
||||
|
||||
+user_said(Message) : norms(Norms) & goals(Goals) <-
|
||||
-user_said(Message);
|
||||
.reply(Message, Norms, Goals).
|
||||
@@ -194,7 +194,7 @@ class Phase(ProgramElement):
|
||||
"""
|
||||
|
||||
name: str = ""
|
||||
norms: list[Norm]
|
||||
norms: list[BasicNorm | ConditionalNorm]
|
||||
goals: list[Goal]
|
||||
triggers: list[Trigger]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user