agent-framework-devui 0.0.1a0__py3-none-any.whl → 1.0.0b251007__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/__init__.py +151 -0
- agent_framework_devui/_cli.py +143 -0
- agent_framework_devui/_discovery.py +822 -0
- agent_framework_devui/_executor.py +777 -0
- agent_framework_devui/_mapper.py +558 -0
- agent_framework_devui/_server.py +577 -0
- agent_framework_devui/_session.py +191 -0
- agent_framework_devui/_tracing.py +168 -0
- agent_framework_devui/_utils.py +421 -0
- agent_framework_devui/models/__init__.py +72 -0
- agent_framework_devui/models/_discovery_models.py +58 -0
- agent_framework_devui/models/_openai_custom.py +209 -0
- agent_framework_devui/ui/agentframework.svg +33 -0
- agent_framework_devui/ui/assets/index-D0SfShuZ.js +445 -0
- agent_framework_devui/ui/assets/index-WsCIE0bH.css +1 -0
- agent_framework_devui/ui/index.html +14 -0
- agent_framework_devui/ui/vite.svg +1 -0
- agent_framework_devui-1.0.0b251007.dist-info/METADATA +172 -0
- agent_framework_devui-1.0.0b251007.dist-info/RECORD +22 -0
- {agent_framework_devui-0.0.1a0.dist-info → agent_framework_devui-1.0.0b251007.dist-info}/WHEEL +1 -2
- agent_framework_devui-1.0.0b251007.dist-info/entry_points.txt +3 -0
- agent_framework_devui-1.0.0b251007.dist-info/licenses/LICENSE +21 -0
- agent_framework_devui-0.0.1a0.dist-info/METADATA +0 -18
- agent_framework_devui-0.0.1a0.dist-info/RECORD +0 -5
- agent_framework_devui-0.0.1a0.dist-info/licenses/LICENSE +0 -9
- agent_framework_devui-0.0.1a0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""FastAPI server implementation."""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import AsyncGenerator
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import Any, get_origin
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
13
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
14
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
15
|
+
from fastapi.staticfiles import StaticFiles
|
|
16
|
+
|
|
17
|
+
from ._discovery import EntityDiscovery
|
|
18
|
+
from ._executor import AgentFrameworkExecutor
|
|
19
|
+
from ._mapper import MessageMapper
|
|
20
|
+
from .models import AgentFrameworkRequest, OpenAIError
|
|
21
|
+
from .models._discovery_models import DiscoveryResponse, EntityInfo
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _extract_executor_message_types(executor: Any) -> list[Any]:
|
|
27
|
+
"""Return declared input types for the given executor."""
|
|
28
|
+
message_types: list[Any] = []
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
input_types = getattr(executor, "input_types", None)
|
|
32
|
+
except Exception as exc: # pragma: no cover - defensive logging path
|
|
33
|
+
logger.debug(f"Failed to access executor input_types: {exc}")
|
|
34
|
+
else:
|
|
35
|
+
if input_types:
|
|
36
|
+
message_types = list(input_types)
|
|
37
|
+
|
|
38
|
+
if not message_types and hasattr(executor, "_handlers"):
|
|
39
|
+
try:
|
|
40
|
+
handlers = executor._handlers
|
|
41
|
+
if isinstance(handlers, dict):
|
|
42
|
+
message_types = list(handlers.keys())
|
|
43
|
+
except Exception as exc: # pragma: no cover - defensive logging path
|
|
44
|
+
logger.debug(f"Failed to read executor handlers: {exc}")
|
|
45
|
+
|
|
46
|
+
return message_types
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _select_primary_input_type(message_types: list[Any]) -> Any | None:
|
|
50
|
+
"""Choose the most user-friendly input type for rendering workflow inputs."""
|
|
51
|
+
if not message_types:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
preferred = (str, dict)
|
|
55
|
+
|
|
56
|
+
for candidate in preferred:
|
|
57
|
+
for message_type in message_types:
|
|
58
|
+
if message_type is candidate:
|
|
59
|
+
return candidate
|
|
60
|
+
origin = get_origin(message_type)
|
|
61
|
+
if origin is candidate:
|
|
62
|
+
return candidate
|
|
63
|
+
|
|
64
|
+
return message_types[0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DevServer:
|
|
68
|
+
"""Development Server - OpenAI compatible API server for debugging agents."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
entities_dir: str | None = None,
|
|
73
|
+
port: int = 8080,
|
|
74
|
+
host: str = "127.0.0.1",
|
|
75
|
+
cors_origins: list[str] | None = None,
|
|
76
|
+
ui_enabled: bool = True,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialize the development server.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
entities_dir: Directory to scan for entities
|
|
82
|
+
port: Port to run server on
|
|
83
|
+
host: Host to bind server to
|
|
84
|
+
cors_origins: List of allowed CORS origins
|
|
85
|
+
ui_enabled: Whether to enable the UI
|
|
86
|
+
"""
|
|
87
|
+
self.entities_dir = entities_dir
|
|
88
|
+
self.port = port
|
|
89
|
+
self.host = host
|
|
90
|
+
self.cors_origins = cors_origins or ["*"]
|
|
91
|
+
self.ui_enabled = ui_enabled
|
|
92
|
+
self.executor: AgentFrameworkExecutor | None = None
|
|
93
|
+
self._app: FastAPI | None = None
|
|
94
|
+
self._pending_entities: list[Any] | None = None
|
|
95
|
+
|
|
96
|
+
async def _ensure_executor(self) -> AgentFrameworkExecutor:
|
|
97
|
+
"""Ensure executor is initialized."""
|
|
98
|
+
if self.executor is None:
|
|
99
|
+
logger.info("Initializing Agent Framework executor...")
|
|
100
|
+
|
|
101
|
+
# Create components directly
|
|
102
|
+
entity_discovery = EntityDiscovery(self.entities_dir)
|
|
103
|
+
message_mapper = MessageMapper()
|
|
104
|
+
self.executor = AgentFrameworkExecutor(entity_discovery, message_mapper)
|
|
105
|
+
|
|
106
|
+
# Discover entities from directory
|
|
107
|
+
discovered_entities = await self.executor.discover_entities()
|
|
108
|
+
logger.info(f"Discovered {len(discovered_entities)} entities from directory")
|
|
109
|
+
|
|
110
|
+
# Register any pending in-memory entities
|
|
111
|
+
if self._pending_entities:
|
|
112
|
+
discovery = self.executor.entity_discovery
|
|
113
|
+
for entity in self._pending_entities:
|
|
114
|
+
try:
|
|
115
|
+
entity_info = await discovery.create_entity_info_from_object(entity, source="in-memory")
|
|
116
|
+
discovery.register_entity(entity_info.id, entity_info, entity)
|
|
117
|
+
logger.info(f"Registered in-memory entity: {entity_info.id}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to register in-memory entity: {e}")
|
|
120
|
+
self._pending_entities = None # Clear after registration
|
|
121
|
+
|
|
122
|
+
# Get the final entity count after all registration
|
|
123
|
+
all_entities = self.executor.entity_discovery.list_entities()
|
|
124
|
+
logger.info(f"Total entities available: {len(all_entities)}")
|
|
125
|
+
|
|
126
|
+
return self.executor
|
|
127
|
+
|
|
128
|
+
async def _cleanup_entities(self) -> None:
|
|
129
|
+
"""Cleanup entity resources (close clients, credentials, etc.)."""
|
|
130
|
+
if not self.executor:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
logger.info("Cleaning up entity resources...")
|
|
134
|
+
entities = self.executor.entity_discovery.list_entities()
|
|
135
|
+
closed_count = 0
|
|
136
|
+
|
|
137
|
+
for entity_info in entities:
|
|
138
|
+
try:
|
|
139
|
+
entity_obj = self.executor.entity_discovery.get_entity_object(entity_info.id)
|
|
140
|
+
if entity_obj and hasattr(entity_obj, "chat_client"):
|
|
141
|
+
client = entity_obj.chat_client
|
|
142
|
+
if hasattr(client, "close") and callable(client.close):
|
|
143
|
+
if inspect.iscoroutinefunction(client.close):
|
|
144
|
+
await client.close()
|
|
145
|
+
else:
|
|
146
|
+
client.close()
|
|
147
|
+
closed_count += 1
|
|
148
|
+
logger.debug(f"Closed client for entity: {entity_info.id}")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning(f"Error closing entity {entity_info.id}: {e}")
|
|
151
|
+
|
|
152
|
+
if closed_count > 0:
|
|
153
|
+
logger.info(f"Closed {closed_count} entity client(s)")
|
|
154
|
+
|
|
155
|
+
def create_app(self) -> FastAPI:
|
|
156
|
+
"""Create the FastAPI application."""
|
|
157
|
+
|
|
158
|
+
@asynccontextmanager
|
|
159
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
160
|
+
# Startup
|
|
161
|
+
logger.info("Starting Agent Framework Server")
|
|
162
|
+
await self._ensure_executor()
|
|
163
|
+
yield
|
|
164
|
+
# Shutdown
|
|
165
|
+
logger.info("Shutting down Agent Framework Server")
|
|
166
|
+
|
|
167
|
+
# Cleanup entity resources (e.g., close credentials, clients)
|
|
168
|
+
if self.executor:
|
|
169
|
+
await self._cleanup_entities()
|
|
170
|
+
|
|
171
|
+
app = FastAPI(
|
|
172
|
+
title="Agent Framework Server",
|
|
173
|
+
description="OpenAI-compatible API server for Agent Framework and other AI frameworks",
|
|
174
|
+
version="1.0.0",
|
|
175
|
+
lifespan=lifespan,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Add CORS middleware
|
|
179
|
+
app.add_middleware(
|
|
180
|
+
CORSMiddleware,
|
|
181
|
+
allow_origins=self.cors_origins,
|
|
182
|
+
allow_credentials=True,
|
|
183
|
+
allow_methods=["*"],
|
|
184
|
+
allow_headers=["*"],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self._register_routes(app)
|
|
188
|
+
self._mount_ui(app)
|
|
189
|
+
|
|
190
|
+
return app
|
|
191
|
+
|
|
192
|
+
def _register_routes(self, app: FastAPI) -> None:
|
|
193
|
+
"""Register API routes."""
|
|
194
|
+
|
|
195
|
+
@app.get("/health")
|
|
196
|
+
async def health_check() -> dict[str, Any]:
|
|
197
|
+
"""Health check endpoint."""
|
|
198
|
+
executor = await self._ensure_executor()
|
|
199
|
+
# Use list_entities() to avoid re-discovering and re-registering entities
|
|
200
|
+
entities = executor.entity_discovery.list_entities()
|
|
201
|
+
|
|
202
|
+
return {"status": "healthy", "entities_count": len(entities), "framework": "agent_framework"}
|
|
203
|
+
|
|
204
|
+
@app.get("/v1/entities", response_model=DiscoveryResponse)
|
|
205
|
+
async def discover_entities() -> DiscoveryResponse:
|
|
206
|
+
"""List all registered entities."""
|
|
207
|
+
try:
|
|
208
|
+
executor = await self._ensure_executor()
|
|
209
|
+
# Use list_entities() instead of discover_entities() to get already-registered entities
|
|
210
|
+
entities = executor.entity_discovery.list_entities()
|
|
211
|
+
return DiscoveryResponse(entities=entities)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Error listing entities: {e}")
|
|
214
|
+
raise HTTPException(status_code=500, detail=f"Entity listing failed: {e!s}") from e
|
|
215
|
+
|
|
216
|
+
@app.get("/v1/entities/{entity_id}/info", response_model=EntityInfo)
|
|
217
|
+
async def get_entity_info(entity_id: str) -> EntityInfo:
|
|
218
|
+
"""Get detailed information about a specific entity."""
|
|
219
|
+
try:
|
|
220
|
+
executor = await self._ensure_executor()
|
|
221
|
+
entity_info = executor.get_entity_info(entity_id)
|
|
222
|
+
|
|
223
|
+
if not entity_info:
|
|
224
|
+
raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
|
|
225
|
+
|
|
226
|
+
# For workflows, populate additional detailed information
|
|
227
|
+
if entity_info.type == "workflow":
|
|
228
|
+
entity_obj = executor.entity_discovery.get_entity_object(entity_id)
|
|
229
|
+
if entity_obj:
|
|
230
|
+
# Get workflow structure
|
|
231
|
+
workflow_dump = None
|
|
232
|
+
if hasattr(entity_obj, "to_dict") and callable(getattr(entity_obj, "to_dict", None)):
|
|
233
|
+
try:
|
|
234
|
+
workflow_dump = entity_obj.to_dict() # type: ignore[attr-defined]
|
|
235
|
+
except Exception:
|
|
236
|
+
workflow_dump = None
|
|
237
|
+
elif hasattr(entity_obj, "to_json") and callable(getattr(entity_obj, "to_json", None)):
|
|
238
|
+
try:
|
|
239
|
+
raw_dump = entity_obj.to_json() # type: ignore[attr-defined]
|
|
240
|
+
except Exception:
|
|
241
|
+
workflow_dump = None
|
|
242
|
+
else:
|
|
243
|
+
if isinstance(raw_dump, (bytes, bytearray)):
|
|
244
|
+
try:
|
|
245
|
+
raw_dump = raw_dump.decode()
|
|
246
|
+
except Exception:
|
|
247
|
+
raw_dump = raw_dump.decode(errors="replace")
|
|
248
|
+
if isinstance(raw_dump, str):
|
|
249
|
+
try:
|
|
250
|
+
parsed_dump = json.loads(raw_dump)
|
|
251
|
+
except Exception:
|
|
252
|
+
workflow_dump = raw_dump
|
|
253
|
+
else:
|
|
254
|
+
workflow_dump = parsed_dump if isinstance(parsed_dump, dict) else raw_dump
|
|
255
|
+
else:
|
|
256
|
+
workflow_dump = raw_dump
|
|
257
|
+
elif hasattr(entity_obj, "__dict__"):
|
|
258
|
+
workflow_dump = {k: v for k, v in entity_obj.__dict__.items() if not k.startswith("_")}
|
|
259
|
+
|
|
260
|
+
# Get input schema information
|
|
261
|
+
input_schema = {}
|
|
262
|
+
input_type_name = "Unknown"
|
|
263
|
+
start_executor_id = ""
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
from ._utils import generate_input_schema
|
|
267
|
+
|
|
268
|
+
start_executor = entity_obj.get_start_executor()
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.debug(f"Could not extract input info for workflow {entity_id}: {e}")
|
|
271
|
+
else:
|
|
272
|
+
if start_executor:
|
|
273
|
+
start_executor_id = getattr(start_executor, "executor_id", "") or getattr(
|
|
274
|
+
start_executor, "id", ""
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
message_types = _extract_executor_message_types(start_executor)
|
|
278
|
+
input_type = _select_primary_input_type(message_types)
|
|
279
|
+
|
|
280
|
+
if input_type:
|
|
281
|
+
input_type_name = getattr(input_type, "__name__", str(input_type))
|
|
282
|
+
|
|
283
|
+
# Generate schema using comprehensive schema generation
|
|
284
|
+
input_schema = generate_input_schema(input_type)
|
|
285
|
+
|
|
286
|
+
if not input_schema:
|
|
287
|
+
input_schema = {"type": "string"}
|
|
288
|
+
if input_type_name == "Unknown":
|
|
289
|
+
input_type_name = "string"
|
|
290
|
+
|
|
291
|
+
# Get executor list
|
|
292
|
+
executor_list = []
|
|
293
|
+
if hasattr(entity_obj, "executors") and entity_obj.executors:
|
|
294
|
+
executor_list = [getattr(ex, "executor_id", str(ex)) for ex in entity_obj.executors]
|
|
295
|
+
|
|
296
|
+
# Create copy of entity info and populate workflow-specific fields
|
|
297
|
+
update_payload: dict[str, Any] = {
|
|
298
|
+
"workflow_dump": workflow_dump,
|
|
299
|
+
"input_schema": input_schema,
|
|
300
|
+
"input_type_name": input_type_name,
|
|
301
|
+
"start_executor_id": start_executor_id,
|
|
302
|
+
}
|
|
303
|
+
if executor_list:
|
|
304
|
+
update_payload["executors"] = executor_list
|
|
305
|
+
return entity_info.model_copy(update=update_payload)
|
|
306
|
+
|
|
307
|
+
# For non-workflow entities, return as-is
|
|
308
|
+
return entity_info
|
|
309
|
+
|
|
310
|
+
except HTTPException:
|
|
311
|
+
raise
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Error getting entity info for {entity_id}: {e}")
|
|
314
|
+
raise HTTPException(status_code=500, detail=f"Failed to get entity info: {e!s}") from e
|
|
315
|
+
|
|
316
|
+
@app.post("/v1/entities/add")
|
|
317
|
+
async def add_entity(request: dict[str, Any]) -> dict[str, Any]:
|
|
318
|
+
"""Add entity from URL."""
|
|
319
|
+
try:
|
|
320
|
+
url = request.get("url")
|
|
321
|
+
metadata = request.get("metadata", {})
|
|
322
|
+
|
|
323
|
+
if not url:
|
|
324
|
+
raise HTTPException(status_code=400, detail="URL is required")
|
|
325
|
+
|
|
326
|
+
logger.info(f"Attempting to add entity from URL: {url}")
|
|
327
|
+
executor = await self._ensure_executor()
|
|
328
|
+
entity_info, error_msg = await executor.entity_discovery.fetch_remote_entity(url, metadata)
|
|
329
|
+
|
|
330
|
+
if not entity_info:
|
|
331
|
+
# Sanitize error message - only return safe, user-friendly errors
|
|
332
|
+
logger.error(f"Failed to fetch or validate entity from {url}: {error_msg}")
|
|
333
|
+
safe_error = error_msg if error_msg else "Failed to fetch or validate entity"
|
|
334
|
+
raise HTTPException(status_code=400, detail=safe_error)
|
|
335
|
+
|
|
336
|
+
logger.info(f"Successfully added entity: {entity_info.id}")
|
|
337
|
+
return {"success": True, "entity": entity_info.model_dump()}
|
|
338
|
+
|
|
339
|
+
except HTTPException:
|
|
340
|
+
raise
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"Error adding entity: {e}", exc_info=True)
|
|
343
|
+
# Don't expose internal error details to client
|
|
344
|
+
raise HTTPException(
|
|
345
|
+
status_code=500, detail="An unexpected error occurred while adding the entity"
|
|
346
|
+
) from e
|
|
347
|
+
|
|
348
|
+
@app.delete("/v1/entities/{entity_id}")
|
|
349
|
+
async def remove_entity(entity_id: str) -> dict[str, Any]:
|
|
350
|
+
"""Remove entity by ID."""
|
|
351
|
+
try:
|
|
352
|
+
executor = await self._ensure_executor()
|
|
353
|
+
|
|
354
|
+
# Cleanup entity resources before removal
|
|
355
|
+
try:
|
|
356
|
+
entity_obj = executor.entity_discovery.get_entity_object(entity_id)
|
|
357
|
+
if entity_obj and hasattr(entity_obj, "chat_client"):
|
|
358
|
+
client = entity_obj.chat_client
|
|
359
|
+
if hasattr(client, "close") and callable(client.close):
|
|
360
|
+
if inspect.iscoroutinefunction(client.close):
|
|
361
|
+
await client.close()
|
|
362
|
+
else:
|
|
363
|
+
client.close()
|
|
364
|
+
logger.info(f"Closed client for entity: {entity_id}")
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.warning(f"Error closing entity {entity_id} during removal: {e}")
|
|
367
|
+
|
|
368
|
+
# Remove entity from registry
|
|
369
|
+
success = executor.entity_discovery.remove_remote_entity(entity_id)
|
|
370
|
+
|
|
371
|
+
if success:
|
|
372
|
+
return {"success": True}
|
|
373
|
+
raise HTTPException(status_code=404, detail="Entity not found or cannot be removed")
|
|
374
|
+
|
|
375
|
+
except HTTPException:
|
|
376
|
+
raise
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error(f"Error removing entity {entity_id}: {e}")
|
|
379
|
+
raise HTTPException(status_code=500, detail=f"Failed to remove entity: {e!s}") from e
|
|
380
|
+
|
|
381
|
+
@app.post("/v1/responses")
|
|
382
|
+
async def create_response(request: AgentFrameworkRequest, raw_request: Request) -> Any:
|
|
383
|
+
"""OpenAI Responses API endpoint."""
|
|
384
|
+
try:
|
|
385
|
+
raw_body = await raw_request.body()
|
|
386
|
+
logger.info(f"Raw request body: {raw_body.decode()}")
|
|
387
|
+
logger.info(f"Parsed request: model={request.model}, extra_body={request.extra_body}")
|
|
388
|
+
|
|
389
|
+
# Get entity_id using the new method
|
|
390
|
+
entity_id = request.get_entity_id()
|
|
391
|
+
logger.info(f"Extracted entity_id: {entity_id}")
|
|
392
|
+
|
|
393
|
+
if not entity_id:
|
|
394
|
+
error = OpenAIError.create(f"Missing entity_id. Request extra_body: {request.extra_body}")
|
|
395
|
+
return JSONResponse(status_code=400, content=error.to_dict())
|
|
396
|
+
|
|
397
|
+
# Get executor and validate entity exists
|
|
398
|
+
executor = await self._ensure_executor()
|
|
399
|
+
try:
|
|
400
|
+
entity_info = executor.get_entity_info(entity_id)
|
|
401
|
+
logger.info(f"Found entity: {entity_info.name} ({entity_info.type})")
|
|
402
|
+
except Exception:
|
|
403
|
+
error = OpenAIError.create(f"Entity not found: {entity_id}")
|
|
404
|
+
return JSONResponse(status_code=404, content=error.to_dict())
|
|
405
|
+
|
|
406
|
+
# Execute request
|
|
407
|
+
if request.stream:
|
|
408
|
+
return StreamingResponse(
|
|
409
|
+
self._stream_execution(executor, request),
|
|
410
|
+
media_type="text/event-stream",
|
|
411
|
+
headers={
|
|
412
|
+
"Cache-Control": "no-cache",
|
|
413
|
+
"Connection": "keep-alive",
|
|
414
|
+
"Access-Control-Allow-Origin": "*",
|
|
415
|
+
},
|
|
416
|
+
)
|
|
417
|
+
return await executor.execute_sync(request)
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error executing request: {e}")
|
|
421
|
+
error = OpenAIError.create(f"Execution failed: {e!s}")
|
|
422
|
+
return JSONResponse(status_code=500, content=error.to_dict())
|
|
423
|
+
|
|
424
|
+
@app.post("/v1/threads")
|
|
425
|
+
async def create_thread(request_data: dict[str, Any]) -> dict[str, Any]:
|
|
426
|
+
"""Create a new thread for an agent."""
|
|
427
|
+
try:
|
|
428
|
+
agent_id = request_data.get("agent_id")
|
|
429
|
+
if not agent_id:
|
|
430
|
+
raise HTTPException(status_code=400, detail="agent_id is required")
|
|
431
|
+
|
|
432
|
+
executor = await self._ensure_executor()
|
|
433
|
+
thread_id = executor.create_thread(agent_id)
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
"id": thread_id,
|
|
437
|
+
"object": "thread",
|
|
438
|
+
"created_at": int(__import__("time").time()),
|
|
439
|
+
"metadata": {"agent_id": agent_id},
|
|
440
|
+
}
|
|
441
|
+
except HTTPException:
|
|
442
|
+
raise
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.error(f"Error creating thread: {e}")
|
|
445
|
+
raise HTTPException(status_code=500, detail=f"Failed to create thread: {e!s}") from e
|
|
446
|
+
|
|
447
|
+
@app.get("/v1/threads")
|
|
448
|
+
async def list_threads(agent_id: str) -> dict[str, Any]:
|
|
449
|
+
"""List threads for an agent."""
|
|
450
|
+
try:
|
|
451
|
+
executor = await self._ensure_executor()
|
|
452
|
+
thread_ids = executor.list_threads_for_agent(agent_id)
|
|
453
|
+
|
|
454
|
+
# Convert thread IDs to thread objects
|
|
455
|
+
threads = []
|
|
456
|
+
for thread_id in thread_ids:
|
|
457
|
+
threads.append({"id": thread_id, "object": "thread", "agent_id": agent_id})
|
|
458
|
+
|
|
459
|
+
return {"object": "list", "data": threads}
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Error listing threads: {e}")
|
|
462
|
+
raise HTTPException(status_code=500, detail=f"Failed to list threads: {e!s}") from e
|
|
463
|
+
|
|
464
|
+
@app.get("/v1/threads/{thread_id}")
|
|
465
|
+
async def get_thread(thread_id: str) -> dict[str, Any]:
|
|
466
|
+
"""Get thread information."""
|
|
467
|
+
try:
|
|
468
|
+
executor = await self._ensure_executor()
|
|
469
|
+
|
|
470
|
+
# Check if thread exists
|
|
471
|
+
thread = executor.get_thread(thread_id)
|
|
472
|
+
if not thread:
|
|
473
|
+
raise HTTPException(status_code=404, detail="Thread not found")
|
|
474
|
+
|
|
475
|
+
# Get the agent that owns this thread
|
|
476
|
+
agent_id = executor.get_agent_for_thread(thread_id)
|
|
477
|
+
|
|
478
|
+
return {"id": thread_id, "object": "thread", "agent_id": agent_id}
|
|
479
|
+
except HTTPException:
|
|
480
|
+
raise
|
|
481
|
+
except Exception as e:
|
|
482
|
+
logger.error(f"Error getting thread {thread_id}: {e}")
|
|
483
|
+
raise HTTPException(status_code=500, detail=f"Failed to get thread: {e!s}") from e
|
|
484
|
+
|
|
485
|
+
@app.delete("/v1/threads/{thread_id}")
|
|
486
|
+
async def delete_thread(thread_id: str) -> dict[str, Any]:
|
|
487
|
+
"""Delete a thread."""
|
|
488
|
+
try:
|
|
489
|
+
executor = await self._ensure_executor()
|
|
490
|
+
success = executor.delete_thread(thread_id)
|
|
491
|
+
|
|
492
|
+
if not success:
|
|
493
|
+
raise HTTPException(status_code=404, detail="Thread not found")
|
|
494
|
+
|
|
495
|
+
return {"id": thread_id, "object": "thread.deleted", "deleted": True}
|
|
496
|
+
except HTTPException:
|
|
497
|
+
raise
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.error(f"Error deleting thread {thread_id}: {e}")
|
|
500
|
+
raise HTTPException(status_code=500, detail=f"Failed to delete thread: {e!s}") from e
|
|
501
|
+
|
|
502
|
+
@app.get("/v1/threads/{thread_id}/messages")
|
|
503
|
+
async def get_thread_messages(thread_id: str) -> dict[str, Any]:
|
|
504
|
+
"""Get messages from a thread."""
|
|
505
|
+
try:
|
|
506
|
+
executor = await self._ensure_executor()
|
|
507
|
+
|
|
508
|
+
# Check if thread exists
|
|
509
|
+
thread = executor.get_thread(thread_id)
|
|
510
|
+
if not thread:
|
|
511
|
+
raise HTTPException(status_code=404, detail="Thread not found")
|
|
512
|
+
|
|
513
|
+
# Get messages from thread
|
|
514
|
+
messages = await executor.get_thread_messages(thread_id)
|
|
515
|
+
|
|
516
|
+
return {"object": "list", "data": messages, "thread_id": thread_id}
|
|
517
|
+
except HTTPException:
|
|
518
|
+
raise
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logger.error(f"Error getting messages for thread {thread_id}: {e}")
|
|
521
|
+
raise HTTPException(status_code=500, detail=f"Failed to get thread messages: {e!s}") from e
|
|
522
|
+
|
|
523
|
+
async def _stream_execution(
|
|
524
|
+
self, executor: AgentFrameworkExecutor, request: AgentFrameworkRequest
|
|
525
|
+
) -> AsyncGenerator[str, None]:
|
|
526
|
+
"""Stream execution directly through executor."""
|
|
527
|
+
try:
|
|
528
|
+
# Direct call to executor - simple and clean
|
|
529
|
+
async for event in executor.execute_streaming(request):
|
|
530
|
+
# IMPORTANT: Check model_dump_json FIRST because to_json() can have newlines (pretty-printing)
|
|
531
|
+
# which breaks SSE format. model_dump_json() returns single-line JSON.
|
|
532
|
+
if hasattr(event, "model_dump_json"):
|
|
533
|
+
payload = event.model_dump_json() # type: ignore[attr-defined]
|
|
534
|
+
elif hasattr(event, "to_json") and callable(getattr(event, "to_json", None)):
|
|
535
|
+
payload = event.to_json() # type: ignore[attr-defined]
|
|
536
|
+
# Strip newlines from pretty-printed JSON for SSE compatibility
|
|
537
|
+
payload = payload.replace("\n", "").replace("\r", "")
|
|
538
|
+
elif isinstance(event, dict):
|
|
539
|
+
# Handle plain dict events (e.g., error events from executor)
|
|
540
|
+
payload = json.dumps(event)
|
|
541
|
+
elif hasattr(event, "to_dict") and callable(getattr(event, "to_dict", None)):
|
|
542
|
+
payload = json.dumps(event.to_dict()) # type: ignore[attr-defined]
|
|
543
|
+
else:
|
|
544
|
+
payload = json.dumps(str(event))
|
|
545
|
+
yield f"data: {payload}\n\n"
|
|
546
|
+
|
|
547
|
+
# Send final done event
|
|
548
|
+
yield "data: [DONE]\n\n"
|
|
549
|
+
|
|
550
|
+
except Exception as e:
|
|
551
|
+
logger.error(f"Error in streaming execution: {e}")
|
|
552
|
+
error_event = {"id": "error", "object": "error", "error": {"message": str(e), "type": "execution_error"}}
|
|
553
|
+
yield f"data: {json.dumps(error_event)}\n\n"
|
|
554
|
+
|
|
555
|
+
def _mount_ui(self, app: FastAPI) -> None:
|
|
556
|
+
"""Mount the UI as static files."""
|
|
557
|
+
from pathlib import Path
|
|
558
|
+
|
|
559
|
+
ui_dir = Path(__file__).parent / "ui"
|
|
560
|
+
if ui_dir.exists() and ui_dir.is_dir() and self.ui_enabled:
|
|
561
|
+
app.mount("/", StaticFiles(directory=str(ui_dir), html=True), name="ui")
|
|
562
|
+
|
|
563
|
+
def register_entities(self, entities: list[Any]) -> None:
|
|
564
|
+
"""Register entities to be discovered when server starts.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
entities: List of entity objects to register
|
|
568
|
+
"""
|
|
569
|
+
if self._pending_entities is None:
|
|
570
|
+
self._pending_entities = []
|
|
571
|
+
self._pending_entities.extend(entities)
|
|
572
|
+
|
|
573
|
+
def get_app(self) -> FastAPI:
|
|
574
|
+
"""Get the FastAPI application instance."""
|
|
575
|
+
if self._app is None:
|
|
576
|
+
self._app = self.create_app()
|
|
577
|
+
return self._app
|