Skip to content

Commit 078449c

Browse files
Merge pull request #1064 from microsoft/AgentName_Fix47839
fix: Consistent agent display names across UI
2 parents 3a3467c + d4e5abe commit 078449c

4 files changed

Lines changed: 100 additions & 15 deletions

File tree

conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
Test configuration for agent tests.
33
"""
44

5+
import sys
6+
from pathlib import Path
7+
58
import pytest
69

710
# Add the agents path

src/backend/callbacks/response_handlers.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,45 @@
1717
logger = logging.getLogger(__name__)
1818

1919

20+
def format_agent_display_name(raw_name: str) -> str:
21+
"""Convert raw agent IDs (e.g. 'HRHelperAgent', 'hr_helper_agent') to
22+
human-readable display names (e.g. 'HR Helper Agent').
23+
24+
Applies similar splitting/casing logic as the frontend's
25+
``cleanTextToSpaces`` + ``getAgentDisplayName`` pipeline, but does NOT
26+
strip the "Agent" suffix (the frontend handles that separately).
27+
"""
28+
if not raw_name:
29+
return "Assistant"
30+
31+
name = raw_name
32+
33+
# Replace underscores with spaces
34+
name = name.replace("_", " ")
35+
36+
# Insert space before each uppercase letter preceded by a lowercase letter
37+
# e.g. "HelperAgent" → "Helper Agent"
38+
name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)
39+
40+
# Insert space between consecutive uppercase and an uppercase+lowercase pair
41+
# e.g. "HRHelper" → "HR Helper"
42+
name = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1 \2', name)
43+
44+
# Collapse multiple spaces
45+
name = re.sub(r'\s+', ' ', name).strip()
46+
47+
# Title-case each word
48+
name = name.title()
49+
50+
# Fix common acronyms back to uppercase (word-boundary safe)
51+
_ACRONYMS = {'Hr': 'HR', 'It': 'IT', 'Ai': 'AI', 'Api': 'API',
52+
'Ui': 'UI', 'Db': 'DB', 'Kb': 'KB'}
53+
for title_form, upper_form in _ACRONYMS.items():
54+
name = re.sub(rf'\b{title_form}\b', upper_form, name)
55+
56+
return name
57+
58+
2059
def clean_citations(text: str) -> str:
2160
"""Remove citation markers from agent responses while preserving formatting."""
2261
if not text:
@@ -66,6 +105,7 @@ def agent_response_callback(
66105
Final (non-streaming) agent response callback using agent_framework Message.
67106
"""
68107
agent_name = getattr(message, "author_name", None) or agent_id or "Unknown Agent"
108+
agent_name = format_agent_display_name(agent_name)
69109
role = getattr(message, "role", "assistant")
70110

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

