feat: integrate AgentSpeak with semantic belief extraction

ref: N25B-429
This commit is contained in:
Twirre Meulenbelt
2026-01-07 11:44:48 +01:00
parent de8e829d3e
commit cabe35cdbd
10 changed files with 120 additions and 60 deletions

View File

@@ -332,7 +332,13 @@ class AgentSpeakGenerator:
@_astify.register
def _(self, sb: SemanticBelief) -> AstExpression:
return AstLiteral(f"semantic_{self._slugify_str(sb.description)}")
return AstLiteral(self.get_semantic_belief_slug(sb))
@staticmethod
def get_semantic_belief_slug(sb: SemanticBelief) -> str:
# If you need a method like this for other types, make a public slugify singledispatch for
# all types.
return f"semantic_{AgentSpeakGenerator._slugify_str(sb.name)}"
@_astify.register
def _(self, ib: InferredBelief) -> AstExpression:

View File

@@ -1,3 +1,5 @@
import asyncio
import zmq
from pydantic import ValidationError
from zmq.asyncio import Context
@@ -5,8 +7,9 @@ from zmq.asyncio import Context
from control_backend.agents import BaseAgent
from control_backend.agents.bdi.agentspeak_generator import AgentSpeakGenerator
from control_backend.core.config import settings
from control_backend.schemas.belief_list import BeliefList
from control_backend.schemas.internal_message import InternalMessage
from control_backend.schemas.program import Program
from control_backend.schemas.program import Belief, ConditionalNorm, InferredBelief, Program
class BDIProgramManager(BaseAgent):
@@ -56,6 +59,45 @@ class BDIProgramManager(BaseAgent):
await self.send(msg)
@staticmethod
def _extract_beliefs_from_program(program: Program) -> list[Belief]:
beliefs: list[Belief] = []
for phase in program.phases:
for norm in phase.norms:
if isinstance(norm, ConditionalNorm):
beliefs += BDIProgramManager._extract_beliefs_from_belief(norm.condition)
for trigger in phase.triggers:
beliefs += BDIProgramManager._extract_beliefs_from_belief(trigger.condition)
return beliefs
@staticmethod
def _extract_beliefs_from_belief(belief: Belief) -> list[Belief]:
if isinstance(belief, InferredBelief):
return BDIProgramManager._extract_beliefs_from_belief(
belief.left
) + BDIProgramManager._extract_beliefs_from_belief(belief.right)
return [belief]
async def _send_beliefs_to_semantic_belief_extractor(self, program: Program):
"""
Extract beliefs from the program and send them to the Semantic Belief Extractor Agent.
:param program: The program received from the API.
"""
beliefs = BeliefList(beliefs=self._extract_beliefs_from_program(program))
message = InternalMessage(
to=settings.agent_settings.text_belief_extractor_name,
sender=self.name,
body=beliefs.model_dump_json(),
thread="beliefs",
)
await self.send(message)
async def _receive_programs(self):
"""
Continuous loop that receives program updates from the HTTP endpoint.
@@ -72,7 +114,10 @@ class BDIProgramManager(BaseAgent):
self.logger.exception("Received an invalid program.")
continue
await self._create_agentspeak_and_send_to_bdi(program)
await asyncio.gather(
self._create_agentspeak_and_send_to_bdi(program),
self._send_beliefs_to_semantic_belief_extractor(program),
)
async def setup(self):
"""

View File

