agent-framework-devui 1.0.0b251007__py3-none-any.whl → 1.0.0b251028__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.

@@ -5,12 +5,12 @@
5
5
  import json
6
6
  import logging
7
7
  import os
8
- import uuid
9
8
  from collections.abc import AsyncGenerator
10
- from typing import Any, get_origin
9
+ from typing import Any
11
10
 
12
- from agent_framework import AgentThread
11
+ from agent_framework import AgentProtocol
13
12
 
13
+ from ._conversations import ConversationStore, InMemoryConversationStore
14
14
  from ._discovery import EntityDiscovery
15
15
  from ._mapper import MessageMapper
16
16
  from ._tracing import capture_traces
@@ -29,21 +29,26 @@ class EntityNotFoundError(Exception):
29
29
  class AgentFrameworkExecutor:
30
30
  """Executor for Agent Framework entities - agents and workflows."""
31
31
 
32
- def __init__(self, entity_discovery: EntityDiscovery, message_mapper: MessageMapper):
32
+ def __init__(
33
+ self,
34
+ entity_discovery: EntityDiscovery,
35
+ message_mapper: MessageMapper,
36
+ conversation_store: ConversationStore | None = None,
37
+ ):
33
38
  """Initialize Agent Framework executor.
34
39
 
35
40
  Args:
36
41
  entity_discovery: Entity discovery instance
37
42
  message_mapper: Message mapper instance
43
+ conversation_store: Optional conversation store (defaults to in-memory)
38
44
  """
39
45
  self.entity_discovery = entity_discovery
40
46
  self.message_mapper = message_mapper
41
47
  self._setup_tracing_provider()
42
48
  self._setup_agent_framework_tracing()
43
49
 
44
- # Minimal thread storage - no metadata needed
45
- self.thread_storage: dict[str, AgentThread] = {}
46
- self.agent_threads: dict[str, list[str]] = {} # agent_id -> thread_ids
50
+ # Use provided conversation store or default to in-memory
51
+ self.conversation_store = conversation_store or InMemoryConversationStore()
47
52
 
48
53
  def _setup_tracing_provider(self) -> None:
49
54
  """Set up our own TracerProvider so we can add processors."""
@@ -83,199 +88,6 @@ class AgentFrameworkExecutor:
83
88
  else:
84
89
  logger.debug("ENABLE_OTEL not set, skipping observability setup")
85
90
 
