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.

@@ -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