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
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
Test configuration for agent tests.
"""

import sys
from pathlib import Path

import pytest

# Add the agents path
Expand Down
46 changes: 44 additions & 2 deletions src/backend/callbacks/response_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,45 @@
logger = logging.getLogger(__name__)


def format_agent_display_name(raw_name: str) -> str:
"""Convert raw agent IDs (e.g. 'HRHelperAgent', 'hr_helper_agent') to
human-readable display names (e.g. 'HR Helper Agent').

Applies similar splitting/casing logic as the frontend's
``cleanTextToSpaces`` + ``getAgentDisplayName`` pipeline, but does NOT
strip the "Agent" suffix (the frontend handles that separately).
"""
if not raw_name:
return "Assistant"

name = raw_name

# Replace underscores with spaces
name = name.replace("_", " ")

# Insert space before each uppercase letter preceded by a lowercase letter
# e.g. "HelperAgent" → "Helper Agent"
name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)

# Insert space between consecutive uppercase and an uppercase+lowercase pair
# e.g. "HRHelper" → "HR Helper"
name = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1 \2', name)

# Collapse multiple spaces
name = re.sub(r'\s+', ' ', name).strip()

# Title-case each word
name = name.title()

# Fix common acronyms back to uppercase (word-boundary safe)
_ACRONYMS = {'Hr': 'HR', 'It': 'IT', 'Ai': 'AI', 'Api': 'API',
'Ui': 'UI', 'Db': 'DB', 'Kb': 'KB'}
for title_form, upper_form in _ACRONYMS.items():
name = re.sub(rf'\b{title_form}\b', upper_form, name)

return name


def clean_citations(text: str) -> str:
"""Remove citation markers from agent responses while preserving formatting."""
if not text:
Expand Down Expand Up @@ -66,6 +105,7 @@ def agent_response_callback(
Final (non-streaming) agent response callback using agent_framework Message.
"""
agent_name = getattr(message, "author_name", None) or agent_id or "Unknown Agent"
agent_name = format_agent_display_name(agent_name)
role = getattr(message, "role", "assistant")
Comment thread
Akhileswara-Microsoft marked this conversation as resolved.

