Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions extensions/python/monologue_end/_85_ensure_response_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
from typing import Any

from helpers.extension import Extension


class EnsureResponseLog(Extension):
async def execute(self, loop_data: Any = None, **kwargs):
if not self.agent:
return

if not loop_data:
return

if "log_item_response" in loop_data.params_temporary:
return

gen_item = loop_data.params_temporary.get("log_item_generating")
if not gen_item:
return

response_text = self._get_latest_ai_response_text()
if not response_text:
return

loop_data.params_temporary["log_item_response"] = self.agent.context.log.log(
type="response",
heading=f"icon://chat {self.agent.agent_name}: Responding",
content=response_text,
id=getattr(gen_item, "id", "") or "",
)

def _get_latest_ai_response_text(self) -> str:
try:
topic = self.agent.history.current if self.agent else None
messages = topic.messages if topic else []
for message in reversed(messages):
if message.ai and message.content:
return self._normalize_response_content(message.content)
return ""
except Exception:
return ""

@staticmethod
def _normalize_response_content(content) -> str:
if isinstance(content, dict):
tool_name = content.get("tool_name")
if tool_name and tool_name != "response":
return ""

if isinstance(content.get("tool_args"), dict):
text = content["tool_args"].get("text")
if text:
return str(text)

if content.get("text"):
return str(content["text"])

if tool_name == "response":
return ""

return str(content)

if not isinstance(content, str):
return str(content)

stripped = content.strip()
try:
parsed = json.loads(stripped)
except Exception:
return stripped

if isinstance(parsed, dict):
return EnsureResponseLog._normalize_response_content(parsed)
return stripped
116 changes: 116 additions & 0 deletions tests/test_response_log_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from types import SimpleNamespace

import pytest

from extensions.python.monologue_end._85_ensure_response_log import EnsureResponseLog


class _LoopData:
def __init__(self, **kwargs):
self.params_temporary = kwargs.get("params_temporary", {})


class FakeLog:
def __init__(self):
self.entries = []

def log(self, **kwargs):
self.entries.append(kwargs)
return SimpleNamespace(id=kwargs.get("id"))


def _agent_with_last_message(content, *, ai=True):
return _agent_with_messages([SimpleNamespace(ai=ai, content=content)])


def _agent_with_messages(messages):
log = FakeLog()
agent = SimpleNamespace(
agent_name="A0",
context=SimpleNamespace(log=log),
history=SimpleNamespace(
current=SimpleNamespace(messages=messages),
),
)
return agent, log


@pytest.mark.asyncio
async def test_monologue_end_logs_plain_text_ai_response_from_history():
agent, log = _agent_with_last_message("Plain text final answer.")
loop_data = _LoopData(
params_temporary={
"log_item_generating": SimpleNamespace(id="stream-log-id"),
}
)

await EnsureResponseLog(agent=agent).execute(loop_data=loop_data)

assert log.entries == [
{
"type": "response",
"heading": "icon://chat A0: Responding",
"content": "Plain text final answer.",
"id": "stream-log-id",
}
]


@pytest.mark.asyncio
async def test_monologue_end_logs_latest_ai_response_when_warning_follows_it():
agent, log = _agent_with_messages(
[
SimpleNamespace(ai=True, content="Plain text final answer."),
SimpleNamespace(
ai=False,
content="You have misformatted your message. Follow system prompt instructions.",
),
]
)
loop_data = _LoopData(
params_temporary={
"log_item_generating": SimpleNamespace(id="stream-log-id"),
}
)

await EnsureResponseLog(agent=agent).execute(loop_data=loop_data)

assert log.entries == [
{
"type": "response",
"heading": "icon://chat A0: Responding",
"content": "Plain text final answer.",
"id": "stream-log-id",
}
]


@pytest.mark.asyncio
async def test_monologue_end_skips_when_live_response_already_logged():
agent, log = _agent_with_last_message("Already logged.")
loop_data = _LoopData(
params_temporary={
"log_item_generating": SimpleNamespace(id="stream-log-id"),
"log_item_response": SimpleNamespace(id="response-log-id"),
}
)

await EnsureResponseLog(agent=agent).execute(loop_data=loop_data)

assert log.entries == []


@pytest.mark.asyncio
async def test_monologue_end_does_not_log_non_response_tool_request():
agent, log = _agent_with_last_message(
'{"tool_name":"search_engine","tool_args":{"query":"Agent Zero"}}'
)
loop_data = _LoopData(
params_temporary={
"log_item_generating": SimpleNamespace(id="stream-log-id"),
}
)

await EnsureResponseLog(agent=agent).execute(loop_data=loop_data)

assert log.entries == []