From b9df47b7d160ba8c54b46a82ce86cf10f429feac Mon Sep 17 00:00:00 2001 From: Kasper Marinus Date: Mon, 26 Jan 2026 12:21:04 +0100 Subject: [PATCH] docs: add docstrings to AgentSpeak stuff ref: N25B-449 --- .../agents/bdi/agentspeak_ast.py | 265 ++++++++++++ .../agents/bdi/agentspeak_generator.py | 376 +++++++++++++++++- src/control_backend/schemas/program.py | 87 +++- 3 files changed, 711 insertions(+), 17 deletions(-) diff --git a/src/control_backend/agents/bdi/agentspeak_ast.py b/src/control_backend/agents/bdi/agentspeak_ast.py index 19f48e2..12c7947 100644 --- a/src/control_backend/agents/bdi/agentspeak_ast.py +++ b/src/control_backend/agents/bdi/agentspeak_ast.py @@ -8,31 +8,78 @@ from enum import StrEnum class AstNode(ABC): """ Abstract base class for all elements of an AgentSpeak program. + + This class serves as the foundation for all AgentSpeak abstract syntax tree (AST) nodes. + It defines the core interface that all AST nodes must implement to generate AgentSpeak code. """ @abstractmethod def _to_agentspeak(self) -> str: """ Generates the AgentSpeak code string. + + This method converts the AST node into its corresponding + AgentSpeak source code representation. + + :return: The AgentSpeak code string representation of this node. """ pass def __str__(self) -> str: + """ + Returns the string representation of this AST node. + + This method provides a convenient way to get the AgentSpeak code representation + by delegating to the _to_agentspeak method. + + :return: The AgentSpeak code string representation of this node. + """ return self._to_agentspeak() class AstExpression(AstNode, ABC): """ Intermediate class for anything that can be used in a logical expression. + + This class extends AstNode to provide common functionality for all expressions + that can be used in logical operations within AgentSpeak programs. """ def __and__(self, other: ExprCoalescible) -> AstBinaryOp: + """ + Creates a logical AND operation between this expression and another. + + This method allows for operator overloading of the & operator to create + binary logical operations in a more intuitive syntax. + + :param other: The right-hand side expression to combine with this one. + :return: A new AstBinaryOp representing the logical AND operation. + """ return AstBinaryOp(self, BinaryOperatorType.AND, _coalesce_expr(other)) def __or__(self, other: ExprCoalescible) -> AstBinaryOp: + """ + Creates a logical OR operation between this expression and another. + + This method allows for operator overloading of the | operator to create + binary logical operations in a more intuitive syntax. + + :param other: The right-hand side expression to combine with this one. + :return: A new AstBinaryOp representing the logical OR operation. + """ return AstBinaryOp(self, BinaryOperatorType.OR, _coalesce_expr(other)) def __invert__(self) -> AstLogicalExpression: + """ + Creates a logical negation of this expression. + + This method allows for operator overloading of the ~ operator to create + negated expressions. If the expression is already a logical expression, + it toggles the negation flag. Otherwise, it wraps the expression in a + new AstLogicalExpression with negation set to True. + + :return: An AstLogicalExpression representing the negated form of this expression. + """ if isinstance(self, AstLogicalExpression): self.negated = not self.negated return self @@ -81,11 +128,25 @@ class AstTerm(AstExpression, ABC): class AstAtom(AstTerm): """ Represents a grounded atom in AgentSpeak (e.g., lowercase constants). + + Atoms are the simplest form of terms in AgentSpeak, representing concrete, + unchanging values. They are typically used as constants in logical expressions. + + :ivar value: The string value of this atom, which will be converted to lowercase + in the AgentSpeak representation. """ value: str def _to_agentspeak(self) -> str: + """ + Converts this atom to its AgentSpeak string representation. + + Atoms are represented in lowercase in AgentSpeak to distinguish them + from variables (which are capitalized). + + :return: The lowercase string representation of this atom. + """ return self.value.lower() @@ -93,11 +154,25 @@ class AstAtom(AstTerm): class AstVar(AstTerm): """ Represents an ungrounded variable in AgentSpeak (e.g., capitalized names). + + Variables in AgentSpeak are placeholders that can be bound to specific values + during execution. They are distinguished from atoms by their capitalization. + + :ivar name: The name of this variable, which will be capitalized in the + AgentSpeak representation. """ name: str def _to_agentspeak(self) -> str: + """ + Converts this variable to its AgentSpeak string representation. + + Variables are represented with capitalized names in AgentSpeak to distinguish + them from atoms (which are lowercase). + + :return: The capitalized string representation of this variable. + """ return self.name.capitalize() @@ -105,11 +180,21 @@ class AstVar(AstTerm): class AstNumber(AstTerm): """ Represents a numeric constant in AgentSpeak. + + Numeric constants can be either integers or floating-point numbers and are + used in logical expressions and comparisons. + + :ivar value: The numeric value of this constant (can be int or float). """ value: int | float def _to_agentspeak(self) -> str: + """ + Converts this numeric constant to its AgentSpeak string representation. + + :return: The string representation of the numeric value. + """ return str(self.value) @@ -117,11 +202,23 @@ class AstNumber(AstTerm): class AstString(AstTerm): """ Represents a string literal in AgentSpeak. + + String literals are used to represent textual data and are enclosed in + double quotes in the AgentSpeak representation. + + :ivar value: The string content of this literal. """ value: str def _to_agentspeak(self) -> str: + """ + Converts this string literal to its AgentSpeak string representation. + + String literals are enclosed in double quotes in AgentSpeak. + + :return: The string literal enclosed in double quotes. + """ return f'"{self.value}"' @@ -129,12 +226,26 @@ class AstString(AstTerm): class AstLiteral(AstTerm): """ Represents a literal (functor and terms) in AgentSpeak. + + Literals are the fundamental building blocks of AgentSpeak programs, consisting + of a functor (predicate name) and an optional list of terms (arguments). + + :ivar functor: The name of the predicate or function. + :ivar terms: A list of terms (arguments) for this literal. Defaults to an empty list. """ functor: str terms: list[AstTerm] = field(default_factory=list) def _to_agentspeak(self) -> str: + """ + Converts this literal to its AgentSpeak string representation. + + If the literal has no terms, it returns just the functor name. + Otherwise, it returns the functor followed by the terms in parentheses. + + :return: The AgentSpeak string representation of this literal. + """ if not self.terms: return self.functor args = ", ".join(map(str, self.terms)) @@ -142,6 +253,13 @@ class AstLiteral(AstTerm): class BinaryOperatorType(StrEnum): + """ + Enumeration of binary operator types used in AgentSpeak expressions. + + These operators are used to create binary operations between expressions, + including logical operations (AND, OR) and comparison operations. + """ + AND = "&" OR = "|" GREATER_THAN = ">" @@ -156,6 +274,13 @@ class BinaryOperatorType(StrEnum): class AstBinaryOp(AstExpression): """ Represents a binary logical or relational operation in AgentSpeak. + + Binary operations combine two expressions using a logical or comparison operator. + They are used to create complex logical conditions in AgentSpeak programs. + + :ivar left: The left-hand side expression of the operation. + :ivar operator: The binary operator type (AND, OR, comparison operators, etc.). + :ivar right: The right-hand side expression of the operation. """ left: AstExpression @@ -163,10 +288,25 @@ class AstBinaryOp(AstExpression): right: AstExpression def __post_init__(self): + """ + Post-initialization processing to ensure proper expression types. + + This method wraps the left and right expressions in AstLogicalExpression + instances if they aren't already, ensuring consistent handling throughout + the AST. + """ self.left = _as_logical(self.left) self.right = _as_logical(self.right) def _to_agentspeak(self) -> str: + """ + Converts this binary operation to its AgentSpeak string representation. + + The method handles proper parenthesization of sub-expressions to maintain + correct operator precedence and readability. + + :return: The AgentSpeak string representation of this binary operation. + """ l_str = str(self.left) r_str = str(self.right) @@ -185,12 +325,27 @@ class AstBinaryOp(AstExpression): class AstLogicalExpression(AstExpression): """ Represents a logical expression, potentially negated, in AgentSpeak. + + Logical expressions can be either positive or negated and form the basis + of conditions and beliefs in AgentSpeak programs. + + :ivar expression: The underlying expression being evaluated. + :ivar negated: Boolean flag indicating whether this expression is negated. """ expression: AstExpression negated: bool = False def _to_agentspeak(self) -> str: + """ + Converts this logical expression to its AgentSpeak string representation. + + If the expression is negated, it prepends 'not ' to the expression string. + For complex expressions (binary operations), it adds parentheses when negated + to maintain correct logical interpretation. + + :return: The AgentSpeak string representation of this logical expression. + """ expr_str = str(self.expression) if isinstance(self.expression, AstBinaryOp) and self.negated: expr_str = f"({expr_str})" @@ -198,31 +353,76 @@ class AstLogicalExpression(AstExpression): def _as_logical(expr: AstExpression) -> AstLogicalExpression: + """ + Converts an expression to a logical expression if it isn't already. + + This helper function ensures that expressions are properly wrapped in + AstLogicalExpression instances, which is necessary for consistent handling + of logical operations in the AST. + + :param expr: The expression to convert. + :return: The expression wrapped in an AstLogicalExpression if it wasn't already. + """ if isinstance(expr, AstLogicalExpression): return expr return AstLogicalExpression(expr) class StatementType(StrEnum): + """ + Enumeration of statement types that can appear in AgentSpeak plans. + + These statement types define the different kinds of actions and operations + that can be performed within the body of an AgentSpeak plan. + """ + EMPTY = "" + """Empty statement (no operation, used when evaluating a plan to true).""" + DO_ACTION = "." + """Execute an action defined in Python.""" + ACHIEVE_GOAL = "!" + """Achieve a goal (add a goal to be accomplished).""" + TEST_GOAL = "?" + """Test a goal (check if a goal can be achieved).""" + ADD_BELIEF = "+" + """Add a belief to the belief base.""" + REMOVE_BELIEF = "-" + """Remove a belief from the belief base.""" + REPLACE_BELIEF = "-+" + """Replace a belief in the belief base.""" @dataclass class AstStatement(AstNode): """ A statement that can appear inside a plan. + + Statements are the executable units within AgentSpeak plans. They consist + of a statement type (defining the operation) and an expression (defining + what to operate on). + + :ivar type: The type of statement (action, goal, belief operation, etc.). + :ivar expression: The expression that this statement operates on. """ type: StatementType expression: AstExpression def _to_agentspeak(self) -> str: + """ + Converts this statement to its AgentSpeak string representation. + + The representation consists of the statement type prefix followed by + the expression. + + :return: The AgentSpeak string representation of this statement. + """ return f"{self.type.value}{self.expression}" @@ -230,26 +430,59 @@ class AstStatement(AstNode): class AstRule(AstNode): """ Represents an inference rule in AgentSpeak. If there is no condition, it always holds. + + Rules define logical implications in AgentSpeak programs. They consist of a + result (conclusion) and an optional condition (premise). When the condition + holds, the result is inferred to be true. + + :ivar result: The conclusion or result of this rule. + :ivar condition: The premise or condition for this rule (optional). """ result: AstExpression condition: AstExpression | None = None def __post_init__(self): + """ + Post-initialization processing to ensure proper expression types. + + If a condition is provided, this method wraps it in an AstLogicalExpression + to ensure consistent handling throughout the AST. + """ if self.condition is not None: self.condition = _as_logical(self.condition) def _to_agentspeak(self) -> str: + """ + Converts this rule to its AgentSpeak string representation. + + If no condition is specified, the rule is represented as a simple fact. + If a condition is specified, it's represented as an implication (result :- condition). + + :return: The AgentSpeak string representation of this rule. + """ if not self.condition: return f"{self.result}." return f"{self.result} :- {self.condition}." class TriggerType(StrEnum): + """ + Enumeration of trigger types for AgentSpeak plans. + + Trigger types define what kind of events can activate an AgentSpeak plan. + Currently, the system supports triggers for added beliefs and added goals. + """ + ADDED_BELIEF = "+" + """Trigger when a belief is added to the belief base.""" + # REMOVED_BELIEF = "-" # TODO # MODIFIED_BELIEF = "^" # TODO + ADDED_GOAL = "+!" + """Trigger when a goal is added to be achieved.""" + # REMOVED_GOAL = "-!" # TODO @@ -257,6 +490,14 @@ class TriggerType(StrEnum): class AstPlan(AstNode): """ Represents a plan in AgentSpeak, consisting of a trigger, context, and body. + + Plans define the reactive behavior of agents in AgentSpeak. They specify what + actions to take when certain conditions are met (trigger and context). + + :ivar type: The type of trigger that activates this plan. + :ivar trigger_literal: The specific event or condition that triggers this plan. + :ivar context: A list of conditions that must hold for this plan to be applicable. + :ivar body: A list of statements to execute when this plan is triggered. """ type: TriggerType @@ -265,6 +506,16 @@ class AstPlan(AstNode): body: list[AstStatement] def _to_agentspeak(self) -> str: + """ + Converts this plan to its AgentSpeak string representation. + + The representation follows the standard AgentSpeak plan format: + trigger_type + trigger_literal + : context_conditions + <- body_statements. + + :return: The AgentSpeak string representation of this plan. + """ assert isinstance(self.trigger_literal, AstLiteral) indent = " " * 6 @@ -290,12 +541,26 @@ class AstPlan(AstNode): class AstProgram(AstNode): """ Represents a full AgentSpeak program, consisting of rules and plans. + + This is the root node of the AgentSpeak AST, containing all the rules + and plans that define the agent's behavior. + + :ivar rules: A list of inference rules in this program. + :ivar plans: A list of reactive plans in this program. """ rules: list[AstRule] = field(default_factory=list) plans: list[AstPlan] = field(default_factory=list) def _to_agentspeak(self) -> str: + """ + Converts this program to its AgentSpeak string representation. + + The representation consists of all rules followed by all plans, + separated by blank lines for readability. + + :return: The complete AgentSpeak source code for this program. + """ lines = [] lines.extend(map(str, self.rules)) diff --git a/src/control_backend/agents/bdi/agentspeak_generator.py b/src/control_backend/agents/bdi/agentspeak_generator.py index 2fe12e3..fba603a 100644 --- a/src/control_backend/agents/bdi/agentspeak_generator.py +++ b/src/control_backend/agents/bdi/agentspeak_generator.py @@ -46,6 +46,15 @@ class AgentSpeakGenerator: It handles the conversion of phases, norms, goals, and triggers into AgentSpeak rules and plans, ensuring the robot follows the defined behavioral logic. + + The generator follows a systematic approach: + 1. Sets up initial phase and cycle notification rules + 2. Adds keyword inference capabilities for natural language processing + 3. Creates default plans for common operations + 4. Processes each phase with its norms, goals, and triggers + 5. Adds fallback plans for robust execution + + :ivar _asp: The internal AgentSpeak program representation being built. """ _asp: AstProgram @@ -54,6 +63,10 @@ class AgentSpeakGenerator: """ Translates a Program object into an AgentSpeak source string. + This is the main entry point for the code generation process. It initializes + the AgentSpeak program structure and orchestrates the conversion of all + program elements into their AgentSpeak representations. + :param program: The behavior program to translate. :return: The generated AgentSpeak code as a string. """ @@ -76,6 +89,18 @@ class AgentSpeakGenerator: return str(self._asp) def _add_keyword_inference(self) -> None: + """ + Adds inference rules for keyword detection in user messages. + + This method creates rules that allow the system to detect when specific + keywords are mentioned in user messages. It uses string operations to + check if a keyword is a substring of the user's message. + + The generated rule has the form: + keyword_said(Keyword) :- user_said(Message) & .substring(Keyword, Message, Pos) & Pos >= 0 + + This enables the system to trigger behaviors based on keyword detection. + """ keyword = AstVar("Keyword") message = AstVar("Message") position = AstVar("Pos") @@ -90,12 +115,32 @@ class AgentSpeakGenerator: ) def _add_default_plans(self): + """ + Adds default plans for common operations. + + This method sets up the standard plans that handle fundamental operations + like replying with goals, simple speech actions, general replies, and + cycle notifications. These plans provide the basic infrastructure for + the agent's reactive behavior. + """ self._add_reply_with_goal_plan() self._add_say_plan() self._add_reply_plan() self._add_notify_cycle_plan() def _add_reply_with_goal_plan(self): + """ + Adds a plan for replying with a specific conversational goal. + + This plan handles the case where the agent needs to respond to user input + while pursuing a specific conversational goal. It: + 1. Marks that the agent has responded this turn + 2. Gathers all active norms + 3. Generates a reply that considers both the user message and the goal + + Trigger: +!reply_with_goal(Goal) + Context: user_said(Message) + """ self._asp.plans.append( AstPlan( TriggerType.ADDED_GOAL, @@ -121,6 +166,17 @@ class AgentSpeakGenerator: ) def _add_say_plan(self): + """ + Adds a plan for simple speech actions. + + This plan handles direct speech actions where the agent needs to say + a specific text. It: + 1. Marks that the agent has responded this turn + 2. Executes the speech action + + Trigger: +!say(Text) + Context: None (can be executed anytime) + """ self._asp.plans.append( AstPlan( TriggerType.ADDED_GOAL, @@ -134,6 +190,18 @@ class AgentSpeakGenerator: ) def _add_reply_plan(self): + """ + Adds a plan for general reply actions. + + This plan handles general reply actions where the agent needs to respond + to user input without a specific conversational goal. It: + 1. Marks that the agent has responded this turn + 2. Gathers all active norms + 3. Generates a reply based on the user message and norms + + Trigger: +!reply + Context: user_said(Message) + """ self._asp.plans.append( AstPlan( TriggerType.ADDED_GOAL, @@ -157,6 +225,19 @@ class AgentSpeakGenerator: ) def _add_notify_cycle_plan(self): + """ + Adds a plan for cycle notification. + + This plan handles the periodic notification cycle that allows the system + to monitor and report on the current state. It: + 1. Gathers all active norms + 2. Notifies the system about the current norms + 3. Waits briefly to allow processing + 4. Recursively triggers the next cycle + + Trigger: +!notify_cycle + Context: None (can be executed anytime) + """ self._asp.plans.append( AstPlan( TriggerType.ADDED_GOAL, @@ -180,6 +261,16 @@ class AgentSpeakGenerator: ) def _process_phases(self, phases: list[Phase]) -> None: + """ + Processes all phases in the program and their transitions. + + This method iterates through each phase and: + 1. Processes the current phase (norms, goals, triggers) + 2. Sets up transitions between phases + 3. Adds special handling for the end phase + + :param phases: The list of phases to process. + """ for curr_phase, next_phase in zip([None] + phases, phases + [None], strict=True): if curr_phase: self._process_phase(curr_phase) @@ -202,6 +293,17 @@ class AgentSpeakGenerator: ) def _process_phase(self, phase: Phase) -> None: + """ + Processes a single phase, including its norms, goals, and triggers. + + This method handles the complete processing of a phase by: + 1. Processing all norms in the phase + 2. Setting up the default execution loop for the phase + 3. Processing all goals in sequence + 4. Processing all triggers for reactive behavior + + :param phase: The phase to process. + """ for norm in phase.norms: self._process_norm(norm, phase) @@ -216,6 +318,21 @@ class AgentSpeakGenerator: self._process_trigger(trigger, phase) def _add_phase_transition(self, from_phase: Phase | None, to_phase: Phase | None) -> None: + """ + Adds plans for transitioning between phases. + + This method creates two plans for each phase transition: + 1. A check plan that verifies if transition conditions are met + 2. A force plan that actually performs the transition (can be forced externally) + + The transition involves: + - Notifying the system about the phase change + - Removing the current phase belief + - Adding the next phase belief + + :param from_phase: The phase being transitioned from (or None for initial setup). + :param to_phase: The phase being transitioned to (or None for end phase). + """ if from_phase is None: return from_phase_ast = self._astify(from_phase) @@ -245,18 +362,6 @@ class AgentSpeakGenerator: 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")]) - # ), - # ] - # ) - # Check self._asp.plans.append( AstPlan( @@ -277,6 +382,17 @@ class AgentSpeakGenerator: ) def _process_norm(self, norm: Norm, phase: Phase) -> None: + """ + Processes a norm and adds it as an inference rule. + + This method converts norms into AgentSpeak rules that define when + the norm should be active. It handles both basic norms (always active + in their phase) and conditional norms (active only when their condition + is met). + + :param norm: The norm to process. + :param phase: The phase this norm belongs to. + """ rule: AstRule | None = None match norm: @@ -295,6 +411,18 @@ class AgentSpeakGenerator: self._asp.rules.append(rule) def _add_default_loop(self, phase: Phase) -> None: + """ + Adds the default execution loop for a phase. + + This method creates the main reactive loop that runs when the agent + receives user input during a phase. The loop: + 1. Notifies the system about the user input + 2. Resets the response tracking + 3. Executes all phase goals + 4. Attempts phase transition + + :param phase: The phase to create the loop for. + """ actions = [] actions.append( @@ -303,7 +431,6 @@ class AgentSpeakGenerator: ) ) 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))) @@ -327,6 +454,22 @@ class AgentSpeakGenerator: continues_response: bool = False, main_goal: bool = False, ) -> None: + """ + Processes a goal and creates plans for achieving it. + + This method creates two plans for each goal: + 1. A main plan that executes the goal's steps when conditions are met + 2. A fallback plan that provides a default empty implementation (prevents crashes) + + The method also recursively processes any subgoals contained within + the goal's plan. + + :param goal: The goal to process. + :param phase: The phase this goal belongs to. + :param previous_goal: The previous goal in sequence (for dependency tracking). + :param continues_response: Whether this goal continues an existing response. + :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 previous_goal and previous_goal.can_fail: @@ -369,14 +512,38 @@ class AgentSpeakGenerator: prev_goal = subgoal def _step_to_statement(self, step: PlanElement) -> AstStatement: + """ + Converts a plan step to an AgentSpeak statement. + + This method transforms different types of plan elements into their + corresponding AgentSpeak statements. Goals and speech-related actions + become achieve-goal statements, while gesture actions become do-action + statements. + + :param step: The plan element to convert. + :return: The corresponding AgentSpeak statement. + """ 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: + """ + Processes a trigger and creates plans for its execution. + + This method creates plans that execute when trigger conditions are met. + It handles both automatic triggering (when conditions are detected) and + manual forcing (from UI). The trigger execution includes: + 1. Notifying the system about trigger start + 2. Executing all trigger steps + 3. Waiting briefly for UI display + 4. Notifying the system about trigger end + + :param trigger: The trigger to process. + :param phase: The phase this trigger belongs to. + """ body = [] subgoals = [] @@ -418,6 +585,18 @@ class AgentSpeakGenerator: self._process_goal(subgoal, phase, continues_response=True) def _add_fallbacks(self): + """ + Adds fallback plans for robust execution, preventing crashes. + + This method creates fallback plans that provide default empty implementations + for key goals. These fallbacks ensure that the system can continue execution + even when no specific plans are applicable, preventing crashes. + + The fallbacks are created for: + - check_triggers: When no triggers are applicable + - transition_phase: When phase transition conditions aren't met + - force_transition_phase: When forced transitions aren't possible + """ # Trigger fallback self._asp.plans.append( AstPlan( @@ -450,18 +629,57 @@ class AgentSpeakGenerator: @singledispatchmethod def _astify(self, element: ProgramElement) -> AstExpression: + """ + Converts program elements to AgentSpeak expressions (base method). + + This is the base method for the singledispatch mechanism that handles + conversion of different program element types to their AgentSpeak + representations. Specific implementations are provided for each + element type through registered methods. + + :param element: The program element to convert. + :return: The corresponding AgentSpeak expression. + :raises NotImplementedError: If no specific implementation exists for the element type. + """ raise NotImplementedError(f"Cannot convert element {element} to an AgentSpeak expression.") @_astify.register def _(self, kwb: KeywordBelief) -> AstExpression: + """ + Converts a KeywordBelief to an AgentSpeak expression. + + Keyword beliefs are converted to keyword_said literals that check + if the keyword was mentioned in user input. + + :param kwb: The KeywordBelief to convert. + :return: An AstLiteral representing the keyword detection. + """ return AstLiteral("keyword_said", [AstString(kwb.keyword)]) @_astify.register def _(self, sb: SemanticBelief) -> AstExpression: + """ + Converts a SemanticBelief to an AgentSpeak expression. + + Semantic beliefs are converted to literals using their slugified names, + which are used for LLM-based belief evaluation. + + :param sb: The SemanticBelief to convert. + :return: An AstLiteral representing the semantic belief. + """ return AstLiteral(self.slugify(sb)) @_astify.register def _(self, ib: InferredBelief) -> AstExpression: + """ + Converts an InferredBelief to an AgentSpeak expression. + + Inferred beliefs are converted to binary operations that combine + their left and right operands using the appropriate logical operator. + + :param ib: The InferredBelief to convert. + :return: An AstBinaryOp representing the logical combination. + """ return AstBinaryOp( self._astify(ib.left), BinaryOperatorType.AND if ib.operator == LogicalOperator.AND else BinaryOperatorType.OR, @@ -470,59 +688,187 @@ class AgentSpeakGenerator: @_astify.register def _(self, norm: Norm) -> AstExpression: + """ + Converts a Norm to an AgentSpeak expression. + + Norms are converted to literals with either 'norm' or 'critical_norm' + functors depending on their critical flag, with the norm text as an argument. + + Note that currently, critical norms are not yet functionally supported. They are possible + to astify for future use. + + :param norm: The Norm to convert. + :return: An AstLiteral representing the norm. + """ functor = "critical_norm" if norm.critical else "norm" return AstLiteral(functor, [AstString(norm.norm)]) @_astify.register def _(self, phase: Phase) -> AstExpression: + """ + Converts a Phase to an AgentSpeak expression. + + Phases are converted to phase literals with their unique identifier + as an argument, which is used for phase tracking and transitions. + + :param phase: The Phase to convert. + :return: An AstLiteral representing the phase. + """ return AstLiteral("phase", [AstString(str(phase.id))]) @_astify.register def _(self, goal: Goal, achieved: bool = False) -> AstExpression: + """ + Converts a Goal to an AgentSpeak expression. + + Goals are converted to literals using their slugified names. If the + achieved parameter is True, the literal is prefixed with 'achieved_'. + + :param goal: The Goal to convert. + :param achieved: Whether to represent this as an achieved goal. + :return: An AstLiteral representing the goal. + """ return AstLiteral(f"{'achieved_' if achieved else ''}{self._slugify_str(goal.name)}") @_astify.register def _(self, trigger: Trigger) -> AstExpression: + """ + Converts a Trigger to an AgentSpeak expression. + + Triggers are converted to literals using their slugified names, + which are used to identify and execute trigger plans. + + :param trigger: The Trigger to convert. + :return: An AstLiteral representing the trigger. + """ return AstLiteral(self.slugify(trigger)) @_astify.register def _(self, sa: SpeechAction) -> AstExpression: + """ + Converts a SpeechAction to an AgentSpeak expression. + + Speech actions are converted to say literals with the text content + as an argument, which are used for direct speech output. + + :param sa: The SpeechAction to convert. + :return: An AstLiteral representing the speech action. + """ return AstLiteral("say", [AstString(sa.text)]) @_astify.register def _(self, ga: GestureAction) -> AstExpression: + """ + Converts a GestureAction to an AgentSpeak expression. + + Gesture actions are converted to gesture literals with the gesture + type and name as arguments, which are used for physical robot gestures. + + :param ga: The GestureAction to convert. + :return: An AstLiteral representing the gesture action. + """ gesture = ga.gesture return AstLiteral("gesture", [AstString(gesture.type), AstString(gesture.name)]) @_astify.register def _(self, la: LLMAction) -> AstExpression: + """ + Converts an LLMAction to an AgentSpeak expression. + + LLM actions are converted to reply_with_goal literals with the + conversational goal as an argument, which are used for LLM-generated + responses guided by specific goals. + + :param la: The LLMAction to convert. + :return: An AstLiteral representing the LLM action. + """ return AstLiteral("reply_with_goal", [AstString(la.goal)]) @singledispatchmethod @staticmethod def slugify(element: ProgramElement) -> str: + """ + Converts program elements to slugs (base method). + + This is the base method for the singledispatch mechanism that handles + conversion of different program element types to their slug representations. + Specific implementations are provided for each element type through + registered methods. + + Slugs are used outside of AgentSpeak, mostly for identifying what to send to the AgentSpeak + program as beliefs. + + :param element: The program element to convert to a slug. + :return: The slug string representation. + :raises NotImplementedError: If no specific implementation exists for the element type. + """ raise NotImplementedError(f"Cannot convert element {element} to a slug.") @slugify.register @staticmethod def _(n: Norm) -> str: + """ + Converts a Norm to a slug. + + Norms are converted to slugs with the 'norm_' prefix followed by + the slugified norm text. + + :param n: The Norm to convert. + :return: The slug string representation. + """ return f"norm_{AgentSpeakGenerator._slugify_str(n.norm)}" @slugify.register @staticmethod def _(sb: SemanticBelief) -> str: + """ + Converts a SemanticBelief to a slug. + + Semantic beliefs are converted to slugs with the 'semantic_' prefix + followed by the slugified belief name. + + :param sb: The SemanticBelief to convert. + :return: The slug string representation. + """ return f"semantic_{AgentSpeakGenerator._slugify_str(sb.name)}" @slugify.register @staticmethod def _(g: BaseGoal) -> str: + """ + Converts a BaseGoal to a slug. + + Goals are converted to slugs using their slugified names directly. + + :param g: The BaseGoal to convert. + :return: The slug string representation. + """ return AgentSpeakGenerator._slugify_str(g.name) @slugify.register @staticmethod - def _(t: Trigger): + def _(t: Trigger) -> str: + """ + Converts a Trigger to a slug. + + Triggers are converted to slugs with the 'trigger_' prefix followed by + the slugified trigger name. + + :param t: The Trigger to convert. + :return: The slug string representation. + """ return f"trigger_{AgentSpeakGenerator._slugify_str(t.name)}" @staticmethod def _slugify_str(text: str) -> str: + """ + Converts a text string to a slug. + + This helper method converts arbitrary text to a URL-friendly slug format + by converting to lowercase, removing special characters, and replacing + spaces with underscores. It also removes common stopwords. + + :param text: The text string to convert. + :return: The slugified string. + """ return slugify(text, separator="_", stopwords=["a", "an", "the", "we", "you", "I"]) diff --git a/src/control_backend/schemas/program.py b/src/control_backend/schemas/program.py index 283e17d..3fb0a19 100644 --- a/src/control_backend/schemas/program.py +++ b/src/control_backend/schemas/program.py @@ -22,6 +22,13 @@ class ProgramElement(BaseModel): class LogicalOperator(Enum): """ Logical operators for combining beliefs. + + These operators define how beliefs can be combined to form more complex + logical conditions. They are used in inferred beliefs to create compound + beliefs from simpler ones. + + AND: Both operands must be true for the result to be true. + OR: At least one operand must be true for the result to be true. """ AND = "AND" @@ -36,7 +43,15 @@ class KeywordBelief(ProgramElement): """ Represents a belief that is activated when a specific keyword is detected in the user's speech. + Keyword beliefs provide a simple but effective way to detect specific topics + or intentions in user speech. They are triggered when the exact keyword + string appears in the transcribed user input. + :ivar keyword: The string to look for in the transcription. + + Example: + A keyword belief with keyword="robot" would be activated when the user + says "I like the robot" or "Tell me about robots". """ name: str = "" @@ -48,8 +63,18 @@ class SemanticBelief(ProgramElement): Represents a belief whose truth value is determined by an LLM analyzing the conversation context. + Semantic beliefs provide more sophisticated belief detection by using + an LLM to analyze the conversation context and determine + if the belief should be considered true. This allows for more nuanced + and context-aware belief evaluation. + :ivar description: A natural language description of what this belief represents, used as a prompt for the LLM. + + Example: + A semantic belief with description="The user is expressing frustration" + would be activated when the LLM determines that the user's statements + indicate frustration, even if no specific keywords are used. """ description: str @@ -59,6 +84,11 @@ class InferredBelief(ProgramElement): """ Represents a belief derived from other beliefs using logical operators. + Inferred beliefs allow for the creation of complex belief structures by + combining simpler beliefs using logical operators. This enables the + representation of sophisticated conditions and relationships between + different aspects of the conversation or context. + :ivar operator: The :class:`LogicalOperator` (AND/OR) to apply. :ivar left: The left operand (another belief). :ivar right: The right operand (another belief). @@ -74,8 +104,16 @@ class Norm(ProgramElement): """ Base class for behavioral norms that guide the robot's interactions. + Norms represent guidelines, principles, or rules that should govern the + robot's behavior during interactions. They can be either basic (always + active in their phase) or conditional (active only when specific beliefs + are true). + :ivar norm: The textual description of the norm. :ivar critical: Whether this norm is considered critical and should be strictly enforced. + + Critical norms are currently not supported yet, but are intended for norms that should + ABSOLUTELY NOT be violated, possible cheched by additional validator agents. """ name: str = "" @@ -86,6 +124,13 @@ class Norm(ProgramElement): class BasicNorm(Norm): """ A simple behavioral norm that is always considered for activation when its phase is active. + + Basic norms are the most straightforward type of norms. They are active + throughout their assigned phase and provide consistent behavioral guidance + without any additional conditions. + + These norms are suitable for general principles that should always apply + during a particular interaction phase. """ pass @@ -95,7 +140,20 @@ class ConditionalNorm(Norm): """ A behavioral norm that is only active when a specific condition (belief) is met. + Conditional norms provide context-sensitive behavioral guidance. They are + only active and considered for activation when their associated condition + (belief) is true. This allows for more nuanced and adaptive behavior that + responds to the specific context of the interaction. + + An important note, is that the current implementation of these norms for keyword-based beliefs + is that they only hold for 1 turn, as keyword-based conditions often express temporary + conditions. + :ivar condition: The :class:`Belief` that must hold for this norm to be active. + + Example: + A conditional norm with the condition "user is frustrated" might specify + that the robot should use more empathetic language and avoid complex topics. """ condition: Belief @@ -107,7 +165,12 @@ type PlanElement = Goal | Action class Plan(ProgramElement): """ Represents a list of steps to execute. Each of these steps can be a goal (with its own plan) - or a simple action. + or a simple action. + + Plans define sequences of actions and subgoals that the robot should execute + to achieve a particular objective. They form the procedural knowledge of + the behavior program, specifying what the robot should do in different + situations. :ivar steps: The actions or subgoals to execute, in order. """ @@ -123,6 +186,10 @@ class BaseGoal(ProgramElement): :ivar description: A description of the goal, used to determine if it has been achieved. :ivar can_fail: Whether we can fail to achieve the goal after executing the plan. + + The can_fail attribute determines whether goal achievement is binary + (success/failure) or whether it can be determined through conversation + analysis. """ description: str = "" @@ -132,9 +199,13 @@ class BaseGoal(ProgramElement): 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 + plan. It inherits from the BaseGoal a variable `can_fail`, which, if true, will cause the completion to be determined based on the conversation. + Goals extend base goals by including a specific plan to achieve the objective. + They form the core of the robot's proactive behavior, defining both what + should be accomplished and how to accomplish it. + Instances of this goal are not hashable because a plan is not hashable. :ivar plan: The plan to execute. @@ -163,6 +234,10 @@ class Gesture(BaseModel): :ivar type: Whether to use a specific "single" gesture or a random one from a "tag" category. :ivar name: The identifier for the gesture or tag. + + The type field determines how the gesture is selected: + - "single": Use the specific gesture identified by name + - "tag": Select a random gesture from the category identified by name """ type: Literal["tag", "single"] @@ -185,6 +260,10 @@ class LLMAction(ProgramElement): An action that triggers an LLM-generated conversational response. :ivar goal: A temporary conversational goal to guide the LLM's response generation. + + The goal parameter provides high-level guidance to the LLM about what + the response should aim to achieve, while allowing the LLM flexibility + in how to express it. """ name: str = "" @@ -222,6 +301,10 @@ class Program(BaseModel): """ The top-level container for a complete robot behavior definition. + The Program class represents the complete specification of a robot's + behavioral logic. It contains all the phases, norms, goals, triggers, + and actions that define how the robot should behave during interactions. + :ivar phases: An ordered list of :class:`Phase` objects defining the interaction flow. """