Compare commits
14 Commits
feat/pause
...
feat/reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91b999104 | ||
|
|
1360567820 | ||
|
|
cc0d5af28c | ||
|
|
be88323cf7 | ||
|
|
76dfcb23ef | ||
|
|
34afca6652 | ||
|
|
324a63e5cc | ||
| 07d70cb781 | |||
| af832980c8 | |||
|
|
cabe35cdbd | ||
|
|
de8e829d3e | ||
|
|
3406e9ac2f | ||
| a357b6990b | |||
| 9eea4ee345 |
@@ -83,6 +83,8 @@ class RobotGestureAgent(BaseAgent):
|
|||||||
self.subsocket.close()
|
self.subsocket.close()
|
||||||
if self.pubsocket:
|
if self.pubsocket:
|
||||||
self.pubsocket.close()
|
self.pubsocket.close()
|
||||||
|
if self.repsocket:
|
||||||
|
self.repsocket.close()
|
||||||
await super().stop()
|
await super().stop()
|
||||||
|
|
||||||
async def handle_message(self, msg: InternalMessage):
|
async def handle_message(self, msg: InternalMessage):
|
||||||
|
|||||||
@@ -187,9 +187,10 @@ class StatementType(StrEnum):
|
|||||||
EMPTY = ""
|
EMPTY = ""
|
||||||
DO_ACTION = "."
|
DO_ACTION = "."
|
||||||
ACHIEVE_GOAL = "!"
|
ACHIEVE_GOAL = "!"
|
||||||
# TEST_GOAL = "?" # TODO
|
TEST_GOAL = "?"
|
||||||
ADD_BELIEF = "+"
|
ADD_BELIEF = "+"
|
||||||
REMOVE_BELIEF = "-"
|
REMOVE_BELIEF = "-"
|
||||||
|
REPLACE_BELIEF = "-+"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
403
src/control_backend/agents/bdi/agentspeak_generator.py
Normal file
403
src/control_backend/agents/bdi/agentspeak_generator.py
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
from functools import singledispatchmethod
|
||||||
|
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
from control_backend.agents.bdi.agentspeak_ast import (
|
||||||
|
AstBinaryOp,
|
||||||
|
AstExpression,
|
||||||
|
AstLiteral,
|
||||||
|
AstPlan,
|
||||||
|
AstProgram,
|
||||||
|
AstRule,
|
||||||
|
AstStatement,
|
||||||
|
AstString,
|
||||||
|
AstVar,
|
||||||
|
BinaryOperatorType,
|
||||||
|
StatementType,
|
||||||
|
TriggerType,
|
||||||
|
)
|
||||||
|
from control_backend.schemas.program import (
|
||||||
|
BasicNorm,
|
||||||
|
ConditionalNorm,
|
||||||
|
GestureAction,
|
||||||
|
Goal,
|
||||||
|
InferredBelief,
|
||||||
|
KeywordBelief,
|
||||||
|
LLMAction,
|
||||||
|
LogicalOperator,
|
||||||
|
Norm,
|
||||||
|
Phase,
|
||||||
|
PlanElement,
|
||||||
|
Program,
|
||||||
|
ProgramElement,
|
||||||
|
SemanticBelief,
|
||||||
|
SpeechAction,
|
||||||
|
Trigger,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentSpeakGenerator:
|
||||||
|
_asp: AstProgram
|
||||||
|
|
||||||
|
def generate(self, program: Program) -> str:
|
||||||
|
self._asp = AstProgram()
|
||||||
|
|
||||||
|
self._asp.rules.append(AstRule(self._astify(program.phases[0])))
|
||||||
|
self._add_keyword_inference()
|
||||||
|
self._add_default_plans()
|
||||||
|
|
||||||
|
self._process_phases(program.phases)
|
||||||
|
|
||||||
|
self._add_fallbacks()
|
||||||
|
|
||||||
|
return str(self._asp)
|
||||||
|
|
||||||
|
def _add_keyword_inference(self) -> None:
|
||||||
|
keyword = AstVar("Keyword")
|
||||||
|
message = AstVar("Message")
|
||||||
|
position = AstVar("Pos")
|
||||||
|
|
||||||
|
self._asp.rules.append(
|
||||||
|
AstRule(
|
||||||
|
AstLiteral("keyword_said", [keyword]),
|
||||||
|
AstLiteral("user_said", [message])
|
||||||
|
& AstLiteral(".substring", [keyword, message, position])
|
||||||
|
& (position >= 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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("reply_with_goal", [AstVar("Goal")]),
|
||||||
|
[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_with_goal", [AstVar("Message"), AstVar("Norms"), AstVar("Goal")]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self._add_default_loop(phase)
|
||||||
|
|
||||||
|
previous_goal = None
|
||||||
|
for goal in phase.goals:
|
||||||
|
self._process_goal(goal, phase, previous_goal)
|
||||||
|
previous_goal = goal
|
||||||
|
|
||||||
|
for trigger in phase.triggers:
|
||||||
|
self._process_trigger(trigger, phase)
|
||||||
|
|
||||||
|
def _add_phase_transition(self, from_phase: Phase | None, to_phase: Phase | None) -> None:
|
||||||
|
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, ~AstLiteral("responded_this_turn")]
|
||||||
|
if from_phase and from_phase.goals:
|
||||||
|
context.append(self._astify(from_phase.goals[-1], achieved=True))
|
||||||
|
|
||||||
|
body = [
|
||||||
|
AstStatement(StatementType.REMOVE_BELIEF, from_phase_ast),
|
||||||
|
AstStatement(StatementType.ADD_BELIEF, to_phase_ast),
|
||||||
|
]
|
||||||
|
|
||||||
|
if from_phase:
|
||||||
|
body.extend(
|
||||||
|
[
|
||||||
|
AstStatement(
|
||||||
|
StatementType.TEST_GOAL, AstLiteral("user_said", [AstVar("Message")])
|
||||||
|
),
|
||||||
|
AstStatement(
|
||||||
|
StatementType.REPLACE_BELIEF, AstLiteral("user_said", [AstVar("Message")])
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(TriggerType.ADDED_GOAL, AstLiteral("transition_phase"), context, body)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_norm(self, norm: Norm, phase: Phase) -> None:
|
||||||
|
rule: AstRule | None = None
|
||||||
|
|
||||||
|
match norm:
|
||||||
|
case ConditionalNorm(condition=cond):
|
||||||
|
rule = AstRule(self._astify(norm), self._astify(phase) & self._astify(cond))
|
||||||
|
case BasicNorm():
|
||||||
|
rule = AstRule(self._astify(norm), self._astify(phase))
|
||||||
|
|
||||||
|
if not rule:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._asp.rules.append(rule)
|
||||||
|
|
||||||
|
def _add_default_loop(self, phase: Phase) -> None:
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
actions.append(AstStatement(StatementType.REMOVE_BELIEF, AstLiteral("responded_this_turn")))
|
||||||
|
actions.append(AstStatement(StatementType.ACHIEVE_GOAL, AstLiteral("check_triggers")))
|
||||||
|
|
||||||
|
for goal in phase.goals:
|
||||||
|
actions.append(AstStatement(StatementType.ACHIEVE_GOAL, self._astify(goal)))
|
||||||
|
|
||||||
|
actions.append(AstStatement(StatementType.ACHIEVE_GOAL, AstLiteral("transition_phase")))
|
||||||
|
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(
|
||||||
|
TriggerType.ADDED_BELIEF,
|
||||||
|
AstLiteral("user_said", [AstVar("Message")]),
|
||||||
|
[self._astify(phase)],
|
||||||
|
actions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_goal(
|
||||||
|
self,
|
||||||
|
goal: Goal,
|
||||||
|
phase: Phase,
|
||||||
|
previous_goal: Goal | None = None,
|
||||||
|
continues_response: bool = False,
|
||||||
|
) -> None:
|
||||||
|
context: list[AstExpression] = [self._astify(phase)]
|
||||||
|
context.append(~self._astify(goal, achieved=True))
|
||||||
|
if previous_goal and previous_goal.can_fail:
|
||||||
|
context.append(self._astify(previous_goal, achieved=True))
|
||||||
|
if not continues_response:
|
||||||
|
context.append(~AstLiteral("responded_this_turn"))
|
||||||
|
|
||||||
|
body = []
|
||||||
|
|
||||||
|
subgoals = []
|
||||||
|
for step in goal.plan.steps:
|
||||||
|
body.append(self._step_to_statement(step))
|
||||||
|
if isinstance(step, Goal):
|
||||||
|
subgoals.append(step)
|
||||||
|
|
||||||
|
if not goal.can_fail and not continues_response:
|
||||||
|
body.append(AstStatement(StatementType.ADD_BELIEF, self._astify(goal, achieved=True)))
|
||||||
|
|
||||||
|
self._asp.plans.append(AstPlan(TriggerType.ADDED_GOAL, self._astify(goal), context, body))
|
||||||
|
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(
|
||||||
|
TriggerType.ADDED_GOAL,
|
||||||
|
self._astify(goal),
|
||||||
|
context=[],
|
||||||
|
body=[AstStatement(StatementType.EMPTY, AstLiteral("true"))],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
prev_goal = None
|
||||||
|
for subgoal in subgoals:
|
||||||
|
self._process_goal(subgoal, phase, prev_goal)
|
||||||
|
prev_goal = subgoal
|
||||||
|
|
||||||
|
def _step_to_statement(self, step: PlanElement) -> AstStatement:
|
||||||
|
match step:
|
||||||
|
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))
|
||||||
|
|
||||||
|
# TODO: separate handling of keyword and others
|
||||||
|
def _process_trigger(self, trigger: Trigger, phase: Phase) -> None:
|
||||||
|
body = []
|
||||||
|
subgoals = []
|
||||||
|
|
||||||
|
for step in trigger.plan.steps:
|
||||||
|
body.append(self._step_to_statement(step))
|
||||||
|
if isinstance(step, Goal):
|
||||||
|
step.can_fail = False # triggers are continuous sequence
|
||||||
|
subgoals.append(step)
|
||||||
|
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(
|
||||||
|
TriggerType.ADDED_GOAL,
|
||||||
|
AstLiteral("check_triggers"),
|
||||||
|
[self._astify(phase), self._astify(trigger.condition)],
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for subgoal in subgoals:
|
||||||
|
self._process_goal(subgoal, phase, continues_response=True)
|
||||||
|
|
||||||
|
def _add_fallbacks(self):
|
||||||
|
# Trigger fallback
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(
|
||||||
|
TriggerType.ADDED_GOAL,
|
||||||
|
AstLiteral("check_triggers"),
|
||||||
|
[],
|
||||||
|
[AstStatement(StatementType.EMPTY, AstLiteral("true"))],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase transition fallback
|
||||||
|
self._asp.plans.append(
|
||||||
|
AstPlan(
|
||||||
|
TriggerType.ADDED_GOAL,
|
||||||
|
AstLiteral("transition_phase"),
|
||||||
|
[],
|
||||||
|
[AstStatement(StatementType.EMPTY, AstLiteral("true"))],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@singledispatchmethod
|
||||||
|
def _astify(self, element: ProgramElement) -> AstExpression:
|
||||||
|
raise NotImplementedError(f"Cannot convert element {element} to an AgentSpeak expression.")
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, kwb: KeywordBelief) -> AstExpression:
|
||||||
|
return AstLiteral("keyword_said", [AstString(kwb.keyword)])
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, sb: SemanticBelief) -> AstExpression:
|
||||||
|
return AstLiteral(self.get_semantic_belief_slug(sb))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_semantic_belief_slug(sb: SemanticBelief) -> str:
|
||||||
|
# If you need a method like this for other types, make a public slugify singledispatch for
|
||||||
|
# all types.
|
||||||
|
return f"semantic_{AgentSpeakGenerator._slugify_str(sb.name)}"
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, ib: InferredBelief) -> AstExpression:
|
||||||
|
return AstBinaryOp(
|
||||||
|
self._astify(ib.left),
|
||||||
|
BinaryOperatorType.AND if ib.operator == LogicalOperator.AND else BinaryOperatorType.OR,
|
||||||
|
self._astify(ib.right),
|
||||||
|
)
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, norm: Norm) -> AstExpression:
|
||||||
|
functor = "critical_norm" if norm.critical else "norm"
|
||||||
|
return AstLiteral(functor, [AstString(norm.norm)])
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, phase: Phase) -> AstExpression:
|
||||||
|
return AstLiteral("phase", [AstString(str(phase.id))])
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, goal: Goal, achieved: bool = False) -> AstExpression:
|
||||||
|
return AstLiteral(f"{'achieved_' if achieved else ''}{self._slugify_str(goal.name)}")
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, trigger: Trigger) -> AstExpression:
|
||||||
|
return AstLiteral(self.slugify(trigger))
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, sa: SpeechAction) -> AstExpression:
|
||||||
|
return AstLiteral("say", [AstString(sa.text)])
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, ga: GestureAction) -> AstExpression:
|
||||||
|
gesture = ga.gesture
|
||||||
|
return AstLiteral("gesture", [AstString(gesture.type), AstString(gesture.name)])
|
||||||
|
|
||||||
|
@_astify.register
|
||||||
|
def _(self, la: LLMAction) -> AstExpression:
|
||||||
|
return AstLiteral("reply_with_goal", [AstString(la.goal)])
|
||||||
|
|
||||||
|
@singledispatchmethod
|
||||||
|
@staticmethod
|
||||||
|
def slugify(element: ProgramElement) -> str:
|
||||||
|
raise NotImplementedError(f"Cannot convert element {element} to a slug.")
|
||||||
|
|
||||||
|
@slugify.register
|
||||||
|
@staticmethod
|
||||||
|
def _(sb: SemanticBelief) -> str:
|
||||||
|
return f"semantic_{AgentSpeakGenerator._slugify_str(sb.name)}"
|
||||||
|
|
||||||
|
@slugify.register
|
||||||
|
@staticmethod
|
||||||
|
def _(g: Goal) -> str:
|
||||||
|
return AgentSpeakGenerator._slugify_str(g.name)
|
||||||
|
|
||||||
|
@slugify.register
|
||||||
|
@staticmethod
|
||||||
|
def _(t: Trigger):
|
||||||
|
return f"trigger_{AgentSpeakGenerator._slugify_str(t.name)}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _slugify_str(text: str) -> str:
|
||||||
|
return slugify(text, separator="_", stopwords=["a", "an", "the", "we", "you", "I"])
|
||||||
@@ -42,13 +42,13 @@ class BDICoreAgent(BaseAgent):
|
|||||||
|
|
||||||
bdi_agent: agentspeak.runtime.Agent
|
bdi_agent: agentspeak.runtime.Agent
|
||||||
|
|
||||||
def __init__(self, name: str, asl: str):
|
def __init__(self, name: str):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.asl_file = asl
|
|
||||||
self.env = agentspeak.runtime.Environment()
|
self.env = agentspeak.runtime.Environment()
|
||||||
# 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:
|
||||||
"""
|
"""
|
||||||
@@ -65,19 +65,22 @@ 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.")
|
||||||
|
|
||||||
async def _load_asl(self):
|
async def _load_asl(self, file_name: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Load and parse the AgentSpeak source file.
|
Load and parse the AgentSpeak source file.
|
||||||
"""
|
"""
|
||||||
|
file_name = file_name or "src/control_backend/agents/bdi/default_behavior.asl"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.asl_file) as source:
|
with open(file_name) as source:
|
||||||
self.bdi_agent = self.env.build_agent(source, self.actions)
|
self.bdi_agent = self.env.build_agent(source, self.actions)
|
||||||
|
self.logger.info(f"Loaded new ASL from {file_name}.")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.logger.warning(f"Could not find the specified ASL file at {self.asl_file}.")
|
self.logger.warning(f"Could not find the specified ASL file at {file_name}.")
|
||||||
self.bdi_agent = agentspeak.runtime.Agent(self.env, self.name)
|
self.bdi_agent = agentspeak.runtime.Agent(self.env, self.name)
|
||||||
|
|
||||||
async def _bdi_loop(self):
|
async def _bdi_loop(self):
|
||||||
@@ -116,6 +119,7 @@ class BDICoreAgent(BaseAgent):
|
|||||||
Handle incoming messages.
|
Handle incoming messages.
|
||||||
|
|
||||||
- **Beliefs**: Updates the internal belief base.
|
- **Beliefs**: Updates the internal belief base.
|
||||||
|
- **Program**: Updates the internal agentspeak file to match the current program.
|
||||||
- **LLM Responses**: Forwards the generated text to the Robot Speech Agent (actuation).
|
- **LLM Responses**: Forwards the generated text to the Robot Speech Agent (actuation).
|
||||||
|
|
||||||
:param msg: The received internal message.
|
:param msg: The received internal message.
|
||||||
@@ -130,6 +134,13 @@ class BDICoreAgent(BaseAgent):
|
|||||||
self.logger.exception("Error processing belief.")
|
self.logger.exception("Error processing belief.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 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
|
# The message was not a belief, handle special cases based on sender
|
||||||
match msg.sender:
|
match msg.sender:
|
||||||
case settings.agent_settings.llm_name:
|
case settings.agent_settings.llm_name:
|
||||||
@@ -246,20 +257,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 +287,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 +299,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)
|
||||||
|
|||||||
@@ -1,598 +1,15 @@
|
|||||||
import uuid
|
import asyncio
|
||||||
from collections.abc import Iterable
|
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from slugify import slugify
|
|
||||||
from zmq.asyncio import Context
|
from zmq.asyncio import Context
|
||||||
|
|
||||||
from control_backend.agents import BaseAgent
|
from control_backend.agents import BaseAgent
|
||||||
|
from control_backend.agents.bdi.agentspeak_generator import AgentSpeakGenerator
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
from control_backend.schemas.program import (
|
from control_backend.schemas.belief_list import BeliefList
|
||||||
Action,
|
from control_backend.schemas.internal_message import InternalMessage
|
||||||
BasicBelief,
|
from control_backend.schemas.program import Belief, ConditionalNorm, InferredBelief, Program
|
||||||
BasicNorm,
|
|
||||||
Belief,
|
|
||||||
ConditionalNorm,
|
|
||||||
GestureAction,
|
|
||||||
Goal,
|
|
||||||
InferredBelief,
|
|
||||||
KeywordBelief,
|
|
||||||
LLMAction,
|
|
||||||
LogicalOperator,
|
|
||||||
Phase,
|
|
||||||
Plan,
|
|
||||||
Program,
|
|
||||||
ProgramElement,
|
|
||||||
SemanticBelief,
|
|
||||||
SpeechAction,
|
|
||||||
Trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
test_program = Program(
|
|
||||||
phases=[
|
|
||||||
Phase(
|
|
||||||
norms=[
|
|
||||||
BasicNorm(norm="Talk like a pirate", id=uuid.uuid4()),
|
|
||||||
ConditionalNorm(
|
|
||||||
condition=InferredBelief(
|
|
||||||
left=KeywordBelief(keyword="Arr", id=uuid.uuid4()),
|
|
||||||
right=SemanticBelief(
|
|
||||||
description="testing", name="semantic belief", id=uuid.uuid4()
|
|
||||||
),
|
|
||||||
operator=LogicalOperator.OR,
|
|
||||||
name="Talking to a pirate",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
norm="Use nautical terms",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
ConditionalNorm(
|
|
||||||
condition=SemanticBelief(
|
|
||||||
description="We are talking to a child",
|
|
||||||
name="talking to child",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
norm="Do not use cuss words",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
triggers=[
|
|
||||||
Trigger(
|
|
||||||
condition=InferredBelief(
|
|
||||||
left=KeywordBelief(keyword="key", id=uuid.uuid4()),
|
|
||||||
right=InferredBelief(
|
|
||||||
left=KeywordBelief(keyword="key2", id=uuid.uuid4()),
|
|
||||||
right=SemanticBelief(
|
|
||||||
description="Decode this", name="semantic belief 2", id=uuid.uuid4()
|
|
||||||
),
|
|
||||||
operator=LogicalOperator.OR,
|
|
||||||
name="test trigger inferred inner",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
operator=LogicalOperator.OR,
|
|
||||||
name="test trigger inferred outer",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
SpeechAction(text="Testing trigger", id=uuid.uuid4()),
|
|
||||||
Goal(
|
|
||||||
name="Testing trigger",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[LLMAction(goal="Do something", id=uuid.uuid4())],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
goals=[
|
|
||||||
Goal(
|
|
||||||
name="Determine user age",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[LLMAction(goal="Determine the age of the user.", id=uuid.uuid4())],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Goal(
|
|
||||||
name="Find the user's name",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
Goal(
|
|
||||||
name="Greet the user",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[LLMAction(goal="Greet the user.", id=uuid.uuid4())],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
can_fail=False,
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Goal(
|
|
||||||
name="Ask for name",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
LLMAction(goal="Obtain the user's name.", id=uuid.uuid4())
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Goal(
|
|
||||||
name="Tell a joke",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[LLMAction(goal="Tell a joke.", id=uuid.uuid4())], id=uuid.uuid4()
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Phase(
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
norms=[
|
|
||||||
BasicNorm(norm="Use very gentle speech.", id=uuid.uuid4()),
|
|
||||||
ConditionalNorm(
|
|
||||||
condition=SemanticBelief(
|
|
||||||
description="We are talking to a child",
|
|
||||||
name="talking to child",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
norm="Do not use cuss words",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
triggers=[
|
|
||||||
Trigger(
|
|
||||||
condition=InferredBelief(
|
|
||||||
left=KeywordBelief(keyword="help", id=uuid.uuid4()),
|
|
||||||
right=SemanticBelief(
|
|
||||||
description="User is stuck", name="stuck", id=uuid.uuid4()
|
|
||||||
),
|
|
||||||
operator=LogicalOperator.OR,
|
|
||||||
name="help_or_stuck",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
Goal(
|
|
||||||
name="Unblock user",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
LLMAction(
|
|
||||||
goal="Provide a step-by-step path to "
|
|
||||||
"resolve the user's issue.",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
goals=[
|
|
||||||
Goal(
|
|
||||||
name="Clarify intent",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
LLMAction(
|
|
||||||
goal="Ask 1-2 targeted questions to clarify the "
|
|
||||||
"user's intent, then proceed.",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Goal(
|
|
||||||
name="Provide solution",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
LLMAction(
|
|
||||||
goal="Deliver a solution to complete the user's goal.",
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
Goal(
|
|
||||||
name="Summarize next steps",
|
|
||||||
plan=Plan(
|
|
||||||
steps=[
|
|
||||||
LLMAction(
|
|
||||||
goal="Summarize what the user should do next.", id=uuid.uuid4()
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
id=uuid.uuid4(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def do_things():
|
|
||||||
print(AgentSpeakGenerator().generate(test_program))
|
|
||||||
|
|
||||||
|
|
||||||
class AgentSpeakGenerator:
|
|
||||||
"""
|
|
||||||
Converts Pydantic representation of behavior programs into AgentSpeak(L) code string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
arrow_prefix = f"{' ' * 2}<-{' ' * 2}"
|
|
||||||
colon_prefix = f"{' ' * 2}:{' ' * 3}"
|
|
||||||
indent_prefix = " " * 6
|
|
||||||
|
|
||||||
def generate(self, program: Program) -> str:
|
|
||||||
lines = []
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines += self._generate_initial_beliefs(program)
|
|
||||||
|
|
||||||
lines += self._generate_basic_flow(program)
|
|
||||||
|
|
||||||
lines += self._generate_phase_transitions(program)
|
|
||||||
|
|
||||||
lines += self._generate_norms(program)
|
|
||||||
|
|
||||||
lines += self._generate_belief_inference(program)
|
|
||||||
|
|
||||||
lines += self._generate_goals(program)
|
|
||||||
|
|
||||||
lines += self._generate_triggers(program)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def _generate_initial_beliefs(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Initial beliefs and agent startup ---"
|
|
||||||
|
|
||||||
yield "phase(start)."
|
|
||||||
|
|
||||||
yield ""
|
|
||||||
|
|
||||||
yield "+started"
|
|
||||||
yield f"{self.colon_prefix}phase(start)"
|
|
||||||
yield f"{self.arrow_prefix}phase({program.phases[0].id if program.phases else 'end'})."
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_basic_flow(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Basic flow ---"
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
yield from self._generate_basic_flow_per_phase(phase)
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_basic_flow_per_phase(self, phase: Phase) -> Iterable[str]:
|
|
||||||
yield "+user_said(Message)"
|
|
||||||
yield f"{self.colon_prefix}phase({phase.id})"
|
|
||||||
|
|
||||||
goals = phase.goals
|
|
||||||
if goals:
|
|
||||||
yield f"{self.arrow_prefix}{self._slugify(goals[0], include_prefix=True)}"
|
|
||||||
for goal in goals[1:]:
|
|
||||||
yield f"{self.indent_prefix}{self._slugify(goal, include_prefix=True)}"
|
|
||||||
|
|
||||||
yield f"{self.indent_prefix if goals else self.arrow_prefix}!transition_phase."
|
|
||||||
|
|
||||||
def _generate_phase_transitions(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Phase transitions ---"
|
|
||||||
|
|
||||||
if len(program.phases) == 0:
|
|
||||||
yield from ["", ""]
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: remove outdated things
|
|
||||||
|
|
||||||
for i in range(-1, len(program.phases)):
|
|
||||||
predecessor = program.phases[i] if i >= 0 else None
|
|
||||||
successor = program.phases[i + 1] if i < len(program.phases) - 1 else None
|
|
||||||
yield from self._generate_phase_transition(predecessor, successor)
|
|
||||||
|
|
||||||
yield from self._generate_phase_transition(None, None) # to avoid failing plan
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_phase_transition(
|
|
||||||
self, phase: Phase | None = None, next_phase: Phase | None = None
|
|
||||||
) -> Iterable[str]:
|
|
||||||
yield "+!transition_phase"
|
|
||||||
|
|
||||||
if phase is None and next_phase is None: # base case true to avoid failing plan
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
return
|
|
||||||
|
|
||||||
yield f"{self.colon_prefix}phase({phase.id if phase else 'start'})"
|
|
||||||
yield f"{self.arrow_prefix}-+phase({next_phase.id if next_phase else 'end'})."
|
|
||||||
|
|
||||||
def _generate_norms(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Norms ---"
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
for norm in phase.norms:
|
|
||||||
if type(norm) is BasicNorm:
|
|
||||||
yield f"{self._slugify(norm)} :- phase({phase.id})."
|
|
||||||
if type(norm) is ConditionalNorm:
|
|
||||||
yield (
|
|
||||||
f"{self._slugify(norm)} :- phase({phase.id}) & "
|
|
||||||
f"{self._slugify(norm.condition)}."
|
|
||||||
)
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_belief_inference(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Belief inference rules ---"
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
for norm in phase.norms:
|
|
||||||
if not isinstance(norm, ConditionalNorm):
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield from self._belief_inference_recursive(norm.condition)
|
|
||||||
|
|
||||||
for trigger in phase.triggers:
|
|
||||||
yield from self._belief_inference_recursive(trigger.condition)
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _belief_inference_recursive(self, belief: Belief) -> Iterable[str]:
|
|
||||||
if type(belief) is KeywordBelief:
|
|
||||||
yield (
|
|
||||||
f"{self._slugify(belief)} :- user_said(Message) & "
|
|
||||||
f'.substring(Message, "{belief.keyword}", Pos) & Pos >= 0.'
|
|
||||||
)
|
|
||||||
if type(belief) is InferredBelief:
|
|
||||||
yield (
|
|
||||||
f"{self._slugify(belief)} :- {self._slugify(belief.left)} "
|
|
||||||
f"{'&' if belief.operator == LogicalOperator.AND else '|'} "
|
|
||||||
f"{self._slugify(belief.right)}."
|
|
||||||
)
|
|
||||||
|
|
||||||
yield from self._belief_inference_recursive(belief.left)
|
|
||||||
yield from self._belief_inference_recursive(belief.right)
|
|
||||||
|
|
||||||
def _generate_goals(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Goals ---"
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
previous_goal: Goal | None = None
|
|
||||||
for goal in phase.goals:
|
|
||||||
yield from self._generate_goal_plan_recursive(goal, phase, previous_goal)
|
|
||||||
previous_goal = goal
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_goal_plan_recursive(
|
|
||||||
self, goal: Goal, phase: Phase, previous_goal: Goal | None = None
|
|
||||||
) -> Iterable[str]:
|
|
||||||
yield f"+{self._slugify(goal, include_prefix=True)}"
|
|
||||||
|
|
||||||
# Context
|
|
||||||
yield f"{self.colon_prefix}phase({phase.id}) &"
|
|
||||||
yield f"{self.indent_prefix}not responded_this_turn &"
|
|
||||||
yield f"{self.indent_prefix}not achieved_{self._slugify(goal)} &"
|
|
||||||
if previous_goal:
|
|
||||||
yield f"{self.indent_prefix}achieved_{self._slugify(previous_goal)}"
|
|
||||||
else:
|
|
||||||
yield f"{self.indent_prefix}true"
|
|
||||||
|
|
||||||
extra_goals_to_generate = []
|
|
||||||
|
|
||||||
steps = goal.plan.steps
|
|
||||||
|
|
||||||
if len(steps) == 0:
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
return
|
|
||||||
|
|
||||||
first_step = steps[0]
|
|
||||||
yield (
|
|
||||||
f"{self.arrow_prefix}{self._slugify(first_step, include_prefix=True)}"
|
|
||||||
f"{'.' if len(steps) == 1 and goal.can_fail else ';'}"
|
|
||||||
)
|
|
||||||
if isinstance(first_step, Goal):
|
|
||||||
extra_goals_to_generate.append(first_step)
|
|
||||||
|
|
||||||
for step in steps[1:-1]:
|
|
||||||
yield f"{self.indent_prefix}{self._slugify(step, include_prefix=True)};"
|
|
||||||
if isinstance(step, Goal):
|
|
||||||
extra_goals_to_generate.append(step)
|
|
||||||
|
|
||||||
if len(steps) > 1:
|
|
||||||
last_step = steps[-1]
|
|
||||||
yield (
|
|
||||||
f"{self.indent_prefix}{self._slugify(last_step, include_prefix=True)}"
|
|
||||||
f"{'.' if goal.can_fail else ';'}"
|
|
||||||
)
|
|
||||||
if isinstance(last_step, Goal):
|
|
||||||
extra_goals_to_generate.append(last_step)
|
|
||||||
|
|
||||||
if not goal.can_fail:
|
|
||||||
yield f"{self.indent_prefix}+achieved_{self._slugify(goal)}."
|
|
||||||
|
|
||||||
yield f"+{self._slugify(goal, include_prefix=True)}"
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
|
|
||||||
yield ""
|
|
||||||
|
|
||||||
extra_previous_goal: Goal | None = None
|
|
||||||
for extra_goal in extra_goals_to_generate:
|
|
||||||
yield from self._generate_goal_plan_recursive(extra_goal, phase, extra_previous_goal)
|
|
||||||
extra_previous_goal = extra_goal
|
|
||||||
|
|
||||||
def _generate_triggers(self, program: Program) -> Iterable[str]:
|
|
||||||
yield "// --- Triggers ---"
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
for trigger in phase.triggers:
|
|
||||||
yield from self._generate_trigger_plan(trigger, phase)
|
|
||||||
|
|
||||||
yield from ["", ""]
|
|
||||||
|
|
||||||
def _generate_trigger_plan(self, trigger: Trigger, phase: Phase) -> Iterable[str]:
|
|
||||||
belief_name = self._slugify(trigger.condition)
|
|
||||||
|
|
||||||
yield f"+{belief_name}"
|
|
||||||
yield f"{self.colon_prefix}phase({phase.id})"
|
|
||||||
|
|
||||||
extra_goals_to_generate = []
|
|
||||||
|
|
||||||
steps = trigger.plan.steps
|
|
||||||
|
|
||||||
if len(steps) == 0:
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
return
|
|
||||||
|
|
||||||
first_step = steps[0]
|
|
||||||
yield (
|
|
||||||
f"{self.arrow_prefix}{self._slugify(first_step, include_prefix=True)}"
|
|
||||||
f"{'.' if len(steps) == 1 else ';'}"
|
|
||||||
)
|
|
||||||
if isinstance(first_step, Goal):
|
|
||||||
extra_goals_to_generate.append(first_step)
|
|
||||||
|
|
||||||
for step in steps[1:-1]:
|
|
||||||
yield f"{self.indent_prefix}{self._slugify(step, include_prefix=True)};"
|
|
||||||
if isinstance(step, Goal):
|
|
||||||
extra_goals_to_generate.append(step)
|
|
||||||
|
|
||||||
if len(steps) > 1:
|
|
||||||
last_step = steps[-1]
|
|
||||||
yield f"{self.indent_prefix}{self._slugify(last_step, include_prefix=True)}."
|
|
||||||
if isinstance(last_step, Goal):
|
|
||||||
extra_goals_to_generate.append(last_step)
|
|
||||||
|
|
||||||
yield ""
|
|
||||||
|
|
||||||
extra_previous_goal: Goal | None = None
|
|
||||||
for extra_goal in extra_goals_to_generate:
|
|
||||||
yield from self._generate_trigger_plan_recursive(extra_goal, phase, extra_previous_goal)
|
|
||||||
extra_previous_goal = extra_goal
|
|
||||||
|
|
||||||
def _generate_trigger_plan_recursive(
|
|
||||||
self, goal: Goal, phase: Phase, previous_goal: Goal | None = None
|
|
||||||
) -> Iterable[str]:
|
|
||||||
yield f"+{self._slugify(goal, include_prefix=True)}"
|
|
||||||
|
|
||||||
extra_goals_to_generate = []
|
|
||||||
|
|
||||||
steps = goal.plan.steps
|
|
||||||
|
|
||||||
if len(steps) == 0:
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
return
|
|
||||||
|
|
||||||
first_step = steps[0]
|
|
||||||
yield (
|
|
||||||
f"{self.arrow_prefix}{self._slugify(first_step, include_prefix=True)}"
|
|
||||||
f"{'.' if len(steps) == 1 and goal.can_fail else ';'}"
|
|
||||||
)
|
|
||||||
if isinstance(first_step, Goal):
|
|
||||||
extra_goals_to_generate.append(first_step)
|
|
||||||
|
|
||||||
for step in steps[1:-1]:
|
|
||||||
yield f"{self.indent_prefix}{self._slugify(step, include_prefix=True)};"
|
|
||||||
if isinstance(step, Goal):
|
|
||||||
extra_goals_to_generate.append(step)
|
|
||||||
|
|
||||||
if len(steps) > 1:
|
|
||||||
last_step = steps[-1]
|
|
||||||
yield (
|
|
||||||
f"{self.indent_prefix}{self._slugify(last_step, include_prefix=True)}"
|
|
||||||
f"{'.' if goal.can_fail else ';'}"
|
|
||||||
)
|
|
||||||
if isinstance(last_step, Goal):
|
|
||||||
extra_goals_to_generate.append(last_step)
|
|
||||||
|
|
||||||
if not goal.can_fail:
|
|
||||||
yield f"{self.indent_prefix}+achieved_{self._slugify(goal)}."
|
|
||||||
|
|
||||||
yield f"+{self._slugify(goal, include_prefix=True)}"
|
|
||||||
yield f"{self.arrow_prefix}true."
|
|
||||||
|
|
||||||
yield ""
|
|
||||||
|
|
||||||
extra_previous_goal: Goal | None = None
|
|
||||||
for extra_goal in extra_goals_to_generate:
|
|
||||||
yield from self._generate_goal_plan_recursive(extra_goal, phase, extra_previous_goal)
|
|
||||||
extra_previous_goal = extra_goal
|
|
||||||
|
|
||||||
def _slugify(self, element: ProgramElement, include_prefix: bool = False) -> str:
|
|
||||||
def base_slugify_call(text: str):
|
|
||||||
return slugify(text, separator="_", stopwords=["a", "the"])
|
|
||||||
|
|
||||||
if type(element) is KeywordBelief:
|
|
||||||
return f'keyword_said("{element.keyword}")'
|
|
||||||
|
|
||||||
if type(element) is SemanticBelief:
|
|
||||||
name = element.name
|
|
||||||
return f"semantic_{base_slugify_call(name if name else element.description)}"
|
|
||||||
|
|
||||||
if isinstance(element, BasicNorm):
|
|
||||||
return f'norm("{element.norm}")'
|
|
||||||
|
|
||||||
if isinstance(element, Goal):
|
|
||||||
return f"{'!' if include_prefix else ''}{base_slugify_call(element.name)}"
|
|
||||||
|
|
||||||
if isinstance(element, SpeechAction):
|
|
||||||
return f'.say("{element.text}")'
|
|
||||||
|
|
||||||
if isinstance(element, GestureAction):
|
|
||||||
return f'.gesture("{element.gesture}")'
|
|
||||||
|
|
||||||
if isinstance(element, LLMAction):
|
|
||||||
return f'!generate_response_with_goal("{element.goal}")'
|
|
||||||
|
|
||||||
if isinstance(element, Action.__value__):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Have not implemented an ASL string representation for this action."
|
|
||||||
)
|
|
||||||
|
|
||||||
if element.name == "":
|
|
||||||
raise ValueError("Name must be initialized for this type of ProgramElement.")
|
|
||||||
|
|
||||||
return base_slugify_call(element.name)
|
|
||||||
|
|
||||||
def _extract_basic_beliefs_from_program(self, program: Program) -> list[BasicBelief]:
|
|
||||||
beliefs = []
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
class BDIProgramManager(BaseAgent):
|
class BDIProgramManager(BaseAgent):
|
||||||
@@ -611,40 +28,75 @@ class BDIProgramManager(BaseAgent):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.sub_socket = None
|
self.sub_socket = None
|
||||||
|
|
||||||
# async def _send_to_bdi(self, program: Program):
|
async def _create_agentspeak_and_send_to_bdi(self, program: Program):
|
||||||
# """
|
"""
|
||||||
# Convert a received program into BDI beliefs and send them to the BDI Core Agent.
|
Convert a received program into BDI beliefs and send them to the BDI Core Agent.
|
||||||
#
|
|
||||||
# Currently, it takes the **first phase** of the program and extracts:
|
Currently, it takes the **first phase** of the program and extracts:
|
||||||
# - **Norms**: Constraints or rules the agent must follow.
|
- **Norms**: Constraints or rules the agent must follow.
|
||||||
# - **Goals**: Objectives the agent must achieve.
|
- **Goals**: Objectives the agent must achieve.
|
||||||
#
|
|
||||||
# These are sent as a ``BeliefMessage`` with ``replace=True``, meaning they will
|
These are sent as a ``BeliefMessage`` with ``replace=True``, meaning they will
|
||||||
# overwrite any existing norms/goals of the same name in the BDI agent.
|
overwrite any existing norms/goals of the same name in the BDI agent.
|
||||||
#
|
|
||||||
# :param program: The program object received from the API.
|
:param program: The program object received from the API.
|
||||||
# """
|
"""
|
||||||
# first_phase = program.phases[0]
|
asg = AgentSpeakGenerator()
|
||||||
# norms_belief = Belief(
|
|
||||||
# name="norms",
|
asl_str = asg.generate(program)
|
||||||
# arguments=[norm.norm for norm in first_phase.norms],
|
|
||||||
# replace=True,
|
file_name = "src/control_backend/agents/bdi/agentspeak.asl"
|
||||||
# )
|
|
||||||
# goals_belief = Belief(
|
with open(file_name, "w") as f:
|
||||||
# name="goals",
|
f.write(asl_str)
|
||||||
# arguments=[goal.description for goal in first_phase.goals],
|
|
||||||
# replace=True,
|
msg = InternalMessage(
|
||||||
# )
|
sender=self.name,
|
||||||
# program_beliefs = BeliefMessage(beliefs=[norms_belief, goals_belief])
|
to=settings.agent_settings.bdi_core_name,
|
||||||
#
|
body=file_name,
|
||||||
# message = InternalMessage(
|
thread="new_program",
|
||||||
# to=settings.agent_settings.bdi_core_name,
|
)
|
||||||
# sender=self.name,
|
|
||||||
# body=program_beliefs.model_dump_json(),
|
await self.send(msg)
|
||||||
# thread="beliefs",
|
|
||||||
# )
|
@staticmethod
|
||||||
# await self.send(message)
|
def _extract_beliefs_from_program(program: Program) -> list[Belief]:
|
||||||
# self.logger.debug("Sent new norms and goals to the BDI agent.")
|
beliefs: list[Belief] = []
|
||||||
|
|
||||||
|
for phase in program.phases:
|
||||||
|
for norm in phase.norms:
|
||||||
|
if isinstance(norm, ConditionalNorm):
|
||||||
|
beliefs += BDIProgramManager._extract_beliefs_from_belief(norm.condition)
|
||||||
|
|
||||||
|
for trigger in phase.triggers:
|
||||||
|
beliefs += BDIProgramManager._extract_beliefs_from_belief(trigger.condition)
|
||||||
|
|
||||||
|
return beliefs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_beliefs_from_belief(belief: Belief) -> list[Belief]:
|
||||||
|
if isinstance(belief, InferredBelief):
|
||||||
|
return BDIProgramManager._extract_beliefs_from_belief(
|
||||||
|
belief.left
|
||||||
|
) + BDIProgramManager._extract_beliefs_from_belief(belief.right)
|
||||||
|
return [belief]
|
||||||
|
|
||||||
|
async def _send_beliefs_to_semantic_belief_extractor(self, program: Program):
|
||||||
|
"""
|
||||||
|
Extract beliefs from the program and send them to the Semantic Belief Extractor Agent.
|
||||||
|
|
||||||
|
:param program: The program received from the API.
|
||||||
|
"""
|
||||||
|
beliefs = BeliefList(beliefs=self._extract_beliefs_from_program(program))
|
||||||
|
|
||||||
|
message = InternalMessage(
|
||||||
|
to=settings.agent_settings.text_belief_extractor_name,
|
||||||
|
sender=self.name,
|
||||||
|
body=beliefs.model_dump_json(),
|
||||||
|
thread="beliefs",
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.send(message)
|
||||||
|
|
||||||
async def _receive_programs(self):
|
async def _receive_programs(self):
|
||||||
"""
|
"""
|
||||||
@@ -662,7 +114,10 @@ class BDIProgramManager(BaseAgent):
|
|||||||
self.logger.exception("Received an invalid program.")
|
self.logger.exception("Received an invalid program.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self._send_to_bdi(program)
|
await asyncio.gather(
|
||||||
|
self._create_agentspeak_and_send_to_bdi(program),
|
||||||
|
self._send_beliefs_to_semantic_belief_extractor(program),
|
||||||
|
)
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
"""
|
"""
|
||||||
@@ -678,7 +133,3 @@ class BDIProgramManager(BaseAgent):
|
|||||||
self.sub_socket.subscribe("program")
|
self.sub_socket.subscribe("program")
|
||||||
|
|
||||||
self.add_behavior(self._receive_programs())
|
self.add_behavior(self._receive_programs())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
do_things()
|
|
||||||
|
|||||||
@@ -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).
|
|
||||||
@@ -3,21 +3,16 @@ import json
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from slugify import slugify
|
|
||||||
|
|
||||||
from control_backend.agents.base import BaseAgent
|
from control_backend.agents.base import BaseAgent
|
||||||
|
from control_backend.agents.bdi.agentspeak_generator import AgentSpeakGenerator
|
||||||
from control_backend.core.agent_system import InternalMessage
|
from control_backend.core.agent_system import InternalMessage
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
|
from control_backend.schemas.belief_list import BeliefList
|
||||||
from control_backend.schemas.belief_message import Belief as InternalBelief
|
from control_backend.schemas.belief_message import Belief as InternalBelief
|
||||||
from control_backend.schemas.belief_message import BeliefMessage
|
from control_backend.schemas.belief_message import BeliefMessage
|
||||||
from control_backend.schemas.chat_history import ChatHistory, ChatMessage
|
from control_backend.schemas.chat_history import ChatHistory, ChatMessage
|
||||||
from control_backend.schemas.program import (
|
from control_backend.schemas.program import SemanticBelief
|
||||||
Belief,
|
|
||||||
ConditionalNorm,
|
|
||||||
InferredBelief,
|
|
||||||
Program,
|
|
||||||
SemanticBelief,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TextBeliefExtractorAgent(BaseAgent):
|
class TextBeliefExtractorAgent(BaseAgent):
|
||||||
@@ -32,11 +27,12 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
the message itself.
|
the message itself.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str, temperature: float = settings.llm_settings.code_temperature):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.beliefs: dict[str, bool] = {}
|
self.beliefs: dict[str, bool] = {}
|
||||||
self.available_beliefs: list[SemanticBelief] = []
|
self.available_beliefs: list[SemanticBelief] = []
|
||||||
self.conversation = ChatHistory(messages=[])
|
self.conversation = ChatHistory(messages=[])
|
||||||
|
self.temperature = temperature
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
"""
|
"""
|
||||||
@@ -85,44 +81,18 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
:param msg: The received message from the program manager.
|
:param msg: The received message from the program manager.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
program = Program.model_validate_json(msg.body)
|
belief_list = BeliefList.model_validate_json(msg.body)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Received message from program manager but it is not a valid program."
|
"Received message from program manager but it is not a valid list of beliefs."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.debug("Received a program from the program manager.")
|
self.available_beliefs = [b for b in belief_list.beliefs if isinstance(b, SemanticBelief)]
|
||||||
|
self.logger.debug(
|
||||||
self.available_beliefs = self._extract_basic_beliefs_from_program(program)
|
"Received %d beliefs from the program manager.",
|
||||||
|
len(self.available_beliefs),
|
||||||
# TODO Copied from an incomplete version of the program manager. Use that one instead.
|
)
|
||||||
@staticmethod
|
|
||||||
def _extract_basic_beliefs_from_program(program: Program) -> list[SemanticBelief]:
|
|
||||||
beliefs = []
|
|
||||||
|
|
||||||
for phase in program.phases:
|
|
||||||
for norm in phase.norms:
|
|
||||||
if isinstance(norm, ConditionalNorm):
|
|
||||||
beliefs += TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
|
|
||||||
norm.condition
|
|
||||||
)
|
|
||||||
|
|
||||||
for trigger in phase.triggers:
|
|
||||||
beliefs += TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
|
|
||||||
trigger.condition
|
|
||||||
)
|
|
||||||
|
|
||||||
return beliefs
|
|
||||||
|
|
||||||
# TODO Copied from an incomplete version of the program manager. Use that one instead.
|
|
||||||
@staticmethod
|
|
||||||
def _extract_basic_beliefs_from_belief(belief: Belief) -> list[SemanticBelief]:
|
|
||||||
if isinstance(belief, InferredBelief):
|
|
||||||
return TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
|
|
||||||
belief.left
|
|
||||||
) + TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(belief.right)
|
|
||||||
return [belief]
|
|
||||||
|
|
||||||
async def _user_said(self, text: str):
|
async def _user_said(self, text: str):
|
||||||
"""
|
"""
|
||||||
@@ -207,8 +177,7 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_belief_schema(belief: SemanticBelief) -> tuple[str, dict]:
|
def _create_belief_schema(belief: SemanticBelief) -> tuple[str, dict]:
|
||||||
# TODO: use real belief names
|
return AgentSpeakGenerator.slugify(belief), {
|
||||||
return belief.name or slugify(belief.description), {
|
|
||||||
"type": ["boolean", "null"],
|
"type": ["boolean", "null"],
|
||||||
"description": belief.description,
|
"description": belief.description,
|
||||||
}
|
}
|
||||||
@@ -237,12 +206,8 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_beliefs(beliefs: list[SemanticBelief]):
|
def _format_beliefs(beliefs: list[SemanticBelief]):
|
||||||
# TODO: use real belief names
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[f"- {AgentSpeakGenerator.slugify(belief)}: {belief.description}" for belief in beliefs]
|
||||||
f"- {belief.name or slugify(belief.description)}: {belief.description}"
|
|
||||||
for belief in beliefs
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _infer_beliefs(
|
async def _infer_beliefs(
|
||||||
@@ -267,7 +232,7 @@ Given the above conversation, what beliefs can be inferred?
|
|||||||
If there is no relevant information about a belief belief, give null.
|
If there is no relevant information about a belief belief, give null.
|
||||||
In case messages conflict, prefer using the most recent messages for inference.
|
In case messages conflict, prefer using the most recent messages for inference.
|
||||||
|
|
||||||
Choose from the following list of beliefs, formatted as (belief_name, description):
|
Choose from the following list of beliefs, formatted as `- <belief_name>: <description>`:
|
||||||
{self._format_beliefs(beliefs)}
|
{self._format_beliefs(beliefs)}
|
||||||
|
|
||||||
Respond with a JSON similar to the following, but with the property names as given above:
|
Respond with a JSON similar to the following, but with the property names as given above:
|
||||||
@@ -304,8 +269,7 @@ Respond with a JSON similar to the following, but with the property names as giv
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
async def _query_llm(self, prompt: str, schema: dict) -> dict:
|
||||||
async def _query_llm(prompt: str, schema: dict) -> dict:
|
|
||||||
"""
|
"""
|
||||||
Query an LLM with the given prompt and schema, return an instance of a dict conforming to
|
Query an LLM with the given prompt and schema, return an instance of a dict conforming to
|
||||||
that schema.
|
that schema.
|
||||||
@@ -333,7 +297,7 @@ Respond with a JSON similar to the following, but with the property names as giv
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"reasoning_effort": "low",
|
"reasoning_effort": "low",
|
||||||
"temperature": settings.llm_settings.code_temperature,
|
"temperature": self.temperature,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
},
|
},
|
||||||
timeout=None,
|
timeout=None,
|
||||||
@@ -342,4 +306,5 @@ Respond with a JSON similar to the following, but with the property names as giv
|
|||||||
|
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
json_message = response_json["choices"][0]["message"]["content"]
|
json_message = response_json["choices"][0]["message"]["content"]
|
||||||
return json.loads(json_message)
|
beliefs = json.loads(json_message)
|
||||||
|
return beliefs
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from zmq.asyncio import Context
|
|||||||
from control_backend.agents import BaseAgent
|
from control_backend.agents import BaseAgent
|
||||||
from control_backend.agents.actuation.robot_gesture_agent import RobotGestureAgent
|
from control_backend.agents.actuation.robot_gesture_agent import RobotGestureAgent
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
|
from control_backend.schemas.internal_message import InternalMessage
|
||||||
|
|
||||||
from ..actuation.robot_speech_agent import RobotSpeechAgent
|
from ..actuation.robot_speech_agent import RobotSpeechAgent
|
||||||
from ..perception import VADAgent
|
from ..perception import VADAgent
|
||||||
@@ -47,6 +48,8 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
self._req_socket: azmq.Socket | None = None
|
self._req_socket: azmq.Socket | None = None
|
||||||
self.pub_socket: azmq.Socket | None = None
|
self.pub_socket: azmq.Socket | None = None
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
self.gesture_agent: RobotGestureAgent | None = None
|
||||||
|
self.speech_agent: RobotSpeechAgent | None = None
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
"""
|
"""
|
||||||
@@ -140,6 +143,7 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
|
|
||||||
# At this point, we have a valid response
|
# At this point, we have a valid response
|
||||||
try:
|
try:
|
||||||
|
self.logger.debug("Negotiation successful. Handling rn")
|
||||||
await self._handle_negotiation_response(received_message)
|
await self._handle_negotiation_response(received_message)
|
||||||
# Let UI know that we're connected
|
# Let UI know that we're connected
|
||||||
topic = b"ping"
|
topic = b"ping"
|
||||||
@@ -188,6 +192,7 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
address=addr,
|
address=addr,
|
||||||
bind=bind,
|
bind=bind,
|
||||||
)
|
)
|
||||||
|
self.speech_agent = robot_speech_agent
|
||||||
robot_gesture_agent = RobotGestureAgent(
|
robot_gesture_agent = RobotGestureAgent(
|
||||||
settings.agent_settings.robot_gesture_name,
|
settings.agent_settings.robot_gesture_name,
|
||||||
address=addr,
|
address=addr,
|
||||||
@@ -195,6 +200,7 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
gesture_data=gesture_data,
|
gesture_data=gesture_data,
|
||||||
single_gesture_data=single_gesture_data,
|
single_gesture_data=single_gesture_data,
|
||||||
)
|
)
|
||||||
|
self.gesture_agent = robot_gesture_agent
|
||||||
await robot_speech_agent.start()
|
await robot_speech_agent.start()
|
||||||
await asyncio.sleep(0.1) # Small delay
|
await asyncio.sleep(0.1) # Small delay
|
||||||
await robot_gesture_agent.start()
|
await robot_gesture_agent.start()
|
||||||
@@ -225,6 +231,7 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
while self._running:
|
while self._running:
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
await asyncio.sleep(settings.behaviour_settings.sleep_s)
|
await asyncio.sleep(settings.behaviour_settings.sleep_s)
|
||||||
|
self.logger.debug("Not connected, skipping ping loop iteration.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We need to listen and send pings.
|
# We need to listen and send pings.
|
||||||
@@ -288,13 +295,36 @@ class RICommunicationAgent(BaseAgent):
|
|||||||
# Tell UI we're disconnected.
|
# Tell UI we're disconnected.
|
||||||
topic = b"ping"
|
topic = b"ping"
|
||||||
data = json.dumps(False).encode()
|
data = json.dumps(False).encode()
|
||||||
|
self.logger.debug("1")
|
||||||
if self.pub_socket:
|
if self.pub_socket:
|
||||||
try:
|
try:
|
||||||
|
self.logger.debug("2")
|
||||||
await asyncio.wait_for(self.pub_socket.send_multipart([topic, data]), 5)
|
await asyncio.wait_for(self.pub_socket.send_multipart([topic, data]), 5)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
self.logger.debug("3")
|
||||||
self.logger.warning("Connection ping for router timed out.")
|
self.logger.warning("Connection ping for router timed out.")
|
||||||
|
|
||||||
# Try to reboot/renegotiate
|
# Try to reboot/renegotiate
|
||||||
|
if self.gesture_agent is not None:
|
||||||
|
await self.gesture_agent.stop()
|
||||||
|
|
||||||
|
if self.speech_agent is not None:
|
||||||
|
await self.speech_agent.stop()
|
||||||
|
|
||||||
|
if self.pub_socket is not None:
|
||||||
|
self.pub_socket.close()
|
||||||
|
|
||||||
self.logger.debug("Restarting communication negotiation.")
|
self.logger.debug("Restarting communication negotiation.")
|
||||||
if await self._negotiate_connection(max_retries=1):
|
if await self._negotiate_connection(max_retries=2):
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
|
||||||
|
async def handle_message(self, msg: InternalMessage):
|
||||||
|
"""
|
||||||
|
Handle an incoming message.
|
||||||
|
|
||||||
|
Currently not implemented for this agent.
|
||||||
|
|
||||||
|
:param msg: The received message.
|
||||||
|
:raises NotImplementedError: Always, since this method is not implemented.
|
||||||
|
"""
|
||||||
|
self.logger.warning("custom warning for handle msg in ri coms %s", self.name)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import zmq.asyncio as azmq
|
|||||||
|
|
||||||
from control_backend.agents import BaseAgent
|
from control_backend.agents import BaseAgent
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
|
from control_backend.schemas.internal_message import InternalMessage
|
||||||
|
|
||||||
from ...schemas.program_status import PROGRAM_STATUS, ProgramStatus
|
from ...schemas.program_status import PROGRAM_STATUS, ProgramStatus
|
||||||
from .transcription_agent.transcription_agent import TranscriptionAgent
|
from .transcription_agent.transcription_agent import TranscriptionAgent
|
||||||
@@ -86,6 +87,12 @@ class VADAgent(BaseAgent):
|
|||||||
self.audio_buffer = np.array([], dtype=np.float32)
|
self.audio_buffer = np.array([], dtype=np.float32)
|
||||||
self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech
|
self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech
|
||||||
self._ready = asyncio.Event()
|
self._ready = asyncio.Event()
|
||||||
|
|
||||||
|
# Pause control
|
||||||
|
self._reset_needed = False
|
||||||
|
self._paused = asyncio.Event()
|
||||||
|
self._paused.set() # Not paused at start
|
||||||
|
|
||||||
self.model = None
|
self.model = None
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
@@ -213,6 +220,16 @@ class VADAgent(BaseAgent):
|
|||||||
"""
|
"""
|
||||||
await self._ready.wait()
|
await self._ready.wait()
|
||||||
while self._running:
|
while self._running:
|
||||||
|
await self._paused.wait()
|
||||||
|
|
||||||
|
# After being unpaused, reset stream and buffers
|
||||||
|
if self._reset_needed:
|
||||||
|
self.logger.debug("Resuming: resetting stream and buffers.")
|
||||||
|
await self._reset_stream()
|
||||||
|
self.audio_buffer = np.array([], dtype=np.float32)
|
||||||
|
self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech
|
||||||
|
self._reset_needed = False
|
||||||
|
|
||||||
assert self.audio_in_poller is not None
|
assert self.audio_in_poller is not None
|
||||||
data = await self.audio_in_poller.poll()
|
data = await self.audio_in_poller.poll()
|
||||||
if data is None:
|
if data is None:
|
||||||
@@ -254,3 +271,27 @@ class VADAgent(BaseAgent):
|
|||||||
# At this point, we know that the speech has ended.
|
# At this point, we know that the speech has ended.
|
||||||
# Prepend the last chunk that had no speech, for a more fluent boundary
|
# Prepend the last chunk that had no speech, for a more fluent boundary
|
||||||
self.audio_buffer = chunk
|
self.audio_buffer = chunk
|
||||||
|
|
||||||
|
async def handle_message(self, msg: InternalMessage):
|
||||||
|
"""
|
||||||
|
Handle incoming messages.
|
||||||
|
|
||||||
|
Expects messages to pause or resume the VAD processing from User Interrupt Agent.
|
||||||
|
|
||||||
|
:param msg: The received internal message.
|
||||||
|
"""
|
||||||
|
sender = msg.sender
|
||||||
|
|
||||||
|
if sender == settings.agent_settings.user_interrupt_name:
|
||||||
|
if msg.body == "PAUSE":
|
||||||
|
self.logger.info("Pausing VAD processing.")
|
||||||
|
self._paused.clear()
|
||||||
|
# If the robot needs to pick up speaking where it left off, do not set _reset_needed
|
||||||
|
self._reset_needed = True
|
||||||
|
elif msg.body == "RESUME":
|
||||||
|
self.logger.info("Resuming VAD processing.")
|
||||||
|
self._paused.set()
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Unknown command from User Interrupt Agent: {msg.body}")
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Ignoring message from unknown sender: {sender}")
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ from zmq.asyncio import Context
|
|||||||
from control_backend.agents import BaseAgent
|
from control_backend.agents import BaseAgent
|
||||||
from control_backend.core.agent_system import InternalMessage
|
from control_backend.core.agent_system import InternalMessage
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
from control_backend.schemas.ri_message import GestureCommand, RIEndpoint, SpeechCommand
|
from control_backend.schemas.ri_message import (
|
||||||
|
GestureCommand,
|
||||||
|
PauseCommand,
|
||||||
|
RIEndpoint,
|
||||||
|
SpeechCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserInterruptAgent(BaseAgent):
|
class UserInterruptAgent(BaseAgent):
|
||||||
@@ -71,6 +76,19 @@ class UserInterruptAgent(BaseAgent):
|
|||||||
"Forwarded button press (override) with context '%s' to BDIProgramManager.",
|
"Forwarded button press (override) with context '%s' to BDIProgramManager.",
|
||||||
event_context,
|
event_context,
|
||||||
)
|
)
|
||||||
|
elif event_type == "pause":
|
||||||
|
self.logger.debug(
|
||||||
|
"Received pause/resume button press with context '%s'.", event_context
|
||||||
|
)
|
||||||
|
await self._send_pause_command(event_context)
|
||||||
|
if event_context:
|
||||||
|
self.logger.info("Sent pause command.")
|
||||||
|
else:
|
||||||
|
self.logger.info("Sent resume command.")
|
||||||
|
|
||||||
|
elif event_type in ["next_phase", "reset_phase", "reset_experiment"]:
|
||||||
|
await self._send_experiment_control_to_bdi_core(event_type)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Received button press with unknown type '%s' (context: '%s').",
|
"Received button press with unknown type '%s' (context: '%s').",
|
||||||
@@ -78,6 +96,36 @@ class UserInterruptAgent(BaseAgent):
|
|||||||
event_context,
|
event_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _send_experiment_control_to_bdi_core(self, type):
|
||||||
|
"""
|
||||||
|
method to send experiment control buttons to bdi core.
|
||||||
|
|
||||||
|
:param type: the type of control button we should send to the bdi core.
|
||||||
|
"""
|
||||||
|
# Switch which thread we should send to bdi core
|
||||||
|
thread = ""
|
||||||
|
match type:
|
||||||
|
case "next_phase":
|
||||||
|
thread = "force_next_phase"
|
||||||
|
case "reset_phase":
|
||||||
|
thread = "reset_current_phase"
|
||||||
|
case "reset_experiment":
|
||||||
|
thread = "reset_experiment"
|
||||||
|
case _:
|
||||||
|
self.logger.warning(
|
||||||
|
"Received unknown experiment control type '%s' to send to BDI Core.",
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
|
||||||
|
out_msg = InternalMessage(
|
||||||
|
to=settings.agent_settings.bdi_core_name,
|
||||||
|
sender=self.name,
|
||||||
|
thread=thread,
|
||||||
|
body="",
|
||||||
|
)
|
||||||
|
self.logger.debug("Sending experiment control '%s' to BDI Core.", thread)
|
||||||
|
await self.send(out_msg)
|
||||||
|
|
||||||
async def _send_to_speech_agent(self, text_to_say: str):
|
async def _send_to_speech_agent(self, text_to_say: str):
|
||||||
"""
|
"""
|
||||||
method to send prioritized speech command to RobotSpeechAgent.
|
method to send prioritized speech command to RobotSpeechAgent.
|
||||||
@@ -130,6 +178,38 @@ class UserInterruptAgent(BaseAgent):
|
|||||||
belief_id,
|
belief_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _send_pause_command(self, pause):
|
||||||
|
"""
|
||||||
|
Send a pause command to the Robot Interface via the RI Communication Agent.
|
||||||
|
Send a pause command to the other internal agents; for now just VAD agent.
|
||||||
|
"""
|
||||||
|
cmd = PauseCommand(data=pause)
|
||||||
|
message = InternalMessage(
|
||||||
|
to=settings.agent_settings.ri_communication_name,
|
||||||
|
sender=self.name,
|
||||||
|
body=cmd.model_dump_json(),
|
||||||
|
)
|
||||||
|
await self.send(message)
|
||||||
|
|
||||||
|
if pause == "true":
|
||||||
|
# Send pause to VAD agent
|
||||||
|
vad_message = InternalMessage(
|
||||||
|
to=settings.agent_settings.vad_name,
|
||||||
|
sender=self.name,
|
||||||
|
body="PAUSE",
|
||||||
|
)
|
||||||
|
await self.send(vad_message)
|
||||||
|
self.logger.info("Sent pause command to VAD Agent and RI Communication Agent.")
|
||||||
|
else:
|
||||||
|
# Send resume to VAD agent
|
||||||
|
vad_message = InternalMessage(
|
||||||
|
to=settings.agent_settings.vad_name,
|
||||||
|
sender=self.name,
|
||||||
|
body="RESUME",
|
||||||
|
)
|
||||||
|
await self.send(vad_message)
|
||||||
|
self.logger.info("Sent resume command to VAD Agent and RI Communication Agent.")
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
"""
|
"""
|
||||||
Initialize the agent.
|
Initialize the agent.
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ async def lifespan(app: FastAPI):
|
|||||||
BDICoreAgent,
|
BDICoreAgent,
|
||||||
{
|
{
|
||||||
"name": settings.agent_settings.bdi_core_name,
|
"name": settings.agent_settings.bdi_core_name,
|
||||||
"asl": "src/control_backend/agents/bdi/rules.asl",
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"BeliefCollectorAgent": (
|
"BeliefCollectorAgent": (
|
||||||
|
|||||||
14
src/control_backend/schemas/belief_list.py
Normal file
14
src/control_backend/schemas/belief_list.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from control_backend.schemas.program import Belief as ProgramBelief
|
||||||
|
|
||||||
|
|
||||||
|
class BeliefList(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents a list of beliefs, separated from a program. Useful in agents which need to
|
||||||
|
communicate beliefs.
|
||||||
|
|
||||||
|
:ivar: beliefs: The list of beliefs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
beliefs: list[ProgramBelief]
|
||||||
@@ -43,7 +43,6 @@ class SemanticBelief(ProgramElement):
|
|||||||
:ivar description: Description of how to form the belief, used by the LLM.
|
:ivar description: Description of how to form the belief, used by the LLM.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str = ""
|
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
|
|
||||||
@@ -113,10 +112,12 @@ class Goal(ProgramElement):
|
|||||||
for example when the achieving of the goal is dependent on the user's reply, this means
|
for example when the achieving of the goal is dependent on the user's reply, this means
|
||||||
that the achieved status will be set from somewhere else in the program.
|
that the achieved status will be set from somewhere else in the program.
|
||||||
|
|
||||||
|
:ivar description: A description of the goal, used to determine if it has been achieved.
|
||||||
:ivar plan: The plan to execute.
|
:ivar plan: The plan to execute.
|
||||||
:ivar can_fail: Whether we can fail to achieve the goal after executing the plan.
|
:ivar can_fail: Whether we can fail to achieve the goal after executing the plan.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
description: str
|
||||||
plan: Plan
|
plan: Plan
|
||||||
can_fail: bool = True
|
can_fail: bool = True
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class RIEndpoint(str, Enum):
|
|||||||
GESTURE_TAG = "actuate/gesture/tag"
|
GESTURE_TAG = "actuate/gesture/tag"
|
||||||
PING = "ping"
|
PING = "ping"
|
||||||
NEGOTIATE_PORTS = "negotiate/ports"
|
NEGOTIATE_PORTS = "negotiate/ports"
|
||||||
|
PAUSE = ""
|
||||||
|
|
||||||
|
|
||||||
class RIMessage(BaseModel):
|
class RIMessage(BaseModel):
|
||||||
@@ -64,3 +65,15 @@ class GestureCommand(RIMessage):
|
|||||||
if self.endpoint not in allowed:
|
if self.endpoint not in allowed:
|
||||||
raise ValueError("endpoint must be GESTURE_SINGLE or GESTURE_TAG")
|
raise ValueError("endpoint must be GESTURE_SINGLE or GESTURE_TAG")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class PauseCommand(RIMessage):
|
||||||
|
"""
|
||||||
|
A specific command to pause or unpause the robot's actions.
|
||||||
|
|
||||||
|
:ivar endpoint: Fixed to ``RIEndpoint.PAUSE``.
|
||||||
|
:ivar data: A boolean indicating whether to pause (True) or unpause (False).
|
||||||
|
"""
|
||||||
|
|
||||||
|
endpoint: RIEndpoint = RIEndpoint(RIEndpoint.PAUSE)
|
||||||
|
data: bool
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def mock_agentspeak_env():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def agent():
|
def agent():
|
||||||
agent = BDICoreAgent("bdi_agent", "dummy.asl")
|
agent = BDICoreAgent("bdi_agent")
|
||||||
agent.send = AsyncMock()
|
agent.send = AsyncMock()
|
||||||
agent.bdi_agent = MagicMock()
|
agent.bdi_agent = MagicMock()
|
||||||
return agent
|
return agent
|
||||||
@@ -133,14 +133,14 @@ async def test_custom_actions(agent):
|
|||||||
|
|
||||||
# Invoke action
|
# Invoke action
|
||||||
mock_term = MagicMock()
|
mock_term = MagicMock()
|
||||||
mock_term.args = ["Hello", "Norm", "Goal"]
|
mock_term.args = ["Hello", "Norm"]
|
||||||
mock_intention = MagicMock()
|
mock_intention = MagicMock()
|
||||||
|
|
||||||
# Run generator
|
# Run generator
|
||||||
gen = action_fn(agent, mock_term, mock_intention)
|
gen = action_fn(agent, mock_term, mock_intention)
|
||||||
next(gen) # Execute
|
next(gen) # Execute
|
||||||
|
|
||||||
agent._send_to_llm.assert_called_with("Hello", "Norm", "Goal")
|
agent._send_to_llm.assert_called_with("Hello", "Norm", "")
|
||||||
|
|
||||||
|
|
||||||
def test_add_belief_sets_event(agent):
|
def test_add_belief_sets_event(agent):
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ def make_valid_program_json(norm="N1", goal="G1") -> str:
|
|||||||
Goal(
|
Goal(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name=goal,
|
name=goal,
|
||||||
|
description="This description can be used to determine whether the goal "
|
||||||
|
"has been achieved.",
|
||||||
plan=Plan(
|
plan=Plan(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name="Goal Plan",
|
name="Goal Plan",
|
||||||
@@ -53,7 +55,7 @@ async def test_send_to_bdi():
|
|||||||
manager.send = AsyncMock()
|
manager.send = AsyncMock()
|
||||||
|
|
||||||
program = Program.model_validate_json(make_valid_program_json())
|
program = Program.model_validate_json(make_valid_program_json())
|
||||||
await manager._send_to_bdi(program)
|
await manager._create_agentspeak_and_send_to_bdi(program)
|
||||||
|
|
||||||
assert manager.send.await_count == 1
|
assert manager.send.await_count == 1
|
||||||
msg: InternalMessage = manager.send.await_args[0][0]
|
msg: InternalMessage = manager.send.await_args[0][0]
|
||||||
@@ -75,8 +77,9 @@ async def test_receive_programs_valid_and_invalid():
|
|||||||
]
|
]
|
||||||
|
|
||||||
manager = BDIProgramManager(name="program_manager_test")
|
manager = BDIProgramManager(name="program_manager_test")
|
||||||
|
manager._internal_pub_socket = AsyncMock()
|
||||||
manager.sub_socket = sub
|
manager.sub_socket = sub
|
||||||
manager._send_to_bdi = AsyncMock()
|
manager._create_agentspeak_and_send_to_bdi = AsyncMock()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Will give StopAsyncIteration when the predefined `sub.recv_multipart` side-effects run out
|
# Will give StopAsyncIteration when the predefined `sub.recv_multipart` side-effects run out
|
||||||
@@ -85,7 +88,7 @@ async def test_receive_programs_valid_and_invalid():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Only valid Program should have triggered _send_to_bdi
|
# Only valid Program should have triggered _send_to_bdi
|
||||||
assert manager._send_to_bdi.await_count == 1
|
assert manager._create_agentspeak_and_send_to_bdi.await_count == 1
|
||||||
forwarded: Program = manager._send_to_bdi.await_args[0][0]
|
forwarded: Program = manager._create_agentspeak_and_send_to_bdi.await_args[0][0]
|
||||||
assert forwarded.phases[0].norms[0].name == "N1"
|
assert forwarded.phases[0].norms[0].name == "N1"
|
||||||
assert forwarded.phases[0].goals[0].name == "G1"
|
assert forwarded.phases[0].goals[0].name == "G1"
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import pytest
|
|||||||
from control_backend.agents.bdi import TextBeliefExtractorAgent
|
from control_backend.agents.bdi import TextBeliefExtractorAgent
|
||||||
from control_backend.core.agent_system import InternalMessage
|
from control_backend.core.agent_system import InternalMessage
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import settings
|
||||||
|
from control_backend.schemas.belief_list import BeliefList
|
||||||
from control_backend.schemas.belief_message import BeliefMessage
|
from control_backend.schemas.belief_message import BeliefMessage
|
||||||
from control_backend.schemas.program import (
|
from control_backend.schemas.program import (
|
||||||
ConditionalNorm,
|
ConditionalNorm,
|
||||||
|
KeywordBelief,
|
||||||
LLMAction,
|
LLMAction,
|
||||||
Phase,
|
Phase,
|
||||||
Plan,
|
Plan,
|
||||||
@@ -186,13 +188,31 @@ async def test_retry_query_llm_fail_immediately(agent):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_extracting_beliefs_from_program(agent, sample_program):
|
async def test_extracting_semantic_beliefs(agent):
|
||||||
|
"""
|
||||||
|
The Program Manager sends beliefs to this agent. Test whether the agent handles them correctly.
|
||||||
|
"""
|
||||||
assert len(agent.available_beliefs) == 0
|
assert len(agent.available_beliefs) == 0
|
||||||
|
beliefs = BeliefList(
|
||||||
|
beliefs=[
|
||||||
|
KeywordBelief(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
name="keyword_hello",
|
||||||
|
keyword="hello",
|
||||||
|
),
|
||||||
|
SemanticBelief(
|
||||||
|
id=uuid.uuid4(), name="semantic_hello_1", description="Some semantic belief 1"
|
||||||
|
),
|
||||||
|
SemanticBelief(
|
||||||
|
id=uuid.uuid4(), name="semantic_hello_2", description="Some semantic belief 2"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
await agent.handle_message(
|
await agent.handle_message(
|
||||||
InternalMessage(
|
InternalMessage(
|
||||||
to=settings.agent_settings.text_belief_extractor_name,
|
to=settings.agent_settings.text_belief_extractor_name,
|
||||||
sender=settings.agent_settings.bdi_program_manager_name,
|
sender=settings.agent_settings.bdi_program_manager_name,
|
||||||
body=sample_program.model_dump_json(),
|
body=beliefs.model_dump_json(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert len(agent.available_beliefs) == 2
|
assert len(agent.available_beliefs) == 2
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ def make_valid_program_dict():
|
|||||||
Goal(
|
Goal(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name="Some goal",
|
name="Some goal",
|
||||||
|
description="This description can be used to determine whether the goal "
|
||||||
|
"has been achieved.",
|
||||||
plan=Plan(
|
plan=Plan(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name="Goal Plan",
|
name="Goal Plan",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def base_goal() -> Goal:
|
|||||||
return Goal(
|
return Goal(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name="testGoalName",
|
name="testGoalName",
|
||||||
|
description="This description can be used to determine whether the goal has been achieved.",
|
||||||
plan=Plan(
|
plan=Plan(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
name="testGoalPlanName",
|
name="testGoalPlanName",
|
||||||
|
|||||||
Reference in New Issue
Block a user