refactor: program restructure

Also includes some AgentSpeak generation.

ref: N25B-376
This commit is contained in:
2025-12-16 10:21:50 +01:00
parent de2e56ffce
commit d043c54336
4 changed files with 532 additions and 69 deletions

View File

@@ -1,12 +1,311 @@
import zmq
from pydantic import ValidationError
from slugify import slugify
from zmq.asyncio import Context
from control_backend.agents import BaseAgent
from control_backend.core.agent_system import InternalMessage
from control_backend.core.config import settings
from control_backend.schemas.belief_message import Belief, BeliefMessage
from control_backend.schemas.program import Program
from control_backend.schemas.program import (
Action,
BasicBelief,
BasicNorm,
Belief,
ConditionalNorm,
GestureAction,
Goal,
InferredBelief,
KeywordBelief,
LLMAction,
LogicalOperator,
Phase,
Plan,
Program,
ProgramElement,
SemanticBelief,
SpeechAction,
)
test_program = Program(
phases=[
Phase(
norms=[
BasicNorm(norm="Talk like a pirate"),
ConditionalNorm(
condition=InferredBelief(
left=KeywordBelief(keyword="Arr"),
right=SemanticBelief(description="testing", name="semantic belief"),
operator=LogicalOperator.OR,
name="Talking to a pirate",
),
norm="Use nautical terms",
),
ConditionalNorm(
condition=SemanticBelief(
description="We are talking to a child", name="talking to child"
),
norm="Do not use cuss words",
),
],
triggers=[
# Trigger(
# condition=InferredBelief(
# left=KeywordBelief(keyword="key"),
# right=InferredBelief(
# left=KeywordBelief(keyword="key2"),
# right=SemanticBelief(
# description="Decode this", name="semantic belief 2"
# ),
# operator=LogicalOperator.OR,
# name="test trigger inferred inner",
# ),
# operator=LogicalOperator.OR,
# name="test trigger inferred outer",
# ),
# plan=Plan(steps=[]),
# )
],
goals=[
Goal(
name="Determine user age",
plan=Plan(steps=[LLMAction(goal="Determine the age of the user.")]),
),
Goal(
name="Find the user's name",
plan=Plan(
steps=[
Goal(
name="Greet the user",
plan=Plan(steps=[LLMAction(goal="Greet the user.")]),
can_fail=False,
),
Goal(
name="Ask for name",
plan=Plan(steps=[LLMAction(goal="Obtain the user's name.")]),
),
]
),
),
Goal(name="Tell a joke", plan=Plan(steps=[LLMAction(goal="Tell a joke.")])),
],
id=1,
)
]
)
class AgentSpeakGenerator:
"""
Converts Pydantic representation of behavior programs into AgentSpeak(L) code string.
"""
def generate(self, program: Program) -> str:
lines = []
lines.append("")
lines += self._generate_initial_beliefs(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) -> list[str]:
lines = []
lines.append("// --- Initial beliefs ---")
lines.append(f"phase({program.phases[0].id}).")
lines += ["", ""]
return lines
def _generate_norms(self, program: Program) -> list[str]:
lines = []
lines.append("// --- Norms ---")
for phase in program.phases:
for norm in phase.norms:
if type(norm) is BasicNorm:
lines.append(f"{self._slugify(norm)} :- phase({phase.id}).")
if type(norm) is ConditionalNorm:
lines.append(
f"{self._slugify(norm)} :- phase({phase.id}) & "
f"{self._slugify(norm.condition)}."
)
lines += ["", ""]
return lines
def _generate_belief_inference(self, program: Program) -> list[str]:
lines = []
lines.append("// --- Belief inference rules ---")
for phase in program.phases:
for norm in phase.norms:
if not isinstance(norm, ConditionalNorm):
continue
lines += self._belief_inference_recursive(norm.condition)
for trigger in phase.triggers:
lines += self._belief_inference_recursive(trigger.condition)
lines += ["", ""]
return lines
def _belief_inference_recursive(self, belief: Belief) -> list[str]:
lines = []
if type(belief) is KeywordBelief:
lines.append(
f"{self._slugify(belief)} :- user_said(Message) & "
f'.substring(Message, "{belief.keyword}", Pos) & Pos >= 0.'
)
if type(belief) is InferredBelief:
lines.append(
f"{self._slugify(belief)} :- {self._slugify(belief.left)} "
f"{'&' if belief.operator == LogicalOperator.AND else '|'} "
f"{self._slugify(belief.right)}."
)
lines += self._belief_inference_recursive(belief.left)
lines += self._belief_inference_recursive(belief.right)
return lines
def _generate_goals(self, program: Program) -> list[str]:
lines = []
lines.append("// --- Goals ---")
for phase in program.phases:
previous_goal: Goal | None = None
for goal in phase.goals:
lines += self._generate_plan_recursive(goal, phase, previous_goal)
previous_goal = goal
lines += ["", ""]
return lines
def _generate_plan_recursive(
self, goal: Goal, phase: Phase, previous_goal: Goal | None = None
) -> list[str]:
lines = []
lines.append(f"+{self._slugify(goal, include_prefix=True)}")
# Context
lines.append(f"{' ' * 2}:{' ' * 3}phase({phase.id}) &")
lines.append(f"{' ' * 6}not responded_this_turn &")
lines.append(f"{' ' * 6}not achieved_{self._slugify(goal)} &")
if previous_goal:
lines.append(f"{' ' * 6}achieved_{self._slugify(previous_goal)}")
else:
lines.append(f"{' ' * 6}true")
extra_goals_to_generate = []
steps = goal.plan.steps
first_step = steps[0]
lines.append(
f"{' ' * 2}<-{' ' * 2}{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]:
lines.append(f"{' ' * 6}{self._slugify(step, include_prefix=True)};")
if isinstance(step, Goal):
extra_goals_to_generate.append(step)
if len(steps) > 1:
last_step = steps[-1]
lines.append(
f"{' ' * 6}{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:
lines.append(f"{' ' * 6}+achieved_{self._slugify(goal)}.")
lines.append("")
extra_previous_goal: Goal | None = None
for extra_goal in extra_goals_to_generate:
lines += self._generate_plan_recursive(extra_goal, phase, extra_previous_goal)
extra_previous_goal = extra_goal
return lines
def _generate_triggers(self, program: Program) -> list[str]:
lines = []
lines.append("// --- Triggers ---")
lines += ["", ""]
return lines
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):
@@ -25,40 +324,40 @@ class BDIProgramManager(BaseAgent):
super().__init__(**kwargs)
self.sub_socket = None
async def _send_to_bdi(self, program: Program):
"""
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:
- **Norms**: Constraints or rules the agent must follow.
- **Goals**: Objectives the agent must achieve.
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.
:param program: The program object received from the API.
"""
first_phase = program.phases[0]
norms_belief = Belief(
name="norms",
arguments=[norm.norm for norm in first_phase.norms],
replace=True,
)
goals_belief = Belief(
name="goals",
arguments=[goal.description for goal in first_phase.goals],
replace=True,
)
program_beliefs = BeliefMessage(beliefs=[norms_belief, goals_belief])
message = InternalMessage(
to=settings.agent_settings.bdi_core_name,
sender=self.name,
body=program_beliefs.model_dump_json(),
thread="beliefs",
)
await self.send(message)
self.logger.debug("Sent new norms and goals to the BDI agent.")
# async def _send_to_bdi(self, program: Program):
# """
# 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:
# - **Norms**: Constraints or rules the agent must follow.
# - **Goals**: Objectives the agent must achieve.
#
# 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.
#
# :param program: The program object received from the API.
# """
# first_phase = program.phases[0]
# norms_belief = Belief(
# name="norms",
# arguments=[norm.norm for norm in first_phase.norms],
# replace=True,
# )
# goals_belief = Belief(
# name="goals",
# arguments=[goal.description for goal in first_phase.goals],
# replace=True,
# )
# program_beliefs = BeliefMessage(beliefs=[norms_belief, goals_belief])
#
# message = InternalMessage(
# to=settings.agent_settings.bdi_core_name,
# sender=self.name,
# body=program_beliefs.model_dump_json(),
# thread="beliefs",
# )
# await self.send(message)
# self.logger.debug("Sent new norms and goals to the BDI agent.")
async def _receive_programs(self):
"""