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.
- agent_framework_devui/_conversations.py +473 -0
- agent_framework_devui/_discovery.py +295 -325
- agent_framework_devui/_executor.py +115 -246
- agent_framework_devui/_mapper.py +747 -88
- agent_framework_devui/_server.py +275 -240
- agent_framework_devui/_utils.py +150 -1
- agent_framework_devui/models/__init__.py +21 -10
- agent_framework_devui/models/_discovery_models.py +1 -2
- agent_framework_devui/models/_openai_custom.py +103 -83
- agent_framework_devui/ui/assets/index-CE4pGoXh.css +1 -0
- agent_framework_devui/ui/assets/index-D_Y1oSGu.js +577 -0
- agent_framework_devui/ui/index.html +2 -2
- agent_framework_devui-1.0.0b251028.dist-info/METADATA +321 -0
- agent_framework_devui-1.0.0b251028.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.0b251028.dist-info}/WHEEL +0 -0
- {agent_framework_devui-1.0.0b251007.dist-info → agent_framework_devui-1.0.0b251028.dist-info}/entry_points.txt +0 -0
- {agent_framework_devui-1.0.0b251007.dist-info → agent_framework_devui-1.0.0b251028.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Conversation storage abstraction for OpenAI Conversations API.
|
|
4
|
+
|
|
5
|
+
This module provides a clean abstraction layer for managing conversations
|
|
6
|
+
while wrapping AgentFramework's AgentThread underneath.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Any, Literal, cast
|
|
13
|
+
|
|
14
|
+
from agent_framework import AgentThread, ChatMessage
|
|
15
|
+
from openai.types.conversations import Conversation, ConversationDeletedResource
|
|
16
|
+
from openai.types.conversations.conversation_item import ConversationItem
|
|
17
|
+
from openai.types.conversations.message import Message
|
|
18
|
+
from openai.types.conversations.text_content import TextContent
|
|
19
|
+
from openai.types.responses import (
|
|
20
|
+
ResponseFunctionToolCallItem,
|
|
21
|
+
ResponseFunctionToolCallOutputItem,
|
|
22
|
+
ResponseInputFile,
|
|
23
|
+
ResponseInputImage,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Type alias for OpenAI Message role literals
|
|
27
|
+
MessageRole = Literal["unknown", "user", "assistant", "system", "critic", "discriminator", "developer", "tool"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConversationStore(ABC):
|
|
31
|
+
"""Abstract base class for conversation storage.
|
|
32
|
+
|
|
33
|
+
Provides OpenAI Conversations API interface while managing
|
|
34
|
+
AgentThread instances underneath.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def create_conversation(self, metadata: dict[str, str] | None = None) -> Conversation:
|
|
39
|
+
"""Create a new conversation (wraps AgentThread creation).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
metadata: Optional metadata dict (e.g., {"agent_id": "weather_agent"})
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Conversation object with generated ID
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
51
|
+
"""Retrieve conversation metadata.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
conversation_id: Conversation ID
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Conversation object or None if not found
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def update_conversation(self, conversation_id: str, metadata: dict[str, str]) -> Conversation:
|
|
63
|
+
"""Update conversation metadata.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
conversation_id: Conversation ID
|
|
67
|
+
metadata: New metadata dict
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Updated Conversation object
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If conversation not found
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def delete_conversation(self, conversation_id: str) -> ConversationDeletedResource:
|
|
79
|
+
"""Delete conversation (including AgentThread).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
conversation_id: Conversation ID
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ConversationDeletedResource object
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If conversation not found
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
async def add_items(self, conversation_id: str, items: list[dict[str, Any]]) -> list[ConversationItem]:
|
|
94
|
+
"""Add items to conversation (syncs to AgentThread.message_store).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
conversation_id: Conversation ID
|
|
98
|
+
items: List of conversation items to add
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of added ConversationItem objects
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If conversation not found
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def list_items(
|
|
110
|
+
self, conversation_id: str, limit: int = 100, after: str | None = None, order: str = "asc"
|
|
111
|
+
) -> tuple[list[ConversationItem], bool]:
|
|
112
|
+
"""List conversation items from AgentThread.message_store.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
conversation_id: Conversation ID
|
|
116
|
+
limit: Maximum number of items to return
|
|
117
|
+
after: Cursor for pagination (item_id)
|
|
118
|
+
order: Sort order ("asc" or "desc")
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (items list, has_more boolean)
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If conversation not found
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def get_item(self, conversation_id: str, item_id: str) -> ConversationItem | None:
|
|
130
|
+
"""Get specific conversation item.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
conversation_id: Conversation ID
|
|
134
|
+
item_id: Item ID
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
ConversationItem or None if not found
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def get_thread(self, conversation_id: str) -> AgentThread | None:
|
|
143
|
+
"""Get underlying AgentThread for execution (internal use).
|
|
144
|
+
|
|
145
|
+
This is the critical method that allows the executor to get the
|
|
146
|
+
AgentThread for running agents with conversation context.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
conversation_id: Conversation ID
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
AgentThread object or None if not found
|
|
153
|
+
"""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]:
|
|
158
|
+
"""Filter conversations by metadata (e.g., agent_id).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
metadata_filter: Metadata key-value pairs to match
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of matching Conversation objects
|
|
165
|
+
"""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class InMemoryConversationStore(ConversationStore):
|
|
170
|
+
"""In-memory conversation storage wrapping AgentThread.
|
|
171
|
+
|
|
172
|
+
This implementation stores conversations in memory with their
|
|
173
|
+
underlying AgentThread instances for execution.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self) -> None:
|
|
177
|
+
"""Initialize in-memory conversation storage.
|
|
178
|
+
|
|
179
|
+
Storage structure maps conversation IDs to conversation data including
|
|
180
|
+
the underlying AgentThread, metadata, and cached ConversationItems.
|
|
181
|
+
"""
|
|
182
|
+
self._conversations: dict[str, dict[str, Any]] = {}
|
|
183
|
+
|
|
184
|
+
# Item index for O(1) lookup: {conversation_id: {item_id: ConversationItem}}
|
|
185
|
+
self._item_index: dict[str, dict[str, ConversationItem]] = {}
|
|
186
|
+
|
|
187
|
+
def create_conversation(self, metadata: dict[str, str] | None = None) -> Conversation:
|
|
188
|
+
"""Create a new conversation with underlying AgentThread."""
|
|
189
|
+
conv_id = f"conv_{uuid.uuid4().hex}"
|
|
190
|
+
created_at = int(time.time())
|
|
191
|
+
|
|
192
|
+
# Create AgentThread with default ChatMessageStore
|
|
193
|
+
thread = AgentThread()
|
|
194
|
+
|
|
195
|
+
self._conversations[conv_id] = {
|
|
196
|
+
"id": conv_id,
|
|
197
|
+
"thread": thread,
|
|
198
|
+
"metadata": metadata or {},
|
|
199
|
+
"created_at": created_at,
|
|
200
|
+
"items": [],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Initialize item index for this conversation
|
|
204
|
+
self._item_index[conv_id] = {}
|
|
205
|
+
|
|
206
|
+
return Conversation(id=conv_id, object="conversation", created_at=created_at, metadata=metadata)
|
|
207
|
+
|
|
208
|
+
def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
209
|
+
"""Retrieve conversation metadata."""
|
|
210
|
+
conv_data = self._conversations.get(conversation_id)
|
|
211
|
+
if not conv_data:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
return Conversation(
|
|
215
|
+
id=conv_data["id"],
|
|
216
|
+
object="conversation",
|
|
217
|
+
created_at=conv_data["created_at"],
|
|
218
|
+
metadata=conv_data.get("metadata"),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def update_conversation(self, conversation_id: str, metadata: dict[str, str]) -> Conversation:
|
|
222
|
+
"""Update conversation metadata."""
|
|
223
|
+
conv_data = self._conversations.get(conversation_id)
|
|
224
|
+
if not conv_data:
|
|
225
|
+
raise ValueError(f"Conversation {conversation_id} not found")
|
|
226
|
+
|
|
227
|
+
conv_data["metadata"] = metadata
|
|
228
|
+
|
|
229
|
+
return Conversation(
|
|
230
|
+
id=conv_data["id"],
|
|
231
|
+
object="conversation",
|
|
232
|
+
created_at=conv_data["created_at"],
|
|
233
|
+
metadata=metadata,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def delete_conversation(self, conversation_id: str) -> ConversationDeletedResource:
|
|
237
|
+
"""Delete conversation and its AgentThread."""
|
|
238
|
+
if conversation_id not in self._conversations:
|
|
239
|
+
raise ValueError(f"Conversation {conversation_id} not found")
|
|
240
|
+
|
|
241
|
+
del self._conversations[conversation_id]
|
|
242
|
+
# Cleanup item index
|
|
243
|
+
self._item_index.pop(conversation_id, None)
|
|
244
|
+
|
|
245
|
+
return ConversationDeletedResource(id=conversation_id, object="conversation.deleted", deleted=True)
|
|
246
|
+
|
|
247
|
+
async def add_items(self, conversation_id: str, items: list[dict[str, Any]]) -> list[ConversationItem]:
|
|
248
|
+
"""Add items to conversation and sync to AgentThread."""
|
|
249
|
+
conv_data = self._conversations.get(conversation_id)
|
|
250
|
+
if not conv_data:
|
|
251
|
+
raise ValueError(f"Conversation {conversation_id} not found")
|
|
252
|
+
|
|
253
|
+
thread: AgentThread = conv_data["thread"]
|
|
254
|
+
|
|
255
|
+
# Convert items to ChatMessages and add to thread
|
|
256
|
+
chat_messages = []
|
|
257
|
+
for item in items:
|
|
258
|
+
# Simple conversion - assume text content for now
|
|
259
|
+
role = item.get("role", "user")
|
|
260
|
+
content = item.get("content", [])
|
|
261
|
+
text = content[0].get("text", "") if content else ""
|
|
262
|
+
|
|
263
|
+
chat_msg = ChatMessage(role=role, contents=[{"type": "text", "text": text}])
|
|
264
|
+
chat_messages.append(chat_msg)
|
|
265
|
+
|
|
266
|
+
# Add messages to AgentThread
|
|
267
|
+
await thread.on_new_messages(chat_messages)
|
|
268
|
+
|
|
269
|
+
# Create Message objects (ConversationItem is a Union - use concrete Message type)
|
|
270
|
+
conv_items: list[ConversationItem] = []
|
|
271
|
+
for msg in chat_messages:
|
|
272
|
+
item_id = f"item_{uuid.uuid4().hex}"
|
|
273
|
+
|
|
274
|
+
# Extract role - handle both string and enum
|
|
275
|
+
role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
|
|
276
|
+
role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles
|
|
277
|
+
|
|
278
|
+
# Convert ChatMessage contents to OpenAI TextContent format
|
|
279
|
+
message_content = []
|
|
280
|
+
for content_item in msg.contents:
|
|
281
|
+
if hasattr(content_item, "type") and content_item.type == "text":
|
|
282
|
+
# Extract text from TextContent object
|
|
283
|
+
text_value = getattr(content_item, "text", "")
|
|
284
|
+
message_content.append(TextContent(type="text", text=text_value))
|
|
285
|
+
|
|
286
|
+
# Create Message object (concrete type from ConversationItem union)
|
|
287
|
+
message = Message(
|
|
288
|
+
id=item_id,
|
|
289
|
+
type="message", # Required discriminator for union
|
|
290
|
+
role=role,
|
|
291
|
+
content=message_content,
|
|
292
|
+
status="completed", # Required field
|
|
293
|
+
)
|
|
294
|
+
conv_items.append(message)
|
|
295
|
+
|
|
296
|
+
# Cache items
|
|
297
|
+
conv_data["items"].extend(conv_items)
|
|
298
|
+
|
|
299
|
+
# Update item index for O(1) lookup
|
|
300
|
+
if conversation_id not in self._item_index:
|
|
301
|
+
self._item_index[conversation_id] = {}
|
|
302
|
+
|
|
303
|
+
for conv_item in conv_items:
|
|
304
|
+
if conv_item.id: # Guard against None
|
|
305
|
+
self._item_index[conversation_id][conv_item.id] = conv_item
|
|
306
|
+
|
|
307
|
+
return conv_items
|
|
308
|
+
|
|
309
|
+
async def list_items(
|
|
310
|
+
self, conversation_id: str, limit: int = 100, after: str | None = None, order: str = "asc"
|
|
311
|
+
) -> tuple[list[ConversationItem], bool]:
|
|
312
|
+
"""List conversation items from AgentThread message store.
|
|
313
|
+
|
|
314
|
+
Converts AgentFramework ChatMessages to proper OpenAI ConversationItem types:
|
|
315
|
+
- Messages with text/images/files → Message
|
|
316
|
+
- Function calls → ResponseFunctionToolCallItem
|
|
317
|
+
- Function results → ResponseFunctionToolCallOutputItem
|
|
318
|
+
"""
|
|
319
|
+
conv_data = self._conversations.get(conversation_id)
|
|
320
|
+
if not conv_data:
|
|
321
|
+
raise ValueError(f"Conversation {conversation_id} not found")
|
|
322
|
+
|
|
323
|
+
thread: AgentThread = conv_data["thread"]
|
|
324
|
+
|
|
325
|
+
# Get messages from thread's message store
|
|
326
|
+
items: list[ConversationItem] = []
|
|
327
|
+
if thread.message_store:
|
|
328
|
+
af_messages = await thread.message_store.list_messages()
|
|
329
|
+
|
|
330
|
+
# Convert each AgentFramework ChatMessage to appropriate ConversationItem type(s)
|
|
331
|
+
for i, msg in enumerate(af_messages):
|
|
332
|
+
item_id = f"item_{i}"
|
|
333
|
+
role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
|
|
334
|
+
role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles
|
|
335
|
+
|
|
336
|
+
# Process each content item in the message
|
|
337
|
+
# A single ChatMessage may produce multiple ConversationItems
|
|
338
|
+
# (e.g., a message with both text and a function call)
|
|
339
|
+
message_contents: list[TextContent | ResponseInputImage | ResponseInputFile] = []
|
|
340
|
+
function_calls = []
|
|
341
|
+
function_results = []
|
|
342
|
+
|
|
343
|
+
for content in msg.contents:
|
|
344
|
+
content_type = getattr(content, "type", None)
|
|
345
|
+
|
|
346
|
+
if content_type == "text":
|
|
347
|
+
# Text content for Message
|
|
348
|
+
text_value = getattr(content, "text", "")
|
|
349
|
+
message_contents.append(TextContent(type="text", text=text_value))
|
|
350
|
+
|
|
351
|
+
elif content_type == "data":
|
|
352
|
+
# Data content (images, files, PDFs)
|
|
353
|
+
uri = getattr(content, "uri", "")
|
|
354
|
+
media_type = getattr(content, "media_type", None)
|
|
355
|
+
|
|
356
|
+
if media_type and media_type.startswith("image/"):
|
|
357
|
+
# Convert to ResponseInputImage
|
|
358
|
+
message_contents.append(
|
|
359
|
+
ResponseInputImage(type="input_image", image_url=uri, detail="auto")
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
# Convert to ResponseInputFile
|
|
363
|
+
# Extract filename from URI if possible
|
|
364
|
+
filename = None
|
|
365
|
+
if media_type == "application/pdf":
|
|
366
|
+
filename = "document.pdf"
|
|
367
|
+
|
|
368
|
+
message_contents.append(
|
|
369
|
+
ResponseInputFile(type="input_file", file_url=uri, filename=filename)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
elif content_type == "function_call":
|
|
373
|
+
# Function call - create separate ConversationItem
|
|
374
|
+
call_id = getattr(content, "call_id", None)
|
|
375
|
+
name = getattr(content, "name", "")
|
|
376
|
+
arguments = getattr(content, "arguments", "")
|
|
377
|
+
|
|
378
|
+
if call_id and name:
|
|
379
|
+
function_calls.append(
|
|
380
|
+
ResponseFunctionToolCallItem(
|
|
381
|
+
id=f"{item_id}_call_{call_id}",
|
|
382
|
+
call_id=call_id,
|
|
383
|
+
name=name,
|
|
384
|
+
arguments=arguments,
|
|
385
|
+
type="function_call",
|
|
386
|
+
status="completed",
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
elif content_type == "function_result":
|
|
391
|
+
# Function result - create separate ConversationItem
|
|
392
|
+
call_id = getattr(content, "call_id", None)
|
|
393
|
+
# Output is stored in additional_properties
|
|
394
|
+
output = ""
|
|
395
|
+
if hasattr(content, "additional_properties"):
|
|
396
|
+
output = content.additional_properties.get("output", "")
|
|
397
|
+
|
|
398
|
+
if call_id:
|
|
399
|
+
function_results.append(
|
|
400
|
+
ResponseFunctionToolCallOutputItem(
|
|
401
|
+
id=f"{item_id}_result_{call_id}",
|
|
402
|
+
call_id=call_id,
|
|
403
|
+
output=output,
|
|
404
|
+
type="function_call_output",
|
|
405
|
+
status="completed",
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Create ConversationItems based on what we found
|
|
410
|
+
# If message has text/images/files, create a Message item
|
|
411
|
+
if message_contents:
|
|
412
|
+
message = Message(
|
|
413
|
+
id=item_id,
|
|
414
|
+
type="message",
|
|
415
|
+
role=role, # type: ignore
|
|
416
|
+
content=message_contents, # type: ignore
|
|
417
|
+
status="completed",
|
|
418
|
+
)
|
|
419
|
+
items.append(message)
|
|
420
|
+
|
|
421
|
+
# Add function call items
|
|
422
|
+
items.extend(function_calls)
|
|
423
|
+
|
|
424
|
+
# Add function result items
|
|
425
|
+
items.extend(function_results)
|
|
426
|
+
|
|
427
|
+
# Apply pagination
|
|
428
|
+
if order == "desc":
|
|
429
|
+
items = items[::-1]
|
|
430
|
+
|
|
431
|
+
start_idx = 0
|
|
432
|
+
if after:
|
|
433
|
+
# Find the index after the cursor
|
|
434
|
+
for i, item in enumerate(items):
|
|
435
|
+
if item.id == after:
|
|
436
|
+
start_idx = i + 1
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
paginated_items = items[start_idx : start_idx + limit]
|
|
440
|
+
has_more = len(items) > start_idx + limit
|
|
441
|
+
|
|
442
|
+
return paginated_items, has_more
|
|
443
|
+
|
|
444
|
+
def get_item(self, conversation_id: str, item_id: str) -> ConversationItem | None:
|
|
445
|
+
"""Get specific conversation item - O(1) lookup via index."""
|
|
446
|
+
# Use index for O(1) lookup instead of linear search
|
|
447
|
+
conv_items = self._item_index.get(conversation_id)
|
|
448
|
+
if not conv_items:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
return conv_items.get(item_id)
|
|
452
|
+
|
|
453
|
+
def get_thread(self, conversation_id: str) -> AgentThread | None:
|
|
454
|
+
"""Get AgentThread for execution - CRITICAL for agent.run_stream()."""
|
|
455
|
+
conv_data = self._conversations.get(conversation_id)
|
|
456
|
+
return conv_data["thread"] if conv_data else None
|
|
457
|
+
|
|
458
|
+
def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]:
|
|
459
|
+
"""Filter conversations by metadata (e.g., agent_id)."""
|
|
460
|
+
results = []
|
|
461
|
+
for conv_data in self._conversations.values():
|
|
462
|
+
conv_meta = conv_data.get("metadata", {})
|
|
463
|
+
# Check if all filter items match
|
|
464
|
+
if all(conv_meta.get(k) == v for k, v in metadata_filter.items()):
|
|
465
|
+
results.append(
|
|
466
|
+
Conversation(
|
|
467
|
+
id=conv_data["id"],
|
|
468
|
+
object="conversation",
|
|
469
|
+
created_at=conv_data["created_at"],
|
|
470
|
+
metadata=conv_meta,
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
return results
|