86
- # Thread Management Methods
87
- def create_thread(self, agent_id: str) -> str:
88
- """Create new thread for agent."""
89
- thread_id = f"thread_{uuid.uuid4().hex[:8]}"
90
- thread = AgentThread()
91
-
92
- self.thread_storage[thread_id] = thread
93
-
94
- if agent_id not in self.agent_threads:
95
- self.agent_threads[agent_id] = []
96
- self.agent_threads[agent_id].append(thread_id)
97
-
98
- return thread_id
99
-
100
- def get_thread(self, thread_id: str) -> AgentThread | None:
101
- """Get AgentThread by ID."""
102
- return self.thread_storage.get(thread_id)
103
-
104
- def list_threads_for_agent(self, agent_id: str) -> list[str]:
105
- """List thread IDs for agent."""
106
- return self.agent_threads.get(agent_id, [])
107
-
108
- def get_agent_for_thread(self, thread_id: str) -> str | None:
109
- """Find which agent owns this thread."""
110
- for agent_id, thread_ids in self.agent_threads.items():
111
- if thread_id in thread_ids:
112
- return agent_id
113
- return None
114
-
115
- def delete_thread(self, thread_id: str) -> bool:
116
- """Delete thread."""
117
- if thread_id not in self.thread_storage:
118
- return False
119
-
120
- for _agent_id, thread_ids in self.agent_threads.items():
121
- if thread_id in thread_ids:
122
- thread_ids.remove(thread_id)
123
- break
124
-
125
- del self.thread_storage[thread_id]
126
- return True
127
-
128
- async def get_thread_messages(self, thread_id: str) -> list[dict[str, Any]]:
129
- """Get messages from a thread's message store, preserving all content types for UI display."""
130
- thread = self.get_thread(thread_id)
131
- if not thread or not thread.message_store:
132
- return []
133
-
134
- try:
135
- # Get AgentFramework ChatMessage objects from thread
136
- af_messages = await thread.message_store.list_messages()
137
-
138
- ui_messages = []
139
- for i, af_msg in enumerate(af_messages):
140
- # Extract role value (handle enum)
141
- role = af_msg.role.value if hasattr(af_msg.role, "value") else str(af_msg.role)
142
-
143
- # Skip tool/function messages - only show user and assistant messages
144
- if role not in ["user", "assistant"]:
145
- continue
146
-
147
- # Extract all user-facing content (text, images, files, etc.)
148
- display_contents = self._extract_display_contents(af_msg.contents)
149
-
150
- # Skip messages with no displayable content
151
- if not display_contents:
152
- continue
153
-
154
- # Extract usage information if present
155
- usage_data = None
156
- for content in af_msg.contents:
157
- content_type = getattr(content, "type", None)
158
- if content_type == "usage":
159
- details = getattr(content, "details", None)
160
- if details:
161
- usage_data = {
162
- "total_tokens": getattr(details, "total_token_count", 0) or 0,
163
- "prompt_tokens": getattr(details, "input_token_count", 0) or 0,
164
- "completion_tokens": getattr(details, "output_token_count", 0) or 0,
165
- }
166
- break
167
-
168
- ui_message = {
169
- "id": af_msg.message_id or f"restored-{i}",
170
- "role": role,
171
- "contents": display_contents,
172
- "timestamp": __import__("datetime").datetime.now().isoformat(),
173
- "author_name": af_msg.author_name,
174
- "message_id": af_msg.message_id,
175
- }
176
-
177
- # Add usage data if available
178
- if usage_data:
179
- ui_message["usage"] = usage_data
180
-
181
- ui_messages.append(ui_message)
182
-
183
- logger.info(f"Restored {len(ui_messages)} display messages for thread {thread_id}")
184
- return ui_messages
185
-
186
- except Exception as e:
187
- logger.error(f"Error getting thread messages: {e}")
188
- import traceback
189
-
190
- logger.error(traceback.format_exc())
191
- return []
192
-
193
- def _extract_display_contents(self, contents: list[Any]) -> list[dict[str, Any]]:
194
- """Extract all user-facing content (text, images, files, etc.) from message contents.
195
-
196
- Filters out internal mechanics like function calls/results while preserving
197
- all content types that should be displayed in the UI.
198
- """
199
- display_contents = []
200
-
201
- for content in contents:
202
- content_type = getattr(content, "type", None)
203
-
204
- # Text content
205
- if content_type == "text":
206
- text = getattr(content, "text", "")
207
-
208
- # Handle double-encoded JSON from user messages
209
- if text.startswith('{"role":'):
210
- try:
211
- import json
212
-
213
- parsed = json.loads(text)
214
- if parsed.get("contents"):
215
- for sub_content in parsed["contents"]:
216
- if sub_content.get("type") == "text":
217
- display_contents.append({"type": "text", "text": sub_content.get("text", "")})
218
- except Exception:
219
- display_contents.append({"type": "text", "text": text})
220
- else:
221
- display_contents.append({"type": "text", "text": text})
222
-
223
- # Data content (images, files, PDFs, etc.)
224
- elif content_type == "data":
225
- display_contents.append({
226
- "type": "data",
227
- "uri": getattr(content, "uri", ""),
228
- "media_type": getattr(content, "media_type", None),
229
- })
230
-
231
- # URI content (external links to images/files)
232
- elif content_type == "uri":
233
- display_contents.append({
234
- "type": "uri",
235
- "uri": getattr(content, "uri", ""),
236
- "media_type": getattr(content, "media_type", None),
237
- })
238
-
239
- # Skip function_call, function_result, and other internal content types
240
-
241
- return display_contents
242
-
243
- async def serialize_thread(self, thread_id: str) -> dict[str, Any] | None:
244
- """Serialize thread state for persistence."""
245
- thread = self.get_thread(thread_id)
246
- if not thread:
247
- return None
248
-
249
- try:
250
- # Use AgentThread's built-in serialization
251
- serialized_state = await thread.serialize()
252
-
253
- # Add our metadata
254
- agent_id = self.get_agent_for_thread(thread_id)
255
- serialized_state["metadata"] = {"agent_id": agent_id, "thread_id": thread_id}
256
-
257
- return serialized_state
258
-
259
- except Exception as e:
260
- logger.error(f"Error serializing thread {thread_id}: {e}")
261
- return None
262
-
263
- async def deserialize_thread(self, thread_id: str, agent_id: str, serialized_state: dict[str, Any]) -> bool:
264
- """Deserialize thread state from persistence."""
265
- try:
266
- thread = await AgentThread.deserialize(serialized_state)
267
- # Store the restored thread
268
- self.thread_storage[thread_id] = thread
269
- if agent_id not in self.agent_threads:
270
- self.agent_threads[agent_id] = []
271
- self.agent_threads[agent_id].append(thread_id)
272
-
273
- return True
274
-
275
- except Exception as e:
276
- logger.error(f"Error deserializing thread {thread_id}: {e}")
277
- return False
278
-
279
91
  async def discover_entities(self) -> list[EntityInfo]:
