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.

@@ -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
 
@@ -406,34 +220,51 @@ class AgentFrameworkExecutor:
406
220
  # Convert input to proper ChatMessage or string
407
221
  user_message = self._convert_input_to_chat_message(request.input)
408
222
 
409
- # Get thread if provided in extra_body
223
+ # Get thread from conversation parameter (OpenAI standard!)
410
224
  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)
225
+ conversation_id = request.get_conversation_id()
226
+ if conversation_id:
227
+ thread = self.conversation_store.get_thread(conversation_id)
414
228
  if thread:
415
- logger.debug(f"Using existing thread: {thread_id}")
229
+ logger.debug(f"Using existing conversation: {conversation_id}")
416
230
  else:
417
- logger.warning(f"Thread {thread_id} not found, proceeding without thread")
231
+ logger.warning(f"Conversation {conversation_id} not found, proceeding without thread")
418
232
 
419
233
  if isinstance(user_message, str):
420
234
  logger.debug(f"Executing agent with text input: {user_message[:100]}...")
421
235
  else:
422
236
  logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}")
237
+ # Check if agent supports streaming
238
+ if hasattr(agent, "run_stream") and callable(agent.run_stream):
239
+ # Use Agent Framework's native streaming with optional thread
240
+ if thread:
241
+ async for update in agent.run_stream(user_message, thread=thread):
242
+ for trace_event in trace_collector.get_pending_events():
243
+ yield trace_event
423
244
 
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
245
+ yield update
246
+ else:
247
+ async for update in agent.run_stream(user_message):
248
+ for trace_event in trace_collector.get_pending_events():
249
+ yield trace_event
250
+
251
+ yield update
252
+ elif hasattr(agent, "run") and callable(agent.run):
253
+ # Non-streaming agent - use run() and yield complete response
254
+ logger.info("Agent lacks run_stream(), using run() method (non-streaming)")
255
+ if thread:
256
+ response = await agent.run(user_message, thread=thread)
257
+ else:
258
+ response = await agent.run(user_message)
429
259
 
430
- yield update
431
- 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
260
+ # Yield trace events before response
261
+ for trace_event in trace_collector.get_pending_events():
262
+ yield trace_event
435
263
 
436
- yield update
264
+ # Yield the complete response (mapper will convert to streaming events)
265
+ yield response
266
+ else:
267
+ raise ValueError("Agent must implement either run() or run_stream() method")
437
268
 
438
269
  except Exception as e:
439
270
  logger.error(f"Error in agent execution: {e}")
@@ -455,8 +286,8 @@ class AgentFrameworkExecutor:
455
286
  try:
456
287
  # Get input data - prefer structured data from extra_body
457
288
  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
289
+ if request.extra_body and isinstance(request.extra_body, dict) and request.extra_body.get("input_data"):
290
+ input_data = request.extra_body.get("input_data") # type: ignore
460
291
  logger.debug(f"Using structured input_data from extra_body: {type(input_data)}")
461
292
  else:
462
293
  input_data = request.input
@@ -483,6 +314,9 @@ class AgentFrameworkExecutor:
483
314
  def _convert_input_to_chat_message(self, input_data: Any) -> Any:
484
315
  """Convert OpenAI Responses API input to Agent Framework ChatMessage or string.
485
316
 
317
+ Handles various input formats including text, images, files, and multimodal content.
318
+ Falls back to string extraction for simple cases.
319
+
486
320
  Args:
487
321
  input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam])
488
322
 
@@ -512,6 +346,9 @@ class AgentFrameworkExecutor:
512
346
  ) -> Any:
513
347
  """Convert OpenAI ResponseInputParam to Agent Framework ChatMessage.
514
348
 
349
+ Processes text, images, files, and other content types from OpenAI format
350
+ to Agent Framework ChatMessage with appropriate content objects.
351
+
515
352
  Args:
516
353
  input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects)
517
354
  ChatMessage: ChatMessage class for creating chat messages
@@ -597,6 +434,40 @@ class AgentFrameworkExecutor:
597
434
  elif file_url:
598
435
  contents.append(DataContent(uri=file_url, media_type=media_type))
599
436
 
437
+ elif content_type == "function_approval_response":
438
+ # Handle function approval response (DevUI extension)
439
+ try:
440
+ from agent_framework import FunctionApprovalResponseContent, FunctionCallContent
441
+
442
+ request_id = content_item.get("request_id", "")
443
+ approved = content_item.get("approved", False)
444
+ function_call_data = content_item.get("function_call", {})
445
+
446
+ # Create FunctionCallContent from the function_call data
447
+ function_call = FunctionCallContent(
448
+ call_id=function_call_data.get("id", ""),
449
+ name=function_call_data.get("name", ""),
450
+ arguments=function_call_data.get("arguments", {}),
451
+ )
452
+
453
+ # Create FunctionApprovalResponseContent with correct signature
454
+ approval_response = FunctionApprovalResponseContent(
455
+ approved, # positional argument
456
+ id=request_id, # keyword argument 'id', NOT 'request_id'
457
+ function_call=function_call, # FunctionCallContent object
458
+ )
459
+ contents.append(approval_response)
460
+ logger.info(
461
+ f"Added FunctionApprovalResponseContent: id={request_id}, "
462
+ f"approved={approved}, call_id={function_call.call_id}"
463
+ )
464
+ except ImportError:
465
+ logger.warning(
466
+ "FunctionApprovalResponseContent not available in agent_framework"
467
+ )
468
+ except Exception as e:
469
+ logger.error(f"Failed to create FunctionApprovalResponseContent: {e}")
470
+
600
471
  # Handle other OpenAI input item types as needed
601
472
  # (tool calls, function results, etc.)
602
473
 
@@ -687,23 +558,6 @@ class AgentFrameworkExecutor:
687
558
 
688
559
  return start_executor, message_types
689
560
 
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
561
  def _parse_structured_workflow_input(self, workflow: Any, input_data: dict[str, Any]) -> Any:
708
562
  """Parse structured input data for workflow execution.
709
563
 
@@ -728,7 +582,9 @@ class AgentFrameworkExecutor:
728
582
  return input_data
729
583
 
730
584
  # Get the first (primary) input type
731
- input_type = self._select_primary_input_type(message_types)
585
+ from ._utils import select_primary_input_type
586
+
587
+ input_type = select_primary_input_type(message_types)
732
588
  if input_type is None:
733
589
  logger.debug("Could not select primary input type for workflow - using raw dict")
734
590
  return input_data
@@ -764,7 +620,9 @@ class AgentFrameworkExecutor:
764
620
  return raw_input
765
621
 
766
622
  # Get the first (primary) input type
767
- input_type = self._select_primary_input_type(message_types)
623
+ from ._utils import select_primary_input_type
624
+
625
+ input_type = select_primary_input_type(message_types)
768
626
  if input_type is None:
769
627
  logger.debug("Could not select primary input type for workflow - using raw string")
770
628
  return raw_input