agent-framework-devui 1.0.0b251007__py3-none-any.whl → 1.0.0b251016__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agent-framework-devui might be problematic. Click here for more details.
- agent_framework_devui/_conversations.py +473 -0
- agent_framework_devui/_discovery.py +295 -325
- agent_framework_devui/_executor.py +99 -241
- agent_framework_devui/_mapper.py +281 -78
- agent_framework_devui/_server.py +232 -239
- agent_framework_devui/_utils.py +127 -0
- agent_framework_devui/models/__init__.py +15 -10
- agent_framework_devui/models/_discovery_models.py +1 -2
- agent_framework_devui/models/_openai_custom.py +45 -90
- agent_framework_devui/ui/assets/index-CE4pGoXh.css +1 -0
- agent_framework_devui/ui/assets/index-DmL7WSFa.js +577 -0
- agent_framework_devui/ui/index.html +2 -2
- agent_framework_devui-1.0.0b251016.dist-info/METADATA +286 -0
- agent_framework_devui-1.0.0b251016.dist-info/RECORD +23 -0
- agent_framework_devui/ui/assets/index-D0SfShuZ.js +0 -445
- agent_framework_devui/ui/assets/index-WsCIE0bH.css +0 -1
- agent_framework_devui-1.0.0b251007.dist-info/METADATA +0 -172
- agent_framework_devui-1.0.0b251007.dist-info/RECORD +0 -22
- {agent_framework_devui-1.0.0b251007.dist-info → agent_framework_devui-1.0.0b251016.dist-info}/WHEEL +0 -0
- {agent_framework_devui-1.0.0b251007.dist-info → agent_framework_devui-1.0.0b251016.dist-info}/entry_points.txt +0 -0
- {agent_framework_devui-1.0.0b251007.dist-info → agent_framework_devui-1.0.0b251016.dist-info}/licenses/LICENSE +0 -0
agent_framework_devui/_mapper.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import uuid
|
|
8
|
+
from collections import OrderedDict
|
|
8
9
|
from collections.abc import Sequence
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from typing import Any, Union
|
|
@@ -17,6 +18,8 @@ from .models import (
|
|
|
17
18
|
ResponseErrorEvent,
|
|
18
19
|
ResponseFunctionCallArgumentsDeltaEvent,
|
|
19
20
|
ResponseFunctionResultComplete,
|
|
21
|
+
ResponseFunctionToolCall,
|
|
22
|
+
ResponseOutputItemAddedEvent,
|
|
20
23
|
ResponseOutputMessage,
|
|
21
24
|
ResponseOutputText,
|
|
22
25
|
ResponseReasoningTextDeltaEvent,
|
|
@@ -24,7 +27,6 @@ from .models import (
|
|
|
24
27
|
ResponseTextDeltaEvent,
|
|
25
28
|
ResponseTraceEventComplete,
|
|
26
29
|
ResponseUsage,
|
|
27
|
-
ResponseUsageEventComplete,
|
|
28
30
|
ResponseWorkflowEventComplete,
|
|
29
31
|
)
|
|
30
32
|
|
|
@@ -34,19 +36,26 @@ logger = logging.getLogger(__name__)
|
|
|
34
36
|
EventType = Union[
|
|
35
37
|
ResponseStreamEvent,
|
|
36
38
|
ResponseWorkflowEventComplete,
|
|
37
|
-
|
|
39
|
+
ResponseOutputItemAddedEvent,
|
|
38
40
|
ResponseTraceEventComplete,
|
|
39
|
-
ResponseUsageEventComplete,
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class MessageMapper:
|
|
44
45
|
"""Maps Agent Framework messages/responses to OpenAI format."""
|
|
45
46
|
|
|
46
|
-
def __init__(self) -> None:
|
|
47
|
-
"""Initialize Agent Framework message mapper.
|
|
47
|
+
def __init__(self, max_contexts: int = 1000) -> None:
|
|
48
|
+
"""Initialize Agent Framework message mapper.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
max_contexts: Maximum number of contexts to keep in memory (default: 1000)
|
|
52
|
+
"""
|
|
48
53
|
self.sequence_counter = 0
|
|
49
|
-
self._conversion_contexts:
|
|
54
|
+
self._conversion_contexts: OrderedDict[int, dict[str, Any]] = OrderedDict()
|
|
55
|
+
self._max_contexts = max_contexts
|
|
56
|
+
|
|
57
|
+
# Track usage per request for final Response.usage (OpenAI standard)
|
|
58
|
+
self._usage_accumulator: dict[str, dict[str, int]] = {}
|
|
50
59
|
|
|
51
60
|
# Register content type mappers for all 12 Agent Framework content types
|
|
52
61
|
self.content_mappers = {
|
|
@@ -95,7 +104,7 @@ class MessageMapper:
|
|
|
95
104
|
|
|
96
105
|
# Import Agent Framework types for proper isinstance checks
|
|
97
106
|
try:
|
|
98
|
-
from agent_framework import AgentRunResponseUpdate, WorkflowEvent
|
|
107
|
+
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, WorkflowEvent
|
|
99
108
|
from agent_framework._workflows._events import AgentRunUpdateEvent
|
|
100
109
|
|
|
101
110
|
# Handle AgentRunUpdateEvent - workflow event wrapping AgentRunResponseUpdate
|
|
@@ -107,6 +116,10 @@ class MessageMapper:
|
|
|
107
116
|
# If no data, treat as generic workflow event
|
|
108
117
|
return await self._convert_workflow_event(raw_event, context)
|
|
109
118
|
|
|
119
|
+
# Handle complete agent response (AgentRunResponse) - for non-streaming agent execution
|
|
120
|
+
if isinstance(raw_event, AgentRunResponse):
|
|
121
|
+
return await self._convert_agent_response(raw_event, context)
|
|
122
|
+
|
|
110
123
|
# Handle agent updates (AgentRunResponseUpdate) - for direct agent execution
|
|
111
124
|
if isinstance(raw_event, AgentRunResponseUpdate):
|
|
112
125
|
return await self._convert_agent_update(raw_event, context)
|
|
@@ -159,17 +172,31 @@ class MessageMapper:
|
|
|
159
172
|
status="completed",
|
|
160
173
|
)
|
|
161
174
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
# Get usage from accumulator (OpenAI standard)
|
|
176
|
+
request_id = str(id(request))
|
|
177
|
+
usage_data = self._usage_accumulator.get(request_id)
|
|
178
|
+
|
|
179
|
+
if usage_data:
|
|
180
|
+
usage = ResponseUsage(
|
|
181
|
+
input_tokens=usage_data["input_tokens"],
|
|
182
|
+
output_tokens=usage_data["output_tokens"],
|
|
183
|
+
total_tokens=usage_data["total_tokens"],
|
|
184
|
+
input_tokens_details=InputTokensDetails(cached_tokens=0),
|
|
185
|
+
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
|
|
186
|
+
)
|
|
187
|
+
# Cleanup accumulator
|
|
188
|
+
del self._usage_accumulator[request_id]
|
|
189
|
+
else:
|
|
190
|
+
# Fallback: estimate if no usage was tracked
|
|
191
|
+
input_token_count = len(str(request.input)) // 4 if request.input else 0
|
|
192
|
+
output_token_count = len(full_content) // 4
|
|
193
|
+
usage = ResponseUsage(
|
|
194
|
+
input_tokens=input_token_count,
|
|
195
|
+
output_tokens=output_token_count,
|
|
196
|
+
total_tokens=input_token_count + output_token_count,
|
|
197
|
+
input_tokens_details=InputTokensDetails(cached_tokens=0),
|
|
198
|
+
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
|
|
199
|
+
)
|
|
173
200
|
|
|
174
201
|
return OpenAIResponse(
|
|
175
202
|
id=f"resp_{uuid.uuid4().hex[:12]}",
|
|
@@ -186,10 +213,18 @@ class MessageMapper:
|
|
|
186
213
|
except Exception as e:
|
|
187
214
|
logger.exception(f"Error aggregating response: {e}")
|
|
188
215
|
return await self._create_error_response(str(e), request)
|
|
216
|
+
finally:
|
|
217
|
+
# Cleanup: Remove context after aggregation to prevent memory leak
|
|
218
|
+
# This handles the common case where streaming completes successfully
|
|
219
|
+
request_key = id(request)
|
|
220
|
+
if self._conversion_contexts.pop(request_key, None):
|
|
221
|
+
logger.debug(f"Cleaned up context for request {request_key} after aggregation")
|
|
189
222
|
|
|
190
223
|
def _get_or_create_context(self, request: AgentFrameworkRequest) -> dict[str, Any]:
|
|
191
224
|
"""Get or create conversion context for this request.
|
|
192
225
|
|
|
226
|
+
Uses LRU eviction when max_contexts is reached to prevent unbounded memory growth.
|
|
227
|
+
|
|
193
228
|
Args:
|
|
194
229
|
request: Request to get context for
|
|
195
230
|
|
|
@@ -197,13 +232,26 @@ class MessageMapper:
|
|
|
197
232
|
Conversion context dictionary
|
|
198
233
|
"""
|
|
199
234
|
request_key = id(request)
|
|
235
|
+
|
|
200
236
|
if request_key not in self._conversion_contexts:
|
|
237
|
+
# Evict oldest context if at capacity (LRU eviction)
|
|
238
|
+
if len(self._conversion_contexts) >= self._max_contexts:
|
|
239
|
+
evicted_key, _ = self._conversion_contexts.popitem(last=False)
|
|
240
|
+
logger.debug(f"Evicted oldest context (key={evicted_key}) - at max capacity ({self._max_contexts})")
|
|
241
|
+
|
|
201
242
|
self._conversion_contexts[request_key] = {
|
|
202
243
|
"sequence_counter": 0,
|
|
203
244
|
"item_id": f"msg_{uuid.uuid4().hex[:8]}",
|
|
204
245
|
"content_index": 0,
|
|
205
246
|
"output_index": 0,
|
|
247
|
+
"request_id": str(request_key), # For usage accumulation
|
|
248
|
+
# Track active function calls: {call_id: {name, item_id, args_chunks}}
|
|
249
|
+
"active_function_calls": {},
|
|
206
250
|
}
|
|
251
|
+
else:
|
|
252
|
+
# Move to end (mark as recently used for LRU)
|
|
253
|
+
self._conversion_contexts.move_to_end(request_key)
|
|
254
|
+
|
|
207
255
|
return self._conversion_contexts[request_key]
|
|
208
256
|
|
|
209
257
|
def _next_sequence(self, context: dict[str, Any]) -> int:
|
|
@@ -240,10 +288,11 @@ class MessageMapper:
|
|
|
240
288
|
|
|
241
289
|
if content_type in self.content_mappers:
|
|
242
290
|
mapped_events = await self.content_mappers[content_type](content, context)
|
|
243
|
-
if
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
if mapped_events is not None: # Handle None returns (e.g., UsageContent)
|
|
292
|
+
if isinstance(mapped_events, list):
|
|
293
|
+
events.extend(mapped_events)
|
|
294
|
+
else:
|
|
295
|
+
events.append(mapped_events)
|
|
247
296
|
else:
|
|
248
297
|
# Graceful fallback for unknown content types
|
|
249
298
|
events.append(await self._create_unknown_content_event(content, context))
|
|
@@ -256,6 +305,59 @@ class MessageMapper:
|
|
|
256
305
|
|
|
257
306
|
return events
|
|
258
307
|
|
|
308
|
+
async def _convert_agent_response(self, response: Any, context: dict[str, Any]) -> Sequence[Any]:
|
|
309
|
+
"""Convert complete AgentRunResponse to OpenAI events.
|
|
310
|
+
|
|
311
|
+
This handles non-streaming agent execution where agent.run() returns
|
|
312
|
+
a complete AgentRunResponse instead of streaming AgentRunResponseUpdate objects.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
response: Agent run response (AgentRunResponse)
|
|
316
|
+
context: Conversion context
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of OpenAI response stream events
|
|
320
|
+
"""
|
|
321
|
+
events: list[Any] = []
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
# Extract all messages from the response
|
|
325
|
+
messages = getattr(response, "messages", [])
|
|
326
|
+
|
|
327
|
+
# Convert each message's contents to streaming events
|
|
328
|
+
for message in messages:
|
|
329
|
+
if hasattr(message, "contents") and message.contents:
|
|
330
|
+
for content in message.contents:
|
|
331
|
+
content_type = content.__class__.__name__
|
|
332
|
+
|
|
333
|
+
if content_type in self.content_mappers:
|
|
334
|
+
mapped_events = await self.content_mappers[content_type](content, context)
|
|
335
|
+
if mapped_events is not None: # Handle None returns (e.g., UsageContent)
|
|
336
|
+
if isinstance(mapped_events, list):
|
|
337
|
+
events.extend(mapped_events)
|
|
338
|
+
else:
|
|
339
|
+
events.append(mapped_events)
|
|
340
|
+
else:
|
|
341
|
+
# Graceful fallback for unknown content types
|
|
342
|
+
events.append(await self._create_unknown_content_event(content, context))
|
|
343
|
+
|
|
344
|
+
context["content_index"] += 1
|
|
345
|
+
|
|
346
|
+
# Add usage information if present
|
|
347
|
+
usage_details = getattr(response, "usage_details", None)
|
|
348
|
+
if usage_details:
|
|
349
|
+
from agent_framework import UsageContent
|
|
350
|
+
|
|
351
|
+
usage_content = UsageContent(details=usage_details)
|
|
352
|
+
await self._map_usage_content(usage_content, context)
|
|
353
|
+
# Note: _map_usage_content returns None - it accumulates usage for final Response.usage
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.warning(f"Error converting agent response: {e}")
|
|
357
|
+
events.append(await self._create_error_event(str(e), context))
|
|
358
|
+
|
|
359
|
+
return events
|
|
360
|
+
|
|
259
361
|
async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> Sequence[Any]:
|
|
260
362
|
"""Convert workflow event to structured OpenAI events.
|
|
261
363
|
|
|
@@ -317,42 +419,141 @@ class MessageMapper:
|
|
|
317
419
|
|
|
318
420
|
async def _map_function_call_content(
|
|
319
421
|
self, content: Any, context: dict[str, Any]
|
|
320
|
-
) -> list[ResponseFunctionCallArgumentsDeltaEvent]:
|
|
321
|
-
"""Map FunctionCallContent to
|
|
322
|
-
events = []
|
|
422
|
+
) -> list[ResponseFunctionCallArgumentsDeltaEvent | ResponseOutputItemAddedEvent]:
|
|
423
|
+
"""Map FunctionCallContent to OpenAI events following Responses API spec.
|
|
323
424
|
|
|
324
|
-
|
|
325
|
-
|
|
425
|
+
Agent Framework emits FunctionCallContent in two patterns:
|
|
426
|
+
1. First event: call_id + name + empty/no arguments
|
|
427
|
+
2. Subsequent events: empty call_id/name + argument chunks
|
|
326
428
|
|
|
327
|
-
|
|
328
|
-
for
|
|
429
|
+
We emit:
|
|
430
|
+
1. response.output_item.added (with full metadata) for the first event
|
|
431
|
+
2. response.function_call_arguments.delta (referencing item_id) for chunks
|
|
432
|
+
"""
|
|
433
|
+
events: list[ResponseFunctionCallArgumentsDeltaEvent | ResponseOutputItemAddedEvent] = []
|
|
434
|
+
|
|
435
|
+
# CASE 1: New function call (has call_id and name)
|
|
436
|
+
# This is the first event that establishes the function call
|
|
437
|
+
if content.call_id and content.name:
|
|
438
|
+
# Use call_id as item_id (simpler, and call_id uniquely identifies the call)
|
|
439
|
+
item_id = content.call_id
|
|
440
|
+
|
|
441
|
+
# Track this function call for later argument deltas
|
|
442
|
+
context["active_function_calls"][content.call_id] = {
|
|
443
|
+
"item_id": item_id,
|
|
444
|
+
"name": content.name,
|
|
445
|
+
"arguments_chunks": [],
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
logger.debug(f"New function call: {content.name} (call_id={content.call_id})")
|
|
449
|
+
|
|
450
|
+
# Emit response.output_item.added event per OpenAI spec
|
|
329
451
|
events.append(
|
|
330
|
-
|
|
331
|
-
type="response.
|
|
332
|
-
|
|
333
|
-
|
|
452
|
+
ResponseOutputItemAddedEvent(
|
|
453
|
+
type="response.output_item.added",
|
|
454
|
+
item=ResponseFunctionToolCall(
|
|
455
|
+
id=content.call_id, # Use call_id as the item id
|
|
456
|
+
call_id=content.call_id,
|
|
457
|
+
name=content.name,
|
|
458
|
+
arguments="", # Empty initially, will be filled by deltas
|
|
459
|
+
type="function_call",
|
|
460
|
+
status="in_progress",
|
|
461
|
+
),
|
|
334
462
|
output_index=context["output_index"],
|
|
335
463
|
sequence_number=self._next_sequence(context),
|
|
336
464
|
)
|
|
337
465
|
)
|
|
338
466
|
|
|
467
|
+
# CASE 2: Argument deltas (content has arguments, possibly without call_id/name)
|
|
468
|
+
if content.arguments:
|
|
469
|
+
# Find the active function call for these arguments
|
|
470
|
+
active_call = self._get_active_function_call(content, context)
|
|
471
|
+
|
|
472
|
+
if active_call:
|
|
473
|
+
item_id = active_call["item_id"]
|
|
474
|
+
|
|
475
|
+
# Convert arguments to string if it's a dict (Agent Framework may send either)
|
|
476
|
+
delta_str = content.arguments if isinstance(content.arguments, str) else json.dumps(content.arguments)
|
|
477
|
+
|
|
478
|
+
# Emit argument delta referencing the item_id
|
|
479
|
+
events.append(
|
|
480
|
+
ResponseFunctionCallArgumentsDeltaEvent(
|
|
481
|
+
type="response.function_call_arguments.delta",
|
|
482
|
+
delta=delta_str,
|
|
483
|
+
item_id=item_id,
|
|
484
|
+
output_index=context["output_index"],
|
|
485
|
+
sequence_number=self._next_sequence(context),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Track chunk for debugging
|
|
490
|
+
active_call["arguments_chunks"].append(delta_str)
|
|
491
|
+
else:
|
|
492
|
+
logger.warning(f"Received function call arguments without active call: {content.arguments[:50]}...")
|
|
493
|
+
|
|
339
494
|
return events
|
|
340
495
|
|
|
496
|
+
def _get_active_function_call(self, content: Any, context: dict[str, Any]) -> dict[str, Any] | None:
|
|
497
|
+
"""Find the active function call for this content.
|
|
498
|
+
|
|
499
|
+
Uses call_id if present, otherwise falls back to most recent call.
|
|
500
|
+
Necessary because Agent Framework may send argument chunks without call_id.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
content: FunctionCallContent with possible call_id
|
|
504
|
+
context: Conversion context with active_function_calls
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Active call dict or None
|
|
508
|
+
"""
|
|
509
|
+
active_calls: dict[str, dict[str, Any]] = context["active_function_calls"]
|
|
510
|
+
|
|
511
|
+
# If content has call_id, use it to find the exact call
|
|
512
|
+
if hasattr(content, "call_id") and content.call_id:
|
|
513
|
+
result = active_calls.get(content.call_id)
|
|
514
|
+
return result if result is not None else None
|
|
515
|
+
|
|
516
|
+
# Otherwise, use the most recent call (last one added)
|
|
517
|
+
# This handles the case where Agent Framework sends argument chunks
|
|
518
|
+
# without call_id in subsequent events
|
|
519
|
+
if active_calls:
|
|
520
|
+
return list(active_calls.values())[-1]
|
|
521
|
+
|
|
522
|
+
return None
|
|
523
|
+
|
|
341
524
|
async def _map_function_result_content(
|
|
342
525
|
self, content: Any, context: dict[str, Any]
|
|
343
526
|
) -> ResponseFunctionResultComplete:
|
|
344
|
-
"""Map FunctionResultContent to
|
|
527
|
+
"""Map FunctionResultContent to DevUI custom event.
|
|
528
|
+
|
|
529
|
+
DevUI extension: The OpenAI Responses API doesn't stream function execution results
|
|
530
|
+
(in OpenAI's model, the application executes functions, not the API).
|
|
531
|
+
"""
|
|
532
|
+
# Get call_id from content
|
|
533
|
+
call_id = getattr(content, "call_id", None)
|
|
534
|
+
if not call_id:
|
|
535
|
+
call_id = f"call_{uuid.uuid4().hex[:8]}"
|
|
536
|
+
|
|
537
|
+
# Extract result
|
|
538
|
+
result = getattr(content, "result", None)
|
|
539
|
+
exception = getattr(content, "exception", None)
|
|
540
|
+
|
|
541
|
+
# Convert result to string
|
|
542
|
+
output = result if isinstance(result, str) else json.dumps(result) if result is not None else ""
|
|
543
|
+
|
|
544
|
+
# Determine status based on exception
|
|
545
|
+
status = "incomplete" if exception else "completed"
|
|
546
|
+
|
|
547
|
+
# Generate item_id
|
|
548
|
+
item_id = f"item_{uuid.uuid4().hex[:8]}"
|
|
549
|
+
|
|
550
|
+
# Return DevUI custom event
|
|
345
551
|
return ResponseFunctionResultComplete(
|
|
346
552
|
type="response.function_result.complete",
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
"exception": str(getattr(content, "exception", None)) if getattr(content, "exception", None) else None,
|
|
352
|
-
"timestamp": datetime.now().isoformat(),
|
|
353
|
-
},
|
|
354
|
-
call_id=getattr(content, "call_id", f"call_{uuid.uuid4().hex[:8]}"),
|
|
355
|
-
item_id=context["item_id"],
|
|
553
|
+
call_id=call_id,
|
|
554
|
+
output=output,
|
|
555
|
+
status=status,
|
|
556
|
+
item_id=item_id,
|
|
356
557
|
output_index=context["output_index"],
|
|
357
558
|
sequence_number=self._next_sequence(context),
|
|
358
559
|
)
|
|
@@ -367,37 +568,34 @@ class MessageMapper:
|
|
|
367
568
|
sequence_number=self._next_sequence(context),
|
|
368
569
|
)
|
|
369
570
|
|
|
370
|
-
async def _map_usage_content(self, content: Any, context: dict[str, Any]) ->
|
|
371
|
-
"""
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
context["usage_data"].append(content)
|
|
571
|
+
async def _map_usage_content(self, content: Any, context: dict[str, Any]) -> None:
|
|
572
|
+
"""Accumulate usage data for final Response.usage field.
|
|
573
|
+
|
|
574
|
+
OpenAI does NOT stream usage events. Usage appears only in final Response.
|
|
575
|
+
This method accumulates usage data per request for later inclusion in Response.usage.
|
|
376
576
|
|
|
577
|
+
Returns:
|
|
578
|
+
None - no event emitted (usage goes in final Response.usage)
|
|
579
|
+
"""
|
|
377
580
|
# Extract usage from UsageContent.details (UsageDetails object)
|
|
378
581
|
details = getattr(content, "details", None)
|
|
379
|
-
total_tokens = 0
|
|
380
|
-
prompt_tokens = 0
|
|
381
|
-
completion_tokens = 0
|
|
582
|
+
total_tokens = getattr(details, "total_token_count", 0) or 0
|
|
583
|
+
prompt_tokens = getattr(details, "input_token_count", 0) or 0
|
|
584
|
+
completion_tokens = getattr(details, "output_token_count", 0) or 0
|
|
382
585
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
586
|
+
# Accumulate for final Response.usage
|
|
587
|
+
request_id = context.get("request_id", "default")
|
|
588
|
+
if request_id not in self._usage_accumulator:
|
|
589
|
+
self._usage_accumulator[request_id] = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
|
|
387
590
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
},
|
|
397
|
-
item_id=context["item_id"],
|
|
398
|
-
output_index=context["output_index"],
|
|
399
|
-
sequence_number=self._next_sequence(context),
|
|
400
|
-
)
|
|
591
|
+
self._usage_accumulator[request_id]["input_tokens"] += prompt_tokens
|
|
592
|
+
self._usage_accumulator[request_id]["output_tokens"] += completion_tokens
|
|
593
|
+
self._usage_accumulator[request_id]["total_tokens"] += total_tokens
|
|
594
|
+
|
|
595
|
+
logger.debug(f"Accumulated usage for {request_id}: {self._usage_accumulator[request_id]}")
|
|
596
|
+
|
|
597
|
+
# NO EVENT RETURNED - usage goes in final Response only
|
|
598
|
+
return
|
|
401
599
|
|
|
402
600
|
async def _map_data_content(self, content: Any, context: dict[str, Any]) -> ResponseTraceEventComplete:
|
|
403
601
|
"""Map DataContent to structured trace event."""
|
|
@@ -462,15 +660,24 @@ class MessageMapper:
|
|
|
462
660
|
|
|
463
661
|
async def _map_approval_request_content(self, content: Any, context: dict[str, Any]) -> dict[str, Any]:
|
|
464
662
|
"""Map FunctionApprovalRequestContent to custom event."""
|
|
663
|
+
# Parse arguments to ensure they're always a dict, not a JSON string
|
|
664
|
+
# This prevents double-escaping when the frontend calls JSON.stringify()
|
|
665
|
+
arguments: dict[str, Any] = {}
|
|
666
|
+
if hasattr(content, "function_call"):
|
|
667
|
+
if hasattr(content.function_call, "parse_arguments"):
|
|
668
|
+
# Use parse_arguments() to convert string arguments to dict
|
|
669
|
+
arguments = content.function_call.parse_arguments() or {}
|
|
670
|
+
else:
|
|
671
|
+
# Fallback to direct access if parse_arguments doesn't exist
|
|
672
|
+
arguments = getattr(content.function_call, "arguments", {})
|
|
673
|
+
|
|
465
674
|
return {
|
|
466
675
|
"type": "response.function_approval.requested",
|
|
467
676
|
"request_id": getattr(content, "id", "unknown"),
|
|
468
677
|
"function_call": {
|
|
469
678
|
"id": getattr(content.function_call, "call_id", "") if hasattr(content, "function_call") else "",
|
|
470
679
|
"name": getattr(content.function_call, "name", "") if hasattr(content, "function_call") else "",
|
|
471
|
-
"arguments":
|
|
472
|
-
if hasattr(content, "function_call")
|
|
473
|
-
else {},
|
|
680
|
+
"arguments": arguments,
|
|
474
681
|
},
|
|
475
682
|
"item_id": context["item_id"],
|
|
476
683
|
"output_index": context["output_index"],
|
|
@@ -510,19 +717,15 @@ class MessageMapper:
|
|
|
510
717
|
|
|
511
718
|
async def _create_unknown_event(self, event_data: Any, context: dict[str, Any]) -> ResponseStreamEvent:
|
|
512
719
|
"""Create event for unknown event types."""
|
|
513
|
-
text = f"Unknown event: {event_data!s}
|
|
720
|
+
text = f"Unknown event: {event_data!s}\n"
|
|
514
721
|
return self._create_text_delta_event(text, context)
|
|
515
722
|
|
|
516
723
|
async def _create_unknown_content_event(self, content: Any, context: dict[str, Any]) -> ResponseStreamEvent:
|
|
517
724
|
"""Create event for unknown content types."""
|
|
518
725
|
content_type = content.__class__.__name__
|
|
519
|
-
text = f"⚠️ Unknown content type: {content_type}
|
|
726
|
+
text = f"⚠️ Unknown content type: {content_type}\n"
|
|
520
727
|
return self._create_text_delta_event(text, context)
|
|
521
728
|
|
|
522
|
-
def _chunk_json_string(self, json_str: str, chunk_size: int = 50) -> list[str]:
|
|
523
|
-
"""Chunk JSON string for streaming."""
|
|
524
|
-
return [json_str[i : i + chunk_size] for i in range(0, len(json_str), chunk_size)]
|
|
525
|
-
|
|
526
729
|
async def _create_error_response(self, error_message: str, request: AgentFrameworkRequest) -> OpenAIResponse:
|
|
527
730
|
"""Create error response."""
|
|
528
731
|
error_text = f"Error: {error_message}"
|