280
92
  """Discover all available entities.
281
93
 
@@ -357,9 +169,11 @@ class AgentFrameworkExecutor:
357
169
  Raw Agent Framework events and trace events
358
170
  """
359
171
  try:
360
- # Get entity info and object
172
+ # Get entity info
361
173
  entity_info = self.get_entity_info(entity_id)
362
- entity_obj = self.entity_discovery.get_entity_object(entity_id)
174
+
175
+ # Trigger lazy loading (will return from cache if already loaded)
176
+ entity_obj = await self.entity_discovery.load_entity(entity_id)
363
177
 
364
178
  if not entity_obj:
365
179
  raise EntityNotFoundError(f"Entity object for '{entity_id}' not found")
@@ -390,7 +204,7 @@ class AgentFrameworkExecutor:
390
204
  yield {"type": "error", "message": str(e), "entity_id": entity_id}
391
205
 
392
206
  async def _execute_agent(
393
- self, agent: Any, request: AgentFrameworkRequest, trace_collector: Any
207
+ self, agent: AgentProtocol, request: AgentFrameworkRequest, trace_collector: Any
394
208
  ) -> AsyncGenerator[Any, None]:
395
209
  """Execute Agent Framework agent with trace collection and optional thread support.
396
210
 
@@ -403,40 +217,73 @@ class AgentFrameworkExecutor:
403
217
  Agent update events and trace events
404
218
  """
405
219
  try:
220
+ # Emit agent lifecycle start event
221
+ from .models._openai_custom import AgentStartedEvent
222
+
223
+ yield AgentStartedEvent()
224
+
406
225
  # Convert input to proper ChatMessage or string
407
226
  user_message = self._convert_input_to_chat_message(request.input)
408
227
 
409
- # Get thread if provided in extra_body
228
+ # Get thread from conversation parameter (OpenAI standard!)
410
229
  thread = None
411
- if request.extra_body and hasattr(request.extra_body, "thread_id") and request.extra_body.thread_id:
412
- thread_id = request.extra_body.thread_id
413
- thread = self.get_thread(thread_id)
230
+ conversation_id = request.get_conversation_id()
231
+ if conversation_id:
232
+ thread = self.conversation_store.get_thread(conversation_id)
414
233
  if thread:
415
- logger.debug(f"Using existing thread: {thread_id}")
234
+ logger.debug(f"Using existing conversation: {conversation_id}")
416
235
  else:
417
- logger.warning(f"Thread {thread_id} not found, proceeding without thread")
236
+ logger.warning(f"Conversation {conversation_id} not found, proceeding without thread")
418
237
 