150+
display_name = format_agent_display_name(agent_id)
151+
110152
try:
111153
chunk_text = getattr(update, "text", None)
112154
if not chunk_text:
@@ -123,7 +165,7 @@ async def streaming_agent_response_callback(
123165
contents = getattr(update, "contents", []) or []
124166
tool_calls = _extract_tool_calls_from_contents(contents)
125167
if tool_calls:
126-
tool_message = AgentToolMessage(agent_name=agent_id)
168+
tool_message = AgentToolMessage(agent_name=display_name)
127169
tool_message.tool_calls.extend(tool_calls)
128170
await connection_config.send_status_update_async(
129171
tool_message,
@@ -134,7 +176,7 @@ async def streaming_agent_response_callback(
134176

135177
if cleaned:
136178
streaming_payload = AgentMessageStreaming(
137-
agent_name=agent_id,
179+
agent_name=display_name,
138180
content=cleaned,
139181
is_final=is_final,
140182
)

src/backend/orchestration/orchestration_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
MagenticPlanReviewRequest)
1717
from agents.agent_factory import AgentFactory
1818
from callbacks.response_handlers import (agent_response_callback,
19+
format_agent_display_name,
1920
streaming_agent_response_callback)
2021
from common.config.app_config import config
2122
from common.database.database_base import DatabaseBase
@@ -782,12 +783,12 @@ async def _process_event_stream(
782783
and executor != current_streaming_agent_ref[0]
783784
):
784785
current_streaming_agent_ref[0] = executor
785-
display_name = executor.replace("_", " ")
786+
display_name = format_agent_display_name(executor)
786787
header_text = f"\n\n---\n### {display_name}\n\n"
787788
try:
788789
await connection_config.send_status_update_async(
789790
AgentMessageStreaming(
790-
agent_name=executor,
791+
agent_name=display_name,
791792
content=header_text,
792793
is_final=False,
793794
),

src/tests/backend/callbacks/test_response_handlers.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def __init__(self, text="", role="assistant", author_name=""):
134134
from backend.callbacks.response_handlers import (
135135
_extract_tool_calls_from_contents, _is_function_call_item,
136136
agent_response_callback, clean_citations,
137+
format_agent_display_name,
137138
streaming_agent_response_callback)
138139

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

218219

219-
class TestIsFunctionCallItem:
220+
class TestFormatAgentDisplayName:
221+
"""Tests for the format_agent_display_name function."""
222+
223+
def test_empty_string_returns_assistant(self):
224+
assert format_agent_display_name("") == "Assistant"
225+
226+
def test_none_returns_assistant(self):
227+
assert format_agent_display_name(None) == "Assistant"
228+
229+
def test_pascal_case(self):
230+
assert format_agent_display_name("ContentAgent") == "Content Agent"
231+
232+
def test_snake_case(self):
233+
assert format_agent_display_name("content_agent") == "Content Agent"
234+
235+
def test_acronym_prefix(self):
236+
assert format_agent_display_name("HRHelperAgent") == "HR Helper Agent"
237+
238+
def test_acronym_word_boundary_no_false_positive(self):
239+
"""Ensure 'It' inside 'Habit' is NOT replaced with 'IT'."""
240+
assert format_agent_display_name("HabitAgent") == "Habit Agent"
241+
242+
def test_multiple_acronyms(self):
243+
assert format_agent_display_name("AIApiAgent") == "AI API Agent"
244+
245+
def test_already_spaced(self):
246+
assert format_agent_display_name("Content Agent") == "Content Agent"
247+
248+
def test_single_word(self):
249+
assert format_agent_display_name("Triage") == "Triage"
250+
251+
def test_mixed_case_id(self):
252+
assert format_agent_display_name("agent_123") == "Agent 123"
253+
254+
def test_consecutive_uppercase(self):
255+
assert format_agent_display_name("DBAdmin") == "DB Admin"
256+
257+
258+
220259
"""Tests for the _is_function_call_item function."""
221260

222261
def test_is_function_call_item_none(self):
@@ -415,7 +454,7 @@ def test_agent_response_callback_with_chat_message(self, mock_time, mock_create_
415454

416455
# Verify AgentMessage was created with cleaned text
417456
mock_agent_message.assert_called_once_with(
418-
agent_name="TestAgent",
457+
agent_name="Test Agent",
419458
timestamp=1234567890.0,
420459
content="Test message with citations "
421460
)
@@ -445,7 +484,7 @@ def test_agent_response_callback_fallback_message(self, mock_time, mock_create_t
445484

446485
# Verify AgentMessage was created with agent_id as agent_name
447486
mock_agent_message.assert_called_once_with(
448-
agent_name="agent_123",
487+
agent_name="Agent 123",
449488
timestamp=1234567890.0,
450489
content="Fallback message text"
451490
)
@@ -469,7 +508,7 @@ def test_agent_response_callback_no_text_attribute(self, mock_time, mock_create_
469508

470509
# Verify AgentMessage was created with empty content
471510
mock_agent_message.assert_called_once_with(
472-
agent_name="TestAgent",
511+
agent_name="Test Agent",
473512
timestamp=1234567890.0,
474513
content=""
475514
)
@@ -515,7 +554,7 @@ def test_agent_response_callback_successful_logging(self, mock_time, mock_create
515554
call_args = mock_logger.info.call_args[0]
516555
assert call_args[0] == "%s message (agent=%s): %s"
517556
assert call_args[1] == "Assistant"
518-
assert call_args[2] == "TestAgent"
557+
assert call_args[2] == "Test Agent"
519558
assert len(call_args[3]) == 193 # Message should be the actual length (not truncated in this case)
520559

521560

@@ -547,7 +586,7 @@ async def test_streaming_callback_with_text(self):
547586

548587
# Verify AgentMessageStreaming was created with cleaned text
549588
mock_streaming.assert_called_once_with(
550-
agent_name="agent_123",
589+
agent_name="Agent 123",
551590
content="Test streaming text ",
552591
is_final=True
553592
)
@@ -587,7 +626,7 @@ async def test_streaming_callback_no_text_with_contents(self):
587626

588627
# Verify AgentMessageStreaming was created with concatenated content text
589628
mock_streaming.assert_called_once_with(
590-
agent_name="agent_123",
629+
agent_name="Agent 123",
591630
content="Content from content attribute",
592631
is_final=False
593632
)
@@ -640,7 +679,7 @@ async def test_streaming_callback_with_tool_calls(self):
640679
await streaming_agent_response_callback("agent_123", mock_update, False, user_id="user_456")
641680

642681
# Verify tool message was created and sent
643-
mock_tool_message.assert_called_once_with(agent_name="agent_123")
682+
mock_tool_message.assert_called_once_with(agent_name="Agent 123")
644683
# Verify tool_calls.extend was called with our mock tool call
645684
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
646685

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

667706
# Should still process the text
668707
mock_streaming.assert_called_once_with(
669-
agent_name="agent_123",
708+
agent_name="Agent 123",
670709
content="Test text",
671710
is_final=True
672711
)
@@ -733,7 +772,7 @@ async def test_streaming_callback_tool_calls_functionality(self):
733772
await streaming_agent_response_callback("agent_123", mock_update, False, user_id="user_456")
734773

735774
# Verify tool message was created and tool calls were processed
736-
mock_tool_message.assert_called_once_with(agent_name="agent_123")
775+
mock_tool_message.assert_called_once_with(agent_name="Agent 123")
737776
assert connection_config.send_status_update_async.called
738777

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

752791
# Verify streaming message was created with correct parameters
753792
mock_streaming.assert_called_once_with(
754-
agent_name="agent_123",
793+
agent_name="Agent 123",
755794
content="Test streaming text for processing",
756795
is_final=True
757796
)

0 commit comments

Comments
 (0)