diff --git a/config.yaml b/config.yaml index 27d6136..fc17190 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # 配置文件 app: secret_key: ${APP_SECRET_KEY} - debug: true + debug: false host: 0.0.0.0 port: 8000 @@ -19,3 +19,7 @@ tools: cache_ttl: 300 max_workers: 4 max_iterations: 10 + + +logging: + level: INFO diff --git a/dashboard/src/style.css b/dashboard/src/style.css index 195e5da..82e7954 100644 --- a/dashboard/src/style.css +++ b/dashboard/src/style.css @@ -323,7 +323,8 @@ body { .message-bubble.assistant .message-container { align-items: flex-start; flex: 1 1 auto; - width: 100%; + width: 80%; + max-width: 80%; min-width: 0; } diff --git a/dashboard/src/utils/api.js b/dashboard/src/utils/api.js index 387f4af..12029cf 100644 --- a/dashboard/src/utils/api.js +++ b/dashboard/src/utils/api.js @@ -99,10 +99,15 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) { onError(data.content) } } catch (e) { - // 忽略解析错误 + console.error('SSE parse error:', e, 'line:', line) } } } + + // 如果没有收到 done 事件,触发错误 + if (!completed && onError) { + onError('stream ended without done event') + } break } diff --git a/luxx/__init__.py b/luxx/__init__.py index b09caac..27ebe70 100644 --- a/luxx/__init__.py +++ b/luxx/__init__.py @@ -1,4 +1,5 @@ """FastAPI application factory""" +import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -7,6 +8,8 @@ from luxx.config import config from luxx.database import init_db from luxx.routes import api_router +logger = logging.getLogger(__name__) + @asynccontextmanager async def lifespan(app: FastAPI): @@ -31,7 +34,7 @@ async def lifespan(app: FastAPI): ) db.add(default_user) db.commit() - print("Default admin user created: admin / admin123") + logger.info("Default admin user created: admin / admin123") finally: db.close() diff --git a/luxx/config.py b/luxx/config.py index bf8297f..6e5d083 100644 --- a/luxx/config.py +++ b/luxx/config.py @@ -111,6 +111,19 @@ class Config: @property def tools_max_iterations(self) -> int: return self.get("tools.max_iterations", 10) + + # Logging configuration + @property + def log_level(self) -> str: + return self.get("logging.level", "INFO") + + @property + def log_format(self) -> str: + return self.get("logging.format", "%(asctime)s | %(levelname)-8s | %(message)s") + + @property + def log_date_format(self) -> str: + return self.get("logging.date_format", "%Y-%m-%d %H:%M:%S") # Global configuration instance diff --git a/luxx/services/llm_client.py b/luxx/services/llm_client.py index 5dcd6de..acec3eb 100644 --- a/luxx/services/llm_client.py +++ b/luxx/services/llm_client.py @@ -1,10 +1,13 @@ """LLM API client""" import json import httpx +import logging from typing import Dict, Any, Optional, List, AsyncGenerator from luxx.config import config +logger = logging.getLogger(__name__) + class LLMResponse: """LLM response""" @@ -147,19 +150,18 @@ class LLMClient: """ body = self._build_body(model, messages, tools, stream=True, **kwargs) - print(f"[LLM] Starting stream_call for model: {model}") - print(f"[LLM] Messages count: {len(messages)}") + logger.info(f"Starting stream_call for model: {model}, messages count: {len(messages)}") try: async with httpx.AsyncClient(timeout=120.0) as client: - print(f"[LLM] Sending request to {self.api_url}") + logger.info(f"Sending request to {self.api_url}") async with client.stream( "POST", self.api_url, headers=self._build_headers(), json=body ) as response: - print(f"[LLM] Response status: {response.status_code}") + logger.info(f"Response status: {response.status_code}") response.raise_for_status() async for line in response.aiter_lines(): @@ -167,10 +169,10 @@ class LLMClient: yield line + "\n" except httpx.HTTPStatusError as e: status_code = e.response.status_code if e.response else "?" - print(f"[LLM] HTTP error: {status_code}") + logger.error(f"HTTP error: {status_code}") yield f"event: error\ndata: {json.dumps({'content': f'HTTP {status_code}: Request failed'})}\n\n" except Exception as e: - print(f"[LLM] Exception: {type(e).__name__}: {str(e)}") + logger.error(f"Exception: {type(e).__name__}: {str(e)}") yield f"event: error\ndata: {json.dumps({'content': str(e)})}\n\n" diff --git a/run.py b/run.py index 54dc763..9ad36a3 100644 --- a/run.py +++ b/run.py @@ -1,17 +1,90 @@ #!/usr/bin/env python3 """Application entry point""" +import logging +import logging.config +from copy import copy import uvicorn +from uvicorn.logging import ColourizedFormatter from luxx.config import config +# Custom formatter extending uvicorn's ColourizedFormatter +class ModuleFormatter(ColourizedFormatter): + """Add module name after level prefix for non-uvicorn loggers""" + + def formatMessage(self, record: logging.LogRecord) -> str: + # Copy record to avoid modifying the original + recordcopy = copy(record) + + # Get level name with color + levelname = recordcopy.levelname + separator = " " * (8 - len(recordcopy.levelname)) + if self.use_colors: + levelname = self.color_level_name(levelname, recordcopy.levelno) + if "color_message" in recordcopy.__dict__: + recordcopy.msg = recordcopy.__dict__["color_message"] + recordcopy.__dict__["message"] = recordcopy.getMessage() + + # Add module name for all loggers + name = record.name + # For uvicorn.error, show "uvicorn" not "error" + if name.startswith('uvicorn.'): + module = 'uvicorn' + else: + module = name + levelprefix = levelname + ":" + separator + recordcopy.__dict__["levelprefix"] = f"{levelprefix} [{module}]" + + return super(ColourizedFormatter, self).formatMessage(recordcopy) + + +def get_log_config() -> dict: + """Get log configuration from config.yaml""" + log_level = getattr(config, 'log_level', 'INFO') + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": ModuleFormatter, + "fmt": "%(levelprefix)s %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + }, + "loggers": { + "uvicorn": {"handlers": ["default"], "level": log_level, "propagate": False}, + "uvicorn.error": {"handlers": ["default"], "level": log_level, "propagate": False}, + "uvicorn.access": {"handlers": [], "level": "WARNING", "propagate": False}, + "uvicorn.asgi": {"handlers": [], "level": "WARNING", "propagate": False}, + }, + "root": { + "handlers": ["default"], + "level": log_level, + }, + } + + +LOG_CONFIG = get_log_config() + + def main(): """Start the application""" + logging.config.dictConfig(LOG_CONFIG) + uvicorn.run( "luxx:app", host=config.app_host, port=config.app_port, reload=config.debug, - log_level="debug" if config.debug else "info" + log_level=config.log_level.lower(), + log_config=LOG_CONFIG )