419
238
  if isinstance(user_message, str):
420
239
  logger.debug(f"Executing agent with text input: {user_message[:100]}...")
421
240
  else:
422
241
  logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}")
242
+ # Check if agent supports streaming
243
+ if hasattr(agent, "run_stream") and callable(agent.run_stream):
244
+ # Use Agent Framework's native streaming with optional thread
245
+ if thread:
246
+ async for update in agent.run_stream(user_message, thread=thread):
247
+ for trace_event in trace_collector.get_pending_events():
248
+ yield trace_event
249
+
250
+ yield update
251
+ else:
252
+ async for update in agent.run_stream(user_message):
253
+ for trace_event in trace_collector.get_pending_events():
254
+ yield trace_event
255
+
256
+ yield update
257
+ elif hasattr(agent, "run") and callable(agent.run):
258
+ # Non-streaming agent - use run() and yield complete response
259
+ logger.info("Agent lacks run_stream(), using run() method (non-streaming)")
260
+ if thread:
261
+ response = await agent.run(user_message, thread=thread)
262
+ else:
263
+ response = await agent.run(user_message)
423
264
 
424
- # Use Agent Framework's native streaming with optional thread
425
- if thread:
426
- async for update in agent.run_stream(user_message, thread=thread):
427
- for trace_event in trace_collector.get_pending_events():
428
- yield trace_event
265
+ # Yield trace events before response
266
+ for trace_event in trace_collector.get_pending_events():
267
+ yield trace_event
429
268
 
430
- yield update
269
+ # Yield the complete response (mapper will convert to streaming events)
270
+ yield response
431
271
  else:
432
- async for update in agent.run_stream(user_message):
433
- for trace_event in trace_collector.get_pending_events():
434
- yield trace_event
272
+ raise ValueError("Agent must implement either run() or run_stream() method")
435
273
 
436
- yield update
274
+ # Emit agent lifecycle completion event
275
+ from .models._openai_custom import AgentCompletedEvent
276
+
277
+ yield AgentCompletedEvent()
437
278
 
438
279
  except Exception as e:
439
280
  logger.error(f"Error in agent execution: {e}")
281
+ # Emit agent lifecycle failure event
282
+ from .models._openai_custom import AgentFailedEvent
283
+
284
+ yield AgentFailedEvent(error=e)
285
+
286
+ # Still yield the error for backward compatibility
440
287
  yield {"type": "error", "message": f"Agent execution error: {e!s}"}
441
288
 
442
289
  async def _execute_workflow(
@@ -453,14 +300,9 @@ class AgentFrameworkExecutor:
453
300
  Workflow events and trace events
454
301
  """
455
302
  try:
456
- # Get input data - prefer structured data from extra_body
457
- input_data: str | list[Any] | dict[str, Any]
458
- if request.extra_body and hasattr(request.extra_body, "input_data") and request.extra_body.input_data:
459
- input_data = request.extra_body.input_data
460
- logger.debug(f"Using structured input_data from extra_body: {type(input_data)}")
461
- else:
462
- input_data = request.input
463
- logger.debug(f"Using input field as fallback: {type(input_data)}")
303
+ # Get input data directly from request.input field
304
+ input_data = request.input
305
+ logger.debug(f"Using input field: {type(input_data)}")
464
306
 
465
307
  # Parse input based on workflow's expected input type
466
308
  parsed_input = await self._parse_workflow_input(workflow, input_data)
@@ -483,6 +325,9 @@ class AgentFrameworkExecutor:
483
325
  def _convert_input_to_chat_message(self, input_data: Any) -> Any:
484
326
  """Convert OpenAI Responses API input to Agent Framework ChatMessage or string.
485
327
 
328
+ Handles various input formats including text, images, files, and multimodal content.
329
+ Falls back to string extraction for simple cases.
330
+
486
331
  Args:
487
332
  input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam])
488
333
 
@@ -512,6 +357,9 @@ class AgentFrameworkExecutor:
512
357
  ) -> Any:
