agent-framework-devui 1.0.0b260106__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.
@@ -0,0 +1,277 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Agent Framework DevUI - Debug interface with OpenAI compatible API server."""
4
+
5
+ import importlib.metadata
6
+ import logging
7
+ import webbrowser
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+ from ._conversations import CheckpointConversationManager
12
+ from ._server import DevServer
13
+ from .models import AgentFrameworkRequest, OpenAIError, OpenAIResponse, ResponseStreamEvent
14
+ from .models._discovery_models import DiscoveryResponse, EntityInfo, EnvVarRequirement
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Module-level cleanup registry (before serve() is called)
19
+ _cleanup_registry: dict[int, list[Callable[[], Any]]] = {}
20
+
21
+ try:
22
+ __version__ = importlib.metadata.version(__name__)
23
+ except importlib.metadata.PackageNotFoundError:
24
+ __version__ = "0.0.0" # Fallback for development mode
25
+
26
+
27
+ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None:
28
+ """Register cleanup hook(s) for an entity.
29
+
30
+ Cleanup hooks execute during DevUI server shutdown, before entity
31
+ clients are closed. Supports both synchronous and asynchronous callables.
32
+
33
+ Args:
34
+ entity: Agent, workflow, or other entity object
35
+ *hooks: One or more cleanup callables (sync or async)
36
+
37
+ Raises:
38
+ ValueError: If no hooks provided
39
+
40
+ Examples:
41
+ Single cleanup hook:
42
+ >>> from agent_framework.devui import serve, register_cleanup
43
+ >>> credential = DefaultAzureCredential()
44
+ >>> agent = ChatAgent(...)
45
+ >>> register_cleanup(agent, credential.close)
46
+ >>> serve(entities=[agent])
47
+
48
+ Multiple cleanup hooks:
49
+ >>> register_cleanup(agent, credential.close, session.close, db_pool.close)
50
+
51
+ Works with file-based discovery:
52
+ >>> # In agents/my_agent/agent.py
53
+ >>> from agent_framework.devui import register_cleanup
54
+ >>> credential = DefaultAzureCredential()
55
+ >>> agent = ChatAgent(...)
56
+ >>> register_cleanup(agent, credential.close)
57
+ >>> # Run: devui ./agents
58
+ """
59
+ if not hooks:
60
+ raise ValueError("At least one cleanup hook required")
61
+
62
+ # Use id() to track entity identity (works across modules)
63
+ entity_id = id(entity)
64
+
65
+ if entity_id not in _cleanup_registry:
66
+ _cleanup_registry[entity_id] = []
67
+
68
+ _cleanup_registry[entity_id].extend(hooks)
69
+
70
+ logger.debug(
71
+ f"Registered {len(hooks)} cleanup hook(s) for {type(entity).__name__} "
72
+ f"(id: {entity_id}, total: {len(_cleanup_registry[entity_id])})"
73
+ )
74
+
75
+
76
+ def _get_registered_cleanup_hooks(entity: Any) -> list[Callable[[], Any]]:
77
+ """Get cleanup hooks registered for an entity (internal use).
78
+
79
+ Args:
80
+ entity: Entity object to get hooks for
81
+
82
+ Returns:
83
+ List of cleanup hooks registered for the entity
84
+ """
85
+ entity_id = id(entity)
86
+ return _cleanup_registry.get(entity_id, [])
87
+
88
+
89
+ def serve(
90
+ entities: list[Any] | None = None,
91
+ entities_dir: str | None = None,
92
+ port: int = 8080,
93
+ host: str = "127.0.0.1",
94
+ auto_open: bool = False,
95
+ cors_origins: list[str] | None = None,
96
+ ui_enabled: bool = True,
97
+ tracing_enabled: bool = False,
98
+ mode: str = "developer",
99
+ auth_enabled: bool = False,
100
+ auth_token: str | None = None,
101
+ ) -> None:
102
+ """Launch Agent Framework DevUI with simple API.
103
+
104
+ Args:
105
+ entities: List of entities for in-memory registration (IDs auto-generated)
106
+ entities_dir: Directory to scan for entities
107
+ port: Port to run server on
108
+ host: Host to bind server to
109
+ auto_open: Whether to automatically open browser
110
+ cors_origins: List of allowed CORS origins
111
+ ui_enabled: Whether to enable the UI
112
+ tracing_enabled: Whether to enable OpenTelemetry tracing
113
+ mode: Server mode - 'developer' (full access, verbose errors) or 'user' (restricted APIs, generic errors)
114
+ auth_enabled: Whether to enable Bearer token authentication
115
+ auth_token: Custom authentication token (auto-generated if not provided with auth_enabled=True)
116
+ """
117
+ import re
118
+
119
+ import uvicorn
120
+
121
+ # Validate host parameter early for security
122
+ if not re.match(r"^(localhost|127\.0\.0\.1|0\.0\.0\.0|[a-zA-Z0-9.-]+)$", host):
123
+ raise ValueError(f"Invalid host: {host}. Must be localhost, IP address, or valid hostname")
124
+
125
+ # Validate port parameter
126
+ if not isinstance(port, int) or not (1 <= port <= 65535):
127
+ raise ValueError(f"Invalid port: {port}. Must be integer between 1 and 65535")
128
+
129
+ # Security check: Warn if network-exposed without authentication
130
+ if host not in ("127.0.0.1", "localhost") and not auth_enabled:
131
+ logger.warning("⚠️ WARNING: Exposing DevUI to network without authentication!")
132
+ logger.warning("⚠️ This is INSECURE - anyone on your network can access your agents")
133
+ logger.warning("💡 For network exposure, add --auth flag: devui --host 0.0.0.0 --auth")
134
+
135
+ # Handle authentication configuration
136
+ if auth_enabled:
137
+ import os
138
+ import secrets
139
+
140
+ # Check if token is in environment variable first
141
+ if not auth_token:
142
+ auth_token = os.environ.get("DEVUI_AUTH_TOKEN")
143
+
144
+ # Auto-generate token if STILL not provided
145
+ if not auth_token:
146
+ # Check if we're in a production-like environment
147
+ is_production = (
148
+ host not in ("127.0.0.1", "localhost") # Exposed to network
149
+ or os.environ.get("CI") == "true" # Running in CI
150
+ or os.environ.get("KUBERNETES_SERVICE_HOST") # Running in k8s
151
+ )
152
+
153
+ if is_production:
154
+ # REFUSE to start without explicit token
155
+ logger.error("❌ Authentication enabled but no token provided")
156
+ logger.error("❌ Auto-generated tokens are NOT secure for network-exposed deployments")
157
+ logger.error("💡 Set token: export DEVUI_AUTH_TOKEN=<your-secure-token>")
158
+ logger.error("💡 Or pass: serve(entities=[...], auth_token='your-token')")
159
+ raise ValueError("DEVUI_AUTH_TOKEN required when host is not localhost")
160
+
161
+ # Development mode: auto-generate and show
162
+ auth_token = secrets.token_urlsafe(32)
163
+ logger.info("🔒 Authentication enabled with auto-generated token")
164
+ logger.info("\n" + "=" * 70)
165
+ logger.info("🔑 DEV TOKEN (localhost only, shown once):")
166
+ logger.info(f" {auth_token}")
167
+ logger.info("=" * 70 + "\n")
168
+ else:
169
+ logger.info("🔒 Authentication enabled with provided token")
170
+
171
+ # Set environment variable for server to use
172
+ os.environ["AUTH_REQUIRED"] = "true"
173
+ os.environ["DEVUI_AUTH_TOKEN"] = auth_token
174
+
175
+ # Configure tracing environment variables if enabled
176
+ if tracing_enabled:
177
+ import os
178
+
179
+ # Only set if not already configured by user
180
+ if not os.environ.get("ENABLE_INSTRUMENTATION"):
181
+ os.environ["ENABLE_INSTRUMENTATION"] = "true"
182
+ logger.info("Set ENABLE_INSTRUMENTATION=true for tracing")
183
+
184
+ if not os.environ.get("ENABLE_SENSITIVE_DATA"):
185
+ os.environ["ENABLE_SENSITIVE_DATA"] = "true"
186
+ logger.info("Set ENABLE_SENSITIVE_DATA=true for tracing")
187
+
188
+ if not os.environ.get("OTLP_ENDPOINT"):
189
+ os.environ["OTLP_ENDPOINT"] = "http://localhost:4317"
190
+ logger.info("Set OTLP_ENDPOINT=http://localhost:4317 for tracing")
191
+
192
+ # Create server with direct parameters
193
+ server = DevServer(
194
+ entities_dir=entities_dir,
195
+ port=port,
196
+ host=host,
197
+ cors_origins=cors_origins,
198
+ ui_enabled=ui_enabled,
199
+ mode=mode,
200
+ )
201
+
202
+ # Register in-memory entities if provided
203
+ if entities:
204
+ logger.info(f"Registering {len(entities)} in-memory entities")
205
+ # Store entities for later registration during server startup
206
+ server._pending_entities = entities
207
+
208
+ app = server.get_app()
209
+
210
+ if auto_open:
211
+
212
+ def open_browser() -> None:
213
+ import http.client
214
+ import re
215
+ import time
216
+
217
+ # Validate host and port for security
218
+ if not re.match(r"^(localhost|127\.0\.0\.1|0\.0\.0\.0|[a-zA-Z0-9.-]+)$", host):
219
+ logger.warning(f"Invalid host for auto-open: {host}")
220
+ return
221
+
222
+ if not isinstance(port, int) or not (1 <= port <= 65535):
223
+ logger.warning(f"Invalid port for auto-open: {port}")
224
+ return
225
+
226
+ # Wait for server to be ready by checking health endpoint
227
+ browser_url = f"http://{host}:{port}"
228
+
229
+ for _ in range(30): # 15 second timeout (30 * 0.5s)
230
+ try:
231
+ # Use http.client for safe connection handling (standard library)
232
+ conn = http.client.HTTPConnection(host, port, timeout=1)
233
+ try:
234
+ conn.request("GET", "/health")
235
+ response = conn.getresponse()
236
+ if response.status == 200:
237
+ webbrowser.open(browser_url)
238
+ return
239
+ finally:
240
+ conn.close()
241
+ except (http.client.HTTPException, OSError, TimeoutError):
242
+ pass
243
+ time.sleep(0.5)
244
+
245
+ # Fallback: open browser anyway after timeout
246
+ webbrowser.open(browser_url)
247
+
248
+ import threading
249
+
250
+ threading.Thread(target=open_browser, daemon=True).start()
251
+
252
+ logger.info(f"Starting Agent Framework DevUI on {host}:{port}")
253
+ uvicorn.run(app, host=host, port=port, log_level="info")
254
+
255
+
256
+ def main() -> None:
257
+ """CLI entry point for devui command."""
258
+ from ._cli import main as cli_main
259
+
260
+ cli_main()
261
+
262
+
263
+ # Export main public API
264
+ __all__ = [
265
+ "AgentFrameworkRequest",
266
+ "CheckpointConversationManager",
267
+ "DevServer",
268
+ "DiscoveryResponse",
269
+ "EntityInfo",
270
+ "EnvVarRequirement",
271
+ "OpenAIError",
272
+ "OpenAIResponse",
273
+ "ResponseStreamEvent",
274
+ "main",
275
+ "register_cleanup",
276
+ "serve",
277
+ ]
@@ -0,0 +1,201 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Command line interface for Agent Framework DevUI."""
4
+
5
+ import argparse
6
+ import logging
7
+ import os
8
+ import sys
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def setup_logging(level: str = "INFO") -> None:
14
+ """Configure logging for the server."""
15
+ log_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
16
+ logging.basicConfig(level=getattr(logging, level.upper()), format=log_format, datefmt="%Y-%m-%d %H:%M:%S")
17
+
18
+
19
+ def create_cli_parser() -> argparse.ArgumentParser:
20
+ """Create the command line argument parser."""
21
+ parser = argparse.ArgumentParser(
22
+ prog="devui",
23
+ description="Launch Agent Framework DevUI - Debug interface with OpenAI compatible API",
24
+ formatter_class=argparse.RawDescriptionHelpFormatter,
25
+ epilog="""
26
+ Examples:
27
+ devui # Scan current directory
28
+ devui ./agents # Scan specific directory
29
+ devui --port 8000 # Custom port
30
+ devui --headless # API only, no UI
31
+ devui --tracing # Enable OpenTelemetry tracing
32
+ """,
33
+ )
34
+
35
+ parser.add_argument(
36
+ "directory", nargs="?", default=".", help="Directory to scan for entities (default: current directory)"
37
+ )
38
+
39
+ parser.add_argument("--port", "-p", type=int, default=8080, help="Port to run server on (default: 8080)")
40
+
41
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind server to (default: 127.0.0.1)")
42
+
43
+ parser.add_argument("--no-open", action="store_true", help="Don't automatically open browser")
44
+
45
+ parser.add_argument("--headless", action="store_true", help="Run without UI (API only)")
46
+
47
+ parser.add_argument(
48
+ "--log-level",
49
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
50
+ default="INFO",
51
+ help="Logging level (default: INFO)",
52
+ )
53
+
54
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
55
+
56
+ parser.add_argument("--tracing", action="store_true", help="Enable OpenTelemetry tracing for Agent Framework")
57
+
58
+ parser.add_argument(
59
+ "--mode",
60
+ choices=["developer", "user"],
61
+ default=None,
62
+ help="Server mode - 'developer' (full access, verbose errors) or 'user' (restricted APIs, generic errors)",
63
+ )
64
+
65
+ # Add --dev/--no-dev as a convenient alternative to --mode
66
+ parser.add_argument(
67
+ "--dev",
68
+ dest="dev_mode",
69
+ action="store_true",
70
+ default=None,
71
+ help="Enable developer mode (shorthand for --mode developer)",
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--no-dev",
76
+ dest="dev_mode",
77
+ action="store_false",
78
+ help="Disable developer mode (shorthand for --mode user)",
79
+ )
80
+
81
+ parser.add_argument(
82
+ "--auth",
83
+ action="store_true",
84
+ help="Enable authentication via Bearer token (required for deployed environments)",
85
+ )
86
+
87
+ parser.add_argument(
88
+ "--auth-token",
89
+ type=str,
90
+ help="Custom authentication token (auto-generated if not provided with --auth)",
91
+ )
92
+
93
+ parser.add_argument("--version", action="version", version=f"Agent Framework DevUI {get_version()}")
94
+
95
+ return parser
96
+
97
+
98
+ def get_version() -> str:
99
+ """Get the package version."""
100
+ try:
101
+ from . import __version__
102
+
103
+ return __version__
104
+ except ImportError:
105
+ return "unknown"
106
+
107
+
108
+ def validate_directory(directory: str) -> str:
109
+ """Validate and normalize the entities directory."""
110
+ if not directory:
111
+ directory = "."
112
+
113
+ abs_dir = os.path.abspath(directory)
114
+
115
+ if not os.path.exists(abs_dir):
116
+ print(f"Error: Directory '{directory}' does not exist", file=sys.stderr) # noqa: T201
117
+ sys.exit(1)
118
+
119
+ if not os.path.isdir(abs_dir):
120
+ print(f"Error: '{directory}' is not a directory", file=sys.stderr) # noqa: T201
121
+ sys.exit(1)
122
+
123
+ return abs_dir
124
+
125
+
126
+ def print_startup_info(
127
+ entities_dir: str, host: str, port: int, ui_enabled: bool, reload: bool, auth_token: str | None = None
128
+ ) -> None:
129
+ """Print startup information."""
130
+ print("Agent Framework DevUI") # noqa: T201
131
+ print("=" * 50) # noqa: T201
132
+ print(f"Entities directory: {entities_dir}") # noqa: T201
133
+ print(f"Server URL: http://{host}:{port}") # noqa: T201
134
+ print(f"UI enabled: {'Yes' if ui_enabled else 'No'}") # noqa: T201
135
+ print(f"Auto-reload: {'Yes' if reload else 'No'}") # noqa: T201
136
+
137
+ # Display auth token if authentication is enabled
138
+ if auth_token:
139
+ print("Authentication: Enabled") # noqa: T201
140
+ print(f"Auth token: {auth_token}") # noqa: T201
141
+ print("💡 Use this token in Authorization: Bearer <token> header") # noqa: T201
142
+
143
+ print("=" * 50) # noqa: T201
144
+ print("Scanning for entities...") # noqa: T201
145
+
146
+
147
+ def main() -> None:
148
+ """Main CLI entry point."""
149
+ parser = create_cli_parser()
150
+ args = parser.parse_args()
151
+
152
+ # Setup logging
153
+ setup_logging(args.log_level)
154
+
155
+ # Validate directory
156
+ entities_dir = validate_directory(args.directory)
157
+
158
+ # Extract parameters directly from args
159
+ ui_enabled = not args.headless
160
+
161
+ # Determine mode from --mode or --dev/--no-dev flags
162
+ if args.dev_mode is not None:
163
+ # --dev or --no-dev was specified
164
+ mode = "developer" if args.dev_mode else "user"
165
+ elif args.mode is not None:
166
+ # --mode was specified
167
+ mode = args.mode
168
+ else:
169
+ # Default to developer mode
170
+ mode = "developer"
171
+
172
+ # Print startup info (don't show token - serve() will handle it)
173
+ print_startup_info(entities_dir, args.host, args.port, ui_enabled, args.reload, None)
174
+
175
+ # Import and start server
176
+ try:
177
+ from . import serve
178
+
179
+ serve(
180
+ entities_dir=entities_dir,
181
+ port=args.port,
182
+ host=args.host,
183
+ auto_open=not args.no_open,
184
+ ui_enabled=ui_enabled,
185
+ tracing_enabled=args.tracing,
186
+ mode=mode,
187
+ auth_enabled=args.auth,
188
+ auth_token=args.auth_token, # Pass through explicit token only
189
+ )
190
+
191
+ except KeyboardInterrupt:
192
+ print("\nShutting down Agent Framework DevUI...") # noqa: T201
193
+ sys.exit(0)
194
+ except Exception as e:
195
+ logger.exception("Failed to start server")
196
+ print(f"Error: {e}", file=sys.stderr) # noqa: T201
197
+ sys.exit(1)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()