@@ -3,21 +3,16 @@ import json
import httpx
from pydantic import ValidationError
from slugify import slugify
from control_backend.agents.base import BaseAgent
from control_backend.agents.bdi.agentspeak_generator import AgentSpeakGenerator
from control_backend.core.agent_system import InternalMessage
from control_backend.core.config import settings
from control_backend.schemas.belief_list import BeliefList
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 (
Belief,
ConditionalNorm,
InferredBelief,
Program,
SemanticBelief,
)
from control_backend.schemas.program import SemanticBelief
class TextBeliefExtractorAgent(BaseAgent):
@@ -32,11 +27,12 @@ class TextBeliefExtractorAgent(BaseAgent):
the message itself.
"""
def __init__(self, name: str):
def __init__(self, name: str, temperature: float = settings.llm_settings.code_temperature):
super().__init__(name)
self.beliefs: dict[str, bool] = {}
self.available_beliefs: list[SemanticBelief] = []
self.conversation = ChatHistory(messages=[])
self.temperature = temperature
async def setup(self):
"""
@@ -85,44 +81,18 @@ class TextBeliefExtractorAgent(BaseAgent):
:param msg: The received message from the program manager.
"""
try:
program = Program.model_validate_json(msg.body)
belief_list = BeliefList.model_validate_json(msg.body)
except ValidationError:
self.logger.warning(
"Received message from program manager but it is not a valid program."
"Received message from program manager but it is not a valid list of beliefs."
)
return
self.logger.debug("Received a program from the program manager.")
self.available_beliefs = self._extract_basic_beliefs_from_program(program)
# TODO Copied from an incomplete version of the program manager. Use that one instead.
@staticmethod
def _extract_basic_beliefs_from_program(program: Program) -> list[SemanticBelief]:
beliefs = []
for phase in program.phases:
for norm in phase.norms:
if isinstance(norm, ConditionalNorm):
beliefs += TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
norm.condition
)
for trigger in phase.triggers:
beliefs += TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
trigger.condition
)
return beliefs
# TODO Copied from an incomplete version of the program manager. Use that one instead.
@staticmethod
def _extract_basic_beliefs_from_belief(belief: Belief) -> list[SemanticBelief]:
if isinstance(belief, InferredBelief):
return TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(
belief.left
) + TextBeliefExtractorAgent._extract_basic_beliefs_from_belief(belief.right)
return [belief]
self.available_beliefs = [b for b in belief_list.beliefs if isinstance(b, SemanticBelief)]
self.logger.debug(
"Received %d beliefs from the program manager.",
len(self.available_beliefs),
)
async def _user_said(self, text: str):
"""
@@ -207,8 +177,7 @@ class TextBeliefExtractorAgent(BaseAgent):
@staticmethod
def _create_belief_schema(belief: SemanticBelief) -> tuple[str, dict]:
# TODO: use real belief names
return belief.name or slugify(belief.description), {
return AgentSpeakGenerator.get_semantic_belief_slug(belief), {
"type": ["boolean", "null"],
"description": belief.description,
}
@@ -237,10 +206,9 @@ class TextBeliefExtractorAgent(BaseAgent):
@staticmethod
def _format_beliefs(beliefs: list[SemanticBelief]):
# TODO: use real belief names
return "\n".join(
[
f"- {belief.name or slugify(belief.description)}: {belief.description}"
f"- {AgentSpeakGenerator.get_semantic_belief_slug(belief)}: {belief.description}"
for belief in beliefs
]
)
@@ -267,7 +235,7 @@ Given the above conversation, what beliefs can be inferred?
If there is no relevant information about a belief belief, give null.
In case messages conflict, prefer using the most recent messages for inference.
Choose from the following list of beliefs, formatted as (belief_name, description):
Choose from the following list of beliefs, formatted as `- <belief_name>: <description>`:
{self._format_beliefs(beliefs)}
Respond with a JSON similar to the following, but with the property names as given above:
@@ -304,8 +272,7 @@ Respond with a JSON similar to the following, but with the property names as giv
return None
@staticmethod
async def _query_llm(prompt: str, schema: dict) -> dict:
async def _query_llm(self, prompt: str, schema: dict) -> dict:
"""
Query an LLM with the given prompt and schema, return an instance of a dict conforming to
that schema.
@@ -333,7 +300,7 @@ Respond with a JSON similar to the following, but with the property names as giv
},
},
"reasoning_effort": "low",
"temperature": settings.llm_settings.code_temperature,
"temperature": self.temperature,
"stream": False,
},
timeout=None,
@@ -342,4 +309,5 @@ Respond with a JSON similar to the following, but with the property names as giv
response_json = response.json()
json_message = response_json["choices"][0]["message"]["content"]
return json.loads(json_message)
beliefs = json.loads(json_message)
return beliefs