513
358
  """Convert OpenAI ResponseInputParam to Agent Framework ChatMessage.
514
359
 
360
+ Processes text, images, files, and other content types from OpenAI format
361
+ to Agent Framework ChatMessage with appropriate content objects.
362
+
515
363
  Args:
516
364
  input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects)
517
365
  ChatMessage: ChatMessage class for creating chat messages
@@ -597,6 +445,40 @@ class AgentFrameworkExecutor:
597
445
  elif file_url:
598
446
  contents.append(DataContent(uri=file_url, media_type=media_type))
599
447
 
448
+ elif content_type == "function_approval_response":
449
+ # Handle function approval response (DevUI extension)
450
+ try:
451
+ from agent_framework import FunctionApprovalResponseContent, FunctionCallContent
452
+
453
+ request_id = content_item.get("request_id", "")
454
+ approved = content_item.get("approved", False)
455
+ function_call_data = content_item.get("function_call", {})
456
+
457
+ # Create FunctionCallContent from the function_call data
458
+ function_call = FunctionCallContent(
459
+ call_id=function_call_data.get("id", ""),
460
+ name=function_call_data.get("name", ""),
461
+ arguments=function_call_data.get("arguments", {}),
462
+ )
463
+
464
+ # Create FunctionApprovalResponseContent with correct signature
465
+ approval_response = FunctionApprovalResponseContent(
466
+ approved, # positional argument
467
+ id=request_id, # keyword argument 'id', NOT 'request_id'
468
+ function_call=function_call, # FunctionCallContent object
469
+ )
470
+ contents.append(approval_response)
471
+ logger.info(
472
+ f"Added FunctionApprovalResponseContent: id={request_id}, "
473
+ f"approved={approved}, call_id={function_call.call_id}"
474
+ )
475
+ except ImportError:
476
+ logger.warning(
477
+ "FunctionApprovalResponseContent not available in agent_framework"
478
+ )
479
+ except Exception as e:
480
+ logger.error(f"Failed to create FunctionApprovalResponseContent: {e}")
481
+
600
482
  # Handle other OpenAI input item types as needed
601
483
  # (tool calls, function results, etc.)
602
484
 
@@ -687,23 +569,6 @@ class AgentFrameworkExecutor:
687
569
 
688
570
  return start_executor, message_types
689
571
 
690
- def _select_primary_input_type(self, message_types: list[Any]) -> Any | None:
691
- """Choose the most user-friendly input type for workflow kick-off."""
692
- if not message_types:
693
- return None
694
-
695
- preferred = (str, dict)
696
-
697
- for candidate in preferred:
698
- for message_type in message_types:
699
- if message_type is candidate:
700
- return candidate
701
- origin = get_origin(message_type)
702
- if origin is candidate:
703
- return candidate
704
-
705
- return message_types[0]
706
-
707
572
  def _parse_structured_workflow_input(self, workflow: Any, input_data: dict[str, Any]) -> Any:
708
573
  """Parse structured input data for workflow execution.
709
574
 
@@ -728,7 +593,9 @@ class AgentFrameworkExecutor:
728
593
  return input_data
729
594
 
730
595
  # Get the first (primary) input type
731
- input_type = self._select_primary_input_type(message_types)
596
+ from ._utils import select_primary_input_type
597
+
598
+ input_type = select_primary_input_type(message_types)
732
599
  if input_type is None:
733
600
  logger.debug("Could not select primary input type for workflow - using raw dict")
734
601
  return input_data
@@ -764,7 +631,9 @@ class AgentFrameworkExecutor:
764
631
  return raw_input
765
632
 
766
633
  # Get the first (primary) input type
767
- input_type = self._select_primary_input_type(message_types)
634
+ from ._utils import select_primary_input_type
635
+
636
+ input_type = select_primary_input_type(message_types)
768
637
  if input_type is None:
769
638
  logger.debug("Could not select primary input type for workflow - using raw string")
770
639
  return raw_input