dtSpark 1.0.4__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.
Files changed (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
dtSpark/web/server.py ADDED
@@ -0,0 +1,578 @@
1
+ """
2
+ FastAPI server for the web interface.
3
+
4
+ Provides HTTP endpoints and SSE streaming for the Spark web UI.
5
+
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import socket
11
+ import logging
12
+ import webbrowser
13
+ import signal
14
+ import asyncio
15
+ from typing import Optional
16
+ from pathlib import Path
17
+
18
+ import uvicorn
19
+ from fastapi import FastAPI, Request, HTTPException, Depends, Cookie
20
+ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
21
+ from fastapi.staticfiles import StaticFiles
22
+ from fastapi.templating import Jinja2Templates
23
+
24
+ from .auth import AuthManager
25
+ from .session import SessionManager
26
+ from .ssl_utils import setup_ssl_certificates
27
+
28
+ # Import application metadata functions
29
+ from dtSpark.core.application import version, full_name, agent_name, description
30
+
31
+
32
+ # Logger for web server
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class WebServer:
37
+ """
38
+ FastAPI-based web server for Spark.
39
+
40
+ Provides web interface as alternative to CLI, with authentication,
41
+ session management, and real-time streaming.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ app_instance, # AWSBedrockCLI instance
47
+ host: str = "127.0.0.1",
48
+ port: int = 0,
49
+ session_timeout_minutes: int = 0,
50
+ dark_theme: bool = True,
51
+ ssl_enabled: bool = False,
52
+ ssl_cert_file: Optional[str] = None,
53
+ ssl_key_file: Optional[str] = None,
54
+ ssl_auto_generate: bool = True,
55
+ auto_open_browser: bool = False,
56
+ ):
57
+ """
58
+ Initialise the web server.
59
+
60
+ Args:
61
+ app_instance: Instance of AWSBedrockCLI
62
+ host: Host to bind to (default: 127.0.0.1 for localhost only)
63
+ port: Port to bind to (0 for random available port)
64
+ session_timeout_minutes: Session timeout in minutes
65
+ dark_theme: Whether to use dark theme
66
+ ssl_enabled: Whether to enable HTTPS with SSL/TLS
67
+ ssl_cert_file: Path to SSL certificate file (required if ssl_enabled)
68
+ ssl_key_file: Path to SSL private key file (required if ssl_enabled)
69
+ ssl_auto_generate: Whether to auto-generate self-signed certificate if files not found
70
+ auto_open_browser: Whether to automatically open browser with authentication URL
71
+ """
72
+ self.app_instance = app_instance
73
+ self.host = host
74
+ self.port = port
75
+ self.session_timeout_minutes = session_timeout_minutes
76
+ self.dark_theme = dark_theme
77
+ self.ssl_enabled = ssl_enabled
78
+ self.auto_open_browser = auto_open_browser
79
+
80
+ # SSL certificate paths
81
+ self.ssl_cert_file = None
82
+ self.ssl_key_file = None
83
+
84
+ # Setup SSL certificates if enabled
85
+ if self.ssl_enabled:
86
+ logger.info("SSL enabled - setting up certificates")
87
+ success, cert_path, key_path = setup_ssl_certificates(
88
+ cert_file=ssl_cert_file,
89
+ key_file=ssl_key_file,
90
+ auto_generate=ssl_auto_generate,
91
+ hostname=host if host != "0.0.0.0" else "localhost",
92
+ )
93
+ if success:
94
+ self.ssl_cert_file = cert_path
95
+ self.ssl_key_file = key_path
96
+ logger.info(f"SSL certificates ready: {cert_path}, {key_path}")
97
+ else:
98
+ logger.error("Failed to setup SSL certificates - SSL will be disabled")
99
+ self.ssl_enabled = False
100
+
101
+ # Initialise auth and session managers
102
+ self.auth_manager = AuthManager()
103
+ self.session_manager = SessionManager(timeout_minutes=session_timeout_minutes)
104
+
105
+ # Initialise web interface for tool permission prompts
106
+ from .web_interface import WebInterface
107
+ self.web_interface = WebInterface()
108
+ # Set web interface on conversation manager so it uses web prompts instead of CLI
109
+ if hasattr(app_instance, 'conversation_manager') and app_instance.conversation_manager:
110
+ app_instance.conversation_manager.web_interface = self.web_interface
111
+ logger.info("Web interface set on conversation manager for tool permission prompts")
112
+
113
+ # Generate one-time authentication code
114
+ self.auth_code = self.auth_manager.generate_code()
115
+
116
+ # Get cost tracking configuration from app instance settings
117
+ from dtPyAppFramework.settings import Settings
118
+ settings = Settings()
119
+ cost_tracking_enabled = settings.get('llm_providers.aws_bedrock.cost_tracking.enabled', None)
120
+ if cost_tracking_enabled is None:
121
+ cost_tracking_enabled = settings.get('aws.cost_tracking.enabled', False)
122
+
123
+ # Create FastAPI app
124
+ self.app = create_app(
125
+ auth_manager=self.auth_manager,
126
+ session_manager=self.session_manager,
127
+ app_instance=self.app_instance,
128
+ dark_theme=self.dark_theme,
129
+ cost_tracking_enabled=cost_tracking_enabled,
130
+ ssl_enabled=self.ssl_enabled,
131
+ session_timeout_minutes=self.session_timeout_minutes,
132
+ )
133
+
134
+ # Determine actual port if using random port
135
+ if self.port == 0:
136
+ self.port = self._find_free_port()
137
+
138
+ def get_access_info(self) -> dict:
139
+ """
140
+ Get information needed to access the web interface.
141
+
142
+ Returns:
143
+ Dictionary with URL and authentication code
144
+ """
145
+ protocol = "https" if self.ssl_enabled else "http"
146
+ return {
147
+ 'url': f"{protocol}://{self.host}:{self.port}",
148
+ 'code': self.auth_code,
149
+ 'host': self.host,
150
+ 'port': self.port,
151
+ 'ssl_enabled': self.ssl_enabled,
152
+ }
153
+
154
+ def run(self):
155
+ """
156
+ Start the web server.
157
+
158
+ Blocks until server is shut down.
159
+ """
160
+ protocol = "https" if self.ssl_enabled else "http"
161
+ logger.info(f"Starting web server on {protocol}://{self.host}:{self.port}")
162
+ logger.info(f"Authentication code: {self.auth_code}")
163
+ logger.info(f"SSL enabled: {self.ssl_enabled}")
164
+
165
+ # Auto-open browser if enabled
166
+ if self.auto_open_browser:
167
+ access_info = self.get_access_info()
168
+ auth_url = f"{access_info['url']}/login?code={self.auth_code}"
169
+ logger.info(f"Attempting to open browser: {auth_url}")
170
+ try:
171
+ webbrowser.open(auth_url)
172
+ logger.info("Browser opened successfully")
173
+ except Exception as e:
174
+ logger.warning(f"Failed to open browser: {e}")
175
+ logger.info("Please open the URL manually in your browser")
176
+
177
+ # Prepare uvicorn configuration
178
+ uvicorn_config = {
179
+ "app": self.app,
180
+ "host": self.host,
181
+ "port": self.port,
182
+ "log_level": "info",
183
+ }
184
+
185
+ # Add SSL configuration if enabled
186
+ if self.ssl_enabled and self.ssl_cert_file and self.ssl_key_file:
187
+ uvicorn_config["ssl_keyfile"] = self.ssl_key_file
188
+ uvicorn_config["ssl_certfile"] = self.ssl_cert_file
189
+ logger.info(f"SSL configured with cert: {self.ssl_cert_file}")
190
+
191
+ uvicorn.run(**uvicorn_config)
192
+
193
+ @staticmethod
194
+ def _find_free_port() -> int:
195
+ """
196
+ Find a free port on localhost.
197
+
198
+ Returns:
199
+ Available port number
200
+ """
201
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
202
+ s.bind(('', 0))
203
+ s.listen(1)
204
+ port = s.getsockname()[1]
205
+ return port
206
+
207
+
208
+ def create_app(
209
+ auth_manager: AuthManager,
210
+ session_manager: SessionManager,
211
+ app_instance, # AWSBedrockCLI instance
212
+ dark_theme: bool = True,
213
+ cost_tracking_enabled: bool = False,
214
+ ssl_enabled: bool = False,
215
+ session_timeout_minutes: int = 0,
216
+ ) -> FastAPI:
217
+ """
218
+ Create and configure the FastAPI application.
219
+
220
+ Args:
221
+ auth_manager: Authentication manager instance
222
+ session_manager: Session manager instance
223
+ app_instance: AWSBedrockCLI instance
224
+ dark_theme: Whether to use dark theme
225
+ cost_tracking_enabled: Whether cost tracking is enabled
226
+ ssl_enabled: Whether SSL/HTTPS is enabled (affects cookie security)
227
+ session_timeout_minutes: Session timeout in minutes (0 = no timeout)
228
+
229
+ Returns:
230
+ Configured FastAPI application
231
+ """
232
+ app = FastAPI(
233
+ title=f"{full_name()} Web Interface",
234
+ description="Web interface for AWS Bedrock CLI with MCP integration",
235
+ version=version(),
236
+ )
237
+
238
+ # Custom exception handler to suppress Windows asyncio connection reset errors
239
+ def _suppress_connection_reset_errors(loop, context):
240
+ """Suppress ConnectionResetError noise on Windows."""
241
+ exception = context.get('exception')
242
+ if isinstance(exception, ConnectionResetError):
243
+ # Silently ignore connection reset errors (common on Windows when browser closes)
244
+ return
245
+ # For other exceptions, use the default handler
246
+ loop.default_exception_handler(context)
247
+
248
+ @app.on_event("startup")
249
+ async def setup_exception_handler():
250
+ """Install custom exception handler on startup."""
251
+ if sys.platform == 'win32':
252
+ loop = asyncio.get_running_loop()
253
+ loop.set_exception_handler(_suppress_connection_reset_errors)
254
+
255
+ # Get template and static directories
256
+ web_dir = Path(__file__).parent
257
+ templates_dir = web_dir / "templates"
258
+ static_dir = web_dir / "static"
259
+
260
+ # Setup templates
261
+ templates = Jinja2Templates(directory=str(templates_dir))
262
+
263
+ # Add global template variables for app name and version
264
+ templates.env.globals['app_name'] = full_name()
265
+ templates.env.globals['app_version'] = version()
266
+ templates.env.globals['app_description'] = description()
267
+ templates.env.globals['agent_name'] = agent_name()
268
+
269
+ # Mount static files
270
+ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
271
+
272
+ # Store references for use in endpoints
273
+ app.state.auth_manager = auth_manager
274
+ app.state.session_manager = session_manager
275
+ app.state.app_instance = app_instance
276
+ app.state.templates = templates
277
+ app.state.dark_theme = dark_theme
278
+ app.state.cost_tracking_enabled = cost_tracking_enabled
279
+
280
+ # Session dependency
281
+ async def get_session(session_id: Optional[str] = Cookie(default=None)) -> str:
282
+ """
283
+ Dependency to validate session.
284
+
285
+ Raises:
286
+ HTTPException: If session is invalid or expired
287
+ """
288
+ if session_id is None:
289
+ raise HTTPException(status_code=401, detail="Not authenticated")
290
+
291
+ if not session_manager.validate_session(session_id):
292
+ raise HTTPException(status_code=401, detail="Session expired or invalid")
293
+
294
+ return session_id
295
+
296
+ # Store session dependency for use in routers
297
+ app.state.get_session = get_session
298
+
299
+ # Custom exception handler for HTTPException - redirect to login for 401 on HTML pages
300
+ @app.exception_handler(HTTPException)
301
+ async def http_exception_handler(request: Request, exc: HTTPException):
302
+ """
303
+ Handle HTTP exceptions.
304
+
305
+ For 401 errors on HTML page requests, redirect to login page.
306
+ For API requests or other errors, return appropriate response.
307
+ """
308
+ if exc.status_code == 401:
309
+ # Check if this is an API request or a page request
310
+ accept_header = request.headers.get("accept", "")
311
+ is_api_request = (
312
+ request.url.path.startswith("/api/") or
313
+ "application/json" in accept_header or
314
+ request.headers.get("x-requested-with") == "XMLHttpRequest"
315
+ )
316
+
317
+ if not is_api_request:
318
+ # Redirect to login for page requests
319
+ logger.info(f"Session invalid for page request {request.url.path}, redirecting to login")
320
+ return RedirectResponse(url="/login", status_code=303)
321
+
322
+ # Return JSON error for API requests and other errors
323
+ return JSONResponse(
324
+ status_code=exc.status_code,
325
+ content={"detail": exc.detail if hasattr(exc, 'detail') else str(exc)}
326
+ )
327
+
328
+ # Basic routes
329
+ @app.get("/", response_class=HTMLResponse)
330
+ async def root(request: Request):
331
+ """Root endpoint - redirects to login page."""
332
+ return RedirectResponse(url="/login")
333
+
334
+ @app.get("/login", response_class=HTMLResponse)
335
+ async def login_page(request: Request):
336
+ """Display login page."""
337
+ # Check if code is provided as query parameter (for auto-open browser)
338
+ code = request.query_params.get("code", "")
339
+
340
+ return templates.TemplateResponse(
341
+ "login.html",
342
+ {
343
+ "request": request,
344
+ "dark_theme": dark_theme,
345
+ "code": code, # Pass code to template for auto-fill
346
+ }
347
+ )
348
+
349
+ @app.post("/login")
350
+ async def login(request: Request):
351
+ """
352
+ Handle login form submission.
353
+
354
+ Validates one-time code and creates session.
355
+ """
356
+ form = await request.form()
357
+ code = form.get("code", "").strip().upper()
358
+
359
+ # Validate code
360
+ if not auth_manager.validate_code(code):
361
+ return templates.TemplateResponse(
362
+ "login.html",
363
+ {
364
+ "request": request,
365
+ "dark_theme": dark_theme,
366
+ "error": "Invalid or expired authentication code",
367
+ }
368
+ )
369
+
370
+ # Create session
371
+ session_id = session_manager.create_session()
372
+
373
+ # Redirect to main menu with session cookie
374
+ response = RedirectResponse(url="/menu", status_code=303)
375
+
376
+ # Calculate cookie max_age:
377
+ # - If session_timeout_minutes > 0, use that value
378
+ # - If session_timeout_minutes == 0 (no timeout), use 1 year (persistent session)
379
+ if session_timeout_minutes > 0:
380
+ cookie_max_age = session_timeout_minutes * 60 # Convert to seconds
381
+ else:
382
+ cookie_max_age = 365 * 24 * 60 * 60 # 1 year in seconds
383
+
384
+ response.set_cookie(
385
+ key="session_id",
386
+ value=session_id,
387
+ httponly=True,
388
+ secure=ssl_enabled, # Use secure cookies when SSL is enabled
389
+ samesite="strict",
390
+ max_age=cookie_max_age, # Persistent cookie instead of session cookie
391
+ )
392
+
393
+ return response
394
+
395
+ @app.get("/logout")
396
+ async def logout(request: Request, session_id: str = Depends(get_session)):
397
+ """Handle logout - invalidate session and show goodbye page."""
398
+ session_manager.invalidate_session()
399
+ response = templates.TemplateResponse(
400
+ "goodbye.html",
401
+ {
402
+ "request": request,
403
+ "dark_theme": dark_theme,
404
+ "shutdown": False,
405
+ }
406
+ )
407
+ response.delete_cookie(key="session_id")
408
+ return response
409
+
410
+ @app.get("/quit")
411
+ async def quit_app(request: Request, session_id: str = Depends(get_session)):
412
+ """Handle quit - invalidate session, show goodbye page, and trigger shutdown."""
413
+ session_manager.invalidate_session()
414
+ response = templates.TemplateResponse(
415
+ "goodbye.html",
416
+ {
417
+ "request": request,
418
+ "dark_theme": dark_theme,
419
+ "shutdown": True,
420
+ }
421
+ )
422
+ response.delete_cookie(key="session_id")
423
+ return response
424
+
425
+ @app.post("/api/shutdown")
426
+ async def shutdown():
427
+ """Shutdown the web server."""
428
+ logger.info("Shutdown request received via API")
429
+ # Send shutdown signal to the process
430
+ # Use a background task to allow the response to be sent first
431
+ import asyncio
432
+ async def shutdown_server():
433
+ await asyncio.sleep(0.5) # Give time for response to be sent
434
+ logger.info("Shutting down web server...")
435
+ os.kill(os.getpid(), signal.SIGTERM)
436
+
437
+ asyncio.create_task(shutdown_server())
438
+ return JSONResponse({"status": "shutdown initiated"})
439
+
440
+ @app.get("/menu", response_class=HTMLResponse)
441
+ async def main_menu(request: Request, session_id: str = Depends(get_session)):
442
+ """Display main menu page."""
443
+ return templates.TemplateResponse(
444
+ "main_menu.html",
445
+ {
446
+ "request": request,
447
+ "dark_theme": dark_theme,
448
+ "cost_tracking_enabled": cost_tracking_enabled,
449
+ }
450
+ )
451
+
452
+ # Import and register routers
453
+ from .endpoints import (
454
+ main_menu_router,
455
+ conversations_router,
456
+ chat_router,
457
+ streaming_router,
458
+ )
459
+ from .endpoints.autonomous_actions import router as autonomous_actions_router
460
+
461
+ app.include_router(main_menu_router, prefix="/api", tags=["Main Menu"])
462
+ app.include_router(conversations_router, prefix="/api", tags=["Conversations"])
463
+ app.include_router(chat_router, prefix="/api", tags=["Chat"])
464
+ app.include_router(streaming_router, prefix="/api", tags=["Streaming"])
465
+ app.include_router(autonomous_actions_router, prefix="/api", tags=["Autonomous Actions"])
466
+
467
+ # Add template routes for conversations and chat
468
+ @app.get("/conversations", response_class=HTMLResponse)
469
+ async def conversations_page(request: Request, session_id: str = Depends(get_session)):
470
+ """Display conversations list page."""
471
+ return templates.TemplateResponse(
472
+ "conversations.html",
473
+ {
474
+ "request": request,
475
+ "dark_theme": dark_theme,
476
+ "session_active": True,
477
+ }
478
+ )
479
+
480
+ @app.get("/conversations/new", response_class=HTMLResponse)
481
+ async def new_conversation_page(request: Request, session_id: str = Depends(get_session)):
482
+ """Display new conversation creation page."""
483
+ return templates.TemplateResponse(
484
+ "new_conversation.html",
485
+ {
486
+ "request": request,
487
+ "dark_theme": dark_theme,
488
+ "session_active": True,
489
+ }
490
+ )
491
+
492
+ @app.get("/chat/{conversation_id}", response_class=HTMLResponse)
493
+ async def chat_page(
494
+ conversation_id: int,
495
+ request: Request,
496
+ session_id: str = Depends(get_session)
497
+ ):
498
+ """Display chat page for a conversation."""
499
+ # Get conversation name
500
+ conv = app_instance.database.get_conversation(conversation_id)
501
+ if not conv:
502
+ return RedirectResponse(url="/conversations", status_code=303)
503
+
504
+ return templates.TemplateResponse(
505
+ "chat.html",
506
+ {
507
+ "request": request,
508
+ "dark_theme": dark_theme,
509
+ "session_active": True,
510
+ "conversation_id": conversation_id,
511
+ "conversation_name": conv['name'],
512
+ }
513
+ )
514
+
515
+ @app.get("/actions", response_class=HTMLResponse)
516
+ async def actions_page(request: Request, session_id: str = Depends(get_session)):
517
+ """Display autonomous actions management page."""
518
+ return templates.TemplateResponse(
519
+ "actions.html",
520
+ {
521
+ "request": request,
522
+ "dark_theme": dark_theme,
523
+ "session_active": True,
524
+ }
525
+ )
526
+
527
+ return app
528
+
529
+
530
+ def run_server(
531
+ app_instance, # AWSBedrockCLI instance
532
+ host: str = "127.0.0.1",
533
+ port: int = 0,
534
+ session_timeout_minutes: int = 0,
535
+ dark_theme: bool = True,
536
+ ssl_enabled: bool = False,
537
+ ssl_cert_file: Optional[str] = None,
538
+ ssl_key_file: Optional[str] = None,
539
+ ssl_auto_generate: bool = True,
540
+ auto_open_browser: bool = False,
541
+ ) -> dict:
542
+ """
543
+ Convenience function to create and run the web server.
544
+
545
+ Args:
546
+ app_instance: Instance of AWSBedrockCLI
547
+ host: Host to bind to
548
+ port: Port to bind to (0 for random available port)
549
+ session_timeout_minutes: Session timeout in minutes
550
+ dark_theme: Whether to use dark theme
551
+ ssl_enabled: Whether to enable HTTPS with SSL/TLS
552
+ ssl_cert_file: Path to SSL certificate file
553
+ ssl_key_file: Path to SSL private key file
554
+ ssl_auto_generate: Whether to auto-generate self-signed certificate if files not found
555
+ auto_open_browser: Whether to automatically open browser with authentication URL
556
+
557
+ Returns:
558
+ Dictionary with access information (url, code)
559
+ """
560
+ server = WebServer(
561
+ app_instance=app_instance,
562
+ host=host,
563
+ port=port,
564
+ session_timeout_minutes=session_timeout_minutes,
565
+ dark_theme=dark_theme,
566
+ ssl_enabled=ssl_enabled,
567
+ ssl_cert_file=ssl_cert_file,
568
+ ssl_key_file=ssl_key_file,
569
+ ssl_auto_generate=ssl_auto_generate,
570
+ auto_open_browser=auto_open_browser,
571
+ )
572
+
573
+ access_info = server.get_access_info()
574
+
575
+ # Start server (blocking)
576
+ server.run()
577
+
578
+ return access_info