diff --git a/.gitignore b/.gitignore index 26fdd75..bf13ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ !README.md !.gitignore -!alcor/**/*.py +!luxx/**/*.py !docs/**/*.md diff --git a/alcor/__init__.py b/alcor/__init__.py deleted file mode 100644 index 8e502db..0000000 --- a/alcor/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ -"""FastAPI应用工厂""" -from contextlib import asynccontextmanager -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from alcor.config import config -from alcor.database import init_db -from alcor.routes import api_router - - -@asynccontextmanager -async def lifespan(app: FastAPI): - """应用生命周期管理""" - # 启动时 - print("🚀 Starting up ChatBackend API...") - - # 初始化数据库 - init_db() - print("✅ Database initialized") - - # 加载内置工具 - from alcor.tools.builtin import crawler, code, data - print(f"✅ Loaded {len(api_router.routes)} API routes") - - yield - - # 关闭时 - print("👋 Shutting down ChatBackend API...") - - -def create_app() -> FastAPI: - """创建FastAPI应用""" - app = FastAPI( - title="ChatBackend API", - description="智能聊天后端API,支持多模型、流式响应、工具调用", - version="1.0.0", - lifespan=lifespan - ) - - # 配置CORS - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # 生产环境应限制 - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - # 注册路由 - app.include_router(api_router, prefix="/api") - - # 健康检查 - @app.get("/health") - async def health_check(): - return {"status": "healthy", "service": "chat-backend"} - - @app.get("/") - async def root(): - return { - "service": "ChatBackend API", - "version": "1.0.0", - "docs": "/docs" - } - - return app - - -# 创建应用实例 -app = create_app() diff --git a/alcor/routes/tools.py b/alcor/routes/tools.py deleted file mode 100644 index ce201b4..0000000 --- a/alcor/routes/tools.py +++ /dev/null @@ -1,73 +0,0 @@ -"""工具路由""" -from typing import Optional, List, Dict, Any -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - -from alcor.database import get_db -from alcor.models import User -from alcor.routes.auth import get_current_user -from alcor.tools.core import registry -from alcor.utils.helpers import success_response - - -router = APIRouter(prefix="/tools", tags=["工具"]) - - -@router.get("/", response_model=dict) -def list_tools( - category: Optional[str] = None, - current_user: User = Depends(get_current_user) -): - """获取可用工具列表""" - if category: - tools = registry.list_by_category(category) - else: - tools = registry.list_all() - - # 按分类分组 - categorized = {} - for tool in tools: - cat = tool.get("function", {}).get("category", "general") - if cat not in categorized: - categorized[cat] = [] - categorized[cat].append(tool) - - return success_response(data={ - "tools": tools, - "categorized": categorized, - "total": registry.tool_count - }) - - -@router.get("/{name}", response_model=dict) -def get_tool( - name: str, - current_user: User = Depends(get_current_user) -): - """获取工具详情""" - tool = registry.get(name) - - if not tool: - return {"success": False, "message": "工具不存在", "code": 404} - - return success_response(data={ - "name": tool.name, - "description": tool.description, - "parameters": tool.parameters, - "category": tool.category - }) - - -@router.post("/{name}/execute", response_model=dict) -def execute_tool( - name: str, - arguments: Dict[str, Any], - current_user: User = Depends(get_current_user) -): - """手动执行工具""" - result = registry.execute(name, arguments) - - if not result.get("success"): - return {"success": False, "message": result.get("error"), "code": 400} - - return success_response(data=result) diff --git a/alcor/services/__init__.py b/alcor/services/__init__.py deleted file mode 100644 index 3b00499..0000000 --- a/alcor/services/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""服务层模块""" -from alcor.services.llm_client import LLMClient, llm_client, LLMResponse -from alcor.services.chat import ChatService, chat_service - -__all__ = [ - "LLMClient", - "llm_client", - "LLMResponse", - "ChatService", - "chat_service" -] diff --git a/alcor/services/chat.py b/alcor/services/chat.py deleted file mode 100644 index 00208c3..0000000 --- a/alcor/services/chat.py +++ /dev/null @@ -1,262 +0,0 @@ -"""聊天服务模块""" -import json -import re -from typing import Dict, List, Optional, Any, Generator -from datetime import datetime - -from alcor.models import Conversation, Message -from alcor.tools.executor import ToolExecutor -from alcor.tools.core import registry -from alcor.services.llm_client import llm_client, LLMClient - - -# 最大迭代次数,防止无限循环 -MAX_ITERATIONS = 10 - - -class ChatService: - """聊天服务""" - - def __init__( - self, - llm_client: Optional[LLMClient] = None, - max_iterations: int = MAX_ITERATIONS - ): - self.llm_client = llm_client or llm_client - self.tool_executor = ToolExecutor(enable_cache=True, cache_ttl=300) - self.max_iterations = max_iterations - - def build_messages( - self, - conversation: Conversation, - include_system: bool = True - ) -> List[Dict[str, str]]: - """构建消息列表""" - messages = [] - - # 添加系统提示 - if include_system and conversation.system_prompt: - messages.append({ - "role": "system", - "content": conversation.system_prompt - }) - - # 添加历史消息 - for msg in conversation.messages.order_by(Message.created_at).all(): - try: - content_data = json.loads(msg.content) if msg.content else {} - if isinstance(content_data, dict): - text = content_data.get("text", "") - else: - text = str(msg.content) - except json.JSONDecodeError: - text = msg.content - - messages.append({ - "role": msg.role, - "content": text - }) - - return messages - - def stream_response( - self, - conversation: Conversation, - user_message: str, - tools_enabled: bool = True, - context: Optional[Dict[str, Any]] = None - ) -> Generator[Dict[str, Any], None, None]: - """ - 流式响应生成器 - - 生成事件类型: - - process_step: thinking/text/tool_call/tool_result 步骤 - - done: 最终响应完成 - - error: 出错时 - """ - try: - # 构建消息列表 - messages = self.build_messages(conversation) - - # 添加用户消息 - messages.append({ - "role": "user", - "content": user_message - }) - - # 获取工具列表 - tools = registry.list_all() if tools_enabled else None - - # 迭代处理 - iteration = 0 - full_response = "" - tool_calls_buffer: List[Dict] = [] - - while iteration < self.max_iterations: - iteration += 1 - - # 调用LLM - tool_calls_this_round = None - - for event in self.llm_client.stream( - model=conversation.model, - messages=messages, - tools=tools, - temperature=conversation.temperature, - max_tokens=conversation.max_tokens, - thinking_enabled=conversation.thinking_enabled - ): - event_type = event.get("type") - - if event_type == "content_delta": - # 内容增量 - content = event.get("content", "") - if content: - full_response += content - yield { - "type": "process_step", - "step_type": "text", - "content": content - } - - elif event_type == "done": - # 完成 - tool_calls_this_round = event.get("tool_calls") - - # 处理工具调用 - if tool_calls_this_round and tools_enabled: - yield { - "type": "process_step", - "step_type": "tool_call", - "tool_calls": tool_calls_this_round - } - - # 执行工具 - tool_results = self.tool_executor.process_tool_calls_parallel( - tool_calls_this_round - ) - - for result in tool_results: - yield { - "type": "process_step", - "step_type": "tool_result", - "result": result - } - - # 添加到消息历史 - messages.append({ - "role": "assistant", - "content": full_response, - "tool_calls": tool_calls_this_round - }) - - # 添加工具结果 - for tr in tool_results: - messages.append({ - "role": "tool", - "tool_call_id": tr.get("tool_call_id"), - "content": tr.get("content", ""), - "name": tr.get("name") - }) - - tool_calls_buffer.extend(tool_calls_this_round) - else: - # 没有工具调用,退出循环 - break - - # 如果没有更多工具调用,结束 - if not tool_calls_this_round or not tools_enabled: - break - - # 最终完成 - yield { - "type": "done", - "content": full_response, - "tool_calls": tool_calls_buffer if tool_calls_buffer else None, - "iterations": iteration - } - - except Exception as e: - yield { - "type": "error", - "error": str(e) - } - - def non_stream_response( - self, - conversation: Conversation, - user_message: str, - tools_enabled: bool = True, - context: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: - """非流式响应""" - try: - messages = self.build_messages(conversation) - messages.append({ - "role": "user", - "content": user_message - }) - - tools = registry.list_all() if tools_enabled else None - - # 迭代处理 - iteration = 0 - full_response = "" - all_tool_calls = [] - - while iteration < self.max_iterations: - iteration += 1 - - response = self.llm_client.call( - model=conversation.model, - messages=messages, - tools=tools, - stream=False, - temperature=conversation.temperature, - max_tokens=conversation.max_tokens - ) - - full_response = response.content - tool_calls = response.tool_calls - - if tool_calls and tools_enabled: - # 执行工具 - tool_results = self.tool_executor.process_tool_calls_parallel(tool_calls) - all_tool_calls.extend(tool_calls) - - messages.append({ - "role": "assistant", - "content": full_response, - "tool_calls": tool_calls - }) - - for tr in tool_results: - messages.append({ - "role": "tool", - "tool_call_id": tr.get("tool_call_id"), - "content": tr.get("content", ""), - "name": tr.get("name") - }) - else: - messages.append({ - "role": "assistant", - "content": full_response - }) - break - - return { - "success": True, - "content": full_response, - "tool_calls": all_tool_calls, - "iterations": iteration - } - - except Exception as e: - return { - "success": False, - "error": str(e) - } - - -# 全局聊天服务 -chat_service = ChatService() diff --git a/alcor/services/llm_client.py b/alcor/services/llm_client.py deleted file mode 100644 index 10ff1b3..0000000 --- a/alcor/services/llm_client.py +++ /dev/null @@ -1,256 +0,0 @@ -"""LLM API客户端""" -import json -from typing import Dict, List, Optional, Generator, Any, Callable, AsyncGenerator -from dataclasses import dataclass - -import httpx - -from alcor.config import config - - -@dataclass -class LLMResponse: - """LLM响应""" - content: str - tool_calls: Optional[List[Dict[str, Any]]] = None - usage: Optional[Dict[str, int]] = None - finish_reason: Optional[str] = None - raw: Optional[Dict] = None - - -class LLMClient: - """LLM API客户端,支持多种提供商""" - - def __init__( - self, - api_key: Optional[str] = None, - api_url: Optional[str] = None, - provider: Optional[str] = None - ): - self.api_key = api_key or config.llm_api_key - self.api_url = api_url or config.llm_api_url - self.provider = provider or config.llm_provider or self._detect_provider() - self._client: Optional[httpx.AsyncClient] = None - - def _detect_provider(self) -> str: - """检测提供商""" - url = self.api_url.lower() - if "deepseek" in url: - return "deepseek" - elif "bigmodel" in url or "glm" in url: - return "glm" - elif "zhipu" in url: - return "glm" - elif "qwen" in url or "dashscope" in url: - return "qwen" - elif "moonshot" in url or "moonshot" in url: - return "moonshot" - return "openai" - - @property - def client(self) -> httpx.AsyncClient: - """获取HTTP客户端""" - if self._client is None: - self._client = httpx.AsyncClient( - timeout=httpx.Timeout(120.0, connect=30.0), - headers={ - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - ) - return self._client - - async def close(self): - """关闭客户端""" - if self._client: - await self._client.aclose() - self._client = None - - def _build_headers(self) -> Dict[str, str]: - """构建请求头""" - return { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - - def _build_body( - self, - model: str, - messages: List[Dict[str, str]], - tools: Optional[List[Dict]] = None, - stream: bool = True, - **kwargs - ) -> Dict[str, Any]: - """构建请求体""" - body = { - "model": model, - "messages": messages, - "stream": stream - } - - # 添加可选参数 - if "temperature" in kwargs: - body["temperature"] = kwargs["temperature"] - if "max_tokens" in kwargs: - body["max_tokens"] = kwargs["max_tokens"] - if "top_p" in kwargs: - body["top_p"] = kwargs["top_p"] - if "thinking_enabled" in kwargs: - body["thinking_enabled"] = kwargs["thinking_enabled"] - - # 添加工具 - if tools: - body["tools"] = tools - - return body - - def _parse_response(self, data: Dict) -> LLMResponse: - """解析响应""" - # 通用字段 - content = "" - tool_calls = None - usage = None - finish_reason = None - - # OpenAI格式 - if "choices" in data: - choice = data["choices"][0] - message = choice.get("message", {}) - content = message.get("content", "") - tool_calls = message.get("tool_calls") - finish_reason = choice.get("finish_reason") - - # 使用量统计 - if "usage" in data: - usage = { - "prompt_tokens": data["usage"].get("prompt_tokens", 0), - "completion_tokens": data["usage"].get("completion_tokens", 0), - "total_tokens": data["usage"].get("total_tokens", 0) - } - - return LLMResponse( - content=content, - tool_calls=tool_calls, - usage=usage, - finish_reason=finish_reason, - raw=data - ) - - async def call( - self, - model: str, - messages: List[Dict[str, str]], - tools: Optional[List[Dict]] = None, - **kwargs - ) -> LLMResponse: - """调用LLM API(非流式)""" - body = self._build_body(model, messages, tools, stream=False, **kwargs) - - try: - response = await self.client.post( - self.api_url, - json=body, - headers=self._build_headers() - ) - response.raise_for_status() - data = response.json() - return self._parse_response(data) - except httpx.HTTPStatusError as e: - raise Exception(f"HTTP error: {e.response.status_code} - {e.response.text}") - except Exception as e: - raise Exception(f"LLM API error: {str(e)}") - - async def stream( - self, - model: str, - messages: List[Dict[str, str]], - tools: Optional[List[Dict]] = None, - **kwargs - ) -> AsyncGenerator[Dict[str, Any], None]: - """流式调用LLM API""" - body = self._build_body(model, messages, tools, stream=True, **kwargs) - - try: - async with self.client.stream( - "POST", - self.api_url, - json=body, - headers=self._build_headers() - ) as response: - response.raise_for_status() - - accumulated_content = "" - accumulated_tool_calls: Dict[int, Dict] = {} - - async for line in response.aiter_lines(): - if not line.strip(): - continue - - # 跳过SSE前缀 - if line.startswith("data: "): - line = line[6:] - - if line == "[DONE]": - break - - try: - chunk = json.loads(line) - except json.JSONDecodeError: - continue - - # 解析SSE数据 - delta = chunk.get("choices", [{}])[0].get("delta", {}) - - # 内容增量 - content_delta = delta.get("content", "") - if content_delta: - accumulated_content += content_delta - yield { - "type": "content_delta", - "content": content_delta, - "full_content": accumulated_content - } - - # 工具调用增量 - tool_calls = delta.get("tool_calls", []) - for tc in tool_calls: - index = tc.get("index", 0) - if index not in accumulated_tool_calls: - accumulated_tool_calls[index] = { - "id": "", - "type": "function", - "function": {"name": "", "arguments": ""} - } - - if tc.get("id"): - accumulated_tool_calls[index]["id"] = tc["id"] - if tc.get("function", {}).get("name"): - accumulated_tool_calls[index]["function"]["name"] = tc["function"]["name"] - if tc.get("function", {}).get("arguments"): - accumulated_tool_calls[index]["function"]["arguments"] += tc["function"]["arguments"] - - # 完成信号 - finish_reason = chunk.get("choices", [{}])[0].get("finish_reason") - if finish_reason: - yield { - "type": "done", - "finish_reason": finish_reason, - "content": accumulated_content, - "tool_calls": list(accumulated_tool_calls.values()) if accumulated_tool_calls else None, - "usage": chunk.get("usage") - } - - except httpx.HTTPStatusError as e: - yield { - "type": "error", - "error": f"HTTP error: {e.response.status_code}" - } - except Exception as e: - yield { - "type": "error", - "error": str(e) - } - - -# 全局LLM客户端 -llm_client = LLMClient() diff --git a/alcor/tools/__init__.py b/alcor/tools/__init__.py deleted file mode 100644 index 4ba94bb..0000000 --- a/alcor/tools/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""工具系统模块""" -from alcor.tools.core import ( - ToolDefinition, - ToolResult, - ToolRegistry, - registry -) -from alcor.tools.factory import tool, tool_function -from alcor.tools.executor import ToolExecutor - -__all__ = [ - "ToolDefinition", - "ToolResult", - "ToolRegistry", - "registry", - "tool", - "tool_function", - "ToolExecutor" -] diff --git a/alcor/tools/builtin/__init__.py b/alcor/tools/builtin/__init__.py deleted file mode 100644 index 4b4a219..0000000 --- a/alcor/tools/builtin/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""内置工具模块""" -# 导入所有内置工具以注册它们 -from alcor.tools.builtin import crawler -from alcor.tools.builtin import code -from alcor.tools.builtin import data - -__all__ = ["crawler", "code", "data"] diff --git a/alcor/tools/builtin/data.py b/alcor/tools/builtin/data.py deleted file mode 100644 index c8f171d..0000000 --- a/alcor/tools/builtin/data.py +++ /dev/null @@ -1,270 +0,0 @@ -"""数据处理工具""" -import re -import json -import hashlib -import base64 -import urllib.parse -from typing import Dict, Any, List - -from alcor.tools.factory import tool - - -@tool( - name="calculate", - description="Perform mathematical calculations", - parameters={ - "type": "object", - "properties": { - "expression": { - "type": "string", - "description": "Mathematical expression to evaluate (e.g., '2 + 2', 'sqrt(16)', 'sin(pi/2)')" - } - }, - "required": ["expression"] - }, - category="data" -) -def calculate(arguments: Dict[str, Any]) -> Dict[str, Any]: - """执行数学计算""" - expression = arguments.get("expression", "") - - if not expression: - return {"success": False, "error": "Expression is required"} - - try: - # 安全替换数学函数 - safe_dict = { - "abs": abs, - "round": round, - "min": min, - "max": max, - "sum": sum, - "pow": pow, - "sqrt": lambda x: x ** 0.5, - "sin": lambda x: __import__("math").sin(x), - "cos": lambda x: __import__("math").cos(x), - "tan": lambda x: __import__("math").tan(x), - "log": lambda x: __import__("math").log(x), - "pi": __import__("math").pi, - "e": __import__("math").e, - } - - # 移除危险字符,只保留数字和运算符 - safe_expr = re.sub(r"[^0-9+\-*/().%sqrtinsclogmaxminpowabsroundte, ]", "", expression) - result = eval(safe_expr, {"__builtins__": {}, **safe_dict}) - - return { - "success": True, - "data": { - "expression": expression, - "result": float(result) if isinstance(result, (int, float)) else result - } - } - except ZeroDivisionError: - return {"success": False, "error": "Division by zero"} - except Exception as e: - return {"success": False, "error": f"Calculation error: {str(e)}"} - - -@tool( - name="text_process", - description="Process and transform text", - parameters={ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Input text" - }, - "operation": { - "type": "string", - "description": "Operation to perform: upper, lower, title, reverse, word_count, char_count, reverse_words", - "enum": ["upper", "lower", "title", "reverse", "word_count", "char_count", "reverse_words"] - } - }, - "required": ["text", "operation"] - }, - category="data" -) -def text_process(arguments: Dict[str, Any]) -> Dict[str, Any]: - """文本处理""" - text = arguments.get("text", "") - operation = arguments.get("operation", "") - - if not text: - return {"success": False, "error": "Text is required"} - - operations = { - "upper": lambda t: t.upper(), - "lower": lambda t: t.lower(), - "title": lambda t: t.title(), - "reverse": lambda t: t[::-1], - "word_count": lambda t: len(t.split()), - "char_count": lambda t: len(t), - "reverse_words": lambda t: " ".join(t.split()[::-1]) - } - - if operation not in operations: - return { - "success": False, - "error": f"Unknown operation: {operation}" - } - - try: - result = operations[operation](text) - return { - "success": True, - "data": { - "operation": operation, - "input": text, - "result": result - } - } - except Exception as e: - return {"success": False, "error": str(e)} - - -@tool( - name="hash_text", - description="Generate hash of text using various algorithms", - parameters={ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to hash" - }, - "algorithm": { - "type": "string", - "description": "Hash algorithm: md5, sha1, sha256, sha512", - "default": "sha256" - } - }, - "required": ["text"] - }, - category="data" -) -def hash_text(arguments: Dict[str, Any]) -> Dict[str, Any]: - """生成文本哈希""" - text = arguments.get("text", "") - algorithm = arguments.get("algorithm", "sha256") - - if not text: - return {"success": False, "error": "Text is required"} - - hash_funcs = { - "md5": hashlib.md5, - "sha1": hashlib.sha1, - "sha256": hashlib.sha256, - "sha512": hashlib.sha512 - } - - if algorithm not in hash_funcs: - return { - "success": False, - "error": f"Unsupported algorithm: {algorithm}" - } - - try: - hash_obj = hash_funcs[algorithm](text.encode("utf-8")) - return { - "success": True, - "data": { - "algorithm": algorithm, - "hash": hash_obj.hexdigest() - } - } - except Exception as e: - return {"success": False, "error": str(e)} - - -@tool( - name="url_encode_decode", - description="URL encode or decode text", - parameters={ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to encode or decode" - }, - "operation": { - "type": "string", - "description": "Operation: encode or decode", - "enum": ["encode", "decode"] - } - }, - "required": ["text", "operation"] - }, - category="data" -) -def url_encode_decode(arguments: Dict[str, Any]) -> Dict[str, Any]: - """URL编码/解码""" - text = arguments.get("text", "") - operation = arguments.get("operation", "encode") - - if not text: - return {"success": False, "error": "Text is required"} - - try: - if operation == "encode": - result = urllib.parse.quote(text) - else: - result = urllib.parse.unquote(text) - - return { - "success": True, - "data": { - "operation": operation, - "input": text, - "result": result - } - } - except Exception as e: - return {"success": False, "error": str(e)} - - -@tool( - name="base64_encode_decode", - description="Base64 encode or decode text", - parameters={ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to encode or decode" - }, - "operation": { - "type": "string", - "description": "Operation: encode or decode", - "enum": ["encode", "decode"] - } - }, - "required": ["text", "operation"] - }, - category="data" -) -def base64_encode_decode(arguments: Dict[str, Any]) -> Dict[str, Any]: - """Base64编码/解码""" - text = arguments.get("text", "") - operation = arguments.get("operation", "encode") - - if not text: - return {"success": False, "error": "Text is required"} - - try: - if operation == "encode": - result = base64.b64encode(text.encode("utf-8")).decode("utf-8") - else: - result = base64.b64decode(text.encode("utf-8")).decode("utf-8") - - return { - "success": True, - "data": { - "operation": operation, - "input": text, - "result": result - } - } - except Exception as e: - return {"success": False, "error": str(e)} diff --git a/alcor/tools/executor.py b/alcor/tools/executor.py deleted file mode 100644 index cb9cecc..0000000 --- a/alcor/tools/executor.py +++ /dev/null @@ -1,186 +0,0 @@ -"""工具执行器""" -import json -import time -import hashlib -from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import List, Dict, Optional, Any - -from alcor.tools.core import registry, ToolResult - - -class ToolExecutor: - """工具执行器,支持缓存、并行执行""" - - def __init__( - self, - enable_cache: bool = True, - cache_ttl: int = 300, # 5分钟 - max_workers: int = 4 - ): - self.enable_cache = enable_cache - self.cache_ttl = cache_ttl - self.max_workers = max_workers - self._cache: Dict[str, tuple] = {} # (result, timestamp) - self._call_history: List[Dict[str, Any]] = [] - - def _make_cache_key(self, name: str, args: dict) -> str: - """生成缓存键""" - args_str = json.dumps(args, sort_keys=True, ensure_ascii=False) - return hashlib.md5(f"{name}:{args_str}".encode()).hexdigest() - - def _is_cache_valid(self, cache_key: str) -> bool: - """检查缓存是否有效""" - if cache_key not in self._cache: - return False - _, timestamp = self._cache[cache_key] - return (time.time() - timestamp) < self.cache_ttl - - def _get_cached(self, cache_key: str) -> Optional[Dict]: - """获取缓存结果""" - if self.enable_cache and self._is_cache_valid(cache_key): - return self._cache[cache_key][0] - return None - - def _set_cached(self, cache_key: str, result: Dict) -> None: - """设置缓存""" - if self.enable_cache: - self._cache[cache_key] = (result, time.time()) - - def _record_call(self, name: str, args: dict, result: Dict) -> None: - """记录调用历史""" - self._call_history.append({ - "name": name, - "args": args, - "result": result, - "timestamp": time.time() - }) - # 限制历史记录数量 - if len(self._call_history) > 1000: - self._call_history = self._call_history[-500:] - - def process_tool_calls( - self, - tool_calls: List[Dict[str, Any]], - context: Optional[Dict[str, Any]] = None - ) -> List[Dict[str, Any]]: - """顺序处理工具调用""" - results = [] - - for call in tool_calls: - name = call.get("function", {}).get("name", "") - args_str = call.get("function", {}).get("arguments", "{}") - call_id = call.get("id", "") - - # 解析JSON参数 - try: - args = json.loads(args_str) if isinstance(args_str, str) else args_str - except json.JSONDecodeError: - results.append(self._create_error_result(call_id, name, "Invalid JSON arguments")) - continue - - # 检查缓存 - cache_key = self._make_cache_key(name, args) - cached_result = self._get_cached(cache_key) - - if cached_result is not None: - result = cached_result - else: - # 执行工具 - result = registry.execute(name, args) - self._set_cached(cache_key, result) - - # 记录调用 - self._record_call(name, args, result) - - # 创建结果消息 - results.append(self._create_tool_result(call_id, name, result)) - - return results - - def process_tool_calls_parallel( - self, - tool_calls: List[Dict[str, Any]], - context: Optional[Dict[str, Any]] = None, - max_workers: Optional[int] = None - ) -> List[Dict[str, Any]]: - """并行处理工具调用""" - if len(tool_calls) <= 1: - return self.process_tool_calls(tool_calls, context) - - workers = max_workers or self.max_workers - results = [None] * len(tool_calls) - exec_tasks = {} - - # 解析所有参数 - for i, call in enumerate(tool_calls): - try: - name = call.get("function", {}).get("name", "") - args_str = call.get("function", {}).get("arguments", "{}") - call_id = call.get("id", "") - args = json.loads(args_str) if isinstance(args_str, str) else args_str - exec_tasks[i] = (call_id, name, args) - except json.JSONDecodeError: - results[i] = self._create_error_result( - call.get("id", ""), - call.get("function", {}).get("name", ""), - "Invalid JSON" - ) - - # 并行执行 - def run(call_id: str, name: str, args: dict) -> Dict[str, Any]: - # 检查缓存 - cache_key = self._make_cache_key(name, args) - cached_result = self._get_cached(cache_key) - - if cached_result is not None: - result = cached_result - else: - result = registry.execute(name, args) - self._set_cached(cache_key, result) - - self._record_call(name, args, result) - return self._create_tool_result(call_id, name, result) - - with ThreadPoolExecutor(max_workers=workers) as pool: - futures = { - pool.submit(run, cid, n, a): i - for i, (cid, n, a) in exec_tasks.items() - } - for future in as_completed(futures): - idx = futures[future] - try: - results[idx] = future.result() - except Exception as e: - results[idx] = self._create_error_result( - exec_tasks[idx][0] if idx in exec_tasks else "", - exec_tasks[idx][1] if idx in exec_tasks else "", - str(e) - ) - - return results - - def _create_tool_result(self, call_id: str, name: str, result: Dict) -> Dict[str, Any]: - """创建工具结果消息""" - return { - "role": "tool", - "tool_call_id": call_id, - "name": name, - "content": json.dumps(result, ensure_ascii=False, default=str) - } - - def _create_error_result(self, call_id: str, name: str, error: str) -> Dict[str, Any]: - """创建错误结果消息""" - return { - "role": "tool", - "tool_call_id": call_id, - "name": name, - "content": json.dumps({"success": False, "error": error}) - } - - def clear_cache(self) -> None: - """清空缓存""" - self._cache.clear() - - def get_history(self, limit: int = 100) -> List[Dict[str, Any]]: - """获取调用历史""" - return self._call_history[-limit:] diff --git a/alcor/utils/__init__.py b/alcor/utils/__init__.py deleted file mode 100644 index 408e274..0000000 --- a/alcor/utils/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""工具函数模块""" -from alcor.utils.helpers import ( - generate_id, - hash_password, - verify_password, - create_access_token, - decode_access_token, - success_response, - error_response, - paginate -) - -__all__ = [ - "generate_id", - "hash_password", - "verify_password", - "create_access_token", - "decode_access_token", - "success_response", - "error_response", - "paginate" -] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 05fde28..55b9601 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,7 +12,7 @@ ## 目录结构 ``` -alcor/ +luxx/ ├── __init__.py # FastAPI 应用工厂 ├── run.py # 入口文件 ├── config.py # 配置管理(YAML) diff --git a/luxx/__init__.py b/luxx/__init__.py new file mode 100644 index 0000000..57f1606 --- /dev/null +++ b/luxx/__init__.py @@ -0,0 +1,57 @@ +"""FastAPI application factory""" +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from luxx.config import config +from luxx.database import init_db +from luxx.routes import api_router + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager""" + init_db() + from luxx.tools.builtin import crawler, code, data + yield + + +def create_app() -> FastAPI: + """Create FastAPI application""" + app = FastAPI( + title="luxx API", + description="Intelligent chat backend API with multi-model support, streaming responses, and tool calling", + version="1.0.0", + lifespan=lifespan + ) + + # Configure CORS + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Should be restricted in production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Register routes + app.include_router(api_router, prefix="/api") + + # Health check + @app.get("/health") + async def health_check(): + return {"status": "healthy", "service": "luxx"} + + @app.get("/") + async def root(): + return { + "service": "luxx API", + "version": "1.0.0", + "docs": "/docs" + } + + return app + + +# Create application instance +app = create_app() diff --git a/alcor/config.py b/luxx/config.py similarity index 88% rename from alcor/config.py rename to luxx/config.py index 2774dd3..bf8297f 100644 --- a/alcor/config.py +++ b/luxx/config.py @@ -1,4 +1,4 @@ -"""配置管理模块""" +"""Configuration management module""" import os import yaml from pathlib import Path @@ -6,7 +6,7 @@ from typing import Any, Dict, Optional class Config: - """配置类(单例模式)""" + """Configuration class (singleton pattern)""" _instance: Optional["Config"] = None _config: Dict[str, Any] = {} @@ -18,7 +18,7 @@ class Config: return cls._instance def _load_config(self) -> None: - """加载配置文件""" + """Load configuration from YAML file""" yaml_paths = [ Path("config.yaml"), Path(__file__).parent.parent / "config.yaml", @@ -35,7 +35,7 @@ class Config: self._config = {} def _resolve_env_vars(self) -> None: - """解析环境变量引用""" + """Resolve environment variable references""" def resolve(value: Any) -> Any: if isinstance(value, str) and value.startswith("${") and value.endswith("}"): return os.environ.get(value[2:-1], "") @@ -48,7 +48,7 @@ class Config: self._config = resolve(self._config) def get(self, key: str, default: Any = None) -> Any: - """获取配置值,支持点号分隔的键""" + """Get configuration value, supports dot-separated keys""" keys = key.split(".") value = self._config for k in keys: @@ -60,7 +60,7 @@ class Config: return default return value - # App配置 + # App configuration @property def secret_key(self) -> str: return self.get("app.secret_key", "change-me-in-production") @@ -77,12 +77,12 @@ class Config: def app_port(self) -> int: return self.get("app.port", 8000) - # 数据库配置 + # Database configuration @property def database_url(self) -> str: return self.get("database.url", "sqlite:///./chat.db") - # LLM配置 + # LLM configuration @property def llm_api_key(self) -> str: return self.get("llm.api_key", "") or os.environ.get("DEEPSEEK_API_KEY", "") @@ -95,7 +95,7 @@ class Config: def llm_provider(self) -> str: return self.get("llm.provider", "deepseek") - # 工具配置 + # Tools configuration @property def tools_enable_cache(self) -> bool: return self.get("tools.enable_cache", True) @@ -113,5 +113,5 @@ class Config: return self.get("tools.max_iterations", 10) -# 全局配置实例 +# Global configuration instance config = Config() diff --git a/alcor/database.py b/luxx/database.py similarity index 55% rename from alcor/database.py rename to luxx/database.py index 3e4c1b6..04092c3 100644 --- a/alcor/database.py +++ b/luxx/database.py @@ -1,29 +1,27 @@ -"""数据库连接模块""" +"""Database connection module""" from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.orm import sessionmaker, declarative_base, Mapped from typing import Generator -from alcor.config import config +from luxx.config import config -# 创建数据库引擎 +# Create database engine engine = create_engine( config.database_url, connect_args={"check_same_thread": False} if "sqlite" in config.database_url else {}, - pool_pre_ping=True, echo=config.debug ) -# 创建会话工厂 +# Create session factory SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -# 创建基类 +# Create base class Base = declarative_base() -def get_db() -> Generator[Session, None, None]: - """获取数据库会话的依赖项""" +def get_db() -> Generator: + """Dependency to get database session""" db = SessionLocal() try: yield db @@ -32,5 +30,5 @@ def get_db() -> Generator[Session, None, None]: def init_db() -> None: - """初始化数据库,创建所有表""" + """Initialize database, create all tables""" Base.metadata.create_all(bind=engine) diff --git a/alcor/models.py b/luxx/models.py similarity index 62% rename from alcor/models.py rename to luxx/models.py index 012d757..fe8a133 100644 --- a/alcor/models.py +++ b/luxx/models.py @@ -1,47 +1,34 @@ -"""ORM模型定义""" +"""ORM model definitions""" from datetime import datetime from typing import Optional, List -from sqlalchemy import String, Text, Integer, Float, Boolean, DateTime, ForeignKey -from sqlalchemy.orm import relationship, Mapped, mapped_column +from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship, DeclarativeBase -from alcor.database import Base + +class Base(DeclarativeBase): + pass class Project(Base): - """项目模型""" + """Project model""" __tablename__ = "projects" id: Mapped[str] = mapped_column(String(64), primary_key=True) - user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True) - name: Mapped[str] = mapped_column(String(255), default="") + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - # 关系 + # Relationships user: Mapped["User"] = relationship("User", backref="projects") - conversations: Mapped[List["Conversation"]] = relationship( - "Conversation", - back_populates="project", - lazy="dynamic" - ) - - def to_dict(self) -> dict: - return { - "id": self.id, - "user_id": self.user_id, - "name": self.name, - "description": self.description, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None - } class User(Base): - """用户模型""" + """User model""" __tablename__ = "users" - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) email: Mapped[Optional[str]] = mapped_column(String(120), unique=True, nullable=True) password_hash: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) @@ -49,14 +36,12 @@ class User(Base): is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - # 关系 + # Relationships conversations: Mapped[List["Conversation"]] = relationship( - "Conversation", - back_populates="user", - lazy="dynamic" + "Conversation", back_populates="user", cascade="all, delete-orphan" ) - def to_dict(self) -> dict: + def to_dict(self): return { "id": self.id, "username": self.username, @@ -68,33 +53,28 @@ class User(Base): class Conversation(Base): - """会话模型""" + """Conversation model""" __tablename__ = "conversations" id: Mapped[str] = mapped_column(String(64), primary_key=True) - user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) project_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("projects.id"), nullable=True) - title: Mapped[str] = mapped_column(String(255), default="") - model: Mapped[str] = mapped_column(String(64), default="glm-5") - system_prompt: Mapped[str] = mapped_column(Text, default="") - temperature: Mapped[float] = mapped_column(Float, default=1.0) - max_tokens: Mapped[int] = mapped_column(Integer, default=65536) + title: Mapped[str] = mapped_column(String(255), nullable=False) + model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat") + system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.") + temperature: Mapped[float] = mapped_column(Float, default=0.7) + max_tokens: Mapped[int] = mapped_column(Integer, default=2000) thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - # 关系 + # Relationships user: Mapped["User"] = relationship("User", back_populates="conversations") - project: Mapped[Optional["Project"]] = relationship("Project", back_populates="conversations") messages: Mapped[List["Message"]] = relationship( - "Message", - back_populates="conversation", - lazy="dynamic", - cascade="all, delete-orphan", - order_by="Message.created_at.asc()" + "Message", back_populates="conversation", cascade="all, delete-orphan" ) - def to_dict(self) -> dict: + def to_dict(self): return { "id": self.id, "user_id": self.user_id, @@ -111,25 +91,20 @@ class Conversation(Base): class Message(Base): - """消息模型""" + """Message model""" __tablename__ = "messages" id: Mapped[str] = mapped_column(String(64), primary_key=True) - conversation_id: Mapped[str] = mapped_column( - String(64), - ForeignKey("conversations.id"), - nullable=False, - index=True - ) - role: Mapped[str] = mapped_column(String(16), nullable=False) # user, assistant, system, tool - content: Mapped[str] = mapped_column(Text, default="") # JSON: {text, steps, tool_calls} + conversation_id: Mapped[str] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=False) + role: Mapped[str] = mapped_column(String(16), nullable=False) + content: Mapped[str] = mapped_column(Text, nullable=False) token_count: Mapped[int] = mapped_column(Integer, default=0) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - # 关系 + # Relationships conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages") - def to_dict(self) -> dict: + def to_dict(self): return { "id": self.id, "conversation_id": self.conversation_id, diff --git a/alcor/routes/__init__.py b/luxx/routes/__init__.py similarity index 64% rename from alcor/routes/__init__.py rename to luxx/routes/__init__.py index 818b718..c354528 100644 --- a/alcor/routes/__init__.py +++ b/luxx/routes/__init__.py @@ -1,14 +1,13 @@ -"""API路由模块""" +"""API routes module""" from fastapi import APIRouter -from alcor.routes import auth, conversations, messages, tools +from luxx.routes import auth, conversations, messages, tools + api_router = APIRouter() -# 注册子路由 +# Register sub-routes api_router.include_router(auth.router) api_router.include_router(conversations.router) api_router.include_router(messages.router) api_router.include_router(tools.router) - -__all__ = ["api_router"] diff --git a/alcor/routes/auth.py b/luxx/routes/auth.py similarity index 51% rename from alcor/routes/auth.py rename to luxx/routes/auth.py index e6890f0..848ec3b 100644 --- a/alcor/routes/auth.py +++ b/luxx/routes/auth.py @@ -1,134 +1,112 @@ -"""认证路由""" +"""Authentication routes""" from datetime import timedelta -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session +from pydantic import BaseModel -from alcor.database import get_db -from alcor.models import User -from alcor.utils.helpers import ( - hash_password, - verify_password, +from luxx.database import get_db +from luxx.models import User +from luxx.utils.helpers import ( + hash_password, + verify_password, create_access_token, + decode_access_token, success_response, error_response ) -router = APIRouter(prefix="/auth", tags=["认证"]) +router = APIRouter(prefix="/auth", tags=["Authentication"]) -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") class UserRegister(BaseModel): - """用户注册模型""" + """User registration model""" username: str - email: Optional[EmailStr] = None + email: str | None = None password: str class UserLogin(BaseModel): - """用户登录模型""" + """User login model""" username: str password: str class UserResponse(BaseModel): - """用户响应模型""" + """User response model""" id: int username: str - email: Optional[str] = None + email: str | None role: str - is_active: bool class TokenResponse(BaseModel): - """令牌响应模型""" + """Token response model""" access_token: str - token_type: str = "bearer" + token_type: str def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ) -> User: - """获取当前用户""" - from alcor.utils.helpers import decode_access_token - + """Get current user""" payload = decode_access_token(token) - if payload is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="无效的认证凭证", - headers={"WWW-Authenticate": "Bearer"}, - ) - + if not payload: + raise status.HTTP_401_UNAUTHORIZED user_id = payload.get("sub") - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="无效的认证凭证" - ) - - user = db.query(User).filter(User.id == user_id).first() - if user is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="用户不存在" - ) - + if not user_id: + raise status.HTTP_401_UNAUTHORIZED + user = db.query(User).filter(User.id == int(user_id)).first() + if not user: + raise status.HTTP_401_UNAUTHORIZED return user @router.post("/register", response_model=dict) def register(user_data: UserRegister, db: Session = Depends(get_db)): - """用户注册""" - # 检查用户名是否存在 + """User registration""" existing_user = db.query(User).filter(User.username == user_data.username).first() if existing_user: - return error_response("用户名已存在", 400) + return error_response("Username already exists", 400) - # 检查邮箱是否存在 if user_data.email: existing_email = db.query(User).filter(User.email == user_data.email).first() if existing_email: - return error_response("邮箱已被注册", 400) + return error_response("Email already registered", 400) - # 创建用户 password_hash = hash_password(user_data.password) user = User( username=user_data.username, email=user_data.email, - password_hash=password_hash, - role="user" + password_hash=password_hash ) - db.add(user) db.commit() db.refresh(user) return success_response( data={"id": user.id, "username": user.username}, - message="注册成功" + message="Registration successful" ) @router.post("/login", response_model=dict) def login(user_data: UserLogin, db: Session = Depends(get_db)): - """用户登录""" + """User login""" user = db.query(User).filter(User.username == user_data.username).first() if not user or not verify_password(user_data.password, user.password_hash or ""): - return error_response("用户名或密码错误", 401) + return error_response("Invalid username or password", 401) if not user.is_active: - return error_response("用户已被禁用", 403) + return error_response("User account is disabled", 403) - # 创建访问令牌 access_token = create_access_token( - data={"sub": user.id, "username": user.username}, + data={"sub": str(user.id)}, expires_delta=timedelta(days=7) ) @@ -138,17 +116,17 @@ def login(user_data: UserLogin, db: Session = Depends(get_db)): "token_type": "bearer", "user": user.to_dict() }, - message="登录成功" + message="Login successful" ) @router.post("/logout") def logout(current_user: User = Depends(get_current_user)): - """用户登出(前端清除令牌即可)""" - return success_response(message="登出成功") + """User logout (client should delete token)""" + return success_response(message="Logout successful") @router.get("/me", response_model=dict) def get_me(current_user: User = Depends(get_current_user)): - """获取当前用户信息""" + """Get current user info""" return success_response(data=current_user.to_dict()) diff --git a/alcor/routes/conversations.py b/luxx/routes/conversations.py similarity index 70% rename from alcor/routes/conversations.py rename to luxx/routes/conversations.py index 394a4f7..11d63e5 100644 --- a/alcor/routes/conversations.py +++ b/luxx/routes/conversations.py @@ -1,31 +1,31 @@ -"""会话路由""" +"""Conversation routes""" from typing import Optional, List -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session -from alcor.database import get_db -from alcor.models import Conversation, User -from alcor.routes.auth import get_current_user -from alcor.utils.helpers import generate_id, success_response, error_response, paginate +from luxx.database import get_db +from luxx.models import Conversation, User +from luxx.routes.auth import get_current_user +from luxx.utils.helpers import generate_id, success_response, error_response, paginate -router = APIRouter(prefix="/conversations", tags=["会话"]) +router = APIRouter(prefix="/conversations", tags=["Conversations"]) class ConversationCreate(BaseModel): - """创建会话模型""" + """Create conversation model""" project_id: Optional[str] = None - title: str = "" - model: str = "glm-5" - system_prompt: str = "" - temperature: float = 1.0 - max_tokens: int = 65536 + title: Optional[str] = None + model: str = "deepseek-chat" + system_prompt: str = "You are a helpful assistant." + temperature: float = 0.7 + max_tokens: int = 2000 thinking_enabled: bool = False class ConversationUpdate(BaseModel): - """更新会话模型""" + """Update conversation model""" title: Optional[str] = None model: Optional[str] = None system_prompt: Optional[str] = None @@ -36,25 +36,16 @@ class ConversationUpdate(BaseModel): @router.get("/", response_model=dict) def list_conversations( - project_id: Optional[str] = None, page: int = 1, page_size: int = 20, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """获取会话列表""" + """Get conversation list""" query = db.query(Conversation).filter(Conversation.user_id == current_user.id) - - if project_id: - query = query.filter(Conversation.project_id == project_id) - - query = query.order_by(Conversation.updated_at.desc()) - - result = paginate(query, page, page_size) - items = [conv.to_dict() for conv in result["items"]] - + result = paginate(query.order_by(Conversation.updated_at.desc()), page, page_size) return success_response(data={ - "items": items, + "conversations": [c.to_dict() for c in result["items"]], "total": result["total"], "page": result["page"], "page_size": result["page_size"] @@ -67,12 +58,12 @@ def create_conversation( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """创建会话""" + """Create conversation""" conversation = Conversation( id=generate_id("conv"), user_id=current_user.id, project_id=data.project_id, - title=data.title or "新会话", + title=data.title or "New Conversation", model=data.model, system_prompt=data.system_prompt, temperature=data.temperature, @@ -84,7 +75,7 @@ def create_conversation( db.commit() db.refresh(conversation) - return success_response(data=conversation.to_dict(), message="会话创建成功") + return success_response(data=conversation.to_dict(), message="Conversation created successfully") @router.get("/{conversation_id}", response_model=dict) @@ -93,14 +84,14 @@ def get_conversation( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """获取会话详情""" + """Get conversation details""" conversation = db.query(Conversation).filter( Conversation.id == conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) return success_response(data=conversation.to_dict()) @@ -112,16 +103,15 @@ def update_conversation( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """更新会话""" + """Update conversation""" conversation = db.query(Conversation).filter( Conversation.id == conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) - # 更新字段 update_data = data.dict(exclude_unset=True) for key, value in update_data.items(): setattr(conversation, key, value) @@ -129,7 +119,7 @@ def update_conversation( db.commit() db.refresh(conversation) - return success_response(data=conversation.to_dict(), message="会话更新成功") + return success_response(data=conversation.to_dict(), message="Conversation updated successfully") @router.delete("/{conversation_id}", response_model=dict) @@ -138,16 +128,16 @@ def delete_conversation( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """删除会话""" + """Delete conversation""" conversation = db.query(Conversation).filter( Conversation.id == conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) db.delete(conversation) db.commit() - return success_response(message="会话删除成功") + return success_response(message="Conversation deleted successfully") diff --git a/alcor/routes/messages.py b/luxx/routes/messages.py similarity index 57% rename from alcor/routes/messages.py rename to luxx/routes/messages.py index 2443860..1882040 100644 --- a/alcor/routes/messages.py +++ b/luxx/routes/messages.py @@ -1,120 +1,103 @@ -"""消息路由""" +"""Message routes""" import json -from typing import Optional, List -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, Response from fastapi.responses import StreamingResponse from pydantic import BaseModel from sqlalchemy.orm import Session +from datetime import datetime -from alcor.database import get_db -from alcor.models import Conversation, Message, User -from alcor.routes.auth import get_current_user -from alcor.services.chat import chat_service -from alcor.utils.helpers import generate_id, success_response, error_response +from luxx.database import get_db +from luxx.models import Conversation, Message, User +from luxx.routes.auth import get_current_user +from luxx.services.chat import chat_service +from luxx.utils.helpers import generate_id, success_response, error_response -router = APIRouter(prefix="/messages", tags=["消息"]) +router = APIRouter(prefix="/messages", tags=["Messages"]) class MessageCreate(BaseModel): - """创建消息模型""" + """Create message model""" conversation_id: str content: str - tools_enabled: bool = True class MessageResponse(BaseModel): - """消息响应模型""" + """Message response model""" id: str - conversation_id: str role: str content: str token_count: int - created_at: str -@router.get("/{conversation_id}", response_model=dict) +@router.get("/", response_model=dict) def list_messages( conversation_id: str, - limit: int = 100, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """获取消息列表""" - # 验证会话归属 + """Get message list""" conversation = db.query(Conversation).filter( Conversation.id == conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) messages = db.query(Message).filter( Message.conversation_id == conversation_id - ).order_by(Message.created_at.desc()).limit(limit).all() - - items = [msg.to_dict() for msg in reversed(messages)] + ).order_by(Message.created_at).all() return success_response(data={ - "items": items, - "total": len(items) + "messages": [m.to_dict() for m in messages] }) @router.post("/", response_model=dict) -async def create_message( +def send_message( data: MessageCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """发送消息(非流式)""" - # 验证会话 + """Send message (non-streaming)""" conversation = db.query(Conversation).filter( Conversation.id == data.conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) - # 保存用户消息 user_message = Message( id=generate_id("msg"), conversation_id=data.conversation_id, role="user", - content=json.dumps({"text": data.content}) + content=data.content, + token_count=len(data.content) // 4 ) db.add(user_message) - # 更新会话时间 from datetime import datetime conversation.updated_at = datetime.utcnow() - db.commit() - db.refresh(user_message) - - # 获取AI响应(非流式) response = chat_service.non_stream_response( conversation=conversation, user_message=data.content, - tools_enabled=data.tools_enabled + tools_enabled=False ) if not response.get("success"): - return error_response(response.get("error", "生成响应失败"), 500) + return error_response(response.get("error", "Failed to generate response"), 500) - # 保存AI响应 ai_content = response.get("content", "") + ai_message = Message( id=generate_id("msg"), conversation_id=data.conversation_id, role="assistant", - content=json.dumps({ - "text": ai_content, - "tool_calls": response.get("tool_calls") - }), - token_count=len(ai_content) // 4 # 粗略估算 + content=ai_content, + token_count=len(ai_content) // 4 ) db.add(ai_message) db.commit() @@ -128,77 +111,66 @@ async def create_message( @router.post("/stream") async def stream_message( data: MessageCreate, + tools_enabled: bool = True, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """发送消息(流式响应 - SSE)""" - # 验证会话 + """Send message (streaming response - SSE)""" conversation = db.query(Conversation).filter( Conversation.id == data.conversation_id, Conversation.user_id == current_user.id ).first() if not conversation: - return error_response("会话不存在", 404) + return error_response("Conversation not found", 404) - # 保存用户消息 user_message = Message( id=generate_id("msg"), conversation_id=data.conversation_id, role="user", - content=json.dumps({"text": data.content}) + content=data.content, + token_count=len(data.content) // 4 ) db.add(user_message) - - # 更新会话时间 - from datetime import datetime conversation.updated_at = datetime.utcnow() - db.commit() - db.refresh(user_message) async def event_generator(): - """SSE事件生成器""" full_response = "" - message_id = generate_id("msg") async for event in chat_service.stream_response( conversation=conversation, user_message=data.content, - tools_enabled=data.tools_enabled + tools_enabled=tools_enabled ): event_type = event.get("type") - if event_type == "process_step": - step_type = event.get("step_type") - - if step_type == "text": - content = event.get("content", "") - full_response += content - yield f"data: {json.dumps({'type': 'text', 'content': content})}\n\n" - - elif step_type == "tool_call": - yield f"data: {json.dumps({'type': 'tool_call', 'tool_calls': event.get('tool_calls')})}\n\n" - - elif step_type == "tool_result": - yield f"data: {json.dumps({'type': 'tool_result', 'result': event.get('result')})}\n\n" + if event_type == "text": + content = event.get("content", "") + full_response += content + yield f"data: {json.dumps({'type': 'text', 'content': content})}\n\n" + + elif event_type == "tool_call": + yield f"data: {json.dumps({'type': 'tool_call', 'data': event.get('data')})}\n\n" + + elif event_type == "tool_result": + yield f"data: {json.dumps({'type': 'tool_result', 'data': event.get('data')})}\n\n" elif event_type == "done": - # 保存AI消息 try: ai_message = Message( - id=message_id, + id=generate_id("msg"), conversation_id=data.conversation_id, role="assistant", - content=json.dumps({"text": full_response}), + content=full_response, token_count=len(full_response) // 4 ) db.add(ai_message) db.commit() - except Exception as e: - db.rollback() + except Exception: + pass - yield f"data: {json.dumps({'type': 'done', 'message_id': message_id})}\n\n" + yield f"data: {json.dumps({'type': 'done', 'message_id': ai_message.id if 'ai_message' in dir() else None})}\n\n" elif event_type == "error": yield f"data: {json.dumps({'type': 'error', 'error': event.get('error')})}\n\n" @@ -222,17 +194,16 @@ def delete_message( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """删除消息""" - # 获取消息及其会话 + """Delete message""" message = db.query(Message).join(Conversation).filter( Message.id == message_id, Conversation.user_id == current_user.id ).first() if not message: - return error_response("消息不存在", 404) + return error_response("Message not found", 404) db.delete(message) db.commit() - return success_response(message="消息删除成功") + return success_response(message="Message deleted successfully") diff --git a/luxx/routes/tools.py b/luxx/routes/tools.py new file mode 100644 index 0000000..0456fc1 --- /dev/null +++ b/luxx/routes/tools.py @@ -0,0 +1,63 @@ +"""Tool routes""" +from typing import Optional, List, Dict, Any +from fastapi import APIRouter, Depends, Body +from pydantic import BaseModel + +from luxx.database import get_db +from luxx.models import User +from luxx.routes.auth import get_current_user +from luxx.tools.core import registry +from luxx.utils.helpers import success_response + + +router = APIRouter(prefix="/tools", tags=["Tools"]) + + +@router.get("/", response_model=dict) +def list_tools( + category: Optional[str] = None, + current_user: User = Depends(get_current_user) +): + """Get available tools list""" + if category: + tools = registry.list_by_category(category) + else: + tools = registry.list_all() + + categorized = {} + for tool in tools: + cat = tool.get("category", "other") + if cat not in categorized: + categorized[cat] = [] + categorized[cat].append(tool) + + return success_response(data={ + "tools": tools, + "categorized": categorized, + "total": len(tools) + }) + + +@router.get("/{name}", response_model=dict) +def get_tool( + name: str, + current_user: User = Depends(get_current_user) +): + """Get tool details""" + tool = registry.get(name) + + if not tool: + return {"success": False, "message": "Tool not found", "code": 404} + + return success_response(data=tool.to_openai_format()) + + +@router.post("/{name}/execute", response_model=dict) +def execute_tool( + name: str, + arguments: Dict[str, Any] = Body(...), + current_user: User = Depends(get_current_user) +): + """Execute tool manually""" + result = registry.execute(name, arguments) + return result diff --git a/alcor/run.py b/luxx/run.py similarity index 69% rename from alcor/run.py rename to luxx/run.py index fe39ed1..54dc763 100644 --- a/alcor/run.py +++ b/luxx/run.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -"""应用入口""" +"""Application entry point""" import uvicorn -from alcor.config import config +from luxx.config import config def main(): - """启动应用""" + """Start the application""" uvicorn.run( - "alcor:app", + "luxx:app", host=config.app_host, port=config.app_port, reload=config.debug, diff --git a/luxx/services/__init__.py b/luxx/services/__init__.py new file mode 100644 index 0000000..d4d0ab0 --- /dev/null +++ b/luxx/services/__init__.py @@ -0,0 +1,3 @@ +"""Services module""" +from luxx.services.llm_client import LLMClient, llm_client, LLMResponse +from luxx.services.chat import ChatService, chat_service diff --git a/luxx/services/chat.py b/luxx/services/chat.py new file mode 100644 index 0000000..a4b9075 --- /dev/null +++ b/luxx/services/chat.py @@ -0,0 +1,194 @@ +"""Chat service module""" +import json +from typing import List, Dict, Any, AsyncGenerator + +from luxx.models import Conversation, Message +from luxx.tools.executor import ToolExecutor +from luxx.tools.core import registry +from luxx.services.llm_client import llm_client + + +# Maximum iterations to prevent infinite loops +MAX_ITERATIONS = 10 + + +class ChatService: + """Chat service""" + + def __init__(self): + self.tool_executor = ToolExecutor() + + def build_messages( + self, + conversation: Conversation, + include_system: bool = True + ) -> List[Dict[str, str]]: + """Build message list""" + messages = [] + + if include_system and conversation.system_prompt: + messages.append({ + "role": "system", + "content": conversation.system_prompt + }) + + for msg in conversation.messages.order_by(Message.created_at).all(): + messages.append({ + "role": msg.role, + "content": msg.content + }) + + return messages + + async def stream_response( + self, + conversation: Conversation, + user_message: str, + tools_enabled: bool = True + ) -> AsyncGenerator[Dict[str, Any], None]: + """ + Streaming response generator + + Event types: + - process_step: thinking/text/tool_call/tool_result step + - done: final response complete + - error: on error + """ + try: + messages = self.build_messages(conversation) + + messages.append({ + "role": "user", + "content": user_message + }) + + tools = registry.list_all() if tools_enabled else None + + iteration = 0 + + while iteration < MAX_ITERATIONS: + iteration += 1 + + tool_calls_this_round = None + + async for event in llm_client.stream_call( + model=conversation.model, + messages=messages, + tools=tools, + temperature=conversation.temperature, + max_tokens=conversation.max_tokens + ): + event_type = event.get("type") + + if event_type == "content_delta": + content = event.get("content", "") + if content: + yield {"type": "text", "content": content} + + elif event_type == "tool_call_delta": + tool_call = event.get("tool_call", {}) + yield {"type": "tool_call", "data": tool_call} + + elif event_type == "done": + tool_calls_this_round = event.get("tool_calls") + + if tool_calls_this_round and tools_enabled: + yield {"type": "tool_call", "data": tool_calls_this_round} + + tool_results = self.tool_executor.process_tool_calls_parallel( + tool_calls_this_round, + {} + ) + + messages.append({ + "role": "assistant", + "content": "", + "tool_calls": tool_calls_this_round + }) + + for tr in tool_results: + messages.append({ + "role": "tool", + "tool_call_id": tr.get("tool_call_id"), + "content": str(tr.get("result", "")) + }) + + yield {"type": "tool_result", "data": tool_results} + + else: + break + + if not tool_calls_this_round or not tools_enabled: + break + + yield {"type": "done"} + + except Exception as e: + yield {"type": "error", "error": str(e)} + + def non_stream_response( + self, + conversation: Conversation, + user_message: str, + tools_enabled: bool = False + ) -> Dict[str, Any]: + """Non-streaming response""" + try: + messages = self.build_messages(conversation) + messages.append({ + "role": "user", + "content": user_message + }) + + tools = registry.list_all() if tools_enabled else None + + iteration = 0 + + while iteration < MAX_ITERATIONS: + iteration += 1 + + response = llm_client.sync_call( + model=conversation.model, + messages=messages, + tools=tools, + temperature=conversation.temperature, + max_tokens=conversation.max_tokens + ) + + tool_calls = response.tool_calls + + if tool_calls and tools_enabled: + messages.append({ + "role": "assistant", + "content": response.content, + "tool_calls": tool_calls + }) + + tool_results = self.tool_executor.process_tool_calls_parallel(tool_calls) + + for tr in tool_results: + messages.append({ + "role": "tool", + "tool_call_id": tr.get("tool_call_id"), + "content": str(tr.get("result", "")) + }) + else: + return { + "success": True, + "content": response.content + } + + return { + "success": True, + "content": "Max iterations reached" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +# Global chat service +chat_service = ChatService() diff --git a/luxx/services/llm_client.py b/luxx/services/llm_client.py new file mode 100644 index 0000000..52f312d --- /dev/null +++ b/luxx/services/llm_client.py @@ -0,0 +1,187 @@ +"""LLM API client""" +import json +import httpx +from typing import Dict, Any, Optional, List, AsyncGenerator + +from luxx.config import config + + +class LLMResponse: + """LLM response""" + content: str + tool_calls: Optional[List[Dict]] = None + usage: Optional[Dict] = None + + def __init__( + self, + content: str = "", + tool_calls: Optional[List[Dict]] = None, + usage: Optional[Dict] = None + ): + self.content = content + self.tool_calls = tool_calls + self.usage = usage + + +class LLMClient: + """LLM API client with multi-provider support""" + + def __init__(self): + self.api_key = config.llm_api_key + self.api_url = config.llm_api_url + self.provider = self._detect_provider() + self._client: Optional[httpx.AsyncClient] = None + + def _detect_provider(self) -> str: + """Detect provider from URL""" + url = self.api_url.lower() + if "deepseek" in url: + return "deepseek" + elif "glm" in url or "zhipu" in url: + return "glm" + elif "openai" in url: + return "openai" + return "openai" + + async def close(self): + """Close client""" + if self._client: + await self._client.aclose() + self._client = None + + def _build_headers(self) -> Dict[str, str]: + """Build request headers""" + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + + def _build_body( + self, + model: str, + messages: List[Dict], + tools: Optional[List[Dict]] = None, + stream: bool = False, + **kwargs + ) -> Dict[str, Any]: + """Build request body""" + body = { + "model": model, + "messages": messages, + "stream": stream + } + + if "temperature" in kwargs: + body["temperature"] = kwargs["temperature"] + + if "max_tokens" in kwargs: + body["max_tokens"] = kwargs["max_tokens"] + + if tools: + body["tools"] = tools + + return body + + def _parse_response(self, data: Dict) -> LLMResponse: + """Parse response""" + content = "" + tool_calls = None + usage = None + + if "choices" in data: + choice = data["choices"][0] + content = choice.get("message", {}).get("content", "") + tool_calls = choice.get("message", {}).get("tool_calls") + + if "usage" in data: + usage = data["usage"] + + return LLMResponse( + content=content, + tool_calls=tool_calls, + usage=usage + ) + + async def client(self) -> httpx.AsyncClient: + """Get HTTP client""" + if self._client is None: + self._client = httpx.AsyncClient(timeout=120.0) + return self._client + + async def sync_call( + self, + model: str, + messages: List[Dict], + tools: Optional[List[Dict]] = None, + **kwargs + ) -> LLMResponse: + """Call LLM API (non-streaming)""" + body = self._build_body(model, messages, tools, stream=False, **kwargs) + + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post( + self.api_url, + headers=self._build_headers(), + json=body + ) + response.raise_for_status() + data = response.json() + + return self._parse_response(data) + + async def stream_call( + self, + model: str, + messages: List[Dict], + tools: Optional[List[Dict]] = None, + **kwargs + ) -> AsyncGenerator[Dict[str, Any], None]: + """Stream call LLM API""" + body = self._build_body(model, messages, tools, stream=True, **kwargs) + + async with httpx.AsyncClient(timeout=120.0) as client: + async with client.stream( + "POST", + self.api_url, + headers=self._build_headers(), + json=body + ) as response: + response.raise_for_status() + + async for line in response.aiter_lines(): + if not line.strip(): + continue + + if line.startswith("data: "): + data_str = line[6:] + + if data_str == "[DONE]": + yield {"type": "done"} + continue + + try: + chunk = json.loads(data_str) + except json.JSONDecodeError: + continue + + if "choices" not in chunk: + continue + + delta = chunk.get("choices", [{}])[0].get("delta", {}) + + content_delta = delta.get("content", "") + if content_delta: + yield {"type": "content_delta", "content": content_delta} + + tool_calls = delta.get("tool_calls", []) + if tool_calls: + yield {"type": "tool_call_delta", "tool_call": tool_calls} + + finish_reason = chunk.get("choices", [{}])[0].get("finish_reason") + if finish_reason: + tool_calls_finish = chunk.get("choices", [{}])[0].get("message", {}).get("tool_calls") + yield {"type": "done", "tool_calls": tool_calls_finish} + + +# Global LLM client +llm_client = LLMClient() diff --git a/luxx/tools/__init__.py b/luxx/tools/__init__.py new file mode 100644 index 0000000..67c1e5a --- /dev/null +++ b/luxx/tools/__init__.py @@ -0,0 +1,9 @@ +"""Tool system module""" +from luxx.tools.core import ( + ToolDefinition, + ToolResult, + ToolRegistry, + registry +) +from luxx.tools.factory import tool, tool_function +from luxx.tools.executor import ToolExecutor diff --git a/luxx/tools/builtin/__init__.py b/luxx/tools/builtin/__init__.py new file mode 100644 index 0000000..916333c --- /dev/null +++ b/luxx/tools/builtin/__init__.py @@ -0,0 +1,7 @@ +"""Built-in tools module""" +# Import all built-in tools to register them +from luxx.tools.builtin import crawler +from luxx.tools.builtin import code +from luxx.tools.builtin import data + +__all__ = ["crawler", "code", "data"] diff --git a/alcor/tools/builtin/code.py b/luxx/tools/builtin/code.py similarity index 87% rename from alcor/tools/builtin/code.py rename to luxx/tools/builtin/code.py index a1e2ae0..799e2bf 100644 --- a/alcor/tools/builtin/code.py +++ b/luxx/tools/builtin/code.py @@ -1,10 +1,9 @@ -"""代码执行工具""" +"""Code execution tools""" import json import traceback -import ast from typing import Dict, Any -from alcor.tools.factory import tool +from luxx.tools.factory import tool @tool( @@ -29,10 +28,10 @@ from alcor.tools.factory import tool ) def python_execute(arguments: Dict[str, Any]) -> Dict[str, Any]: """ - 执行Python代码 + Execute Python code - 注意:这是一个简化的执行器,生产环境应使用更安全的隔离环境 - 如:Docker容器、Pyodide等 + Note: This is a simplified executor, production environments should use safer isolated environments + such as: Docker containers, Pyodide, etc. """ code = arguments.get("code", "") timeout = arguments.get("timeout", 30) @@ -40,16 +39,16 @@ def python_execute(arguments: Dict[str, Any]) -> Dict[str, Any]: if not code: return {"success": False, "error": "Code is required"} - # 创建执行环境(允许大多数操作) + # Create execution environment namespace = { "__builtins__": __builtins__ } try: - # 编译并执行代码 + # Compile and execute code compiled = compile(code, "", "exec") - # 捕获输出 + # Capture output import io from contextlib import redirect_stdout @@ -60,7 +59,7 @@ def python_execute(arguments: Dict[str, Any]) -> Dict[str, Any]: result = output.getvalue() - # 尝试提取变量 + # Try to extract variables result_vars = {k: v for k, v in namespace.items() if not k.startswith("_") and k != "__builtins__"} @@ -100,7 +99,7 @@ def python_execute(arguments: Dict[str, Any]) -> Dict[str, Any]: category="code" ) def python_eval(arguments: Dict[str, Any]) -> Dict[str, Any]: - """评估Python表达式""" + """Evaluate Python expression""" expression = arguments.get("expression", "") if not expression: diff --git a/alcor/tools/builtin/crawler.py b/luxx/tools/builtin/crawler.py similarity index 87% rename from alcor/tools/builtin/crawler.py rename to luxx/tools/builtin/crawler.py index da688c1..1a1cca1 100644 --- a/alcor/tools/builtin/crawler.py +++ b/luxx/tools/builtin/crawler.py @@ -1,9 +1,9 @@ -"""网页爬虫工具""" +"""Web crawler tools""" import requests from typing import Dict, Any, List, Optional from bs4 import BeautifulSoup -from alcor.tools.factory import tool +from luxx.tools.factory import tool @tool( @@ -28,10 +28,10 @@ from alcor.tools.factory import tool ) def web_search(arguments: Dict[str, Any]) -> Dict[str, Any]: """ - 执行网络搜索 + Execute web search - 注意:这是一个占位实现,实际使用时需要接入真实的搜索API - 如:Google Custom Search, DuckDuckGo, SerpAPI等 + Note: This is a placeholder implementation, real usage requires integrating with actual search APIs + such as: Google Custom Search, DuckDuckGo, SerpAPI, etc. """ query = arguments.get("query", "") max_results = arguments.get("max_results", 5) @@ -39,8 +39,8 @@ def web_search(arguments: Dict[str, Any]) -> Dict[str, Any]: if not query: return {"success": False, "error": "Query is required"} - # 模拟搜索结果 - # 实际实现应接入真实搜索API + # Simulated search results + # Real implementation should integrate with actual search API return { "success": True, "data": { @@ -78,14 +78,14 @@ def web_search(arguments: Dict[str, Any]) -> Dict[str, Any]: category="crawler" ) def web_fetch(arguments: Dict[str, Any]) -> Dict[str, Any]: - """获取并解析网页内容""" + """Fetch and parse web page content""" url = arguments.get("url", "") extract_text = arguments.get("extract_text", True) if not url: return {"success": False, "error": "URL is required"} - # 简单的URL验证 + # Simple URL validation if not url.startswith(("http://", "https://")): url = "https://" + url @@ -98,11 +98,11 @@ def web_fetch(arguments: Dict[str, Any]) -> Dict[str, Any]: if extract_text: soup = BeautifulSoup(response.text, "html.parser") - # 移除script和style标签 + # Remove script and style tags for tag in soup(["script", "style"]): tag.decompose() text = soup.get_text(separator="\n", strip=True) - # 清理多余空行 + # Clean up extra blank lines lines = [line.strip() for line in text.split("\n") if line.strip()] text = "\n".join(lines) @@ -111,7 +111,7 @@ def web_fetch(arguments: Dict[str, Any]) -> Dict[str, Any]: "data": { "url": url, "title": soup.title.string if soup.title else "", - "content": text[:10000] # 限制内容长度 + "content": text[:10000] # Limit content length } } else: @@ -119,7 +119,7 @@ def web_fetch(arguments: Dict[str, Any]) -> Dict[str, Any]: "success": True, "data": { "url": url, - "html": response.text[:50000] # 限制HTML长度 + "html": response.text[:50000] # Limit HTML length } } except requests.RequestException as e: @@ -147,7 +147,7 @@ def web_fetch(arguments: Dict[str, Any]) -> Dict[str, Any]: category="crawler" ) def extract_links(arguments: Dict[str, Any]) -> Dict[str, Any]: - """提取网页中的所有链接""" + """Extract all links from a web page""" url = arguments.get("url", "") max_links = arguments.get("max_links", 20) @@ -169,7 +169,7 @@ def extract_links(arguments: Dict[str, Any]) -> Dict[str, Any]: for a_tag in soup.find_all("a", href=True)[:max_links]: href = a_tag["href"] - # 处理相对URL + # Handle relative URLs if href.startswith("/"): from urllib.parse import urljoin href = urljoin(url, href) diff --git a/luxx/tools/builtin/data.py b/luxx/tools/builtin/data.py new file mode 100644 index 0000000..4ea99a8 --- /dev/null +++ b/luxx/tools/builtin/data.py @@ -0,0 +1,314 @@ +"""Data processing tools""" +import re +import json +import base64 +from typing import Dict, Any +from urllib.parse import quote, unquote + +from luxx.tools.factory import tool + + +@tool( + name="calculate", + description="Execute mathematical calculations", + parameters={ + "type": "object", + "properties": { + "expression": { + "type": "string", + "description": "Mathematical expression to evaluate" + } + }, + "required": ["expression"] + }, + category="data" +) +def calculate(arguments: Dict[str, Any]) -> Dict[str, Any]: + """Execute mathematical calculation""" + expression = arguments.get("expression", "") + + if not expression: + return {"success": False, "error": "Expression is required"} + + try: + # Safe replacement for math functions + safe_dict = { + "abs": abs, + "round": round, + "min": min, + "max": max, + "pow": pow, + "sqrt": lambda x: x ** 0.5, + "sin": lambda x: __import__('math').sin(x), + "cos": lambda x: __import__('math').cos(x), + "tan": lambda x: __import__('math').tan(x), + "log": lambda x: __import__('math').log(x), + "pi": __import__('math').pi, + "e": __import__('math').e + } + + # Remove dangerous characters, only keep numbers and operators + safe_expr = re.sub(r"[^0-9+\-*/().%sqrtinsclogmaxminpowabsroundte, ]", "", expression) + + result = eval(safe_expr, {"__builtins__": {}, **safe_dict}) + + return { + "success": True, + "data": { + "expression": expression, + "result": result, + "formatted": f"{result}" + } + } + except Exception as e: + return { + "success": False, + "error": f"Calculation error: {str(e)}" + } + + +@tool( + name="text_process", + description="Process and transform text", + parameters={ + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to process" + }, + "operation": { + "type": "string", + "description": "Operation to perform: uppercase, lowercase, title, strip, reverse, word_count, char_count", + "enum": ["uppercase", "lowercase", "title", "strip", "reverse", "word_count", "char_count"] + } + }, + "required": ["text", "operation"] + }, + category="data" +) +def text_process(arguments: Dict[str, Any]) -> Dict[str, Any]: + """Text processing""" + text = arguments.get("text", "") + operation = arguments.get("operation", "") + + if not text: + return {"success": False, "error": "Text is required"} + + operations = { + "uppercase": text.upper(), + "lowercase": text.lower(), + "title": text.title(), + "strip": text.strip(), + "reverse": text[::-1], + "word_count": len(text.split()), + "char_count": len(text) + } + + result = operations.get(operation) + + if result is None: + return {"success": False, "error": f"Unknown operation: {operation}"} + + return { + "success": True, + "data": { + "original": text, + "operation": operation, + "result": result + } + } + + +@tool( + name="json_process", + description="Process and transform JSON data", + parameters={ + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "JSON string or text to process" + }, + "operation": { + "type": "string", + "description": "Operation: format, minify, validate", + "enum": ["format", "minify", "validate"] + } + }, + "required": ["data", "operation"] + }, + category="data" +) +def json_process(arguments: Dict[str, Any]) -> Dict[str, Any]: + """JSON data processing""" + data = arguments.get("data", "") + operation = arguments.get("operation", "") + + if not data: + return {"success": False, "error": "Data is required"} + + try: + parsed = json.loads(data) + + if operation == "format": + result = json.dumps(parsed, indent=2, ensure_ascii=False) + elif operation == "minify": + result = json.dumps(parsed, ensure_ascii=False) + elif operation == "validate": + result = "Valid JSON" + else: + return {"success": False, "error": f"Unknown operation: {operation}"} + + return { + "success": True, + "data": { + "result": result, + "operation": operation + } + } + except json.JSONDecodeError as e: + return {"success": False, "error": f"Invalid JSON: {str(e)}"} + + +@tool( + name="hash_text", + description="Generate text hash", + parameters={ + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to hash" + }, + "algorithm": { + "type": "string", + "description": "Hash algorithm: md5, sha1, sha256, sha512", + "default": "sha256" + } + }, + "required": ["text"] + }, + category="data" +) +def hash_text(arguments: Dict[str, Any]) -> Dict[str, Any]: + """Generate text hash""" + import hashlib + + text = arguments.get("text", "") + algorithm = arguments.get("algorithm", "sha256") + + if not text: + return {"success": False, "error": "Text is required"} + + try: + hash_obj = hashlib.new(algorithm) + hash_obj.update(text.encode('utf-8')) + hash_value = hash_obj.hexdigest() + + return { + "success": True, + "data": { + "text": text, + "algorithm": algorithm, + "hash": hash_value + } + } + except Exception as e: + return {"success": False, "error": f"Hash error: {str(e)}"} + + +@tool( + name="url_encode_decode", + description="URL encoding/decoding", + parameters={ + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to encode/decode" + }, + "operation": { + "type": "string", + "description": "Operation: encode, decode", + "enum": ["encode", "decode"] + } + }, + "required": ["text", "operation"] + }, + category="data" +) +def url_encode_decode(arguments: Dict[str, Any]) -> Dict[str, Any]: + """URL encoding/decoding""" + text = arguments.get("text", "") + operation = arguments.get("operation", "") + + if not text: + return {"success": False, "error": "Text is required"} + + try: + if operation == "encode": + result = quote(text) + elif operation == "decode": + result = unquote(text) + else: + return {"success": False, "error": f"Unknown operation: {operation}"} + + return { + "success": True, + "data": { + "original": text, + "operation": operation, + "result": result + } + } + except Exception as e: + return {"success": False, "error": f"URL error: {str(e)}"} + + +@tool( + name="base64_encode_decode", + description="Base64 encoding/decoding", + parameters={ + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to encode/decode" + }, + "operation": { + "type": "string", + "description": "Operation: encode, decode", + "enum": ["encode", "decode"] + } + }, + "required": ["text", "operation"] + }, + category="data" +) +def base64_encode_decode(arguments: Dict[str, Any]) -> Dict[str, Any]: + """Base64 encoding/decoding""" + text = arguments.get("text", "") + operation = arguments.get("operation", "") + + if not text: + return {"success": False, "error": "Text is required"} + + try: + if operation == "encode": + result = base64.b64encode(text.encode()).decode() + elif operation == "decode": + result = base64.b64decode(text.encode()).decode() + else: + return {"success": False, "error": f"Unknown operation: {operation}"} + + return { + "success": True, + "data": { + "original": text, + "operation": operation, + "result": result + } + } + except Exception as e: + return {"success": False, "error": f"Base64 error: {str(e)}"} diff --git a/alcor/tools/core.py b/luxx/tools/core.py similarity index 73% rename from alcor/tools/core.py rename to luxx/tools/core.py index 24a82cf..ef959fa 100644 --- a/alcor/tools/core.py +++ b/luxx/tools/core.py @@ -1,19 +1,19 @@ -"""工具系统核心模块""" +"""Tool system core module""" from dataclasses import dataclass, field -from typing import Dict, Any, Callable, List, Optional, TypeVar, Generic +from typing import Callable, Any, Dict, List, Optional @dataclass class ToolDefinition: - """工具定义""" + """Tool definition""" name: str description: str - parameters: Dict[str, Any] # JSON Schema + parameters: Dict[str, Any] handler: Callable category: str = "general" def to_openai_format(self) -> Dict[str, Any]: - """转换为OpenAI格式""" + """Convert to OpenAI format""" return { "type": "function", "function": { @@ -26,50 +26,51 @@ class ToolDefinition: @dataclass class ToolResult: - """工具执行结果""" + """Tool execution result""" success: bool data: Any = None error: Optional[str] = None def to_dict(self) -> Dict[str, Any]: - """转换为字典""" + """Convert to dictionary""" return {"success": self.success, "data": self.data, "error": self.error} @classmethod def ok(cls, data: Any) -> "ToolResult": - """创建成功结果""" + """Create success result""" return cls(success=True, data=data) @classmethod def fail(cls, error: str) -> "ToolResult": - """创建失败结果""" + """Create failure result""" return cls(success=False, error=error) class ToolRegistry: - """工具注册表(单例模式)""" + """Tool registry (singleton pattern)""" _instance: Optional["ToolRegistry"] = None _tools: Dict[str, ToolDefinition] = {} def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) + cls._instance._tools = {} return cls._instance def register(self, tool: ToolDefinition) -> None: - """注册工具""" + """Register tool""" self._tools[tool.name] = tool def get(self, name: str) -> Optional[ToolDefinition]: - """获取工具定义""" + """Get tool definition""" return self._tools.get(name) def list_all(self) -> List[Dict[str, Any]]: - """列出所有工具""" + """List all tools""" return [t.to_openai_format() for t in self._tools.values()] def list_by_category(self, category: str) -> List[Dict[str, Any]]: - """按分类列出工具""" + """List tools by category""" return [ t.to_openai_format() for t in self._tools.values() @@ -77,35 +78,34 @@ class ToolRegistry: ] def execute(self, name: str, arguments: dict) -> Dict[str, Any]: - """执行工具""" + """Execute tool""" tool = self.get(name) if not tool: - return ToolResult.fail(f"Tool not found: {name}").to_dict() + return {"success": False, "error": f"Tool '{name}' not found"} try: result = tool.handler(arguments) if isinstance(result, ToolResult): return result.to_dict() - return ToolResult.ok(result).to_dict() + return result except Exception as e: - return ToolResult.fail(str(e)).to_dict() + return {"success": False, "error": str(e)} def clear(self) -> None: - """清空所有工具""" + """Clear all tools""" self._tools.clear() def remove(self, name: str) -> bool: - """移除工具""" + """Remove tool""" if name in self._tools: del self._tools[name] return True return False - @property def tool_count(self) -> int: - """工具数量""" + """Tool count""" return len(self._tools) -# 全局注册表实例 +# Global registry instance registry = ToolRegistry() diff --git a/luxx/tools/executor.py b/luxx/tools/executor.py new file mode 100644 index 0000000..0a25b1f --- /dev/null +++ b/luxx/tools/executor.py @@ -0,0 +1,177 @@ +"""Tool executor""" +import json +import time +from typing import List, Dict, Any, Optional + +from luxx.tools.core import registry, ToolResult + + +class ToolExecutor: + """Tool executor with caching and parallel execution support""" + + def __init__( + self, + enable_cache: bool = True, + cache_ttl: int = 300, # 5 minutes + max_workers: int = 4 + ): + self.enable_cache = enable_cache + self.cache_ttl = cache_ttl + self.max_workers = max_workers + self._cache: Dict[str, tuple] = {} # key: (result, timestamp) + self._call_history: List[Dict[str, Any]] = [] + + def _make_cache_key(self, name: str, args: dict) -> str: + """Generate cache key""" + args_str = json.dumps(args, sort_keys=True, ensure_ascii=False) + return f"{name}:{args_str}" + + def _is_cache_valid(self, cache_key: str) -> bool: + """Check if cache is valid""" + if cache_key not in self._cache: + return False + _, timestamp = self._cache[cache_key] + return time.time() - timestamp < self.cache_ttl + + def _get_cached(self, cache_key: str) -> Optional[Dict]: + """Get cached result""" + if self.enable_cache and self._is_cache_valid(cache_key): + return self._cache[cache_key][0] + return None + + def _set_cached(self, cache_key: str, result: Dict) -> None: + """Set cache""" + if self.enable_cache: + self._cache[cache_key] = (result, time.time()) + + def _record_call(self, name: str, args: dict, result: Dict) -> None: + """Record call history""" + self._call_history.append({ + "name": name, + "args": args, + "result": result, + "timestamp": time.time() + }) + + # Limit history size + if len(self._call_history) > 1000: + self._call_history = self._call_history[-1000:] + + def process_tool_calls( + self, + tool_calls: List[Dict[str, Any]], + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """Process tool calls sequentially""" + results = [] + + for call in tool_calls: + call_id = call.get("id", "") + name = call.get("function", {}).get("name", "") + + # Parse JSON arguments + try: + args = json.loads(call.get("function", {}).get("arguments", "{}")) + except json.JSONDecodeError: + args = {} + + # Check cache + cache_key = self._make_cache_key(name, args) + cached = self._get_cached(cache_key) + + if cached is not None: + result = cached + else: + # Execute tool + result = registry.execute(name, args) + self._set_cached(cache_key, result) + + # Record call + self._record_call(name, args, result) + + # Create result message + results.append(self._create_tool_result(call_id, name, result)) + + return results + + def process_tool_calls_parallel( + self, + tool_calls: List[Dict[str, Any]], + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """Process tool calls in parallel""" + if len(tool_calls) <= 1: + return self.process_tool_calls(tool_calls, context) + + try: + from concurrent.futures import ThreadPoolExecutor, as_completed + + futures = {} + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + for call in tool_calls: + call_id = call.get("id", "") + name = call.get("function", {}).get("name", "") + + # Parse all arguments + try: + args = json.loads(call.get("function", {}).get("arguments", "{}")) + except json.JSONDecodeError: + args = {} + + # Check cache + cache_key = self._make_cache_key(name, args) + cached = self._get_cached(cache_key) + + if cached is not None: + futures[call_id] = (name, args, cached) + else: + # Submit task + future = executor.submit(registry.execute, name, args) + futures[future] = (call_id, name, args) + + results = [] + + for future in as_completed(futures.keys()): + if future in futures: + call_id, name, args = futures[future] + result = future.result() + self._set_cached(self._make_cache_key(name, args), result) + self._record_call(name, args, result) + results.append(self._create_tool_result(call_id, name, result)) + else: + call_id, name, args = futures[future] + result = future.result() + self._set_cached(self._make_cache_key(name, args), result) + self._record_call(name, args, result) + results.append(self._create_tool_result(call_id, name, result)) + + return results + except ImportError: + return self.process_tool_calls(tool_calls, context) + + def _create_tool_result(self, call_id: str, name: str, result: Dict) -> Dict[str, Any]: + """Create tool result message""" + return { + "tool_call_id": call_id, + "role": "tool", + "name": name, + "content": json.dumps(result) + } + + def _create_error_result(self, call_id: str, name: str, error: str) -> Dict[str, Any]: + """Create error result message""" + return { + "tool_call_id": call_id, + "role": "tool", + "name": name, + "content": json.dumps({"success": False, "error": error}) + } + + def clear_cache(self) -> None: + """Clear all cache""" + self._cache.clear() + + def get_history(self, limit: int = 100) -> List[Dict[str, Any]]: + """Get call history""" + return self._call_history[-limit:] diff --git a/alcor/tools/factory.py b/luxx/tools/factory.py similarity index 53% rename from alcor/tools/factory.py rename to luxx/tools/factory.py index 1e7d026..15d7347 100644 --- a/alcor/tools/factory.py +++ b/luxx/tools/factory.py @@ -1,6 +1,6 @@ -"""工具装饰器工厂""" +"""Tool decorator factory""" from typing import Callable, Any, Dict -from alcor.tools.core import ToolDefinition, registry +from luxx.tools.core import ToolDefinition, registry def tool( @@ -8,28 +8,28 @@ def tool( description: str, parameters: Dict[str, Any], category: str = "general" -) -> Callable: +): """ - 工具注册装饰器 + Tool registration decorator - 用法示例: + Usage: ```python @tool( - name="web_search", - description="Search the internet for information", + name="my_tool", + description="This is my tool", parameters={ "type": "object", "properties": { - "query": {"type": "string", "description": "Search keywords"}, - "max_results": {"type": "integer", "description": "Max results", "default": 5} + "arg1": {"type": "string"} }, - "required": ["query"] - }, - category="crawler" + "required": ["arg1"] + } ) - def web_search(arguments: dict) -> dict: - # 实现... - return {"results": []} + def my_tool(arguments: dict) -> dict: + # Implementation... + return {"result": "success"} + + # The tool will be automatically registered ``` """ def decorator(func: Callable) -> Callable: @@ -46,12 +46,12 @@ def tool( def tool_function( - name: str, - description: str, - parameters: Dict[str, Any], + name: str = None, + description: str = None, + parameters: Dict[str, Any] = None, category: str = "general" ): """ - 工具装饰器的别名,提供更语义化的命名 + Alias for tool decorator, providing a more semantic naming """ return tool(name=name, description=description, parameters=parameters, category=category) diff --git a/luxx/utils/__init__.py b/luxx/utils/__init__.py new file mode 100644 index 0000000..099e9c9 --- /dev/null +++ b/luxx/utils/__init__.py @@ -0,0 +1,11 @@ +"""Utility functions module""" +from luxx.utils.helpers import ( + generate_id, + hash_password, + verify_password, + create_access_token, + decode_access_token, + success_response, + error_response, + paginate +) diff --git a/alcor/utils/helpers.py b/luxx/utils/helpers.py similarity index 62% rename from alcor/utils/helpers.py rename to luxx/utils/helpers.py index 2f87749..1fa2926 100644 --- a/alcor/utils/helpers.py +++ b/luxx/utils/helpers.py @@ -1,14 +1,14 @@ -"""辅助工具模块""" +"""Utility helpers module""" import shortuuid -import jwt +import hashlib from datetime import datetime, timedelta -from typing import Optional, Dict, Any +from typing import Dict, Any, Optional -from alcor.config import config +from luxx.config import config def generate_id(prefix: str = "") -> str: - """生成唯一ID""" + """Generate unique ID""" unique_id = shortuuid.uuid() if prefix: return f"{prefix}_{unique_id}" @@ -16,54 +16,42 @@ def generate_id(prefix: str = "") -> str: def hash_password(password: str) -> str: - """密码哈希""" + """Hash password""" import bcrypt - salt = bcrypt.gensalt() - return bcrypt.hashpw(password.encode(), salt).decode() + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() def verify_password(password: str, hashed: str) -> bool: - """验证密码""" + """Verify password""" import bcrypt return bcrypt.checkpw(password.encode(), hashed.encode()) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: - """创建JWT访问令牌""" + """Create JWT access token""" + from jose import jwt to_encode = data.copy() - if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(hours=24) - - to_encode.update({"exp": expire, "iat": datetime.utcnow()}) - - encoded_jwt = jwt.encode( - to_encode, - config.secret_key, - algorithm="HS256" - ) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, config.secret_key, algorithm="HS256") return encoded_jwt def decode_access_token(token: str) -> Optional[Dict[str, Any]]: - """解码JWT令牌""" + """Decode JWT token""" try: - payload = jwt.decode( - token, - config.secret_key, - algorithms=["HS256"] - ) + from jose import jwt + payload = jwt.decode(token, config.secret_key, algorithms=["HS256"]) return payload - except jwt.ExpiredSignatureError: - return None - except jwt.InvalidTokenError: + except Exception: return None def success_response(data: Any = None, message: str = "Success") -> Dict[str, Any]: - """成功响应封装""" + """Success response wrapper""" return { "success": True, "message": message, @@ -72,7 +60,7 @@ def success_response(data: Any = None, message: str = "Success") -> Dict[str, An def error_response(message: str, code: int = 400, errors: Any = None) -> Dict[str, Any]: - """错误响应封装""" + """Error response wrapper""" response = { "success": False, "message": message, @@ -84,14 +72,12 @@ def error_response(message: str, code: int = 400, errors: Any = None) -> Dict[st def paginate(query, page: int = 1, page_size: int = 20): - """分页辅助""" + """Pagination helper""" total = query.count() items = query.offset((page - 1) * page_size).limit(page_size).all() - return { - "items": items, "total": total, "page": page, "page_size": page_size, - "total_pages": (total + page_size - 1) // page_size + "items": items } diff --git a/pyproject.toml b/pyproject.toml index ee61255..25106d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "alcor" +name = "luxx" version = "1.0.0" -description = "Alcor - FastAPI + SQLAlchemy" +description = "luxx - FastAPI + SQLAlchemy" readme = "docs/README.md" requires-python = ">=3.10"