# Message has a .text property that concatenates all TextContent items
Expand Down Expand Up @@ -107,6 +147,8 @@ async def streaming_agent_response_callback(
if not user_id:
return

display_name = format_agent_display_name(agent_id)

try:
chunk_text = getattr(update, "text", None)
if not chunk_text:
Expand All @@ -123,7 +165,7 @@ async def streaming_agent_response_callback(
contents = getattr(update, "contents", []) or []
tool_calls = _extract_tool_calls_from_contents(contents)
if tool_calls:
tool_message = AgentToolMessage(agent_name=agent_id)
tool_message = AgentToolMessage(agent_name=display_name)
tool_message.tool_calls.extend(tool_calls)
await connection_config.send_status_update_async(
tool_message,
Expand All @@ -134,7 +176,7 @@ async def streaming_agent_response_callback(

if cleaned:
streaming_payload = AgentMessageStreaming(
agent_name=agent_id,
agent_name=display_name,
content=cleaned,
is_final=is_final,
)
Expand Down
5 changes: 3 additions & 2 deletions src/backend/orchestration/orchestration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MagenticPlanReviewRequest)
from agents.agent_factory import AgentFactory
from callbacks.response_handlers import (agent_response_callback,
format_agent_display_name,
streaming_agent_response_callback)
from common.config.app_config import config
from common.database.database_base import DatabaseBase
Expand Down Expand Up @@ -782,12 +783,12 @@ async def _process_event_stream(
and executor != current_streaming_agent_ref[0]
):
current_streaming_agent_ref[0] = executor
display_name = executor.replace("_", " ")
display_name = format_agent_display_name(executor)
header_text = f"\n\n---\n### {display_name}\n\n"
try:
await connection_config.send_status_update_async(
AgentMessageStreaming(
agent_name=executor,
agent_name=display_name,
content=header_text,
is_final=False,
),
Expand Down
61 changes: 50 additions & 11 deletions src/tests/backend/callbacks/test_response_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def __init__(self, text="", role="assistant", author_name=""):
from backend.callbacks.response_handlers import (
_extract_tool_calls_from_contents, _is_function_call_item,
agent_response_callback, clean_citations,
format_agent_display_name,
streaming_agent_response_callback)

# Access mocked modules that we'll use in tests
Expand Down Expand Up @@ -216,7 +217,45 @@ def test_clean_citations_preserves_formatting(self):
assert clean_citations(text) == expected


class TestIsFunctionCallItem:
class TestFormatAgentDisplayName:
"""Tests for the format_agent_display_name function."""

def test_empty_string_returns_assistant(self):
assert format_agent_display_name("") == "Assistant"

def test_none_returns_assistant(self):
assert format_agent_display_name(None) == "Assistant"

def test_pascal_case(self):
assert format_agent_display_name("ContentAgent") == "Content Agent"

def test_snake_case(self):
assert format_agent_display_name("content_agent") == "Content Agent"

def test_acronym_prefix(self):
assert format_agent_display_name("HRHelperAgent") == "HR Helper Agent"

def test_acronym_word_boundary_no_false_positive(self):
"""Ensure 'It' inside 'Habit' is NOT replaced with 'IT'."""
assert format_agent_display_name("HabitAgent") == "Habit Agent"

def test_multiple_acronyms(self):
assert format_agent_display_name("AIApiAgent") == "AI API Agent"

def test_already_spaced(self):
assert format_agent_display_name("Content Agent") == "Content Agent"

def test_single_word(self):
assert format_agent_display_name("Triage") == "Triage"

def test_mixed_case_id(self):
assert format_agent_display_name("agent_123") == "Agent 123"

def test_consecutive_uppercase(self):
assert format_agent_display_name("DBAdmin") == "DB Admin"



"""Tests for the _is_function_call_item function."""

def test_is_function_call_item_none(self):
Expand Down Expand Up @@ -415,7 +454,7 @@ def test_agent_response_callback_with_chat_message(self, mock_time, mock_create_

# Verify AgentMessage was created with cleaned text
mock_agent_message.assert_called_once_with(
agent_name="TestAgent",
agent_name="Test Agent",
timestamp=1234567890.0,
content="Test message with citations "
)
Expand Down Expand Up @@ -445,7 +484,7 @@ def test_agent_response_callback_fallback_message(self, mock_time, mock_create_t

# Verify AgentMessage was created with agent_id as agent_name
mock_agent_message.assert_called_once_with(
agent_name="agent_123",
agent_name="Agent 123",
timestamp=1234567890.0,
content="Fallback message text"
)
Expand All @@ -469,7 +508,7 @@ def test_agent_response_callback_no_text_attribute(self, mock_time, mock_create_

# Verify AgentMessage was created with empty content
mock_agent_message.assert_called_once_with(
agent_name="TestAgent",
agent_name="Test Agent",
timestamp=1234567890.0,
content=""
)
Expand Down Expand Up @@ -515,7 +554,7 @@ def test_agent_response_callback_successful_logging(self, mock_time, mock_create
call_args = mock_logger.info.call_args[0]
assert call_args[0] == "%s message (agent=%s): %s"
assert call_args[1] == "Assistant"
assert call_args[2] == "TestAgent"
assert call_args[2] == "Test Agent"
assert len(call_args[3]) == 193 # Message should be the actual length (not truncated in this case)


Expand Down Expand Up @@ -547,7 +586,7 @@ async def test_streaming_callback_with_text(self):

# Verify AgentMessageStreaming was created with cleaned text
mock_streaming.assert_called_once_with(
agent_name="agent_123",
agent_name="Agent 123",
content="Test streaming text ",
is_final=True
)
Expand Down Expand Up @@ -587,7 +626,7 @@ async def test_streaming_callback_no_text_with_contents(self):

# Verify AgentMessageStreaming was created with concatenated content text
mock_streaming.assert_called_once_with(
agent_name="agent_123",
agent_name="Agent 123",
content="Content from content attribute",
is_final=False
)
Expand Down Expand Up @@ -640,7 +679,7 @@ async def test_streaming_callback_with_tool_calls(self):
await streaming_agent_response_callback("agent_123", mock_update, False, user_id="user_456")

# Verify tool message was created and sent
mock_tool_message.assert_called_once_with(agent_name="agent_123")
mock_tool_message.assert_called_once_with(agent_name="Agent 123")
# Verify tool_calls.extend was called with our mock tool call
assert mock_tool_call in mock_tool_msg.tool_calls or mock_tool_msg.tool_calls.extend.called # type: ignore[union-attr] # noqa: E1101 # pylint: disable=no-member

Expand All @@ -666,7 +705,7 @@ async def test_streaming_callback_no_contents_attribute(self):

# Should still process the text
mock_streaming.assert_called_once_with(
agent_name="agent_123",
agent_name="Agent 123",
content="Test text",
is_final=True
)
Expand Down Expand Up @@ -733,7 +772,7 @@ async def test_streaming_callback_tool_calls_functionality(self):
await streaming_agent_response_callback("agent_123", mock_update, False, user_id="user_456")

# Verify tool message was created and tool calls were processed
mock_tool_message.assert_called_once_with(agent_name="agent_123")
mock_tool_message.assert_called_once_with(agent_name="Agent 123")
assert connection_config.send_status_update_async.called

@pytest.mark.asyncio
Expand All @@ -751,7 +790,7 @@ async def test_streaming_callback_chunk_processing(self):

# Verify streaming message was created with correct parameters
mock_streaming.assert_called_once_with(
agent_name="agent_123",
agent_name="Agent 123",
content="Test streaming text for processing",
is_final=True
)
Expand Down
Loading