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,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_id: 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_id: 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_id": model_id,
|
|
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
|