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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- 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
|