From f79b65a6fae1f6fee08e17a33c66944370aa4da6 Mon Sep 17 00:00:00 2001 From: Kasper Marinus Date: Thu, 29 Jan 2026 15:18:09 +0100 Subject: [PATCH 1/2] fix: allow subgoals in triggers and empty plan Copies the goal and changes can_fail to false. Also add a warning for empty plans in goals. ref: N25B-460 --- .../agents/bdi/agentspeak_generator.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/control_backend/agents/bdi/agentspeak_generator.py b/src/control_backend/agents/bdi/agentspeak_generator.py index 7c9d8f0..c9e5275 100644 --- a/src/control_backend/agents/bdi/agentspeak_generator.py +++ b/src/control_backend/agents/bdi/agentspeak_generator.py @@ -1,3 +1,4 @@ +import logging from functools import singledispatchmethod from slugify import slugify @@ -59,6 +60,7 @@ class AgentSpeakGenerator: """ _asp: AstProgram + logger = logging.getLogger(__name__) def generate(self, program: Program) -> str: """ @@ -472,7 +474,8 @@ class AgentSpeakGenerator: :param main_goal: Whether this is a main goal (for UI notification purposes). """ context: list[AstExpression] = [self._astify(phase)] - context.append(~self._astify(goal, achieved=True)) + if goal.can_fail: + 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: @@ -496,6 +499,10 @@ class AgentSpeakGenerator: if not goal.can_fail and not continues_response: body.append(AstStatement(StatementType.ADD_BELIEF, self._astify(goal, achieved=True))) + if len(body) == 0: + self.logger.warning("Goal with no plan detected: %s", goal.name) + body.append(AstStatement(StatementType.EMPTY, AstLiteral("true"))) + self._asp.plans.append(AstPlan(TriggerType.ADDED_GOAL, self._astify(goal), context, body)) self._asp.plans.append( @@ -556,10 +563,10 @@ class AgentSpeakGenerator: ) ) 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) + new_step = step.model_copy(update={"can_fail": False}) # triggers are sequence + subgoals.append(new_step) + body.append(self._step_to_statement(step)) # Arbitrary wait for UI to display nicely body.append( From b53bf872a5345fa03f13015c3630efbbd911e745 Mon Sep 17 00:00:00 2001 From: Kasper Date: Fri, 30 Jan 2026 19:30:40 +0100 Subject: [PATCH 2/2] chore: better name checks for ProgramElement --- src/control_backend/schemas/program.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/control_backend/schemas/program.py b/src/control_backend/schemas/program.py index 8f23cb9..1f3d1d9 100644 --- a/src/control_backend/schemas/program.py +++ b/src/control_backend/schemas/program.py @@ -7,7 +7,7 @@ University within the Software Project course. from enum import Enum from typing import Literal -from pydantic import UUID4, BaseModel +from pydantic import UUID4, BaseModel, field_validator class ProgramElement(BaseModel): @@ -24,6 +24,13 @@ class ProgramElement(BaseModel): # To make program elements hashable model_config = {"frozen": True} + @field_validator("name") + @classmethod + def name_must_not_start_with_number(cls, v: str) -> str: + if v and v[0].isdigit(): + raise ValueError('Field "name" must not start with a number.') + return v + class LogicalOperator(Enum): """ @@ -105,6 +112,7 @@ class InferredBelief(ProgramElement): left: Belief right: Belief + class EmotionBelief(ProgramElement): """ Represents a belief that is set when a certain emotion is detected. @@ -115,6 +123,7 @@ class EmotionBelief(ProgramElement): name: str = "" emotion: str + class Norm(ProgramElement): """ Base class for behavioral norms that guide the robot's interactions. @@ -329,4 +338,4 @@ class Program(BaseModel): if __name__ == "__main__": input = input("Enter program JSON: ") program = Program.model_validate_json(input) - print(program) \ No newline at end of file + print(program)