Skip to content

Commit 545e9a2

Browse files
2 parents da7a8da + 2fee0fc commit 545e9a2

6 files changed

Lines changed: 213 additions & 43 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: 16 additions & 3 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
@@ -142,8 +143,20 @@ async def init_orchestration(
142143

143144
manager_agent = Agent(manager_chat_client, name="MagenticManager")
144145

146+
# Collect participant agent names so the orchestrator plan prompt can
147+
# enforce mandatory inclusion of every team agent (e.g. TriageAgent,
148+
# ComplianceAgent) — otherwise the manager silently drops them.
149+
participant_agent_names = []
150+
for ag in agents:
151+
nm = getattr(ag, "agent_name", None) or getattr(ag, "name", None)
152+
if nm:
153+
participant_agent_names.append(nm)
154+
145155
# Get prompt customization kwargs
146-
prompt_kwargs = get_magentic_prompt_kwargs(has_user_responses=has_user_responses)
156+
prompt_kwargs = get_magentic_prompt_kwargs(
157+
has_user_responses=has_user_responses,
158+
participant_names=participant_agent_names,
159+
)
147160

148161
cls.logger.info(
149162
"Building MagenticBuilder for user '%s' with max_rounds=%d, "
@@ -782,12 +795,12 @@ async def _process_event_stream(
782795
and executor != current_streaming_agent_ref[0]
783796
):
784797
current_streaming_agent_ref[0] = executor
785-
display_name = executor.replace("_", " ")
798+
display_name = format_agent_display_name(executor)
786799
header_text = f"\n\n---\n### {display_name}\n\n"
787800
try:
788801
await connection_config.send_status_update_async(
789802
AgentMessageStreaming(
790-
agent_name=executor,
803+
agent_name=display_name,
791804
content=header_text,
792805
is_final=False,
793806
),

src/backend/orchestration/plan_review_helpers.py

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,23 @@
3131
# Prompt kwargs builder
3232
# ---------------------------------------------------------------------------
3333

34-
def get_magentic_prompt_kwargs(*, has_user_responses: bool = False) -> dict:
34+
def get_magentic_prompt_kwargs(
35+
*,
36+
has_user_responses: bool = False,
37+
participant_names: Optional[list[str]] = None,
38+
) -> dict:
3539
"""Build the prompt-override kwargs dict for ``MagenticBuilder``.
3640
3741
Args:
3842
has_user_responses: Whether any agent has ``user_responses: true``,
3943
giving it access to the ``ask_user`` tool for user clarification.
4044
When True, prompts allow agents to gather info via their tools;
4145
when False, agents must use defaults only.
46+
participant_names: Names of the team's participant agents. When provided,
47+
the orchestrator plan prompt is augmented with a MANDATORY AGENTS
48+
clause requiring every one of these agents to appear as a plan step,
49+
so coordinator-like agents (e.g. TriageAgent) and final-validation
50+
agents (e.g. ComplianceAgent) are not silently dropped.
4251
4352
Returns:
4453
A dict suitable for unpacking into ``MagenticBuilder(**kwargs)``.
@@ -70,12 +79,26 @@ def get_magentic_prompt_kwargs(*, has_user_responses: bool = False) -> dict:
7079
- Ask EXACTLY 0 questions. Always proceed with sensible defaults.
7180
"""
7281

82+
mandatory_block = ""
83+
if participant_names:
84+
agent_lines = "\n".join(f"- {name}" for name in participant_names)
85+
mandatory_block = (
86+
"\n\nMANDATORY AGENTS (CRITICAL — NON-NEGOTIABLE):\n"
87+
"Every plan you generate MUST include EVERY ONE of the following agents,\n"
88+
"each as its own distinct step, each invoked at least once:\n"
89+
+ agent_lines
90+
+ "\nDo NOT omit any of these agents, even if a step seems optional,\n"
91+
"redundant, already covered by another agent, or something you "
92+
"(MagenticManager)\ncould do yourself. A plan missing any listed agent "
93+
"is INVALID — regenerate it\nuntil every listed agent appears as a step.\n"
94+
)
95+
7396
plan_append = """
7497
7598
PLAN RULES:
7699
- Steps are HIGH-LEVEL task assignments — one step per agent. Do NOT prescribe
77100
sub-tasks, parameters, or data retrieval. Agents discover their own processes.
78-
""" + clarification_policy + """
101+
""" + mandatory_block + clarification_policy + """
79102
OUTPUT FORMAT (CRITICAL — use EXACTLY this JSON structure, nothing else):
80103
```json
81104
[
@@ -86,11 +109,14 @@ def get_magentic_prompt_kwargs(*, has_user_responses: bool = False) -> dict:
86109
Use exact agent names from the team list above. Output ONLY the JSON array — no
87110
markdown fences, no commentary before or after.
88111
89-
IMPORTANT: There is NO UserInteractionAgent. Do NOT include any user-interaction
112+
""" + ("""IMPORTANT: There is NO UserInteractionAgent. Do NOT include any user-interaction
90113
agent in the plan. Domain agents gather user info themselves via their
91114
request_user_clarification tool — the framework pauses automatically when they
92115
call it and resumes when the user answers.
93-
116+
""" if has_user_responses else """IMPORTANT: There is NO UserInteractionAgent. Do NOT include any user-interaction
117+
agent in the plan. Agents apply sensible defaults for missing details and proceed
118+
without asking the user any questions.
119+
""") + """
94120
Example plan:
95121
[
96122
{{"agent": "HRHelperAgent", "action": "execute the onboarding process for the new employee"}},
@@ -143,12 +169,42 @@ def get_magentic_prompt_kwargs(*, has_user_responses: bool = False) -> dict:
143169
ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT + facts_append
144170
)
145171

146-
progress_append = """
172+
# Completion-enforcement progress-ledger rules. Applied ALWAYS (not only when
173+
# has_user_responses) so that EVERY plan-step agent — e.g. TriageAgent and
174+
# ComplianceAgent in the content_gen team, whose agents all have
175+
# user_responses=false — must actually run before the request can be marked
176+
# satisfied, and the orchestrator re-selects any uninvoked plan-step agent
177+
# instead of silently finishing early.
178+
progress_append = """
147179
148180
EXECUTION RULES:
149181
- When selecting next_speaker, prefer a work agent that has NOT yet been invoked.
150-
- MagenticManager MUST NOT generate answers, ask questions, or list missing info.
151-
It only routes tasks to the appropriate agent.
182+
- MagenticManager MUST NOT generate answers or fabricate content on behalf of
183+
work agents. It only routes tasks and compiles the final output.
184+
185+
COMPLETION CHECK (CRITICAL):
186+
Before setting is_request_satisfied to true, you MUST verify:
187+
1. Review the conversation history and list every agent that has actually produced
188+
a substantive response (meaningful output — calling tools and returning results
189+
where the agent has tools, or producing a substantive text response otherwise).
190+
2. Compare that list against the plan steps. If ANY plan-step agent has NOT been
191+
invoked and produced a substantive response, set is_request_satisfied to false
192+
and select the next uninvoked agent as next_speaker.
193+
3. is_request_satisfied = true ONLY when ALL plan-step agents have completed
194+
their work (produced a substantive response — tool results, or meaningful text
195+
output for agents that have no tools).
196+
- Each agent handles a DISTINCT domain. One agent's output does NOT satisfy
197+
another agent's step.
198+
- Do NOT re-invoke an agent that already completed its step successfully.
199+
- IGNORE agent-level completion language (e.g. "all steps are complete",
200+
"onboarding is done"). An individual agent only knows about its own domain.
201+
The workflow is NOT complete until every plan-step agent has been invoked."""
202+
203+
if has_user_responses:
204+
progress_append += """
205+
206+
USER-CLARIFICATION EXECUTION RULES:
207+
- MagenticManager MUST NOT ask questions or list missing info — it only routes.
152208
- There is NO UserInteractionAgent. Do NOT select it as next_speaker.
153209
- Domain agents that need user info will call their request_user_clarification
154210
tool. The framework handles the pause/resume automatically via
@@ -168,26 +224,11 @@ def get_magentic_prompt_kwargs(*, has_user_responses: bool = False) -> dict:
168224
STALL DETECTION OVERRIDE:
169225
- An agent calling request_user_clarification is NOT stalling. The framework
170226
pauses automatically. Set is_progress_being_made=true and is_in_loop=false.
171-
- Do NOT treat a framework pause as a stall or loop.
227+
- Do NOT treat a framework pause as a stall or loop."""
172228

173-
COMPLETION CHECK (CRITICAL):
174-
Before setting is_request_satisfied to true, you MUST verify:
175-
1. Review the conversation history and list every agent that has actually produced
176-
a substantive response (called tools and returned results).
177-
2. Compare that list against the plan steps. If ANY plan-step agent has NOT been
178-
invoked and produced a substantive response, set is_request_satisfied to false
179-
and select the next uninvoked agent as next_speaker.
180-
3. is_request_satisfied = true ONLY when ALL plan-step agents have completed
181-
their work successfully (called their tools, returned results).
182-
- Each agent handles a DISTINCT domain. One agent's output does NOT satisfy
183-
another agent's step.
184-
- Do NOT re-invoke an agent that already completed its step successfully.
185-
- IGNORE agent-level completion language (e.g. "all steps are complete",
186-
"onboarding is done"). An individual agent only knows about its own domain.
187-
The workflow is NOT complete until every plan-step agent has been invoked."""
188-
kwargs["progress_ledger_prompt"] = (
189-
ORCHESTRATOR_PROGRESS_LEDGER_PROMPT + progress_append
190-
)
229+
kwargs["progress_ledger_prompt"] = (
230+
ORCHESTRATOR_PROGRESS_LEDGER_PROMPT + progress_append
231+
)
191232

192233
return kwargs
193234

0 commit comments

Comments
 (0)