diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4c498a5..b35cdd3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -60,7 +60,17 @@ "Bash(grep:*)", "Bash(tasklist:*)", "Bash(./xrequest/Scripts/python.exe:*)", - "Bash(xrequest/Scripts/python.exe:*)" + "Bash(xrequest/Scripts/python.exe:*)", + "Bash(start:*)", + "Bash(do:*)", + "Bash(powershell:*)", + "Bash(\"D:\\\\Softwares\\\\Anaconda\\\\python\":*)", + "Bash(export PYTHONPATH=\"d:\\\\Code\\\\Project\\\\FT-Platform\\\\request:$PYTHONPATH\":*)", + "Bash(export PYTHONPATH=\"/d/Code/Project/FT-Platform/request:$PYTHONPATH\":*)", + "Bash(PYTHONPATH=src python -m uvicorn:*)", + "Bash(ip route get 1.1.1.1)", + "Bash(/d/Softwares/Anaconda/python:*)", + "Bash(set PYTHONPATH=src)" ] } } diff --git a/clear_cache.bat b/clear_cache.bat new file mode 100644 index 0000000..d842b30 --- /dev/null +++ b/clear_cache.bat @@ -0,0 +1,25 @@ +@echo off +echo 正在清除所有缓存文件... +echo. + +REM 切换到项目根目录 +cd /d "%~dp0" +cd request + +echo 1. 清除Python缓存文件... +REM 删除所有__pycache__目录 +for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" 2>nul +REM 删除所有.pyc文件 +for /r . %%f in (*.pyc) do @if exist "%%f" del /q "%%f" 2>nul +echo Python缓存清除完成 + +echo. +echo 2. 清除浏览器缓存(请手动清除或按Ctrl+Shift+Delete) +echo 请在浏览器中清除"缓存的图片和文件" +echo. + +echo 3. 重启服务提示 +echo 请按任意键打开启动命令窗口,然后运行启动命令 +echo 例如: python -m uvicorn src.core.app:app --host 0.0.0.0 --port 8000 --reload +echo. +pause diff --git a/models/models.json b/models/models.json index bd0d739..f1f15d7 100644 --- a/models/models.json +++ b/models/models.json @@ -1,104 +1,4 @@ [ - { - "id": "model-1", - "name": "GPT-4 Turbo", - "type": "gpt", - "provider": "openai", - "version": "gpt-4-turbo", - "apiUrl": "https://api.openai.com/v1", - "apiKey": "sk-test123456789", - "timeout": 30, - "maxRetries": 3, - "temperature": 0.7, - "topP": 1.0, - "topK": 50, - "maxTokens": 2048, - "systemPrompt": "你是一个有用的AI助手。", - "streaming": true, - "functions": false, - "logRequests": true, - "status": "连接失败" - }, - { - "id": "model-2", - "name": "Claude-3 Sonnet", - "type": "claude", - "provider": "anthropic", - "version": "claude-3-sonnet-20240229", - "apiUrl": "https://api.anthropic.com/v1", - "apiKey": "", - "timeout": 30, - "maxRetries": 3, - "temperature": 0.5, - "topP": 0.9, - "topK": 40, - "maxTokens": 4096, - "systemPrompt": "你是一个有用的AI助手。", - "streaming": true, - "functions": true, - "logRequests": true, - "status": "未测试" - }, - { - "id": "model-3", - "name": "LLaMA-2 7B", - "type": "llama", - "provider": "local", - "version": "llama-2-7b-chat", - "apiUrl": "http://localhost:8080/v1", - "apiKey": "", - "timeout": 60, - "maxRetries": 3, - "temperature": 0.8, - "topP": 0.95, - "topK": 60, - "maxTokens": 2048, - "systemPrompt": "你是一个有用的AI助手。", - "streaming": true, - "functions": false, - "logRequests": false, - "status": "已测试" - }, - { - "id": "0c13f76a-6a40-48d2-86d3-2638fd2be652", - "name": "Test Model", - "type": "gpt", - "provider": "openai", - "version": "gpt-3.5-turbo", - "apiUrl": "", - "apiKey": "", - "timeout": 30, - "maxRetries": 3, - "temperature": 0.7, - "topP": 1.0, - "topK": 50, - "maxTokens": 2048, - "systemPrompt": "你是一个有用的AI助手。", - "streaming": true, - "functions": false, - "logRequests": true, - "status": "未测试" - }, - { - "id": "9674adec-124c-4641-898b-7f7557e9e412", - "name": "nova", - "type": "gpt", - "provider": "openai", - "version": "nova", - "apiUrl": "http://10.10.10.122:1234/v1", - "apiKey": "123", - "timeout": 30, - "maxRetries": 3, - "temperature": 0.7, - "topP": 1, - "topK": 50, - "maxTokens": 2048, - "systemPrompt": "你是一个有用的AI助手。", - "streaming": true, - "functions": false, - "logRequests": true, - "status": "已测试" - }, { "id": "e81c21e1-a4ce-4237-ba22-0922b741b9be", "name": "qwen3-flash", @@ -117,6 +17,26 @@ "streaming": true, "functions": false, "logRequests": true, - "status": "已测试" + "status": 1 + }, + { + "id": "3e61f8de-831b-4439-9f0a-8b8ad753c5ca", + "name": "Nova", + "type": "custom", + "provider": "custom", + "version": "nova", + "apiUrl": "http://10.10.10.122:1234/v1", + "apiKey": "123", + "timeout": 30, + "maxRetries": 3, + "temperature": 0.7, + "topP": 1, + "topK": 50, + "maxTokens": 2048, + "systemPrompt": "你是一个有用的AI助手。", + "streaming": true, + "functions": false, + "logRequests": true, + "status": 1 } ] \ No newline at end of file diff --git a/request/_backend.bat b/request/_backend.bat new file mode 100644 index 0000000..10210de --- /dev/null +++ b/request/_backend.bat @@ -0,0 +1,39 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +:: 读取配置 +cd /d "%~dp0" +powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.host; } catch { Write-Host 'ERROR'; exit 1; }" > temp_host.txt +set /p backend_host= temp_port.txt +set /p backend_port= temp_python.txt +set /p python_path= temp_module.txt +set /p main_module= temp_loglevel.txt +set /p log_level=nul + +echo [后端] 启动配置: +echo [后端] 主机: !backend_host! +echo [后端] 端口: !backend_port! +echo [后端] 日志级别: !log_level! +echo [后端] 主模块: !main_module! +echo. + +:: 设置环境变量 +set PYTHONPATH=src +set LOGLEVEL=!log_level! + +:: 启动后端服务 +echo [后端] 正在启动服务... +!python_path! -m uvicorn !main_module! --host !backend_host! --port !backend_port! + +pause \ No newline at end of file diff --git a/request/cleanup.sh b/request/cleanup.sh deleted file mode 100644 index 18c1d3e..0000000 --- a/request/cleanup.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# 永远从脚本所在目录运行(避免在别的目录执行导致路径错误) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -echo "🧹 X-Request 框架环境清理" -echo "==========================" - -# 函数:加载 .env 文件中的变量 -load_env_file() { - local env_file=".env" - if [ -f "$env_file" ]; then - while IFS='=' read -r key value; do - [[ "$key" =~ ^#.*$ ]] && continue - [[ -z "$key" ]] && continue - value=$(echo "$value" | sed 's/^["'\'']//' | sed 's/["'\'']$//') - export "$key=$value" - done < "$env_file" - fi -} - -# 加载环境配置 -load_env_file - -# 检查虚拟环境是否存在 -if [ ! -d "xrequest" ]; then - echo "⚠️ 虚拟环境不存在,无需清理" - exit 0 -fi - -echo "📋 检测到虚拟环境: xrequest" - -# 询问用户确认 -read -p "确定要删除虚拟环境吗?(y/N): " confirm -if [[ ! "$confirm" =~ ^[Yy]$ ]]; then - echo "❌ 操作已取消" - exit 0 -fi - -# 删除虚拟环境 -echo "🗑️ 正在删除虚拟环境..." -rm -rf xrequest - -if [ $? -eq 0 ]; then - echo "✅ 虚拟环境已删除" -else - echo "❌ 虚拟环境删除失败" - exit 1 -fi - -# 询问是否清理日志 -if [ -d "${LOGS_DIR:-logs}" ]; then - echo "" - read -p "是否也要清理日志目录?(y/N): " clean_logs - if [[ "$clean_logs" =~ ^[Yy]$ ]]; then - echo "🗑️ 正在清理日志目录..." - rm -rf "${LOGS_DIR:-logs}" - if [ $? -eq 0 ]; then - echo "✅ 日志目录已清理" - else - echo "⚠️ 日志目录清理失败" - fi - fi -fi - -# 询问是否清理 __pycache__ 和 .pyc 文件 -echo "" -read -p "是否清理 Python 缓存文件 (__pycache__, *.pyc)?(y/N): " clean_cache -if [[ "$clean_cache" =~ ^[Yy]$ ]]; then - echo "🗑️ 正在清理 Python 缓存..." - find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find . -type f -name "*.pyc" -delete 2>/dev/null || true - echo "✅ Python 缓存已清理" -fi - -echo "" -echo "🎉 清理完成!" -echo "" -echo "📝 如需重新设置环境,请运行:" -echo " ./setup.sh" -echo "" diff --git a/request/logs/xrequest.pid b/request/logs/xrequest.pid index 39ecd9a..1d50096 100644 --- a/request/logs/xrequest.pid +++ b/request/logs/xrequest.pid @@ -1 +1 @@ -3807 +2916 diff --git a/request/src/api/modules/dataset.py b/request/src/api/modules/dataset.py index 0870d9f..ce4c0d2 100644 --- a/request/src/api/modules/dataset.py +++ b/request/src/api/modules/dataset.py @@ -111,168 +111,64 @@ class DatasetAPI(BaseAPI): return StandardResponse.error(f"上传失败: {str(e)}") @get("", response_model=StandardResponse) - async def list_datasets(self, list_all: bool = False): + async def list_datasets(self): """ - 获取所有数据集列表 - - Args: - list_all: 是否列出data目录下的所有文件(物理文件),默认False(只列出API上传的文件) + 获取数据集列表(只返回filename_mapping.json中记录的文件) Returns: StandardResponse: 包含数据集列表的标准响应 """ - # 添加调试日志 - import logging - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - logger.info(f"list_datasets called with list_all={list_all}") - try: - if list_all: - # 列出data目录下的所有文件(物理文件) - import json - from pathlib import Path + import json - data_dir = file_upload_service.upload_dir - mapping_file = data_dir / "filename_mapping.json" + data_dir = file_upload_service.upload_dir + mapping_file = data_dir / "filename_mapping.json" - # 读取文件名映射 - mappings = {} - if mapping_file.exists(): - try: - with open(mapping_file, 'r', encoding='utf-8') as f: - mapping_data = json.load(f) - mappings = mapping_data.get("mappings", {}) - except Exception: - mappings = {} + # 读取文件名映射 + mappings = {} + if mapping_file.exists(): + with open(mapping_file, 'r', encoding='utf-8') as f: + mapping_data = json.load(f) + mappings = mapping_data.get("mappings", {}) - # 获取data目录下的所有JSON文件 - datasets = [] - if data_dir.exists(): - for file_path in data_dir.iterdir(): - # 跳过目录和映射文件本身 - if file_path.is_file() and file_path.name != "filename_mapping.json": - file_id = file_path.stem # 去掉.json后缀得到file_id + # 根据映射文件构建数据集列表 + datasets = [] + for file_id, mapping_info in mappings.items(): + # 获取物理文件信息 + storage_filename = mapping_info.get("storage_filename", f"{file_id}.json") + file_path = data_dir / storage_filename - # 从映射文件获取真实文件名 - mapping_info = mappings.get(file_id, {}) - original_filename = mapping_info.get("original_filename", file_path.name) - uploaded_at = mapping_info.get("uploaded_at", "") + if file_path.exists(): + # 获取文件大小 + file_size = file_path.stat().st_size + size_mb = round(file_size / 1024 / 1024, 2) + size_display = format_file_size(file_size) - # 获取文件大小 - file_size = file_path.stat().st_size + datasets.append({ + "file_id": file_id, + "name": mapping_info.get("original_filename", ""), + "size": file_size, + "size_mb": size_mb, + "size_display": size_display, + "status": "已处理", + "description": mapping_info.get("original_filename", ""), + "uploaded_at": mapping_info.get("uploaded_at", ""), + "download_count": 0, + "is_physical_file": True + }) - # 格式化文件大小 - size_mb = round(file_size / 1024 / 1024, 2) - size_display = format_file_size(file_size) + # 按上传时间排序 + datasets.sort(key=lambda x: x["uploaded_at"], reverse=True) - datasets.append({ - "file_id": file_id, - "name": original_filename, - "size": file_size, - "size_mb": size_mb, - "size_display": size_display, - "status": "已处理", - "description": mapping_info.get("original_filename", "") if mapping_info else "", - "uploaded_at": uploaded_at, - "download_count": 0, - "is_physical_file": True - }) - - # 按文件名排序 - datasets.sort(key=lambda x: x["name"]) - - return StandardResponse.success({ - "datasets": datasets, - "total": len(datasets), - "source": "physical_files" - }) - else: - # 获取所有文件(API上传的文件) - all_files = file_upload_service.get_all_files() - - # 转换为前端期望的格式 - datasets = [] - for uploaded_file in all_files: - # 只返回JSON/JSONL文件(数据集文件) - file_ext = uploaded_file.original_filename.lower().split('.')[-1] if '.' in uploaded_file.original_filename else '' - if file_ext in ['json', 'jsonl']: - # 获取文件名映射(显示真实文件名) - mapping = file_upload_service.get_filename_mapping(uploaded_file.file_id) - display_name = mapping["original_filename"] if mapping else uploaded_file.original_filename - - # 格式化文件大小 - size_mb = round(uploaded_file.file_size / 1024 / 1024, 2) - size_display = format_file_size(uploaded_file.file_size) - - datasets.append({ - "file_id": uploaded_file.file_id, - "name": display_name, - "size": uploaded_file.file_size, - "size_mb": size_mb, - "size_display": size_display, - "status": "已处理", - "description": uploaded_file.description or "", - "uploaded_at": uploaded_file.uploaded_at, - "download_count": uploaded_file.download_count, - "is_physical_file": False - }) - - return StandardResponse.success({ - "datasets": datasets, - "total": len(datasets), - "source": "api_uploaded" - }) + return StandardResponse.success({ + "datasets": datasets, + "total": len(datasets), + "source": "filename_mapping" + }) except Exception as e: return StandardResponse.error(f"获取数据集列表失败: {str(e)}") - @get("/{file_id}", response_model=StandardResponse) - async def get_dataset(self, file_id: str): - """ - 获取特定数据集的详细信息 - - Args: - file_id: 文件ID - - Returns: - StandardResponse: 包含数据集详情的标准响应 - """ - try: - file_info = file_upload_service.get_file(file_id) - - if not file_info: - return StandardResponse.error(f"数据集 {file_id} 不存在") - - # 转换为前端期望的格式 - # 显示真实文件名(从映射文件中获取) - mapping = file_upload_service.get_filename_mapping(file_info.file_id) - display_name = mapping["original_filename"] if mapping else file_info.original_filename - - # 格式化文件大小 - size_mb = round(file_info.file_size / 1024 / 1024, 2) - size_display = format_file_size(file_info.file_size) - - dataset_info = { - "file_id": file_info.file_id, - "name": display_name, - "size": file_info.file_size, - "size_mb": size_mb, - "size_display": size_display, - "status": "已处理", - "description": file_info.description or "", - "uploaded_at": file_info.uploaded_at, - "updated_at": file_info.updated_at, - "download_count": file_info.download_count, - "content_type": file_info.content_type, - "file_hash": file_info.file_hash - } - - return StandardResponse.success(dataset_info) - - except Exception as e: - return StandardResponse.error(f"获取数据集详情失败: {str(e)}") - @get("/{file_id}", response_model=StandardResponse) async def get_dataset(self, file_id: str): """ diff --git a/request/src/api/modules/models.py b/request/src/api/modules/models.py index fe9aeb9..3accbd2 100644 --- a/request/src/api/modules/models.py +++ b/request/src/api/modules/models.py @@ -7,10 +7,8 @@ import os import json import uuid import httpx -import asyncio from typing import List, Dict, Any, Optional from fastapi import HTTPException, Body, Response -from fastapi.responses import StreamingResponse import logging # 导入基类 @@ -27,81 +25,6 @@ MODELS_CONFIG_PATH = os.path.join(BASE_DIR, "..", "..", "models", "models.json") MODELS_CONFIG_PATH = os.path.normpath(MODELS_CONFIG_PATH) -async def handle_streaming_response(response: httpx.Response, model: Dict[str, Any], attempt: int, max_attempts: int) -> Dict[str, Any]: - """处理流式响应""" - try: - if response.status_code != 200: - error_msg = f"API调用失败 (状态码: {response.status_code})" - try: - error_detail = response.json() - error_msg += f": {error_detail}" - except: - error_msg += f": {response.text}" - return { - 'success': False, - 'error': error_msg, - 'status_code': response.status_code - } - - full_content = "" - chunk_count = 0 - usage = {} - - # 处理流式数据 - async for line in response.aiter_lines(): - if line.startswith('data: '): - data_str = line[6:] # 移除 'data: ' 前缀 - - if data_str.strip() == '[DONE]': - break - - try: - chunk = json.loads(data_str) - chunk_count += 1 - - # 提取内容 - if 'choices' in chunk and len(chunk['choices']) > 0: - delta = chunk['choices'][0].get('delta', {}) - if 'content' in delta: - content = delta['content'] - full_content += content - - # 提取使用统计 - if 'usage' in chunk: - usage = chunk['usage'] - - except json.JSONDecodeError: - continue - - logger.info(f"流式响应完成 (尝试 {attempt}/{max_attempts}) - 接收 {chunk_count} 个数据块") - - return { - 'success': True, - 'model': model['name'], - 'content': full_content, - 'usage': usage, - 'streaming': True, - 'chunks_received': chunk_count, - 'raw_response': { - 'object': 'chat.completion', - 'model': model.get('version', 'unknown'), - 'choices': [{ - 'message': {'role': 'assistant', 'content': full_content}, - 'finish_reason': 'stop' - }], - 'usage': usage - } - } - - except Exception as e: - logger.error(f"流式响应处理错误: {str(e)}") - return { - 'success': False, - 'error': f'流式响应处理失败: {str(e)}', - 'error_type': type(e).__name__ - } - - class ModelManager: """模型配置管理器""" @@ -227,245 +150,124 @@ class ModelManager: @staticmethod async def call_model(model_id: str, prompt: str, system_prompt: Optional[str] = None) -> Dict[str, Any]: - """真实调用模型API""" + """真实调用模型API - 简化版本""" model = ModelManager.get_model_by_id(model_id) if not model: - raise HTTPException(status_code=404, detail="模型不存在") + return {'success': False, 'error': '模型不存在'} # 检查API配置 api_url = model.get('apiUrl', '').strip() api_key = model.get('apiKey', '').strip() version = model.get('version', '').strip() - provider = model.get('provider', '').strip() - # 调试日志 - logger.info(f"Model {model.get('name')} config", extra={ - 'provider': provider, - 'api_url': api_url, - 'version': version, - 'has_api_key': bool(api_key), - 'api_key_length': len(api_key) if api_key else 0 - }) + # 记录调试信息 + model_name = str(model.get('name', 'unknown')) if model.get('name') is not None else 'unknown' + logger.info(f"测试模型: {model_name}") + logger.info(f"API地址: {api_url}") + logger.info(f"模型版本: {version}") + logger.info(f"API密钥长度: {len(api_key) if api_key else 0}") if not api_url: - raise HTTPException(status_code=400, detail="模型API地址未配置") + return {'success': False, 'error': 'API地址未配置'} if not version: - raise HTTPException(status_code=400, detail="模型版本未配置") + return {'success': False, 'error': '模型版本未配置'} - # 准备请求数据 - request_data = { - "model": version, - "messages": [], - "temperature": model.get('temperature', 0.7), - "top_p": model.get('topP', 1.0), - "max_tokens": model.get('maxTokens', 2048), - "stream": model.get('streaming', False) - } + # 准备请求数据 - 标准的OpenAI格式 + messages = [] # 添加系统提示词 if system_prompt: - request_data["messages"].append({ - "role": "system", - "content": system_prompt - }) + messages.append({"role": "system", "content": system_prompt}) elif model.get('systemPrompt'): - request_data["messages"].append({ - "role": "system", - "content": model.get('systemPrompt') - }) + system_prompt_content = model.get('systemPrompt') + if system_prompt_content: + messages.append({"role": "system", "content": system_prompt_content}) # 添加用户提示词 - request_data["messages"].append({ - "role": "user", - "content": prompt - }) + messages.append({"role": "user", "content": prompt}) - # 设置请求头 + logger.info(f"准备发送的消息: {messages}") + + request_data = { + "model": version, + "messages": messages, + "temperature": model.get('temperature', 0.7), + "top_p": model.get('topP', 1.0), + "max_tokens": model.get('maxTokens', 2048), + "stream": False # 强制使用非流式响应 + } + + # 设置请求头 - 根据是否有API密钥决定认证方式 headers = { "Content-Type": "application/json" } - # 添加API密钥(如果提供) + # 只有在有API密钥时才添加认证头 if api_key: - provider_lower = provider.lower() + headers["Authorization"] = f"Bearer {api_key}" - # 根据提供商类型设置认证头 - if provider_lower == 'anthropic': - # Anthropic API - headers["x-api-key"] = api_key - headers["anthropic-version"] = "2023-06-01" - logger.info(f"设置Anthropic认证头,x-api-key长度: {len(api_key)}") - else: - # OpenAI 和其他兼容的API(包括自定义) - headers["Authorization"] = f"Bearer {api_key}" - logger.info(f"设置Bearer认证头,key长度: {len(api_key)}") + try: + # 发送API请求 - 只支持OpenAI兼容格式 + timeout = httpx.Timeout(30) + async with httpx.AsyncClient(timeout=timeout) as client: + # 记录请求信息(隐藏API密钥) + masked_headers = {k: (v[:10] + '...' if k == 'Authorization' and len(v) > 10 else v) for k, v in headers.items()} + logger.info(f"发送请求到: {api_url.rstrip('/')}/chat/completions") + logger.info(f"请求头: {masked_headers}") + logger.info(f"请求体: {request_data}") - # 记录最终使用的请求头(隐藏API key) - logger.info(f"最终请求头", extra={ - 'headers': {k: v if k != 'Authorization' and k != 'x-api-key' else '***HIDDEN***' for k, v in headers.items()}, - 'api_key_masked': f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else '***' - }) - else: - logger.warning(f"未提供API key for model {model.get('name')}") - # 记录最终使用的请求头 - logger.info(f"最终请求头", extra={ - 'headers': headers, - 'api_key': 'NONE' - }) + response = await client.post( + f"{api_url.rstrip('/')}/chat/completions", + headers=headers, + json=request_data + ) - # 获取重试配置 - max_retries = model.get('maxRetries', 3) - retry_delay = 2 # 重试间隔2秒 + # 检查响应 + logger.info(f"响应状态码: {response.status_code}") + if response.status_code == 200: + result = response.json() + logger.info(f"响应内容: {result}") - last_error = None + # 解析响应 + if "choices" in result and len(result["choices"]) > 0: + message = result["choices"][0]["message"] + content = message.get("content", "") or "" + logger.info(f"原始响应内容: {message}") + logger.info(f"解析成功,收到回复长度: {len(content)}") - # 重试循环 - for attempt in range(max_retries + 1): - try: - # 发送API请求 - timeout = httpx.Timeout(model.get('timeout', 30)) - provider = model.get('provider', '').lower() - - async with httpx.AsyncClient(timeout=timeout) as client: - # 根据提供商类型调用不同的API端点 - if provider_lower == 'anthropic': - # Anthropic API格式 - anthropic_request = { - "model": version, - "max_tokens": request_data["max_tokens"], - "messages": request_data["messages"][1:] # 移除system message,Anthropic使用system参数 - } - if system_prompt or model.get('systemPrompt'): - anthropic_request["system"] = system_prompt or model.get('systemPrompt') - - logger.info(f"发送Anthropic API请求 (尝试 {attempt + 1}/{max_retries + 1})", extra={ - 'url': f"{api_url.rstrip('/')}/messages", - 'headers': {k: v if k != 'x-api-key' else '***HIDDEN***' for k, v in headers.items()}, - 'request_body': anthropic_request - }) - - response = await client.post( - f"{api_url.rstrip('/')}/messages", - headers=headers, - json=anthropic_request - ) - else: - # OpenAI 和其他兼容的API(包括自定义、本地部署等) - - logger.info(f"发送OpenAI兼容API请求 (尝试 {attempt + 1}/{max_retries + 1})", extra={ - 'url': f"{api_url.rstrip('/')}/chat/completions", - 'headers': {k: v if k != 'Authorization' else '***HIDDEN***' for k, v in headers.items()}, - 'request_body': request_data - }) - - response = await client.post( - f"{api_url.rstrip('/')}/chat/completions", - headers=headers, - json=request_data - ) - - # 处理流式响应 - if request_data.get('stream', False): - return await handle_streaming_response(response, model, attempt + 1, max_retries + 1) - - # 检查响应状态(非流式) - if response.status_code == 200: - result = response.json() - - # 解析响应 - 根据提供商类型 - provider = model.get('provider', '').lower() - - if provider == 'anthropic' and "content" in result and len(result["content"]) > 0: - # Anthropic格式响应 - content = result["content"][0]["text"] - logger.info(f"API调用成功 (尝试 {attempt + 1})") - return { - 'success': True, - 'model': model['name'], - 'content': content, - 'usage': result.get('usage', {}), - 'raw_response': result - } - elif "choices" in result and len(result["choices"]) > 0: - # OpenAI和其他兼容API格式响应 - content = result["choices"][0]["message"]["content"] - logger.info(f"API调用成功 (尝试 {attempt + 1})") - return { - 'success': True, - 'model': model['name'], - 'content': content, - 'usage': result.get('usage', {}), - 'raw_response': result - } + # 如果内容为空,记录警告但仍然返回成功 + if not content: + logger.warning("API返回的内容为空") return { - 'success': False, - 'error': '无法解析API响应', + 'success': True, + 'model': str(model.get('name', 'unknown')), + 'content': content, + 'usage': result.get('usage', {}), 'raw_response': result } else: - error_msg = f"API调用失败 (状态码: {response.status_code})" - try: - error_detail = response.json() - error_msg += f": {error_detail}" - except: - error_msg += f": {response.text}" - - last_error = { - 'success': False, - 'error': error_msg, - 'status_code': response.status_code - } - - # 如果不是最后一次尝试,等待后重试 - if attempt < max_retries: - logger.warning(f"API调用失败,{retry_delay}秒后重试 (尝试 {attempt + 1}/{max_retries + 1}): {error_msg}") - await asyncio.sleep(retry_delay) - continue - else: - # 最后一次尝试失败,返回错误 - return last_error - - except httpx.TimeoutException as e: - last_error = { - 'success': False, - 'error': 'API调用超时', - 'timeout': model.get('timeout', 30) - } - logger.warning(f"API调用超时 (尝试 {attempt + 1}/{max_retries + 1})") - # 如果不是最后一次尝试,等待后重试 - if attempt < max_retries: - await asyncio.sleep(retry_delay) - continue + logger.error(f"API响应格式异常: {result}") + return {'success': False, 'error': 'API响应格式异常'} else: - return last_error + # 获取错误信息 + try: + error_detail = response.json() + error_detail_str = str(error_detail) if error_detail is not None else '无错误详情' + logger.error(f"API错误响应: {error_detail}") + except: + error_detail_str = response.text or '未知错误' + logger.error(f"API错误响应文本: {error_detail_str}") - except httpx.RequestError as e: - last_error = { - 'success': False, - 'error': f'网络请求错误: {str(e)}' - } - logger.warning(f"网络请求错误 (尝试 {attempt + 1}/{max_retries + 1}): {str(e)}") - # 如果不是最后一次尝试,等待后重试 - if attempt < max_retries: - await asyncio.sleep(retry_delay) - continue - else: - return last_error + return { + 'success': False, + 'error': f'API调用失败 ({response.status_code}): {error_detail_str}' + } - except Exception as e: - last_error = { - 'success': False, - 'error': f'调用失败: {str(e)}', - 'error_type': type(e).__name__ - } - logger.error(f"未知错误 (尝试 {attempt + 1}/{max_retries + 1}): {str(e)}") - # 其他错误不重试,直接返回 - return last_error - - # 如果所有重试都失败,返回最后一次错误 - return last_error + except Exception as e: + error_str = str(e) if e is not None else '未知异常' + return {'success': False, 'error': f'调用异常: {error_str}'} @staticmethod def test_model(model_id: str) -> Dict[str, Any]: @@ -530,7 +332,8 @@ class ModelManager: message = '连接测试成功' else: model['status'] = '连接失败' - message = f'连接测试失败: {test_result.get("error", "未知错误")}' + error = test_result.get("error") + message = f'连接测试失败: {str(error) if error else "未知错误"}' # 保存更新 ModelManager.update_model(model_id, {'status': model['status']}) @@ -656,7 +459,8 @@ class ModelsAPI(BaseAPI): message = '连接测试成功' else: model['status'] = '连接失败' - message = f'连接测试失败: {result.get("error", "未知错误")}' + error = result.get("error") + message = f'连接测试失败: {str(error) if error else "未知错误"}' # 保存更新 ModelManager.update_model(model_id, {'status': model['status']}) @@ -678,157 +482,28 @@ class ModelsAPI(BaseAPI): @post("/{model_id}/call") async def call_model_api(self, model_id: str, request_data: Dict[str, Any] = Body(...)): - """调用模型进行对话(支持流式输出)""" + """调用模型进行对话(使用非流式响应)""" try: # 获取请求参数 prompt = request_data.get('prompt', '') system_prompt = request_data.get('systemPrompt', None) - stream_enabled = request_data.get('stream', False) if not prompt: raise HTTPException(status_code=400, detail="提示词不能为空") - # 如果启用流式,返回流式响应 - if stream_enabled: - return StreamingResponse( - self._stream_data(model_id, prompt, system_prompt), - media_type="text/plain", - headers={ - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "Transfer-Encoding": "chunked" - } - ) - else: - # 普通非流式调用 - result = await ModelManager.call_model( - model_id=model_id, - prompt=prompt, - system_prompt=system_prompt - ) - return self.success(result, "模型调用成功" if result.get('success') else "模型调用失败") + # 强制使用非流式调用 + result = await ModelManager.call_model( + model_id=model_id, + prompt=prompt, + system_prompt=system_prompt + ) + return self.success(result, "模型调用成功" if result.get('success') else "模型调用失败") except HTTPException: raise except Exception as e: self.logger.error(f"调用模型失败: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) - async def _stream_data(self, model_id: str, prompt: str, system_prompt: Optional[str] = None): - """真正的流式输出数据""" - try: - # 重新调用模型,获取流式响应 - model = ModelManager.get_model_by_id(model_id) - if not model: - yield "data: " + json.dumps({"error": "模型不存在", "done": True}) + "\n\n" - return - - # 设置流式请求 - api_url = model.get('apiUrl', '').strip() - api_key = model.get('apiKey', '').strip() - version = model.get('version', '').strip() - provider = model.get('provider', '').strip() - - # 准备请求数据 - request_data = { - "model": version, - "messages": [], - "temperature": model.get('temperature', 0.7), - "top_p": model.get('topP', 1.0), - "max_tokens": model.get('maxTokens', 2048), - "stream": True # 启用流式 - } - - # 添加系统提示词 - if system_prompt: - request_data["messages"].append({ - "role": "system", - "content": system_prompt - }) - elif model.get('systemPrompt'): - request_data["messages"].append({ - "role": "system", - "content": model.get('systemPrompt') - }) - - # 添加用户提示词 - request_data["messages"].append({ - "role": "user", - "content": prompt - }) - - # 设置请求头 - headers = { - "Content-Type": "application/json" - } - - if api_key: - if provider.lower() == 'anthropic': - headers["x-api-key"] = api_key - headers["anthropic-version"] = "2023-06-01" - else: - headers["Authorization"] = f"Bearer {api_key}" - - # 发送流式请求 - async with httpx.AsyncClient(timeout=60) as client: - if provider.lower() == 'anthropic': - # Anthropic流式请求 - anthropic_request = { - "model": version, - "max_tokens": request_data["max_tokens"], - "messages": request_data["messages"][1:], - "stream": True - } - if system_prompt or model.get('systemPrompt'): - anthropic_request["system"] = system_prompt or model.get('systemPrompt') - - async with client.stream( - "POST", - f"{api_url.rstrip('/')}/messages", - headers=headers, - json=anthropic_request - ) as response: - async for line in response.aiter_lines(): - if line.startswith('data: '): - data_str = line[6:] - if data_str.strip() == '[DONE]': - yield "data: " + json.dumps({"content": "", "done": True}) + "\n\n" - break - - try: - chunk_data = json.loads(data_str) - if "content" in chunk_data and len(chunk_data["content"]) > 0: - content = chunk_data["content"][0]["text"] - yield f"data: {json.dumps({'content': content, 'done': False})}\n\n" - except: - continue - else: - # OpenAI兼容流式请求 - async with client.stream( - "POST", - f"{api_url.rstrip('/')}/chat/completions", - headers=headers, - json=request_data - ) as response: - async for line in response.aiter_lines(): - if line.startswith('data: '): - data_str = line[6:] - if data_str.strip() == '[DONE]': - yield "data: " + json.dumps({"content": "", "done": True}) + "\n\n" - break - - try: - chunk_data = json.loads(data_str) - if "choices" in chunk_data and len(chunk_data["choices"]) > 0: - delta = chunk_data["choices"][0].get("delta", {}) - if "content" in delta and delta["content"]: - content = delta["content"] - yield f"data: {json.dumps({'content': content, 'done': False})}\n\n" - except: - continue - - except Exception as e: - yield "data: " + json.dumps({"error": str(e), "done": True}) + "\n\n" - @post("/batch-delete") async def batch_delete_models(self, model_ids: List[str] = Body(...)): """批量删除模型""" diff --git a/request/src/config/settings.py b/request/src/config/settings.py index e3c99e5..b6f73f2 100644 --- a/request/src/config/settings.py +++ b/request/src/config/settings.py @@ -1,7 +1,7 @@ from pydantic_settings import BaseSettings from typing import Optional import os -from pathlib import Path +import json class Settings(BaseSettings): @@ -12,21 +12,21 @@ class Settings(BaseSettings): # 服务器配置 host: str = "0.0.0.0" - port: int = 1112 + port: int = 3000 # 默认使用config.json中的端口 workers: int = 1 # 日志配置 log_level: str = "INFO" - log_file: Optional[str] = "logs/app.log" - log_format: str = "json" - log_to_console: bool = False + log_file: Optional[str] = "logs/app.log" # 默认写入日志文件 + log_format: str = "json" # json 或 console + log_to_console: bool = False # 是否同时输出到控制台 # 高级日志配置 - advanced_logging: bool = True - logs_dir: str = "logs" - max_log_days: int = 30 - enable_log_cleanup: bool = True - route_based_logging: bool = True + advanced_logging: bool = True # 是否启用高级日志系统 + logs_dir: str = "logs" # 日志目录 + max_log_days: int = 30 # 日志文件保存天数 + enable_log_cleanup: bool = True # 是否启用自动日志清理 + route_based_logging: bool = True # 是否启用基于路由的日志分类 # 性能配置 max_requests: int = 1000 @@ -39,10 +39,35 @@ class Settings(BaseSettings): cors_headers: list[str] = ["*"] class Config: - env_file = str(Path(__file__).parent.parent.parent / ".env") - env_file_encoding = "utf-8" + env_file = ".env" case_sensitive = False + def __init__(self, **kwargs): + super().__init__(**kwargs) + # 尝试读取根目录的config.json配置文件 + try: + # 获取项目根目录(相对于当前文件的路径) + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.abspath(os.path.join(current_dir, "..", "..")) + config_path = os.path.join(root_dir, "config.json") + + if os.path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + # 从config.json读取配置,覆盖默认配置 + if 'backend' in config: + backend_config = config['backend'] + if 'port' in backend_config: + self.port = int(backend_config['port']) + if 'host' in backend_config: + self.host = backend_config['host'] + if 'log_level' in backend_config: + self.log_level = backend_config['log_level'] + except Exception as e: + # 如果读取配置文件失败,使用默认配置 + print(f"警告: 无法读取配置文件 config.json,使用默认配置. 错误: {e}") + # 全局设置实例 settings = Settings() \ No newline at end of file diff --git a/request/src/core/app.py b/request/src/core/app.py index d6447b9..d92313d 100644 --- a/request/src/core/app.py +++ b/request/src/core/app.py @@ -224,7 +224,7 @@ def setup_routes(app: FastAPI): @app.get("/routes") async def get_routes_info(): """获取所有路由信息""" - from ..api.discovery import get_registered_modules_info + from ..api.internal.discovery import get_registered_modules_info response_data = { "app_routes": [ @@ -246,6 +246,64 @@ def setup_routes(app: FastAPI): registration_result = auto_register_routes(app) + # SSE模拟大模型输出端点(在路由注册之后添加) + @app.get("/sse") + async def sse_demo(): + """SSE模拟大模型流式输出演示""" + from fastapi.responses import StreamingResponse + import asyncio + import json + + async def generate_ai_response(): + """模拟大模型的流式输出""" + # 模拟大模型回答关于人工智能的内容 + response_parts = [ + "人工智能(AI)是计算机科学的一个分支,", + "它试图理解智能的实质,", + "并生产出一种新的能以人类智能相似的方式做出反应的智能机器。", + "包括机器人、语言识别、图像识别、自然语言处理和专家系统等。", + "", + "AI的发展可以追溯到1956年,", + "当时在达特茅斯会议上,", + "约翰·麦卡锡首次提出了'人工智能'这一术语。", + "", + "经过几十年的发展,", + "AI已经取得了巨大的进步,", + "特别是在深度学习、神经网络和大数据的推动下,", + "AI技术已经广泛应用于各个领域,", + "包括医疗、金融、交通、教育、娱乐等。", + "", + "未来,AI将继续发展,", + "有望在更多领域发挥重要作用,", + "但同时也需要关注其带来的挑战,", + "如隐私保护、伦理问题、就业影响等。", + "", + "总的来说,", + "人工智能是人类智慧的结晶,", + "它将继续推动社会进步,", + "但我们也需要谨慎地对待它的发展和应用。" + ] + + for part in response_parts: + # 每部分作为一条消息发送 + yield f"data: {json.dumps({'content': part, 'done': False})}\n\n" + # 模拟生成延迟 + await asyncio.sleep(0.5) + + # 发送完成信号 + yield f"data: {json.dumps({'content': '', 'done': True})}\n\n" + + return StreamingResponse( + generate_ai_response(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*" + } + ) + # 挂载静态文件目录 from fastapi.responses import FileResponse @@ -288,4 +346,7 @@ def get_app() -> FastAPI: with _app_lock: if _app_instance is None: _app_instance = create_app() - return _app_instance \ No newline at end of file + return _app_instance + +# 导出应用实例 +app = get_app() \ No newline at end of file diff --git a/request/src/services/file_upload.py b/request/src/services/file_upload.py index 26da879..766b5c1 100644 --- a/request/src/services/file_upload.py +++ b/request/src/services/file_upload.py @@ -94,14 +94,88 @@ class FileUploadService: # 文件信息存储(内存存储,可扩展为数据库) self.files: Dict[str, UploadedFile] = {} - + # 配置 self.max_file_size = 100 * 1024 * 1024 # 100MB 默认最大文件大小 self.allowed_extensions = None # None表示允许所有扩展名 + + # 初始化时加载已存在的文件信息 + self._load_existing_files() def _generate_file_id(self) -> str: """生成唯一文件ID""" return str(uuid.uuid4()) + + def _load_existing_files(self): + """ + 从文件名映射文件中加载已存在的文件信息 + """ + try: + import json + from datetime import datetime + + mapping_file = self.upload_dir / "filename_mapping.json" + if not mapping_file.exists(): + print(f"[FileUploadService] 映射文件不存在,跳过加载") + return + + print(f"[FileUploadService] 正在加载映射文件: {mapping_file}") + with open(mapping_file, 'r', encoding='utf-8') as f: + mapping_data = json.load(f) + mappings = mapping_data.get("mappings", {}) + + loaded_count = 0 + for file_id, mapping_info in mappings.items(): + try: + storage_filename = mapping_info.get("storage_filename", f"{file_id}.json") + file_path = self.upload_dir / storage_filename + + # 检查物理文件是否存在 + if not file_path.exists(): + print(f"[FileUploadService] 警告: 文件不存在 {file_path}") + continue + + # 获取文件大小 + file_size = file_path.stat().st_size + + # 创建UploadedFile对象 + uploaded_file = UploadedFile( + file_id=file_id, + filename=storage_filename, + original_filename=mapping_info.get("original_filename", storage_filename), + file_path=str(file_path), + file_size=file_size, + content_type="application/json", + description=mapping_info.get("original_filename", storage_filename) + ) + + # 设置上传时间 + uploaded_at = mapping_info.get("uploaded_at") + if uploaded_at: + uploaded_file.uploaded_at = uploaded_at + uploaded_file.updated_at = uploaded_at + + # 计算文件哈希 + try: + uploaded_file.file_hash = self._calculate_file_hash(file_path) + except Exception: + uploaded_file.file_hash = None + + # 添加到内存字典 + self.files[file_id] = uploaded_file + loaded_count += 1 + print(f"[FileUploadService] 已加载文件: {uploaded_file.original_filename} (ID: {file_id})") + + except Exception as e: + print(f"[FileUploadService] 加载文件 {file_id} 时出错: {e}") + continue + + print(f"[FileUploadService] 加载完成,共加载 {loaded_count} 个文件") + + except Exception as e: + print(f"[FileUploadService] 加载映射文件时出错: {e}") + import traceback + traceback.print_exc() def _generate_storage_filename(self, original_filename: str, file_id: str) -> str: """生成存储文件名""" diff --git a/request/start-backend.bat b/request/start-backend.bat new file mode 100644 index 0000000..8928350 --- /dev/null +++ b/request/start-backend.bat @@ -0,0 +1,10 @@ +@echo off +echo Starting Backend API Server on Port 3000... +echo. +echo Backend API: http://localhost:3000/models/ +echo Press Ctrl+C to stop the server +echo. +cd /d "%~dp0" +set PYTHONPATH=src +/d/Softwares/Anaconda/python -m uvicorn src.core.app:app --host 0.0.0.0 --port 3000 +pause diff --git a/request/static/.openapi_cache b/request/static/.openapi_cache index b38ee46..d3ff57a 100644 --- a/request/static/.openapi_cache +++ b/request/static/.openapi_cache @@ -1 +1 @@ -0bbf8a1a0807bf0cee9cf3cc5634f318 \ No newline at end of file +58852b1a007ac2ceb1f790a423c070f2 \ No newline at end of file diff --git a/request/static/doc.html b/request/static/doc.html index 1b6a9c7..85816d2 100644 --- a/request/static/doc.html +++ b/request/static/doc.html @@ -854,20 +854,8 @@ "Datasets" ], "summary": "list_datasets", - "description": "获取所有数据集列表\n\nArgs:\n list_all: 是否列出data目录下的所有文件(物理文件),默认False(只列出API上传的文件)\n\nReturns:\n StandardResponse: 包含数据集列表的标准响应", + "description": "获取数据集列表(只返回filename_mapping.json中记录的文件)\n\nReturns:\n StandardResponse: 包含数据集列表的标准响应", "operationId": "list_datasets_datasets_get", - "parameters": [ - { - "name": "list_all", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": false, - "title": "List All" - } - } - ], "responses": { "200": { "description": "Successful Response", @@ -878,16 +866,6 @@ } } } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } } } } @@ -984,7 +962,7 @@ "Models" ], "summary": "call_model_api", - "description": "调用模型进行对话(支持流式输出)", + "description": "调用模型进行对话(强制使用非流式响应)", "operationId": "call_model_api_models__model_id__call_post", "parameters": [ { @@ -1285,7 +1263,7 @@ "Models" ], "summary": "test_model_connection", - "description": "测试模型连接", + "description": "测试模型连接 - 禁用流式响应", "operationId": "test_model_connection_models__model_id__test_post", "parameters": [ { @@ -1319,6 +1297,23 @@ } } } + }, + "/sse": { + "get": { + "summary": "Sse Demo", + "description": "SSE模拟大模型流式输出演示", + "operationId": "sse_demo_sse_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } } }, "components": { diff --git a/request/static/sse-test.html b/request/static/sse-test.html new file mode 100644 index 0000000..eae5b28 --- /dev/null +++ b/request/static/sse-test.html @@ -0,0 +1,98 @@ + + + + + + SSE流式输出演示 + + + +

🚀 大模型流式输出演示

+
正在连接服务器...
+ + + + \ No newline at end of file diff --git a/streaming-test.html b/streaming-test.html new file mode 100644 index 0000000..054a2de --- /dev/null +++ b/streaming-test.html @@ -0,0 +1,396 @@ + + + + + + 流式输出测试页面 + + + +
+

✨ 流式输出测试

+ +
+ + + +
+ +
+
点击上方按钮开始测试流式输出...
+
+ +
+
+ 状态: + 就绪 +
+
+ 字符数: + 0 +
+
+ 耗时: + 0.00s +
+
+
+ + + + \ No newline at end of file diff --git a/web/nul b/web/nul deleted file mode 100644 index e69de29..0000000 diff --git a/web/pages/main.html b/web/pages/main.html index 35a9930..90ab3e8 100644 --- a/web/pages/main.html +++ b/web/pages/main.html @@ -34,85 +34,6 @@ } } - -