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:
|
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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
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 = ""
|
name: str = ""
|
||||||
norms: list[Norm]
|
norms: list[BasicNorm | ConditionalNorm]
|
||||||
goals: list[Goal]
|
goals: list[Goal]
|
||||||
triggers: list[Trigger]
|
triggers: list[Trigger]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user