Skip to content

Commit c249025

Browse files
resolved the human in the loop issue
1 parent 4856b0b commit c249025

4 files changed

Lines changed: 63 additions & 8 deletions

File tree

src/backend/v4/magentic_agents/proxy_agent.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,19 @@ async def _invoke_stream_internal(
147147
logger.debug("ProxyAgent: Message text: %s", message_text[:100])
148148

149149
clarification_req_text = f"{message_text}"
150+
request_id = str(uuid.uuid4())
150151
clarification_request = UserClarificationRequest(
151152
question=clarification_req_text,
152-
request_id=str(uuid.uuid4()),
153+
request_id=request_id,
153154
)
154155

155156
# Dispatch websocket event requesting clarification
157+
# Serialize dataclass to a plain dict so json.dumps produces proper JSON
158+
# instead of relying on str() repr which is fragile for the frontend parser.
156159
await connection_config.send_status_update_async(
157160
{
158-
"type": WebsocketMessageType.USER_CLARIFICATION_REQUEST,
159-
"data": clarification_request,
161+
"question": clarification_req_text,
162+
"request_id": request_id,
160163
},
161164
user_id=self.user_id,
162165
message_type=WebsocketMessageType.USER_CLARIFICATION_REQUEST,

src/backend/v4/orchestration/human_approval_manager.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Optional
99

1010
import v4.models.messages as messages
11-
from agent_framework import Message
11+
from agent_framework import AgentResponse, Message
1212
from agent_framework_orchestrations._magentic import (
1313
MagenticContext,
1414
StandardMagenticManager,
@@ -94,6 +94,45 @@ def __init__(self, user_id: str, agent, *args, **kwargs):
9494
# New API: StandardMagenticManager takes agent as first positional argument
9595
super().__init__(agent, *args, **kwargs)
9696

97+
async def _complete(self, messages: list[Message]) -> Message:
98+
"""Override to pass session=None, making each LLM call stateless.
99+
100+
The base class passes session=self._session which triggers
101+
InMemoryHistoryProvider auto-injection and previous_response_id
102+
chaining in rc4. This causes message payloads to grow with every
103+
internal call (facts, plan, progress ledger, etc.), burning through
104+
TPM quota (429 errors) and confusing the orchestrator LLM's routing
105+
decisions (e.g. skipping ProxyAgent for user clarification).
106+
107+
Passing session=None restores the old stateless behavior where each
108+
call only sends the messages explicitly provided.
109+
"""
110+
from openai import RateLimitError
111+
112+
max_retries = 5
113+
base_delay = 2.0 # seconds
114+
115+
for attempt in range(max_retries):
116+
try:
117+
response: AgentResponse = await self._agent.run(messages, session=None)
118+
if not response.messages:
119+
raise RuntimeError("Agent returned no messages in response.")
120+
if len(response.messages) > 1:
121+
logger.warning("Agent returned multiple messages; using the last one.")
122+
return response.messages[-1]
123+
except Exception as exc:
124+
inner = getattr(exc, "inner_exception", None)
125+
is_rate_limit = isinstance(inner, RateLimitError) or "429" in str(exc)
126+
if is_rate_limit and attempt < max_retries - 1:
127+
delay = base_delay * (2 ** attempt)
128+
logger.warning(
129+
"Rate limit hit (attempt %d/%d). Retrying in %.1fs...",
130+
attempt + 1, max_retries, delay,
131+
)
132+
await asyncio.sleep(delay)
133+
continue
134+
raise
135+
97136
async def plan(self, magentic_context: MagenticContext) -> Any:
98137
"""
99138
Override the plan method to create the plan first, then ask for approval before execution.

src/backend/v4/orchestration/orchestration_manager.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414
ChatOptions,
1515
Message,
1616
InMemoryCheckpointStorage,
17-
WorkflowEvent,
1817
)
1918
from agent_framework_orchestrations import MagenticBuilder
2019
from agent_framework_orchestrations._base_group_chat_orchestrator import (
2120
GroupChatRequestSentEvent,
2221
GroupChatResponseReceivedEvent,
2322
)
2423
from agent_framework_orchestrations._magentic import (
25-
MagenticOrchestratorEvent,
2624
MagenticProgressLedger,
2725
)
2826

@@ -34,7 +32,6 @@
3432
from v4.common.services.team_service import TeamService
3533
import time as _time
3634
from v4.callbacks.response_handlers import (
37-
agent_response_callback,
3835
streaming_agent_response_callback,
3936
)
4037
from v4.config.settings import connection_config, orchestration_config
@@ -387,7 +384,6 @@ async def run_orchestration(self, user_id: str, input_task) -> None:
387384

388385
self.logger.info("Starting workflow execution...")
389386

390-
last_message_id: str | None = None
391387
async for event in workflow.run(task_text, stream=True):
392388
try:
393389
# WorkflowEvent has a .type field (string) instead of specific event classes

src/frontend/src/services/PlanDataService.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,23 @@ export class PlanDataService {
765765
*/
766766
static parseUserClarificationRequest(rawData: any): ParsedUserClarification | null {
767767
try {
768+
// First try direct JSON extraction (clean dict format from backend)
769+
const extractDirect = (val: any, depth = 0): ParsedUserClarification | null => {
770+
if (depth > 10 || !val || typeof val !== 'object') return null;
771+
if (typeof val.question === 'string' && typeof val.request_id === 'string') {
772+
return {
773+
type: WebsocketMessageType.USER_CLARIFICATION_REQUEST,
774+
question: val.question.trim(),
775+
request_id: val.request_id,
776+
};
777+
}
778+
if (val.data !== undefined) return extractDirect(val.data, depth + 1);
779+
return null;
780+
};
781+
const direct = extractDirect(rawData);
782+
if (direct) return direct;
783+
784+
// Fallback: extract from Python repr string (legacy format)
768785
const extractString = (val: any, depth = 0): string | null => {
769786
if (depth > 15) return null;
770787
if (typeof val === 'string') {

0 commit comments

Comments
 (0)