The Big One #43

Merged
k.marinus merged 93 commits from feat/reset-experiment-and-phase into dev 2026-01-26 19:20:45 +00:00
6 changed files with 110 additions and 81 deletions
Showing only changes of commit 57b1276cb5 - Show all commits

View File

@@ -89,9 +89,9 @@ class BDICoreAgent(BaseAgent):
the agent has deferred intentions (deadlines). the agent has deferred intentions (deadlines).
""" """
while self._running: while self._running:
# await ( await (
# self._wake_bdi_loop.wait() self._wake_bdi_loop.wait()
# ) # gets set whenever there's an update to the belief base ) # gets set whenever there's an update to the belief base
# Agent knows when it's expected to have to do its next thing # Agent knows when it's expected to have to do its next thing
maybe_more_work = True maybe_more_work = True

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
import json
import sys import sys
import uuid
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest import pytest
@@ -8,31 +8,45 @@ import pytest
from control_backend.agents.bdi.bdi_program_manager import BDIProgramManager from control_backend.agents.bdi.bdi_program_manager import BDIProgramManager
from control_backend.core.agent_system import InternalMessage from control_backend.core.agent_system import InternalMessage
from control_backend.schemas.belief_message import BeliefMessage from control_backend.schemas.belief_message import BeliefMessage
from control_backend.schemas.program import Program from control_backend.schemas.program import BasicNorm, Goal, Phase, Plan, Program
# Fix Windows Proactor loop for zmq # Fix Windows Proactor loop for zmq
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
def make_valid_program_json(norm="N1", goal="G1"): def make_valid_program_json(norm="N1", goal="G1") -> str:
return json.dumps( return Program(
{ phases=[
"phases": [ Phase(
{ id=uuid.uuid4(),
"id": "phase1", name="Basic Phase",
"label": "Phase 1", norms=[
"triggers": [], BasicNorm(
"norms": [{"id": "n1", "label": "Norm 1", "norm": norm}], id=uuid.uuid4(),
"goals": [ name=norm,
{"id": "g1", "label": "Goal 1", "description": goal, "achieved": False} norm=norm,
], ),
} ],
] goals=[
} Goal(
) id=uuid.uuid4(),
name=goal,
plan=Plan(
id=uuid.uuid4(),
name="Goal Plan",
steps=[],
),
can_fail=False,
),
],
triggers=[],
),
],
).model_dump_json()
@pytest.mark.skip(reason="Functionality being rebuilt.")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_to_bdi(): async def test_send_to_bdi():
manager = BDIProgramManager(name="program_manager_test") manager = BDIProgramManager(name="program_manager_test")
@@ -73,5 +87,5 @@ async def test_receive_programs_valid_and_invalid():
# Only valid Program should have triggered _send_to_bdi # Only valid Program should have triggered _send_to_bdi
assert manager._send_to_bdi.await_count == 1 assert manager._send_to_bdi.await_count == 1
forwarded: Program = manager._send_to_bdi.await_args[0][0] forwarded: Program = manager._send_to_bdi.await_args[0][0]
assert forwarded.phases[0].norms[0].norm == "N1" assert forwarded.phases[0].norms[0].name == "N1"
assert forwarded.phases[0].goals[0].description == "G1" assert forwarded.phases[0].goals[0].name == "G1"

View File

@@ -45,10 +45,10 @@ async def test_handle_message_from_transcriber(agent, mock_settings):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_process_transcription_demo(agent, mock_settings): async def test_process_user_said(agent, mock_settings):
transcription = "this is a test" transcription = "this is a test"
await agent._process_transcription_demo(transcription) await agent._user_said(transcription)
agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it. agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it.
sent: InternalMessage = agent.send.call_args.args[0] # noqa sent: InternalMessage = agent.send.call_args.args[0] # noqa
@@ -56,10 +56,3 @@ async def test_process_transcription_demo(agent, mock_settings):
assert sent.thread == "beliefs" assert sent.thread == "beliefs"
parsed = json.loads(sent.body) parsed = json.loads(sent.body)
assert parsed["beliefs"]["user_said"] == [transcription] assert parsed["beliefs"]["user_said"] == [transcription]
@pytest.mark.asyncio
async def test_setup_initializes_beliefs(agent):
"""Covers the setup method and ensures beliefs are initialized."""
await agent.setup()
assert agent.beliefs == {"mood": ["X"], "car": ["Y"]}

View File

@@ -66,7 +66,7 @@ async def test_llm_processing_success(mock_httpx_client, mock_settings):
# "Hello world." constitutes one sentence/chunk based on punctuation split # "Hello world." constitutes one sentence/chunk based on punctuation split
# The agent should call send once with the full sentence # The agent should call send once with the full sentence
assert agent.send.called assert agent.send.called
args = agent.send.call_args[0][0] args = agent.send.call_args_list[0][0][0]
assert args.to == mock_settings.agent_settings.bdi_core_name assert args.to == mock_settings.agent_settings.bdi_core_name
assert "Hello world." in args.body assert "Hello world." in args.body

View File

