agent-framework-devui 0.0.1a0__py3-none-any.whl → 1.0.0b251001__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 +724 -0
- agent_framework_devui/_executor.py +770 -0
- agent_framework_devui/_mapper.py +582 -0
- agent_framework_devui/_server.py +530 -0
- agent_framework_devui/_session.py +191 -0
- agent_framework_devui/_tracing.py +168 -0
- agent_framework_devui/models/__init__.py +72 -0
- agent_framework_devui/models/_discovery_models.py +51 -0
- agent_framework_devui/models/_openai_custom.py +209 -0
- agent_framework_devui/ui/assets/index-D1AmQWga.css +1 -0
- agent_framework_devui/ui/assets/index-DPEaaIdK.js +435 -0
- agent_framework_devui/ui/index.html +14 -0
- agent_framework_devui/ui/vite.svg +1 -0
- agent_framework_devui-1.0.0b251001.dist-info/METADATA +172 -0
- agent_framework_devui-1.0.0b251001.dist-info/RECORD +20 -0
- {agent_framework_devui-0.0.1a0.dist-info → agent_framework_devui-1.0.0b251001.dist-info}/WHEEL +1 -2
- agent_framework_devui-1.0.0b251001.dist-info/entry_points.txt +3 -0
- agent_framework_devui-1.0.0b251001.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,191 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Session management for agent execution tracking."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Type aliases for better readability
|
|
13
|
+
SessionData = dict[str, Any]
|
|
14
|
+
RequestRecord = dict[str, Any]
|
|
15
|
+
SessionSummary = dict[str, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SessionManager:
|
|
19
|
+
"""Manages execution sessions for tracking requests and context."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Initialize the session manager."""
|
|
23
|
+
self.sessions: dict[str, SessionData] = {}
|
|
24
|
+
|
|
25
|
+
def create_session(self, session_id: str | None = None) -> str:
|
|
26
|
+
"""Create a new execution session.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
session_id: Optional session ID, if not provided a new one is generated
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Session ID
|
|
33
|
+
"""
|
|
34
|
+
if not session_id:
|
|
35
|
+
session_id = str(uuid.uuid4())
|
|
36
|
+
|
|
37
|
+
self.sessions[session_id] = {
|
|
38
|
+
"id": session_id,
|
|
39
|
+
"created_at": datetime.now(),
|
|
40
|
+
"requests": [],
|
|
41
|
+
"context": {},
|
|
42
|
+
"active": True,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.debug(f"Created session: {session_id}")
|
|
46
|
+
return session_id
|
|
47
|
+
|
|
48
|
+
def get_session(self, session_id: str) -> SessionData | None:
|
|
49
|
+
"""Get session information.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session_id: Session ID
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Session data or None if not found
|
|
56
|
+
"""
|
|
57
|
+
return self.sessions.get(session_id)
|
|
58
|
+
|
|
59
|
+
def close_session(self, session_id: str) -> None:
|
|
60
|
+
"""Close and cleanup a session.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session_id: Session ID to close
|
|
64
|
+
"""
|
|
65
|
+
if session_id in self.sessions:
|
|
66
|
+
self.sessions[session_id]["active"] = False
|
|
67
|
+
logger.debug(f"Closed session: {session_id}")
|
|
68
|
+
|
|
69
|
+
def add_request_record(
|
|
70
|
+
self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model: str
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Add a request record to a session.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
session_id: Session ID
|
|
76
|
+
entity_id: ID of the entity being executed
|
|
77
|
+
executor_name: Name of the executor
|
|
78
|
+
request_input: Input for the request
|
|
79
|
+
model: Model name
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Request ID
|
|
83
|
+
"""
|
|
84
|
+
session = self.get_session(session_id)
|
|
85
|
+
if not session:
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
request_record: RequestRecord = {
|
|
89
|
+
"id": str(uuid.uuid4()),
|
|
90
|
+
"timestamp": datetime.now(),
|
|
91
|
+
"entity_id": entity_id,
|
|
92
|
+
"executor": executor_name,
|
|
93
|
+
"input": request_input,
|
|
94
|
+
"model": model,
|
|
95
|
+
"stream": True,
|
|
96
|
+
}
|
|
97
|
+
session["requests"].append(request_record)
|
|
98
|
+
return str(request_record["id"])
|
|
99
|
+
|
|
100
|
+
def update_request_record(self, session_id: str, request_id: str, updates: dict[str, Any]) -> None:
|
|
101
|
+
"""Update a request record in a session.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
session_id: Session ID
|
|
105
|
+
request_id: Request ID to update
|
|
106
|
+
updates: Dictionary of updates to apply
|
|
107
|
+
"""
|
|
108
|
+
session = self.get_session(session_id)
|
|
109
|
+
if not session:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for request in session["requests"]:
|
|
113
|
+
if request["id"] == request_id:
|
|
114
|
+
request.update(updates)
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
def get_session_history(self, session_id: str) -> SessionSummary | None:
|
|
118
|
+
"""Get session execution history.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
session_id: Session ID
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Session history or None if not found
|
|
125
|
+
"""
|
|
126
|
+
session = self.get_session(session_id)
|
|
127
|
+
if not session:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"session_id": session_id,
|
|
132
|
+
"created_at": session["created_at"].isoformat(),
|
|
133
|
+
"active": session["active"],
|
|
134
|
+
"request_count": len(session["requests"]),
|
|
135
|
+
"requests": [
|
|
136
|
+
{
|
|
137
|
+
"id": req["id"],
|
|
138
|
+
"timestamp": req["timestamp"].isoformat(),
|
|
139
|
+
"entity_id": req["entity_id"],
|
|
140
|
+
"executor": req["executor"],
|
|
141
|
+
"model": req["model"],
|
|
142
|
+
"input_length": len(str(req["input"])) if req["input"] else 0,
|
|
143
|
+
"execution_time": req.get("execution_time"),
|
|
144
|
+
"status": req.get("status", "unknown"),
|
|
145
|
+
}
|
|
146
|
+
for req in session["requests"]
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def get_active_sessions(self) -> list[SessionSummary]:
|
|
151
|
+
"""Get list of active sessions.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
List of active session summaries
|
|
155
|
+
"""
|
|
156
|
+
active_sessions = []
|
|
157
|
+
|
|
158
|
+
for session_id, session in self.sessions.items():
|
|
159
|
+
if session["active"]:
|
|
160
|
+
active_sessions.append({
|
|
161
|
+
"session_id": session_id,
|
|
162
|
+
"created_at": session["created_at"].isoformat(),
|
|
163
|
+
"request_count": len(session["requests"]),
|
|
164
|
+
"last_activity": (
|
|
165
|
+
session["requests"][-1]["timestamp"].isoformat()
|
|
166
|
+
if session["requests"]
|
|
167
|
+
else session["created_at"].isoformat()
|
|
168
|
+
),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return active_sessions
|
|
172
|
+
|
|
173
|
+
def cleanup_old_sessions(self, max_age_hours: int = 24) -> None:
|
|
174
|
+
"""Cleanup old sessions to prevent memory leaks.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
max_age_hours: Maximum age of sessions to keep in hours
|
|
178
|
+
"""
|
|
179
|
+
cutoff_time = datetime.now().timestamp() - (max_age_hours * 3600)
|
|
180
|
+
|
|
181
|
+
sessions_to_remove = []
|
|
182
|
+
for session_id, session in self.sessions.items():
|
|
183
|
+
if session["created_at"].timestamp() < cutoff_time:
|
|
184
|
+
sessions_to_remove.append(session_id)
|
|
185
|
+
|
|
186
|
+
for session_id in sessions_to_remove:
|
|
187
|
+
del self.sessions[session_id]
|
|
188
|
+
logger.debug(f"Cleaned up old session: {session_id}")
|
|
189
|
+
|
|
190
|
+
if sessions_to_remove:
|
|
191
|
+
logger.info(f"Cleaned up {len(sessions_to_remove)} old sessions")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Simplified tracing integration for Agent Framework Server."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Generator, Sequence
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
12
|
+
|
|
13
|
+
from .models import ResponseTraceEvent
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SimpleTraceCollector(SpanExporter):
|
|
19
|
+
"""Simple trace collector that captures spans for direct yielding."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session_id: str | None = None, entity_id: str | None = None) -> None:
|
|
22
|
+
"""Initialize trace collector.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session_id: Session identifier for context
|
|
26
|
+
entity_id: Entity identifier for context
|
|
27
|
+
"""
|
|
28
|
+
self.session_id = session_id
|
|
29
|
+
self.entity_id = entity_id
|
|
30
|
+
self.collected_events: list[ResponseTraceEvent] = []
|
|
31
|
+
|
|
32
|
+
def export(self, spans: Sequence[Any]) -> SpanExportResult:
|
|
33
|
+
"""Collect spans as trace events.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
spans: Sequence of OpenTelemetry spans
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SpanExportResult indicating success
|
|
40
|
+
"""
|
|
41
|
+
logger.debug(f"SimpleTraceCollector received {len(spans)} spans")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
for span in spans:
|
|
45
|
+
trace_event = self._convert_span_to_trace_event(span)
|
|
46
|
+
if trace_event:
|
|
47
|
+
self.collected_events.append(trace_event)
|
|
48
|
+
logger.debug(f"Collected trace event: {span.name}")
|
|
49
|
+
|
|
50
|
+
return SpanExportResult.SUCCESS
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Error collecting trace spans: {e}")
|
|
54
|
+
return SpanExportResult.FAILURE
|
|
55
|
+
|
|
56
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
57
|
+
"""Force flush spans (no-op for simple collection)."""
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def get_pending_events(self) -> list[ResponseTraceEvent]:
|
|
61
|
+
"""Get and clear pending trace events.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of collected trace events, clearing the internal list
|
|
65
|
+
"""
|
|
66
|
+
events = self.collected_events.copy()
|
|
67
|
+
self.collected_events.clear()
|
|
68
|
+
return events
|
|
69
|
+
|
|
70
|
+
def _convert_span_to_trace_event(self, span: Any) -> ResponseTraceEvent | None:
|
|
71
|
+
"""Convert OpenTelemetry span to ResponseTraceEvent.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
span: OpenTelemetry span
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
ResponseTraceEvent or None if conversion fails
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
start_time = span.start_time / 1_000_000_000 # Convert from nanoseconds
|
|
81
|
+
end_time = span.end_time / 1_000_000_000 if span.end_time else None
|
|
82
|
+
duration_ms = ((end_time - start_time) * 1000) if end_time else None
|
|
83
|
+
|
|
84
|
+
# Build trace data
|
|
85
|
+
trace_data = {
|
|
86
|
+
"type": "trace_span",
|
|
87
|
+
"span_id": str(span.context.span_id),
|
|
88
|
+
"trace_id": str(span.context.trace_id),
|
|
89
|
+
"parent_span_id": str(span.parent.span_id) if span.parent else None,
|
|
90
|
+
"operation_name": span.name,
|
|
91
|
+
"start_time": start_time,
|
|
92
|
+
"end_time": end_time,
|
|
93
|
+
"duration_ms": duration_ms,
|
|
94
|
+
"attributes": dict(span.attributes) if span.attributes else {},
|
|
95
|
+
"status": str(span.status.status_code) if hasattr(span, "status") else "OK",
|
|
96
|
+
"session_id": self.session_id,
|
|
97
|
+
"entity_id": self.entity_id,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Add events if available
|
|
101
|
+
if hasattr(span, "events") and span.events:
|
|
102
|
+
trace_data["events"] = [
|
|
103
|
+
{
|
|
104
|
+
"name": event.name,
|
|
105
|
+
"timestamp": event.timestamp / 1_000_000_000,
|
|
106
|
+
"attributes": dict(event.attributes) if event.attributes else {},
|
|
107
|
+
}
|
|
108
|
+
for event in span.events
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Add error information if span failed
|
|
112
|
+
if hasattr(span, "status") and span.status.status_code.name == "ERROR":
|
|
113
|
+
trace_data["error"] = span.status.description or "Unknown error"
|
|
114
|
+
|
|
115
|
+
return ResponseTraceEvent(type="trace_event", data=trace_data, timestamp=datetime.now().isoformat())
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warning(f"Failed to convert span {getattr(span, 'name', 'unknown')}: {e}")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@contextmanager
|
|
123
|
+
def capture_traces(
|
|
124
|
+
session_id: str | None = None, entity_id: str | None = None
|
|
125
|
+
) -> Generator[SimpleTraceCollector, None, None]:
|
|
126
|
+
"""Context manager to capture traces during execution.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
session_id: Session identifier for context
|
|
130
|
+
entity_id: Entity identifier for context
|
|
131
|
+
|
|
132
|
+
Yields:
|
|
133
|
+
SimpleTraceCollector instance to get trace events from
|
|
134
|
+
"""
|
|
135
|
+
collector = SimpleTraceCollector(session_id, entity_id)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
from opentelemetry import trace
|
|
139
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
140
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
141
|
+
|
|
142
|
+
# Get current tracer provider and add our collector
|
|
143
|
+
provider = trace.get_tracer_provider()
|
|
144
|
+
processor = SimpleSpanProcessor(collector)
|
|
145
|
+
|
|
146
|
+
# Check if this is a real TracerProvider (not the default NoOpTracerProvider)
|
|
147
|
+
if isinstance(provider, TracerProvider):
|
|
148
|
+
provider.add_span_processor(processor)
|
|
149
|
+
logger.debug(f"Added trace collector to TracerProvider for session: {session_id}, entity: {entity_id}")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
yield collector
|
|
153
|
+
finally:
|
|
154
|
+
# Clean up - shutdown processor
|
|
155
|
+
try:
|
|
156
|
+
processor.shutdown()
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.debug(f"Error shutting down processor: {e}")
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(f"No real TracerProvider available, got: {type(provider)}")
|
|
161
|
+
yield collector
|
|
162
|
+
|
|
163
|
+
except ImportError:
|
|
164
|
+
logger.debug("OpenTelemetry not available")
|
|
165
|
+
yield collector
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Error setting up trace capture: {e}")
|
|
168
|
+
yield collector
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Agent Framework DevUI Models - OpenAI-compatible types and custom extensions."""
|
|
4
|
+
|
|
5
|
+
# Import discovery models
|
|
6
|
+
# Import all OpenAI types directly from the openai package
|
|
7
|
+
from openai.types.responses import (
|
|
8
|
+
Response,
|
|
9
|
+
ResponseErrorEvent,
|
|
10
|
+
ResponseFunctionCallArgumentsDeltaEvent,
|
|
11
|
+
ResponseInputParam,
|
|
12
|
+
ResponseOutputMessage,
|
|
13
|
+
ResponseOutputText,
|
|
14
|
+
ResponseReasoningTextDeltaEvent,
|
|
15
|
+
ResponseStreamEvent,
|
|
16
|
+
ResponseTextDeltaEvent,
|
|
17
|
+
ResponseUsage,
|
|
18
|
+
ToolParam,
|
|
19
|
+
)
|
|
20
|
+
from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
|
|
21
|
+
from openai.types.shared import Metadata, ResponsesModel
|
|
22
|
+
|
|
23
|
+
from ._discovery_models import DiscoveryResponse, EntityInfo
|
|
24
|
+
from ._openai_custom import (
|
|
25
|
+
AgentFrameworkRequest,
|
|
26
|
+
OpenAIError,
|
|
27
|
+
ResponseFunctionResultComplete,
|
|
28
|
+
ResponseFunctionResultDelta,
|
|
29
|
+
ResponseTraceEvent,
|
|
30
|
+
ResponseTraceEventComplete,
|
|
31
|
+
ResponseTraceEventDelta,
|
|
32
|
+
ResponseUsageEventComplete,
|
|
33
|
+
ResponseUsageEventDelta,
|
|
34
|
+
ResponseWorkflowEventComplete,
|
|
35
|
+
ResponseWorkflowEventDelta,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Type alias for compatibility
|
|
39
|
+
OpenAIResponse = Response
|
|
40
|
+
|
|
41
|
+
# Export all types for easy importing
|
|
42
|
+
__all__ = [
|
|
43
|
+
"AgentFrameworkRequest",
|
|
44
|
+
"DiscoveryResponse",
|
|
45
|
+
"EntityInfo",
|
|
46
|
+
"InputTokensDetails",
|
|
47
|
+
"Metadata",
|
|
48
|
+
"OpenAIError",
|
|
49
|
+
"OpenAIResponse",
|
|
50
|
+
"OutputTokensDetails",
|
|
51
|
+
"Response",
|
|
52
|
+
"ResponseErrorEvent",
|
|
53
|
+
"ResponseFunctionCallArgumentsDeltaEvent",
|
|
54
|
+
"ResponseFunctionResultComplete",
|
|
55
|
+
"ResponseFunctionResultDelta",
|
|
56
|
+
"ResponseInputParam",
|
|
57
|
+
"ResponseOutputMessage",
|
|
58
|
+
"ResponseOutputText",
|
|
59
|
+
"ResponseReasoningTextDeltaEvent",
|
|
60
|
+
"ResponseStreamEvent",
|
|
61
|
+
"ResponseTextDeltaEvent",
|
|
62
|
+
"ResponseTraceEvent",
|
|
63
|
+
"ResponseTraceEventComplete",
|
|
64
|
+
"ResponseTraceEventDelta",
|
|
65
|
+
"ResponseUsage",
|
|
66
|
+
"ResponseUsageEventComplete",
|
|
67
|
+
"ResponseUsageEventDelta",
|
|
68
|
+
"ResponseWorkflowEventComplete",
|
|
69
|
+
"ResponseWorkflowEventDelta",
|
|
70
|
+
"ResponsesModel",
|
|
71
|
+
"ToolParam",
|
|
72
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Discovery API models for entity information."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnvVarRequirement(BaseModel):
|
|
13
|
+
"""Environment variable requirement for an entity."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
required: bool = True
|
|
18
|
+
example: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EntityInfo(BaseModel):
|
|
22
|
+
"""Entity information for discovery and detailed views."""
|
|
23
|
+
|
|
24
|
+
# Always present (core entity data)
|
|
25
|
+
id: str
|
|
26
|
+
type: str # "agent", "workflow"
|
|
27
|
+
name: str
|
|
28
|
+
description: str | None = None
|
|
29
|
+
framework: str
|
|
30
|
+
tools: list[str | dict[str, Any]] | None = None
|
|
31
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
# Source information
|
|
34
|
+
source: str = "directory" # "directory", "in_memory", "remote_gallery"
|
|
35
|
+
original_url: str | None = None
|
|
36
|
+
|
|
37
|
+
# Environment variable requirements
|
|
38
|
+
required_env_vars: list[EnvVarRequirement] | None = None
|
|
39
|
+
|
|
40
|
+
# Workflow-specific fields (populated only for detailed info requests)
|
|
41
|
+
executors: list[str] | None = None
|
|
42
|
+
workflow_dump: dict[str, Any] | None = None
|
|
43
|
+
input_schema: dict[str, Any] | None = None
|
|
44
|
+
input_type_name: str | None = None
|
|
45
|
+
start_executor_id: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DiscoveryResponse(BaseModel):
|
|
49
|
+
"""Response model for entity discovery."""
|
|
50
|
+
|
|
51
|
+
entities: list[EntityInfo] = Field(default_factory=list)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Custom OpenAI-compatible event types for Agent Framework extensions.
|
|
4
|
+
|
|
5
|
+
These are custom event types that extend beyond the standard OpenAI Responses API
|
|
6
|
+
to support Agent Framework specific features like workflows, traces, and function results.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
# Custom Agent Framework OpenAI event types for structured data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResponseWorkflowEventDelta(BaseModel):
|
|
19
|
+
"""Structured workflow event with completion tracking."""
|
|
20
|
+
|
|
21
|
+
type: Literal["response.workflow_event.delta"] = "response.workflow_event.delta"
|
|
22
|
+
delta: dict[str, Any]
|
|
23
|
+
executor_id: str | None = None
|
|
24
|
+
is_complete: bool = False # Track if this is the final part
|
|
25
|
+
item_id: str
|
|
26
|
+
output_index: int = 0
|
|
27
|
+
sequence_number: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResponseWorkflowEventComplete(BaseModel):
|
|
31
|
+
"""Complete workflow event data."""
|
|
32
|
+
|
|
33
|
+
type: Literal["response.workflow_event.complete"] = "response.workflow_event.complete"
|
|
34
|
+
data: dict[str, Any] # Complete event data, not delta
|
|
35
|
+
executor_id: str | None = None
|
|
36
|
+
item_id: str
|
|
37
|
+
output_index: int = 0
|
|
38
|
+
sequence_number: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ResponseFunctionResultDelta(BaseModel):
|
|
42
|
+
"""Structured function result with completion tracking."""
|
|
43
|
+
|
|
44
|
+
type: Literal["response.function_result.delta"] = "response.function_result.delta"
|
|
45
|
+
delta: dict[str, Any]
|
|
46
|
+
call_id: str
|
|
47
|
+
is_complete: bool = False
|
|
48
|
+
item_id: str
|
|
49
|
+
output_index: int = 0
|
|
50
|
+
sequence_number: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ResponseFunctionResultComplete(BaseModel):
|
|
54
|
+
"""Complete function result data."""
|
|
55
|
+
|
|
56
|
+
type: Literal["response.function_result.complete"] = "response.function_result.complete"
|
|
57
|
+
data: dict[str, Any] # Complete function result data, not delta
|
|
58
|
+
call_id: str
|
|
59
|
+
item_id: str
|
|
60
|
+
output_index: int = 0
|
|
61
|
+
sequence_number: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ResponseTraceEventDelta(BaseModel):
|
|
65
|
+
"""Structured trace event with completion tracking."""
|
|
66
|
+
|
|
67
|
+
type: Literal["response.trace.delta"] = "response.trace.delta"
|
|
68
|
+
delta: dict[str, Any]
|
|
69
|
+
span_id: str | None = None
|
|
70
|
+
is_complete: bool = False
|
|
71
|
+
item_id: str
|
|
72
|
+
output_index: int = 0
|
|
73
|
+
sequence_number: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ResponseTraceEventComplete(BaseModel):
|
|
77
|
+
"""Complete trace event data."""
|
|
78
|
+
|
|
79
|
+
type: Literal["response.trace.complete"] = "response.trace.complete"
|
|
80
|
+
data: dict[str, Any] # Complete trace data, not delta
|
|
81
|
+
span_id: str | None = None
|
|
82
|
+
item_id: str
|
|
83
|
+
output_index: int = 0
|
|
84
|
+
sequence_number: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ResponseUsageEventDelta(BaseModel):
|
|
88
|
+
"""Structured usage event with completion tracking."""
|
|
89
|
+
|
|
90
|
+
type: Literal["response.usage.delta"] = "response.usage.delta"
|
|
91
|
+
delta: dict[str, Any]
|
|
92
|
+
is_complete: bool = False
|
|
93
|
+
item_id: str
|
|
94
|
+
output_index: int = 0
|
|
95
|
+
sequence_number: int
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ResponseUsageEventComplete(BaseModel):
|
|
99
|
+
"""Complete usage event data."""
|
|
100
|
+
|
|
101
|
+
type: Literal["response.usage.complete"] = "response.usage.complete"
|
|
102
|
+
data: dict[str, Any] # Complete usage data, not delta
|
|
103
|
+
item_id: str
|
|
104
|
+
output_index: int = 0
|
|
105
|
+
sequence_number: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Agent Framework extension fields
|
|
109
|
+
class AgentFrameworkExtraBody(BaseModel):
|
|
110
|
+
"""Agent Framework specific routing fields for OpenAI requests."""
|
|
111
|
+
|
|
112
|
+
entity_id: str
|
|
113
|
+
thread_id: str | None = None
|
|
114
|
+
input_data: dict[str, Any] | None = None
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(extra="allow")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Agent Framework Request Model - Extending real OpenAI types
|
|
120
|
+
class AgentFrameworkRequest(BaseModel):
|
|
121
|
+
"""OpenAI ResponseCreateParams with Agent Framework extensions.
|
|
122
|
+
|
|
123
|
+
This properly extends the real OpenAI API request format while adding
|
|
124
|
+
our custom routing fields in extra_body.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# All OpenAI fields from ResponseCreateParams
|
|
128
|
+
model: str
|
|
129
|
+
input: str | list[Any] # ResponseInputParam
|
|
130
|
+
stream: bool | None = False
|
|
131
|
+
|
|
132
|
+
# Common OpenAI optional fields
|
|
133
|
+
instructions: str | None = None
|
|
134
|
+
metadata: dict[str, Any] | None = None
|
|
135
|
+
temperature: float | None = None
|
|
136
|
+
max_output_tokens: int | None = None
|
|
137
|
+
tools: list[dict[str, Any]] | None = None
|
|
138
|
+
|
|
139
|
+
# Agent Framework extension - strongly typed
|
|
140
|
+
extra_body: AgentFrameworkExtraBody | None = None
|
|
141
|
+
|
|
142
|
+
entity_id: str | None = None # Allow entity_id as top-level field
|
|
143
|
+
|
|
144
|
+
model_config = ConfigDict(extra="allow")
|
|
145
|
+
|
|
146
|
+
def get_entity_id(self) -> str | None:
|
|
147
|
+
"""Get entity_id from either top-level field or extra_body."""
|
|
148
|
+
# Priority 1: Top-level entity_id field
|
|
149
|
+
if self.entity_id:
|
|
150
|
+
return self.entity_id
|
|
151
|
+
|
|
152
|
+
# Priority 2: entity_id in extra_body
|
|
153
|
+
if self.extra_body and hasattr(self.extra_body, "entity_id"):
|
|
154
|
+
return self.extra_body.entity_id
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def to_openai_params(self) -> dict[str, Any]:
|
|
159
|
+
"""Convert to dict for OpenAI client compatibility."""
|
|
160
|
+
data = self.model_dump(exclude={"extra_body", "entity_id"}, exclude_none=True)
|
|
161
|
+
if self.extra_body:
|
|
162
|
+
# Don't merge extra_body into main params to keep them separate
|
|
163
|
+
data["extra_body"] = self.extra_body
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Error handling
|
|
168
|
+
class ResponseTraceEvent(BaseModel):
|
|
169
|
+
"""Trace event for execution tracing."""
|
|
170
|
+
|
|
171
|
+
type: Literal["trace_event"] = "trace_event"
|
|
172
|
+
data: dict[str, Any]
|
|
173
|
+
timestamp: str
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class OpenAIError(BaseModel):
|
|
177
|
+
"""OpenAI standard error response model."""
|
|
178
|
+
|
|
179
|
+
error: dict[str, Any]
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def create(cls, message: str, type: str = "invalid_request_error", code: str | None = None) -> OpenAIError:
|
|
183
|
+
"""Create a standard OpenAI error response."""
|
|
184
|
+
error_data = {"message": message, "type": type, "code": code}
|
|
185
|
+
return cls(error=error_data)
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> dict[str, Any]:
|
|
188
|
+
"""Return the error payload as a plain mapping."""
|
|
189
|
+
return {"error": dict(self.error)}
|
|
190
|
+
|
|
191
|
+
def to_json(self) -> str:
|
|
192
|
+
"""Return the error payload serialized to JSON."""
|
|
193
|
+
return self.model_dump_json()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Export all custom types
|
|
197
|
+
__all__ = [
|
|
198
|
+
"AgentFrameworkRequest",
|
|
199
|
+
"OpenAIError",
|
|
200
|
+
"ResponseFunctionResultComplete",
|
|
201
|
+
"ResponseFunctionResultDelta",
|
|
202
|
+
"ResponseTraceEvent",
|
|
203
|
+
"ResponseTraceEventComplete",
|
|
204
|
+
"ResponseTraceEventDelta",
|
|
205
|
+
"ResponseUsageEventComplete",
|
|
206
|
+
"ResponseUsageEventDelta",
|
|
207
|
+
"ResponseWorkflowEventComplete",
|
|
208
|
+
"ResponseWorkflowEventDelta",
|
|
209
|
+
]
|