Compare commits
14
Commits
d258105be2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea8eff34d | ||
|
|
7e7afe3907 | ||
|
|
4276015cf3 | ||
|
|
59ac17e609 | ||
|
|
83bd9f93a5 | ||
|
|
6282e1434c | ||
|
|
591213b40d | ||
|
|
b1d6563fe3 | ||
|
|
f69a389aed | ||
|
|
e41c8db8cf | ||
|
|
ab073d09b3 | ||
|
|
7b9d1253a5 | ||
|
|
150a416522 | ||
|
|
3f8a83c950 |
@@ -55,7 +55,7 @@ ai_configs = {
|
||||
'key': env.llm_key_huoshan,
|
||||
},
|
||||
'doubao-vision-lite': {
|
||||
'model': 'doubao-1.5-vision-lite-250315',
|
||||
'model': 'doubao-1-5-vision-pro-32k-250115',
|
||||
'url': 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
|
||||
'key': env.llm_key_huoshan,
|
||||
},
|
||||
@@ -76,9 +76,15 @@ ai_configs = {
|
||||
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||||
'key': env.llm_key_bailian,
|
||||
},
|
||||
'bailian-qwen-plus': {
|
||||
'model': 'qwen3.6-plus',
|
||||
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||||
'key': env.llm_key_bailian,
|
||||
},
|
||||
'bailian-embedding': {
|
||||
'model': 'text-embedding-v4',
|
||||
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
|
||||
# 'url': 'https://llm-z0ugjqj2yf8a7i89.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings',
|
||||
"url": "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings",
|
||||
'key': env.llm_key_bailian,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ def add_sqlmodel_route(app: FastAPI):
|
||||
if user_dict.get("id") is None:
|
||||
raise HTTPException(status_code=500, detail="Update row missing id")
|
||||
|
||||
update_user = (await session.exec(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
|
||||
update_user = (await session.execute(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
|
||||
if update_user is None:
|
||||
raise HTTPException(status_code=500, detail="Update row not found")
|
||||
|
||||
@@ -41,7 +41,7 @@ def add_sqlmodel_route(app: FastAPI):
|
||||
if user_dict.get("id") is None:
|
||||
raise HTTPException(status_code=500, detail="Update row missing id")
|
||||
|
||||
delete_user: LlmUser = (await session.exec(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
|
||||
delete_user: LlmUser = (await session.execute(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
|
||||
|
||||
if delete_user is None:
|
||||
raise HTTPException(status_code=500, detail="Delete row not found")
|
||||
|
||||
@@ -33,5 +33,5 @@ class VoiceGenerateParamSchema(BaseModel):
|
||||
model: str = Field(default="sambert-zhichu-v1", description="模型名称")
|
||||
sample_rate: int = Field(default=16000, description="采样率")
|
||||
format: str = Field(default="mp3", description="音频格式")
|
||||
volume: int = Field(default=1, description="音量大小")
|
||||
volume: int = Field(default=100, description="音量大小")
|
||||
rate: int = Field(default=1, description="语速")
|
||||
|
||||
+2
-2
@@ -112,9 +112,9 @@ add_routes(
|
||||
|
||||
add_routes(
|
||||
app=app,
|
||||
runnable=RunnableLambda(lambda x: x['messages']) | create_llm("doubao-vision-lite") | StrOutputParser(),
|
||||
runnable=RunnableLambda(lambda x: x['messages']) | create_llm("bailian-qwen-plus") | StrOutputParser(),
|
||||
input_type=ModelInputSchema,
|
||||
path="/doubao-vision-lite"
|
||||
path="/bailian-qwen-plus"
|
||||
)
|
||||
|
||||
add_routes(
|
||||
|
||||
@@ -23,6 +23,7 @@ class FileModel(BasicModel, table=True):
|
||||
attr1: Optional[str] = Field(default=None, description='扩展属性1')
|
||||
attr2: Optional[str] = Field(default=None, description='扩展属性2')
|
||||
attr3: Optional[str] = Field(default=None, description='扩展属性3')
|
||||
content: Optional[str] = Field(default=None, description='扩展属性内容文本')
|
||||
|
||||
|
||||
FileService = create_model_service(Cls=FileModel)
|
||||
|
||||
@@ -34,7 +34,7 @@ async def tool_book_hotel(config: RunnableConfig) -> str:
|
||||
# print("proj_id", proj_id)
|
||||
# print("user_id", user_id)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
response = await client.post(
|
||||
url=f"http://localhost:{env.server_port}/book_hotel",
|
||||
headers={
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.utils.db_utils import async_session, AsyncSessionDep
|
||||
|
||||
@tool(
|
||||
name_or_callable="tool_project_report",
|
||||
description="查询项目日报周报的工具,在调用这个工具之前要先调用获取时间工具"
|
||||
description="查询项目日报周报的工具,在调用这个工具之前要先调用获取时间工具。这个工具调用结束之后,需要你对工具执行结果做一个分析总结,不能仅仅回复“工具已经执行完毕”"
|
||||
)
|
||||
async def tool_project_report(
|
||||
project_name: Annotated[str, "项目名称"],
|
||||
@@ -49,7 +49,7 @@ async def tool_project_report(
|
||||
"content": doc.content,
|
||||
})
|
||||
|
||||
return json.dumps(user_reports, ensure_ascii=False)
|
||||
return f"报告信息如下:{json.dumps(user_reports, ensure_ascii=False)}"
|
||||
|
||||
|
||||
async def query_users_reports(session: AsyncSessionDep, user_id_list: List[str], start_time: Optional[str], end_time: Optional[str]):
|
||||
|
||||
+22
-13
@@ -1,44 +1,53 @@
|
||||
from datetime import timedelta, datetime, timezone
|
||||
from typing import TypedDict, Literal, TypeAlias
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.config.env import env
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
BCRYPT_MAX_BYTES = 72
|
||||
|
||||
# token的类型,access用于接口认证,refresh用于刷新access token,verify用于激活用户账号
|
||||
AccessTokenType: TypeAlias = Literal["access", "refresh", "verify", "api"]
|
||||
|
||||
|
||||
class TokenInfo(TypedDict):
|
||||
# 用户名信息
|
||||
username: str
|
||||
# token过期时间
|
||||
exp: datetime
|
||||
type: AccessTokenType
|
||||
|
||||
|
||||
def _safe_password_bytes(password: str) -> bytes:
|
||||
data = password.encode("utf-8")
|
||||
if len(data) > BCRYPT_MAX_BYTES:
|
||||
data = data[:BCRYPT_MAX_BYTES]
|
||||
return data
|
||||
|
||||
|
||||
class CryptUtils:
|
||||
@staticmethod
|
||||
def get_password_hash(password: str):
|
||||
return pwd_context.hash(password)
|
||||
return bcrypt.hashpw(_safe_password_bytes(password), bcrypt.gensalt()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
try:
|
||||
return bcrypt.checkpw(
|
||||
_safe_password_bytes(plain_password), hashed_password.encode("utf-8")
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def create_token(
|
||||
username: str,
|
||||
type: AccessTokenType,
|
||||
expires_delta: timedelta
|
||||
):
|
||||
def create_token(username: str, type: AccessTokenType, expires_delta: timedelta):
|
||||
data: TokenInfo = {
|
||||
"username": username,
|
||||
"type": type,
|
||||
"exp": datetime.now(timezone.utc) + expires_delta
|
||||
"exp": datetime.now(timezone.utc) + expires_delta,
|
||||
}
|
||||
return jwt.encode(data, env.jwt_secret_key, env.jwt_algorithm)
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ def create_model_service(
|
||||
if not row_dict.get('id'):
|
||||
raise HTTPException(status_code=400, detail="ID不能为空")
|
||||
# 根据id查询要更新的记录
|
||||
update_cls = (await session.exec(self.select_cls().where(Cls.id == row_dict.get('id')))).first()
|
||||
update_cls = (await session.execute(self.select_cls().where(Cls.id == row_dict.get('id')))).scalars().first()
|
||||
if not update_cls:
|
||||
# 若记录不存在,抛出异常
|
||||
raise HTTPException(status_code=500, detail="Update row not found")
|
||||
@@ -442,7 +442,7 @@ def create_model_service(
|
||||
# 提取所有待更新记录的id
|
||||
update_id_list = [row_dict['id'] for row_dict in row_dict_list]
|
||||
# 根据id查询所有待更新的记录
|
||||
update_cls_list = (await session.exec(self.select_cls().where(Cls.id.in_(update_id_list)))).all()
|
||||
update_cls_list = (await session.execute(self.select_cls().where(Cls.id.in_(update_id_list)))).all()
|
||||
# 若查询到的记录数量与待更新数量不一致,说明部分id不存在
|
||||
if len(update_cls_list) != len(row_dict_list):
|
||||
# 抛出异常并提示不存在的id
|
||||
@@ -480,7 +480,7 @@ def create_model_service(
|
||||
await before_delete(row_dict, session)
|
||||
|
||||
# 根据id查询要删除的记录
|
||||
delete_cls = (await session.exec(self.select_cls().where(Cls.id == row_dict.get('id')))).first()
|
||||
delete_cls = (await session.execute(self.select_cls().where(Cls.id == row_dict.get('id')))).scalars().first()
|
||||
if not delete_cls:
|
||||
# 若记录不存在,返回删除失败
|
||||
return False
|
||||
@@ -509,7 +509,7 @@ def create_model_service(
|
||||
row_id_list = [row_dict.get("id") for row_dict in row_dict_list]
|
||||
|
||||
# 根据id查询所有待删除的记录
|
||||
delete_cls_list = (await session.exec(self.select_cls().where(Cls.id.in_(row_id_list)))).all()
|
||||
delete_cls_list = (await session.execute(self.select_cls().where(Cls.id.in_(row_id_list)))).all()
|
||||
# 若查询到的记录数量与待删除数量不一致,说明部分id不存在
|
||||
if len(delete_cls_list) != len(row_id_list):
|
||||
# 抛出异常并提示不存在的id
|
||||
|
||||
+22
-22
@@ -1,36 +1,36 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Annotated, AsyncContextManager
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi.params import Depends
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from sqlalchemy import AsyncAdaptedQueuePool, text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.config.env import env
|
||||
from sqlmodel import create_engine
|
||||
|
||||
DATABASE_URL = f"mysql+asyncmy://{env.db_username}:{env.db_password}@{env.db_host}:{env.db_port}/{env.db_database}?charset=utf8mb4"
|
||||
|
||||
# 创建异步引擎实例,用于异步操作数据库
|
||||
async_engine = AsyncEngine(create_engine(
|
||||
# 建议直接使用这个函数,不要嵌套 create_engine(AsyncAdaptedQueuePool)
|
||||
async_engine = create_async_engine(
|
||||
DATABASE_URL,
|
||||
poolclass=AsyncAdaptedQueuePool, # 使用异步适配的队列池
|
||||
pool_size=5, # 连接池保持的连接数
|
||||
max_overflow=10, # 允许超过pool_size的最大连接数
|
||||
pool_timeout=30, # 获取连接的超时时间(秒)
|
||||
pool_recycle=180, # 连接回收时间(秒)
|
||||
pool_pre_ping=True, # 启用连接有效性检测
|
||||
echo=True, # 启用SQL语句日志输出,便于开发调试
|
||||
future=True, # 启用SQLAlchemy 2.0风格的未来模式API
|
||||
))
|
||||
# 连接池保持的连接数
|
||||
pool_size=5,
|
||||
# 允许超过pool_size的最大连接数
|
||||
max_overflow=10,
|
||||
# 获取连接的超时时间(秒)
|
||||
pool_timeout=30,
|
||||
# 连接回收时间(秒)
|
||||
pool_recycle=60, # 针对 Docker NAT 环境优化
|
||||
# 启用连接有效性检测
|
||||
pool_pre_ping=True, # 解决 Lost connection 的核心
|
||||
# 启用SQL语句日志输出,便于开发调试
|
||||
echo=True,
|
||||
# 启用SQLAlchemy 2.0风格的未来模式API
|
||||
future=True,
|
||||
)
|
||||
|
||||
# 创建一个会话工厂函数
|
||||
# 使用这个 Session 工厂
|
||||
async_session = sessionmaker(
|
||||
bind=async_engine,
|
||||
async_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ def create_llm(
|
||||
model=_ai_config.get('model'),
|
||||
temperature=temperature,
|
||||
disable_streaming=disable_streaming,
|
||||
extra_body={"enable_thinking": False, "thinking": {"type": "disabled"}}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class MilvusService:
|
||||
|
||||
# 检查Milvus连接是否正常
|
||||
async def check_milvus_connection(self):
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.5)
|
||||
await self.async_search(KnowledgeQueryParam(question="hello", kb_code=""))
|
||||
print("✅ Milvus connection successful:", f"Milvus://{env.milvus_username}:{env.milvus_password}@{env.milvus_uri}/{env.llama_index_database}/{env.llama_index_collection}")
|
||||
|
||||
|
||||
+38
-16
@@ -1,63 +1,81 @@
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Annotated, AsyncContextManager
|
||||
from typing import Annotated, AsyncContextManager, Any, Callable
|
||||
|
||||
import redis.asyncio as redis
|
||||
from fastapi import Depends
|
||||
|
||||
from app.config.env import env
|
||||
|
||||
# 设置日志,方便调试
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RedisUtils():
|
||||
|
||||
class RedisUtils:
|
||||
def __init__(self):
|
||||
self.redis_pool = None
|
||||
self.redis_pool: redis.ConnectionPool | None = None
|
||||
|
||||
# 检查 Redis 连接,先创建连接池管理对象
|
||||
# 1. 优化连接池初始化:加入心跳和保活机制
|
||||
async def check_redis_connection(self):
|
||||
"""初始化连接池并执行读写测试"""
|
||||
self.redis_pool = redis.ConnectionPool(
|
||||
host=env.redis_host,
|
||||
port=env.redis_port,
|
||||
password=env.redis_password,
|
||||
db=env.redis_db,
|
||||
encoding="utf-8",
|
||||
decode_responses=True
|
||||
decode_responses=True,
|
||||
# --- 解决 10054 报错的核心配置 ---
|
||||
health_check_interval=5, # 每5秒自动发送PING,保持连接活跃
|
||||
socket_keepalive=True, # 启用TCP层面的保活探测
|
||||
socket_connect_timeout=3, # 连接超时设置
|
||||
retry_on_timeout=True, # 遇到超时自动重试一次
|
||||
)
|
||||
|
||||
# 立即对redis做一次读写测试
|
||||
async with self.get_redis_connection() as redis_client:
|
||||
try:
|
||||
await redis_client.ping()
|
||||
random_id = str(uuid.uuid4())
|
||||
await redis_client.set("__init__setup__", random_id)
|
||||
await redis_client.set("__init__setup__", random_id, ex=10) # 设置10秒过期
|
||||
cache_random_id = await redis_client.get("__init__setup__")
|
||||
|
||||
if cache_random_id != random_id:
|
||||
raise Exception("Redis 连接失败,读写测试不一致")
|
||||
raise Exception("Redis 读写测试一致性校验失败")
|
||||
|
||||
print("✅ Redis connection successful:", f"redis://{env.redis_password}@${env.redis_host}:{env.redis_port}/{env.redis_db}")
|
||||
except Exception as e:
|
||||
print(f"❌ Database connection failed")
|
||||
logger.error(f"❌ Redis connection failed: {e}")
|
||||
raise e
|
||||
|
||||
# 获取一个Redis连接客户端
|
||||
# 2. 获取连接:利用连接池自动管理,无需手动关闭 client
|
||||
@asynccontextmanager
|
||||
async def get_redis_connection(self) -> AsyncContextManager[redis.Redis]:
|
||||
if not self.redis_pool:
|
||||
raise Exception("Redis 连接池未初始化")
|
||||
if self.redis_pool is None:
|
||||
await self.check_redis_connection()
|
||||
|
||||
# 异步连接池中,Redis 实例创建很轻量
|
||||
# 上下文管理器结束后,它会自动将连接归还给 pool,而不是物理断开
|
||||
redis_client = redis.Redis(connection_pool=self.redis_pool)
|
||||
try:
|
||||
yield redis_client
|
||||
finally:
|
||||
await redis_client.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Redis Connection Error: {e}")
|
||||
raise
|
||||
|
||||
# 清理Redis连接池
|
||||
# 3. 清理连接池
|
||||
async def close_redis_connection(self):
|
||||
if self.redis_pool:
|
||||
await self.redis_pool.disconnect()
|
||||
print("🚀 Redis connection pool closed.")
|
||||
|
||||
|
||||
redis_utils = RedisUtils()
|
||||
|
||||
|
||||
# 作用:用于在接口中注入得到会话实例对象session,在接口执行完毕之后,自动执行close动作关闭会话
|
||||
# --- FastAPI 依赖注入 ---
|
||||
async def get_redis_client() -> redis.Redis:
|
||||
async with redis_utils.get_redis_connection() as redis_client:
|
||||
yield redis_client
|
||||
@@ -66,8 +84,12 @@ async def get_redis_client() -> redis.Redis:
|
||||
RedisClientDep = Annotated[redis.Redis, Depends(get_redis_client)]
|
||||
|
||||
|
||||
# 从redis中获取key的缓存,如果没有值则执行默认值获取函数,并保存到redis中
|
||||
async def get_redis_cache(key: str, default_value_getter):
|
||||
"""
|
||||
从redis中获取key的缓存,如果没有值则执行默认值获取函数,并保存到redis中
|
||||
@param key 缓存的key
|
||||
@param default_value_getter 如果缓存值不存在的情况下则调用异步函数 default_value_getter 来获取默认值,这个函数的返回值必须是一个字典 dict
|
||||
"""
|
||||
async with redis_utils.get_redis_connection() as redis_client:
|
||||
# default_value_getter返回的字段可能嵌套多层对象,这里改成用json字符串缓存
|
||||
json_string = await redis_client.get(key)
|
||||
|
||||
Generated
+3982
-2888
File diff suppressed because it is too large
Load Diff
+4
-7
@@ -20,7 +20,8 @@ psutil = "^7.0.0"
|
||||
asyncmy = "^0.2.10"
|
||||
sqlmodel = "^0.0.24"
|
||||
greenlet = "^3.2.3"
|
||||
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
|
||||
passlib = {extras = ["bcrypt"], version = "^1.7.4", optional = true}
|
||||
bcrypt = "^4.0.0"
|
||||
pyjwt = "^2.10.1"
|
||||
python-multipart = "^0.0.20"
|
||||
langgraph = "^0.6.3"
|
||||
@@ -33,21 +34,17 @@ aiofiles = "^24.1.0"
|
||||
redis = "^6.4.0"
|
||||
websockets = "^15.0.1"
|
||||
dashscope = "^1.25.1"
|
||||
psycopg = {extras = ["binary"], version = "^3.3.3"}
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
langchain-cli = ">=0.0.15"
|
||||
|
||||
# 配置国内镜像源
|
||||
[[tool.poetry.source]]
|
||||
name = "tsinghua"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
priority = "primary" # 最高优先级
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "aliyun"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
priority = "supplemental" # 次级优先级
|
||||
priority = "primary" # 最高优先级
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
Reference in New Issue
Block a user