feat: support force completed goals in semantic belief agent
ref: N25B-427
This commit is contained in:
@@ -18,6 +18,7 @@ from control_backend.agents.bdi.agentspeak_ast import (
|
|||||||
TriggerType,
|
TriggerType,
|
||||||
)
|
)
|
||||||
from control_backend.schemas.program import (
|
from control_backend.schemas.program import (
|
||||||
|
BaseGoal,
|
||||||
BasicNorm,
|
BasicNorm,
|
||||||
ConditionalNorm,
|
ConditionalNorm,
|
||||||
GestureAction,
|
GestureAction,
|
||||||
@@ -436,7 +437,7 @@ class AgentSpeakGenerator:
|
|||||||
|
|
||||||
@slugify.register
|
@slugify.register
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _(g: Goal) -> str:
|
def _(g: BaseGoal) -> str:
|
||||||
return AgentSpeakGenerator._slugify_str(g.name)
|
return AgentSpeakGenerator._slugify_str(g.name)
|
||||||
|
|
||||||
@slugify.register
|
@slugify.register
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from control_backend.schemas.belief_list import BeliefList, GoalList
|
|||||||
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 Goal, SemanticBelief
|
from control_backend.schemas.program import BaseGoal, SemanticBelief
|
||||||
|
|
||||||
type JSONLike = None | bool | int | float | str | list["JSONLike"] | dict[str, "JSONLike"]
|
type JSONLike = None | bool | int | float | str | list["JSONLike"] | dict[str, "JSONLike"]
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
self.goal_inferrer = GoalAchievementInferrer(self._llm)
|
self.goal_inferrer = GoalAchievementInferrer(self._llm)
|
||||||
self._current_beliefs = BeliefState()
|
self._current_beliefs = BeliefState()
|
||||||
self._current_goal_completions: dict[str, bool] = {}
|
self._current_goal_completions: dict[str, bool] = {}
|
||||||
|
self._force_completed_goals: set[BaseGoal] = set()
|
||||||
self.conversation = ChatHistory(messages=[])
|
self.conversation = ChatHistory(messages=[])
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
@@ -118,13 +119,15 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
case "goals":
|
case "goals":
|
||||||
self._handle_goals_message(msg)
|
self._handle_goals_message(msg)
|
||||||
await self._infer_goal_completions()
|
await self._infer_goal_completions()
|
||||||
|
case "achieved_goals":
|
||||||
|
self._handle_goal_achieved_message(msg)
|
||||||
case "conversation_history":
|
case "conversation_history":
|
||||||
if msg.body == "reset":
|
if msg.body == "reset":
|
||||||
self._reset()
|
self._reset_phase()
|
||||||
case _:
|
case _:
|
||||||
self.logger.warning("Received unexpected message from %s", msg.sender)
|
self.logger.warning("Received unexpected message from %s", msg.sender)
|
||||||
|
|
||||||
def _reset(self):
|
def _reset_phase(self):
|
||||||
self.conversation = ChatHistory(messages=[])
|
self.conversation = ChatHistory(messages=[])
|
||||||
self.belief_inferrer.available_beliefs.clear()
|
self.belief_inferrer.available_beliefs.clear()
|
||||||
self._current_beliefs = BeliefState()
|
self._current_beliefs = BeliefState()
|
||||||
@@ -158,7 +161,8 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Use only goals that can fail, as the others are always assumed to be completed
|
# Use only goals that can fail, as the others are always assumed to be completed
|
||||||
available_goals = [g for g in goals_list.goals if g.can_fail]
|
available_goals = {g for g in goals_list.goals if g.can_fail}
|
||||||
|
available_goals -= self._force_completed_goals
|
||||||
self.goal_inferrer.goals = available_goals
|
self.goal_inferrer.goals = available_goals
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Received %d failable goals from the program manager: %s",
|
"Received %d failable goals from the program manager: %s",
|
||||||
@@ -166,6 +170,23 @@ class TextBeliefExtractorAgent(BaseAgent):
|
|||||||
", ".join(g.name for g in available_goals),
|
", ".join(g.name for g in available_goals),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_goal_achieved_message(self, msg: InternalMessage):
|
||||||
|
# NOTE: When goals can be marked unachieved, remember to re-add them to the goal_inferrer
|
||||||
|
try:
|
||||||
|
goals_list = GoalList.model_validate_json(msg.body)
|
||||||
|
except ValidationError:
|
||||||
|
self.logger.warning(
|
||||||
|
"Received goal achieved message from the program manager, "
|
||||||
|
"but it is not a valid list of goals."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for goal in goals_list.goals:
|
||||||
|
self._force_completed_goals.add(goal)
|
||||||
|
self._current_goal_completions[f"achieved_{AgentSpeakGenerator.slugify(goal)}"] = True
|
||||||
|
|
||||||
|
self.goal_inferrer.goals -= self._force_completed_goals
|
||||||
|
|
||||||
async def _user_said(self, text: str):
|
async def _user_said(self, text: str):
|
||||||
"""
|
"""
|
||||||
Create a belief for the user's full speech.
|
Create a belief for the user's full speech.
|
||||||
@@ -445,7 +466,7 @@ Respond with a JSON similar to the following, but with the property names as giv
|
|||||||
class GoalAchievementInferrer(SemanticBeliefInferrer):
|
class GoalAchievementInferrer(SemanticBeliefInferrer):
|
||||||
def __init__(self, llm: TextBeliefExtractorAgent.LLM):
|
def __init__(self, llm: TextBeliefExtractorAgent.LLM):
|
||||||
super().__init__(llm)
|
super().__init__(llm)
|
||||||
self.goals = []
|
self.goals: set[BaseGoal] = set()
|
||||||
|
|
||||||
async def infer_from_conversation(self, conversation: ChatHistory) -> dict[str, bool]:
|
async def infer_from_conversation(self, conversation: ChatHistory) -> dict[str, bool]:
|
||||||
"""
|
"""
|
||||||
@@ -465,7 +486,7 @@ class GoalAchievementInferrer(SemanticBeliefInferrer):
|
|||||||
for goal, achieved in zip(self.goals, goals_achieved, strict=True)
|
for goal, achieved in zip(self.goals, goals_achieved, strict=True)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _infer_goal(self, conversation: ChatHistory, goal: Goal) -> bool:
|
async def _infer_goal(self, conversation: ChatHistory, goal: BaseGoal) -> bool:
|
||||||
prompt = f"""{self._format_conversation(conversation)}
|
prompt = f"""{self._format_conversation(conversation)}
|
||||||
|
|
||||||
Given the above conversation, what has the following goal been achieved?
|
Given the above conversation, what has the following goal been achieved?
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ class UserInterruptAgent(BaseAgent):
|
|||||||
async def _send_to_bdi_belief(self, asl_goal: str):
|
async def _send_to_bdi_belief(self, asl_goal: str):
|
||||||
"""Send belief to BDI Core"""
|
"""Send belief to BDI Core"""
|
||||||
belief_name = f"achieved_{asl_goal}"
|
belief_name = f"achieved_{asl_goal}"
|
||||||
belief = Belief(name=belief_name)
|
belief = Belief(name=belief_name, arguments=None)
|
||||||
self.logger.debug(f"Sending belief to BDI Core: {belief_name}")
|
self.logger.debug(f"Sending belief to BDI Core: {belief_name}")
|
||||||
belief_message = BeliefMessage(create=[belief])
|
belief_message = BeliefMessage(create=[belief])
|
||||||
msg = InternalMessage(
|
msg = InternalMessage(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from control_backend.schemas.program import BaseGoal
|
||||||
from control_backend.schemas.program import Belief as ProgramBelief
|
from control_backend.schemas.program import Belief as ProgramBelief
|
||||||
from control_backend.schemas.program import Goal
|
|
||||||
|
|
||||||
|
|
||||||
class BeliefList(BaseModel):
|
class BeliefList(BaseModel):
|
||||||
@@ -16,4 +16,4 @@ class BeliefList(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class GoalList(BaseModel):
|
class GoalList(BaseModel):
|
||||||
goals: list[Goal]
|
goals: list[BaseGoal]
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class ProgramElement(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
id: UUID4
|
id: UUID4
|
||||||
|
|
||||||
|
# To make program elements hashable
|
||||||
|
model_config = {"frozen": True}
|
||||||
|
|
||||||
|
|
||||||
class LogicalOperator(Enum):
|
class LogicalOperator(Enum):
|
||||||
AND = "AND"
|
AND = "AND"
|
||||||
@@ -105,23 +108,33 @@ class Plan(ProgramElement):
|
|||||||
steps: list[PlanElement]
|
steps: list[PlanElement]
|
||||||
|
|
||||||
|
|
||||||
class Goal(ProgramElement):
|
class BaseGoal(ProgramElement):
|
||||||
"""
|
"""
|
||||||
Represents an objective to be achieved. To reach the goal, we should execute
|
Represents an objective to be achieved. This base version does not include a plan to achieve
|
||||||
the corresponding plan. If we can fail to achieve a goal after executing the plan,
|
this goal, and is used in semantic belief extraction.
|
||||||
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.
|
|
||||||
|
|
||||||
:ivar description: A description of the goal, used to determine if it has been achieved.
|
:ivar description: A description of the goal, used to determine if it has been achieved.
|
||||||
: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 = ""
|
description: str = ""
|
||||||
plan: Plan
|
|
||||||
can_fail: bool = True
|
can_fail: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(BaseGoal):
|
||||||
|
"""
|
||||||
|
Represents an objective to be achieved. To reach the goal, we should execute the corresponding
|
||||||
|
plan. It inherits from the BaseGoal a variable `can_fail`, which if true will cause the
|
||||||
|
completion to be determined based on the conversation.
|
||||||
|
|
||||||
|
Instances of this goal are not hashable because a plan is not hashable.
|
||||||
|
|
||||||
|
:ivar plan: The plan to execute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plan: Plan
|
||||||
|
|
||||||
|
|
||||||
type Action = SpeechAction | GestureAction | LLMAction
|
type Action = SpeechAction | GestureAction | LLMAction
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user