chore: 修改项目名

This commit is contained in:
ViperEkura 2026-04-12 14:27:31 +08:00
parent 6749213f62
commit 72a3738388
36 changed files with 1289 additions and 1546 deletions

2
.gitignore vendored
View File

@ -9,5 +9,5 @@
!README.md
!.gitignore
!alcor/**/*.py
!luxx/**/*.py
!docs/**/*.md

View File

@ -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()

View File

@ -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)

View File

@ -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"
]

View File

@ -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()

View File

@ -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()

View File

@ -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"
]

View File

@ -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"]

View File

@ -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)}

View File

@ -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:]

View File

@ -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"
]

View File

@ -12,7 +12,7 @@
## 目录结构
```
alcor/
luxx/
├── __init__.py # FastAPI 应用工厂
├── run.py # 入口文件
├── config.py # 配置管理YAML

57
luxx/__init__.py Normal file
View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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"]

View File

@ -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())

View File

@ -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")

View File

@ -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")

63
luxx/routes/tools.py Normal file
View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,3 @@
"""Services module"""
from luxx.services.llm_client import LLMClient, llm_client, LLMResponse
from luxx.services.chat import ChatService, chat_service

194
luxx/services/chat.py Normal file
View File

@ -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()

187
luxx/services/llm_client.py Normal file
View File

@ -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()

9
luxx/tools/__init__.py Normal file
View File

@ -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

View File

@ -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"]

View File

@ -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, "<string>", "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:

View File

@ -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)

314
luxx/tools/builtin/data.py Normal file
View File

@ -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)}"}

View File

@ -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()

177
luxx/tools/executor.py Normal file
View File

@ -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:]

View File

@ -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)

11
luxx/utils/__init__.py Normal file
View File

@ -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
)

View File

@ -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
}

View File

@ -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"