feat: support force completed goals in semantic belief agent

ref: N25B-427
This commit is contained in:
Twirre Meulenbelt
2026-01-13 17:04:44 +01:00
parent 8f52f8bf0c
commit f7669c021b
5 changed files with 52 additions and 17 deletions

View File

@@ -18,6 +18,7 @@ from control_backend.agents.bdi.agentspeak_ast import (
TriggerType,
)
from control_backend.schemas.program import (
BaseGoal,
BasicNorm,
ConditionalNorm,
GestureAction,
@@ -436,7 +437,7 @@ class AgentSpeakGenerator:
@slugify.register
@staticmethod
def _(g: Goal) -> str:
def _(g: BaseGoal) -> str:
return AgentSpeakGenerator._slugify_str(g.name)
@slugify.register

View File

@@ -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 BeliefMessage
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"]
@@ -62,6 +62,7 @@ class TextBeliefExtractorAgent(BaseAgent):
self.goal_inferrer = GoalAchievementInferrer(self._llm)
self._current_beliefs = BeliefState()
self._current_goal_completions: dict[str, bool] = {}
self._force_completed_goals: set[BaseGoal] = set()
self.conversation = ChatHistory(messages=[])
async def setup(self):
@@ -118,13 +119,15 @@ class TextBeliefExtractorAgent(BaseAgent):
case "goals":
self._handle_goals_message(msg)
await self._infer_goal_completions()
case "achieved_goals":
self._handle_goal_achieved_message(msg)
case "conversation_history":
if msg.body == "reset":
self._reset()
self._reset_phase()
case _:
self.logger.warning("Received unexpected message from %s", msg.sender)
def _reset(self):
def _reset_phase(self):
self.conversation = ChatHistory(messages=[])
self.belief_inferrer.available_beliefs.clear()
self._current_beliefs = BeliefState()
@@ -158,7 +161,8 @@ class TextBeliefExtractorAgent(BaseAgent):
return
# 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.logger.debug(
"Received %d failable goals from the program manager: %s",
@@ -166,6 +170,23 @@ class TextBeliefExtractorAgent(BaseAgent):
", ".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):
"""
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):
def __init__(self, llm: TextBeliefExtractorAgent.LLM):
super().__init__(llm)
self.goals = []
self.goals: set[BaseGoal] = set()
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)
}
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)}
Given the above conversation, what has the following goal been achieved?

View File

@@ -310,7 +310,7 @@ class UserInterruptAgent(BaseAgent):
async def _send_to_bdi_belief(self, asl_goal: str):
"""Send belief to BDI Core"""
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}")
belief_message = BeliefMessage(create=[belief])
msg = InternalMessage(

View File

@@ -1,7 +1,7 @@
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 Goal
class BeliefList(BaseModel):
@@ -16,4 +16,4 @@ class BeliefList(BaseModel):
class GoalList(BaseModel):
goals: list[Goal]
goals: list[BaseGoal]

View File

@@ -15,6 +15,9 @@ class ProgramElement(BaseModel):
name: str
id: UUID4
# To make program elements hashable
model_config = {"frozen": True}
class LogicalOperator(Enum):
AND = "AND"
@@ -105,23 +108,33 @@ class Plan(ProgramElement):
steps: list[PlanElement]
class Goal(ProgramElement):
class BaseGoal(ProgramElement):
"""
Represents an objective to be achieved. To reach the goal, we should execute
the corresponding plan. If we can fail to achieve a goal after executing the plan,
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.
Represents an objective to be achieved. This base version does not include a plan to achieve
this goal, and is used in semantic belief extraction.
: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.
"""
description: str = ""
plan: Plan
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