@@ -1,4 +1,5 @@
import json import json
import uuid
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest import pytest
@@ -6,7 +7,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from control_backend.api.v1.endpoints import program from control_backend.api.v1.endpoints import program
from control_backend.schemas.program import Program from control_backend.schemas.program import BasicNorm, Goal, Phase, Plan, Program
@pytest.fixture @pytest.fixture
@@ -25,29 +26,37 @@ def client(app):
def make_valid_program_dict(): def make_valid_program_dict():
"""Helper to create a valid Program JSON structure.""" """Helper to create a valid Program JSON structure."""
return { # Converting to JSON using Pydantic because it knows how to convert a UUID object
"phases": [ program_json_str = Program(
{ phases=[
"id": "phase1", Phase(
"label": "basephase", id=uuid.uuid4(),
"norms": [{"id": "n1", "label": "norm", "norm": "be nice"}], name="Basic Phase",
"goals": [ norms=[
{"id": "g1", "label": "goal", "description": "test goal", "achieved": False} BasicNorm(
id=uuid.uuid4(),
name="Some norm",
norm="Do normal.",
),
], ],
"triggers": [ goals=[
{ Goal(
"id": "t1", id=uuid.uuid4(),
"label": "trigger", name="Some goal",
"type": "keywords", plan=Plan(
"keywords": [ id=uuid.uuid4(),
{"id": "kw1", "keyword": "keyword1"}, name="Goal Plan",
{"id": "kw2", "keyword": "keyword2"}, steps=[],
], ),
}, can_fail=False,
),
], ],
} triggers=[],
] ),
} ],
).model_dump_json()
# Converting back to a dict because that's what's expected
return json.loads(program_json_str)
def test_receive_program_success(client): def test_receive_program_success(client):
@@ -71,7 +80,8 @@ def test_receive_program_success(client):
sent_bytes = args[0][1] sent_bytes = args[0][1]
sent_obj = json.loads(sent_bytes.decode()) sent_obj = json.loads(sent_bytes.decode())
expected_obj = Program.model_validate(program_dict).model_dump() # Converting to JSON using Pydantic because it knows how to handle UUIDs
expected_obj = json.loads(Program.model_validate(program_dict).model_dump_json())
assert sent_obj == expected_obj assert sent_obj == expected_obj

View File

@@ -1,49 +1,61 @@
import uuid
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from control_backend.schemas.program import ( from control_backend.schemas.program import (
BasicNorm,
Goal, Goal,
KeywordTrigger, KeywordBelief,
Norm,
Phase, Phase,
Plan,
Program, Program,
TriggerKeyword, Trigger,
) )
def base_norm() -> Norm: def base_norm() -> BasicNorm:
return Norm( return BasicNorm(
id="norm1", id=uuid.uuid4(),
label="testNorm", name="testNormName",
norm="testNormNorm", norm="testNormNorm",
critical=False,
) )
def base_goal() -> Goal: def base_goal() -> Goal:
return Goal( return Goal(
id="goal1", id=uuid.uuid4(),
label="testGoal", name="testGoalName",
description="testGoalDescription", plan=Plan(
achieved=False, id=uuid.uuid4(),
name="testGoalPlanName",
steps=[],
),
can_fail=False,
) )
def base_trigger() -> KeywordTrigger: def base_trigger() -> Trigger:
return KeywordTrigger( return Trigger(
id="trigger1", id=uuid.uuid4(),
label="testTrigger", name="testTriggerName",
type="keywords", condition=KeywordBelief(
keywords=[ id=uuid.uuid4(),
TriggerKeyword(id="keyword1", keyword="testKeyword1"), name="testTriggerKeywordBeliefTriggerName",
TriggerKeyword(id="keyword1", keyword="testKeyword2"), keyword="Keyword",
], ),
plan=Plan(
id=uuid.uuid4(),
name="testTriggerPlanName",
steps=[],
),
) )
def base_phase() -> Phase: def base_phase() -> Phase:
return Phase( return Phase(
id="phase1", id=uuid.uuid4(),
label="basephase",
norms=[base_norm()], norms=[base_norm()],
goals=[base_goal()], goals=[base_goal()],
triggers=[base_trigger()], triggers=[base_trigger()],
@@ -58,7 +70,7 @@ def invalid_program() -> dict:
# wrong types inside phases list (not Phase objects) # wrong types inside phases list (not Phase objects)
return { return {
"phases": [ "phases": [
{"id": "phase1"}, # incomplete {"id": uuid.uuid4()}, # incomplete
{"not_a_phase": True}, {"not_a_phase": True},
] ]
} }
@@ -77,8 +89,8 @@ def test_valid_deepprogram():
# validate nested components directly # validate nested components directly
phase = validated.phases[0] phase = validated.phases[0]
assert isinstance(phase.goals[0], Goal) assert isinstance(phase.goals[0], Goal)
assert isinstance(phase.triggers[0], KeywordTrigger) assert isinstance(phase.triggers[0], Trigger)
assert isinstance(phase.norms[0], Norm) assert isinstance(phase.norms[0], BasicNorm)
def test_invalid_program(): def test_invalid_program():