commit 6e733683f85d04734a45ccde78b2bd54ba886636 Author: 孙小云 Date: Thu Dec 4 14:48:38 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc5d3d8 --- /dev/null +++ b/README.md @@ -0,0 +1,464 @@ +# th - 企业级智能体应用平台 + + +🚀 **完全开源的大模型应用平台** +- 集成智能问答、智能问数、知识库、工作流和智能体编排的大模型解决方案。 +- 采用Vue.js + FastAPI + PostgreSQL+Langchain/LangGraph架构。 +- 专为企业级应用设计,代码完全开源,支持私有化部署,可灵活扩展及二次开发。 +- 用户级数据隔离:每个用户的数据仅对其可见,确保数据安全。 + + + +## 🏗️ 技术架构 + + + +### 后端技术栈 +- **Web框架**: FastAPI + SQLAlchemy + Alembic +- **数据库**: PostgreSQL 16+ (开源关系型数据库) +- **向量数据库**: PostgreSQL + pgvector 扩展 (开源向量数据库) +- **智能体编排**: LangGraph 状态图 + 条件路由 +- **工具调用**: Function Calling +- **模型连接协议**: MCP (Model Context Protocol) +- **RAG检索**: LangChain Vector Store +- **对话记忆**: ConversationBufferMemory +- **文档处理**: PyPDF2 + python-docx + markdown +- **数据分析**: Pandas + NumPy + +### 前端技术栈 +- **框架**: Vue 3 + TypeScript + Vite +- **UI组件**: Element Plus (开源UI库) +- **HTTP客户端**: Axios +- **工作流编辑器**: 自研可视化编辑器 +- **工作流引擎**: 基于DAG的流程执行引擎 +- **图形渲染**: Canvas API + SVG +- **拖拽交互**: Vue Draggable +- **节点连接**: 自定义连线算法 + + + +## 本地部署指南 + + +### 环境要求 +- Python 3.10+ +- Node.js 18+ +- PostgreSQL 16+ + +### 1. 安装数据库:PostgreSQL及pgvector插件(向量搜索) + +#### 方式一:Docker安装(推荐) +使用 Docker + Docker Compose 部署 PostgreSQL 16 + pgvector 插件。 + +**1. 创建docker-compose.yml文件** + +内容如下: + +```yaml +version: '3.8' + +services: + db: + image: pgvector/pgvector:pg16 + container_name: pgvector-db + environment: + POSTGRES_USER: myuser + POSTGRES_PASSWORD: your_password + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + pgdata: +``` + +**说明:** +- 使用 `pgvector/pgvector:pg16` 镜像,内置 PostgreSQL 16 + pgvector 插件 +- 数据保存在 Docker 卷 `pgdata` 中,重启不会丢失 +- 监听宿主机端口 5432,可用本地工具如 pgAdmin, DBeaver, psql 连接 +- 默认数据库名称:mydb +- 默认用户名:myuser +- 默认密码:your_password + +**2. 启动服务** + +在 `docker-compose.yml` 所在目录下运行: +```bash +docker-compose up -d +``` + +查看容器状态: +```bash +docker ps +``` + +输出应包含一个名为 `pgvector-db` 的容器,状态为 Up。 + +**3. 验证 pgvector 安装成功** + +进入 PostgreSQL 容器: +```bash +docker exec -it pgvector-db psql -U myuser -d mydb +``` + +启用 pgvector 插件: +```sql +CREATE EXTENSION IF NOT EXISTS vector; +``` + +插入并查询向量数据(示例,可以在客户端,如dbeaver等)** + +```sql +-- 创建表,包含一个向量字段(维度为3) +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + embedding vector(3) +); + +-- 插入向量数据 +INSERT INTO items (embedding) VALUES + ('[1,1,1]'), + ('[2,2,2]'), + ('[1,0,0]'); + +-- 查询与 [1,1,1] 最接近的向量(基于欧几里得距离) +SELECT id, embedding +FROM items +ORDER BY embedding <-> '[1,1,1]' +LIMIT 3; +``` +-- 上述没报错且有结果返回,即安装成功 + +### 2. 后端部署 +```bash +# 克隆项目 +git clone https://github.com/lkpAgent/chat-agent.git +cd chat-agent/backend + +#创建python虚拟环境,推荐使用conda创建虚拟环境 +conda create -n chat-agent python=3.10 +conda activate chat-agent + +# 安装依赖 +pip install -r requirements.txt + +# 配置环境变量,windows下直接复制.env.example文件为.env +cp .env.example .env + +# 编辑.env文件,配置数据库连接和AI API密钥。相关配置信息见后面的配置说明 + +# 配置完数据库信息后,初始化数据库表及创建登录账号(用户名: test@example.com, 密码: 123456) +cd backend/tests +python init_db.py + +# 启动后端服务,默认8000端口 +python -m uvicorn th_agenter.main:app --reload --host 0.0.0.0 --port 8000 +# 或者直接运行main.py +# cd backend/th_agenter +# python main.py + +``` + + +### 3. 前端部署 +```bash +# 进入前端目录 +cd ../frontend + +# 安装依赖 +npm install + +# 配置环境变量 +cp .env.example .env +# 编辑.env文件,配置后端API地址 +VITE_API_BASE_URL = http://localhost:8000 + +# 开发环境,启动前端服务,默认端口3000 +npm run dev +# 发布到生产环境,比如部署在{nginx_home}/html/yourdomain,则指定base路径编译 +# npm run build -- --base=yourdomain +``` +启动成功后,访问http://localhost:3000,会进入到登录页面,默认账号密码为test@example.com/123456 + +![登录界面](docs/images/login.png) + +### 4. 访问应用 +- 前端地址: http://localhost:3000 +- 后端API: http://localhost:8000 +- API文档: http://localhost:8000/docs + +### 5. 后端配置说明 + +#### 后端环境变量配置 (backend/.env) +几个核心配置:系统数据库地址DATABASE_URL,向量数据库配置,CHAT大模型提供商:LLM_PROVIDER及相关配置,向量大模型提供商:EMBEDDING_PROVIDER +几个工具API_KEY:tavilySearch,心知天气API + +```env + +# 数据库配置 +# ======================================== +DATABASE_URL=postgresql://your_username:your_password@your_host:your_port/your_db +# 示例: +# DATABASE_URL=postgresql://myuser:mypassword@127.0.0.1:5432/mydb + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=your_host +PGVECTOR_PORT=your_port +PGVECTOR_DATABASE=mydb +PGVECTOR_USER=myuser +PGVECTOR_PASSWORD=your_password + +# 大模型配置 (支持OpenAI协议的第三方服务) 只需要配置一种chat大模型以及embedding大模型 +# ======================================== +# chat大模型配置 +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_MODEL=gpt-4 +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 + + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding + +# 豆包配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# Moonshot配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key +``` + + +## 📖 API文档 + +### 主要API端点 + +#### 认证相关 +- `POST /auth/login` - 用户登录 +- `POST /auth/register` - 用户注册 +- `POST /auth/refresh` - 刷新Token + +#### 对话管理 +- `GET /chat/conversations` - 获取对话列表 +- `POST /chat/conversations` - 创建新对话 +- `POST /chat/conversations/{id}/chat` - 发送消息 + +#### 知识库管理 +- `POST /knowledge/upload` - 上传文档 +- `GET /knowledge/documents` - 获取文档列表 +- `DELETE /knowledge/documents/{id}` - 删除文档 + +#### 智能查询 +- `POST /smart-query/query` - 智能数据查询 +- `POST /smart-query/upload` - 上传Excel文件 +- `GET /smart-query/files` - 获取文件列表 + +### 完整API文档 +启动后端服务后访问: http://localhost:8000/docs + +## 🔧 开发指南 + +### 项目结构 +``` +open-agent/ +├── backend/ # 后端代码 +│ ├── th_agenter/ # 主应用包 +│ │ ├── api/ # API路由 +│ │ ├── core/ # 核心配置 +│ │ ├── db/ # 数据库相关 +│ │ ├── models/ # 数据库模型 +│ │ ├── services/ # 业务逻辑 +│ │ ├── utils/ # 工具函数 +│ │ └── main.py # 应用入口 +│ ├── tests/ # 测试文件 +│ └── requirements.txt # Python依赖 +├── frontend/ # 前端代码 +│ ├── src/ +│ │ ├── components/ # Vue组件 +│ │ ├── views/ # 页面组件 +│ │ │ ├── chat/ # 对话页面 +│ │ │ ├── knowledge/ # 知识库页面 +│ │ │ ├── workflow/ # 工作流页面 +│ │ │ └── agent/ # 智能体页面 +│ │ ├── stores/ # Pinia状态管理 +│ │ ├── api/ # API调用 +│ │ ├── types/ # TypeScript类型 +│ │ └── router/ # 路由配置 +│ └── package.json # Node.js依赖 +├── data/ # 数据目录 +│ ├── uploads/ # 上传文件 +│ └── logs/ # 日志文件 +└── docs/ # 文档目录 +``` + + +## ✨ 核心能力 + + + +### 🤖 智能问答 + + +- **多模型支持**:集成DeepSeek、智谱AI、豆包等国内主流AI服务商 +- **三种对话模式**: + - 自由对话:直接与AI模型交互 + - RAG对话:基于知识库的检索增强生成 + - 智能体对话:多智能体协作处理复杂任务 +- **多轮对话**:支持连续对话,上下文理解和记忆 +- **对话历史**:完整的会话记录和管理 + +## 🌟 技术特色 + +### 基于LangGraph的智能体对话系统 +- **自主规划能力**:智能体能够根据任务需求自主调用工具并规划执行流程 +- **动态工具调用**:根据上下文自动选择最合适的工具并执行 +- **多步任务分解**:复杂任务自动拆解为多个子任务并顺序执行 + +**示例场景**: +当用户询问"推荐长沙和北京哪个适宜旅游"时: +1. 智能体首先调用搜索工具查找相关信息 +2. 未找到合适结果时,自动规划调用天气查询工具 +3. 智能拆分为两次执行:先查询长沙天气,再查询北京天气 +4. 根据气温数据判断北京更适宜旅游 +5. 自动调用搜索工具查找北京景点信息 +6. 最终整合所有信息生成总结推荐 + +第一步:调用搜索引擎搜索哪个城市更适宜旅游 +![智能体问答界面](docs/images/agent1.png) + +第二步:搜索的内容没有找到合适的答案,意识到错了后,改变策划,重新调用天气工具,从天气的角度判断哪个城市更适合当下旅游 。 +并且自动进行任务拆解,对北京、长沙分别调用一次天气工具,获取到两个城市的天气情况。 + +![智能体问答界面](docs/images/agent2.png) + +第三步:根据天气判断北京更适合旅游,再调用搜索引擎工具,搜索北京的特色景点。最后将工具调用结果与问题进行总结,完成本次对话过程。 + +![智能体问答界面](docs/images/agent3.png) + +### 📊 智能问数 + + +- **Excel分析**:上传Excel文件进行智能数据分析 +- **自然语言查询**:用自然语言提问,自动生成Python代码 +- **数据库查询**:连接PostgreSQL等数据库进行智能问答 +- **多表关联**:支持复杂的多表/多文件联合查询 +- **可视化思维链**:大模型思考过程可视化呈现 + +## 🌟 技术特色 + +### 双引擎智能问数系统 + +**基于Excel的智能问数** +- 使用LangChain代码解释器插件,将Excel数据读取到Pandas DataFrame +- 大模型将自然语言问题转换为Pandas语法并执行 +- 支持多表格文件联合查询和复杂数据分析 + +**基于数据库的智能问数** +- 实现PostgreSQL MCP(Model Context Protocol)接口 +- 大模型先提取表元数据,了解表结构和关系 +- 根据用户问题自动生成优化SQL查询语句 +- 支持多表关联查询和复杂数据检索 + +基于Excel报表的智能问数 +![智能问数界面](docs/images/smart_data.png) +基于数据库的智能问数 +![智能问数界面](docs/images/smart_data_db.png) +### 📚 知识库管理 + + + +- **文档处理**:支持PDF、Word、Markdown、TXT等格式 +- **向量存储**:基于PostgreSQL + pgvector的向量数据库 +- **智能检索**:向量相似度搜索和BM25算法关键词检索 +- **文档管理**:上传、删除、分类和标签管理 +- **RAG集成**:与对话系统无缝集成 + +## 🌟 技术特色 +### 高级语义分割知识库处理 +- **智能段落分割**:基于大模型的语义理解分割技术,而非传统的文本相似度判断 +- **精准切分识别**:大模型直接识别最适合的切分位置并输出分割标记字符串 +- **高效处理流程**:仅输出分割位置字符串,再由代码执行实际分割操作 +- **性能优化**:避免了传统方法中大量的向量计算和相似度比较,提升处理速度 +- **质量保证**:大模型的深层语义理解确保分割边界的准确性和合理性 +### 双重召回检索机制 +- **多模态检索**:结合向量相似度匹配(语义搜索)与BM25关键词检索(字面匹配) +- **混合排序策略**:采用加权融合算法,综合语义相关性和关键词匹配度进行结果排序 +- **召回增强**:双重召回机制有效解决了单纯向量检索的"词汇不匹配"问题 +- **精准度提升**:相比单一检索方式,显著提高相关文档的召回率和准确率 + +![知识库管理界面](docs/images/knowledge_base.png) +![知识库管理界面](docs/images/split.png) + + +### 🔧 工作流编排 + +- **可视化设计**:拖拽式工作流设计器 +- **节点类型**:支持AI对话、数据处理、条件判断等节点 +- **流程控制**:条件分支、循环、并行执行 + +![工作流编排界面](docs/images/workflow.png) + +### 🤖 智能体编排 +- **多智能体协作**:不同专业领域的AI智能体协同工作 +- **角色定义**:自定义智能体的专业能力和知识领域 +- **任务分配**:智能分解复杂任务到合适的智能体 +- **结果整合**:汇总多个智能体的输出生成最终答案 +### 在线体验地址,可自己注册账号使用 +http://113.240.110.92:81/ + +#### 💼 商业使用 +- ✅ 可用于商业项目 +- ✅ 可修改源码 +- ✅ 可私有化部署 +- ✅ 可集成到现有系统 +- ✅ 无需支付许可费用 + +## 📄 许可证 + +本项目采用 [MIT License](LICENSE) 许可证,这意味着: +- 可以自由使用、修改、分发 +- 可以用于商业目的 +- 只需保留原始许可证声明 +- 作者不承担任何责任 + +## 🙏 致谢 + + + +**如果这个项目对你有帮助,请给它一个 ⭐️!** \ No newline at end of file diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..4af5cae --- /dev/null +++ b/backend/.env @@ -0,0 +1,90 @@ +# ======================================== +# 大模型配置 (支持OpenAI协议的第三方服务) +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-3.5-turbo + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +# 豆包(字节跳动)配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 + +# 月之暗面配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# Embedding模型配置 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key + +# ======================================== +# 应用配置 +# ======================================== +# 后端应用配置 +APP_NAME=TH-Agenter +APP_VERSION=0.1.0 +DEBUG=true +ENVIRONMENT=development +HOST=0.0.0.0 +PORT=8000 + +# 前端应用配置 +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_MOCK=false +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx + +# ======================================== +# 安全配置 +# ======================================== +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=300 + +# ======================================== +# 数据库配置 +# ======================================== +# 数据库URL配置 +DATABASE_URL=sqlite:///./TH-Agenter.db +# DATABASE_URL=postgresql://iagent:iagent@118.196.30.45:5432/iagent + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=118.196.30.45 +PGVECTOR_PORT=5432 +PGVECTOR_DATABASE=iagent +PGVECTOR_USER=iagent +PGVECTOR_PASSWORD=iagent \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..f213015 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,89 @@ +# ======================================== +# 大模型配置 (支持OpenAI协议的第三方服务) +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-3.5-turbo + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +# 豆包(字节跳动)配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 + +# 月之暗面配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# Embedding模型配置 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key + +# ======================================== +# 应用配置 +# ======================================== +# 后端应用配置 +APP_NAME=TH-Agenter +APP_VERSION=0.1.0 +DEBUG=true +ENVIRONMENT=development +HOST=0.0.0.0 +PORT=8000 + +# 前端应用配置 +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_MOCK=false +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx + +# ======================================== +# 安全配置 +# ======================================== +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=300 + +# ======================================== +# 数据库配置 +# ======================================== + +DATABASE_URL=postgresql://iagent:iagent@118.196.30.45:5432/iagent + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=localhost +PGVECTOR_PORT=5432 +PGVECTOR_DATABASE=mydb +PGVECTOR_USER=myuser +PGVECTOR_PASSWORD=mypassword \ No newline at end of file diff --git a/backend/TH-Agenter.db b/backend/TH-Agenter.db new file mode 100644 index 0000000..15f889c Binary files /dev/null and b/backend/TH-Agenter.db differ diff --git a/backend/configs/settings.yaml b/backend/configs/settings.yaml new file mode 100644 index 0000000..d2afc03 --- /dev/null +++ b/backend/configs/settings.yaml @@ -0,0 +1,49 @@ +# Chat Agent Configuration +app: + name: "TH-Agenter" + version: "0.1.0" + debug: true + environment: "development" + host: "0.0.0.0" + port: 8000 + +# File Configuration +file: + upload_dir: "./data/uploads" + max_size: 10485760 # 10MB + allowed_extensions: [".txt", ".pdf", ".docx", ".md"] + chunk_size: 1000 + chunk_overlap: 200 + semantic_splitter_enabled: true # 启用语义分割器 + +# Storage Configuration +storage: + storage_type: "local" # local or s3 + upload_directory: "./data/uploads" + + # S3 Configuration + s3_bucket_name: "chat-agent-files" + aws_access_key_id: null + aws_secret_access_key: null + aws_region: "us-east-1" + s3_endpoint_url: null + +# Logging Configuration +logging: + level: "INFO" + file: "./data/logs/app.log" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + max_bytes: 10485760 # 10MB + backup_count: 5 + +# CORS Configuration +cors: + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] + +# Chat Configuration +chat: + max_history_length: 10 + system_prompt: "你是一个有用的AI助手,请根据提供的上下文信息回答用户的问题。" + max_response_tokens: 1000 \ No newline at end of file diff --git a/backend/data/logs/app.log b/backend/data/logs/app.log new file mode 100644 index 0000000..f386176 --- /dev/null +++ b/backend/data/logs/app.log @@ -0,0 +1,3668 @@ +2025-12-04 11:02:51,315 - root - INFO - Logging configured successfully +2025-12-04 11:02:51,440 - root - INFO - Logging configured successfully +2025-12-04 11:02:51,630 - root - INFO - Starting up TH-Agenter application... +2025-12-04 11:02:51,660 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 11:02:51,799 - root - INFO - Database tables created +2025-12-04 11:02:51,799 - root - INFO - Database initialized +2025-12-04 13:10:14,338 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:14,340 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:14,341 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:14,342 - root - INFO - Clearing user context +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:14,806 - root - INFO - Clearing user context +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:15,153 - root - INFO - Clearing user context +2025-12-04 13:10:16,868 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:16,868 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:16,869 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:16,869 - root - INFO - Clearing user context +2025-12-04 13:10:17,206 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,207 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,207 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,207 - root - INFO - Clearing user context +2025-12-04 13:10:17,343 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,343 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,344 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,344 - root - INFO - Clearing user context +2025-12-04 13:10:17,501 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,502 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,502 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,502 - root - INFO - Clearing user context +2025-12-04 13:10:18,639 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:18,639 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:18,640 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:18,640 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:18,653 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:19,955 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:19,961 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,489 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,493 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,699 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,871 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,875 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:21,071 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:21,071 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:21,072 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:21,072 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:21,075 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:21,264 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:21,267 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:24,881 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:24,884 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:28,598 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,599 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,599 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,599 - root - INFO - Clearing user context +2025-12-04 13:10:28,625 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,626 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,626 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,627 - root - INFO - Clearing user context +2025-12-04 13:10:28,833 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,834 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,834 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,834 - root - INFO - Clearing user context +2025-12-04 13:10:28,840 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:10:28,841 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,841 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:10:28,841 - root - INFO - Clearing user context +2025-12-04 13:10:28,844 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,845 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,845 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,845 - root - INFO - Clearing user context +2025-12-04 13:10:28,852 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,853 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,853 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,853 - root - INFO - Clearing user context +2025-12-04 13:10:28,857 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,858 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,858 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,859 - root - INFO - Clearing user context +2025-12-04 13:10:28,862 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:10:28,863 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,863 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:10:28,864 - root - INFO - Clearing user context +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,871 - root - INFO - Clearing user context +2025-12-04 13:10:31,010 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:31,010 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:31,011 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:31,011 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:31,013 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:58,228 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:58,228 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:58,229 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:58,229 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:58,232 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:12,492 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:12,496 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,239 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,239 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,240 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,240 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,243 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,449 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,454 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,658 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,661 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:13:12,453 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:13:19,714 - root - INFO - Logging configured successfully +2025-12-04 13:13:19,817 - root - INFO - Logging configured successfully +2025-12-04 13:13:20,007 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:13:20,037 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:13:20,043 - root - INFO - Database tables created +2025-12-04 13:13:20,043 - root - INFO - Database initialized +2025-12-04 13:14:21,552 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:14:42,425 - root - INFO - Logging configured successfully +2025-12-04 13:14:42,531 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:14:42,535 - root - INFO - Database tables created +2025-12-04 13:14:42,790 - th_agenter.th_agenter.services.user - INFO - User created successfully: test +2025-12-04 13:15:01,086 - root - INFO - Logging configured successfully +2025-12-04 13:15:01,183 - root - INFO - Logging configured successfully +2025-12-04 13:15:01,329 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:15:01,347 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:15:01,352 - root - INFO - Database tables created +2025-12-04 13:15:01,352 - root - INFO - Database initialized +2025-12-04 13:19:26,858 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:20:43,291 - root - INFO - Logging configured successfully +2025-12-04 13:20:43,383 - root - INFO - Logging configured successfully +2025-12-04 13:20:43,531 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:20:43,548 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:20:43,552 - root - INFO - Database tables created +2025-12-04 13:20:43,552 - root - INFO - Database initialized +2025-12-04 13:20:54,188 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:20:54,188 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:54,189 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:20:54,189 - root - INFO - Clearing user context +2025-12-04 13:20:54,243 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:54,243 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,243 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:54,244 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:54,247 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:54,247 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764828763} +2025-12-04 13:20:54,248 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:54,250 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:54,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,250 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:54,258 - root - INFO - Clearing user context +2025-12-04 13:20:54,269 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:54,270 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:54,270 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:54,270 - root - INFO - Clearing user context +2025-12-04 13:20:54,273 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:54,273 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,273 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:54,274 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:54,275 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:54,276 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764828763} +2025-12-04 13:20:54,276 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:54,277 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:54,278 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,278 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:54,283 - root - INFO - Clearing user context +2025-12-04 13:20:55,495 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:20:55,734 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:20:55,735 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,735 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:20:55,735 - root - INFO - Clearing user context +2025-12-04 13:20:55,737 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,737 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,738 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,738 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,740 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,740 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,740 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,742 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,742 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,742 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,744 - root - INFO - Clearing user context +2025-12-04 13:20:55,751 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:55,752 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,752 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:55,752 - root - INFO - Clearing user context +2025-12-04 13:20:55,754 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,755 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,755 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,755 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,757 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,757 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,757 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,759 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,759 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,760 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,762 - root - INFO - Clearing user context +2025-12-04 13:20:55,904 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,905 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,905 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,905 - root - INFO - Clearing user context +2025-12-04 13:20:55,908 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,908 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,908 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,909 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,910 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:20:55,911 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,911 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:20:55,911 - root - INFO - Clearing user context +2025-12-04 13:20:55,913 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,914 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,914 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,914 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,930 - root - INFO - Clearing user context +2025-12-04 13:20:55,932 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,932 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,933 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,935 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,935 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,935 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,960 - root - INFO - Clearing user context +2025-12-04 13:20:55,962 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,962 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,963 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,963 - root - INFO - Clearing user context +2025-12-04 13:20:55,966 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,967 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,967 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,967 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,969 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:55,970 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,970 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:55,970 - root - INFO - Clearing user context +2025-12-04 13:20:55,972 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,973 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,973 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,973 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,980 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:20:55,981 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,981 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:20:55,981 - root - INFO - Clearing user context +2025-12-04 13:20:55,983 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,983 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,983 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,984 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,986 - root - INFO - Clearing user context +2025-12-04 13:20:55,988 - root - INFO - Clearing user context +2025-12-04 13:20:55,992 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,993 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,994 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,996 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,996 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,997 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,997 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,997 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,998 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,998 - root - INFO - Clearing user context +2025-12-04 13:20:56,000 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,000 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,000 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,000 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,008 - root - INFO - Clearing user context +2025-12-04 13:20:56,009 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:56,010 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:56,010 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:56,010 - root - INFO - Clearing user context +2025-12-04 13:20:56,012 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,012 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,013 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,013 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,018 - root - INFO - Clearing user context +2025-12-04 13:20:56,024 - root - INFO - Clearing user context +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:56,035 - root - INFO - Clearing user context +2025-12-04 13:20:56,036 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,036 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,037 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,037 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,039 - root - INFO - Clearing user context +2025-12-04 13:21:04,714 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/45 +2025-12-04 13:21:04,715 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/45 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:04,716 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/45 +2025-12-04 13:21:04,716 - root - INFO - Clearing user context +2025-12-04 13:21:04,722 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:04,722 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:04,722 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:04,722 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:04,729 - root - INFO - Clearing user context +2025-12-04 13:21:04,744 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/45/messages +2025-12-04 13:21:04,744 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/45/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:04,745 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/45/messages +2025-12-04 13:21:04,745 - root - INFO - Clearing user context +2025-12-04 13:21:04,747 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:04,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:04,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:04,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:04,756 - root - INFO - Clearing user context +2025-12-04 13:21:05,324 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/44 +2025-12-04 13:21:05,325 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/44 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,325 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/44 +2025-12-04 13:21:05,325 - root - INFO - Clearing user context +2025-12-04 13:21:05,326 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,327 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,327 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,327 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,330 - root - INFO - Clearing user context +2025-12-04 13:21:05,345 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/44/messages +2025-12-04 13:21:05,346 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/44/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,346 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/44/messages +2025-12-04 13:21:05,346 - root - INFO - Clearing user context +2025-12-04 13:21:05,348 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,348 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,349 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,349 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,352 - root - INFO - Clearing user context +2025-12-04 13:21:05,734 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/43 +2025-12-04 13:21:05,734 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/43 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,735 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/43 +2025-12-04 13:21:05,735 - root - INFO - Clearing user context +2025-12-04 13:21:05,737 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,737 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,737 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,737 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,740 - root - INFO - Clearing user context +2025-12-04 13:21:05,753 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/43/messages +2025-12-04 13:21:05,754 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/43/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,754 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/43/messages +2025-12-04 13:21:05,754 - root - INFO - Clearing user context +2025-12-04 13:21:05,755 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,756 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,756 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,756 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,758 - root - INFO - Clearing user context +2025-12-04 13:21:06,085 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/42 +2025-12-04 13:21:06,085 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/42 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,086 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/42 +2025-12-04 13:21:06,086 - root - INFO - Clearing user context +2025-12-04 13:21:06,088 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,089 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,089 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,089 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,092 - root - INFO - Clearing user context +2025-12-04 13:21:06,105 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/42/messages +2025-12-04 13:21:06,106 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/42/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,106 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/42/messages +2025-12-04 13:21:06,106 - root - INFO - Clearing user context +2025-12-04 13:21:06,108 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,108 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,108 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,108 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,111 - root - INFO - Clearing user context +2025-12-04 13:21:06,388 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/40 +2025-12-04 13:21:06,389 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/40 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,389 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/40 +2025-12-04 13:21:06,389 - root - INFO - Clearing user context +2025-12-04 13:21:06,390 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,391 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,391 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,394 - root - INFO - Clearing user context +2025-12-04 13:21:06,406 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/40/messages +2025-12-04 13:21:06,406 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/40/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,407 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/40/messages +2025-12-04 13:21:06,407 - root - INFO - Clearing user context +2025-12-04 13:21:06,408 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,409 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,409 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,409 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,412 - root - INFO - Clearing user context +2025-12-04 13:21:27,175 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:21:27,175 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,176 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:21:27,176 - root - INFO - Clearing user context +2025-12-04 13:21:27,178 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,178 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,179 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,179 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,181 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,182 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,182 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,184 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,184 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,185 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,188 - root - INFO - Clearing user context +2025-12-04 13:21:27,208 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:27,209 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,209 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:27,209 - root - INFO - Clearing user context +2025-12-04 13:21:27,211 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,212 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,215 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,215 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,216 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,218 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,218 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,218 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,224 - root - INFO - Clearing user context +2025-12-04 13:21:27,785 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,786 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,786 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,786 - root - INFO - Clearing user context +2025-12-04 13:21:27,788 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,788 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,789 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,789 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,790 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:21:27,790 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,791 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:21:27,791 - root - INFO - Clearing user context +2025-12-04 13:21:27,794 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,794 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,795 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,795 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,800 - root - INFO - Clearing user context +2025-12-04 13:21:27,801 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,801 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,802 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,803 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,804 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,804 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,813 - root - INFO - Clearing user context +2025-12-04 13:21:27,816 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,817 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,817 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,817 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,819 - root - INFO - Clearing user context +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,821 - root - INFO - Clearing user context +2025-12-04 13:21:27,824 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,824 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,825 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,825 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,834 - root - INFO - Clearing user context +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:21:27,839 - root - INFO - Clearing user context +2025-12-04 13:21:27,841 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,842 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,842 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,842 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,847 - root - INFO - Clearing user context +2025-12-04 13:21:27,849 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,850 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,850 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,850 - root - INFO - Clearing user context +2025-12-04 13:21:27,852 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,853 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,853 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,853 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,856 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,857 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,857 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,858 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,859 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,859 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,859 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,859 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,860 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,860 - root - INFO - Clearing user context +2025-12-04 13:21:27,862 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,862 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,862 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,863 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,869 - root - INFO - Clearing user context +2025-12-04 13:21:27,871 - root - INFO - Clearing user context +2025-12-04 13:21:27,877 - root - INFO - Clearing user context +2025-12-04 13:21:27,883 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,884 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,884 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,884 - root - INFO - Clearing user context +2025-12-04 13:21:27,886 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,886 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,887 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,887 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,890 - root - INFO - Clearing user context +2025-12-04 13:21:29,610 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:29,611 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:29,611 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:29,611 - root - INFO - Clearing user context +2025-12-04 13:21:29,613 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:29,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:29,614 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:29,614 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:29,617 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:29,617 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:29,618 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:29,619 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:29,620 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:29,620 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:29,636 - root - INFO - Clearing user context +2025-12-04 13:21:32,906 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:21:32,907 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:32,907 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:21:32,907 - root - INFO - Clearing user context +2025-12-04 13:21:32,909 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:32,909 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,909 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:32,910 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:32,912 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:32,912 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:32,913 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:32,916 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:32,916 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,916 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:32,919 - root - INFO - Clearing user context +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:32,939 - root - INFO - Clearing user context +2025-12-04 13:21:32,940 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:32,941 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,941 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:32,941 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:32,943 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:32,944 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:32,944 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:32,946 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:32,946 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,946 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:32,950 - root - INFO - Clearing user context +2025-12-04 13:21:33,277 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:33,278 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,278 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:33,278 - root - INFO - Clearing user context +2025-12-04 13:21:33,280 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,280 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,281 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,281 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,283 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:33,283 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,284 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:33,284 - root - INFO - Clearing user context +2025-12-04 13:21:33,285 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,286 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,286 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,286 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,292 - root - INFO - Clearing user context +2025-12-04 13:21:33,293 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:33,294 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:33,295 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:33,297 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:33,297 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,297 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:33,305 - root - INFO - Clearing user context +2025-12-04 13:21:33,307 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:33,307 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,308 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:33,308 - root - INFO - Clearing user context +2025-12-04 13:21:33,311 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,312 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,312 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,312 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,315 - root - INFO - Clearing user context +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:34,848 - root - INFO - Clearing user context +2025-12-04 13:21:34,849 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,850 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,850 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,850 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,852 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,852 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,852 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,853 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,854 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,854 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,861 - root - INFO - Clearing user context +2025-12-04 13:21:34,871 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 13:21:34,871 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,872 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 13:21:34,872 - root - INFO - Clearing user context +2025-12-04 13:21:34,873 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,874 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,874 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,874 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,875 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,876 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,876 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,878 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,879 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,879 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,887 - root - INFO - Clearing user context +2025-12-04 13:21:34,898 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 13:21:34,898 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,899 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:34,899 - root - INFO - Clearing user context +2025-12-04 13:21:34,900 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,901 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,901 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,901 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,903 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,903 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,904 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,905 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,905 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,905 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,910 - root - INFO - Clearing user context +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:37,978 - root - INFO - Clearing user context +2025-12-04 13:21:37,979 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:37,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:37,980 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:37,980 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:37,983 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:37,983 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:37,984 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:37,985 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:37,985 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:37,985 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:38,009 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:38,010 - root - INFO - Clearing user context +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:43,134 - root - INFO - Clearing user context +2025-12-04 13:21:43,135 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:43,136 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:43,136 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:43,136 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:43,138 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:43,139 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:43,139 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:43,140 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:43,141 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:43,141 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:43,152 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:43,153 - root - INFO - Clearing user context +2025-12-04 13:21:45,966 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:45,967 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:45,967 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:45,967 - root - INFO - Clearing user context +2025-12-04 13:21:45,969 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:45,969 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:45,969 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:45,969 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:45,971 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:45,972 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:45,972 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:45,973 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:45,974 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:45,974 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:45,981 - root - INFO - Clearing user context +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:46,975 - root - INFO - Clearing user context +2025-12-04 13:21:46,976 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:46,976 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:46,977 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:46,977 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:46,978 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:46,979 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:46,979 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:46,980 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:46,981 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:46,981 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:46,988 - root - INFO - Clearing user context +2025-12-04 13:21:46,999 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 13:21:47,000 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:47,000 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 13:21:47,000 - root - INFO - Clearing user context +2025-12-04 13:21:47,001 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:47,002 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,002 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:47,002 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:47,004 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:47,004 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:47,004 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:47,006 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:47,006 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,007 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:47,009 - root - INFO - Clearing user context +2025-12-04 13:21:47,026 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 13:21:47,026 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:47,027 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:47,027 - root - INFO - Clearing user context +2025-12-04 13:21:47,029 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:47,030 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,030 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:47,030 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:47,032 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:47,032 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:47,033 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:47,034 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:47,034 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,034 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:47,036 - root - INFO - Clearing user context +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:52,086 - root - INFO - Clearing user context +2025-12-04 13:21:52,088 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:52,088 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:52,088 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:52,089 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:52,091 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:52,091 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:52,091 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:52,092 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:52,093 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:52,093 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:52,101 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:52,102 - root - INFO - Clearing user context +2025-12-04 13:21:54,088 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:54,089 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:54,089 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:54,089 - root - INFO - Clearing user context +2025-12-04 13:21:54,090 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:54,091 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:54,091 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:54,091 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:54,093 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:54,093 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:54,094 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:54,095 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:54,095 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:54,095 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:54,102 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:54,103 - root - INFO - Clearing user context +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:58,302 - root - INFO - Clearing user context +2025-12-04 13:21:58,303 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:58,304 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:58,304 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:58,304 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:58,307 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:58,307 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:58,307 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:58,309 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:58,309 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:58,309 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:58,319 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:58,321 - root - INFO - Clearing user context +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:26:57,110 - root - INFO - Clearing user context +2025-12-04 13:26:57,112 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:26:57,112 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:26:57,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:26:57,113 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:26:57,115 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:26:57,116 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:26:57,116 - root - INFO - Looking for user with username: admin +2025-12-04 13:26:57,118 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:26:57,118 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:26:57,118 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:26:57,126 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:26:57,128 - root - INFO - Clearing user context +2025-12-04 13:27:03,458 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:03,458 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:03,459 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:03,459 - root - INFO - Clearing user context +2025-12-04 13:27:03,460 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:03,461 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:03,461 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:03,461 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:03,463 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:03,463 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:03,463 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:03,465 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:03,465 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:03,465 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:03,474 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:03,475 - root - INFO - Clearing user context +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:13,259 - root - INFO - Clearing user context +2025-12-04 13:27:13,260 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:13,260 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:13,260 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:13,261 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:13,262 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:13,263 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:13,263 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:13,264 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:13,264 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:13,264 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:13,281 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:13,282 - root - INFO - Clearing user context +2025-12-04 13:27:15,107 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:15,107 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:15,108 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:15,108 - root - INFO - Clearing user context +2025-12-04 13:27:15,109 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:15,110 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:15,110 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:15,110 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:15,112 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:15,112 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:15,112 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:15,114 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:15,114 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:15,114 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:15,121 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:15,123 - root - INFO - Clearing user context +2025-12-04 13:27:17,494 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:17,495 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:17,495 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:17,495 - root - INFO - Clearing user context +2025-12-04 13:27:17,497 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:17,497 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:17,497 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:17,497 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:17,499 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:17,500 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:17,500 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:17,501 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:17,501 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:17,501 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:17,508 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:17,509 - root - INFO - Clearing user context +2025-12-04 13:27:20,474 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:20,475 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:20,475 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:20,475 - root - INFO - Clearing user context +2025-12-04 13:27:20,477 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:20,477 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:20,477 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:20,477 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:20,479 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:20,480 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:20,480 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:20,481 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:20,481 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:20,481 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:20,489 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:20,490 - root - INFO - Clearing user context +2025-12-04 14:07:57,592 - root - INFO - Logging configured successfully +2025-12-04 14:07:57,705 - root - INFO - Logging configured successfully +2025-12-04 14:07:57,910 - root - INFO - Starting up TH-Agenter application... +2025-12-04 14:07:57,958 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 14:07:57,972 - root - INFO - Database tables created +2025-12-04 14:07:57,973 - root - INFO - Database initialized +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 14:16:08,130 - root - INFO - Clearing user context +2025-12-04 14:16:08,197 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:08,197 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,198 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:08,198 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:08,201 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:08,202 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:08,202 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:08,204 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:08,204 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,204 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:08,222 - root - INFO - Clearing user context +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 14:16:08,235 - root - INFO - Clearing user context +2025-12-04 14:16:08,238 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:08,238 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,238 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:08,238 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:08,240 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:08,241 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:08,241 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:08,243 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:08,244 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,244 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:08,250 - root - INFO - Clearing user context +2025-12-04 14:16:09,125 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,126 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,126 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,126 - root - INFO - Clearing user context +2025-12-04 14:16:09,127 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,127 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,128 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,128 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,129 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 14:16:09,129 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,130 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 14:16:09,130 - root - INFO - Clearing user context +2025-12-04 14:16:09,132 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,132 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,132 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,133 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,149 - root - INFO - Clearing user context +2025-12-04 14:16:09,150 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:09,151 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:09,151 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:09,153 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:09,154 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,155 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,155 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:09,155 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,156 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,156 - root - INFO - Clearing user context +2025-12-04 14:16:09,161 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,161 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,161 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,161 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,183 - root - INFO - Clearing user context +2025-12-04 14:16:09,190 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,197 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,197 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,197 - root - INFO - Clearing user context +2025-12-04 14:16:09,201 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,201 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,202 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,202 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,203 - root - INFO - Clearing user context +2025-12-04 14:16:09,205 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 14:16:09,206 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,206 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 14:16:09,206 - root - INFO - Clearing user context +2025-12-04 14:16:09,208 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,208 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,208 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,209 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,212 - root - INFO - Clearing user context +2025-12-04 14:16:09,215 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,215 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,216 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,216 - root - INFO - Clearing user context +2025-12-04 14:16:09,218 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,218 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,219 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,219 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,221 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:09,222 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:09,223 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:09,226 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:09,226 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,228 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,228 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:09,228 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,229 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,229 - root - INFO - Clearing user context +2025-12-04 14:16:09,230 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,231 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,231 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,231 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,232 - root - INFO - Clearing user context +2025-12-04 14:16:09,238 - root - INFO - Clearing user context +2025-12-04 14:16:09,242 - root - INFO - Clearing user context +2025-12-04 14:16:09,251 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,251 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,252 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,252 - root - INFO - Clearing user context +2025-12-04 14:16:09,253 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,254 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,254 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,254 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,257 - root - INFO - Clearing user context +2025-12-04 14:22:37,976 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:22:37,977 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:37,977 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:22:37,977 - root - INFO - Clearing user context +2025-12-04 14:22:37,979 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:37,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:37,980 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:37,980 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:37,982 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:37,982 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:37,982 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:37,984 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:37,984 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:37,984 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:38,000 - root - INFO - Clearing user context +2025-12-04 14:22:40,162 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:22:40,163 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,163 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:22:40,163 - root - INFO - Clearing user context +2025-12-04 14:22:40,165 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,166 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,166 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,166 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,168 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,168 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,169 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,171 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,171 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,171 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,178 - root - INFO - Clearing user context +2025-12-04 14:22:40,188 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:22:40,188 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,189 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:22:40,189 - root - INFO - Clearing user context +2025-12-04 14:22:40,190 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,191 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,191 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,191 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,193 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,193 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,193 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,195 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,195 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,195 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,200 - root - INFO - Clearing user context +2025-12-04 14:22:40,210 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/2 +2025-12-04 14:22:40,210 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,211 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:40,211 - root - INFO - Clearing user context +2025-12-04 14:22:40,212 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,213 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,214 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,214 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,215 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,215 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,216 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,216 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,219 - root - INFO - Clearing user context +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:51,717 - root - INFO - Clearing user context +2025-12-04 14:22:51,719 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:51,719 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:51,720 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:51,720 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:51,722 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:51,722 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:51,722 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:51,723 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:51,724 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:51,724 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:51,735 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:51,736 - root - INFO - Clearing user context +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:53,744 - root - INFO - Clearing user context +2025-12-04 14:22:53,746 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:53,746 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:53,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:53,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:53,749 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:53,749 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:53,749 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:53,751 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:53,751 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:53,751 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:53,768 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:53,770 - root - INFO - Clearing user context +2025-12-04 14:22:57,124 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:57,124 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:57,125 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:57,125 - root - INFO - Clearing user context +2025-12-04 14:22:57,126 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:57,126 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:57,127 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:57,127 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:57,128 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:57,129 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:57,129 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:57,130 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:57,130 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:57,131 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:57,137 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:57,138 - root - INFO - Clearing user context +2025-12-04 14:23:06,944 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:06,945 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:06,945 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:06,945 - root - INFO - Clearing user context +2025-12-04 14:23:06,946 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:06,946 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:06,947 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:06,947 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:06,948 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:06,949 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:06,949 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:06,950 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:06,950 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:06,950 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:06,965 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:06,967 - root - INFO - Clearing user context +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:12,409 - root - INFO - Clearing user context +2025-12-04 14:23:12,410 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:12,410 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:12,410 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:12,410 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:12,412 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:12,412 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:12,412 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:12,413 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:12,413 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:12,413 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:12,421 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:12,423 - root - INFO - Clearing user context +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:37,041 - root - INFO - Clearing user context +2025-12-04 14:23:37,042 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:37,042 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:37,042 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:37,043 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:37,044 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:37,044 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:37,045 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:37,046 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:37,046 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:37,046 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:37,061 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:37,063 - root - INFO - Clearing user context +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:40,766 - root - INFO - Clearing user context +2025-12-04 14:23:40,767 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:40,767 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:40,767 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:40,767 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:40,769 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:40,769 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:40,770 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:40,771 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:40,771 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:40,771 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:40,777 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:40,778 - root - INFO - Clearing user context +2025-12-04 14:23:53,043 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:53,043 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:53,044 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:53,044 - root - INFO - Clearing user context +2025-12-04 14:23:53,045 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:53,045 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:53,045 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:53,046 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:53,048 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:53,049 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:53,049 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:53,050 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:53,051 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:53,051 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:53,058 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:53,059 - root - INFO - Clearing user context +2025-12-04 14:24:09,610 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:09,610 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:09,611 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:09,611 - root - INFO - Clearing user context +2025-12-04 14:24:09,612 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:09,613 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:09,613 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:09,613 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:09,615 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:09,615 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:09,616 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:09,617 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:09,618 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:09,618 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:09,635 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:24:09,636 - root - INFO - Clearing user context +2025-12-04 14:24:11,846 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:11,847 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:11,847 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:11,847 - root - INFO - Clearing user context +2025-12-04 14:24:11,848 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:11,849 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:11,849 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:11,849 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:11,851 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:11,851 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:11,851 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:11,852 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:11,852 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:11,852 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:11,868 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:24:11,869 - root - INFO - Clearing user context +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:15,771 - root - INFO - Clearing user context +2025-12-04 14:24:15,773 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:15,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:15,773 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:15,773 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:15,775 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:15,775 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:15,775 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:15,776 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:15,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:15,776 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:15,779 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:24:15,780 - root - INFO - Clearing user context +2025-12-04 14:31:43,954 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:31:43,954 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:31:43,955 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:31:43,955 - root - INFO - Clearing user context +2025-12-04 14:31:43,956 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:31:43,956 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:31:43,956 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:31:43,957 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:31:43,958 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:31:43,958 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:31:43,959 - root - INFO - Looking for user with username: admin +2025-12-04 14:31:43,960 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:31:43,960 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:31:43,960 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:31:43,961 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:31:43,962 - root - INFO - Clearing user context +2025-12-04 14:32:20,663 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:32:20,664 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:20,664 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:32:20,664 - root - INFO - Clearing user context +2025-12-04 14:32:20,666 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:20,666 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:20,666 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:20,666 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:20,669 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:20,669 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:20,669 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:20,671 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:20,671 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:20,671 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:20,672 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:32:20,673 - root - INFO - Clearing user context +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:32:25,181 - root - INFO - Clearing user context +2025-12-04 14:32:25,183 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:25,183 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:25,183 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:25,184 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:25,185 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:25,186 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:25,186 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:25,188 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:25,189 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:25,189 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:25,190 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:32:25,191 - root - INFO - Clearing user context +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:36,767 - root - INFO - Clearing user context +2025-12-04 14:32:36,769 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:36,769 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:36,769 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:36,769 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:36,771 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:36,771 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:36,771 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:36,773 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:36,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:36,773 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:36,781 - root - INFO - Clearing user context +2025-12-04 14:32:37,707 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:37,708 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,708 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:37,708 - root - INFO - Clearing user context +2025-12-04 14:32:37,709 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,710 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,710 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,710 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,711 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,712 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,712 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,713 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,713 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,713 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,718 - root - INFO - Clearing user context +2025-12-04 14:32:37,726 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:37,727 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,727 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:37,727 - root - INFO - Clearing user context +2025-12-04 14:32:37,728 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,728 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,729 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,729 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,730 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,730 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,730 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,731 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,731 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,732 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,734 - root - INFO - Clearing user context +2025-12-04 14:32:37,744 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:32:37,745 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,745 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:37,745 - root - INFO - Clearing user context +2025-12-04 14:32:37,747 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,749 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,750 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,750 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,752 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,752 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,752 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,754 - root - INFO - Clearing user context +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:39,843 - root - INFO - Clearing user context +2025-12-04 14:32:39,845 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:39,845 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:39,845 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:39,846 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:39,847 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:39,847 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:39,848 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:39,849 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:39,849 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:39,849 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:39,865 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:32:39,866 - root - INFO - Clearing user context +2025-12-04 14:32:42,232 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:32:42,233 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:42,233 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:42,233 - root - INFO - Clearing user context +2025-12-04 14:32:42,235 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:42,235 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:42,235 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:42,236 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:42,238 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:42,239 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:42,239 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:42,241 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:42,241 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:42,241 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:42,249 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:32:42,250 - root - INFO - Clearing user context +2025-12-04 14:32:54,152 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:54,152 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:54,153 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:54,153 - root - INFO - Clearing user context +2025-12-04 14:32:54,154 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:54,155 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:54,155 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:54,155 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:54,157 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:54,157 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:54,157 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:54,159 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:54,160 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:54,160 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:54,169 - root - INFO - Clearing user context +2025-12-04 14:32:55,078 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:55,079 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,079 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:55,079 - root - INFO - Clearing user context +2025-12-04 14:32:55,081 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,081 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,081 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,081 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,083 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,083 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,083 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,085 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,085 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,085 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,092 - root - INFO - Clearing user context +2025-12-04 14:32:55,099 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:55,100 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,100 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:55,100 - root - INFO - Clearing user context +2025-12-04 14:32:55,102 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,102 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,102 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,102 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,104 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,104 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,104 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,106 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,106 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,106 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,109 - root - INFO - Clearing user context +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/8 +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/8 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/8 +2025-12-04 14:32:55,120 - root - INFO - Clearing user context +2025-12-04 14:32:55,122 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,122 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,122 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,122 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,124 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,124 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,125 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,127 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,127 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,127 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,129 - root - INFO - Clearing user context +2025-12-04 14:32:56,813 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:56,813 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:56,814 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:56,814 - root - INFO - Clearing user context +2025-12-04 14:32:56,815 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:56,816 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:56,816 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:56,816 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:56,818 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:56,819 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:56,819 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:56,820 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:56,821 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:56,821 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:56,828 - root - INFO - Clearing user context +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:57,970 - root - INFO - Clearing user context +2025-12-04 14:32:57,972 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:57,972 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:57,973 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:57,973 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:57,975 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:57,975 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:57,975 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:57,976 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:57,977 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:57,977 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:57,989 - root - INFO - Clearing user context +2025-12-04 14:32:58,003 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:58,004 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:58,004 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:58,004 - root - INFO - Clearing user context +2025-12-04 14:32:58,006 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:58,007 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,007 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:58,007 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:58,008 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:58,009 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:58,009 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:58,010 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:58,011 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,011 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:58,013 - root - INFO - Clearing user context +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:58,022 - root - INFO - Clearing user context +2025-12-04 14:32:58,024 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:58,024 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,024 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:58,025 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:58,026 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:58,026 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:58,026 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:58,028 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:58,029 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,029 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:58,031 - root - INFO - Clearing user context +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:00,494 - root - INFO - Clearing user context +2025-12-04 14:33:00,495 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:00,495 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:00,495 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:00,495 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:00,497 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:00,497 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:00,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:00,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:00,499 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:00,499 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:00,505 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:00,506 - root - INFO - Clearing user context +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:02,302 - root - INFO - Clearing user context +2025-12-04 14:33:02,304 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:02,304 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,308 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:02,309 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:02,310 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:02,311 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:02,311 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:02,312 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:02,313 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,313 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:02,319 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:02,320 - root - INFO - Clearing user context +2025-12-04 14:33:02,697 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:02,698 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:02,698 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:02,698 - root - INFO - Clearing user context +2025-12-04 14:33:02,699 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:02,700 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,700 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:02,700 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:02,701 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:02,702 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:02,703 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:02,705 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:02,705 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,705 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:02,711 - root - INFO - Clearing user context +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:03,607 - root - INFO - Clearing user context +2025-12-04 14:33:03,608 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,608 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,609 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,609 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,611 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,611 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,612 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,613 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,613 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,614 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,623 - root - INFO - Clearing user context +2025-12-04 14:33:03,630 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:03,630 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,631 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:03,631 - root - INFO - Clearing user context +2025-12-04 14:33:03,633 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,633 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,633 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,634 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,635 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,636 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,636 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,638 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,638 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,639 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,642 - root - INFO - Clearing user context +2025-12-04 14:33:03,651 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:33:03,652 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,652 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:03,652 - root - INFO - Clearing user context +2025-12-04 14:33:03,654 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,654 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,654 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,655 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,656 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,657 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,657 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,658 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,658 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,658 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,661 - root - INFO - Clearing user context +2025-12-04 14:33:05,789 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:05,790 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:05,790 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:05,790 - root - INFO - Clearing user context +2025-12-04 14:33:05,792 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:05,792 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,792 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:05,792 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:05,794 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:05,794 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:05,794 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:05,795 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:05,795 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,795 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:05,801 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:05,802 - root - INFO - Clearing user context +2025-12-04 14:33:05,858 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:05,858 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:05,859 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:05,859 - root - INFO - Clearing user context +2025-12-04 14:33:05,860 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:05,860 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,861 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:05,861 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:05,862 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:05,862 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:05,863 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:05,864 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:05,864 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,864 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:05,870 - root - INFO - Clearing user context +2025-12-04 14:33:06,668 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:06,669 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,669 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:06,669 - root - INFO - Clearing user context +2025-12-04 14:33:06,670 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,671 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,671 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,671 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,673 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,673 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,674 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,675 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,675 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,675 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,682 - root - INFO - Clearing user context +2025-12-04 14:33:06,691 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:06,692 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,692 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:06,692 - root - INFO - Clearing user context +2025-12-04 14:33:06,693 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,693 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,694 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,694 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,695 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,695 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,695 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,696 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,696 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,696 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,699 - root - INFO - Clearing user context +2025-12-04 14:33:06,708 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:33:06,709 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,709 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:06,709 - root - INFO - Clearing user context +2025-12-04 14:33:06,711 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,711 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,712 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,712 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,713 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,713 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,714 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,715 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,715 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,715 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,718 - root - INFO - Clearing user context +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:08,426 - root - INFO - Clearing user context +2025-12-04 14:33:08,428 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:08,428 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:08,428 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:08,428 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:08,430 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:08,430 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:08,430 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:08,431 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:08,432 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:08,432 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:08,438 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:08,439 - root - INFO - Clearing user context +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:11,647 - root - INFO - Clearing user context +2025-12-04 14:33:11,648 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:11,648 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:11,648 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:11,649 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:11,650 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:11,651 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:11,651 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:11,652 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:11,652 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:11,653 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:11,659 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:11,660 - root - INFO - Clearing user context +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:33,113 - root - INFO - Clearing user context +2025-12-04 14:33:33,114 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:33,114 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:33,114 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:33,115 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:33,116 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:33,117 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:33,117 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:33,119 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:33,119 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:33,119 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:33,136 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:33,138 - root - INFO - Clearing user context +2025-12-04 14:33:35,869 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:35,869 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:35,870 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:35,870 - root - INFO - Clearing user context +2025-12-04 14:33:35,871 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:35,871 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:35,872 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:35,872 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:35,873 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:35,874 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:35,874 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:35,875 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:35,875 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:35,876 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:35,882 - root - INFO - Clearing user context +2025-12-04 14:33:37,254 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:37,255 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,255 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:37,255 - root - INFO - Clearing user context +2025-12-04 14:33:37,257 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,257 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,258 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,258 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,259 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,260 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,260 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,262 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,262 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,262 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,268 - root - INFO - Clearing user context +2025-12-04 14:33:37,275 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:37,276 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,276 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:37,276 - root - INFO - Clearing user context +2025-12-04 14:33:37,278 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,278 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,278 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,278 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,279 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,280 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,280 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,281 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,281 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,281 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,283 - root - INFO - Clearing user context +2025-12-04 14:33:37,291 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:33:37,291 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,292 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:33:37,292 - root - INFO - Clearing user context +2025-12-04 14:33:37,294 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,294 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,294 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,294 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,296 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,296 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,296 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,298 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,299 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,299 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,301 - root - INFO - Clearing user context +2025-12-04 14:33:38,312 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:38,313 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:38,313 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:38,313 - root - INFO - Clearing user context +2025-12-04 14:33:38,315 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:38,315 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:38,315 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:38,315 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:38,317 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:38,317 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:38,318 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:38,319 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:38,319 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:38,319 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:38,330 - root - INFO - Clearing user context +2025-12-04 14:33:39,210 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:39,211 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,211 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:39,211 - root - INFO - Clearing user context +2025-12-04 14:33:39,213 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,213 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,213 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,215 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,215 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,216 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,217 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,217 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,217 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,223 - root - INFO - Clearing user context +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:39,231 - root - INFO - Clearing user context +2025-12-04 14:33:39,232 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,232 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,232 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,232 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,233 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,234 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,234 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,235 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,235 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,235 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,239 - root - INFO - Clearing user context +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:39,248 - root - INFO - Clearing user context +2025-12-04 14:33:39,250 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,250 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,251 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,252 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,252 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,252 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,254 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,254 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,254 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,257 - root - INFO - Clearing user context +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:41,227 - root - INFO - Clearing user context +2025-12-04 14:33:41,228 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:41,229 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:41,229 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:41,229 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:41,230 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:41,231 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:41,231 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:41,232 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:41,232 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:41,233 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:41,238 - root - INFO - Clearing user context +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:34:36,973 - root - INFO - Clearing user context +2025-12-04 14:34:36,976 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:36,976 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:36,976 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:36,976 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:36,978 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:36,978 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:36,979 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:36,980 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:36,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:36,981 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:36,988 - root - INFO - Clearing user context +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:34:37,000 - root - INFO - Clearing user context +2025-12-04 14:34:37,002 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:37,002 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,003 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:37,003 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:37,004 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:37,005 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:37,005 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:37,006 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:37,007 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,007 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:37,011 - root - INFO - Clearing user context +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:34:37,020 - root - INFO - Clearing user context +2025-12-04 14:34:37,021 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:37,021 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,021 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:37,022 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:37,023 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:37,024 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:37,024 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:37,026 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:37,026 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,026 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:37,029 - root - INFO - Clearing user context +2025-12-04 14:34:44,248 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:34:44,248 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:44,249 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:34:44,249 - root - INFO - Clearing user context +2025-12-04 14:34:44,250 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:44,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:44,251 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:44,251 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:44,253 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:44,253 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:44,253 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:44,254 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:44,255 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:44,255 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:44,272 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:34:44,273 - root - INFO - Clearing user context +2025-12-04 14:35:00,349 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:00,349 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,350 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:00,350 - root - INFO - Clearing user context +2025-12-04 14:35:00,351 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,352 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,352 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,352 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,354 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,355 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,355 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,356 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,356 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,357 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,362 - root - INFO - Clearing user context +2025-12-04 14:35:00,385 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:00,386 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,386 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:00,386 - root - INFO - Clearing user context +2025-12-04 14:35:00,387 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,387 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,388 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,388 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,389 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,389 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,390 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,391 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,391 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,394 - root - INFO - Clearing user context +2025-12-04 14:35:00,400 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:00,400 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,401 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:00,401 - root - INFO - Clearing user context +2025-12-04 14:35:00,402 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,402 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,402 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,402 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,403 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,404 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,404 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,405 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,405 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,407 - root - INFO - Clearing user context +2025-12-04 14:35:13,567 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:13,568 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:13,568 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:13,568 - root - INFO - Clearing user context +2025-12-04 14:35:13,570 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:13,570 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:13,570 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:13,571 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:13,572 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:13,572 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:13,572 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:13,574 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:13,574 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:13,574 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:13,582 - root - INFO - Clearing user context +2025-12-04 14:35:14,492 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:14,492 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,493 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:14,493 - root - INFO - Clearing user context +2025-12-04 14:35:14,494 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,495 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,495 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,495 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,496 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,497 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,499 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,499 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,506 - root - INFO - Clearing user context +2025-12-04 14:35:14,513 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:14,514 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,514 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:14,514 - root - INFO - Clearing user context +2025-12-04 14:35:14,515 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,516 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,516 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,516 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,517 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,518 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,518 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,519 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,520 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,520 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,523 - root - INFO - Clearing user context +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/8 +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/8 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/8 +2025-12-04 14:35:14,533 - root - INFO - Clearing user context +2025-12-04 14:35:14,535 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,536 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,536 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,536 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,538 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,538 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,538 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,540 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,540 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,540 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,542 - root - INFO - Clearing user context +2025-12-04 14:35:15,825 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:15,825 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:15,826 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:15,826 - root - INFO - Clearing user context +2025-12-04 14:35:15,827 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:15,827 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:15,828 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:15,828 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:15,829 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:15,830 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:15,830 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:15,831 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:15,832 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:15,832 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:15,838 - root - INFO - Clearing user context +2025-12-04 14:35:16,606 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:16,607 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,607 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:16,607 - root - INFO - Clearing user context +2025-12-04 14:35:16,609 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,609 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,609 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,609 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,611 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,611 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,612 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,613 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,614 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,624 - root - INFO - Clearing user context +2025-12-04 14:35:16,632 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:16,633 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,633 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:16,633 - root - INFO - Clearing user context +2025-12-04 14:35:16,634 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,635 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,635 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,637 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,638 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,638 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,640 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,640 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,640 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,645 - root - INFO - Clearing user context +2025-12-04 14:35:16,652 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:16,652 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,653 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:16,653 - root - INFO - Clearing user context +2025-12-04 14:35:16,655 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,655 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,655 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,656 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,657 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,658 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,658 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,660 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,661 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,661 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,664 - root - INFO - Clearing user context +2025-12-04 14:35:17,431 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:17,431 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,432 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:17,432 - root - INFO - Clearing user context +2025-12-04 14:35:17,433 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,433 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,434 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,434 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,435 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,435 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,435 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,437 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,437 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,438 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,445 - root - INFO - Clearing user context +2025-12-04 14:35:17,473 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:17,473 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,474 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:17,474 - root - INFO - Clearing user context +2025-12-04 14:35:17,476 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,476 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,476 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,476 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,477 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,478 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,478 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,479 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,479 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,480 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,482 - root - INFO - Clearing user context +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:17,493 - root - INFO - Clearing user context +2025-12-04 14:35:17,494 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,494 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,494 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,494 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,496 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,496 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,498 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,498 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,500 - root - INFO - Clearing user context +2025-12-04 14:35:18,139 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:18,139 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,140 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:18,140 - root - INFO - Clearing user context +2025-12-04 14:35:18,141 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,142 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,142 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,142 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,144 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,144 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,144 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,145 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,146 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,146 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,152 - root - INFO - Clearing user context +2025-12-04 14:35:18,893 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:18,894 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,895 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:18,896 - root - INFO - Clearing user context +2025-12-04 14:35:18,898 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,899 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,899 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,899 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,901 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,902 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,902 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,903 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,904 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,904 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,914 - root - INFO - Clearing user context +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:18,926 - root - INFO - Clearing user context +2025-12-04 14:35:18,927 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,928 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,928 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,928 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,930 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,931 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,931 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,932 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,933 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,933 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,937 - root - INFO - Clearing user context +2025-12-04 14:35:18,947 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:18,947 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,948 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:18,948 - root - INFO - Clearing user context +2025-12-04 14:35:18,949 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,950 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,950 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,950 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,952 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,952 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,952 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,954 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,955 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,955 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,958 - root - INFO - Clearing user context +2025-12-04 14:35:24,108 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:24,109 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:24,109 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:24,109 - root - INFO - Clearing user context +2025-12-04 14:35:24,111 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:24,111 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:24,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:24,112 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:24,115 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:24,115 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:24,115 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:24,117 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:24,117 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:24,117 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:24,126 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:24,128 - root - INFO - Clearing user context +2025-12-04 14:35:28,778 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:28,779 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:28,779 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:28,779 - root - INFO - Clearing user context +2025-12-04 14:35:28,781 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:28,781 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:28,781 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:28,781 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:28,783 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:28,783 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:28,783 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:28,785 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:28,785 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:28,785 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:28,792 - root - INFO - Clearing user context +2025-12-04 14:35:29,636 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:29,636 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,637 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:29,637 - root - INFO - Clearing user context +2025-12-04 14:35:29,638 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,639 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,639 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,639 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,641 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,641 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,641 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,643 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,643 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,643 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,649 - root - INFO - Clearing user context +2025-12-04 14:35:29,656 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:29,657 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,657 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:29,657 - root - INFO - Clearing user context +2025-12-04 14:35:29,659 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,659 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,660 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,660 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,662 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,663 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,663 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,666 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,669 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,669 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,673 - root - INFO - Clearing user context +2025-12-04 14:35:29,684 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:29,684 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,685 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:29,685 - root - INFO - Clearing user context +2025-12-04 14:35:29,687 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,688 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,688 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,688 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,690 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,691 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,691 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,693 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,693 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,694 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,697 - root - INFO - Clearing user context +2025-12-04 14:35:31,809 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:31,809 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:31,810 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:31,810 - root - INFO - Clearing user context +2025-12-04 14:35:31,811 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:31,812 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:31,812 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:31,812 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:31,814 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:31,814 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:31,814 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:31,815 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:31,815 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:31,816 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:31,823 - root - INFO - Clearing user context +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:32,636 - root - INFO - Clearing user context +2025-12-04 14:35:32,638 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,638 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,638 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,639 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,640 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,640 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,641 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,642 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,642 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,642 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,650 - root - INFO - Clearing user context +2025-12-04 14:35:32,657 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:32,658 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,658 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:32,658 - root - INFO - Clearing user context +2025-12-04 14:35:32,660 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,660 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,661 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,661 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,662 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,663 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,663 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,664 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,665 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,665 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,668 - root - INFO - Clearing user context +2025-12-04 14:35:32,678 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:32,679 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,679 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:32,679 - root - INFO - Clearing user context +2025-12-04 14:35:32,681 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,681 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,682 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,682 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,683 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,684 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,684 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,686 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,686 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,687 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,689 - root - INFO - Clearing user context +2025-12-04 14:35:39,433 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:39,433 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:39,434 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:39,434 - root - INFO - Clearing user context +2025-12-04 14:35:39,435 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:39,435 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:39,435 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:39,435 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:39,437 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:39,437 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:39,437 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:39,439 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:39,439 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:39,439 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:39,456 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:39,457 - root - INFO - Clearing user context +2025-12-04 14:35:50,180 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:50,180 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:50,181 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:50,181 - root - INFO - Clearing user context +2025-12-04 14:35:50,182 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:50,182 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:50,183 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:50,183 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:50,184 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:50,185 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:50,185 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:50,186 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:50,186 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:50,186 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:50,192 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:50,193 - root - INFO - Clearing user context +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:53,211 - root - INFO - Clearing user context +2025-12-04 14:35:53,212 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:53,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:53,212 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:53,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:53,214 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:53,214 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:53,214 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:53,216 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:53,216 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:53,216 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:53,222 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:53,223 - root - INFO - Clearing user context +2025-12-04 14:35:56,611 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:56,611 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:56,612 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:56,612 - root - INFO - Clearing user context +2025-12-04 14:35:56,613 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:56,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:56,614 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:56,614 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:56,616 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:56,616 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:56,617 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:56,618 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:56,618 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:56,619 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:56,635 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:56,636 - root - INFO - Clearing user context +2025-12-04 14:35:58,898 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:58,898 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:58,899 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:58,899 - root - INFO - Clearing user context +2025-12-04 14:35:58,900 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:58,900 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:58,900 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:58,901 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:58,902 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:58,902 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:58,902 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:58,903 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:58,904 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:58,904 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:58,911 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:58,913 - root - INFO - Clearing user context +2025-12-04 14:36:03,187 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:03,188 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:03,188 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:03,188 - root - INFO - Clearing user context +2025-12-04 14:36:03,190 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:03,190 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:03,190 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:03,191 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:03,193 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:03,193 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:03,194 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:03,195 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:03,196 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:03,196 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:03,203 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:03,205 - root - INFO - Clearing user context +2025-12-04 14:36:06,891 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:06,891 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:06,892 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:06,892 - root - INFO - Clearing user context +2025-12-04 14:36:06,893 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:06,893 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:06,894 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:06,894 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:06,895 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:06,896 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:06,896 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:06,897 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:06,898 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:06,898 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:06,914 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:06,915 - root - INFO - Clearing user context +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:13,634 - root - INFO - Clearing user context +2025-12-04 14:36:13,635 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:13,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:13,635 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:13,636 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:13,637 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:13,638 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:13,638 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:13,640 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:13,640 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:13,640 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:13,658 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:13,660 - root - INFO - Clearing user context +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:21,353 - root - INFO - Clearing user context +2025-12-04 14:36:21,354 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:21,354 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:21,354 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:21,354 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:21,356 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:21,356 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:21,356 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:21,357 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:21,358 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:21,358 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:21,373 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:21,375 - root - INFO - Clearing user context +2025-12-04 14:36:26,419 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:26,419 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:26,420 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:26,420 - root - INFO - Clearing user context +2025-12-04 14:36:26,421 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:26,426 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:26,426 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:26,426 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:26,428 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:26,429 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:26,429 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:26,431 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:26,431 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:26,432 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:26,439 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:26,440 - root - INFO - Clearing user context +2025-12-04 14:36:36,411 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:36,412 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:36,412 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:36,412 - root - INFO - Clearing user context +2025-12-04 14:36:36,413 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:36,414 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:36,414 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:36,414 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:36,415 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:36,416 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:36,416 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:36,417 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:36,417 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:36,417 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:36,423 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:36,425 - root - INFO - Clearing user context +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:38,415 - root - INFO - Clearing user context +2025-12-04 14:36:38,416 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:38,416 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:38,417 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:38,417 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:38,420 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:38,420 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:38,420 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:38,422 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:38,422 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:38,422 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:38,428 - root - INFO - Clearing user context +2025-12-04 14:36:39,360 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:39,361 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,361 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:39,361 - root - INFO - Clearing user context +2025-12-04 14:36:39,363 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,363 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,363 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,363 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,365 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,365 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,365 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,366 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,367 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,367 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,376 - root - INFO - Clearing user context +2025-12-04 14:36:39,383 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:36:39,383 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,384 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:36:39,384 - root - INFO - Clearing user context +2025-12-04 14:36:39,385 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,385 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,386 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,386 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,387 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,387 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,388 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,389 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,389 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,389 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,392 - root - INFO - Clearing user context +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:39,399 - root - INFO - Clearing user context +2025-12-04 14:36:39,401 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,401 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,401 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,401 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,402 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,403 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,403 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,405 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,405 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,408 - root - INFO - Clearing user context +2025-12-04 14:36:41,348 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:41,348 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:41,349 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:41,349 - root - INFO - Clearing user context +2025-12-04 14:36:41,350 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:41,350 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,350 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:41,351 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:41,352 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:41,352 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:41,352 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:41,353 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:41,354 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,354 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:41,369 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:41,371 - root - INFO - Clearing user context +2025-12-04 14:36:41,840 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:41,841 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:41,841 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:41,841 - root - INFO - Clearing user context +2025-12-04 14:36:41,843 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:41,843 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,843 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:41,843 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:41,845 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:41,845 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:41,845 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:41,847 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:41,847 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,847 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:41,853 - root - INFO - Clearing user context +2025-12-04 14:36:42,758 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:42,759 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,759 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:42,759 - root - INFO - Clearing user context +2025-12-04 14:36:42,760 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,760 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,761 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,761 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,763 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,763 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,763 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,764 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,765 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,765 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,771 - root - INFO - Clearing user context +2025-12-04 14:36:42,780 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:36:42,781 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,781 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:36:42,781 - root - INFO - Clearing user context +2025-12-04 14:36:42,782 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,783 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,783 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,783 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,784 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,785 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,785 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,786 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,786 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,786 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,788 - root - INFO - Clearing user context +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:42,798 - root - INFO - Clearing user context +2025-12-04 14:36:42,799 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,800 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,800 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,800 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,801 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,802 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,802 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,803 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,803 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,803 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,806 - root - INFO - Clearing user context +2025-12-04 14:36:45,510 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:45,510 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:45,511 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:45,511 - root - INFO - Clearing user context +2025-12-04 14:36:45,513 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:45,513 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:45,514 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:45,514 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:45,515 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:45,516 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:45,516 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:45,517 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:45,517 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:45,518 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:45,525 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:45,526 - root - INFO - Clearing user context +2025-12-04 14:36:48,047 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:48,047 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:48,048 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:48,048 - root - INFO - Clearing user context +2025-12-04 14:36:48,049 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:48,049 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:48,050 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:48,050 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:48,051 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:48,052 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:48,052 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:48,053 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:48,053 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:48,053 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:48,070 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:48,071 - root - INFO - Clearing user context +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:52,404 - root - INFO - Clearing user context +2025-12-04 14:36:52,405 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:52,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:52,405 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:52,405 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:52,407 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:52,407 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:52,407 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:52,408 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:52,408 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:52,409 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:52,421 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:52,422 - root - INFO - Clearing user context +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:55,636 - root - INFO - Clearing user context +2025-12-04 14:36:55,637 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:55,637 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:55,638 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:55,638 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:55,639 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:55,640 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:55,640 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:55,641 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:55,641 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:55,641 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:55,650 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:55,651 - root - INFO - Clearing user context +2025-12-04 14:36:58,967 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:58,968 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:58,968 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:58,968 - root - INFO - Clearing user context +2025-12-04 14:36:58,969 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:58,970 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:58,970 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:58,970 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:58,972 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:58,972 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:58,972 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:58,973 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:58,973 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:58,974 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:58,981 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:58,982 - root - INFO - Clearing user context +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:11,008 - root - INFO - Clearing user context +2025-12-04 14:37:11,009 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:11,009 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:11,009 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:11,009 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:11,011 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:11,011 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:11,011 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:11,013 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:11,013 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:11,013 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:11,020 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:11,021 - root - INFO - Clearing user context +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:17,725 - root - INFO - Clearing user context +2025-12-04 14:37:17,726 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:17,726 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:17,726 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:17,727 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:17,728 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:17,729 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:17,729 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:17,731 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:17,732 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:17,732 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:17,748 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:17,749 - root - INFO - Clearing user context +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:28,103 - root - INFO - Clearing user context +2025-12-04 14:37:28,104 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:28,104 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:28,104 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:28,104 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:28,106 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:28,106 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:28,106 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:28,107 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:28,107 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:28,107 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:28,114 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:28,114 - root - INFO - Clearing user context +2025-12-04 14:37:29,741 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:29,741 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:29,742 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:29,742 - root - INFO - Clearing user context +2025-12-04 14:37:29,743 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:29,743 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:29,743 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:29,744 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:29,745 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:29,746 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:29,746 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:29,747 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:29,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:29,747 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:29,754 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:29,755 - root - INFO - Clearing user context +2025-12-04 14:37:34,915 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:34,916 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:34,916 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:34,916 - root - INFO - Clearing user context +2025-12-04 14:37:34,917 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:34,918 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:34,918 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:34,918 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:34,919 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:34,920 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:34,920 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:34,921 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:34,921 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:34,921 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:34,936 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:34,937 - root - INFO - Clearing user context +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:37,250 - root - INFO - Clearing user context +2025-12-04 14:37:37,252 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:37,252 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:37,252 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:37,253 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:37,254 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:37,254 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:37,255 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:37,256 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:37,256 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:37,256 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:37,263 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:37,264 - root - INFO - Clearing user context +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:42,173 - root - INFO - Clearing user context +2025-12-04 14:37:42,175 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:42,175 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:42,175 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:42,175 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:42,176 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:42,177 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:42,177 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:42,178 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:42,178 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:42,178 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:42,195 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:42,197 - root - INFO - Clearing user context +2025-12-04 14:37:45,619 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:45,619 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:45,620 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:45,620 - root - INFO - Clearing user context +2025-12-04 14:37:45,621 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:45,621 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:45,622 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:45,622 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:45,623 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:45,624 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:45,624 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:45,625 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:45,625 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:45,625 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:45,641 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:45,642 - root - INFO - Clearing user context +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:50,502 - root - INFO - Clearing user context +2025-12-04 14:37:50,503 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:50,503 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:50,503 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:50,503 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:50,505 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:50,505 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:50,505 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:50,507 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:50,507 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:50,507 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:50,515 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:50,516 - root - INFO - Clearing user context +2025-12-04 14:37:57,432 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:37:57,433 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,433 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:37:57,433 - root - INFO - Clearing user context +2025-12-04 14:37:57,434 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,434 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,434 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,435 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,436 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,436 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,436 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,437 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,437 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,437 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,442 - root - INFO - Clearing user context +2025-12-04 14:37:57,451 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:37:57,452 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,452 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:37:57,452 - root - INFO - Clearing user context +2025-12-04 14:37:57,453 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,453 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,453 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,454 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,455 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,455 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,455 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,456 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,456 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,456 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,458 - root - INFO - Clearing user context +2025-12-04 14:37:57,467 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:37:57,467 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,468 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:57,468 - root - INFO - Clearing user context +2025-12-04 14:37:57,469 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,469 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,470 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,470 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,472 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,472 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,472 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,474 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,474 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,474 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,477 - root - INFO - Clearing user context +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:00,992 - root - INFO - Clearing user context +2025-12-04 14:38:00,993 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:00,994 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:00,994 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:00,994 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:00,996 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:00,996 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:00,997 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:00,998 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:00,999 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:00,999 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:01,009 - root - INFO - Clearing user context +2025-12-04 14:38:03,751 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:03,751 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,752 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:03,752 - root - INFO - Clearing user context +2025-12-04 14:38:03,753 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,753 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,754 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,754 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,755 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,756 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,757 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,758 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,759 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,759 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,766 - root - INFO - Clearing user context +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:38:03,775 - root - INFO - Clearing user context +2025-12-04 14:38:03,776 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,777 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,777 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,778 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,779 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,779 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,782 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,782 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,782 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,786 - root - INFO - Clearing user context +2025-12-04 14:38:03,793 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:38:03,794 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,794 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:38:03,794 - root - INFO - Clearing user context +2025-12-04 14:38:03,796 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,796 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,796 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,796 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,798 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,799 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,799 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,801 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,801 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,802 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,804 - root - INFO - Clearing user context +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:05,862 - root - INFO - Clearing user context +2025-12-04 14:38:05,863 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:05,863 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:05,864 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:05,864 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:05,865 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:05,866 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:05,866 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:05,867 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:05,867 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:05,867 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:05,874 - root - INFO - Clearing user context +2025-12-04 14:38:07,092 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:07,093 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,093 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:07,093 - root - INFO - Clearing user context +2025-12-04 14:38:07,096 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,096 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,096 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,096 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,098 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,098 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,099 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,100 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,100 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,100 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,108 - root - INFO - Clearing user context +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:38:07,118 - root - INFO - Clearing user context +2025-12-04 14:38:07,120 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,120 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,120 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,120 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,122 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,122 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,122 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,123 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,124 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,124 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,126 - root - INFO - Clearing user context +2025-12-04 14:38:07,135 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:38:07,136 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,136 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:07,136 - root - INFO - Clearing user context +2025-12-04 14:38:07,138 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,138 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,139 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,139 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,140 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,140 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,140 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,142 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,142 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,142 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,144 - root - INFO - Clearing user context +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:10,315 - root - INFO - Clearing user context +2025-12-04 14:38:10,316 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:10,317 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:10,317 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:10,317 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:10,319 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:10,319 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:10,320 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:10,321 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:10,321 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:10,321 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:10,329 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:10,330 - root - INFO - Clearing user context +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:11,872 - root - INFO - Clearing user context +2025-12-04 14:38:11,874 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:11,874 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:11,874 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:11,874 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:11,876 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:11,876 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:11,877 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:11,878 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:11,878 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:11,878 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:11,885 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:11,886 - root - INFO - Clearing user context +2025-12-04 14:38:18,109 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:18,110 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:18,110 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:18,110 - root - INFO - Clearing user context +2025-12-04 14:38:18,111 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:18,112 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:18,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:18,112 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:18,114 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:18,114 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:18,114 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:18,115 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:18,115 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:18,116 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:18,123 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:18,124 - root - INFO - Clearing user context +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:20,386 - root - INFO - Clearing user context +2025-12-04 14:38:20,388 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:20,388 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:20,388 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:20,388 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:20,390 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:20,390 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:20,390 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:20,391 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:20,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:20,392 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:20,408 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:20,409 - root - INFO - Clearing user context +2025-12-04 14:38:26,002 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:26,003 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:26,003 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:26,003 - root - INFO - Clearing user context +2025-12-04 14:38:26,005 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:26,005 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:26,005 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:26,005 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:26,007 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:26,007 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:26,007 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:26,009 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:26,009 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:26,009 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:26,026 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:26,027 - root - INFO - Clearing user context +2025-12-04 14:39:19,598 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:19,599 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:19,599 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:19,599 - root - INFO - Clearing user context +2025-12-04 14:39:19,600 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:19,601 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:19,601 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:19,601 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:19,603 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:19,604 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:19,604 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:19,605 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:19,605 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:19,606 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:19,617 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:19,619 - root - INFO - Clearing user context +2025-12-04 14:39:24,135 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:24,136 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:24,136 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:24,136 - root - INFO - Clearing user context +2025-12-04 14:39:24,138 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:24,138 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:24,138 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:24,139 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:24,140 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:24,141 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:24,141 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:24,143 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:24,143 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:24,143 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:24,152 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:24,154 - root - INFO - Clearing user context +2025-12-04 14:39:26,208 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:26,208 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:26,209 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:26,209 - root - INFO - Clearing user context +2025-12-04 14:39:26,210 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:26,210 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:26,211 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:26,211 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:26,213 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:26,213 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:26,213 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:26,215 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:26,215 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:26,215 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:26,223 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:26,224 - root - INFO - Clearing user context +2025-12-04 14:39:28,072 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:28,072 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:28,073 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:28,073 - root - INFO - Clearing user context +2025-12-04 14:39:28,074 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:28,075 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:28,075 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:28,075 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:28,077 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:28,078 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:28,078 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:28,079 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:28,080 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:28,080 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:28,088 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:28,089 - root - INFO - Clearing user context +2025-12-04 14:39:31,310 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:31,311 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:31,311 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:31,311 - root - INFO - Clearing user context +2025-12-04 14:39:31,313 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:31,313 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:31,313 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:31,313 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:31,315 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:31,316 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:31,316 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:31,317 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:31,318 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:31,318 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:31,327 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:31,328 - root - INFO - Clearing user context +2025-12-04 14:39:35,917 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:35,917 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:35,918 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:35,918 - root - INFO - Clearing user context +2025-12-04 14:39:35,919 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:35,919 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:35,919 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:35,920 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:35,921 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:35,921 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:35,922 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:35,922 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:35,923 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:35,923 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:35,929 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:35,930 - root - INFO - Clearing user context +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:37,771 - root - INFO - Clearing user context +2025-12-04 14:39:37,773 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:37,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:37,773 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:37,773 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:37,774 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:37,775 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:37,775 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:37,776 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:37,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:37,776 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:37,782 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:37,783 - root - INFO - Clearing user context +2025-12-04 14:39:40,220 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:40,220 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:40,221 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:40,221 - root - INFO - Clearing user context +2025-12-04 14:39:40,222 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:40,222 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:40,223 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:40,223 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:40,224 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:40,225 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:40,225 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:40,226 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:40,226 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:40,226 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:40,233 - root - INFO - Clearing user context +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:41,145 - root - INFO - Clearing user context +2025-12-04 14:39:41,146 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,147 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,147 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,147 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,149 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,149 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,150 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,151 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,151 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,152 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,156 - root - INFO - Clearing user context +2025-12-04 14:39:41,165 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:41,165 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,166 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:41,166 - root - INFO - Clearing user context +2025-12-04 14:39:41,168 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,169 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,169 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,169 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,171 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,171 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,171 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,172 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,173 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,173 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,175 - root - INFO - Clearing user context +2025-12-04 14:39:41,184 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:39:41,184 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,185 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:41,185 - root - INFO - Clearing user context +2025-12-04 14:39:41,187 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,187 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,187 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,187 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,189 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,189 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,189 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,191 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,191 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,192 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,194 - root - INFO - Clearing user context +2025-12-04 14:39:45,242 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:39:45,242 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:45,243 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:45,243 - root - INFO - Clearing user context +2025-12-04 14:39:45,244 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:45,244 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,244 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:45,245 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:45,246 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:45,246 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:45,246 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:45,247 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:45,248 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,248 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:45,262 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:39:45,263 - root - INFO - Clearing user context +2025-12-04 14:39:45,854 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:45,855 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:45,855 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:45,855 - root - INFO - Clearing user context +2025-12-04 14:39:45,856 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:45,857 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,857 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:45,857 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:45,859 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:45,859 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:45,859 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:45,860 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:45,860 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,861 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:45,867 - root - INFO - Clearing user context +2025-12-04 14:39:46,893 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:46,894 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,894 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:46,894 - root - INFO - Clearing user context +2025-12-04 14:39:46,896 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,896 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,896 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,896 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,898 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,899 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,900 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,902 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,903 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,903 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,913 - root - INFO - Clearing user context +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:46,926 - root - INFO - Clearing user context +2025-12-04 14:39:46,928 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,928 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,928 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,928 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,930 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,931 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,931 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,932 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,933 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,933 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,938 - root - INFO - Clearing user context +2025-12-04 14:39:46,948 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:39:46,948 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,949 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:46,949 - root - INFO - Clearing user context +2025-12-04 14:39:46,952 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,952 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,953 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,953 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,955 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,956 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,956 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,958 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,958 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,959 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,961 - root - INFO - Clearing user context +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:48,857 - root - INFO - Clearing user context +2025-12-04 14:39:48,858 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:48,858 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:48,859 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:48,859 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:48,860 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:48,860 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:48,861 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:48,862 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:48,862 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:48,862 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:48,868 - root - INFO - Clearing user context +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:49,685 - root - INFO - Clearing user context +2025-12-04 14:39:49,687 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,687 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,687 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,688 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,689 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,689 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,689 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,690 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,691 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,691 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,699 - root - INFO - Clearing user context +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:49,712 - root - INFO - Clearing user context +2025-12-04 14:39:49,713 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,713 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,714 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,714 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,715 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,716 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,716 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,718 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,719 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,719 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,723 - root - INFO - Clearing user context +2025-12-04 14:39:49,732 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:39:49,732 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,733 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:49,733 - root - INFO - Clearing user context +2025-12-04 14:39:49,735 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,736 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,736 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,736 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,738 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,738 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,739 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,740 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,741 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,741 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,743 - root - INFO - Clearing user context +2025-12-04 14:39:54,932 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:39:54,932 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:54,933 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:54,933 - root - INFO - Clearing user context +2025-12-04 14:39:54,935 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:54,936 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:54,936 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:54,936 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:54,938 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:54,939 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:54,939 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:54,940 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:54,941 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:54,941 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:54,947 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:39:54,948 - root - INFO - Clearing user context +2025-12-04 14:40:03,539 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:03,539 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:03,540 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:03,540 - root - INFO - Clearing user context +2025-12-04 14:40:03,541 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:03,541 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:03,542 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:03,542 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:03,543 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:03,544 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:03,544 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:03,545 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:03,545 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:03,545 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:03,553 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:40:03,554 - root - INFO - Clearing user context +2025-12-04 14:40:11,962 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:11,963 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:11,963 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:11,963 - root - INFO - Clearing user context +2025-12-04 14:40:11,964 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:11,965 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:11,965 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:11,965 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:11,967 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:11,967 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:11,967 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:11,968 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:11,968 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:11,968 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:11,970 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:11,971 - root - INFO - Clearing user context +2025-12-04 14:40:17,596 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:17,597 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:17,597 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:17,597 - root - INFO - Clearing user context +2025-12-04 14:40:17,598 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:17,599 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:17,599 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:17,599 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:17,601 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:17,601 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:17,601 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:17,602 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:17,602 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:17,603 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:17,604 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:17,604 - root - INFO - Clearing user context +2025-12-04 14:40:20,249 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:20,249 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:20,250 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:20,250 - root - INFO - Clearing user context +2025-12-04 14:40:20,251 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:20,251 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:20,251 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:20,252 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:20,253 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:20,253 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:20,254 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:20,255 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:20,255 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:20,255 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:20,256 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:20,256 - root - INFO - Clearing user context +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:23,533 - root - INFO - Clearing user context +2025-12-04 14:40:23,535 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:23,544 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:23,545 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:23,545 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:23,547 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:23,547 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:23,547 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:23,548 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:23,549 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:23,549 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:23,550 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:23,551 - root - INFO - Clearing user context +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:40:24,441 - root - INFO - Clearing user context +2025-12-04 14:40:24,442 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:24,443 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:24,443 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:24,443 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:24,444 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:24,445 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:24,445 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:24,446 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:24,447 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:24,447 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:24,456 - root - INFO - Clearing user context +2025-12-04 14:40:25,392 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:40:25,392 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,393 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:40:25,393 - root - INFO - Clearing user context +2025-12-04 14:40:25,394 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,395 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,395 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,395 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,397 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,397 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,398 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,399 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,399 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,400 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,405 - root - INFO - Clearing user context +2025-12-04 14:40:25,415 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:40:25,416 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,416 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:40:25,416 - root - INFO - Clearing user context +2025-12-04 14:40:25,419 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,419 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,419 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,419 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,421 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,421 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,421 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,423 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,423 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,423 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,425 - root - INFO - Clearing user context +2025-12-04 14:40:25,436 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:40:25,436 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,437 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:25,437 - root - INFO - Clearing user context +2025-12-04 14:40:25,439 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,439 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,440 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,440 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,441 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,441 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,442 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,443 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,443 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,443 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,445 - root - INFO - Clearing user context +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:37,160 - root - INFO - Clearing user context +2025-12-04 14:40:37,162 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:37,162 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:37,162 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:37,163 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:37,164 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:37,164 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:37,165 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:37,166 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:37,166 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:37,166 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:37,167 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:37,167 - root - INFO - Clearing user context +2025-12-04 14:40:40,585 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:40,586 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:40,586 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:40,586 - root - INFO - Clearing user context +2025-12-04 14:40:40,587 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:40,588 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:40,588 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:40,588 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:40,590 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:40,590 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:40,590 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:40,591 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:40,592 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:40,592 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:40,601 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:40,603 - root - INFO - Clearing user context +2025-12-04 14:40:45,309 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:45,309 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:45,310 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:45,310 - root - INFO - Clearing user context +2025-12-04 14:40:45,311 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:45,311 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:45,312 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:45,312 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:45,313 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:45,314 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:45,314 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:45,315 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:45,315 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:45,315 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:45,323 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:45,324 - root - INFO - Clearing user context +2025-12-04 14:40:51,648 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:51,648 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:51,649 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:51,649 - root - INFO - Clearing user context +2025-12-04 14:40:51,650 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:51,650 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:51,650 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:51,651 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:51,652 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:51,653 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:51,653 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:51,654 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:51,654 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:51,654 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:51,669 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:51,670 - root - INFO - Clearing user context +2025-12-04 14:40:57,629 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:57,629 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:57,630 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:57,630 - root - INFO - Clearing user context +2025-12-04 14:40:57,631 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:57,631 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:57,632 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:57,632 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:57,633 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:57,634 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:57,634 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:57,635 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:57,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:57,635 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:57,651 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:57,652 - root - INFO - Clearing user context +2025-12-04 14:41:32,811 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:41:32,811 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:41:32,812 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:41:32,812 - root - INFO - Clearing user context +2025-12-04 14:41:32,813 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:41:32,813 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:32,814 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:41:32,814 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:41:32,815 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:41:32,816 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:41:32,816 - root - INFO - Looking for user with username: admin +2025-12-04 14:41:32,817 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:41:32,817 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:32,818 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:41:32,825 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:41:32,826 - root - INFO - Clearing user context +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:41:35,306 - root - INFO - Clearing user context +2025-12-04 14:41:35,307 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:41:35,307 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:35,308 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:41:35,308 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:41:35,309 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:41:35,310 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:41:35,310 - root - INFO - Looking for user with username: admin +2025-12-04 14:41:35,311 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:41:35,311 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:35,311 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:41:35,326 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:41:35,327 - root - INFO - Clearing user context +2025-12-04 14:42:15,152 - root - INFO - Shutting down TH-Agenter application... diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..61638b7 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,77 @@ +# Web框架和核心依赖 +fastapi>=0.104.1 +uvicorn[standard]>=0.24.0 +pydantic>=2.5.0 +sqlalchemy>=2.0.23 +alembic>=1.13.1 +python-multipart>=0.0.6 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +python-dotenv>=1.0.0 + +# 数据库和向量数据库 +psycopg2-binary>=2.9.7 # PostgreSQL +pgvector>=0.2.4 # PostgreSQL pgvector extension +pymysql>=1.1.2 #mysql + +# Excel和数据分析(智能问数功能) +pandas>=2.1.0 +numpy>=1.24.0 +openpyxl>=3.1.0 # Excel文件读写 +xlrd>=2.0.1 # 旧版Excel文件支持 + +# LangChain AI框架 +langchain>=0.1.0 +langchain-community>=0.0.10 +langchain-experimental>=0.0.50 # pandas代理 +langchain-postgres>=0.0.6 # PGVector支持 +langchain-openai>=0.0.5 # OpenAI集成 +langgraph>=0.0.40 # LangGraph工作流编排 + +# AI模型服务商 +zhipuai>=2.0.0 # 智谱AI +openai>=1.0.0 # OpenAI + +# 文档处理(知识库功能) +pypdf2>=3.0.0 # PDF文件处理 +python-docx>=0.8.11 # Word文档处理 +markdown>=3.5.0 # Markdown文件处理 +chardet>=5.2.0 # 文件编码检测 +pdfplumber>=0.11.7 #pdf内容提取 + +# 工作流编排和智能体 +celery>=5.3.0 # 异步任务队列 +redis>=5.0.0 # Redis缓存和消息队列 +apscheduler>=3.10.0 # 定时任务调度 + +# 文件和网络处理 +aiofiles>=23.2.0 # 异步文件操作 +requests>=2.31.0 +httpx>=0.25.0 +pyyaml>=6.0 # YAML配置文件解析 +boto3>=1.40.30 #云对象存储 + +# 开发和测试工具 +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +black>=23.0.0 +isort>=5.12.0 +flake8>=6.0.0 +mypy>=1.5.0 +pre-commit>=3.3.0 + +# 数据库迁移 +alembic>=1.12.0 + +# 监控和日志 +prometheus-client>=0.17.0 +structlog>=23.1.0 + +# 安全 +cryptography>=41.0.0 +passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 + +# 性能优化 +orjson>=3.9.0 \ No newline at end of file diff --git a/backend/tests/fastapi_test/main.py b/backend/tests/fastapi_test/main.py new file mode 100644 index 0000000..65fd6f9 --- /dev/null +++ b/backend/tests/fastapi_test/main.py @@ -0,0 +1,153 @@ +from contextvars import ContextVar +from fastapi import FastAPI, Request, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing import Optional +import uuid +import uvicorn + +app = FastAPI() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# 创建上下文变量存储当前用户和请求ID +current_user_ctx: ContextVar[dict] = ContextVar("current_user", default=None) +request_id_ctx: ContextVar[str] = ContextVar("request_id", default=None) + + +# 用户模型 +class User(BaseModel): + id: int + username: str + email: Optional[str] = None + + +# 模拟用户服务 +class UserService: + @staticmethod + def get_current_user_id() -> int: + """在service中直接获取当前用户ID""" + user = current_user_ctx.get() + if not user: + raise RuntimeError("No current user available") + return user["id"] + + @staticmethod + def get_current_user() -> dict: + """获取完整的当前用户信息""" + user = current_user_ctx.get() + if not user: + raise RuntimeError("No current user available") + return user + + +# 业务服务示例 +class TaskService: + def create_task(self, task_data: dict): + """创建任务时自动添加当前用户ID""" + current_user_id = UserService.get_current_user_id() + + # 这里模拟数据库操作 + task = { + **task_data, + "created_by": current_user_id, + "created_at": "2023-10-01 12:00:00" + } + + print(f"Task created by user {current_user_id}: {task}") + return task + + def get_user_tasks(self): + """获取当前用户的任务""" + user = current_user_ctx.get() + current_user_id = UserService.get_current_user_id() + + # 模拟根据用户ID查询任务 + return [{"id": 1, "title": "Sample task", "user_id": current_user_id}] + + +# 中间件:设置上下文 +@app.middleware("http") +async def set_context_vars(request: Request, call_next): + # 为每个请求生成唯一ID + request_id = str(uuid.uuid4()) + request_id_token = request_id_ctx.set(request_id) + + # 尝试提取用户信息 + user_token = None + try: + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + token = auth_header.replace("Bearer ", "") + user = await decode_token_and_get_user(token) # 您的认证逻辑 + user_token = current_user_ctx.set(user) + + response = await call_next(request) + return response + finally: + # 清理上下文 + request_id_ctx.reset(request_id_token) + if user_token: + current_user_ctx.reset(user_token) + + +# 模拟认证函数 +async def decode_token_and_get_user(token: str) -> dict: + # 这里应该是您的实际认证逻辑,例如JWT解码或数据库查询 + # 简单模拟:根据token返回用户信息 + if token == "valid_token_123": + return {"id": 123, "username": "john_doe", "email": "john@example.com"} + elif token == "valid_token_456": + return {"id": 456, "username": "jane_doe", "email": "jane@example.com"} + else: + return None + + +# 依赖项:用于路由层认证 +async def get_current_user_route(token: str = Depends(oauth2_scheme)) -> dict: + """路由层的用户认证""" + user = await decode_token_and_get_user(token) + if not user: + raise HTTPException(status_code=401, detail="Invalid credentials") + return user + + +# 路由处理函数 +@app.post("/tasks") +async def create_task( + task_data: dict, + current_user: dict = Depends(get_current_user_route) +): + """创建任务""" + # 不需要显式传递user_id到service! + task_service = TaskService() + task = task_service.create_task(task_data) + return {"task": task, "message": "Task created successfully"} + + +@app.get("/tasks") +async def get_tasks(current_user: dict = Depends(get_current_user_route)): + """获取当前用户的任务""" + task_service = TaskService() + tasks = task_service.get_user_tasks() + return {"tasks": tasks} + + +@app.get("/users/me") +async def read_users_me(current_user: dict = Depends(get_current_user_route)): + """获取当前用户信息""" + return current_user + + +# 测试端点 - 直接在路由中获取上下文用户 +@app.get("/test-context") +async def test_context(): + """测试直接通过上下文获取用户(不通过依赖注入)""" + try: + user = UserService.get_current_user() + return {"message": "Successfully got user from context", "user": user} + except RuntimeError as e: + return {"error": str(e)} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/backend/tests/fastapi_test/test_main.py b/backend/tests/fastapi_test/test_main.py new file mode 100644 index 0000000..e206c62 --- /dev/null +++ b/backend/tests/fastapi_test/test_main.py @@ -0,0 +1,93 @@ +import pytest +from fastapi.testclient import TestClient +from main import app, current_user_ctx, UserService +from contextvars import ContextVar + +client = TestClient(app) + + +def test_read_users_me_with_valid_token(): + """测试有效令牌获取用户信息""" + response = client.get( + "/users/me", + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response.status_code == 200 + assert response.json()["id"] == 123 + assert response.json()["username"] == "john_doe" + + +def test_read_users_me_with_invalid_token(): + """测试无效令牌""" + response = client.get( + "/users/me", + headers={"Authorization": "Bearer invalid_token"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Invalid credentials" + + +def test_create_task_with_user_context(): + """测试创建任务时用户上下文是否正确""" + response = client.post( + "/tasks", + json={"title": "Test task", "description": "Test description"}, + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response.status_code == 200 + # 检查响应中是否包含正确的用户ID + assert response.json()["task"]["created_by"] == 123 + + +def test_get_tasks_with_different_users(): + """测试不同用户获取任务""" + # 用户1 + response1 = client.get( + "/tasks", + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response1.status_code == 200 + # 这里应该只返回用户1的任务 + + # 用户2 + response2 = client.get( + "/tasks", + headers={"Authorization": "Bearer valid_token_456"} + ) + assert response2.status_code == 200 + # 这里应该只返回用户2的任务 + + +def test_context_outside_request(): + """测试在请求上下文外获取用户(应该失败)""" + try: + UserService.get_current_user() + assert False, "Should have raised an exception" + except RuntimeError as e: + assert "No current user available" in str(e) + + +# 手动设置上下文进行测试 +def test_user_service_with_manual_context(): + """测试手动设置上下文后获取用户""" + test_user = {"id": 999, "username": "test_user"} + token = current_user_ctx.set(test_user) + + try: + user_id = UserService.get_current_user_id() + assert user_id == 999 + + user = UserService.get_current_user() + assert user["username"] == "test_user" + finally: + current_user_ctx.reset(token) + + +if __name__ == "__main__": + # pytest.main([__file__, "-v"]) + test_read_users_me_with_valid_token() + test_read_users_me_with_invalid_token() + test_create_task_with_user_context() + test_get_tasks_with_different_users() + test_context_outside_request() + test_user_service_with_manual_context() diff --git a/backend/tests/init_db.py b/backend/tests/init_db.py new file mode 100644 index 0000000..2554f75 --- /dev/null +++ b/backend/tests/init_db.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""init db""" + +import sys +import os + + +def find_project_root(): + """智能查找项目根目录""" + current_dir = os.path.abspath(os.getcwd()) + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # 可能的项目根目录位置 + possible_roots = [ + current_dir, # 当前工作目录 + script_dir, # 脚本所在目录 + os.path.dirname(script_dir), # 脚本父目录 + os.path.dirname(os.path.dirname(script_dir)) # 脚本祖父目录 + ] + + for root in possible_roots: + backend_dir = os.path.join(root, 'backend') + if os.path.exists(backend_dir) and os.path.exists(os.path.join(backend_dir, 'th_agenter')): + return root, backend_dir + + raise FileNotFoundError("无法找到项目根目录和backend目录") + + +# 查找项目根目录和backend目录 +project_root, backend_dir = find_project_root() + +# 添加backend目录到Python路径 +sys.path.insert(0, backend_dir) + +# 保存原始工作目录 +original_cwd = os.getcwd() + +# 设置工作目录为backend,以便找到.env文件 +os.chdir(backend_dir) + +from th_agenter.db.database import get_db, init_db +from th_agenter.services.user import UserService +from th_agenter.utils.schemas import UserCreate +import asyncio + + +async def create_database_tables(): + """Create all database tables using SQLAlchemy models.""" + try: + await init_db() + print('Database tables created successfully using SQLAlchemy models') + return True + except Exception as e: + print(f'Error creating database tables: {e}') + return False + + +async def create_test_user(): + """Create a test user.""" + # First, create all database tables using SQLAlchemy models + if not await create_database_tables(): + print('Failed to create database tables') + return None + + db = next(get_db()) + + try: + user_service = UserService(db) + + # Create test user + user_data = UserCreate( + username='test', + email='test@example.com', + password='123456', + full_name='Test User 1' + ) + + # Check if user already exists + existing_user = user_service.get_user_by_email(user_data.email) + if existing_user: + print(f'User already exists: {existing_user.username} ({existing_user.email})') + return existing_user + + # Create new user + user = user_service.create_user(user_data) + print(f'Created user: {user.username} ({user.email})') + return user + except Exception as e: + print(f'Error creating user: {e}') + return None + finally: + db.close() + + +if __name__ == "__main__": + try: + asyncio.run(create_test_user()) + finally: + # 恢复原始工作目录 + os.chdir(original_cwd) \ No newline at end of file diff --git a/backend/tests/pandas_test.py b/backend/tests/pandas_test.py new file mode 100644 index 0000000..0dbf552 --- /dev/null +++ b/backend/tests/pandas_test.py @@ -0,0 +1,125 @@ +import os +import sys +import asyncio +import pandas as pd +import tempfile +import pickle +from datetime import datetime +from typing import Dict, Any, List + +sys.path.insert(0,os.path.join(os.path.dirname(__file__),'..','','backend')) + + +def execute(df_1,df_2): + # 假设合同日期列是字符串类型,将其转换为日期类型 + if '合同日期' in df_1.columns: + df_1['合同日期'] = pd.to_datetime(df_1['合同日期']) + if '合同日期' in df_2.columns: + df_2['合同日期'] = pd.to_datetime(df_2['合同日期']) + + # 筛选出2024年和2025年的数据 + filtered_df_1 = df_1[ + (df_1['合同日期'].dt.year == 2024) | (df_1['合同日期'].dt.year == 2025)] + filtered_df_2 = df_2[ + (df_2['合同日期'].dt.year == 2024) | (df_2['合同日期'].dt.year == 2025)] + # 合并两个数据框 + combined_df = pd.concat([filtered_df_1[:5], filtered_df_2[:7]], ignore_index=True) + # 在去重前清理空值 + # combined_df_clean = combined_df.dropna(subset=['项目号']) # 确保主键不为空 + + # 填充数值列的空值 + combined_df_filled = combined_df.fillna({ + '总合同额': 0, + '已确认比例': 0, + '分包合同额': 0 + }) + # 找出不同的项目 + unique_projects = combined_df.drop_duplicates(subset=['项目号']) + return unique_projects + + + +def test_load_selected_dataframes(): + + try: + file1_path = '2025年在手合同数据.xlsx.pkl' + file2_path = '2024年在手合同数据.xlsx.pkl' + target_filenames = [file1_path,file2_path] + dataframes = {} + base_dir = os.path.join("D:\workspace-py\chat-agent\\backend","data","uploads","excel_6") + + all_files = os.listdir(base_dir) + for filename in target_filenames: + matching_files = [] + for file in all_files: + if file.endswith(f"_{filename}") or file.endswith(f"_{filename}.pkl"): + matching_files.append(file) + if not matching_files: + print(f"未找到匹配的文件: {filename}") + + # 如果有多个匹配文件,选择最新的 + if len(matching_files) > 1: + matching_files.sort(key=lambda x: os.path.getmtime(os.path.join(base_dir, x)), reverse=True) + print(f"找到多个匹配文件,选择最新的: {matching_files[0]}") + continue + + selected_file = matching_files[0] + file_path = os.path.join(base_dir, selected_file) + + try: + # 优先加载pickle文件 + if selected_file.endswith('.pkl'): + with open(file_path, 'rb') as f: + df = pickle.load(f) + print(f"成功从pickle加载文件: {selected_file}") + else: + # 如果没有pickle文件,尝试加载原始文件 + if selected_file.endswith(('.xlsx', '.xls')): + df = pd.read_excel(file_path) + elif selected_file.endswith('.csv'): + df = pd.read_csv(file_path) + else: + print(f"不支持的文件格式: {selected_file}") + continue + print(f"成功从原始文件加载: {selected_file}") + + # 使用原始文件名作为key + dataframes[filename] = df + print(f"成功加载DataFrame: {filename}, 形状: {df.shape}") + + except Exception as e: + print(f"加载文件失败 {selected_file}: {e}") + continue + + return dataframes + except Exception as e: + print(e) + +if __name__ == '__main__': + dataframes = test_load_selected_dataframes() + df_names = list(dataframes.keys()) + if len(df_names) >= 2: + df_1 = dataframes[df_names[0]] + df_2 = dataframes[df_names[1]] + + print(f"DataFrame 1 ({df_names[0]}) 形状: {df_1.shape}") + print(f"DataFrame 1 列名: {list(df_1.columns)}") + print(f"DataFrame 1 前几行:") + print(df_1.head()) + print() + + print(f"DataFrame 2 ({df_names[1]}) 形状: {df_2.shape}") + print(f"DataFrame 2 列名: {list(df_2.columns)}") + print(f"DataFrame 2 前几行:") + print(df_2.head()) + print() + + # 执行用户提供的数据处理逻辑 + print("执行数据处理逻辑...") + result = execute(df_1, df_2) + + print("处理结果:") + print(f"结果形状: {result.shape}") + print(f"结果列名: {list(result.columns)}") + print("结果数据:") + print(result) diff --git a/backend/th_agenter/__init__.py b/backend/th_agenter/__init__.py new file mode 100644 index 0000000..18c0364 --- /dev/null +++ b/backend/th_agenter/__init__.py @@ -0,0 +1,12 @@ +"""th - A modern chat agent application.""" + +__version__ = "0.1.0" +__author__ = "Your Name" +__email__ = "your.email@example.com" +__description__ = "A modern chat agent application with Vue frontend and FastAPI backend" + +# 导出主要组件 +from .core.config import settings +from .core.app import create_app + +__all__ = ["settings", "create_app", "__version__"] \ No newline at end of file diff --git a/backend/th_agenter/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..1cddd67 Binary files /dev/null and b/backend/th_agenter/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/__pycache__/main.cpython-313.pyc b/backend/th_agenter/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..06c1974 Binary files /dev/null and b/backend/th_agenter/__pycache__/main.cpython-313.pyc differ diff --git a/backend/th_agenter/api/__init__.py b/backend/th_agenter/api/__init__.py new file mode 100644 index 0000000..6ec7778 --- /dev/null +++ b/backend/th_agenter/api/__init__.py @@ -0,0 +1 @@ +"""API module for TH-Agenter.""" \ No newline at end of file diff --git a/backend/th_agenter/api/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6a7da22 Binary files /dev/null and b/backend/th_agenter/api/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/api/__pycache__/routes.cpython-313.pyc b/backend/th_agenter/api/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000..7fdacfd Binary files /dev/null and b/backend/th_agenter/api/__pycache__/routes.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__init__.py b/backend/th_agenter/api/endpoints/__init__.py new file mode 100644 index 0000000..b1df529 --- /dev/null +++ b/backend/th_agenter/api/endpoints/__init__.py @@ -0,0 +1 @@ +"""API endpoints for TH-Agenter.""" \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e4e9b60 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..a5180d6 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/chat.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/chat.cpython-313.pyc new file mode 100644 index 0000000..ead441e Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/chat.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/database_config.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/database_config.cpython-313.pyc new file mode 100644 index 0000000..9938445 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/database_config.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/knowledge_base.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/knowledge_base.cpython-313.pyc new file mode 100644 index 0000000..f5fe1f5 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/knowledge_base.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc new file mode 100644 index 0000000..e150410 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/roles.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/roles.cpython-313.pyc new file mode 100644 index 0000000..a6c2e67 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/roles.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc new file mode 100644 index 0000000..0c80152 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc new file mode 100644 index 0000000..23b62e0 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc new file mode 100644 index 0000000..cbb9525 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..9c8e85a Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000..1c14264 Binary files /dev/null and b/backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/api/endpoints/auth.py b/backend/th_agenter/api/endpoints/auth.py new file mode 100644 index 0000000..f063d7a --- /dev/null +++ b/backend/th_agenter/api/endpoints/auth.py @@ -0,0 +1,125 @@ +"""Authentication endpoints.""" + +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session + +from ...core.config import get_settings +from ...db.database import get_db +from ...services.auth import AuthService +from ...services.user import UserService +from ...schemas.user import UserResponse, UserCreate +from ...utils.schemas import Token, LoginRequest + +router = APIRouter() +settings = get_settings() + + +@router.post("/register", response_model=UserResponse) +async def register( + user_data: UserCreate, + db: Session = Depends(get_db) +): + """Register a new user.""" + user_service = UserService(db) + + # Check if user already exists + if user_service.get_user_by_email(user_data.email): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + if user_service.get_user_by_username(user_data.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already taken" + ) + + # Create user + user = user_service.create_user(user_data) + return UserResponse.from_orm(user) + + +@router.post("/login", response_model=Token) +async def login( + login_data: LoginRequest, + db: Session = Depends(get_db) +): + """Login with email and password.""" + # Authenticate user by email + user = AuthService.authenticate_user_by_email(db, login_data.email, login_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.post("/login-oauth", response_model=Token) +async def login_oauth( + form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db) +): + """Login with username and password (OAuth2 compatible).""" + # Authenticate user + user = AuthService.authenticate_user(db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.post("/refresh", response_model=Token) +async def refresh_token( + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Refresh access token.""" + # Create new access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": current_user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.get("/me", response_model=UserResponse) +async def get_current_user_info( + current_user = Depends(AuthService.get_current_user) +): + """Get current user information.""" + return UserResponse.from_orm(current_user) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/chat.py b/backend/th_agenter/api/endpoints/chat.py new file mode 100644 index 0000000..cc51746 --- /dev/null +++ b/backend/th_agenter/api/endpoints/chat.py @@ -0,0 +1,237 @@ +"""Chat endpoints.""" + +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...models.user import User +from ...services.auth import AuthService +from ...services.chat import ChatService +from ...services.conversation import ConversationService +from ...utils.schemas import ( + ConversationCreate, + ConversationResponse, + ConversationUpdate, + MessageCreate, + MessageResponse, + ChatRequest, + ChatResponse +) + +router = APIRouter() + + +# Conversation management +@router.post("/conversations", response_model=ConversationResponse) +async def create_conversation( + conversation_data: ConversationCreate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Create a new conversation.""" + conversation_service = ConversationService(db) + conversation = conversation_service.create_conversation( + user_id=current_user.id, + conversation_data=conversation_data + ) + return ConversationResponse.from_orm(conversation) + + +@router.get("/conversations", response_model=List[ConversationResponse]) +async def list_conversations( + skip: int = 0, + limit: int = 50, + search: str = None, + include_archived: bool = False, + order_by: str = "updated_at", + order_desc: bool = True, + db: Session = Depends(get_db) +): + """List user's conversations with search and filtering.""" + conversation_service = ConversationService(db) + conversations = conversation_service.get_user_conversations( + skip=skip, + limit=limit, + search_query=search, + include_archived=include_archived, + order_by=order_by, + order_desc=order_desc + ) + return [ConversationResponse.from_orm(conv) for conv in conversations] + + +@router.get("/conversations/count") +async def get_conversations_count( + search: str = None, + include_archived: bool = False, + db: Session = Depends(get_db) +): + """Get total count of conversations.""" + conversation_service = ConversationService(db) + count = conversation_service.get_user_conversations_count( + search_query=search, + include_archived=include_archived + ) + return {"count": count} + + +@router.get("/conversations/{conversation_id}", response_model=ConversationResponse) +async def get_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Get a specific conversation.""" + conversation_service = ConversationService(db) + conversation = conversation_service.get_conversation( + conversation_id=conversation_id + ) + if not conversation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Conversation not found" + ) + return ConversationResponse.from_orm(conversation) + + +@router.put("/conversations/{conversation_id}", response_model=ConversationResponse) +async def update_conversation( + conversation_id: int, + conversation_update: ConversationUpdate, + db: Session = Depends(get_db) +): + """Update a conversation.""" + conversation_service = ConversationService(db) + updated_conversation = conversation_service.update_conversation( + conversation_id, conversation_update + ) + return ConversationResponse.from_orm(updated_conversation) + + +@router.delete("/conversations/{conversation_id}") +async def delete_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Delete a conversation.""" + conversation_service = ConversationService(db) + conversation_service.delete_conversation(conversation_id) + return {"message": "Conversation deleted successfully"} + + +@router.delete("/conversations") +async def delete_all_conversations( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Delete all conversations.""" + conversation_service = ConversationService(db) + conversation_service.delete_all_conversations() + return {"message": "All conversations deleted successfully"} + + +@router.put("/conversations/{conversation_id}/archive") +async def archive_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Archive a conversation.""" + conversation_service = ConversationService(db) + success = conversation_service.archive_conversation(conversation_id) + if not success: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to archive conversation" + ) + + return {"message": "Conversation archived successfully"} + + +@router.put("/conversations/{conversation_id}/unarchive") +async def unarchive_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Unarchive a conversation.""" + conversation_service = ConversationService(db) + success = conversation_service.unarchive_conversation(conversation_id) + if not success: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to unarchive conversation" + ) + + return {"message": "Conversation unarchived successfully"} + + +# Message management +@router.get("/conversations/{conversation_id}/messages", response_model=List[MessageResponse]) +async def get_conversation_messages( + conversation_id: int, + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db) +): + """Get messages from a conversation.""" + conversation_service = ConversationService(db) + messages = conversation_service.get_conversation_messages( + conversation_id, skip=skip, limit=limit + ) + return [MessageResponse.from_orm(msg) for msg in messages] + + +# Chat functionality +@router.post("/conversations/{conversation_id}/chat", response_model=ChatResponse) +async def chat( + conversation_id: int, + chat_request: ChatRequest, + db: Session = Depends(get_db) +): + """Send a message and get AI response.""" + chat_service = ChatService(db) + response = await chat_service.chat( + conversation_id=conversation_id, + message=chat_request.message, + stream=False, + temperature=chat_request.temperature, + max_tokens=chat_request.max_tokens, + use_agent=chat_request.use_agent, + use_langgraph=chat_request.use_langgraph, + use_knowledge_base=chat_request.use_knowledge_base, + knowledge_base_id=chat_request.knowledge_base_id + ) + + return response + + +@router.post("/conversations/{conversation_id}/chat/stream") +async def chat_stream( + conversation_id: int, + chat_request: ChatRequest, + db: Session = Depends(get_db) +): + """Send a message and get streaming AI response.""" + chat_service = ChatService(db) + + async def generate_response(): + async for chunk in chat_service.chat_stream( + conversation_id=conversation_id, + message=chat_request.message, + temperature=chat_request.temperature, + max_tokens=chat_request.max_tokens, + use_agent=chat_request.use_agent, + use_langgraph=chat_request.use_langgraph, + use_knowledge_base=chat_request.use_knowledge_base, + knowledge_base_id=chat_request.knowledge_base_id + ): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + generate_response(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + } + ) diff --git a/backend/th_agenter/api/endpoints/database_config.py b/backend/th_agenter/api/endpoints/database_config.py new file mode 100644 index 0000000..f526921 --- /dev/null +++ b/backend/th_agenter/api/endpoints/database_config.py @@ -0,0 +1,207 @@ +"""数据库配置管理API""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Dict, Any +from pydantic import BaseModel, Field +from th_agenter.models.user import User +from th_agenter.db.database import get_db +from th_agenter.services.database_config_service import DatabaseConfigService +from th_agenter.utils.logger import get_logger +from th_agenter.services.auth import AuthService +logger = get_logger("database_config_api") +router = APIRouter(prefix="/api/database-config", tags=["database-config"]) +from th_agenter.utils.schemas import FileListResponse,ExcelPreviewRequest,NormalResponse + +# 在文件顶部添加 +from functools import lru_cache + +# 创建服务单例 +@lru_cache() +def get_database_config_service() -> DatabaseConfigService: + """获取DatabaseConfigService单例""" + # 注意:这里需要处理db session的问题 + return DatabaseConfigService(None) # 临时方案 + +# 或者使用全局变量 +_database_service_instance = None + +def get_database_service(db: Session = Depends(get_db)) -> DatabaseConfigService: + """获取DatabaseConfigService实例""" + global _database_service_instance + if _database_service_instance is None: + _database_service_instance = DatabaseConfigService(db) + else: + # 更新db session + _database_service_instance.db = db + return _database_service_instance +class DatabaseConfigCreate(BaseModel): + name: str = Field(..., description="配置名称") + db_type: str = Field(default="postgresql", description="数据库类型") + host: str = Field(..., description="主机地址") + port: int = Field(..., description="端口号") + database: str = Field(..., description="数据库名") + username: str = Field(..., description="用户名") + password: str = Field(..., description="密码") + is_default: bool = Field(default=False, description="是否为默认配置") + connection_params: Dict[str, Any] = Field(default=None, description="额外连接参数") + + +class DatabaseConfigResponse(BaseModel): + id: int + name: str + db_type: str + host: str + port: int + database: str + username: str + password: str # 添加密码字段 + is_active: bool + is_default: bool + created_at: str + updated_at: str = None + + +@router.post("/", response_model=NormalResponse) +async def create_database_config( + config_data: DatabaseConfigCreate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """创建或更新数据库配置""" + try: + service = DatabaseConfigService(db) + config = await service.create_or_update_config(current_user.id, config_data.dict()) + return NormalResponse( + success=True, + message="保存数据库配置成功", + data=config + ) + except Exception as e: + logger.error(f"创建或更新数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/", response_model=List[DatabaseConfigResponse]) +async def get_database_configs( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取用户的数据库配置列表""" + try: + service = DatabaseConfigService(db) + configs = service.get_user_configs(current_user.id) + + config_list = [config.to_dict(include_password=True, decrypt_service=service) for config in configs] + return config_list + except Exception as e: + logger.error(f"获取数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@router.post("/{config_id}/test") +async def test_database_connection( + config_id: int, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """测试数据库连接""" + try: + service = DatabaseConfigService(db) + result = await service.test_connection(config_id, current_user.id) + return result + except Exception as e: + logger.error(f"测试数据库连接失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.post("/{config_id}/connect") +async def connect_database( + config_id: int, + current_user: User = Depends(AuthService.get_current_user), + service: DatabaseConfigService = Depends(get_database_service) +): + """连接数据库并获取表列表""" + try: + result = await service.connect_and_get_tables(config_id, current_user.id) + return result + except Exception as e: + logger.error(f"连接数据库失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/tables/{table_name}/data") +async def get_table_data( + table_name: str, + db_type: str, + limit: int = 100, + current_user: User = Depends(AuthService.get_current_user), + service: DatabaseConfigService = Depends(get_database_service) +): + """获取表数据预览""" + try: + result = await service.get_table_data(table_name, current_user.id, db_type, limit) + return result + except Exception as e: + logger.error(f"获取表数据失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/tables/{table_name}/schema") +async def get_table_schema( + table_name: str, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取表结构信息""" + try: + service = DatabaseConfigService(db) + result = await service.describe_table(table_name, current_user.id) + return result + except Exception as e: + logger.error(f"获取表结构失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/by-type/{db_type}", response_model=DatabaseConfigResponse) +async def get_config_by_type( + db_type: str, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """根据数据库类型获取配置""" + try: + service = DatabaseConfigService(db) + config = service.get_config_by_type(current_user.id, db_type) + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"未找到类型为 {db_type} 的配置" + ) + # 返回包含解密密码的配置 + return config.to_dict(include_password=True, decrypt_service=service) + except Exception as e: + logger.error(f"获取数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + diff --git a/backend/th_agenter/api/endpoints/knowledge_base.py b/backend/th_agenter/api/endpoints/knowledge_base.py new file mode 100644 index 0000000..4dee618 --- /dev/null +++ b/backend/th_agenter/api/endpoints/knowledge_base.py @@ -0,0 +1,666 @@ +"""Knowledge base API endpoints.""" + +import os +import logging +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...models.user import User +from ...models.knowledge_base import KnowledgeBase, Document +from ...services.knowledge_base import KnowledgeBaseService +from ...services.document import DocumentService +from ...services.auth import AuthService +from ...utils.schemas import ( + KnowledgeBaseCreate, + KnowledgeBaseResponse, + DocumentResponse, + DocumentListResponse, + DocumentUpload, + DocumentProcessingStatus, + DocumentChunksResponse, + ErrorResponse +) +from ...utils.file_utils import FileUtils +from ...core.config import settings + +logger = logging.getLogger(__name__) +router = APIRouter(tags=["knowledge-bases"]) + + +@router.post("/", response_model=KnowledgeBaseResponse) +async def create_knowledge_base( + kb_data: KnowledgeBaseCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Create a new knowledge base.""" + try: + # Check if knowledge base with same name already exists for this user + service = KnowledgeBaseService(db) + existing_kb = service.get_knowledge_base_by_name(kb_data.name) + if existing_kb: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Knowledge base with this name already exists" + ) + + # Create knowledge base + kb = service.create_knowledge_base(kb_data) + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=0, + active_document_count=0 + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create knowledge base: {str(e)}" + ) + + +@router.get("/", response_model=List[KnowledgeBaseResponse]) +async def list_knowledge_bases( + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """List knowledge bases for current user.""" + try: + service = KnowledgeBaseService(db) + knowledge_bases = service.get_knowledge_bases(skip=skip, limit=limit) + + result = [] + for kb in knowledge_bases: + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + result.append(KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + )) + + return result + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to list knowledge bases: {str(e)}" + ) + + +@router.get("/{kb_id}", response_model=KnowledgeBaseResponse) +async def get_knowledge_base( + kb_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get knowledge base by ID.""" + try: + service = KnowledgeBaseService(db) + kb = service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get knowledge base: {str(e)}" + ) + + +@router.put("/{kb_id}", response_model=KnowledgeBaseResponse) +async def update_knowledge_base( + kb_id: int, + kb_data: KnowledgeBaseCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Update knowledge base.""" + try: + service = KnowledgeBaseService(db) + kb = service.update_knowledge_base(kb_id, kb_data) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update knowledge base: {str(e)}" + ) + + +@router.delete("/{kb_id}") +async def delete_knowledge_base( + kb_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Delete knowledge base.""" + try: + service = KnowledgeBaseService(db) + success = service.delete_knowledge_base(kb_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + return {"message": "Knowledge base deleted successfully"} + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete knowledge base: {str(e)}" + ) + + +# Document management endpoints +@router.post("/{kb_id}/documents", response_model=DocumentResponse) +async def upload_document( + kb_id: int, + file: UploadFile = File(...), + process_immediately: bool = Form(True), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Upload document to knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Validate file + if not FileUtils.validate_file_extension(file.filename): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File type not supported. Allowed types: {', '.join(FileUtils.ALLOWED_EXTENSIONS)}" + ) + + # Check file size (50MB limit) + max_size = 50 * 1024 * 1024 # 50MB + if file.size and file.size > max_size: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File too large. Maximum size: {FileUtils.format_file_size(max_size)}" + ) + + # Upload document + doc_service = DocumentService(db) + document = await doc_service.upload_document( + file, kb_id + ) + + # Process document immediately if requested + if process_immediately: + try: + await doc_service.process_document(document.id, kb_id) + # Refresh document to get updated status + db.refresh(document) + except Exception as e: + # Log error but don't fail the upload + logger.error(f"Failed to process document immediately: {e}") + + return DocumentResponse( + id=document.id, + created_at=document.created_at, + updated_at=document.updated_at, + knowledge_base_id=document.knowledge_base_id, + filename=document.filename, + original_filename=document.original_filename, + file_path=document.file_path, + file_type=document.file_type, + file_size=document.file_size, + mime_type=document.mime_type, + is_processed=document.is_processed, + processing_error=document.processing_error, + chunk_count=document.chunk_count or 0, + embedding_model=document.embedding_model, + file_size_mb=round(document.file_size / (1024 * 1024), 2) + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to upload document: {str(e)}" + ) + + +@router.get("/{kb_id}/documents", response_model=DocumentListResponse) +async def list_documents( + kb_id: int, + skip: int = 0, + limit: int = 50, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """List documents in knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + documents, total = doc_service.list_documents(kb_id, skip, limit) + + doc_responses = [] + for doc in documents: + doc_responses.append(DocumentResponse( + id=doc.id, + created_at=doc.created_at, + updated_at=doc.updated_at, + knowledge_base_id=doc.knowledge_base_id, + filename=doc.filename, + original_filename=doc.original_filename, + file_path=doc.file_path, + file_type=doc.file_type, + file_size=doc.file_size, + mime_type=doc.mime_type, + is_processed=doc.is_processed, + processing_error=doc.processing_error, + chunk_count=doc.chunk_count or 0, + embedding_model=doc.embedding_model, + file_size_mb=round(doc.file_size / (1024 * 1024), 2) + )) + + return DocumentListResponse( + documents=doc_responses, + total=total, + page=skip // limit + 1, + page_size=limit + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to list documents: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}", response_model=DocumentResponse) +async def get_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get document by ID.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + return DocumentResponse( + id=document.id, + created_at=document.created_at, + updated_at=document.updated_at, + knowledge_base_id=document.knowledge_base_id, + filename=document.filename, + original_filename=document.original_filename, + file_path=document.file_path, + file_type=document.file_type, + file_size=document.file_size, + mime_type=document.mime_type, + is_processed=document.is_processed, + processing_error=document.processing_error, + chunk_count=document.chunk_count or 0, + embedding_model=document.embedding_model, + file_size_mb=round(document.file_size / (1024 * 1024), 2) + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document: {str(e)}" + ) + + +@router.delete("/{kb_id}/documents/{doc_id}") +async def delete_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Delete document from knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + success = doc_service.delete_document(doc_id, kb_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + return {"message": "Document deleted successfully"} + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete document: {str(e)}" + ) + + +@router.post("/{kb_id}/documents/{doc_id}/process", response_model=DocumentProcessingStatus) +async def process_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Process document for vector search.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Check if document exists + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Process the document + result = await doc_service.process_document(doc_id, kb_id) + + return DocumentProcessingStatus( + document_id=doc_id, + status=result["status"], + progress=result.get("progress", 0.0), + error_message=result.get("error_message"), + chunks_created=result.get("chunks_created", 0) + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to process document: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}/status", response_model=DocumentProcessingStatus) +async def get_document_processing_status( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get document processing status.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Determine status + if document.processing_error: + status_str = "failed" + progress = 0.0 + elif document.is_processed: + status_str = "completed" + progress = 100.0 + else: + status_str = "pending" + progress = 0.0 + + return DocumentProcessingStatus( + document_id=document.id, + status=status_str, + progress=progress, + error_message=document.processing_error, + chunks_created=document.chunk_count or 0 + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document status: {str(e)}" + ) + + +@router.get("/{kb_id}/search") +async def search_knowledge_base( + kb_id: int, + query: str, + limit: int = 5, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Search documents in a knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Perform search + doc_service = DocumentService(db) + results = doc_service.search_documents(kb_id, query, limit) + + return { + "knowledge_base_id": kb_id, + "query": query, + "results": results, + "total_results": len(results) + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to search knowledge base: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}/chunks", response_model=DocumentChunksResponse) +async def get_document_chunks( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """ + Get document chunks (segments) for a specific document. + + Args: + kb_id: Knowledge base ID + doc_id: Document ID + db: Database session + current_user: Current authenticated user + + Returns: + DocumentChunksResponse: Document chunks with metadata + """ + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + knowledge_base = kb_service.get_knowledge_base(kb_id) + if not knowledge_base: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Verify document exists in the knowledge base + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Get document chunks + chunks = doc_service.get_document_chunks(doc_id) + + return DocumentChunksResponse( + document_id=doc_id, + document_name=document.filename, + total_chunks=len(chunks), + chunks=chunks + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document chunks: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/llm_configs.py b/backend/th_agenter/api/endpoints/llm_configs.py new file mode 100644 index 0000000..8532fd4 --- /dev/null +++ b/backend/th_agenter/api/endpoints/llm_configs.py @@ -0,0 +1,528 @@ +"""LLM configuration management API endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import or_ + +from ...db.database import get_db +from ...models.user import User +from ...models.llm_config import LLMConfig +from ...core.simple_permissions import require_super_admin, require_authenticated_user +from ...services.auth import AuthService +from ...utils.logger import get_logger +from ...schemas.llm_config import ( + LLMConfigCreate, LLMConfigUpdate, LLMConfigResponse, + LLMConfigTest +) +from th_agenter.services.document_processor import get_document_processor +logger = get_logger(__name__) +router = APIRouter(prefix="/llm-configs", tags=["llm-configs"]) + + +@router.get("/", response_model=List[LLMConfigResponse]) +async def get_llm_configs( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + search: Optional[str] = Query(None), + provider: Optional[str] = Query(None), + is_active: Optional[bool] = Query(None), + is_embedding: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取大模型配置列表.""" + try: + query = db.query(LLMConfig) + + # 搜索 + if search: + query = query.filter( + or_( + LLMConfig.name.ilike(f"%{search}%"), + LLMConfig.model_name.ilike(f"%{search}%"), + LLMConfig.description.ilike(f"%{search}%") + ) + ) + + # 服务商筛选 + if provider: + query = query.filter(LLMConfig.provider == provider) + + # 状态筛选 + if is_active is not None: + query = query.filter(LLMConfig.is_active == is_active) + + # 模型类型筛选 + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + # 排序 + query = query.order_by(LLMConfig.name) + + # 分页 + configs = query.offset(skip).limit(limit).all() + + return [config.to_dict(include_sensitive=True) for config in configs] + + except Exception as e: + logger.error(f"Error getting LLM configs: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取大模型配置列表失败" + ) + + +@router.get("/providers") +async def get_llm_providers( + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取支持的大模型服务商列表.""" + try: + providers = db.query(LLMConfig.provider).distinct().all() + return [provider[0] for provider in providers if provider[0]] + + except Exception as e: + logger.error(f"Error getting LLM providers: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取服务商列表失败" + ) + + +@router.get("/active", response_model=List[LLMConfigResponse]) +async def get_active_llm_configs( + is_embedding: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取所有激活的大模型配置.""" + try: + query = db.query(LLMConfig).filter(LLMConfig.is_active == True) + + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + configs = query.order_by(LLMConfig.created_at).all() + + return [config.to_dict(include_sensitive=True) for config in configs] + + except Exception as e: + logger.error(f"Error getting active LLM configs: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取激活配置列表失败" + ) + + +@router.get("/default", response_model=LLMConfigResponse) +async def get_default_llm_config( + is_embedding: bool = Query(False, description="是否获取嵌入模型默认配置"), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取默认大模型配置.""" + try: + config = db.query(LLMConfig).filter( + LLMConfig.is_default == True, + LLMConfig.is_embedding == is_embedding, + LLMConfig.is_active == True + ).first() + + if not config: + model_type = "嵌入模型" if is_embedding else "对话模型" + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"未找到默认{model_type}配置" + ) + + return config.to_dict(include_sensitive=True) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting default LLM config: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取默认配置失败" + ) + + +@router.get("/{config_id}", response_model=LLMConfigResponse) +async def get_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取大模型配置详情.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + return config.to_dict(include_sensitive=True) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取大模型配置详情失败" + ) + + +@router.post("/", response_model=LLMConfigResponse, status_code=status.HTTP_201_CREATED) +async def create_llm_config( + config_data: LLMConfigCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """创建大模型配置.""" + try: + # 检查配置名称是否已存在 + existing_config = db.query(LLMConfig).filter( + LLMConfig.name == config_data.name + ).first() + if existing_config: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="配置名称已存在" + ) + + # 创建临时配置对象进行验证 + temp_config = LLMConfig( + name=config_data.name, + provider=config_data.provider, + model_name=config_data.model_name, + api_key=config_data.api_key, + base_url=config_data.base_url, + max_tokens=config_data.max_tokens, + temperature=config_data.temperature, + top_p=config_data.top_p, + frequency_penalty=config_data.frequency_penalty, + presence_penalty=config_data.presence_penalty, + description=config_data.description, + is_active=config_data.is_active, + is_default=config_data.is_default, + is_embedding=config_data.is_embedding, + extra_config=config_data.extra_config or {} + ) + + # 验证配置 + validation_result = temp_config.validate_config() + if not validation_result['valid']: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=validation_result['error'] + ) + + # 如果设为默认,取消同类型的其他默认配置 + if config_data.is_default: + db.query(LLMConfig).filter( + LLMConfig.is_embedding == config_data.is_embedding + ).update({"is_default": False}) + + # 创建配置 + config = LLMConfig( + name=config_data.name, + provider=config_data.provider, + model_name=config_data.model_name, + api_key=config_data.api_key, + base_url=config_data.base_url, + max_tokens=config_data.max_tokens, + temperature=config_data.temperature, + top_p=config_data.top_p, + frequency_penalty=config_data.frequency_penalty, + presence_penalty=config_data.presence_penalty, + description=config_data.description, + is_active=config_data.is_active, + is_default=config_data.is_default, + is_embedding=config_data.is_embedding, + extra_config=config_data.extra_config or {} + ) + config.set_audit_fields(current_user.id) + + db.add(config) + db.commit() + db.refresh(config) + + logger.info(f"LLM config created: {config.name} by user {current_user.username}") + return config.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error creating LLM config: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建大模型配置失败" + ) + + +@router.put("/{config_id}", response_model=LLMConfigResponse) +async def update_llm_config( + config_id: int, + config_data: LLMConfigUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """更新大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 检查配置名称是否已存在(排除自己) + if config_data.name and config_data.name != config.name: + existing_config = db.query(LLMConfig).filter( + LLMConfig.name == config_data.name, + LLMConfig.id != config_id + ).first() + if existing_config: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="配置名称已存在" + ) + + # 如果设为默认,取消同类型的其他默认配置 + if config_data.is_default is True: + # 获取当前配置的embedding类型,如果更新中包含is_embedding则使用新值 + is_embedding = config_data.is_embedding if config_data.is_embedding is not None else config.is_embedding + db.query(LLMConfig).filter( + LLMConfig.is_embedding == is_embedding, + LLMConfig.id != config_id + ).update({"is_default": False}) + + # 更新字段 + update_data = config_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + logger.info(f"LLM config updated: {config.name} by user {current_user.username}") + return config.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新大模型配置失败" + ) + + +@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """删除大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # TODO: 检查是否有对话或其他功能正在使用该配置 + # 这里可以添加相关的检查逻辑 + + db.delete(config) + db.commit() + + logger.info(f"LLM config deleted: {config.name} by user {current_user.username}") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除大模型配置失败" + ) + + +@router.post("/{config_id}/test") +async def test_llm_config( + config_id: int, + test_data: LLMConfigTest, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """测试大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 验证配置 + validation_result = config.validate_config() + if not validation_result["valid"]: + return { + "success": False, + "message": f"配置验证失败: {validation_result['error']}", + "details": validation_result + } + + # 尝试创建客户端并发送测试请求 + try: + # 这里应该根据不同的服务商创建相应的客户端 + # 由于具体的客户端实现可能因服务商而异,这里提供一个通用的框架 + + test_message = test_data.message or "Hello, this is a test message." + + # TODO: 实现具体的测试逻辑 + # 例如: + # client = config.get_client() + # response = client.chat.completions.create( + # model=config.model_name, + # messages=[{"role": "user", "content": test_message}], + # max_tokens=100 + # ) + + # 模拟测试成功 + logger.info(f"LLM config test: {config.name} by user {current_user.username}") + + return { + "success": True, + "message": "配置测试成功", + "test_message": test_message, + "response": "这是一个模拟的测试响应。实际实现中,这里会是大模型的真实响应。", + "latency_ms": 150, # 模拟延迟 + "config_info": config.get_client_config() + } + + except Exception as test_error: + logger.error(f"LLM config test failed: {config.name}, error: {str(test_error)}") + return { + "success": False, + "message": f"配置测试失败: {str(test_error)}", + "test_message": test_message, + "config_info": config.get_client_config() + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error testing LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="测试大模型配置失败" + ) + + +@router.post("/{config_id}/toggle-status") +async def toggle_llm_config_status( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """切换大模型配置状态.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 切换状态 + config.is_active = not config.is_active + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + status_text = "激活" if config.is_active else "禁用" + logger.info(f"LLM config status toggled: {config.name} {status_text} by user {current_user.username}") + + return { + "message": f"配置已{status_text}", + "is_active": config.is_active + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error toggling LLM config status {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="切换配置状态失败" + ) + + +@router.post("/{config_id}/set-default") +async def set_default_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """设置默认大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 检查配置是否激活 + if not config.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="只能将激活的配置设为默认" + ) + + # 取消同类型的其他默认配置 + db.query(LLMConfig).filter( + LLMConfig.is_embedding == config.is_embedding, + LLMConfig.id != config_id + ).update({"is_default": False}) + + # 设置当前配置为默认 + config.is_default = True + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + model_type = "嵌入模型" if config.is_embedding else "对话模型" + logger.info(f"Default LLM config set: {config.name} ({model_type}) by user {current_user.username}") + # 更新文档处理器默认embedding + get_document_processor()._init_embeddings() + return { + "message": f"已将 {config.name} 设为默认{model_type}配置", + "is_default": config.is_default + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error setting default LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="设置默认配置失败" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/roles.py b/backend/th_agenter/api/endpoints/roles.py new file mode 100644 index 0000000..0b88249 --- /dev/null +++ b/backend/th_agenter/api/endpoints/roles.py @@ -0,0 +1,346 @@ +"""Role management API endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from ...core.simple_permissions import require_super_admin +from ...db.database import get_db +from ...models.user import User +from ...models.permission import Role, UserRole +from ...services.auth import AuthService +from ...utils.logger import get_logger +from ...schemas.permission import ( + RoleCreate, RoleUpdate, RoleResponse, + UserRoleAssign +) + +logger = get_logger(__name__) +router = APIRouter(prefix="/roles", tags=["roles"]) + + +@router.get("/", response_model=List[RoleResponse]) +async def get_roles( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + search: Optional[str] = Query(None), + is_active: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user = Depends(require_super_admin), +): + """获取角色列表.""" + try: + query = db.query(Role) + + # 搜索 + if search: + query = query.filter( + or_( + Role.name.ilike(f"%{search}%"), + Role.code.ilike(f"%{search}%"), + Role.description.ilike(f"%{search}%") + ) + ) + + # 状态筛选 + if is_active is not None: + query = query.filter(Role.is_active == is_active) + + # 分页 + roles = query.offset(skip).limit(limit).all() + + return [role.to_dict() for role in roles] + + except Exception as e: + logger.error(f"Error getting roles: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取角色列表失败" + ) + + +@router.get("/{role_id}", response_model=RoleResponse) +async def get_role( + role_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """获取角色详情.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取角色详情失败" + ) + + +@router.post("/", response_model=RoleResponse, status_code=status.HTTP_201_CREATED) +async def create_role( + role_data: RoleCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """创建角色.""" + try: + # 检查角色代码是否已存在 + existing_role = db.query(Role).filter( + Role.code == role_data.code + ).first() + if existing_role: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="角色代码已存在" + ) + + # 创建角色 + role = Role( + name=role_data.name, + code=role_data.code, + description=role_data.description, + is_active=role_data.is_active + ) + role.set_audit_fields(current_user.id) + + db.add(role) + db.commit() + db.refresh(role) + + logger.info(f"Role created: {role.name} by user {current_user.username}") + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error creating role: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建角色失败" + ) + + +@router.put("/{role_id}", response_model=RoleResponse) +async def update_role( + role_id: int, + role_data: RoleUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """更新角色.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + # 超级管理员角色不能被编辑 + if role.code == "SUPER_ADMIN": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="超级管理员角色不能被编辑" + ) + + # 检查角色编码是否已存在(排除当前角色) + if role_data.code and role_data.code != role.code: + existing_role = db.query(Role).filter( + and_( + Role.code == role_data.code, + Role.id != role_id + ) + ).first() + if existing_role: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="角色代码已存在" + ) + + # 更新字段 + update_data = role_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(role, field, value) + + role.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(role) + + logger.info(f"Role updated: {role.name} by user {current_user.username}") + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新角色失败" + ) + + +@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_role( + role_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """删除角色.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + # 超级管理员角色不能被删除 + if role.code == "SUPER_ADMIN": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="超级管理员角色不能被删除" + ) + + # 检查是否有用户使用该角色 + user_count = db.query(UserRole).filter( + UserRole.role_id == role_id + ).count() + if user_count > 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"无法删除角色,还有 {user_count} 个用户关联此角色" + ) + + # 删除角色 + db.delete(role) + db.commit() + + logger.info(f"Role deleted: {role.name} by user {current_user.username}") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除角色失败" + ) + + +# 用户角色管理路由 +user_role_router = APIRouter(prefix="/user-roles", tags=["user-roles"]) + + +@user_role_router.post("/assign", status_code=status.HTTP_201_CREATED) +async def assign_user_roles( + assignment_data: UserRoleAssign, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """为用户分配角色.""" + try: + # 验证用户是否存在 + user = db.query(User).filter(User.id == assignment_data.user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="用户不存在" + ) + + # 验证角色是否存在 + roles = db.query(Role).filter( + Role.id.in_(assignment_data.role_ids) + ).all() + if len(roles) != len(assignment_data.role_ids): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="部分角色不存在" + ) + + # 删除现有角色关联 + db.query(UserRole).filter( + UserRole.user_id == assignment_data.user_id + ).delete() + + # 添加新的角色关联 + for role_id in assignment_data.role_ids: + user_role = UserRole( + user_id=assignment_data.user_id, + role_id=role_id + ) + db.add(user_role) + + db.commit() + + logger.info(f"User roles assigned: user {user.username}, roles {assignment_data.role_ids} by user {current_user.username}") + + return {"message": "角色分配成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error assigning roles to user {assignment_data.user_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="角色分配失败" + ) + + +@user_role_router.get("/user/{user_id}", response_model=List[RoleResponse]) +async def get_user_roles( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_active_user) +): + """获取用户角色列表.""" + try: + # 检查权限:用户只能查看自己的角色,或者是超级管理员 + if current_user.id != user_id and not current_user.is_superuser(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权限查看其他用户的角色" + ) + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="用户不存在" + ) + + roles = db.query(Role).join( + UserRole, Role.id == UserRole.role_id + ).filter( + UserRole.user_id == user_id + ).all() + + return [role.to_dict() for role in roles] + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting user roles {user_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取用户角色失败" + ) + + +# 将子路由添加到主路由 +router.include_router(user_role_router) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/smart_chat.py b/backend/th_agenter/api/endpoints/smart_chat.py new file mode 100644 index 0000000..828d526 --- /dev/null +++ b/backend/th_agenter/api/endpoints/smart_chat.py @@ -0,0 +1,342 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any +import logging +from datetime import datetime + +from th_agenter.db.database import get_db +from th_agenter.services.auth import AuthService +from th_agenter.services.smart_workflow import SmartWorkflowManager +from th_agenter.services.conversation import ConversationService +from th_agenter.services.conversation_context import conversation_context_service +from th_agenter.utils.schemas import BaseResponse +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/smart-chat", tags=["smart-chat"]) +security = HTTPBearer() + +# Request/Response Models +class SmartQueryRequest(BaseModel): + query: str + conversation_id: Optional[int] = None + is_new_conversation: bool = False + +class SmartQueryResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + workflow_steps: Optional[list] = None + conversation_id: Optional[int] = None + +class ConversationContextResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + +@router.post("/query", response_model=SmartQueryResponse) +async def smart_query( + request: SmartQueryRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 智能问数查询接口 + 支持新对话时自动加载文件列表,智能选择相关Excel文件,生成和执行pandas代码 + """ + conversation_id = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="查询内容不能为空" + ) + + if len(request.query) > 1000: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="查询内容过长,请控制在1000字符以内" + ) + + # 初始化工作流管理器 + workflow_manager = SmartWorkflowManager(db) + conversation_service = ConversationService(db) + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"智能问数: {request.query[:20]}..." + ) + request.is_new_conversation = True + logger.info(f"创建新对话: {conversation_id}") + except Exception as e: + logger.warning(f"创建对话失败,使用临时会话: {e}") + conversation_id = None + else: + # 验证对话是否存在且属于当前用户 + try: + context = await conversation_context_service.get_conversation_context(conversation_id) + if not context or context.get('user_id') != current_user.id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话不存在或无权访问" + ) + logger.info(f"使用现有对话: {conversation_id}") + except HTTPException: + raise + except Exception as e: + logger.error(f"验证对话失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="对话验证失败" + ) + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + # 不阻断流程,继续执行查询 + + # 执行智能查询工作流 + try: + result = await workflow_manager.process_smart_query( + user_query=request.query, + user_id=current_user.id, + conversation_id=conversation_id, + is_new_conversation=request.is_new_conversation + ) + except Exception as e: + logger.error(f"智能查询执行失败: {e}") + # 返回结构化的错误响应 + return SmartQueryResponse( + success=False, + message=f"查询执行失败: {str(e)}", + data={'error_type': 'query_execution_error'}, + workflow_steps=[{ + 'step': 'error', + 'status': 'failed', + 'message': str(e) + }], + conversation_id=conversation_id + ) + + # 如果查询成功,保存助手回复和更新上下文 + if result['success'] and conversation_id: + try: + # 保存助手回复 + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result.get('data', {}).get('summary', '查询完成'), + metadata={ + 'query_result': result.get('data'), + 'workflow_steps': result.get('workflow_steps', []), + 'selected_files': result.get('data', {}).get('used_files', []) + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=result.get('data', {}).get('used_files', []) + ) + + logger.info(f"查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + # 不影响返回结果,只记录警告 + + # 返回结果,包含对话ID + response_data = result.get('data', {}) + if conversation_id: + response_data['conversation_id'] = conversation_id + + return SmartQueryResponse( + success=result['success'], + message=result.get('message', '查询完成'), + data=response_data, + workflow_steps=result.get('workflow_steps', []), + conversation_id=conversation_id + ) + + except HTTPException: + print(e) + raise + except Exception as e: + logger.error(f"智能查询接口异常: {e}", exc_info=True) + # 返回通用错误响应 + return SmartQueryResponse( + success=False, + message="服务器内部错误,请稍后重试", + data={'error_type': 'internal_server_error'}, + workflow_steps=[{ + 'step': 'error', + 'status': 'failed', + 'message': '系统异常' + }], + conversation_id=conversation_id + ) + +@router.get("/conversation/{conversation_id}/context", response_model=ConversationContextResponse) +async def get_conversation_context( + conversation_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取对话上下文信息,包括已使用的文件和历史查询 + """ + try: + # 获取对话上下文 + context = await conversation_context_service.get_conversation_context(conversation_id) + + if not context: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话上下文不存在" + ) + + # 验证用户权限 + if context['user_id'] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权访问此对话" + ) + + # 获取对话历史 + history = await conversation_context_service.get_conversation_history(conversation_id) + context['message_history'] = history + + return ConversationContextResponse( + success=True, + message="获取对话上下文成功", + data=context + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取对话上下文失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取对话上下文失败: {str(e)}" + ) + +@router.get("/files/status", response_model=ConversationContextResponse) +async def get_files_status( + current_user = Depends(AuthService.get_current_user) +): + """ + 获取用户当前的文件状态和统计信息 + """ + try: + workflow_manager = SmartWorkflowManager() + + # 获取用户文件列表 + file_list = await workflow_manager.excel_workflow._load_user_file_list(current_user.id) + + # 统计信息 + total_files = len(file_list) + total_rows = sum(f.get('row_count', 0) for f in file_list) + total_columns = sum(f.get('column_count', 0) for f in file_list) + + # 文件类型统计 + file_types = {} + for file_info in file_list: + filename = file_info['filename'] + ext = filename.split('.')[-1].lower() if '.' in filename else 'unknown' + file_types[ext] = file_types.get(ext, 0) + 1 + + status_data = { + 'total_files': total_files, + 'total_rows': total_rows, + 'total_columns': total_columns, + 'file_types': file_types, + 'files': [{ + 'id': f['id'], + 'filename': f['filename'], + 'row_count': f.get('row_count', 0), + 'column_count': f.get('column_count', 0), + 'columns': f.get('columns', []), + 'upload_time': f.get('upload_time') + } for f in file_list], + 'ready_for_query': total_files > 0 + } + + return ConversationContextResponse( + success=True, + message=f"当前有{total_files}个可用文件" if total_files > 0 else "暂无可用文件,请先上传Excel文件", + data=status_data + ) + + except Exception as e: + logger.error(f"获取文件状态失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取文件状态失败: {str(e)}" + ) + +@router.post("/conversation/{conversation_id}/reset") +async def reset_conversation_context( + conversation_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 重置对话上下文,清除历史查询记录但保留文件 + """ + try: + # 验证对话存在和用户权限 + context = await conversation_context_service.get_conversation_context(conversation_id) + + if not context: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话上下文不存在" + ) + + if context['user_id'] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权访问此对话" + ) + + # 重置对话上下文 + success = await conversation_context_service.reset_conversation_context(conversation_id) + + if success: + return { + "success": True, + "message": "对话上下文已重置,可以开始新的数据分析会话" + } + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="重置对话上下文失败" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"重置对话上下文失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"重置对话上下文失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/smart_query.py b/backend/th_agenter/api/endpoints/smart_query.py new file mode 100644 index 0000000..176f6fb --- /dev/null +++ b/backend/th_agenter/api/endpoints/smart_query.py @@ -0,0 +1,754 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any, List +import pandas as pd +from th_agenter.utils.schemas import FileListResponse,ExcelPreviewRequest,NormalResponse +import os +import tempfile +from th_agenter.utils.schemas import BaseResponse +from th_agenter.services.smart_query import ( + SmartQueryService, + ExcelAnalysisService, + DatabaseQueryService +) +from th_agenter.services.excel_metadata_service import ExcelMetadataService + +import uuid +from pathlib import Path +from th_agenter.utils.file_utils import FileUtils + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import Optional, AsyncGenerator +import json +from datetime import datetime + +from th_agenter.db.database import get_db +from th_agenter.services.auth import AuthService +from th_agenter.services.smart_workflow import SmartWorkflowManager +from th_agenter.services.conversation_context import ConversationContextService +import logging +from pydantic import BaseModel + + + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/smart-query", tags=["smart-query"]) +security = HTTPBearer() + +# Request/Response Models +class DatabaseConfig(BaseModel): + type: str + host: str + port: str + database: str + username: str + password: str + +class QueryRequest(BaseModel): + query: str + page: int = 1 + page_size: int = 20 + table_name: Optional[str] = None + +class TableSchemaRequest(BaseModel): + table_name: str + +class ExcelUploadResponse(BaseModel): + file_id: int + success: bool + message: str + data: Optional[Dict[str, Any]] = None # 添加data字段 + + +class QueryResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + + +@router.post("/upload-excel", response_model=ExcelUploadResponse) +async def upload_excel( + file: UploadFile = File(...), + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 上传Excel文件并进行预处理 + """ + try: + # 验证文件类型 + allowed_extensions = ['.xlsx', '.xls', '.csv'] + file_extension = os.path.splitext(file.filename)[1].lower() + + if file_extension not in allowed_extensions: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的文件格式,请上传 .xlsx, .xls 或 .csv 文件" + ) + + # 验证文件大小 (10MB) + content = await file.read() + file_size = len(content) + if file_size > 10 * 1024 * 1024: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="文件大小不能超过 10MB" + ) + + # 创建持久化目录结构 + backend_dir = Path(__file__).parent.parent.parent.parent # 获取backend目录 + data_dir = backend_dir / "data/uploads" + excel_user_dir = data_dir / f"excel_{current_user.id}" + + # 确保目录存在 + excel_user_dir.mkdir(parents=True, exist_ok=True) + + # 生成文件名:{uuid}_{原始文件名称} + file_id = str(uuid.uuid4()) + safe_filename = FileUtils.sanitize_filename(file.filename) + new_filename = f"{file_id}_{safe_filename}" + file_path = excel_user_dir / new_filename + + # 保存文件 + with open(file_path, 'wb') as f: + f.write(content) + + # 使用Excel元信息服务提取并保存元信息 + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.save_file_metadata( + file_path=str(file_path), + original_filename=file.filename, + user_id=current_user.id, + file_size=file_size + ) + + # 为了兼容现有前端,仍然创建pickle文件 + try: + if file_extension == '.csv': + df = pd.read_csv(file_path, encoding='utf-8') + else: + df = pd.read_excel(file_path) + except UnicodeDecodeError: + if file_extension == '.csv': + df = pd.read_csv(file_path, encoding='gbk') + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="文件编码错误,请确保文件为UTF-8或GBK编码" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"文件读取失败: {str(e)}" + ) + + # 保存pickle文件到同一目录 + pickle_filename = f"{file_id}_{safe_filename}.pkl" + pickle_path = excel_user_dir / pickle_filename + df.to_pickle(pickle_path) + + # 数据预处理和分析(保持兼容性) + excel_service = ExcelAnalysisService() + analysis_result = excel_service.analyze_dataframe(df, file.filename) + + # 添加数据库文件信息 + analysis_result.update({ + 'file_id': str(excel_file.id), + 'database_id': excel_file.id, + 'temp_file_path': str(pickle_path), # 更新为新的pickle路径 + 'original_filename': file.filename, + 'file_size_mb': excel_file.file_size_mb, + 'sheet_names': excel_file.sheet_names, + }) + + return ExcelUploadResponse( + file_id=excel_file.id, + success=True, + message="Excel文件上传成功", + data=analysis_result + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"文件处理失败: {str(e)}" + ) + +@router.post("/preview-excel", response_model=QueryResponse) +async def preview_excel( + request: ExcelPreviewRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 预览Excel文件数据 + """ + try: + logger.info(f"Preview request for file_id: {request.file_id}, user: {current_user.id}") + + # 验证file_id格式 + try: + file_id = int(request.file_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"无效的文件ID格式: {request.file_id}" + ) + + # 从数据库获取文件信息 + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.get_file_by_id(file_id, current_user.id) + + if not excel_file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件不存在或已被删除" + ) + + # 检查文件是否存在 + if not os.path.exists(excel_file.file_path): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件已被移动或删除" + ) + + # 更新最后访问时间 + metadata_service.update_last_accessed(file_id, current_user.id) + + # 读取Excel文件 + if excel_file.file_type.lower() == 'csv': + df = pd.read_csv(excel_file.file_path, encoding='utf-8') + else: + # 对于Excel文件,使用默认sheet或第一个sheet + sheet_name = excel_file.default_sheet if excel_file.default_sheet else 0 + df = pd.read_excel(excel_file.file_path, sheet_name=sheet_name) + + # 计算分页 + total_rows = len(df) + start_idx = (request.page - 1) * request.page_size + end_idx = start_idx + request.page_size + + # 获取分页数据 + paginated_df = df.iloc[start_idx:end_idx] + + # 转换为字典格式 + data = paginated_df.fillna('').to_dict('records') + columns = df.columns.tolist() + + return QueryResponse( + success=True, + message="Excel文件预览加载成功", + data={ + 'data': data, + 'columns': columns, + 'total_rows': total_rows, + 'page': request.page, + 'page_size': request.page_size, + 'total_pages': (total_rows + request.page_size - 1) // request.page_size + } + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"预览文件失败: {str(e)}" + ) + +@router.post("/test-db-connection", response_model=NormalResponse) +async def test_database_connection( + config: DatabaseConfig, + current_user = Depends(AuthService.get_current_user) +): + """ + 测试数据库连接 + """ + try: + db_service = DatabaseQueryService() + is_connected = await db_service.test_connection(config.dict()) + + if is_connected: + return NormalResponse( + success=True, + message="数据库连接测试成功" + ) + else: + return NormalResponse( + success=False, + message="数据库连接测试失败" + ) + + except Exception as e: + return NormalResponse( + success=False, + message=f"连接测试失败: {str(e)}" + ) + +# 删除第285-314行的connect_database方法 +# @router.post("/connect-database", response_model=QueryResponse) +# async def connect_database( +# config_id: int, +# current_user = Depends(AuthService.get_current_user), +# db: Session = Depends(get_db) +# ): +# """连接数据库并获取表列表""" +# ... (整个方法都删除) + +@router.post("/table-schema", response_model=QueryResponse) +async def get_table_schema( + request: TableSchemaRequest, + current_user = Depends(AuthService.get_current_user) +): + """ + 获取数据表结构 + """ + try: + db_service = DatabaseQueryService() + schema_result = await db_service.get_table_schema(request.table_name, current_user.id) + + if schema_result['success']: + return QueryResponse( + success=True, + message="获取表结构成功", + data=schema_result['data'] + ) + else: + return QueryResponse( + success=False, + message=schema_result['message'] + ) + + except Exception as e: + return QueryResponse( + success=False, + message=f"获取表结构失败: {str(e)}" + ) + + +class StreamQueryRequest(BaseModel): + query: str + conversation_id: Optional[int] = None + is_new_conversation: bool = False + +class DatabaseStreamQueryRequest(BaseModel): + query: str + database_config_id: int + conversation_id: Optional[int] = None + is_new_conversation: bool = False + + +@router.post("/execute-excel-query") +async def stream_smart_query( + request: StreamQueryRequest, + current_user=Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 流式智能问答查询接口 + 支持实时推送工作流步骤和最终结果 + """ + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_manager = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容不能为空'}, ensure_ascii=False)}\n\n" + return + + if len(request.query) > 1000: + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容过长,请控制在1000字符以内'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'message': '开始处理查询', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 初始化服务 + workflow_manager = SmartWorkflowManager(db) + conversation_context_service = ConversationContextService() + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"智能问数: {request.query[:20]}..." + ) + yield f"data: {json.dumps({'type': 'conversation_created', 'conversation_id': conversation_id}, ensure_ascii=False)}\n\n" + except Exception as e: + logger.warning(f"创建对话失败: {e}") + # 不阻断流程,继续执行查询 + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + + # 执行智能查询工作流(带流式推送) + async for step_data in workflow_manager.process_excel_query_stream( + user_query=request.query, + user_id=current_user.id, + conversation_id=conversation_id, + is_new_conversation=request.is_new_conversation + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 如果是最终结果,保存到对话历史 + if step_data.get('type') == 'final_result' and conversation_id: + try: + result_data = step_data.get('data', {}) + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result_data.get('summary', '查询完成'), + metadata={ + 'query_result': result_data, + 'workflow_steps': step_data.get('workflow_steps', []), + 'selected_files': result_data.get('used_files', []) + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=result_data.get('used_files', []) + ) + + logger.info(f"查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'complete', 'message': '查询处理完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式智能查询异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'查询执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + finally: + # 清理资源 + if workflow_manager: + try: + workflow_manager.excel_workflow.executor.shutdown(wait=False) + except: + pass + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) + + +@router.post("/execute-db-query") +async def execute_database_query( + request: DatabaseStreamQueryRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 流式数据库查询接口 + 支持实时推送工作流步骤和最终结果 + """ + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_manager = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容不能为空'}, ensure_ascii=False)}\n\n" + return + + if len(request.query) > 1000: + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容过长,请控制在1000字符以内'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'message': '开始处理数据库查询', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 初始化服务 + workflow_manager = SmartWorkflowManager(db) + conversation_context_service = ConversationContextService() + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"数据库查询: {request.query[:20]}..." + ) + yield f"data: {json.dumps({'type': 'conversation_created', 'conversation_id': conversation_id}, ensure_ascii=False)}\n\n" + except Exception as e: + logger.warning(f"创建对话失败: {e}") + # 不阻断流程,继续执行查询 + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + + # 执行数据库查询工作流(带流式推送) + async for step_data in workflow_manager.process_database_query_stream( + user_query=request.query, + user_id=current_user.id, + database_config_id=request.database_config_id + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 如果是最终结果,保存到对话历史 + if step_data.get('type') == 'final_result' and conversation_id: + try: + result_data = step_data.get('data', {}) + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result_data.get('summary', '查询完成'), + metadata={ + 'query_result': result_data, + 'workflow_steps': step_data.get('workflow_steps', []), + 'generated_sql': result_data.get('generated_sql', '') + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=[] + ) + + logger.info(f"数据库查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'complete', 'message': '数据库查询处理完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式数据库查询异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'查询执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + finally: + # 清理资源 + if workflow_manager: + try: + workflow_manager.database_workflow.executor.shutdown(wait=False) + except: + pass + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) + +@router.delete("/cleanup-temp-files") +async def cleanup_temp_files( + current_user = Depends(AuthService.get_current_user) +): + """ + 清理临时文件 + """ + try: + temp_dir = tempfile.gettempdir() + user_prefix = f"excel_{current_user.id}_" + + cleaned_count = 0 + for filename in os.listdir(temp_dir): + if filename.startswith(user_prefix) and filename.endswith('.pkl'): + file_path = os.path.join(temp_dir, filename) + try: + os.remove(file_path) + cleaned_count += 1 + except OSError: + pass + + return BaseResponse( + success=True, + message=f"已清理 {cleaned_count} 个临时文件" + ) + + except Exception as e: + return BaseResponse( + success=False, + message=f"清理临时文件失败: {str(e)}" + ) + +@router.get("/files", response_model=FileListResponse) +async def get_file_list( + page: int = 1, + page_size: int = 20, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取用户上传的Excel文件列表 + """ + try: + metadata_service = ExcelMetadataService(db) + skip = (page - 1) * page_size + files, total = metadata_service.get_user_files(current_user.id, skip, page_size) + + file_list = [] + for file in files: + file_info = { + 'id': file.id, + 'filename': file.original_filename, + 'file_size': file.file_size, + 'file_size_mb': file.file_size_mb, + 'file_type': file.file_type, + 'sheet_names': file.sheet_names, + 'sheet_count': file.sheet_count, + 'last_accessed': file.last_accessed.isoformat() if file.last_accessed else None, + 'is_processed': file.is_processed, + 'processing_error': file.processing_error + } + file_list.append(file_info) + + return FileListResponse( + success=True, + message="获取文件列表成功", + data={ + 'files': file_list, + 'total': total, + 'page': page, + 'page_size': page_size, + 'total_pages': (total + page_size - 1) // page_size + } + ) + + except Exception as e: + return FileListResponse( + success=False, + message=f"获取文件列表失败: {str(e)}" + ) + +@router.delete("/files/{file_id}", response_model=NormalResponse) +async def delete_file( + file_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 删除指定的Excel文件 + """ + try: + metadata_service = ExcelMetadataService(db) + success = metadata_service.delete_file(file_id, current_user.id) + + if success: + return NormalResponse( + success=True, + message="文件删除成功" + ) + else: + return NormalResponse( + success=False, + message="文件不存在或删除失败" + ) + + except Exception as e: + return NormalResponse( + success=True, + message=str(e) + ) + +@router.get("/files/{file_id}/info", response_model=QueryResponse) +async def get_file_info( + file_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取指定文件的详细信息 + """ + try: + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.get_file_by_id(file_id, current_user.id) + + if not excel_file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件不存在" + ) + + # 更新最后访问时间 + metadata_service.update_last_accessed(file_id, current_user.id) + + file_info = { + 'id': excel_file.id, + 'filename': excel_file.original_filename, + 'file_size': excel_file.file_size, + 'file_size_mb': excel_file.file_size_mb, + 'file_type': excel_file.file_type, + 'sheet_names': excel_file.sheet_names, + 'default_sheet': excel_file.default_sheet, + 'columns_info': excel_file.columns_info, + 'preview_data': excel_file.preview_data, + 'data_types': excel_file.data_types, + 'total_rows': excel_file.total_rows, + 'total_columns': excel_file.total_columns, + 'upload_time': excel_file.upload_time.isoformat() if excel_file.upload_time else None, + 'last_accessed': excel_file.last_accessed.isoformat() if excel_file.last_accessed else None, + 'sheets_summary': excel_file.get_all_sheets_summary() + } + + return QueryResponse( + success=True, + message="获取文件信息成功", + data=file_info + ) + + except HTTPException: + raise + except Exception as e: + return QueryResponse( + success=False, + message=f"获取文件信息失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/table_metadata.py b/backend/th_agenter/api/endpoints/table_metadata.py new file mode 100644 index 0000000..95b8785 --- /dev/null +++ b/backend/th_agenter/api/endpoints/table_metadata.py @@ -0,0 +1,248 @@ +"""表元数据管理API""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Dict, Any +from pydantic import BaseModel, Field + +from th_agenter.models.user import User +from th_agenter.db.database import get_db +from th_agenter.services.table_metadata_service import TableMetadataService +from th_agenter.utils.logger import get_logger +from th_agenter.services.auth import AuthService + +logger = get_logger("table_metadata_api") +router = APIRouter(prefix="/api/table-metadata", tags=["table-metadata"]) + + +class TableSelectionRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_names: List[str] = Field(..., description="选中的表名列表") + + +class TableMetadataResponse(BaseModel): + id: int + table_name: str + table_schema: str + table_type: str + table_comment: str + columns_count: int + row_count: int + is_enabled_for_qa: bool + qa_description: str + business_context: str + last_synced_at: str + + +class QASettingsUpdate(BaseModel): + is_enabled_for_qa: bool = Field(default=True) + qa_description: str = Field(default="") + business_context: str = Field(default="") + + +class TableByNameRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_name: str = Field(..., description="表名") + + +@router.post("/collect") +async def collect_table_metadata( + request: TableSelectionRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """收集选中表的元数据""" + try: + service = TableMetadataService(db) + result = await service.collect_and_save_table_metadata( + current_user.id, + request.database_config_id, + request.table_names + ) + return result + except Exception as e: + logger.error(f"收集表元数据失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/") +async def get_table_metadata( + database_config_id: int = None, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取表元数据列表""" + try: + service = TableMetadataService(db) + metadata_list = service.get_user_table_metadata( + current_user.id, + database_config_id + ) + + data = [ + { + "id": meta.id, + "table_name": meta.table_name, + "table_schema": meta.table_schema, + "table_type": meta.table_type, + "table_comment": meta.table_comment or "", + "columns": meta.columns_info if meta.columns_info else [], + "column_count": len(meta.columns_info) if meta.columns_info else 0, + "row_count": meta.row_count, + "is_enabled_for_qa": meta.is_enabled_for_qa, + "qa_description": meta.qa_description or "", + "business_context": meta.business_context or "", + "created_at": meta.created_at.isoformat() if meta.created_at else "", + "updated_at": meta.updated_at.isoformat() if meta.updated_at else "", + "last_synced_at": meta.last_synced_at.isoformat() if meta.last_synced_at else "", + "qa_settings": { + "is_enabled_for_qa": meta.is_enabled_for_qa, + "qa_description": meta.qa_description or "", + "business_context": meta.business_context or "" + } + } + for meta in metadata_list + ] + + return { + "success": True, + "data": data + } + + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + +@router.post("/by-table") +async def get_table_metadata_by_name( + request: TableByNameRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """根据表名获取表元数据""" + try: + service = TableMetadataService(db) + metadata = service.get_table_metadata_by_name( + current_user.id, + request.database_config_id, + request.table_name + ) + + if metadata: + data = { + "id": metadata.id, + "table_name": metadata.table_name, + "table_schema": metadata.table_schema, + "table_type": metadata.table_type, + "table_comment": metadata.table_comment or "", + "columns": metadata.columns_info if metadata.columns_info else [], + "column_count": len(metadata.columns_info) if metadata.columns_info else 0, + "row_count": metadata.row_count, + "is_enabled_for_qa": metadata.is_enabled_for_qa, + "qa_description": metadata.qa_description or "", + "business_context": metadata.business_context or "", + "created_at": metadata.created_at.isoformat() if metadata.created_at else "", + "updated_at": metadata.updated_at.isoformat() if metadata.updated_at else "", + "last_synced_at": metadata.last_synced_at.isoformat() if metadata.last_synced_at else "", + "qa_settings": { + "is_enabled_for_qa": metadata.is_enabled_for_qa, + "qa_description": metadata.qa_description or "", + "business_context": metadata.business_context or "" + } + } + return {"success": True, "data": data} + else: + return {"success": False, "data": None, "message": "表元数据不存在"} + + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + return { + "success": True, + "data": data + } + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + +@router.put("/{metadata_id}/qa-settings") +async def update_qa_settings( + metadata_id: int, + settings: QASettingsUpdate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """更新表的问答设置""" + try: + service = TableMetadataService(db) + success = service.update_table_qa_settings( + current_user.id, + metadata_id, + settings.dict() + ) + + if success: + return {"success": True, "message": "设置更新成功"} + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="表元数据不存在" + ) + except Exception as e: + logger.error(f"更新问答设置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +class TableSaveRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_names: List[str] = Field(..., description="要保存的表名列表") + + +@router.post("/save") +async def save_table_metadata( + request: TableSaveRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """保存选中表的元数据配置""" + try: + service = TableMetadataService(db) + result = await service.save_table_metadata_config( + user_id=current_user.id, + database_config_id=request.database_config_id, + table_names=request.table_names + ) + + logger.info(f"用户 {current_user.id} 保存了 {len(request.table_names)} 个表的配置") + + return { + "success": True, + "message": f"成功保存 {len(result['saved_tables'])} 个表的配置", + "saved_tables": result['saved_tables'], + "failed_tables": result.get('failed_tables', []) + } + + except Exception as e: + logger.error(f"保存表元数据配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"保存配置失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/users.py b/backend/th_agenter/api/endpoints/users.py new file mode 100644 index 0000000..76e01bf --- /dev/null +++ b/backend/th_agenter/api/endpoints/users.py @@ -0,0 +1,241 @@ +"""User management endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...core.simple_permissions import require_super_admin +from ...services.auth import AuthService +from ...services.user import UserService +from ...schemas.user import UserResponse, UserUpdate, UserCreate, ChangePasswordRequest, ResetPasswordRequest + +router = APIRouter() + + +@router.get("/profile", response_model=UserResponse) +async def get_user_profile( + current_user = Depends(AuthService.get_current_user) +): + """Get current user profile.""" + return UserResponse.from_orm(current_user) + + +@router.put("/profile", response_model=UserResponse) +async def update_user_profile( + user_update: UserUpdate, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Update current user profile.""" + user_service = UserService(db) + + # Check if email is being changed and is already taken + if user_update.email and user_update.email != current_user.email: + existing_user = user_service.get_user_by_email(user_update.email) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + # Update user + updated_user = user_service.update_user(current_user.id, user_update) + return UserResponse.from_orm(updated_user) + + +@router.delete("/profile") +async def delete_user_account( + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Delete current user account.""" + user_service = UserService(db) + user_service.delete_user(current_user.id) + return {"message": "Account deleted successfully"} + + +# Admin endpoints +@router.post("/", response_model=UserResponse) +async def create_user( + user_create: UserCreate, + # current_user = Depends(require_superuser), + db: Session = Depends(get_db) +): + """Create a new user (admin only).""" + user_service = UserService(db) + + # Check if username already exists + existing_user = user_service.get_user_by_username(user_create.username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # Check if email already exists + existing_user = user_service.get_user_by_email(user_create.email) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + # Create user + new_user = user_service.create_user(user_create) + return UserResponse.from_orm(new_user) + + +@router.get("/") +async def list_users( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + search: Optional[str] = Query(None), + role_id: Optional[int] = Query(None), + is_active: Optional[bool] = Query(None), + # current_user = Depends(require_superuser), + db: Session = Depends(get_db) +): + """List all users with pagination and filters (admin only).""" + user_service = UserService(db) + skip = (page - 1) * size + users, total = user_service.get_users_with_filters( + skip=skip, + limit=size, + search=search, + role_id=role_id, + is_active=is_active + ) + result = { + "users": [UserResponse.from_orm(user) for user in users], + "total": total, + "page": page, + "page_size": size + } + return result + + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user( + user_id: int, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Get user by ID (admin only).""" + user_service = UserService(db) + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return UserResponse.from_orm(user) + + +@router.put("/change-password") +async def change_password( + request: ChangePasswordRequest, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Change current user's password.""" + user_service = UserService(db) + + try: + user_service.change_password( + user_id=current_user.id, + current_password=request.current_password, + new_password=request.new_password + ) + return {"message": "Password changed successfully"} + except Exception as e: + if "Current password is incorrect" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Current password is incorrect" + ) + elif "must be at least 6 characters" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="New password must be at least 6 characters long" + ) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to change password" + ) + + +@router.put("/{user_id}/reset-password") +async def reset_user_password( + user_id: int, + request: ResetPasswordRequest, + current_user = Depends(require_super_admin), + db: Session = Depends(get_db) +): + """Reset user password (admin only).""" + user_service = UserService(db) + + try: + user_service.reset_password( + user_id=user_id, + new_password=request.new_password + ) + return {"message": "Password reset successfully"} + except Exception as e: + if "User not found" in str(e): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + elif "must be at least 6 characters" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="New password must be at least 6 characters long" + ) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to reset password" + ) + + +@router.put("/{user_id}", response_model=UserResponse) +async def update_user( + user_id: int, + user_update: UserUpdate, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Update user by ID (admin only).""" + user_service = UserService(db) + + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + updated_user = user_service.update_user(user_id, user_update) + return UserResponse.from_orm(updated_user) + + +@router.delete("/{user_id}") +async def delete_user( + user_id: int, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Delete user by ID (admin only).""" + user_service = UserService(db) + + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + user_service.delete_user(user_id) + return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/workflow.py b/backend/th_agenter/api/endpoints/workflow.py new file mode 100644 index 0000000..9fd7f54 --- /dev/null +++ b/backend/th_agenter/api/endpoints/workflow.py @@ -0,0 +1,538 @@ +"""工作流管理API""" + +from typing import List, Optional, AsyncGenerator +from fastapi import APIRouter, Depends, HTTPException, status, Query +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from sqlalchemy import and_ +import json +from datetime import datetime + +from ...db.database import get_db +from ...schemas.workflow import ( + WorkflowCreate, WorkflowUpdate, WorkflowResponse, WorkflowListResponse, + WorkflowExecuteRequest, WorkflowExecutionResponse, NodeExecutionResponse, WorkflowStatus +) +from ...models.workflow import WorkflowStatus as ModelWorkflowStatus +from ...services.workflow_engine import get_workflow_engine +from ...services.auth import AuthService +from ...models.user import User +from ...utils.logger import get_logger + +logger = get_logger("workflow_api") + +router = APIRouter() + +def convert_workflow_for_response(workflow_dict): + """转换工作流数据以适配响应模型""" + if workflow_dict.get('definition') and workflow_dict['definition'].get('connections'): + for conn in workflow_dict['definition']['connections']: + if 'from_node' in conn: + conn['from'] = conn.pop('from_node') + if 'to_node' in conn: + conn['to'] = conn.pop('to_node') + return workflow_dict + +@router.post("/", response_model=WorkflowResponse) +async def create_workflow( + workflow_data: WorkflowCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """创建工作流""" + try: + from ...models.workflow import Workflow + + # 创建工作流 + workflow = Workflow( + name=workflow_data.name, + description=workflow_data.description, + definition=workflow_data.definition.dict(), + version="1.0.0", + status=workflow_data.status, + owner_id=current_user.id + ) + workflow.set_audit_fields(current_user.id) + + db.add(workflow) + db.commit() + db.refresh(workflow) + + # 转换definition中的字段映射 + workflow_dict = convert_workflow_for_response(workflow.to_dict()) + + logger.info(f"Created workflow: {workflow.name} by user {current_user.username}") + return WorkflowResponse(**workflow_dict) + + except Exception as e: + db.rollback() + logger.error(f"Error creating workflow: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建工作流失败" + ) + +@router.get("/", response_model=WorkflowListResponse) +async def list_workflows( + skip: Optional[int] = Query(None, ge=0), + limit: Optional[int] = Query(None, ge=1, le=100), + workflow_status: Optional[WorkflowStatus] = None, + search: Optional[str] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流列表""" + try: + from ...models.workflow import Workflow + + # 构建查询 + query = db.query(Workflow).filter(Workflow.owner_id == current_user.id) + + if workflow_status: + query = query.filter(Workflow.status == workflow_status) + + # 添加搜索功能 + if search: + query = query.filter(Workflow.name.ilike(f"%{search}%")) + + # 获取总数 + total = query.count() + + # 如果没有传分页参数,返回所有数据 + if skip is None and limit is None: + workflows = query.all() + return WorkflowListResponse( + workflows=[WorkflowResponse(**convert_workflow_for_response(w.to_dict())) for w in workflows], + total=total, + page=1, + size=total + ) + + # 使用默认分页参数 + if skip is None: + skip = 0 + if limit is None: + limit = 10 + + # 分页查询 + workflows = query.offset(skip).limit(limit).all() + + return WorkflowListResponse( + workflows=[WorkflowResponse(**convert_workflow_for_response(w.to_dict())) for w in workflows], + total=total, + page=skip // limit + 1, # 计算页码 + size=limit + ) + + except Exception as e: + logger.error(f"Error listing workflows: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取工作流列表失败" + ) + +@router.get("/{workflow_id}", response_model=WorkflowResponse) +async def get_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流详情""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + return WorkflowResponse(**convert_workflow_for_response(workflow.to_dict())) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取工作流失败" + ) + +@router.put("/{workflow_id}", response_model=WorkflowResponse) +async def update_workflow( + workflow_id: int, + workflow_data: WorkflowUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """更新工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + # 更新字段 + update_data = workflow_data.dict(exclude_unset=True) + for field, value in update_data.items(): + if field == "definition" and value: + # 如果value是Pydantic模型,转换为字典;如果已经是字典,直接使用 + if hasattr(value, 'dict'): + setattr(workflow, field, value.dict()) + else: + setattr(workflow, field, value) + else: + setattr(workflow, field, value) + + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(workflow) + + logger.info(f"Updated workflow: {workflow.name} by user {current_user.username}") + return WorkflowResponse(**convert_workflow_for_response(workflow.to_dict())) + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新工作流失败" + ) + +@router.delete("/{workflow_id}") +async def delete_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """删除工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + db.delete(workflow) + db.commit() + + logger.info(f"Deleted workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流删除成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除工作流失败" + ) + +@router.post("/{workflow_id}/activate") +async def activate_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """激活工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + workflow.status = ModelWorkflowStatus.PUBLISHED + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + + logger.info(f"Activated workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流激活成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error activating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="激活工作流失败" + ) + +@router.post("/{workflow_id}/deactivate") +async def deactivate_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """停用工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + workflow.status = ModelWorkflowStatus.ARCHIVED + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + + logger.info(f"Deactivated workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流停用成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deactivating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="停用工作流失败" + ) + +@router.post("/{workflow_id}/execute", response_model=WorkflowExecutionResponse) +async def execute_workflow( + workflow_id: int, + request: WorkflowExecuteRequest, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """执行工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + if workflow.status != ModelWorkflowStatus.PUBLISHED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="工作流未激活,无法执行" + ) + + # 获取工作流引擎并执行 + engine = get_workflow_engine() + execution_result = await engine.execute_workflow( + workflow=workflow, + input_data=request.input_data, + user_id=current_user.id, + db=db + ) + + logger.info(f"Executed workflow: {workflow.name} by user {current_user.username}") + return execution_result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error executing workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"执行工作流失败: {str(e)}" + ) + +@router.get("/{workflow_id}/executions", response_model=List[WorkflowExecutionResponse]) +async def list_workflow_executions( + workflow_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流执行历史""" + try: + from ...models.workflow import Workflow, WorkflowExecution + + # 验证工作流所有权 + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + # 获取执行历史 + executions = db.query(WorkflowExecution).filter( + WorkflowExecution.workflow_id == workflow_id + ).order_by(WorkflowExecution.created_at.desc()).offset(skip).limit(limit).all() + + return [WorkflowExecutionResponse.from_orm(execution) for execution in executions] + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error listing workflow executions {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取执行历史失败" + ) + +@router.get("/executions/{execution_id}", response_model=WorkflowExecutionResponse) +async def get_workflow_execution( + execution_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流执行详情""" + try: + from ...models.workflow import WorkflowExecution, Workflow + + execution = db.query(WorkflowExecution).join( + Workflow, WorkflowExecution.workflow_id == Workflow.id + ).filter( + and_( + WorkflowExecution.id == execution_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not execution: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="执行记录不存在" + ) + + return WorkflowExecutionResponse.from_orm(execution) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting workflow execution {execution_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取执行详情失败" + ) + + +@router.post("/{workflow_id}/execute-stream") +async def execute_workflow_stream( + workflow_id: int, + request: WorkflowExecuteRequest, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """流式执行工作流,实时推送节点执行状态""" + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_engine = None + + try: + from ...models.workflow import Workflow + + # 验证工作流 + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + yield f"data: {json.dumps({'type': 'error', 'message': '工作流不存在'}, ensure_ascii=False)}\n\n" + return + + if workflow.status != ModelWorkflowStatus.PUBLISHED: + yield f"data: {json.dumps({'type': 'error', 'message': '工作流未激活,无法执行'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'workflow_start', 'workflow_id': workflow_id, 'workflow_name': workflow.name, 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 获取工作流引擎 + workflow_engine = get_workflow_engine() + + # 执行工作流(流式版本) + async for step_data in workflow_engine.execute_workflow_stream( + workflow=workflow, + input_data=request.input_data, + user_id=current_user.id, + db=db + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'workflow_complete', 'message': '工作流执行完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式工作流执行异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'工作流执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) \ No newline at end of file diff --git a/backend/th_agenter/api/routes.py b/backend/th_agenter/api/routes.py new file mode 100644 index 0000000..d5dcece --- /dev/null +++ b/backend/th_agenter/api/routes.py @@ -0,0 +1,101 @@ +"""Main API router.""" + +from fastapi import APIRouter +from .endpoints import chat + + +# TODO: Add other routers when implemented +from .endpoints import auth +from .endpoints import knowledge_base +from .endpoints import smart_query +from .endpoints import smart_chat + +from .endpoints import database_config +from .endpoints import table_metadata + +# System management endpoints +from .endpoints import roles +from .endpoints import llm_configs +from .endpoints import users + +# Workflow endpoints +from .endpoints import workflow + + +# Create main API router +router = APIRouter() + +router.include_router( + auth.router, + prefix="/auth", + tags=["authentication"] +) + +# Include sub-routers +router.include_router( + chat.router, + prefix="/chat", + tags=["chat"] +) + + + +router.include_router( + knowledge_base.router, + prefix="/knowledge-bases", + tags=["knowledge-bases"] +) + +router.include_router( + smart_query.router, + tags=["smart-query"] +) + +router.include_router( + smart_chat.router, + tags=["smart-chat"] +) + + + + + +router.include_router( + database_config.router, + tags=["database-config"] +) + +router.include_router( + table_metadata.router, + tags=["table-metadata"] +) + +# System management routers +router.include_router( + roles.router, + prefix="/admin", + tags=["admin-roles"] +) + +router.include_router( + llm_configs.router, + prefix="/admin", + tags=["admin-llm-configs"] +) + +router.include_router( + users.router, + prefix="/users", + tags=["users"] +) + +router.include_router( + workflow.router, + prefix="/workflows", + tags=["workflows"] +) + +# Test endpoint +@router.get("/test") +async def test_endpoint(): + return {"message": "API test is working"} \ No newline at end of file diff --git a/backend/th_agenter/core/__init__.py b/backend/th_agenter/core/__init__.py new file mode 100644 index 0000000..cacaf5f --- /dev/null +++ b/backend/th_agenter/core/__init__.py @@ -0,0 +1 @@ +"""Core module for TH-Agenter.""" \ No newline at end of file diff --git a/backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..91dca0d Binary files /dev/null and b/backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/app.cpython-313.pyc b/backend/th_agenter/core/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..b543f4e Binary files /dev/null and b/backend/th_agenter/core/__pycache__/app.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/config.cpython-313.pyc b/backend/th_agenter/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..86e4932 Binary files /dev/null and b/backend/th_agenter/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/context.cpython-313.pyc b/backend/th_agenter/core/__pycache__/context.cpython-313.pyc new file mode 100644 index 0000000..e811a84 Binary files /dev/null and b/backend/th_agenter/core/__pycache__/context.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/llm.cpython-313.pyc b/backend/th_agenter/core/__pycache__/llm.cpython-313.pyc new file mode 100644 index 0000000..54dedc0 Binary files /dev/null and b/backend/th_agenter/core/__pycache__/llm.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/logging.cpython-313.pyc b/backend/th_agenter/core/__pycache__/logging.cpython-313.pyc new file mode 100644 index 0000000..e7878f2 Binary files /dev/null and b/backend/th_agenter/core/__pycache__/logging.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/middleware.cpython-313.pyc b/backend/th_agenter/core/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000..aac375d Binary files /dev/null and b/backend/th_agenter/core/__pycache__/middleware.cpython-313.pyc differ diff --git a/backend/th_agenter/core/__pycache__/simple_permissions.cpython-313.pyc b/backend/th_agenter/core/__pycache__/simple_permissions.cpython-313.pyc new file mode 100644 index 0000000..ac7ff43 Binary files /dev/null and b/backend/th_agenter/core/__pycache__/simple_permissions.cpython-313.pyc differ diff --git a/backend/th_agenter/core/app.py b/backend/th_agenter/core/app.py new file mode 100644 index 0000000..33cbd4e --- /dev/null +++ b/backend/th_agenter/core/app.py @@ -0,0 +1,177 @@ +"""FastAPI application factory.""" + +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException + +from .config import Settings +from .logging import setup_logging +from .middleware import UserContextMiddleware +from ..api.routes import router +from ..db.database import init_db +from ..api.endpoints import table_metadata + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager.""" + # Startup + logging.info("Starting up TH-Agenter application...") + await init_db() + logging.info("Database initialized") + + yield + + # Shutdown + logging.info("Shutting down TH-Agenter application...") + + +def create_app(settings: Settings = None) -> FastAPI: + """Create and configure FastAPI application.""" + if settings is None: + from .config import get_settings + settings = get_settings() + + # Setup logging + setup_logging(settings.logging) + + # Create FastAPI app + app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description="A modern chat agent application with Vue frontend and FastAPI backend", + debug=settings.debug, + lifespan=lifespan, + ) + + # Add middleware + setup_middleware(app, settings) + + # Add exception handlers + setup_exception_handlers(app) + + # Include routers + app.include_router(router, prefix="/api") + + + + app.include_router(table_metadata.router) + # 在现有导入中添加 + from ..api.endpoints import database_config + + # 在路由注册部分添加 + app.include_router(database_config.router) + # Health check endpoint + @app.get("/health") + async def health_check(): + return {"status": "healthy", "version": settings.app_version} + + # Root endpoint + @app.get("/") + async def root(): + return {"message": "Chat Agent API is running"} + + # Test endpoint + @app.get("/test") + async def test_endpoint(): + return {"message": "API is working"} + + return app + + +def setup_middleware(app: FastAPI, settings: Settings) -> None: + """Setup application middleware.""" + + # User context middleware (should be first to set context for all requests) + app.add_middleware(UserContextMiddleware) + + # CORS middleware + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors.allowed_origins, + allow_credentials=True, + allow_methods=settings.cors.allowed_methods, + allow_headers=settings.cors.allowed_headers, + ) + + # Trusted host middleware (for production) + if settings.environment == "production": + app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["*"] # Configure this properly in production + ) + + +def setup_exception_handlers(app: FastAPI) -> None: + """Setup global exception handlers.""" + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request, exc): + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "type": "http_error", + "message": exc.detail, + "status_code": exc.status_code + } + } + ) + + def make_json_serializable(obj): + """递归地将对象转换为JSON可序列化的格式""" + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + elif isinstance(obj, bytes): + return obj.decode('utf-8') + elif isinstance(obj, (ValueError, Exception)): + return str(obj) + elif isinstance(obj, dict): + return {k: make_json_serializable(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [make_json_serializable(item) for item in obj] + else: + # For any other object, convert to string + return str(obj) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request, exc): + # Convert any non-serializable objects to strings in error details + try: + errors = make_json_serializable(exc.errors()) + except Exception as e: + # Fallback: if even our conversion fails, use a simple error message + errors = [{"type": "serialization_error", "msg": f"Error processing validation details: {str(e)}"}] + + return JSONResponse( + status_code=422, + content={ + "error": { + "type": "validation_error", + "message": "Request validation failed", + "details": errors + } + } + ) + + @app.exception_handler(Exception) + async def general_exception_handler(request, exc): + logging.error(f"Unhandled exception: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={ + "error": { + "type": "internal_error", + "message": "Internal server error" + } + } + ) + + +# Create the app instance +app = create_app() \ No newline at end of file diff --git a/backend/th_agenter/core/config.py b/backend/th_agenter/core/config.py new file mode 100644 index 0000000..dc76123 --- /dev/null +++ b/backend/th_agenter/core/config.py @@ -0,0 +1,482 @@ +"""Configuration management for TH-Agenter.""" + +import os +import yaml +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class DatabaseSettings(BaseSettings): + """Database configuration.""" + url: str = Field(..., alias="database_url") # Must be provided via environment variable + echo: bool = Field(default=False) + pool_size: int = Field(default=5) + max_overflow: int = Field(default=10) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class SecuritySettings(BaseSettings): + """Security configuration.""" + secret_key: str = Field(default="your-secret-key-here-change-in-production") + algorithm: str = Field(default="HS256") + access_token_expire_minutes: int = Field(default=300) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + +class ToolSetings(BaseSettings): + # Tavily搜索配置 + tavily_api_key: Optional[str] = Field(default=None) + weather_api_key: Optional[str] = Field(default=None) + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } +class LLMSettings(BaseSettings): + """大模型配置 - 支持多种OpenAI协议兼容的服务商.""" + provider: str = Field(default="openai", alias="llm_provider") # openai, deepseek, doubao, zhipu, moonshot + + # OpenAI配置 + openai_api_key: Optional[str] = Field(default=None) + openai_base_url: str = Field(default="https://api.openai.com/v1") + openai_model: str = Field(default="gpt-3.5-turbo") + + # DeepSeek配置 + deepseek_api_key: Optional[str] = Field(default=None) + deepseek_base_url: str = Field(default="https://api.deepseek.com/v1") + deepseek_model: str = Field(default="deepseek-chat") + + # 豆包配置 + doubao_api_key: Optional[str] = Field(default=None) + doubao_base_url: str = Field(default="https://ark.cn-beijing.volces.com/api/v3") + doubao_model: str = Field(default="doubao-lite-4k") + + # 智谱AI配置 + zhipu_api_key: Optional[str] = Field(default=None) + zhipu_base_url: str = Field(default="https://open.bigmodel.cn/api/paas/v4") + zhipu_model: str = Field(default="glm-4") + zhipu_embedding_model: str = Field(default="embedding-3") + + # 月之暗面配置 + moonshot_api_key: Optional[str] = Field(default=None) + moonshot_base_url: str = Field(default="https://api.moonshot.cn/v1") + moonshot_model: str = Field(default="moonshot-v1-8k") + + # 通用配置 + max_tokens: int = Field(default=2048) + temperature: float = Field(default=0.7) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + def get_current_config(self) -> dict: + """获取当前选择的提供商配置 - 优先从数据库读取默认配置.""" + try: + # 尝试从数据库读取默认聊天模型配置 + from th_agenter.services.llm_config_service import LLMConfigService + llm_service = LLMConfigService() + db_config = llm_service.get_default_chat_config() + + if db_config: + # 如果数据库中有默认配置,使用数据库配置 + config = { + "api_key": db_config.api_key, + "base_url": db_config.base_url, + "model": db_config.model_name, + "max_tokens": self.max_tokens, + "temperature": self.temperature + } + return config + except Exception as e: + # 如果数据库读取失败,记录错误并回退到环境变量 + import logging + logging.warning(f"Failed to read LLM config from database, falling back to env vars: {e}") + + # 回退到原有的环境变量配置 + provider_configs = { + "openai": { + "api_key": self.openai_api_key, + "base_url": self.openai_base_url, + "model": self.openai_model + }, + "deepseek": { + "api_key": self.deepseek_api_key, + "base_url": self.deepseek_base_url, + "model": self.deepseek_model + }, + "doubao": { + "api_key": self.doubao_api_key, + "base_url": self.doubao_base_url, + "model": self.doubao_model + }, + "zhipu": { + "api_key": self.zhipu_api_key, + "base_url": self.zhipu_base_url, + "model": self.zhipu_model + }, + "moonshot": { + "api_key": self.moonshot_api_key, + "base_url": self.moonshot_base_url, + "model": self.moonshot_model + } + } + + config = provider_configs.get(self.provider, provider_configs["openai"]) + config.update({ + "max_tokens": self.max_tokens, + "temperature": self.temperature + }) + return config + + +class EmbeddingSettings(BaseSettings): + """Embedding模型配置 - 支持多种提供商.""" + provider: str = Field(default="zhipu", alias="embedding_provider") # openai, deepseek, doubao, zhipu, moonshot + + # OpenAI配置 + openai_api_key: Optional[str] = Field(default=None) + openai_base_url: str = Field(default="https://api.openai.com/v1") + openai_embedding_model: str = Field(default="text-embedding-ada-002") + + # DeepSeek配置 + deepseek_api_key: Optional[str] = Field(default=None) + deepseek_base_url: str = Field(default="https://api.deepseek.com/v1") + deepseek_embedding_model: str = Field(default="deepseek-embedding") + + # 豆包配置 + doubao_api_key: Optional[str] = Field(default=None) + doubao_base_url: str = Field(default="https://ark.cn-beijing.volces.com/api/v3") + doubao_embedding_model: str = Field(default="doubao-embedding") + + # 智谱AI配置 + zhipu_api_key: Optional[str] = Field(default=None) + zhipu_base_url: str = Field(default="https://open.bigmodel.cn/api/paas/v4") + zhipu_embedding_model: str = Field(default="embedding-3") + + # 月之暗面配置 + moonshot_api_key: Optional[str] = Field(default=None) + moonshot_base_url: str = Field(default="https://api.moonshot.cn/v1") + moonshot_embedding_model: str = Field(default="moonshot-embedding") + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + def get_current_config(self) -> dict: + """获取当前选择的embedding提供商配置 - 优先从数据库读取默认配置.""" + try: + # 尝试从数据库读取默认嵌入模型配置 + from th_agenter.services.llm_config_service import LLMConfigService + llm_service = LLMConfigService() + db_config = llm_service.get_default_embedding_config() + + if db_config: + # 如果数据库中有默认配置,使用数据库配置 + config = { + "api_key": db_config.api_key, + "base_url": db_config.base_url, + "model": db_config.model_name + } + return config + except Exception as e: + # 如果数据库读取失败,记录错误并回退到环境变量 + import logging + logging.warning(f"Failed to read embedding config from database, falling back to env vars: {e}") + + # 回退到原有的环境变量配置 + provider_configs = { + "openai": { + "api_key": self.openai_api_key, + "base_url": self.openai_base_url, + "model": self.openai_embedding_model + }, + "deepseek": { + "api_key": self.deepseek_api_key, + "base_url": self.deepseek_base_url, + "model": self.deepseek_embedding_model + }, + "doubao": { + "api_key": self.doubao_api_key, + "base_url": self.doubao_base_url, + "model": self.doubao_embedding_model + }, + "zhipu": { + "api_key": self.zhipu_api_key, + "base_url": self.zhipu_base_url, + "model": self.zhipu_embedding_model + }, + "moonshot": { + "api_key": self.moonshot_api_key, + "base_url": self.moonshot_base_url, + "model": self.moonshot_embedding_model + } + } + + return provider_configs.get(self.provider, provider_configs["zhipu"]) + + +class VectorDBSettings(BaseSettings): + """Vector database configuration.""" + type: str = Field(default="pgvector", alias="vector_db_type") + persist_directory: str = Field(default="./data/chroma") + collection_name: str = Field(default="documents") + embedding_dimension: int = Field(default=2048) # 智谱AI embedding-3模型的维度 + + # PostgreSQL pgvector configuration + pgvector_host: str = Field(default="localhost") + pgvector_port: int = Field(default=5432) + pgvector_database: str = Field(default="vectordb") + pgvector_user: str = Field(default="postgres") + pgvector_password: str = Field(default="") + pgvector_table_name: str = Field(default="embeddings") + pgvector_vector_dimension: int = Field(default=1024) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class FileSettings(BaseSettings): + """File processing configuration.""" + upload_dir: str = Field(default="./data/uploads") + max_size: int = Field(default=10485760) # 10MB + allowed_extensions: Union[str, List[str]] = Field(default=[".txt", ".pdf", ".docx", ".md"]) + chunk_size: int = Field(default=1000) + chunk_overlap: int = Field(default=200) + semantic_splitter_enabled: bool = Field(default=False) # 是否启用语义分割器 + + @field_validator('allowed_extensions', mode='before') + @classmethod + def parse_allowed_extensions(cls, v): + """Parse comma-separated string to list of extensions.""" + if isinstance(v, str): + # Split by comma and add dots if not present + extensions = [ext.strip() for ext in v.split(',')] + return [ext if ext.startswith('.') else f'.{ext}' for ext in extensions] + elif isinstance(v, list): + # Ensure all extensions start with dot + return [ext if ext.startswith('.') else f'.{ext}' for ext in v] + return v + + def get_allowed_extensions_list(self) -> List[str]: + """Get allowed extensions as a list.""" + if isinstance(self.allowed_extensions, list): + return self.allowed_extensions + elif isinstance(self.allowed_extensions, str): + # Split by comma and add dots if not present + extensions = [ext.strip() for ext in self.allowed_extensions.split(',')] + return [ext if ext.startswith('.') else f'.{ext}' for ext in extensions] + return [] + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class StorageSettings(BaseSettings): + """Storage configuration.""" + storage_type: str = Field(default="local") # local or s3 + upload_directory: str = Field(default="./data/uploads") + + # S3 settings + s3_bucket_name: str = Field(default="chat-agent-files") + aws_access_key_id: Optional[str] = Field(default=None) + aws_secret_access_key: Optional[str] = Field(default=None) + aws_region: str = Field(default="us-east-1") + s3_endpoint_url: Optional[str] = Field(default=None) # For S3-compatible services + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class LoggingSettings(BaseSettings): + """Logging configuration.""" + level: str = Field(default="INFO") + file: str = Field(default="./data/logs/app.log") + format: str = Field(default="%(asctime)s - %(name)s - %(levelname)s - %(message)s") + max_bytes: int = Field(default=10485760) # 10MB + backup_count: int = Field(default=5) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class CORSSettings(BaseSettings): + """CORS configuration.""" + allowed_origins: List[str] = Field(default=["*"]) + allowed_methods: List[str] = Field(default=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + allowed_headers: List[str] = Field(default=["*"]) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class ChatSettings(BaseSettings): + """Chat configuration.""" + max_history_length: int = Field(default=10) + system_prompt: str = Field(default="你是一个有用的AI助手,请根据提供的上下文信息回答用户的问题。") + max_response_tokens: int = Field(default=1000) + + +class Settings(BaseSettings): + """Main application settings.""" + + # App info + app_name: str = Field(default="TH-Agenter") + app_version: str = Field(default="0.1.0") + debug: bool = Field(default=True) + environment: str = Field(default="development") + + # Server + host: str = Field(default="0.0.0.0") + port: int = Field(default=8000) + + # Configuration sections + database: DatabaseSettings = Field(default_factory=DatabaseSettings) + security: SecuritySettings = Field(default_factory=SecuritySettings) + llm: LLMSettings = Field(default_factory=LLMSettings) + embedding: EmbeddingSettings = Field(default_factory=EmbeddingSettings) + vector_db: VectorDBSettings = Field(default_factory=VectorDBSettings) + file: FileSettings = Field(default_factory=FileSettings) + storage: StorageSettings = Field(default_factory=StorageSettings) + logging: LoggingSettings = Field(default_factory=LoggingSettings) + cors: CORSSettings = Field(default_factory=CORSSettings) + chat: ChatSettings = Field(default_factory=ChatSettings) + tool: ToolSetings = Field(default_factory=ToolSetings) + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + @classmethod + def load_from_yaml(cls, config_path: str = "../configs/settings.yaml") -> "Settings": + """Load settings from YAML file.""" + config_file = Path(config_path) + + # 如果配置文件不存在,尝试从backend目录查找 + if not config_file.exists(): + # 获取当前文件所在目录(backend/th_agenter/core) + current_dir = Path(__file__).parent + # 向上两级到backend目录,然后找configs/settings.yaml + backend_config_path = current_dir.parent.parent / "configs" / "settings.yaml" + if backend_config_path.exists(): + config_file = backend_config_path + else: + return cls() + + with open(config_file, "r", encoding="utf-8") as f: + config_data = yaml.safe_load(f) or {} + + # 处理环境变量替换 + config_data = cls._resolve_env_vars_nested(config_data) + + # 为每个子设置类创建实例,确保它们能正确加载环境变量 + # 如果YAML中没有对应配置,则使用默认的BaseSettings加载(会自动读取.env文件) + settings_kwargs = {} + + # 显式处理各个子设置,以解决debug等情况因为环境的变化没有自动加载.env配置的问题 + settings_kwargs['database'] = DatabaseSettings(**(config_data.get('database', {}))) + settings_kwargs['security'] = SecuritySettings(**(config_data.get('security', {}))) + settings_kwargs['llm'] = LLMSettings(**(config_data.get('llm', {}))) + settings_kwargs['embedding'] = EmbeddingSettings(**(config_data.get('embedding', {}))) + settings_kwargs['vector_db'] = VectorDBSettings(**(config_data.get('vector_db', {}))) + settings_kwargs['file'] = FileSettings(**(config_data.get('file', {}))) + settings_kwargs['storage'] = StorageSettings(**(config_data.get('storage', {}))) + settings_kwargs['logging'] = LoggingSettings(**(config_data.get('logging', {}))) + settings_kwargs['cors'] = CORSSettings(**(config_data.get('cors', {}))) + settings_kwargs['chat'] = ChatSettings(**(config_data.get('chat', {}))) + settings_kwargs['tool'] = ToolSetings(**(config_data.get('tool', {}))) + + # 添加顶级配置 + for key, value in config_data.items(): + if key not in settings_kwargs: + settings_kwargs[key] = value + + return cls(**settings_kwargs) + + @staticmethod + def _flatten_config(config: Dict[str, Any], prefix: str = "") -> Dict[str, Any]: + """Flatten nested configuration dictionary.""" + flat = {} + for key, value in config.items(): + new_key = f"{prefix}_{key}" if prefix else key + if isinstance(value, dict): + flat.update(Settings._flatten_config(value, new_key)) + else: + flat[new_key] = value + return flat + + @staticmethod + def _resolve_env_vars_nested(config: Dict[str, Any]) -> Dict[str, Any]: + """Resolve environment variables in nested configuration.""" + if isinstance(config, dict): + return {key: Settings._resolve_env_vars_nested(value) for key, value in config.items()} + elif isinstance(config, str) and config.startswith("${") and config.endswith("}"): + env_var = config[2:-1] + return os.getenv(env_var, config) + else: + return config + + @staticmethod + def _resolve_env_vars(config: Dict[str, Any]) -> Dict[str, Any]: + """Resolve environment variables in configuration values.""" + resolved = {} + for key, value in config.items(): + if isinstance(value, str) and value.startswith("${") and value.endswith("}"): + env_var = value[2:-1] + resolved[key] = os.getenv(env_var, value) + else: + resolved[key] = value + return resolved + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings.load_from_yaml() + + +# Global settings instance +settings = get_settings() \ No newline at end of file diff --git a/backend/th_agenter/core/context.py b/backend/th_agenter/core/context.py new file mode 100644 index 0000000..debd894 --- /dev/null +++ b/backend/th_agenter/core/context.py @@ -0,0 +1,120 @@ +""" +HTTP请求上下文管理,如:获取当前登录用户信息及Token信息 +""" + +from contextvars import ContextVar +from typing import Optional +import threading +from ..models.user import User + +# Context variable to store current user +current_user_context: ContextVar[Optional[User]] = ContextVar('current_user', default=None) + +# Thread-local storage as backup +_thread_local = threading.local() + + +class UserContext: + """User context manager for accessing current user globally.""" + + @staticmethod + def set_current_user(user: User) -> None: + """Set current user in context.""" + import logging + logging.info(f"Setting user in context: {user.username} (ID: {user.id})") + + # Set in ContextVar + current_user_context.set(user) + + # Also set in thread-local as backup + _thread_local.current_user = user + + # Verify it was set + verify_user = current_user_context.get() + logging.info(f"Verification - ContextVar user: {verify_user.username if verify_user else None}") + + @staticmethod + def set_current_user_with_token(user: User): + """Set current user in context and return token for cleanup.""" + import logging + logging.info(f"Setting user in context with token: {user.username} (ID: {user.id})") + + # Set in ContextVar and get token + token = current_user_context.set(user) + + # Also set in thread-local as backup + _thread_local.current_user = user + + # Verify it was set + verify_user = current_user_context.get() + logging.info(f"Verification - ContextVar user: {verify_user.username if verify_user else None}") + + return token + + @staticmethod + def reset_current_user_token(token): + """Reset current user context using token.""" + import logging + logging.info("Resetting user context using token") + + # Reset ContextVar using token + current_user_context.reset(token) + + # Clear thread-local as well + if hasattr(_thread_local, 'current_user'): + delattr(_thread_local, 'current_user') + + @staticmethod + def get_current_user() -> Optional[User]: + """Get current user from context.""" + import logging + + # Try ContextVar first + user = current_user_context.get() + if user: + logging.debug(f"Got user from ContextVar: {user.username} (ID: {user.id})") + return user + + # Fallback to thread-local + user = getattr(_thread_local, 'current_user', None) + if user: + logging.debug(f"Got user from thread-local: {user.username} (ID: {user.id})") + return user + + logging.debug("No user found in context (neither ContextVar nor thread-local)") + return None + + @staticmethod + def get_current_user_id() -> Optional[int]: + """Get current user ID from context.""" + user = UserContext.get_current_user() + return user.id if user else None + + @staticmethod + def clear_current_user() -> None: + """Clear current user from context.""" + import logging + logging.info("Clearing user context") + + current_user_context.set(None) + if hasattr(_thread_local, 'current_user'): + delattr(_thread_local, 'current_user') + + @staticmethod + def require_current_user() -> User: + """Get current user from context, raise exception if not found.""" + # Use the same logic as get_current_user to check both ContextVar and thread-local + user = UserContext.get_current_user() + if user is None: + from fastapi import HTTPException, status + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="No authenticated user in context" + ) + return user + + @staticmethod + def require_current_user_id() -> int: + """Get current user ID from context, raise exception if not found.""" + user = UserContext.require_current_user() + return user.id \ No newline at end of file diff --git a/backend/th_agenter/core/exceptions.py b/backend/th_agenter/core/exceptions.py new file mode 100644 index 0000000..4791128 --- /dev/null +++ b/backend/th_agenter/core/exceptions.py @@ -0,0 +1,52 @@ +"""Custom exceptions for the application.""" + +from typing import Any, Dict, Optional + + +class BaseCustomException(Exception): + """Base custom exception class.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + self.message = message + self.details = details or {} + super().__init__(self.message) + + +class NotFoundError(BaseCustomException): + """Exception raised when a resource is not found.""" + pass + + +class ValidationError(BaseCustomException): + """Exception raised when validation fails.""" + pass + + +class AuthenticationError(BaseCustomException): + """Exception raised when authentication fails.""" + pass + + +class AuthorizationError(BaseCustomException): + """Exception raised when authorization fails.""" + pass + + +class DatabaseError(BaseCustomException): + """Exception raised when database operations fail.""" + pass + + +class ConfigurationError(BaseCustomException): + """Exception raised when configuration is invalid.""" + pass + + +class ExternalServiceError(BaseCustomException): + """Exception raised when external service calls fail.""" + pass + + +class BusinessLogicError(BaseCustomException): + """Exception raised when business logic validation fails.""" + pass \ No newline at end of file diff --git a/backend/th_agenter/core/llm.py b/backend/th_agenter/core/llm.py new file mode 100644 index 0000000..c9d4b30 --- /dev/null +++ b/backend/th_agenter/core/llm.py @@ -0,0 +1,47 @@ +"""LLM工厂类,用于创建和管理LLM实例""" + +from typing import Optional +from langchain_openai import ChatOpenAI +from .config import get_settings + +def create_llm(model: Optional[str] = None, temperature: Optional[float] = None, streaming: bool = False) -> ChatOpenAI: + """创建LLM实例 + + Args: + model: 可选,指定使用的模型名称。如果不指定,将使用配置文件中的默认模型 + temperature: 可选,模型温度参数 + streaming: 是否启用流式响应,默认False + + Returns: + ChatOpenAI实例 + """ + settings = get_settings() + llm_config = settings.llm.get_current_config() + + if model: + # 根据指定的模型获取对应配置 + if model.startswith('deepseek'): + llm_config['model'] = settings.llm.deepseek_model + llm_config['api_key'] = settings.llm.deepseek_api_key + llm_config['base_url'] = settings.llm.deepseek_base_url + elif model.startswith('doubao'): + llm_config['model'] = settings.llm.doubao_model + llm_config['api_key'] = settings.llm.doubao_api_key + llm_config['base_url'] = settings.llm.doubao_base_url + elif model.startswith('glm'): + llm_config['model'] = settings.llm.zhipu_model + llm_config['api_key'] = settings.llm.zhipu_api_key + llm_config['base_url'] = settings.llm.zhipu_base_url + elif model.startswith('moonshot'): + llm_config['model'] = settings.llm.moonshot_model + llm_config['api_key'] = settings.llm.moonshot_api_key + llm_config['base_url'] = settings.llm.moonshot_base_url + + return ChatOpenAI( + model=llm_config['model'], + api_key=llm_config['api_key'], + base_url=llm_config['base_url'], + temperature=temperature if temperature is not None else llm_config['temperature'], + max_tokens=llm_config['max_tokens'], + streaming=streaming + ) \ No newline at end of file diff --git a/backend/th_agenter/core/logging.py b/backend/th_agenter/core/logging.py new file mode 100644 index 0000000..2a0e333 --- /dev/null +++ b/backend/th_agenter/core/logging.py @@ -0,0 +1,64 @@ +"""Logging configuration for TH-Agenter.""" + +import logging +import logging.handlers +from pathlib import Path +from typing import Optional + +from .config import LoggingSettings + + +def setup_logging(logging_config: LoggingSettings) -> None: + """Setup application logging.""" + + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + log_file_path = logging_config.file + if not Path(log_file_path).is_absolute(): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + log_file_path = str(backend_dir / log_file_path) + + # Create logs directory if it doesn't exist + log_file = Path(log_file_path) + log_file.parent.mkdir(parents=True, exist_ok=True) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, logging_config.level.upper())) + + # Clear existing handlers + root_logger.handlers.clear() + + # Create formatter + formatter = logging.Formatter(logging_config.format) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # File handler with rotation + file_handler = logging.handlers.RotatingFileHandler( + filename=log_file_path, + maxBytes=logging_config.max_bytes, + backupCount=logging_config.backup_count, + encoding="utf-8" + ) + file_handler.setLevel(getattr(logging, logging_config.level.upper())) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + # Set specific logger levels + logging.getLogger("uvicorn").setLevel(logging.INFO) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + logging.getLogger("fastapi").setLevel(logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) + + logging.info("Logging configured successfully") + + +def get_logger(name: Optional[str] = None) -> logging.Logger: + """Get a logger instance.""" + return logging.getLogger(name or __name__) \ No newline at end of file diff --git a/backend/th_agenter/core/middleware.py b/backend/th_agenter/core/middleware.py new file mode 100644 index 0000000..56d5bf8 --- /dev/null +++ b/backend/th_agenter/core/middleware.py @@ -0,0 +1,163 @@ +""" +中间件管理,如上下文中间件:校验Token等 +""" + +from fastapi import Request, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import Response +from typing import Callable + +from ..db.database import get_db_session +from ..services.auth import AuthService +from .context import UserContext + + +class UserContextMiddleware(BaseHTTPMiddleware): + """Middleware to set user context for authenticated requests.""" + + def __init__(self, app, exclude_paths: list = None): + super().__init__(app) + # Paths that don't require authentication + self.exclude_paths = exclude_paths or [ + "/docs", + "/redoc", + "/openapi.json", + "/api/auth/login", + "/api/auth/register", + "/api/auth/login-oauth", + "/auth/login", + "/auth/register", + "/auth/login-oauth", + "/health", + "/test" + ] + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """Process request and set user context if authenticated.""" + import logging + logging.info(f"[MIDDLEWARE] Processing request: {request.method} {request.url.path}") + + # Skip authentication for excluded paths + path = request.url.path + logging.info(f"[MIDDLEWARE] Checking path: {path} against exclude_paths: {self.exclude_paths}") + + should_skip = False + for exclude_path in self.exclude_paths: + # Exact match + if path == exclude_path: + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} exactly matches exclude_path {exclude_path}") + break + # For paths ending with '/', check if request path starts with it + elif exclude_path.endswith('/') and path.startswith(exclude_path): + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}") + break + # For paths not ending with '/', check if request path starts with it + '/' + elif not exclude_path.endswith('/') and exclude_path != '/' and path.startswith(exclude_path + '/'): + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}/") + break + + if should_skip: + logging.info(f"[MIDDLEWARE] Skipping authentication for excluded path: {path}") + response = await call_next(request) + return response + + logging.info(f"[MIDDLEWARE] Processing authenticated request: {path}") + + # Always clear any existing user context to ensure fresh authentication + UserContext.clear_current_user() + + # Initialize context token + user_token = None + + # Try to extract and validate token + try: + # Get authorization header + authorization = request.headers.get("Authorization") + if not authorization or not authorization.startswith("Bearer "): + # No token provided, return 401 error + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Missing or invalid authorization header"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Extract token + token = authorization.split(" ")[1] + + # Verify token + payload = AuthService.verify_token(token) + if payload is None: + # Invalid token, return 401 error + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Invalid or expired token"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Get username from token + username = payload.get("sub") + if not username: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Invalid token payload"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Get user from database + db = get_db_session() + try: + from ..models.user import User + user = db.query(User).filter(User.username == username).first() + if not user: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "User not found"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + if not user.is_active: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "User account is inactive"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Set user in context using token mechanism + user_token = UserContext.set_current_user_with_token(user) + import logging + logging.info(f"User {user.username} (ID: {user.id}) authenticated and set in context") + + # Verify context is set correctly + current_user_id = UserContext.get_current_user_id() + logging.info(f"Verified current user ID in context: {current_user_id}") + finally: + db.close() + + except Exception as e: + # Log error but don't fail the request + import logging + logging.warning(f"Error setting user context: {e}") + + # Continue with request + try: + response = await call_next(request) + return response + finally: + # Always clear user context after request processing + UserContext.clear_current_user() + logging.debug(f"[MIDDLEWARE] Cleared user context after processing request: {path}") \ No newline at end of file diff --git a/backend/th_agenter/core/simple_permissions.py b/backend/th_agenter/core/simple_permissions.py new file mode 100644 index 0000000..30ea5e4 --- /dev/null +++ b/backend/th_agenter/core/simple_permissions.py @@ -0,0 +1,90 @@ +"""简化的权限检查系统.""" + +from functools import wraps +from typing import Optional +from fastapi import HTTPException, Depends +from sqlalchemy.orm import Session + +from ..db.database import get_db +from ..models.user import User +from ..models.permission import Role +from ..services.auth import AuthService + + +def is_super_admin(user: User, db: Session) -> bool: + """检查用户是否为超级管理员.""" + if not user or not user.is_active: + return False + + # 检查用户是否有超级管理员角色 + for role in user.roles: + if role.code == "SUPER_ADMIN": + return True + return False + + +def require_super_admin( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +) -> User: + """要求超级管理员权限的依赖项.""" + if not is_super_admin(current_user, db): + raise HTTPException( + status_code=403, + detail="需要超级管理员权限" + ) + return current_user + + +def require_authenticated_user( + current_user: User = Depends(AuthService.get_current_user) +) -> User: + """要求已认证用户的依赖项.""" + if not current_user or not current_user.is_active: + raise HTTPException( + status_code=401, + detail="需要登录" + ) + return current_user + + +class SimplePermissionChecker: + """简化的权限检查器.""" + + def __init__(self, db: Session): + self.db = db + + def check_super_admin(self, user: User) -> bool: + """检查是否为超级管理员.""" + return is_super_admin(user, self.db) + + def check_user_access(self, user: User, target_user_id: int) -> bool: + """检查用户访问权限(自己或超级管理员).""" + if not user or not user.is_active: + return False + + # 超级管理员可以访问所有用户 + if self.check_super_admin(user): + return True + + # 用户只能访问自己的信息 + return user.id == target_user_id + + +# 权限装饰器 +def super_admin_required(func): + """超级管理员权限装饰器.""" + @wraps(func) + def wrapper(*args, **kwargs): + # 这个装饰器主要用于服务层,实际的FastAPI依赖项检查在路由层 + return func(*args, **kwargs) + return wrapper + + +def authenticated_required(func): + """认证用户权限装饰器.""" + @wraps(func) + def wrapper(*args, **kwargs): + # 这个装饰器主要用于服务层,实际的FastAPI依赖项检查在路由层 + return func(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/backend/th_agenter/core/user_utils.py b/backend/th_agenter/core/user_utils.py new file mode 100644 index 0000000..fff0e81 --- /dev/null +++ b/backend/th_agenter/core/user_utils.py @@ -0,0 +1,76 @@ +"""User utility functions for easy access to current user context.""" + +from typing import Optional +from ..models.user import User +from .context import UserContext + + +def get_current_user() -> Optional[User]: + """Get current authenticated user from context. + + Returns: + Current user if authenticated, None otherwise + """ + return UserContext.get_current_user() + + +def get_current_user_id() -> Optional[int]: + """Get current authenticated user ID from context. + + Returns: + Current user ID if authenticated, None otherwise + """ + return UserContext.get_current_user_id() + + +def require_current_user() -> User: + """Get current authenticated user from context, raise exception if not found. + + Returns: + Current user + + Raises: + HTTPException: If no authenticated user in context + """ + return UserContext.require_current_user() + + +def require_current_user_id() -> int: + """Get current authenticated user ID from context, raise exception if not found. + + Returns: + Current user ID + + Raises: + HTTPException: If no authenticated user in context + """ + return UserContext.require_current_user_id() + + +def is_user_authenticated() -> bool: + """Check if there is an authenticated user in the current context. + + Returns: + True if user is authenticated, False otherwise + """ + return UserContext.get_current_user() is not None + + +def get_current_username() -> Optional[str]: + """Get current authenticated user's username from context. + + Returns: + Current user's username if authenticated, None otherwise + """ + user = UserContext.get_current_user() + return user.username if user else None + + +def get_current_user_email() -> Optional[str]: + """Get current authenticated user's email from context. + + Returns: + Current user's email if authenticated, None otherwise + """ + user = UserContext.get_current_user() + return user.email if user else None \ No newline at end of file diff --git a/backend/th_agenter/db/__init__.py b/backend/th_agenter/db/__init__.py new file mode 100644 index 0000000..7ffefdc --- /dev/null +++ b/backend/th_agenter/db/__init__.py @@ -0,0 +1,6 @@ +"""Database module for TH-Agenter.""" + +from .database import get_db, init_db +from .base import Base + +__all__ = ["get_db", "init_db", "Base"] \ No newline at end of file diff --git a/backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..1bac90e Binary files /dev/null and b/backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/db/__pycache__/base.cpython-313.pyc b/backend/th_agenter/db/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000..ff90775 Binary files /dev/null and b/backend/th_agenter/db/__pycache__/base.cpython-313.pyc differ diff --git a/backend/th_agenter/db/__pycache__/database.cpython-313.pyc b/backend/th_agenter/db/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..2bd09d1 Binary files /dev/null and b/backend/th_agenter/db/__pycache__/database.cpython-313.pyc differ diff --git a/backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc b/backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc new file mode 100644 index 0000000..30b5c94 Binary files /dev/null and b/backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc differ diff --git a/backend/th_agenter/db/base.py b/backend/th_agenter/db/base.py new file mode 100644 index 0000000..e631680 --- /dev/null +++ b/backend/th_agenter/db/base.py @@ -0,0 +1,62 @@ +"""Database base model.""" + +from datetime import datetime +from sqlalchemy import Column, Integer, DateTime, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql import func +from typing import Optional + + +Base = declarative_base() + + +class BaseModel(Base): + """Base model with common fields.""" + + __abstract__ = True + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) + created_by = Column(Integer, nullable=True) + updated_by = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + """Initialize model with automatic audit fields setting.""" + super().__init__(**kwargs) + # Set audit fields for new instances + self.set_audit_fields() + def set_audit_fields(self, user_id: Optional[int] = None, is_update: bool = False): + """Set audit fields for create/update operations. + + Args: + user_id: ID of the user performing the operation (optional, will use context if not provided) + is_update: True for update operations, False for create operations + """ + # Get user_id from context if not provided + if user_id is None: + from ..core.context import UserContext + try: + user_id = UserContext.get_current_user_id() + except Exception: + # If no user in context, skip setting audit fields + return + + # Skip if still no user_id + if user_id is None: + return + + if not is_update: + # For create operations, set both create_by and update_by + self.created_by = user_id + self.updated_by = user_id + else: + # For update operations, only set update_by + self.updated_by = user_id + + def to_dict(self): + """Convert model to dictionary.""" + return { + column.name: getattr(self, column.name) + for column in self.__table__.columns + } \ No newline at end of file diff --git a/backend/th_agenter/db/database.py b/backend/th_agenter/db/database.py new file mode 100644 index 0000000..d566cbf --- /dev/null +++ b/backend/th_agenter/db/database.py @@ -0,0 +1,89 @@ +"""Database connection and session management.""" + +import logging +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from typing import Generator + +from ..core.config import get_settings +from .base import Base + + +# Global variables +engine = None +SessionLocal = None + + +def create_database_engine(): + """Create database engine.""" + global engine, SessionLocal + + settings = get_settings() + database_url = settings.database.url + + # Determine database type and configure engine + engine_kwargs = { + "echo": settings.database.echo, + } + + if database_url.startswith("sqlite"): + # SQLite configuration + engine = create_engine(database_url, **engine_kwargs) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + logging.info(f"SQLite database engine created: {database_url}") + elif database_url.startswith("postgresql"): + # PostgreSQL configuration + engine_kwargs.update({ + "pool_size": settings.database.pool_size, + "max_overflow": settings.database.max_overflow, + "pool_pre_ping": True, + "pool_recycle": 3600, + }) + engine = create_engine(database_url, **engine_kwargs) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + logging.info(f"PostgreSQL database engine created: {database_url}") + else: + raise ValueError(f"Unsupported database type. Please use PostgreSQL or SQLite. URL: {database_url}") + + +async def init_db(): + """Initialize database.""" + global engine + + if engine is None: + create_database_engine() + + # Import all models to ensure they are registered + from ..models import user, conversation, message, knowledge_base, permission, workflow + + # Create all tables + Base.metadata.create_all(bind=engine) + logging.info("Database tables created") + + +def get_db() -> Generator[Session, None, None]: + """Get database session.""" + global SessionLocal + + if SessionLocal is None: + create_database_engine() + + db = SessionLocal() + try: + yield db + except Exception as e: + db.rollback() + logging.error(f"Database session error: {e}") + raise + finally: + db.close() + + +def get_db_session() -> Session: + """Get database session (synchronous).""" + global SessionLocal + + if SessionLocal is None: + create_database_engine() + + return SessionLocal() \ No newline at end of file diff --git a/backend/th_agenter/db/db_config_key.key b/backend/th_agenter/db/db_config_key.key new file mode 100644 index 0000000..c71bfaf --- /dev/null +++ b/backend/th_agenter/db/db_config_key.key @@ -0,0 +1 @@ +nRWvsuXfWm1IXThkPQ7lA4HlTiNP4CkCYxqczEfrRR4= \ No newline at end of file diff --git a/backend/th_agenter/db/init_system_data.py b/backend/th_agenter/db/init_system_data.py new file mode 100644 index 0000000..ddef4da --- /dev/null +++ b/backend/th_agenter/db/init_system_data.py @@ -0,0 +1,121 @@ +"""Initialize system management data.""" + +from sqlalchemy.orm import Session +from ..models.permission import Role +from ..models.user import User +from ..services.auth import AuthService +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + + + + +def init_roles(db: Session) -> None: + """初始化系统角色.""" + roles_data = [ + { + "name": "超级管理员", + "code": "SUPER_ADMIN", + "description": "系统超级管理员,拥有所有权限" + }, + { + "name": "普通用户", + "code": "USER", + "description": "普通用户,基础功能权限" + } + ] + + for role_data in roles_data: + # 检查角色是否已存在 + existing_role = db.query(Role).filter( + Role.code == role_data["code"] + ).first() + + if not existing_role: + # 创建角色 + role = Role( + name=role_data["name"], + code=role_data["code"], + description=role_data["description"] + ) + role.set_audit_fields(1) # 系统用户ID为1 + db.add(role) + logger.info(f"Created role: {role_data['name']} ({role_data['code']})") + + db.commit() + logger.info("Roles initialization completed") + + + + + +def init_admin_user(db: Session) -> None: + """初始化默认管理员用户.""" + logger.info("Starting admin user initialization...") + + # 检查是否已存在管理员用户 + existing_admin = db.query(User).filter( + User.username == "admin" + ).first() + + if existing_admin: + logger.info("Admin user already exists") + return + + # 创建管理员用户 + hashed_password = AuthService.get_password_hash("admin123") + + admin_user = User( + username="admin", + email="admin@example.com", + hashed_password=hashed_password, + full_name="系统管理员", + is_active=True + ) + + admin_user.set_audit_fields(1) + db.add(admin_user) + db.flush() + + # 分配超级管理员角色 + super_admin_role = db.query(Role).filter( + Role.code == "SUPER_ADMIN" + ).first() + + if super_admin_role: + admin_user.roles.append(super_admin_role) + + db.commit() + logger.info("Admin user created: admin / admin123") + + +def init_system_data(db: Session) -> None: + """初始化所有系统数据.""" + logger.info("Starting system data initialization...") + + try: + # 初始化角色 + init_roles(db) + + # 初始化管理员用户 + init_admin_user(db) + + logger.info("System data initialization completed successfully") + + except Exception as e: + logger.error(f"Error during system data initialization: {str(e)}") + db.rollback() + raise + + +if __name__ == "__main__": + # 可以单独运行此脚本来初始化数据 + from ..db.database import SessionLocal + + db = SessionLocal() + try: + init_system_data(db) + finally: + db.close() \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/add_system_management.py b/backend/th_agenter/db/migrations/add_system_management.py new file mode 100644 index 0000000..8bbd9f0 --- /dev/null +++ b/backend/th_agenter/db/migrations/add_system_management.py @@ -0,0 +1,216 @@ +"""Add system management tables. + +Revision ID: add_system_management +Revises: +Create Date: 2024-01-01 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'add_system_management' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + """Create system management tables.""" + + # Create departments table + op.create_table('departments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['parent_id'], ['departments.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=False) + op.create_index(op.f('ix_departments_parent_id'), 'departments', ['parent_id'], unique=False) + + # Create permissions table + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=100), nullable=False), + sa.Column('category', sa.String(length=50), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_permissions_category'), 'permissions', ['category'], unique=False) + op.create_index(op.f('ix_permissions_name'), 'permissions', ['name'], unique=False) + + # Create roles table + op.create_table('roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False) + + # Create role_permissions table + op.create_table('role_permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('role_id', 'permission_id', name='uq_role_permission') + ) + op.create_index(op.f('ix_role_permissions_permission_id'), 'role_permissions', ['permission_id'], unique=False) + op.create_index(op.f('ix_role_permissions_role_id'), 'role_permissions', ['role_id'], unique=False) + + # Create user_roles table + op.create_table('user_roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'role_id', name='uq_user_role') + ) + op.create_index(op.f('ix_user_roles_role_id'), 'user_roles', ['role_id'], unique=False) + op.create_index(op.f('ix_user_roles_user_id'), 'user_roles', ['user_id'], unique=False) + + # Create user_permissions table + op.create_table('user_permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.Column('granted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'permission_id', name='uq_user_permission') + ) + op.create_index(op.f('ix_user_permissions_permission_id'), 'user_permissions', ['permission_id'], unique=False) + op.create_index(op.f('ix_user_permissions_user_id'), 'user_permissions', ['user_id'], unique=False) + + # Create llm_configs table + op.create_table('llm_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('model_name', sa.String(length=100), nullable=False), + sa.Column('api_key', sa.Text(), nullable=True), + sa.Column('api_base', sa.String(length=500), nullable=True), + sa.Column('api_version', sa.String(length=20), nullable=True), + sa.Column('max_tokens', sa.Integer(), nullable=True), + sa.Column('temperature', sa.Float(), nullable=True), + sa.Column('top_p', sa.Float(), nullable=True), + sa.Column('frequency_penalty', sa.Float(), nullable=True), + sa.Column('presence_penalty', sa.Float(), nullable=True), + sa.Column('timeout', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_llm_configs_name'), 'llm_configs', ['name'], unique=False) + op.create_index(op.f('ix_llm_configs_provider'), 'llm_configs', ['provider'], unique=False) + + # Add new columns to users table + op.add_column('users', sa.Column('department_id', sa.Integer(), nullable=True)) + op.add_column('users', sa.Column('is_superuser', sa.Boolean(), nullable=True, default=False)) + op.add_column('users', sa.Column('is_admin', sa.Boolean(), nullable=True, default=False)) + op.add_column('users', sa.Column('last_login_at', sa.DateTime(), nullable=True)) + op.add_column('users', sa.Column('login_count', sa.Integer(), nullable=True, default=0)) + + # Create foreign key constraint for department_id + op.create_foreign_key('fk_users_department_id', 'users', 'departments', ['department_id'], ['id']) + op.create_index(op.f('ix_users_department_id'), 'users', ['department_id'], unique=False) + + +def downgrade(): + """Drop system management tables.""" + + # Drop foreign key and index for users.department_id + op.drop_index(op.f('ix_users_department_id'), table_name='users') + op.drop_constraint('fk_users_department_id', 'users', type_='foreignkey') + + # Drop new columns from users table + op.drop_column('users', 'login_count') + op.drop_column('users', 'last_login_at') + op.drop_column('users', 'is_admin') + op.drop_column('users', 'is_superuser') + op.drop_column('users', 'department_id') + + # Drop llm_configs table + op.drop_index(op.f('ix_llm_configs_provider'), table_name='llm_configs') + op.drop_index(op.f('ix_llm_configs_name'), table_name='llm_configs') + op.drop_table('llm_configs') + + # Drop user_permissions table + op.drop_index(op.f('ix_user_permissions_user_id'), table_name='user_permissions') + op.drop_index(op.f('ix_user_permissions_permission_id'), table_name='user_permissions') + op.drop_table('user_permissions') + + # Drop user_roles table + op.drop_index(op.f('ix_user_roles_user_id'), table_name='user_roles') + op.drop_index(op.f('ix_user_roles_role_id'), table_name='user_roles') + op.drop_table('user_roles') + + # Drop role_permissions table + op.drop_index(op.f('ix_role_permissions_role_id'), table_name='role_permissions') + op.drop_index(op.f('ix_role_permissions_permission_id'), table_name='role_permissions') + op.drop_table('role_permissions') + + # Drop roles table + op.drop_index(op.f('ix_roles_name'), table_name='roles') + op.drop_table('roles') + + # Drop permissions table + op.drop_index(op.f('ix_permissions_name'), table_name='permissions') + op.drop_index(op.f('ix_permissions_category'), table_name='permissions') + op.drop_table('permissions') + + # Drop departments table + op.drop_index(op.f('ix_departments_parent_id'), table_name='departments') + op.drop_index(op.f('ix_departments_name'), table_name='departments') + op.drop_table('departments') \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/add_user_department_table.py b/backend/th_agenter/db/migrations/add_user_department_table.py new file mode 100644 index 0000000..edfa137 --- /dev/null +++ b/backend/th_agenter/db/migrations/add_user_department_table.py @@ -0,0 +1,83 @@ +"""Add user_department association table migration.""" + +import sys +import os +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(project_root)) + +import asyncio +import asyncpg +from th_agenter.core.config import get_settings + +async def create_user_department_table(): + """Create user_departments association table.""" + settings = get_settings() + database_url = settings.database.url + + print(f"Database URL: {database_url}") + + try: + # 解析PostgreSQL连接URL + # postgresql://user:password@host:port/database + url_parts = database_url.replace('postgresql://', '').split('/') + db_name = url_parts[1] if len(url_parts) > 1 else 'postgres' + user_host = url_parts[0].split('@') + user_pass = user_host[0].split(':') + host_port = user_host[1].split(':') + + user = user_pass[0] + password = user_pass[1] if len(user_pass) > 1 else '' + host = host_port[0] + port = int(host_port[1]) if len(host_port) > 1 else 5432 + + # 连接PostgreSQL数据库 + conn = await asyncpg.connect( + user=user, + password=password, + database=db_name, + host=host, + port=port + ) + + # 创建user_departments表 + create_table_sql = """ + CREATE TABLE IF NOT EXISTS user_departments ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + department_id INTEGER NOT NULL, + is_primary BOOLEAN NOT NULL DEFAULT true, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE CASCADE + ); + """ + + await conn.execute(create_table_sql) + + # 创建索引 + create_indexes_sql = [ + "CREATE INDEX IF NOT EXISTS idx_user_departments_user_id ON user_departments (user_id);", + "CREATE INDEX IF NOT EXISTS idx_user_departments_department_id ON user_departments (department_id);", + "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_departments_unique ON user_departments (user_id, department_id);" + ] + + for index_sql in create_indexes_sql: + await conn.execute(index_sql) + + print("User departments table created successfully") + + except Exception as e: + print(f"Error creating user departments table: {e}") + raise + finally: + if 'conn' in locals(): + await conn.close() + + +if __name__ == "__main__": + asyncio.run(create_user_department_table()) \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py b/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py new file mode 100644 index 0000000..fb3474c --- /dev/null +++ b/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +"""Migration script to move hardcoded resources to database.""" + +import sys +import os +from pathlib import Path + +# Add the backend directory to Python path +backend_dir = Path(__file__).parent.parent.parent +sys.path.insert(0, str(backend_dir)) + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from th_agenter.core.config import settings +from th_agenter.db.database import Base, get_db_session +from th_agenter.models import * # Import all models to ensure they're registered +from th_agenter.utils.logger import get_logger +from th_agenter.models.resource import Resource +from th_agenter.models.permission import Role +from th_agenter.models.resource import RoleResource + +logger = get_logger(__name__) + +def migrate_hardcoded_resources(): + """Migrate hardcoded resources from init_resource_data.py to database.""" + db = None + try: + # Get database session + db = get_db_session() + + if db is None: + logger.error("Failed to create database session") + return False + + # Create all tables if they don't exist + from th_agenter.db.database import engine as global_engine + if global_engine: + Base.metadata.create_all(bind=global_engine) + + logger.info("Starting hardcoded resources migration...") + + # Check if resources already exist + existing_count = db.query(Resource).count() + if existing_count > 0: + logger.info(f"Found {existing_count} existing resources. Checking role assignments.") + # 即使资源已存在,也要检查并分配角色资源关联 + admin_role = db.query(Role).filter(Role.name == "系统管理员").first() + if admin_role: + # 获取所有资源 + all_resources = db.query(Resource).all() + assigned_count = 0 + + for resource in all_resources: + # 检查关联是否已存在 + existing = db.query(RoleResource).filter( + RoleResource.role_id == admin_role.id, + RoleResource.resource_id == resource.id + ).first() + + if not existing: + role_resource = RoleResource( + role_id=admin_role.id, + resource_id=resource.id + ) + db.add(role_resource) + assigned_count += 1 + + if assigned_count > 0: + db.commit() + logger.info(f"已为系统管理员角色分配 {assigned_count} 个新资源") + else: + logger.info("系统管理员角色已拥有所有资源") + else: + logger.warning("未找到系统管理员角色") + + return True + + # Define hardcoded resource data + main_menu_data = [ + { + "name": "智能问答", + "code": "CHAT", + "type": "menu", + "path": "/chat", + "component": "views/Chat.vue", + "icon": "ChatDotRound", + "description": "智能问答功能", + "sort_order": 1, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "智能问数", + "code": "SMART_QUERY", + "type": "menu", + "path": "/smart-query", + "component": "views/SmartQuery.vue", + "icon": "DataAnalysis", + "description": "智能问数功能", + "sort_order": 2, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "知识库", + "code": "KNOWLEDGE", + "type": "menu", + "path": "/knowledge", + "component": "views/KnowledgeBase.vue", + "icon": "Collection", + "description": "知识库管理", + "sort_order": 3, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "工作流编排", + "code": "WORKFLOW", + "type": "menu", + "path": "/workflow", + "component": "views/Workflow.vue", + "icon": "Connection", + "description": "工作流编排功能", + "sort_order": 4, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "智能体管理", + "code": "AGENT", + "type": "menu", + "path": "/agent", + "component": "views/Agent.vue", + "icon": "User", + "description": "智能体管理功能", + "sort_order": 5, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "系统管理", + "code": "SYSTEM", + "type": "menu", + "path": "/system", + "component": "views/SystemManagement.vue", + "icon": "Setting", + "description": "系统管理功能", + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create main menu resources + created_resources = {} + for menu_data in main_menu_data: + resource = Resource(**menu_data) + db.add(resource) + db.flush() + created_resources[menu_data["code"]] = resource + logger.info(f"Created main menu resource: {menu_data['name']}") + + # System management submenu data + system_submenu_data = [ + { + "name": "用户管理", + "code": "SYSTEM_USERS", + "type": "menu", + "path": "/system/users", + "component": "components/system/UserManagement.vue", + "icon": "User", + "description": "用户管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "部门管理", + "code": "SYSTEM_DEPARTMENTS", + "type": "menu", + "path": "/system/departments", + "component": "components/system/DepartmentManagement.vue", + "icon": "OfficeBuilding", + "description": "部门管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "角色管理", + "code": "SYSTEM_ROLES", + "type": "menu", + "path": "/system/roles", + "component": "components/system/RoleManagement.vue", + "icon": "Avatar", + "description": "角色管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 3, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "权限管理", + "code": "SYSTEM_PERMISSIONS", + "type": "menu", + "path": "/system/permissions", + "component": "components/system/PermissionManagement.vue", + "icon": "Lock", + "description": "权限管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 4, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "资源管理", + "code": "SYSTEM_RESOURCES", + "type": "menu", + "path": "/system/resources", + "component": "components/system/ResourceManagement.vue", + "icon": "Grid", + "description": "资源管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 5, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "大模型管理", + "code": "SYSTEM_LLM_CONFIGS", + "type": "menu", + "path": "/system/llm-configs", + "component": "components/system/LLMConfigManagement.vue", + "icon": "Cpu", + "description": "大模型配置管理", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create system management submenu + for submenu_data in system_submenu_data: + submenu = Resource(**submenu_data) + db.add(submenu) + db.flush() + created_resources[submenu_data["code"]] = submenu + logger.info(f"Created system submenu resource: {submenu_data['name']}") + + # Button resources data + button_resources_data = [ + # User management buttons + { + "name": "新增用户", + "code": "USER_CREATE_BTN", + "type": "button", + "description": "新增用户按钮", + "parent_id": created_resources["SYSTEM_USERS"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑用户", + "code": "USER_EDIT_BTN", + "type": "button", + "description": "编辑用户按钮", + "parent_id": created_resources["SYSTEM_USERS"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Role management buttons + { + "name": "新增角色", + "code": "ROLE_CREATE_BTN", + "type": "button", + "description": "新增角色按钮", + "parent_id": created_resources["SYSTEM_ROLES"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑角色", + "code": "ROLE_EDIT_BTN", + "type": "button", + "description": "编辑角色按钮", + "parent_id": created_resources["SYSTEM_ROLES"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Permission management buttons + { + "name": "新增权限", + "code": "PERMISSION_CREATE_BTN", + "type": "button", + "description": "新增权限按钮", + "parent_id": created_resources["SYSTEM_PERMISSIONS"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑权限", + "code": "PERMISSION_EDIT_BTN", + "type": "button", + "description": "编辑权限按钮", + "parent_id": created_resources["SYSTEM_PERMISSIONS"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create button resources + for button_data in button_resources_data: + button = Resource(**button_data) + db.add(button) + db.flush() + created_resources[button_data["code"]] = button + logger.info(f"Created button resource: {button_data['name']}") + + # API resources data + api_resources_data = [ + # User management APIs + { + "name": "用户列表API", + "code": "USER_LIST_API", + "type": "api", + "path": "/api/users", + "description": "获取用户列表API", + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建用户API", + "code": "USER_CREATE_API", + "type": "api", + "path": "/api/users", + "description": "创建用户API", + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Role management APIs + { + "name": "角色列表API", + "code": "ROLE_LIST_API", + "type": "api", + "path": "/api/admin/roles", + "description": "获取角色列表API", + "sort_order": 5, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建角色API", + "code": "ROLE_CREATE_API", + "type": "api", + "path": "/api/admin/roles", + "description": "创建角色API", + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + }, + # Resource management APIs + { + "name": "资源列表API", + "code": "RESOURCE_LIST_API", + "type": "api", + "path": "/api/admin/resources", + "description": "获取资源列表API", + "sort_order": 10, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建资源API", + "code": "RESOURCE_CREATE_API", + "type": "api", + "path": "/api/admin/resources", + "description": "创建资源API", + "sort_order": 11, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create API resources + for api_data in api_resources_data: + api_resource = Resource(**api_data) + db.add(api_resource) + db.flush() + created_resources[api_data["code"]] = api_resource + logger.info(f"Created API resource: {api_data['name']}") + + # 分配资源给系统管理员角色 + admin_role = db.query(Role).filter(Role.name == "系统管理员").first() + if admin_role: + all_resources = list(created_resources.values()) + for resource in all_resources: + # 检查关联是否已存在 + existing = db.query(RoleResource).filter( + RoleResource.role_id == admin_role.id, + RoleResource.resource_id == resource.id + ).first() + + if not existing: + role_resource = RoleResource( + role_id=admin_role.id, + resource_id=resource.id + ) + db.add(role_resource) + + logger.info(f"已为系统管理员角色分配 {len(all_resources)} 个资源") + else: + logger.warning("未找到系统管理员角色") + + db.commit() + + total_resources = db.query(Resource).count() + logger.info(f"Migration completed successfully. Total resources: {total_resources}") + + return True + + except Exception as e: + logger.error(f"Migration failed: {str(e)}") + if db: + db.rollback() + return False + finally: + if db: + db.close() + +def main(): + """Main function to run the migration.""" + print("=== 硬编码资源数据迁移 ===") + success = migrate_hardcoded_resources() + if success: + print("\n🎉 资源数据迁移完成!") + else: + print("\n❌ 资源数据迁移失败!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/remove_permission_tables.py b/backend/th_agenter/db/migrations/remove_permission_tables.py new file mode 100644 index 0000000..9f514ef --- /dev/null +++ b/backend/th_agenter/db/migrations/remove_permission_tables.py @@ -0,0 +1,146 @@ +"""删除权限相关表的迁移脚本 + +Revision ID: remove_permission_tables +Revises: add_system_management +Create Date: 2024-01-25 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + + +# revision identifiers, used by Alembic. +revision = 'remove_permission_tables' +down_revision = 'add_system_management' +branch_labels = None +depends_on = None + + +def upgrade(): + """删除权限相关表.""" + + # 获取数据库连接 + connection = op.get_bind() + + # 删除外键约束和表(按依赖关系顺序) + tables_to_drop = [ + 'user_permissions', # 用户权限关联表 + 'role_permissions', # 角色权限关联表 + 'permission_resources', # 权限资源关联表 + 'permissions', # 权限表 + 'role_resources', # 角色资源关联表 + 'resources', # 资源表 + 'user_departments', # 用户部门关联表 + 'departments' # 部门表 + ] + + for table_name in tables_to_drop: + try: + # 检查表是否存在 + result = connection.execute(text(f""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = '{table_name}' + ); + """)) + table_exists = result.scalar() + + if table_exists: + print(f"删除表: {table_name}") + op.drop_table(table_name) + else: + print(f"表 {table_name} 不存在,跳过") + + except Exception as e: + print(f"删除表 {table_name} 时出错: {e}") + # 继续删除其他表 + continue + + # 删除用户表中的部门相关字段 + try: + # 检查字段是否存在 + result = connection.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'department_id'; + """)) + column_exists = result.fetchone() + + if column_exists: + print("删除用户表中的 department_id 字段") + op.drop_column('users', 'department_id') + else: + print("用户表中的 department_id 字段不存在,跳过") + + except Exception as e: + print(f"删除 department_id 字段时出错: {e}") + + # 简化 user_roles 表结构(如果需要的话) + try: + # 检查 user_roles 表是否有多余的字段 + result = connection.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'user_roles' AND column_name IN ('id', 'created_at', 'updated_at', 'created_by', 'updated_by'); + """)) + extra_columns = [row[0] for row in result.fetchall()] + + if extra_columns: + print("简化 user_roles 表结构") + # 创建新的简化表 + op.execute(text(""" + CREATE TABLE user_roles_new ( + user_id INTEGER NOT NULL, + role_id INTEGER NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE + ); + """)) + + # 迁移数据 + op.execute(text(""" + INSERT INTO user_roles_new (user_id, role_id) + SELECT DISTINCT user_id, role_id FROM user_roles; + """)) + + # 删除旧表,重命名新表 + op.drop_table('user_roles') + op.execute(text("ALTER TABLE user_roles_new RENAME TO user_roles;")) + + except Exception as e: + print(f"简化 user_roles 表时出错: {e}") + + +def downgrade(): + """回滚操作 - 重新创建权限相关表.""" + + # 注意:这是一个破坏性操作,回滚会丢失数据 + # 在生产环境中应该谨慎使用 + + print("警告:回滚操作会重新创建权限相关表,但不会恢复数据") + + # 重新创建基本的权限表结构(简化版) + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(100), nullable=False), + sa.Column('code', sa.String(100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + + op.create_table('role_permissions', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'permission_id') + ) + + # 添加用户表的 department_id 字段 + op.add_column('users', sa.Column('department_id', sa.Integer(), nullable=True)) \ No newline at end of file diff --git a/backend/th_agenter/main.py b/backend/th_agenter/main.py new file mode 100644 index 0000000..424e126 --- /dev/null +++ b/backend/th_agenter/main.py @@ -0,0 +1,35 @@ +from dotenv import load_dotenv +import os +import sys +from pathlib import Path + +# Add backend directory to Python path for direct execution +if __name__ == "__main__": + backend_dir = Path(__file__).parent.parent + if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + +# Load environment variables from .env file +load_dotenv() + +from th_agenter.core.app import create_app + +# Create FastAPI application using factory function +app = create_app() + +# 在 main.py 中添加表元数据路由 + + + +if __name__ == "__main__": + import uvicorn + import argparse + + parser = argparse.ArgumentParser(description='TH-Agenter Backend Server') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind to') + parser.add_argument('--port', type=int, default=8000, help='Port to bind to') + parser.add_argument('--reload', action='store_true', help='Enable auto-reload') + + args = parser.parse_args() + + uvicorn.run(app, host=args.host, port=args.port, reload=args.reload) \ No newline at end of file diff --git a/backend/th_agenter/models/__init__.py b/backend/th_agenter/models/__init__.py new file mode 100644 index 0000000..c95acf6 --- /dev/null +++ b/backend/th_agenter/models/__init__.py @@ -0,0 +1,27 @@ +"""Database models for TH-Agenter.""" + +from .user import User +from .conversation import Conversation +from .message import Message +from .knowledge_base import KnowledgeBase, Document +from .agent_config import AgentConfig +from .excel_file import ExcelFile +from .permission import Role, UserRole +from .llm_config import LLMConfig +from .workflow import Workflow, WorkflowExecution, NodeExecution + +__all__ = [ + "User", + "Conversation", + "Message", + "KnowledgeBase", + "Document", + "AgentConfig", + "ExcelFile", + "Role", + "UserRole", + "LLMConfig", + "Workflow", + "WorkflowExecution", + "NodeExecution" +] \ No newline at end of file diff --git a/backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9720d42 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc b/backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc new file mode 100644 index 0000000..2159ffb Binary files /dev/null and b/backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc b/backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000..ca55213 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/database_config.cpython-313.pyc b/backend/th_agenter/models/__pycache__/database_config.cpython-313.pyc new file mode 100644 index 0000000..d6c6538 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/database_config.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc b/backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc new file mode 100644 index 0000000..eb5da68 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc b/backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc new file mode 100644 index 0000000..559e02e Binary files /dev/null and b/backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc b/backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc new file mode 100644 index 0000000..a306a38 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/message.cpython-313.pyc b/backend/th_agenter/models/__pycache__/message.cpython-313.pyc new file mode 100644 index 0000000..8ad3256 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/message.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/permission.cpython-313.pyc b/backend/th_agenter/models/__pycache__/permission.cpython-313.pyc new file mode 100644 index 0000000..c126978 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/permission.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/table_metadata.cpython-313.pyc b/backend/th_agenter/models/__pycache__/table_metadata.cpython-313.pyc new file mode 100644 index 0000000..5038008 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/table_metadata.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/user.cpython-313.pyc b/backend/th_agenter/models/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..ea78cb3 Binary files /dev/null and b/backend/th_agenter/models/__pycache__/user.cpython-313.pyc differ diff --git a/backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000..33441dc Binary files /dev/null and b/backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/models/agent_config.py b/backend/th_agenter/models/agent_config.py new file mode 100644 index 0000000..3152626 --- /dev/null +++ b/backend/th_agenter/models/agent_config.py @@ -0,0 +1,53 @@ +"""Agent configuration model.""" + +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON +from sqlalchemy.sql import func +from ..db.base import BaseModel + + +class AgentConfig(BaseModel): + """Agent configuration model.""" + + __tablename__ = "agent_configs" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False, index=True) + description = Column(Text, nullable=True) + + # Agent configuration + enabled_tools = Column(JSON, nullable=False, default=list) + max_iterations = Column(Integer, default=10) + temperature = Column(String(10), default="0.1") + system_message = Column(Text, nullable=True) + verbose = Column(Boolean, default=True) + + # Model configuration + model_name = Column(String(100), default="gpt-3.5-turbo") + max_tokens = Column(Integer, default=2048) + + # Status + is_active = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) + + + def __repr__(self): + return f"" + + def to_dict(self): + """Convert to dictionary.""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "enabled_tools": self.enabled_tools or [], + "max_iterations": self.max_iterations, + "temperature": self.temperature, + "system_message": self.system_message, + "verbose": self.verbose, + "model_name": self.model_name, + "max_tokens": self.max_tokens, + "is_active": self.is_active, + "is_default": self.is_default, + "created_at": self.created_at, + "updated_at": self.updated_at + } \ No newline at end of file diff --git a/backend/th_agenter/models/base.py b/backend/th_agenter/models/base.py new file mode 100644 index 0000000..6312301 --- /dev/null +++ b/backend/th_agenter/models/base.py @@ -0,0 +1,5 @@ +"""Database base model.""" + +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() \ No newline at end of file diff --git a/backend/th_agenter/models/conversation.py b/backend/th_agenter/models/conversation.py new file mode 100644 index 0000000..d82f758 --- /dev/null +++ b/backend/th_agenter/models/conversation.py @@ -0,0 +1,38 @@ +"""Conversation model.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean +from sqlalchemy.orm import relationship + +from ..db.base import BaseModel + + +class Conversation(BaseModel): + """Conversation model.""" + + __tablename__ = "conversations" + + title = Column(String(200), nullable=False) + user_id = Column(Integer, nullable=False) # Removed ForeignKey("users.id") + knowledge_base_id = Column(Integer, nullable=True) # Removed ForeignKey("knowledge_bases.id") + system_prompt = Column(Text, nullable=True) + model_name = Column(String(100), nullable=False, default="gpt-3.5-turbo") + temperature = Column(String(10), nullable=False, default="0.7") + max_tokens = Column(Integer, nullable=False, default=2048) + is_archived = Column(Boolean, default=False, nullable=False) + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def message_count(self): + """Get the number of messages in this conversation.""" + return len(self.messages) + + @property + def last_message_at(self): + """Get the timestamp of the last message.""" + if self.messages: + return self.messages[-1].created_at + return self.created_at \ No newline at end of file diff --git a/backend/th_agenter/models/database_config.py b/backend/th_agenter/models/database_config.py new file mode 100644 index 0000000..ad30432 --- /dev/null +++ b/backend/th_agenter/models/database_config.py @@ -0,0 +1,52 @@ +"""数据库配置模型""" + +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON +from sqlalchemy.sql import func +from ..db.base import BaseModel + + +# 在现有的DatabaseConfig类中添加关系 +from sqlalchemy.orm import relationship + +class DatabaseConfig(BaseModel): + """数据库配置表""" + __tablename__ = "database_configs" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) # 配置名称 + db_type = Column(String(20), nullable=False, unique=True) # 数据库类型:postgresql, mysql等 + host = Column(String(255), nullable=False) + port = Column(Integer, nullable=False) + database = Column(String(100), nullable=False) + username = Column(String(100), nullable=False) + password = Column(Text, nullable=False) # 加密存储 + is_active = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) + connection_params = Column(JSON, nullable=True) # 额外连接参数 + + def to_dict(self, include_password=False, decrypt_service=None): + result = { + "id": self.id, + "created_by": self.created_by, + "name": self.name, + "db_type": self.db_type, + "host": self.host, + "port": self.port, + "database": self.database, + "username": self.username, + "is_active": self.is_active, + "is_default": self.is_default, + "connection_params": self.connection_params, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None + } + + # 如果需要包含密码且提供了解密服务 + if include_password and decrypt_service: + print('begin decrypt password') + result["password"] = decrypt_service._decrypt_password(self.password) + + return result + + # 添加关系 + # table_metadata = relationship("TableMetadata", back_populates="database_config") \ No newline at end of file diff --git a/backend/th_agenter/models/excel_file.py b/backend/th_agenter/models/excel_file.py new file mode 100644 index 0000000..da3c841 --- /dev/null +++ b/backend/th_agenter/models/excel_file.py @@ -0,0 +1,89 @@ +"""Excel file models for smart query.""" + +from sqlalchemy import Column, String, Integer, Text, Boolean, JSON, DateTime +from sqlalchemy.sql import func + +from ..db.base import BaseModel + + +class ExcelFile(BaseModel): + """Excel file model for storing file metadata.""" + + __tablename__ = "excel_files" + + + + # Basic file information + # user_id = Column(Integer, nullable=False) # 用户ID + original_filename = Column(String(255), nullable=False) # 原始文件名 + file_path = Column(String(500), nullable=False) # 文件存储路径 + file_size = Column(Integer, nullable=False) # 文件大小(字节) + file_type = Column(String(50), nullable=False) # 文件类型 (.xlsx, .xls, .csv) + + # Excel specific information + sheet_names = Column(JSON, nullable=False) # 所有sheet名称列表 + default_sheet = Column(String(100), nullable=True) # 默认sheet名称 + + # Data preview information + columns_info = Column(JSON, nullable=False) # 列信息:{sheet_name: [column_names]} + preview_data = Column(JSON, nullable=False) # 前5行数据:{sheet_name: [[row1], [row2], ...]} + data_types = Column(JSON, nullable=True) # 数据类型信息:{sheet_name: {column: dtype}} + + # Statistics + total_rows = Column(JSON, nullable=True) # 每个sheet的总行数:{sheet_name: row_count} + total_columns = Column(JSON, nullable=True) # 每个sheet的总列数:{sheet_name: column_count} + + # Processing status + is_processed = Column(Boolean, default=True, nullable=False) # 是否已处理 + processing_error = Column(Text, nullable=True) # 处理错误信息 + + # Upload information + # upload_time = Column(DateTime, default=func.now(), nullable=False) # 上传时间 + last_accessed = Column(DateTime, nullable=True) # 最后访问时间 + + def __repr__(self): + return f"" + + @property + def file_size_mb(self): + """Get file size in MB.""" + return round(self.file_size / (1024 * 1024), 2) + + @property + def sheet_count(self): + """Get number of sheets.""" + return len(self.sheet_names) if self.sheet_names else 0 + + def get_sheet_info(self, sheet_name: str = None): + """Get information for a specific sheet or default sheet.""" + if not sheet_name: + sheet_name = self.default_sheet or (self.sheet_names[0] if self.sheet_names else None) + + if not sheet_name or sheet_name not in self.sheet_names: + return None + + return { + 'sheet_name': sheet_name, + 'columns': self.columns_info.get(sheet_name, []) if self.columns_info else [], + 'preview_data': self.preview_data.get(sheet_name, []) if self.preview_data else [], + 'data_types': self.data_types.get(sheet_name, {}) if self.data_types else {}, + 'total_rows': self.total_rows.get(sheet_name, 0) if self.total_rows else 0, + 'total_columns': self.total_columns.get(sheet_name, 0) if self.total_columns else 0 + } + + def get_all_sheets_summary(self): + """Get summary information for all sheets.""" + if not self.sheet_names: + return [] + + summary = [] + for sheet_name in self.sheet_names: + sheet_info = self.get_sheet_info(sheet_name) + if sheet_info: + summary.append({ + 'sheet_name': sheet_name, + 'columns_count': len(sheet_info['columns']), + 'rows_count': sheet_info['total_rows'], + 'columns': sheet_info['columns'][:10] # 只显示前10列 + }) + return summary \ No newline at end of file diff --git a/backend/th_agenter/models/knowledge_base.py b/backend/th_agenter/models/knowledge_base.py new file mode 100644 index 0000000..32ead50 --- /dev/null +++ b/backend/th_agenter/models/knowledge_base.py @@ -0,0 +1,92 @@ +"""Knowledge base models.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean, JSON, Float +from sqlalchemy.orm import relationship + +from ..db.base import BaseModel + + +class KnowledgeBase(BaseModel): + """Knowledge base model.""" + + __tablename__ = "knowledge_bases" + + name = Column(String(100), unique=False, index=True, nullable=False) + description = Column(Text, nullable=True) + embedding_model = Column(String(100), nullable=False, default="sentence-transformers/all-MiniLM-L6-v2") + chunk_size = Column(Integer, nullable=False, default=1000) + chunk_overlap = Column(Integer, nullable=False, default=200) + is_active = Column(Boolean, default=True, nullable=False) + + # Vector database settings + vector_db_type = Column(String(50), nullable=False, default="chroma") + collection_name = Column(String(100), nullable=True) # For vector DB collection + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def document_count(self): + """Get the number of documents in this knowledge base.""" + return len(self.documents) + + @property + def active_document_count(self): + """Get the number of active documents in this knowledge base.""" + return len([doc for doc in self.documents if doc.is_processed]) + + +class Document(BaseModel): + """Document model.""" + + __tablename__ = "documents" + + knowledge_base_id = Column(Integer, nullable=False) # Removed ForeignKey("knowledge_bases.id") + filename = Column(String(255), nullable=False) + original_filename = Column(String(255), nullable=False) + file_path = Column(String(500), nullable=False) + file_size = Column(Integer, nullable=False) # in bytes + file_type = Column(String(50), nullable=False) # .pdf, .txt, .docx, etc. + mime_type = Column(String(100), nullable=True) + + # Processing status + is_processed = Column(Boolean, default=False, nullable=False) + processing_error = Column(Text, nullable=True) + + # Content and metadata + content = Column(Text, nullable=True) # Extracted text content + doc_metadata = Column(JSON, nullable=True) # Additional metadata + + # Chunking information + chunk_count = Column(Integer, default=0, nullable=False) + + # Embedding information + embedding_model = Column(String(100), nullable=True) + vector_ids = Column(JSON, nullable=True) # Store vector database IDs for chunks + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def file_size_mb(self): + """Get file size in MB.""" + return round(self.file_size / (1024 * 1024), 2) + + @property + def is_text_file(self): + """Check if document is a text file.""" + return self.file_type.lower() in ['.txt', '.md', '.csv'] + + @property + def is_pdf_file(self): + """Check if document is a PDF file.""" + return self.file_type.lower() == '.pdf' + + @property + def is_office_file(self): + """Check if document is an Office file.""" + return self.file_type.lower() in ['.docx', '.xlsx', '.pptx'] \ No newline at end of file diff --git a/backend/th_agenter/models/llm_config.py b/backend/th_agenter/models/llm_config.py new file mode 100644 index 0000000..cfe631e --- /dev/null +++ b/backend/th_agenter/models/llm_config.py @@ -0,0 +1,165 @@ +"""LLM Configuration model for managing multiple AI models.""" + +from sqlalchemy import Column, String, Text, Boolean, Integer, Float, JSON +from sqlalchemy.orm import relationship +from typing import Dict, Any, Optional +import json + +from ..db.base import BaseModel + + +class LLMConfig(BaseModel): + """LLM Configuration model for managing AI model settings.""" + + __tablename__ = "llm_configs" + + name = Column(String(100), nullable=False, index=True) # 配置名称 + provider = Column(String(50), nullable=False, index=True) # 服务商:openai, deepseek, doubao, zhipu, moonshot, baidu + model_name = Column(String(100), nullable=False) # 模型名称 + api_key = Column(String(500), nullable=False) # API密钥(加密存储) + base_url = Column(String(200), nullable=True) # API基础URL + + # 模型参数 + max_tokens = Column(Integer, default=2048, nullable=False) + temperature = Column(Float, default=0.7, nullable=False) + top_p = Column(Float, default=1.0, nullable=False) + frequency_penalty = Column(Float, default=0.0, nullable=False) + presence_penalty = Column(Float, default=0.0, nullable=False) + + # 配置信息 + description = Column(Text, nullable=True) # 配置描述 + is_active = Column(Boolean, default=True, nullable=False) # 是否启用 + is_default = Column(Boolean, default=False, nullable=False) # 是否为默认配置 + is_embedding = Column(Boolean, default=False, nullable=False) # 是否为嵌入模型 + + # 扩展配置(JSON格式) + extra_config = Column(JSON, nullable=True) # 额外配置参数 + + # 使用统计 + usage_count = Column(Integer, default=0, nullable=False) # 使用次数 + last_used_at = Column(String(50), nullable=True) # 最后使用时间 + + def __repr__(self): + return f"" + + def to_dict(self, include_sensitive=False): + """Convert to dictionary, optionally excluding sensitive data.""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'provider': self.provider, + 'model_name': self.model_name, + 'base_url': self.base_url, + 'max_tokens': self.max_tokens, + 'temperature': self.temperature, + 'top_p': self.top_p, + 'frequency_penalty': self.frequency_penalty, + 'presence_penalty': self.presence_penalty, + 'description': self.description, + 'is_active': self.is_active, + 'is_default': self.is_default, + 'is_embedding': self.is_embedding, + 'extra_config': self.extra_config, + 'usage_count': self.usage_count, + 'last_used_at': self.last_used_at + }) + + if include_sensitive: + data['api_key'] = self.api_key + else: + # 只显示API密钥的前几位和后几位 + if self.api_key: + key_len = len(self.api_key) + if key_len > 8: + data['api_key_masked'] = f"{self.api_key[:4]}...{self.api_key[-4:]}" + else: + data['api_key_masked'] = "***" + else: + data['api_key_masked'] = None + + return data + + def get_client_config(self) -> Dict[str, Any]: + """获取用于创建客户端的配置.""" + config = { + 'api_key': self.api_key, + 'base_url': self.base_url, + 'model': self.model_name, + 'max_tokens': self.max_tokens, + 'temperature': self.temperature, + 'top_p': self.top_p, + 'frequency_penalty': self.frequency_penalty, + 'presence_penalty': self.presence_penalty + } + + # 添加额外配置 + if self.extra_config: + config.update(self.extra_config) + + return config + + def validate_config(self) -> Dict[str, Any]: + """验证配置是否有效.""" + if not self.name or not self.name.strip(): + return {"valid": False, "error": "配置名称不能为空"} + + if not self.provider or self.provider not in ['openai', 'deepseek', 'doubao', 'zhipu', 'moonshot', 'baidu']: + return {"valid": False, "error": "不支持的服务商"} + + if not self.model_name or not self.model_name.strip(): + return {"valid": False, "error": "模型名称不能为空"} + + if not self.api_key or not self.api_key.strip(): + return {"valid": False, "error": "API密钥不能为空"} + + if self.max_tokens <= 0 or self.max_tokens > 32000: + return {"valid": False, "error": "最大令牌数必须在1-32000之间"} + + if self.temperature < 0 or self.temperature > 2: + return {"valid": False, "error": "温度参数必须在0-2之间"} + + return {"valid": True, "error": None} + + def increment_usage(self): + """增加使用次数.""" + from datetime import datetime + self.usage_count += 1 + self.last_used_at = datetime.now().isoformat() + + @classmethod + def get_default_config(cls, provider: str, is_embedding: bool = False): + """获取服务商的默认配置模板.""" + templates = { + 'openai': { + 'base_url': 'https://api.openai.com/v1', + 'model_name': 'gpt-3.5-turbo' if not is_embedding else 'text-embedding-ada-002', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'deepseek': { + 'base_url': 'https://api.deepseek.com/v1', + 'model_name': 'deepseek-chat' if not is_embedding else 'deepseek-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'doubao': { + 'base_url': 'https://ark.cn-beijing.volces.com/api/v3', + 'model_name': 'doubao-lite-4k' if not is_embedding else 'doubao-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'zhipu': { + 'base_url': 'https://open.bigmodel.cn/api/paas/v4', + 'model_name': 'glm-4' if not is_embedding else 'embedding-3', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'moonshot': { + 'base_url': 'https://api.moonshot.cn/v1', + 'model_name': 'moonshot-v1-8k' if not is_embedding else 'moonshot-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + } + } + + return templates.get(provider, {}) \ No newline at end of file diff --git a/backend/th_agenter/models/message.py b/backend/th_agenter/models/message.py new file mode 100644 index 0000000..4ba2553 --- /dev/null +++ b/backend/th_agenter/models/message.py @@ -0,0 +1,69 @@ +"""Message model.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Enum, JSON +from sqlalchemy.orm import relationship +import enum + +from ..db.base import BaseModel + + +class MessageRole(str, enum.Enum): + """Message role enumeration.""" + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + + +class MessageType(str, enum.Enum): + """Message type enumeration.""" + TEXT = "text" + IMAGE = "image" + FILE = "file" + AUDIO = "audio" + + +class Message(BaseModel): + """Message model.""" + + __tablename__ = "messages" + + conversation_id = Column(Integer, nullable=False) # Removed ForeignKey("conversations.id") + role = Column(Enum(MessageRole), nullable=False) + content = Column(Text, nullable=False) + message_type = Column(Enum(MessageType), default=MessageType.TEXT, nullable=False) + message_metadata = Column(JSON, nullable=True) # Store additional data like file info, tokens used, etc. + + # For knowledge base context + context_documents = Column(JSON, nullable=True) # Store retrieved document references + + # Token usage tracking + prompt_tokens = Column(Integer, nullable=True) + completion_tokens = Column(Integer, nullable=True) + total_tokens = Column(Integer, nullable=True) + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + content_preview = self.content[:50] + "..." if len(self.content) > 50 else self.content + return f"" + + def to_dict(self, include_metadata=True): + """Convert to dictionary.""" + data = super().to_dict() + if not include_metadata: + data.pop('message_metadata', None) + data.pop('context_documents', None) + data.pop('prompt_tokens', None) + data.pop('completion_tokens', None) + data.pop('total_tokens', None) + return data + + @property + def is_from_user(self): + """Check if message is from user.""" + return self.role == MessageRole.USER + + @property + def is_from_assistant(self): + """Check if message is from assistant.""" + return self.role == MessageRole.ASSISTANT \ No newline at end of file diff --git a/backend/th_agenter/models/permission.py b/backend/th_agenter/models/permission.py new file mode 100644 index 0000000..a9fbf38 --- /dev/null +++ b/backend/th_agenter/models/permission.py @@ -0,0 +1,53 @@ +"""Role models for simplified RBAC system.""" + +from sqlalchemy import Column, String, Text, Boolean, ForeignKey, Integer +from sqlalchemy.orm import relationship +from typing import List, Dict, Any + +from ..db.base import BaseModel, Base + + +class Role(BaseModel): + """Role model for simplified RBAC system.""" + + __tablename__ = "roles" + + name = Column(String(100), nullable=False, unique=True, index=True) # 角色名称 + code = Column(String(100), nullable=False, unique=True, index=True) # 角色编码 + description = Column(Text, nullable=True) # 角色描述 + is_system = Column(Boolean, default=False, nullable=False) # 是否系统角色 + is_active = Column(Boolean, default=True, nullable=False) + + # 关系 - 只保留用户关系 + users = relationship("User", secondary="user_roles", back_populates="roles") + + def __repr__(self): + return f"" + + def to_dict(self): + """Convert to dictionary.""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'code': self.code, + 'description': self.description, + 'is_system': self.is_system, + 'is_active': self.is_active + }) + return data + + +class UserRole(Base): + """User role association model.""" + + __tablename__ = "user_roles" + + user_id = Column(Integer, ForeignKey('users.id'), primary_key=True) + role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) + + # 关系 - 用于直接操作关联表的场景 + user = relationship("User", viewonly=True) + role = relationship("Role", viewonly=True) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/th_agenter/models/table_metadata.py b/backend/th_agenter/models/table_metadata.py new file mode 100644 index 0000000..afa3926 --- /dev/null +++ b/backend/th_agenter/models/table_metadata.py @@ -0,0 +1,61 @@ +"""表元数据模型""" + +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON, ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from ..db.base import BaseModel + + +class TableMetadata(BaseModel): + """表元数据表""" + __tablename__ = "table_metadata" + + id = Column(Integer, primary_key=True, index=True) + # database_config_id = Column(Integer, ForeignKey('database_configs.id'), nullable=False) + table_name = Column(String(100), nullable=False, index=True) + table_schema = Column(String(50), default='public') + table_type = Column(String(20), default='BASE TABLE') + table_comment = Column(Text, nullable=True) # 表描述 + database_config_id = Column(Integer, nullable=True) #数据库配置ID + # 表结构信息 + columns_info = Column(JSON, nullable=False) # 列信息:名称、类型、注释等 + primary_keys = Column(JSON, nullable=True) # 主键列表 + foreign_keys = Column(JSON, nullable=True) # 外键信息 + indexes = Column(JSON, nullable=True) # 索引信息 + + # 示例数据 + sample_data = Column(JSON, nullable=True) # 前5条示例数据 + row_count = Column(Integer, default=0) # 总行数 + + # 问答相关 + is_enabled_for_qa = Column(Boolean, default=True) # 是否启用问答 + qa_description = Column(Text, nullable=True) # 问答描述 + business_context = Column(Text, nullable=True) # 业务上下文 + + last_synced_at = Column(DateTime(timezone=True), nullable=True) # 最后同步时间 + + # 关系 + # database_config = relationship("DatabaseConfig", back_populates="table_metadata") + + def to_dict(self): + return { + "id": self.id, + "created_by": self.created_by, # 改为created_by + "database_config_id": self.database_config_id, + "table_name": self.table_name, + "table_schema": self.table_schema, + "table_type": self.table_type, + "table_comment": self.table_comment, + "columns_info": self.columns_info, + "primary_keys": self.primary_keys, + # "foreign_keys": self.foreign_keys, + "indexes": self.indexes, + "sample_data": self.sample_data, + "row_count": self.row_count, + "is_enabled_for_qa": self.is_enabled_for_qa, + "qa_description": self.qa_description, + "business_context": self.business_context, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "last_synced_at": self.last_synced_at.isoformat() if self.last_synced_at else None + } \ No newline at end of file diff --git a/backend/th_agenter/models/user.py b/backend/th_agenter/models/user.py new file mode 100644 index 0000000..c69d632 --- /dev/null +++ b/backend/th_agenter/models/user.py @@ -0,0 +1,92 @@ +"""User model.""" + +from sqlalchemy import Column, String, Boolean, Text +from sqlalchemy.orm import relationship +from typing import List, Optional + +from ..db.base import BaseModel + + +class User(BaseModel): + """User model.""" + + __tablename__ = "users" + + username = Column(String(50), unique=True, index=True, nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + full_name = Column(String(100), nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + avatar_url = Column(String(255), nullable=True) + bio = Column(Text, nullable=True) + + # 关系 - 只保留角色关系 + roles = relationship("Role", secondary="user_roles", back_populates="users") + + def __repr__(self): + return f"" + + def to_dict(self, include_sensitive=False, include_roles=False): + """Convert to dictionary, optionally excluding sensitive data.""" + data = super().to_dict() + data.update({ + 'username': self.username, + 'email': self.email, + 'full_name': self.full_name, + 'is_active': self.is_active, + 'avatar_url': self.avatar_url, + 'bio': self.bio, + 'is_superuser': self.is_superuser() + }) + + if not include_sensitive: + data.pop('hashed_password', None) + + if include_roles: + data['roles'] = [role.to_dict() for role in self.roles if role.is_active] + + return data + + def has_role(self, role_code: str) -> bool: + """检查用户是否拥有指定角色.""" + try: + return any(role.code == role_code and role.is_active for role in self.roles) + except Exception: + # 如果对象已分离,使用数据库查询 + from sqlalchemy.orm import object_session + from .permission import Role, UserRole + + session = object_session(self) + if session is None: + # 如果没有会话,创建新的会话 + from ..db.database import SessionLocal + session = SessionLocal() + try: + user_role = session.query(UserRole).join(Role).filter( + UserRole.user_id == self.id, + Role.code == role_code, + Role.is_active == True + ).first() + return user_role is not None + finally: + session.close() + else: + user_role = session.query(UserRole).join(Role).filter( + UserRole.user_id == self.id, + Role.code == role_code, + Role.is_active == True + ).first() + return user_role is not None + + def is_superuser(self) -> bool: + """检查用户是否为超级管理员.""" + return self.has_role('SUPER_ADMIN') + + def is_admin_user(self) -> bool: + """检查用户是否为管理员(兼容性方法).""" + return self.is_superuser() + + @property + def is_admin(self) -> bool: + """检查用户是否为管理员(属性方式).""" + return self.is_superuser() \ No newline at end of file diff --git a/backend/th_agenter/models/workflow.py b/backend/th_agenter/models/workflow.py new file mode 100644 index 0000000..a5e3dda --- /dev/null +++ b/backend/th_agenter/models/workflow.py @@ -0,0 +1,175 @@ +"""Workflow models.""" + +from sqlalchemy import Column, String, Text, Boolean, Integer, JSON, ForeignKey, Enum +from sqlalchemy.orm import relationship +from typing import Dict, Any, Optional, List +import enum + +from ..db.base import BaseModel + + +class WorkflowStatus(enum.Enum): + """工作流状态枚举""" + DRAFT = "DRAFT" # 草稿 + PUBLISHED = "PUBLISHED" # 已发布 + ARCHIVED = "ARCHIVED" # 已归档 + + +class NodeType(enum.Enum): + """节点类型枚举""" + START = "start" # 开始节点 + END = "end" # 结束节点 + LLM = "llm" # 大模型节点 + CONDITION = "condition" # 条件分支节点 + LOOP = "loop" # 循环节点 + CODE = "code" # 代码执行节点 + HTTP = "http" # HTTP请求节点 + TOOL = "tool" # 工具节点 + + +class ExecutionStatus(enum.Enum): + """执行状态枚举""" + PENDING = "pending" # 等待执行 + RUNNING = "running" # 执行中 + COMPLETED = "completed" # 执行完成 + FAILED = "failed" # 执行失败 + CANCELLED = "cancelled" # 已取消 + + +class Workflow(BaseModel): + """工作流模型""" + + __tablename__ = "workflows" + + name = Column(String(100), nullable=False, comment="工作流名称") + description = Column(Text, nullable=True, comment="工作流描述") + status = Column(Enum(WorkflowStatus), default=WorkflowStatus.DRAFT, nullable=False, comment="工作流状态") + is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") + + # 工作流定义(JSON格式存储节点和连接信息) + definition = Column(JSON, nullable=False, comment="工作流定义") + + # 版本信息 + version = Column(String(20), default="1.0.0", nullable=False, comment="版本号") + + # 关联用户 + owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, comment="所有者ID") + + # 关系 + executions = relationship("WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + def to_dict(self, include_definition=True): + """转换为字典""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'description': self.description, + 'status': self.status.value, + 'is_active': self.is_active, + 'version': self.version, + 'owner_id': self.owner_id + }) + + if include_definition: + data['definition'] = self.definition + + return data + + +class WorkflowExecution(BaseModel): + """工作流执行记录""" + + __tablename__ = "workflow_executions" + + workflow_id = Column(Integer, ForeignKey("workflows.id"), nullable=False, comment="工作流ID") + status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False, comment="执行状态") + + # 执行输入和输出 + input_data = Column(JSON, nullable=True, comment="输入数据") + output_data = Column(JSON, nullable=True, comment="输出数据") + + # 执行信息 + started_at = Column(String(50), nullable=True, comment="开始时间") + completed_at = Column(String(50), nullable=True, comment="完成时间") + error_message = Column(Text, nullable=True, comment="错误信息") + + # 执行者 + executor_id = Column(Integer, ForeignKey("users.id"), nullable=False, comment="执行者ID") + + # 关系 + workflow = relationship("Workflow", back_populates="executions") + node_executions = relationship("NodeExecution", back_populates="workflow_execution", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + def to_dict(self, include_nodes=False): + """转换为字典""" + data = super().to_dict() + data.update({ + 'workflow_id': self.workflow_id, + 'status': self.status.value, + 'input_data': self.input_data, + 'output_data': self.output_data, + 'started_at': self.started_at, + 'completed_at': self.completed_at, + 'error_message': self.error_message, + 'executor_id': self.executor_id + }) + + if include_nodes: + data['node_executions'] = [node.to_dict() for node in self.node_executions] + + return data + + +class NodeExecution(BaseModel): + """节点执行记录""" + + __tablename__ = "node_executions" + + workflow_execution_id = Column(Integer, ForeignKey("workflow_executions.id"), nullable=False, comment="工作流执行ID") + node_id = Column(String(50), nullable=False, comment="节点ID") + node_type = Column(Enum(NodeType), nullable=False, comment="节点类型") + node_name = Column(String(100), nullable=False, comment="节点名称") + + # 执行状态和结果 + status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False, comment="执行状态") + input_data = Column(JSON, nullable=True, comment="输入数据") + output_data = Column(JSON, nullable=True, comment="输出数据") + + # 执行时间 + started_at = Column(String(50), nullable=True, comment="开始时间") + completed_at = Column(String(50), nullable=True, comment="完成时间") + duration_ms = Column(Integer, nullable=True, comment="执行时长(毫秒)") + + # 错误信息 + error_message = Column(Text, nullable=True, comment="错误信息") + + # 关系 + workflow_execution = relationship("WorkflowExecution", back_populates="node_executions") + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典""" + data = super().to_dict() + data.update({ + 'workflow_execution_id': self.workflow_execution_id, + 'node_id': self.node_id, + 'node_type': self.node_type.value, + 'node_name': self.node_name, + 'status': self.status.value, + 'input_data': self.input_data, + 'output_data': self.output_data, + 'started_at': self.started_at, + 'completed_at': self.completed_at, + 'duration_ms': self.duration_ms, + 'error_message': self.error_message + }) + + return data \ No newline at end of file diff --git a/backend/th_agenter/schemas/__init__.py b/backend/th_agenter/schemas/__init__.py new file mode 100644 index 0000000..ff37910 --- /dev/null +++ b/backend/th_agenter/schemas/__init__.py @@ -0,0 +1,16 @@ +"""Schemas package initialization.""" + +from .user import UserCreate, UserUpdate, UserResponse +from .permission import ( + RoleCreate, RoleUpdate, RoleResponse, + UserRoleAssign +) + +__all__ = [ + # User schemas + "UserCreate", "UserUpdate", "UserResponse", + + # Permission schemas + "RoleCreate", "RoleUpdate", "RoleResponse", + "UserRoleAssign", +] \ No newline at end of file diff --git a/backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d9ef9f4 Binary files /dev/null and b/backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/schemas/__pycache__/llm_config.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/llm_config.cpython-313.pyc new file mode 100644 index 0000000..dafa964 Binary files /dev/null and b/backend/th_agenter/schemas/__pycache__/llm_config.cpython-313.pyc differ diff --git a/backend/th_agenter/schemas/__pycache__/permission.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/permission.cpython-313.pyc new file mode 100644 index 0000000..59fc6eb Binary files /dev/null and b/backend/th_agenter/schemas/__pycache__/permission.cpython-313.pyc differ diff --git a/backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..63c39e4 Binary files /dev/null and b/backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc differ diff --git a/backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000..07e6b57 Binary files /dev/null and b/backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/schemas/llm_config.py b/backend/th_agenter/schemas/llm_config.py new file mode 100644 index 0000000..165cf78 --- /dev/null +++ b/backend/th_agenter/schemas/llm_config.py @@ -0,0 +1,152 @@ +"""LLM Configuration Pydantic schemas.""" + +from typing import Optional, Dict, Any +from pydantic import BaseModel, Field, validator +from datetime import datetime + + +class LLMConfigBase(BaseModel): + """大模型配置基础模式.""" + name: str = Field(..., min_length=1, max_length=100, description="配置名称") + provider: str = Field(..., min_length=1, max_length=50, description="服务商") + model_name: str = Field(..., min_length=1, max_length=100, description="模型名称") + api_key: str = Field(..., min_length=1, description="API密钥") + base_url: Optional[str] = Field(None, description="API基础URL") + max_tokens: Optional[int] = Field(4096, ge=1, le=32000, description="最大令牌数") + temperature: Optional[float] = Field(0.7, ge=0.0, le=2.0, description="温度参数") + top_p: Optional[float] = Field(1.0, ge=0.0, le=1.0, description="Top-p参数") + frequency_penalty: Optional[float] = Field(0.0, ge=-2.0, le=2.0, description="频率惩罚") + presence_penalty: Optional[float] = Field(0.0, ge=-2.0, le=2.0, description="存在惩罚") + description: Optional[str] = Field(None, max_length=500, description="配置描述") + + is_active: bool = Field(True, description="是否激活") + is_default: bool = Field(False, description="是否为默认配置") + is_embedding: bool = Field(False, description="是否为嵌入模型") + extra_config: Optional[Dict[str, Any]] = Field(None, description="额外配置") + + +class LLMConfigCreate(LLMConfigBase): + """创建大模型配置模式.""" + + @validator('provider') + def validate_provider(cls, v): + allowed_providers = [ + 'openai', 'azure', 'anthropic', 'google', 'baidu', + 'alibaba', 'tencent', 'zhipu', 'moonshot', 'deepseek', + 'ollama', 'custom', "doubao" + ] + if v.lower() not in allowed_providers: + raise ValueError(f'不支持的服务商: {v},支持的服务商: {", ".join(allowed_providers)}') + return v.lower() + + @validator('api_key') + def validate_api_key(cls, v): + if len(v.strip()) < 10: + raise ValueError('API密钥长度不能少于10个字符') + return v.strip() + + +class LLMConfigUpdate(BaseModel): + """更新大模型配置模式.""" + name: Optional[str] = Field(None, min_length=1, max_length=100, description="配置名称") + provider: Optional[str] = Field(None, min_length=1, max_length=50, description="服务商") + model_name: Optional[str] = Field(None, min_length=1, max_length=100, description="模型名称") + api_key: Optional[str] = Field(None, min_length=1, description="API密钥") + base_url: Optional[str] = Field(None, description="API基础URL") + max_tokens: Optional[int] = Field(None, ge=1, le=32000, description="最大令牌数") + temperature: Optional[float] = Field(None, ge=0.0, le=2.0, description="温度参数") + top_p: Optional[float] = Field(None, ge=0.0, le=1.0, description="Top-p参数") + frequency_penalty: Optional[float] = Field(None, ge=-2.0, le=2.0, description="频率惩罚") + presence_penalty: Optional[float] = Field(None, ge=-2.0, le=2.0, description="存在惩罚") + description: Optional[str] = Field(None, max_length=500, description="配置描述") + + is_active: Optional[bool] = Field(None, description="是否激活") + is_default: Optional[bool] = Field(None, description="是否为默认配置") + is_embedding: Optional[bool] = Field(None, description="是否为嵌入模型") + extra_config: Optional[Dict[str, Any]] = Field(None, description="额外配置") + + @validator('provider') + def validate_provider(cls, v): + if v is not None: + allowed_providers = [ + 'openai', 'azure', 'anthropic', 'google', 'baidu', + 'alibaba', 'tencent', 'zhipu', 'moonshot', 'deepseek', + 'ollama', 'custom',"doubao" + ] + if v.lower() not in allowed_providers: + raise ValueError(f'不支持的服务商: {v},支持的服务商: {", ".join(allowed_providers)}') + return v.lower() + return v + + @validator('api_key') + def validate_api_key(cls, v): + if v is not None and len(v.strip()) < 10: + raise ValueError('API密钥长度不能少于10个字符') + return v.strip() if v else v + + +class LLMConfigResponse(BaseModel): + """大模型配置响应模式.""" + id: int + name: str + provider: str + model_name: str + api_key: Optional[str] = None # 完整的API密钥(仅在include_sensitive=True时返回) + base_url: Optional[str] = None + max_tokens: Optional[int] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + frequency_penalty: Optional[float] = None + presence_penalty: Optional[float] = None + description: Optional[str] = None + + is_active: bool + is_default: bool + is_embedding: bool + extra_config: Optional[Dict[str, Any]] = None + created_at: datetime + updated_at: Optional[datetime] = None + created_by: Optional[int] = None + updated_by: Optional[int] = None + + # 敏感信息处理 + api_key_masked: Optional[str] = None + + class Config: + from_attributes = True + + @validator('api_key_masked', pre=True, always=True) + def mask_api_key(cls, v, values): + # 在响应中隐藏API密钥,只显示前4位和后4位 + if 'api_key' in values and values['api_key']: + key = values['api_key'] + if len(key) > 8: + return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}" + else: + return '*' * len(key) + return None + + +class LLMConfigTest(BaseModel): + """大模型配置测试模式.""" + message: Optional[str] = Field( + "Hello, this is a test message.", + max_length=1000, + description="测试消息" + ) + + +class LLMConfigClientResponse(BaseModel): + """大模型配置客户端响应模式(用于前端).""" + id: int + name: str + provider: str + model_name: str + max_tokens: Optional[int] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + is_active: bool + description: Optional[str] = None + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/th_agenter/schemas/permission.py b/backend/th_agenter/schemas/permission.py new file mode 100644 index 0000000..553999b --- /dev/null +++ b/backend/th_agenter/schemas/permission.py @@ -0,0 +1,68 @@ +"""Role Pydantic schemas.""" + +from typing import Optional, List +from pydantic import BaseModel, Field, validator +from datetime import datetime + + +class RoleBase(BaseModel): + """角色基础模式.""" + name: str = Field(..., min_length=1, max_length=100, description="角色名称") + code: str = Field(..., min_length=1, max_length=50, description="角色代码") + description: Optional[str] = Field(None, max_length=500, description="角色描述") + sort_order: Optional[int] = Field(0, ge=0, description="排序") + is_active: bool = Field(True, description="是否激活") + + +class RoleCreate(RoleBase): + """创建角色模式.""" + + @validator('code') + def validate_code(cls, v): + if not v.replace('_', '').replace('-', '').isalnum(): + raise ValueError('角色代码只能包含字母、数字、下划线和连字符') + return v.upper() + + +class RoleUpdate(BaseModel): + """更新角色模式.""" + name: Optional[str] = Field(None, min_length=1, max_length=100, description="角色名称") + code: Optional[str] = Field(None, min_length=1, max_length=50, description="角色代码") + description: Optional[str] = Field(None, max_length=500, description="角色描述") + sort_order: Optional[int] = Field(None, ge=0, description="排序") + is_active: Optional[bool] = Field(None, description="是否激活") + + @validator('code') + def validate_code(cls, v): + if v is not None and not v.replace('_', '').replace('-', '').isalnum(): + raise ValueError('角色代码只能包含字母、数字、下划线和连字符') + return v.upper() if v else v + + +class RoleResponse(RoleBase): + """角色响应模式.""" + id: int + created_at: datetime + updated_at: Optional[datetime] = None + created_by: Optional[int] = None + updated_by: Optional[int] = None + + # 关联信息 + user_count: Optional[int] = 0 + + class Config: + from_attributes = True + + +class UserRoleAssign(BaseModel): + """用户角色分配模式.""" + user_id: int = Field(..., description="用户ID") + role_ids: List[int] = Field(..., description="角色ID列表") + + @validator('role_ids') + def validate_role_ids(cls, v): + if not v: + raise ValueError('角色ID列表不能为空') + if len(v) != len(set(v)): + raise ValueError('角色ID列表不能包含重复项') + return v \ No newline at end of file diff --git a/backend/th_agenter/schemas/user.py b/backend/th_agenter/schemas/user.py new file mode 100644 index 0000000..7057e8d --- /dev/null +++ b/backend/th_agenter/schemas/user.py @@ -0,0 +1,61 @@ +"""User schemas.""" + +from typing import Optional, List, Dict, Any +from pydantic import BaseModel, Field +from datetime import datetime + +from ..utils.schemas import BaseResponse + + +class UserBase(BaseModel): + """User base schema.""" + username: str = Field(..., min_length=3, max_length=50) + email: str = Field(..., max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + + +class UserCreate(UserBase): + """User creation schema.""" + password: str = Field(..., min_length=6) + + +class UserUpdate(BaseModel): + """User update schema.""" + username: Optional[str] = Field(None, min_length=3, max_length=50) + email: Optional[str] = Field(None, max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + password: Optional[str] = Field(None, min_length=6) + is_active: Optional[bool] = None + + +class ChangePasswordRequest(BaseModel): + """Change password request schema.""" + current_password: str = Field(..., description="Current password") + new_password: str = Field(..., min_length=6, description="New password") + + +class ResetPasswordRequest(BaseModel): + """Admin reset password request schema.""" + new_password: str = Field(..., min_length=6, description="New password") + + +class UserResponse(BaseResponse, UserBase): + """User response schema.""" + is_active: bool + is_superuser: Optional[bool] = Field(default=False, description="是否为超级管理员") + + class Config: + from_attributes = True + + @classmethod + def from_orm(cls, obj): + """从ORM对象创建响应模型,正确处理is_superuser方法""" + data = obj.__dict__.copy() + # 调用is_superuser方法获取布尔值 + if hasattr(obj, 'is_superuser') and callable(obj.is_superuser): + data['is_superuser'] = obj.is_superuser() + return cls(**data) \ No newline at end of file diff --git a/backend/th_agenter/schemas/workflow.py b/backend/th_agenter/schemas/workflow.py new file mode 100644 index 0000000..46d96cc --- /dev/null +++ b/backend/th_agenter/schemas/workflow.py @@ -0,0 +1,231 @@ +"""Workflow schemas.""" + +from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, List +from datetime import datetime +from enum import Enum + + +class WorkflowStatus(str, Enum): + """工作流状态枚举""" + DRAFT = "DRAFT" + PUBLISHED = "PUBLISHED" + ARCHIVED = "ARCHIVED" + + +class NodeType(str, Enum): + """节点类型""" + START = "start" + END = "end" + LLM = "llm" + CONDITION = "condition" + LOOP = "loop" + CODE = "code" + HTTP = "http" + TOOL = "tool" + + +class ExecutionStatus(str, Enum): + """执行状态""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +# 节点定义相关模式 +class NodePosition(BaseModel): + """节点位置""" + x: float + y: float + + +# 参数定义相关模式 +class ParameterType(str, Enum): + """参数类型""" + STRING = "string" + NUMBER = "number" + BOOLEAN = "boolean" + OBJECT = "object" + ARRAY = "array" + + +class NodeParameter(BaseModel): + """节点参数定义""" + name: str = Field(..., min_length=1, max_length=50) + type: ParameterType + description: Optional[str] = None + required: bool = True + default_value: Optional[Any] = None + source: Optional[str] = None # 参数来源:'input'(用户输入), 'node'(其他节点输出), 'variable'(变量引用) + source_node_id: Optional[str] = None # 来源节点ID(当source为'node'时) + source_field: Optional[str] = None # 来源字段名 + variable_name: Optional[str] = None # 变量名称(用于结束节点的输出参数) + + +class NodeInputOutput(BaseModel): + """节点输入输出定义""" + inputs: List[NodeParameter] = [] + outputs: List[NodeParameter] = [] + + +class NodeConfig(BaseModel): + """节点配置基类""" + pass + + +class LLMNodeConfig(NodeConfig): + """LLM节点配置""" + model_id: Optional[int] = None # 大模型配置ID + model_name: Optional[str] = None # 模型名称(兼容前端) + temperature: float = Field(default=0.7, ge=0, le=2) + max_tokens: Optional[int] = Field(default=None, gt=0) + prompt: str = Field(..., min_length=1) + enable_variable_substitution: bool = True # 是否启用变量替换 + + +class ConditionNodeConfig(NodeConfig): + """条件节点配置""" + condition: str = Field(..., min_length=1) + + +class LoopNodeConfig(NodeConfig): + """循环节点配置""" + loop_type: str = Field(..., pattern="^(count|while|foreach)$") + count: Optional[int] = Field(None, description="循环次数(当loop_type为count时)") + condition: Optional[str] = Field(None, description="循环条件(当loop_type为while时)") + iterable: Optional[str] = Field(None, description="可迭代对象(当loop_type为foreach时)") + + +class CodeNodeConfig(NodeConfig): + """代码执行节点配置""" + language: str = Field(..., pattern="^(python|javascript)$") + code: str = Field(..., min_length=1) + + +class HttpNodeConfig(NodeConfig): + """HTTP请求节点配置""" + method: str = Field(..., pattern="^(GET|POST|PUT|DELETE|PATCH)$") + url: str = Field(..., min_length=1) + headers: Optional[Dict[str, str]] = None + body: Optional[str] = None + + +class ToolNodeConfig(NodeConfig): + """工具节点配置""" + tool_type: str + parameters: Optional[Dict[str, Any]] = None + + +class WorkflowNode(BaseModel): + """工作流节点""" + id: str + type: NodeType + name: str + description: Optional[str] = None + position: NodePosition + config: Optional[Dict[str, Any]] = None + parameters: Optional[NodeInputOutput] = None # 节点输入输出参数定义 + + +class WorkflowConnection(BaseModel): + """工作流连接""" + id: str + from_node: str = Field(..., alias="from") + to_node: str = Field(..., alias="to") + from_point: str = Field(default="output") + to_point: str = Field(default="input") + + +class WorkflowDefinition(BaseModel): + """工作流定义""" + nodes: List[WorkflowNode] + connections: List[WorkflowConnection] + + +# 工作流CRUD模式 +class WorkflowCreate(BaseModel): + """创建工作流""" + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = None + definition: WorkflowDefinition + status: WorkflowStatus = WorkflowStatus.DRAFT + + +class WorkflowUpdate(BaseModel): + """更新工作流""" + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = None + definition: Optional[WorkflowDefinition] = None + status: Optional[WorkflowStatus] = None + is_active: Optional[bool] = None + + +class WorkflowResponse(BaseModel): + """工作流响应""" + id: int + name: str + description: Optional[str] + status: WorkflowStatus + is_active: bool + version: str + owner_id: int + definition: Optional[WorkflowDefinition] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# 工作流执行相关模式 +class WorkflowExecuteRequest(BaseModel): + """工作流执行请求""" + input_data: Optional[Dict[str, Any]] = None + + +class NodeExecutionResponse(BaseModel): + """节点执行响应""" + id: int + node_id: str + node_type: NodeType + node_name: str + status: ExecutionStatus + input_data: Optional[Dict[str, Any]] + output_data: Optional[Dict[str, Any]] + started_at: Optional[str] + completed_at: Optional[str] + duration_ms: Optional[int] + error_message: Optional[str] + + class Config: + from_attributes = True + + +class WorkflowExecutionResponse(BaseModel): + """工作流执行响应""" + id: int + workflow_id: int + status: ExecutionStatus + input_data: Optional[Dict[str, Any]] + output_data: Optional[Dict[str, Any]] + started_at: Optional[str] + completed_at: Optional[str] + error_message: Optional[str] + executor_id: int + node_executions: Optional[List[NodeExecutionResponse]] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# 工作流列表响应 +class WorkflowListResponse(BaseModel): + """工作流列表响应""" + workflows: List[WorkflowResponse] + total: int + page: int + size: int \ No newline at end of file diff --git a/backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc b/backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc new file mode 100644 index 0000000..22040b2 Binary files /dev/null and b/backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc differ diff --git a/backend/th_agenter/scripts/init_system.py b/backend/th_agenter/scripts/init_system.py new file mode 100644 index 0000000..f43e293 --- /dev/null +++ b/backend/th_agenter/scripts/init_system.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Initialize system management functionality.""" + +import sys +import os +from pathlib import Path + +# Add the backend directory to Python path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from th_agenter.core.config import settings +from th_agenter.db.database import Base, create_database_engine +from th_agenter.db import database +from th_agenter.db.init_system_data import init_system_data +from th_agenter.models import * # Import all models to ensure they're registered +from th_agenter.utils.logger import get_logger + +logger = get_logger(__name__) + + +def create_tables(): + """Create all database tables.""" + logger.info("Creating database tables...") + try: + Base.metadata.create_all(bind=database.engine) + logger.info("Database tables created successfully") + except Exception as e: + logger.error(f"Error creating tables: {str(e)}") + raise + + +def init_database(): + """Initialize database with system data.""" + logger.info("Initializing database with system data...") + + # Create session + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=database.engine) + db = SessionLocal() + + try: + # Initialize system data + init_system_data(db) + logger.info("Database initialization completed successfully") + + except Exception as e: + logger.error(f"Error initializing database: {str(e)}") + db.rollback() + raise + finally: + db.close() + + +def check_database_connection(): + """Check database connection.""" + logger.info("Checking database connection...") + try: + with database.engine.connect() as conn: + result = conn.execute(text("SELECT 1")) + logger.info("Database connection successful") + return True + except Exception as e: + logger.error(f"Database connection failed: {str(e)}") + return False + + +def main(): + """Main initialization function.""" + logger.info("Starting system initialization...") + + try: + # Initialize database engine + create_database_engine() + + # Check database connection + if not check_database_connection(): + logger.error("Cannot connect to database. Please check your database configuration.") + sys.exit(1) + + # Create tables + create_tables() + + # Initialize system data + init_database() + + logger.info("System initialization completed successfully!") + print("\n✅ System initialization completed successfully!") + print("\n📋 What was initialized:") + print(" • Database tables for system management") + print(" • Default permissions and roles") + print(" • Default departments structure") + print(" • System configuration") + print("\n🚀 You can now start the application and access the system management features.") + + except Exception as e: + logger.error(f"System initialization failed: {str(e)}") + print(f"\n❌ System initialization failed: {str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/th_agenter/services/__init__.py b/backend/th_agenter/services/__init__.py new file mode 100644 index 0000000..0c19635 --- /dev/null +++ b/backend/th_agenter/services/__init__.py @@ -0,0 +1 @@ +"""Services package.""" \ No newline at end of file diff --git a/backend/th_agenter/services/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..481e688 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc b/backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc new file mode 100644 index 0000000..2e79e3a Binary files /dev/null and b/backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/auth.cpython-313.pyc b/backend/th_agenter/services/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..87840e1 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/chat.cpython-313.pyc new file mode 100644 index 0000000..d92f032 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/chat.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc b/backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000..c523063 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc b/backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc new file mode 100644 index 0000000..68f9ce4 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc new file mode 100644 index 0000000..c9a700a Binary files /dev/null and b/backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/document.cpython-313.pyc b/backend/th_agenter/services/__pycache__/document.cpython-313.pyc new file mode 100644 index 0000000..7f31584 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/document.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc b/backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc new file mode 100644 index 0000000..9a92723 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/embedding_factory.cpython-313.pyc b/backend/th_agenter/services/__pycache__/embedding_factory.cpython-313.pyc new file mode 100644 index 0000000..2c87661 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/embedding_factory.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/excel_metadata_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/excel_metadata_service.cpython-313.pyc new file mode 100644 index 0000000..065d218 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/excel_metadata_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc b/backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc new file mode 100644 index 0000000..56ee1e9 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc new file mode 100644 index 0000000..f712ec7 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc new file mode 100644 index 0000000..2a0f604 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/llm_config_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/llm_config_service.cpython-313.pyc new file mode 100644 index 0000000..5fe5cd4 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/llm_config_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc new file mode 100644 index 0000000..badb7fc Binary files /dev/null and b/backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc b/backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc new file mode 100644 index 0000000..e67c538 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc b/backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc new file mode 100644 index 0000000..cf49969 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/smart_db_workflow.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_db_workflow.cpython-313.pyc new file mode 100644 index 0000000..a55b247 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/smart_db_workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc new file mode 100644 index 0000000..e91527e Binary files /dev/null and b/backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/smart_query.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_query.cpython-313.pyc new file mode 100644 index 0000000..2d37383 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/smart_query.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc new file mode 100644 index 0000000..6905909 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/storage.cpython-313.pyc b/backend/th_agenter/services/__pycache__/storage.cpython-313.pyc new file mode 100644 index 0000000..497e474 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/storage.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/table_metadata_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/table_metadata_service.cpython-313.pyc new file mode 100644 index 0000000..ea53b3f Binary files /dev/null and b/backend/th_agenter/services/__pycache__/table_metadata_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/user.cpython-313.pyc b/backend/th_agenter/services/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..21dd9f0 Binary files /dev/null and b/backend/th_agenter/services/__pycache__/user.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/workflow_engine.cpython-313.pyc b/backend/th_agenter/services/__pycache__/workflow_engine.cpython-313.pyc new file mode 100644 index 0000000..499cd2b Binary files /dev/null and b/backend/th_agenter/services/__pycache__/workflow_engine.cpython-313.pyc differ diff --git a/backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc b/backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc new file mode 100644 index 0000000..30ef39a Binary files /dev/null and b/backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc differ diff --git a/backend/th_agenter/services/agent/__init__.py b/backend/th_agenter/services/agent/__init__.py new file mode 100644 index 0000000..62f5fd3 --- /dev/null +++ b/backend/th_agenter/services/agent/__init__.py @@ -0,0 +1,10 @@ +"""Agent services package.""" + +from .base import BaseTool, ToolRegistry +from .agent_service import AgentService + +__all__ = [ + "BaseTool", + "ToolRegistry", + "AgentService" +] \ No newline at end of file diff --git a/backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6747786 Binary files /dev/null and b/backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/services/agent/__pycache__/agent_service.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/agent_service.cpython-313.pyc new file mode 100644 index 0000000..a9f73e2 Binary files /dev/null and b/backend/th_agenter/services/agent/__pycache__/agent_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000..4d78439 Binary files /dev/null and b/backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc differ diff --git a/backend/th_agenter/services/agent/__pycache__/langgraph_agent_service.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/langgraph_agent_service.cpython-313.pyc new file mode 100644 index 0000000..040856a Binary files /dev/null and b/backend/th_agenter/services/agent/__pycache__/langgraph_agent_service.cpython-313.pyc differ diff --git a/backend/th_agenter/services/agent/agent_service.py b/backend/th_agenter/services/agent/agent_service.py new file mode 100644 index 0000000..41886f0 --- /dev/null +++ b/backend/th_agenter/services/agent/agent_service.py @@ -0,0 +1,468 @@ +"""LangChain Agent service with tool calling capabilities.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from langchain_core.tools import BaseTool as LangChainBaseTool +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.messages import HumanMessage, AIMessage + +from ...utils.logger import get_logger +logger = get_logger("agent_service") + +# Try to import langchain_classic with exception handling +try: + from langchain_classic.agents import AgentExecutor + from langchain_classic.agents.tool_calling_agent.base import create_tool_calling_agent + LANGCHAIN_CLASSIC_AVAILABLE = True +except ImportError: + logger.warning("langchain_classic not available. Agent functionality will be disabled.") + AgentExecutor = None + create_tool_calling_agent = None + LANGCHAIN_CLASSIC_AVAILABLE = False +from pydantic import BaseModel, Field + +from .base import BaseTool, ToolRegistry, ToolResult +from th_agenter.services.tools import WeatherQueryTool, TavilySearchTool, DateTimeTool +from ..postgresql_tool_manager import get_postgresql_tool +from ..mysql_tool_manager import get_mysql_tool +from ...core.config import get_settings +from ..agent_config import AgentConfigService + + +class LangChainToolWrapper(LangChainBaseTool): + """Wrapper to convert our BaseTool to LangChain tool.""" + + name: str = Field(...) + description: str = Field(...) + base_tool: BaseTool = Field(...) + + def __init__(self, base_tool: BaseTool, **kwargs): + super().__init__( + name=base_tool.get_name(), + description=base_tool.get_description(), + base_tool=base_tool, + **kwargs + ) + + def _run(self, *args, **kwargs) -> str: + """Synchronous run method.""" + # Handle both positional and keyword arguments + if args: + # If positional arguments are provided, convert them to kwargs + # based on the tool's parameter names + params = self.base_tool.get_parameters() + for i, arg in enumerate(args): + if i < len(params): + kwargs[params[i].name] = arg + + # Run async method in sync context + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + result = loop.run_until_complete(self.base_tool.execute(**kwargs)) + return self._format_result(result) + finally: + loop.close() + + async def _arun(self, *args, **kwargs) -> str: + """Asynchronous run method.""" + # Handle both positional and keyword arguments + if args: + # If positional arguments are provided, convert them to kwargs + # based on the tool's parameter names + params = self.base_tool.get_parameters() + for i, arg in enumerate(args): + if i < len(params): + kwargs[params[i].name] = arg + + result = await self.base_tool.execute(**kwargs) + return self._format_result(result) + + def _format_result(self, result: ToolResult) -> str: + """Format tool result for LangChain.""" + if result.success: + if isinstance(result.result, dict) and "summary" in result.result: + return result.result["summary"] + return str(result.result) + else: + return f"Error: {result.error}" + + +class AgentConfig(BaseModel): + """Agent configuration.""" + enabled_tools: List[str] = Field(default_factory=lambda: [ + "calculator", "weather", "search", "datetime", "file", "generate_image", "postgresql_mcp", "mysql_mcp" + ]) + max_iterations: int = Field(default=10) + temperature: float = Field(default=0.1) + system_message: str = Field( + default="You are a helpful AI assistant with access to various tools. " + "Use the available tools to help answer user questions accurately. " + "Always explain your reasoning and the tools you're using." + ) + verbose: bool = Field(default=True) + + +class AgentService: + """LangChain Agent service with tool calling capabilities.""" + + def __init__(self, db_session=None): + self.settings = get_settings() + self.tool_registry = ToolRegistry() + self.config = AgentConfig() + self.agent_executor: Optional[AgentExecutor] = None + self.db_session = db_session + self.config_service = AgentConfigService(db_session) if db_session else None + self._initialize_tools() + self._load_config() + + def _initialize_tools(self): + """Initialize and register all available tools.""" + tools = [ + WeatherQueryTool(), + TavilySearchTool(), + DateTimeTool(), + get_postgresql_tool(), # 使用单例PostgreSQL MCP工具 + get_mysql_tool() # 使用单例MySQL MCP工具 + ] + + for tool in tools: + self.tool_registry.register(tool) + logger.info(f"Registered tool: {tool.get_name()}") + + def _load_config(self): + """Load configuration from database if available.""" + if self.config_service: + try: + config_dict = self.config_service.get_config_dict() + # Update config with database values + for key, value in config_dict.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info("Loaded agent configuration from database") + except Exception as e: + logger.warning(f"Failed to load config from database, using defaults: {str(e)}") + + def _get_enabled_tools(self) -> List[LangChainToolWrapper]: + """Get list of enabled LangChain tools.""" + enabled_tools = [] + + for tool_name in self.config.enabled_tools: + tool = self.tool_registry.get_tool(tool_name) + if tool: + langchain_tool = LangChainToolWrapper(base_tool=tool) + enabled_tools.append(langchain_tool) + logger.debug(f"Enabled tool: {tool_name}") + else: + logger.warning(f"Tool not found: {tool_name}") + + return enabled_tools + + def _create_agent_executor(self) -> AgentExecutor: + """Create LangChain agent executor.""" + if not LANGCHAIN_CLASSIC_AVAILABLE: + raise ValueError("Agent functionality is disabled because langchain_classic is not available.") + + # Get LLM configuration + from ...core.llm import create_llm + llm = create_llm() + + # Get enabled tools + tools = self._get_enabled_tools() + + # Create prompt template + prompt = ChatPromptTemplate.from_messages([ + ("system", self.config.system_message), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad") + ]) + + # Create agent + agent = create_tool_calling_agent(llm, tools, prompt) + + # Create agent executor + agent_executor = AgentExecutor( + agent=agent, + tools=tools, + max_iterations=self.config.max_iterations, + verbose=self.config.verbose, + return_intermediate_steps=True + ) + + return agent_executor + + async def chat(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> Dict[str, Any]: + """Process chat message with agent.""" + try: + logger.info(f"Processing agent chat message: {message[:100]}...") + + # Create agent executor if not exists + if not self.agent_executor: + self.agent_executor = self._create_agent_executor() + + # Convert chat history to LangChain format + langchain_history = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + langchain_history.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + langchain_history.append(AIMessage(content=msg["content"])) + + # Execute agent + result = await self.agent_executor.ainvoke({ + "input": message, + "chat_history": langchain_history + }) + + # Extract response and intermediate steps + response = result["output"] + intermediate_steps = result.get("intermediate_steps", []) + + # Format tool calls for response + tool_calls = [] + for step in intermediate_steps: + if len(step) >= 2: + action, observation = step[0], step[1] + tool_calls.append({ + "tool": action.tool, + "input": action.tool_input, + "output": observation + }) + + logger.info(f"Agent response generated successfully with {len(tool_calls)} tool calls") + + return { + "response": response, + "tool_calls": tool_calls, + "success": True + } + + except Exception as e: + logger.error(f"Agent chat error: {str(e)}", exc_info=True) + return { + "response": f"Sorry, I encountered an error: {str(e)}", + "tool_calls": [], + "success": False, + "error": str(e) + } + + async def chat_stream(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> AsyncGenerator[Dict[str, Any], None]: + """Process chat message with agent (streaming).""" + tool_calls = [] # Initialize tool_calls at the beginning + try: + logger.info(f"Processing agent chat stream: {message[:100]}...") + + # Create agent executor if not exists + if not self.agent_executor: + self.agent_executor = self._create_agent_executor() + + # Convert chat history to LangChain format + langchain_history = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + langchain_history.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + langchain_history.append(AIMessage(content=msg["content"])) + + # Yield initial status + yield { + "type": "status", + "content": "🤖 开始分析您的请求...", + "done": False + } + await asyncio.sleep(0.2) + + # Use astream_events for real streaming (if available) or fallback to simulation + try: + # Try to use streaming events if available + async for event in self.agent_executor.astream_events( + {"input": message, "chat_history": langchain_history}, + version="v1" + ): + if event["event"] == "on_tool_start": + tool_name = event["name"] + yield { + "type": "tool_start", + "content": f"🔧 正在使用工具: {tool_name}", + "tool_name": tool_name, + "done": False + } + await asyncio.sleep(0.1) + + elif event["event"] == "on_tool_end": + tool_name = event["name"] + yield { + "type": "tool_end", + "content": f"✅ 工具 {tool_name} 执行完成", + "tool_name": tool_name, + "done": False + } + await asyncio.sleep(0.1) + + elif event["event"] == "on_chat_model_stream": + chunk = event["data"]["chunk"] + if hasattr(chunk, 'content') and chunk.content: + yield { + "type": "content", + "content": chunk.content, + "done": False + } + await asyncio.sleep(0.05) + + except Exception as stream_error: + logger.warning(f"Streaming events not available, falling back to simulation: {stream_error}") + + # Fallback: Execute agent and simulate streaming + result = await self.agent_executor.ainvoke({ + "input": message, + "chat_history": langchain_history + }) + + # Extract response and intermediate steps + response = result["output"] + intermediate_steps = result.get("intermediate_steps", []) + + # Yield tool execution steps + tool_calls = [] + for i, step in enumerate(intermediate_steps): + if len(step) >= 2: + action, observation = step[0], step[1] + tool_calls.append({ + "tool": action.tool, + "input": action.tool_input, + "output": observation + }) + + # Yield tool execution status + yield { + "type": "tool", + "content": f"🔧 使用工具 {action.tool}: {str(action.tool_input)[:100]}...", + "tool_name": action.tool, + "tool_input": action.tool_input, + "done": False + } + await asyncio.sleep(0.3) + + yield { + "type": "tool_result", + "content": f"✅ 工具结果: {str(observation)[:200]}...", + "tool_name": action.tool, + "done": False + } + await asyncio.sleep(0.2) + + # Yield thinking status + yield { + "type": "thinking", + "content": "🤔 正在整理回答...", + "done": False + } + await asyncio.sleep(0.3) + + # Yield the final response in chunks to simulate streaming + words = response.split() + current_content = "" + + for i, word in enumerate(words): + current_content += word + " " + + # Yield every 2-3 words or at the end + if (i + 1) % 2 == 0 or i == len(words) - 1: + yield { + "type": "response", + "content": current_content.strip(), + "tool_calls": tool_calls if i == len(words) - 1 else [], + "done": i == len(words) - 1 + } + + # Small delay to simulate typing + if i < len(words) - 1: + await asyncio.sleep(0.05) + + logger.info(f"Agent stream response completed with {len(tool_calls)} tool calls") + + except Exception as e: + logger.error(f"Agent chat stream error: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"Sorry, I encountered an error: {str(e)}", + "done": True + } + + def update_config(self, config: Dict[str, Any]): + """Update agent configuration.""" + try: + # Update configuration + for key, value in config.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info(f"Updated agent config: {key} = {value}") + + # Reset agent executor to apply new config + self.agent_executor = None + + except Exception as e: + logger.error(f"Error updating agent config: {str(e)}", exc_info=True) + raise + + def load_config_from_db(self, config_id: Optional[int] = None): + """Load configuration from database.""" + if not self.config_service: + logger.warning("No database session available for loading config") + return + + try: + config_dict = self.config_service.get_config_dict(config_id) + self.update_config(config_dict) + logger.info(f"Loaded configuration from database (ID: {config_id})") + except Exception as e: + logger.error(f"Error loading config from database: {str(e)}") + raise + + def get_available_tools(self) -> List[Dict[str, Any]]: + """Get list of available tools.""" + tools = [] + for tool_name, tool in self.tool_registry._tools.items(): + tools.append({ + "name": tool.get_name(), + "description": tool.get_description(), + "parameters": [{ + "name": param.name, + "type": param.type.value, + "description": param.description, + "required": param.required, + "default": param.default, + "enum": param.enum + } for param in tool.get_parameters()], + "enabled": tool_name in self.config.enabled_tools + }) + return tools + + def get_config(self) -> Dict[str, Any]: + """Get current agent configuration.""" + return self.config.dict() + + +# Global agent service instance +_agent_service: Optional[AgentService] = None + + +def get_agent_service(db_session=None) -> Optional[AgentService]: + """Get global agent service instance.""" + global _agent_service + if _agent_service is None: + try: + _agent_service = AgentService(db_session) + except Exception as e: + logger.warning(f"Failed to initialize AgentService: {str(e)}. Agent functionality will be disabled.") + _agent_service = None + elif db_session and _agent_service and not _agent_service.db_session: + # Update with database session if not already set + _agent_service.db_session = db_session + _agent_service.config_service = AgentConfigService(db_session) + _agent_service._load_config() + return _agent_service \ No newline at end of file diff --git a/backend/th_agenter/services/agent/base.py b/backend/th_agenter/services/agent/base.py new file mode 100644 index 0000000..6c5e163 --- /dev/null +++ b/backend/th_agenter/services/agent/base.py @@ -0,0 +1,248 @@ +"""Base classes for Agent tools.""" + +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Type, Callable +from pydantic import BaseModel, Field +from dataclasses import dataclass +from enum import Enum + +from ...utils.logger import get_logger + +logger = get_logger("agent_tools") + + +class ToolParameterType(str, Enum): + """Tool parameter types.""" + STRING = "string" + INTEGER = "integer" + FLOAT = "float" + BOOLEAN = "boolean" + ARRAY = "array" + OBJECT = "object" + + +@dataclass +class ToolParameter: + """Tool parameter definition.""" + name: str + type: ToolParameterType + description: str + required: bool = True + default: Any = None + enum: Optional[List[Any]] = None + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON schema.""" + param_dict = { + "type": self.type.value, + "description": self.description + } + + if self.enum: + param_dict["enum"] = self.enum + + if self.default is not None: + param_dict["default"] = self.default + + return param_dict + + +class ToolResult(BaseModel): + """Tool execution result.""" + success: bool = Field(description="Whether the tool execution was successful") + result: Any = Field(description="The result data") + error: Optional[str] = Field(default=None, description="Error message if failed") + metadata: Optional[Dict[str, Any]] = Field(default=None, description="Additional metadata") + + +class BaseTool(ABC): + """Base class for all Agent tools.""" + + def __init__(self): + self.name = self.get_name() + self.description = self.get_description() + self.parameters = self.get_parameters() + + @abstractmethod + def get_name(self) -> str: + """Get tool name.""" + pass + + @abstractmethod + def get_description(self) -> str: + """Get tool description.""" + pass + + @abstractmethod + def get_parameters(self) -> List[ToolParameter]: + """Get tool parameters.""" + pass + + @abstractmethod + async def execute(self, **kwargs) -> ToolResult: + """Execute the tool with given parameters.""" + pass + + def get_schema(self) -> Dict[str, Any]: + """Get tool schema for LangChain.""" + properties = {} + required = [] + + for param in self.parameters: + properties[param.name] = param.to_dict() + if param.required: + required.append(param.name) + + return { + "type": "function", + "function": { + "name": self.name, + "description": self.description, + "parameters": { + "type": "object", + "properties": properties, + "required": required + } + } + } + + def validate_parameters(self, **kwargs) -> Dict[str, Any]: + """Validate and process input parameters.""" + validated = {} + + for param in self.parameters: + value = kwargs.get(param.name) + + # Check required parameters + if param.required and value is None: + raise ValueError(f"Required parameter '{param.name}' is missing") + + # Use default if not provided + if value is None and param.default is not None: + value = param.default + + # Type validation (basic) + if value is not None: + if param.type == ToolParameterType.INTEGER and not isinstance(value, int): + try: + value = int(value) + except (ValueError, TypeError): + raise ValueError(f"Parameter '{param.name}' must be an integer") + + elif param.type == ToolParameterType.FLOAT and not isinstance(value, (int, float)): + try: + value = float(value) + except (ValueError, TypeError): + raise ValueError(f"Parameter '{param.name}' must be a number") + + elif param.type == ToolParameterType.BOOLEAN and not isinstance(value, bool): + if isinstance(value, str): + value = value.lower() in ('true', '1', 'yes', 'on') + else: + value = bool(value) + + # Enum validation + if param.enum and value not in param.enum: + raise ValueError(f"Parameter '{param.name}' must be one of {param.enum}") + + validated[param.name] = value + + return validated + + +class ToolRegistry: + """Registry for managing Agent tools.""" + + def __init__(self): + self._tools: Dict[str, BaseTool] = {} + self._enabled_tools: Dict[str, bool] = {} + + def register(self, tool: BaseTool, enabled: bool = True) -> None: + """Register a tool.""" + tool_name = tool.get_name() + self._tools[tool_name] = tool + self._enabled_tools[tool_name] = enabled + logger.info(f"Registered tool: {tool_name} (enabled: {enabled})") + + def unregister(self, tool_name: str) -> None: + """Unregister a tool.""" + if tool_name in self._tools: + del self._tools[tool_name] + del self._enabled_tools[tool_name] + logger.info(f"Unregistered tool: {tool_name}") + + def get_tool(self, tool_name: str) -> Optional[BaseTool]: + """Get a tool by name.""" + return self._tools.get(tool_name) + + def get_enabled_tools(self) -> Dict[str, BaseTool]: + """Get all enabled tools.""" + return { + name: tool for name, tool in self._tools.items() + if self._enabled_tools.get(name, False) + } + + def get_all_tools(self) -> Dict[str, BaseTool]: + """Get all registered tools.""" + return self._tools.copy() + + def enable_tool(self, tool_name: str) -> None: + """Enable a tool.""" + if tool_name in self._tools: + self._enabled_tools[tool_name] = True + logger.info(f"Enabled tool: {tool_name}") + + def disable_tool(self, tool_name: str) -> None: + """Disable a tool.""" + if tool_name in self._tools: + self._enabled_tools[tool_name] = False + logger.info(f"Disabled tool: {tool_name}") + + def is_enabled(self, tool_name: str) -> bool: + """Check if a tool is enabled.""" + return self._enabled_tools.get(tool_name, False) + + def get_tools_schema(self) -> List[Dict[str, Any]]: + """Get schema for all enabled tools.""" + enabled_tools = self.get_enabled_tools() + return [tool.get_schema() for tool in enabled_tools.values()] + + async def execute_tool(self, tool_name: str, **kwargs) -> ToolResult: + """Execute a tool with given parameters.""" + tool = self.get_tool(tool_name) + + if not tool: + return ToolResult( + success=False, + result=None, + error=f"Tool '{tool_name}' not found" + ) + + if not self.is_enabled(tool_name): + return ToolResult( + success=False, + result=None, + error=f"Tool '{tool_name}' is disabled" + ) + + try: + # Validate parameters + validated_params = tool.validate_parameters(**kwargs) + + # Execute tool + result = await tool.execute(**validated_params) + logger.info(f"Tool '{tool_name}' executed successfully") + return result + + except Exception as e: + logger.error(f"Tool '{tool_name}' execution failed: {str(e)}", exc_info=True) + return ToolResult( + success=False, + result=None, + error=f"Tool execution failed: {str(e)}" + ) + + +# Global tool registry instance +tool_registry = ToolRegistry() \ No newline at end of file diff --git a/backend/th_agenter/services/agent/langgraph_agent_service.py b/backend/th_agenter/services/agent/langgraph_agent_service.py new file mode 100644 index 0000000..d956586 --- /dev/null +++ b/backend/th_agenter/services/agent/langgraph_agent_service.py @@ -0,0 +1,441 @@ +"""LangGraph Agent service with tool calling capabilities.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from langchain_core.messages import HumanMessage, AIMessage +from langchain_core.tools import tool + +# Import logger first +from ...utils.logger import get_logger +logger = get_logger("langgraph_agent_service") + +# Try to import langgraph related modules +try: + from langgraph.prebuilt import create_react_agent + LANGGRAPH_AVAILABLE = True +except ImportError: + logger.warning("langgraph not available. LangGraph agent functionality will be disabled.") + create_react_agent = None + LANGGRAPH_AVAILABLE = False + +# Try to import init_chat_model from langchain_openai +try: + from langchain_openai import ChatOpenAI + # Create a simple init_chat_model function as a replacement + def init_chat_model(model_name: str, **kwargs): + return ChatOpenAI(model=model_name, **kwargs) +except ImportError: + logger.warning("langchain_openai not available. Chat model functionality may be limited.") + init_chat_model = None +from pydantic import BaseModel, Field + +from .base import ToolRegistry +from th_agenter.services.tools import WeatherQueryTool, TavilySearchTool, DateTimeTool +from ..postgresql_tool_manager import get_postgresql_tool +from ...core.config import get_settings +from ...utils.logger import get_logger +from ..agent_config import AgentConfigService + +logger = get_logger("langgraph_agent_service") + + + +class LangGraphAgentConfig(BaseModel): + """LangGraph Agent configuration.""" + model_name: str = Field(default="gpt-3.5-turbo") + model_provider: str = Field(default="openai") + base_url: Optional[str] = Field(default=None) + api_key: Optional[str] = Field(default=None) + enabled_tools: List[str] = Field(default_factory=lambda: [ + "calculator", "weather", "search", "file", "image" + ]) + max_iterations: int = Field(default=10) + temperature: float = Field(default=0.7) + max_tokens: int = Field(default=1000) + system_message: str = Field( + default="""你是一个有用的AI助手,可以使用各种工具来帮助用户解决问题。 + 重要规则: + 1. 工具调用失败时,必须仔细分析失败原因,特别是参数格式问题 + 3. 在重新调用工具前,先解释上次失败的原因和改进方案 + 4. 确保每个工具调用的参数格式严格符合工具的要求 """ + ) + verbose: bool = Field(default=True) + + +class LangGraphAgentService: + """LangGraph Agent service using create_react_agent.""" + + def __init__(self, db_session=None): + self.settings = get_settings() + self.tool_registry = ToolRegistry() + self.config = LangGraphAgentConfig() + + # Check if langgraph is available + if not LANGGRAPH_AVAILABLE: + logger.warning("LangGraph is not available. Some features may be disabled.") + self.tools = [] + self.db_session = db_session + self.config_service = AgentConfigService(db_session) if db_session else None + self._initialize_tools() + self._load_config() + self._create_agent() + + def _initialize_tools(self): + """Initialize available tools.""" + # Use the @tool decorated functions + self.tools = [ + + WeatherQueryTool(), + TavilySearchTool(), + DateTimeTool() + ] + + + + def _load_config(self): + """Load configuration from database if available.""" + if self.config_service: + try: + db_config = self.config_service.get_active_config() + if db_config: + # Update config with database values + config_dict = db_config.config_data + for key, value in config_dict.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info("Loaded configuration from database") + except Exception as e: + logger.warning(f"Failed to load config from database: {e}") + + + + def _create_agent(self): + """Create LangGraph agent using create_react_agent.""" + try: + # Initialize the model + llm_config = get_settings().llm.get_current_config() + self.model = init_chat_model( + model=llm_config['model'], + model_provider='openai', + temperature=llm_config['temperature'], + max_tokens=llm_config['max_tokens'], + base_url= llm_config['base_url'], + api_key=llm_config['api_key'] + ) + + + + # Create the react agent + self.agent = create_react_agent( + model=self.model, + tools=self.tools,) + + logger.info("LangGraph React agent created successfully") + + except Exception as e: + logger.error(f"Failed to create agent: {str(e)}") + raise + + + + + + + + def _format_tools_info(self) -> str: + """Format tools information for the prompt.""" + tools_info = [] + for tool_name in self.config.enabled_tools: + tool = self.tool_registry.get_tool(tool_name) + if tool: + params_info = [] + for param in tool.get_parameters(): + params_info.append(f" - {param.name} ({param.type.value}): {param.description}") + + tool_info = f"**{tool.get_name()}**: {tool.get_description()}" + if params_info: + tool_info += "\n" + "\n".join(params_info) + tools_info.append(tool_info) + + return "\n\n".join(tools_info) + + + + async def chat(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> Dict[str, Any]: + """Process a chat message using LangGraph.""" + try: + logger.info(f"Starting chat with message: {message[:100]}...") + + # Convert chat history to messages + messages = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + messages.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + messages.append(AIMessage(content=msg["content"])) + + # Add current message + messages.append(HumanMessage(content=message)) + + # Use the react agent directly + result = await self.agent.ainvoke({"messages": messages}, {"recursion_limit": 6},) + + # Extract final response + final_response = "" + if "messages" in result and result["messages"]: + last_message = result["messages"][-1] + if hasattr(last_message, 'content'): + final_response = last_message.content + elif isinstance(last_message, dict) and "content" in last_message: + final_response = last_message["content"] + + return { + "response": final_response, + "intermediate_steps": [], + "success": True, + "error": None + } + + except Exception as e: + logger.error(f"LangGraph chat error: {str(e)}", exc_info=True) + return { + "response": f"抱歉,处理您的请求时出现错误: {str(e)}", + "intermediate_steps": [], + "success": False, + "error": str(e) + } + + async def chat_stream(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> AsyncGenerator[ + Dict[str, Any], None]: + """Process a chat message using LangGraph with streaming.""" + try: + logger.info(f"Starting streaming chat with message: {message[:100]}...") + + # Convert chat history to messages + messages = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + messages.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + messages.append(AIMessage(content=msg["content"])) + + # Add current message + messages.append(HumanMessage(content=message)) + + # Track state for streaming + intermediate_steps = [] + final_response_started = False + accumulated_response = "" + final_ai_message = None + + # Stream the agent execution + async for event in self.agent.astream({"messages": messages}): + # Handle different event types from LangGraph + print('event===', event) + if isinstance(event, dict): + for node_name, node_output in event.items(): + logger.info(f"Processing node: {node_name}, output type: {type(node_output)}") + + # 处理 tools 节点 + if "tools" in node_name.lower(): + # 提取工具信息 + tool_infos = [] + + if isinstance(node_output, dict) and "messages" in node_output: + messages_in_output = node_output["messages"] + + for msg in messages_in_output: + # 处理 ToolMessage 对象 + if hasattr(msg, 'name') and hasattr(msg, 'content'): + tool_info = { + "tool_name": msg.name, + "tool_output": msg.content, + "tool_call_id": getattr(msg, 'tool_call_id', ''), + "status": "completed" + } + tool_infos.append(tool_info) + elif isinstance(msg, dict): + if 'name' in msg and 'content' in msg: + tool_info = { + "tool_name": msg['name'], + "tool_output": msg['content'], + "tool_call_id": msg.get('tool_call_id', ''), + "status": "completed" + } + tool_infos.append(tool_info) + + # 返回 tools_end 事件 + for tool_info in tool_infos: + yield { + "type": "tools_end", + "content": f"工具 {tool_info['tool_name']} 执行完成", + "tool_name": tool_info["tool_name"], + "tool_output": tool_info["tool_output"], + "node_name": node_name, + "done": False + } + await asyncio.sleep(0.1) + + # 处理 agent 节点 + elif "agent" in node_name.lower(): + if isinstance(node_output, dict) and "messages" in node_output: + messages_in_output = node_output["messages"] + if messages_in_output: + last_msg = messages_in_output[-1] + + # 获取 finish_reason + finish_reason = None + if hasattr(last_msg, 'response_metadata'): + finish_reason = last_msg.response_metadata.get('finish_reason') + elif isinstance(last_msg, dict) and 'response_metadata' in last_msg: + finish_reason = last_msg['response_metadata'].get('finish_reason') + + # 判断是否为 thinking 或 response + if finish_reason == 'tool_calls': + # thinking 状态 + thinking_content = "🤔 正在思考..." + if hasattr(last_msg, 'content') and last_msg.content: + thinking_content = f"🤔 思考: {last_msg.content[:200]}..." + elif isinstance(last_msg, dict) and "content" in last_msg: + thinking_content = f"🤔 思考: {last_msg['content'][:200]}..." + + yield { + "type": "thinking", + "content": thinking_content, + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + elif finish_reason == 'stop': + # response 状态 + if hasattr(last_msg, 'content') and hasattr(last_msg, + '__class__') and 'AI' in last_msg.__class__.__name__: + current_content = last_msg.content + final_ai_message = last_msg + + if not final_response_started and current_content: + final_response_started = True + yield { + "type": "response_start", + "content": "", + "intermediate_steps": intermediate_steps, + "done": False + } + + if current_content and len(current_content) > len(accumulated_response): + new_content = current_content[len(accumulated_response):] + + for char in new_content: + accumulated_response += char + yield { + "type": "response", + "content": accumulated_response, + "intermediate_steps": intermediate_steps, + "done": False + } + await asyncio.sleep(0.03) + + else: + # 其他 agent 状态 + yield { + "type": "step", + "content": f"📋 执行步骤: {node_name}", + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + # 处理其他节点 + else: + yield { + "type": "step", + "content": f"📋 执行步骤: {node_name}", + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + # 最终完成事件 + yield { + "type": "complete", + "content": accumulated_response, + "intermediate_steps": intermediate_steps, + "done": True + } + + except Exception as e: + logger.error(f"Error in chat_stream: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"处理请求时出错: {str(e)}", + "done": True + } + + # 确保最终响应包含完整内容 + final_content = accumulated_response + if not final_content and final_ai_message and hasattr(final_ai_message, 'content'): + final_content = final_ai_message.content or "" + + # Final completion signal + yield { + "type": "response", + "content": final_content, + "intermediate_steps": intermediate_steps, + "done": True + } + + except Exception as e: + logger.error(f"LangGraph chat stream error: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"抱歉,处理您的请求时出现错误: {str(e)}", + "error": str(e), + "done": True + } + + def get_available_tools(self) -> List[Dict[str, Any]]: + """Get list of available tools.""" + tools = [] + for tool in self.tools: + tools.append({ + "name": tool.name, + "description": tool.description, + "parameters": [], + "enabled": True + }) + return tools + + def get_config(self) -> Dict[str, Any]: + """Get current agent configuration.""" + return self.config.dict() + + def update_config(self, config: Dict[str, Any]): + """Update agent configuration.""" + for key, value in config.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + + # Recreate agent with new config + self._create_agent() + logger.info("Agent configuration updated") + + +# Global instance +_langgraph_agent_service: Optional[LangGraphAgentService] = None + + +def get_langgraph_agent_service(db_session=None) -> LangGraphAgentService: + """Get or create LangGraph agent service instance.""" + global _langgraph_agent_service + + if _langgraph_agent_service is None: + _langgraph_agent_service = LangGraphAgentService(db_session) + logger.info("LangGraph Agent service initialized") + + return _langgraph_agent_service \ No newline at end of file diff --git a/backend/th_agenter/services/agent_config.py b/backend/th_agenter/services/agent_config.py new file mode 100644 index 0000000..945bdbe --- /dev/null +++ b/backend/th_agenter/services/agent_config.py @@ -0,0 +1,206 @@ +"""Agent configuration service.""" + +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_ + +from ..models.agent_config import AgentConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError + +logger = get_logger("agent_config_service") + + +class AgentConfigService: + """Service for managing agent configurations.""" + + def __init__(self, db: Session): + self.db = db + + def create_config(self, config_data: Dict[str, Any]) -> AgentConfig: + """Create a new agent configuration.""" + try: + # Validate required fields + if not config_data.get("name"): + raise ValidationError("Configuration name is required") + + # Check if name already exists + existing = self.db.query(AgentConfig).filter( + AgentConfig.name == config_data["name"] + ).first() + if existing: + raise ValidationError(f"Configuration with name '{config_data['name']}' already exists") + + # Create new configuration + config = AgentConfig( + name=config_data["name"], + description=config_data.get("description", ""), + enabled_tools=config_data.get("enabled_tools", ["calculator", "weather", "search", "datetime", "file"]), + max_iterations=config_data.get("max_iterations", 10), + temperature=config_data.get("temperature", 0.1), + system_message=config_data.get("system_message", "You are a helpful AI assistant with access to various tools. Use the available tools to help answer user questions accurately. Always explain your reasoning and the tools you're using."), + verbose=config_data.get("verbose", True), + is_active=config_data.get("is_active", True), + is_default=config_data.get("is_default", False) + ) + + # If this is set as default, unset other defaults + if config.is_default: + self.db.query(AgentConfig).filter( + AgentConfig.is_default == True + ).update({"is_default": False}) + + self.db.add(config) + self.db.commit() + self.db.refresh(config) + + logger.info(f"Created agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error creating agent configuration: {str(e)}") + raise + + def get_config(self, config_id: int) -> Optional[AgentConfig]: + """Get agent configuration by ID.""" + return self.db.query(AgentConfig).filter( + AgentConfig.id == config_id + ).first() + + def get_config_by_name(self, name: str) -> Optional[AgentConfig]: + """Get agent configuration by name.""" + return self.db.query(AgentConfig).filter( + AgentConfig.name == name + ).first() + + def get_default_config(self) -> Optional[AgentConfig]: + """Get default agent configuration.""" + return self.db.query(AgentConfig).filter( + and_(AgentConfig.is_default == True, AgentConfig.is_active == True) + ).first() + + def list_configs(self, active_only: bool = True) -> List[AgentConfig]: + """List all agent configurations.""" + query = self.db.query(AgentConfig) + if active_only: + query = query.filter(AgentConfig.is_active == True) + return query.order_by(AgentConfig.created_at.desc()).all() + + def update_config(self, config_id: int, config_data: Dict[str, Any]) -> AgentConfig: + """Update agent configuration.""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + # Check if name change conflicts with existing + if "name" in config_data and config_data["name"] != config.name: + existing = self.db.query(AgentConfig).filter( + and_( + AgentConfig.name == config_data["name"], + AgentConfig.id != config_id + ) + ).first() + if existing: + raise ValidationError(f"Configuration with name '{config_data['name']}' already exists") + + # Update fields + for key, value in config_data.items(): + if hasattr(config, key): + setattr(config, key, value) + + # If this is set as default, unset other defaults + if config_data.get("is_default", False): + self.db.query(AgentConfig).filter( + and_( + AgentConfig.is_default == True, + AgentConfig.id != config_id + ) + ).update({"is_default": False}) + + self.db.commit() + self.db.refresh(config) + + logger.info(f"Updated agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating agent configuration: {str(e)}") + raise + + def delete_config(self, config_id: int) -> bool: + """Delete agent configuration (soft delete by setting is_active=False).""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + # Don't allow deleting the default configuration + if config.is_default: + raise ValidationError("Cannot delete the default configuration") + + config.is_active = False + self.db.commit() + + logger.info(f"Deleted agent configuration: {config.name}") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting agent configuration: {str(e)}") + raise + + def set_default_config(self, config_id: int) -> AgentConfig: + """Set a configuration as default.""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + if not config.is_active: + raise ValidationError("Cannot set inactive configuration as default") + + # Unset other defaults + self.db.query(AgentConfig).filter( + AgentConfig.is_default == True + ).update({"is_default": False}) + + # Set this as default + config.is_default = True + self.db.commit() + self.db.refresh(config) + + logger.info(f"Set default agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error setting default agent configuration: {str(e)}") + raise + + def get_config_dict(self, config_id: Optional[int] = None) -> Dict[str, Any]: + """Get configuration as dictionary. If no ID provided, returns default config.""" + if config_id: + config = self.get_config(config_id) + else: + config = self.get_default_config() + + if not config: + # Return default values if no configuration found + return { + "enabled_tools": ["calculator", "weather", "search", "datetime", "file", "generate_image"], + "max_iterations": 10, + "temperature": 0.1, + "system_message": "You are a helpful AI assistant with access to various tools. Use the available tools to help answer user questions accurately. Always explain your reasoning and the tools you're using.", + "verbose": True + } + + return { + "enabled_tools": config.enabled_tools, + "max_iterations": config.max_iterations, + "temperature": config.temperature, + "system_message": config.system_message, + "verbose": config.verbose + } \ No newline at end of file diff --git a/backend/th_agenter/services/auth.py b/backend/th_agenter/services/auth.py new file mode 100644 index 0000000..fc77f3e --- /dev/null +++ b/backend/th_agenter/services/auth.py @@ -0,0 +1,141 @@ +"""Authentication service.""" + +from typing import Optional +from datetime import datetime, timedelta +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +import jwt +import bcrypt + +from ..core.config import settings +from ..db.database import get_db +from ..models.user import User + + +security = HTTPBearer() + + +class AuthService: + """Authentication service.""" + + @staticmethod + def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + # Bcrypt has a maximum password length of 72 bytes + plain_password = plain_password[:72] + return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) + + @staticmethod + def get_password_hash(password: str) -> str: + """Generate password hash.""" + # Bcrypt has a maximum password length of 72 bytes + password = password[:72] + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password.encode('utf-8'), salt) + return hashed.decode('utf-8') + + @staticmethod + def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.security.access_token_expire_minutes) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, + settings.security.secret_key, + algorithm=settings.security.algorithm + ) + return encoded_jwt + + @staticmethod + def verify_token(token: str) -> Optional[dict]: + """Verify JWT token.""" + try: + payload = jwt.decode( + token, + settings.security.secret_key, + algorithms=[settings.security.algorithm] + ) + return payload + except jwt.PyJWTError as e: + import logging + logging.error(f"Token verification failed: {e}") + logging.error(f"Token: {token[:50]}...") + logging.error(f"Secret key: {settings.security.secret_key[:20]}...") + logging.error(f"Algorithm: {settings.security.algorithm}") + return None + + @staticmethod + def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: + """Authenticate user with username and password.""" + user = db.query(User).filter(User.username == username).first() + if not user: + return None + if not AuthService.verify_password(password, user.hashed_password): + return None + return user + + @staticmethod + def authenticate_user_by_email(db: Session, email: str, password: str) -> Optional[User]: + """Authenticate user with email and password.""" + user = db.query(User).filter(User.email == email).first() + if not user: + return None + if not AuthService.verify_password(password, user.hashed_password): + return None + return user + + @staticmethod + def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) + ) -> User: + """Get current authenticated user.""" + import logging + from ..core.context import UserContext + + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + token = credentials.credentials + logging.info(f"Received token: {token[:50]}...") + payload = AuthService.verify_token(token) + if payload is None: + logging.error("Token verification failed") + raise credentials_exception + + logging.info(f"Token payload: {payload}") + username: str = payload.get("sub") + if username is None: + logging.error("No username in token payload") + raise credentials_exception + + logging.info(f"Looking for user with username: {username}") + user = db.query(User).filter(User.username == username).first() + if user is None: + logging.error(f"User not found with username: {username}") + raise credentials_exception + + # Set user in context for global access + UserContext.set_current_user(user) + logging.info(f"User {user.username} (ID: {user.id}) set in UserContext") + + return user + + @staticmethod + def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + """Get current active user.""" + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + return current_user \ No newline at end of file diff --git a/backend/th_agenter/services/chat.py b/backend/th_agenter/services/chat.py new file mode 100644 index 0000000..040e26c --- /dev/null +++ b/backend/th_agenter/services/chat.py @@ -0,0 +1,363 @@ +"""Chat service for AI model integration using LangChain.""" + +import json +import asyncio +import os +from typing import AsyncGenerator, Optional, List, Dict, Any +from sqlalchemy.orm import Session + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, StreamChunk, MessageResponse +from ..utils.exceptions import ChatServiceError, OpenAIError +from ..utils.logger import get_logger +from .conversation import ConversationService +from .langchain_chat import LangChainChatService +from .knowledge_chat import KnowledgeChatService +from .agent.agent_service import get_agent_service +from .agent.langgraph_agent_service import get_langgraph_agent_service + +logger = get_logger("chat_service") + + +class ChatService: + """Service for handling AI chat functionality using LangChain.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + # Initialize LangChain chat service + self.langchain_service = LangChainChatService(db) + + # Initialize Knowledge chat service with exception handling + try: + self.knowledge_service = KnowledgeChatService(db) + except Exception as e: + logger.warning(f"Failed to initialize KnowledgeChatService: {str(e)}. Knowledge base features will be disabled.") + self.knowledge_service = None + + # Initialize Agent service with database session + try: + self.agent_service = get_agent_service(db) + if not self.agent_service: + logger.warning("AgentService is not available. Agent functionality will be disabled.") + except Exception as e: + logger.warning(f"Failed to initialize AgentService: {str(e)}. Agent functionality will be disabled.") + self.agent_service = None + + # Initialize LangGraph Agent service with database session + try: + self.langgraph_service = get_langgraph_agent_service(db) + if not self.langgraph_service: + logger.warning("LangGraphAgentService is not available. LangGraph functionality will be disabled.") + except Exception as e: + logger.warning(f"Failed to initialize LangGraphAgentService: {str(e)}. LangGraph functionality will be disabled.") + self.langgraph_service = None + + logger.info("ChatService initialized with LangChain backend and Agent support") + + + + async def chat( + self, + conversation_id: int, + message: str, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + use_agent: bool = False, + use_langgraph: bool = False, + use_knowledge_base: bool = False, + knowledge_base_id: Optional[int] = None + ) -> ChatResponse: + """Send a message and get AI response using LangChain, Agent, or Knowledge Base.""" + if use_knowledge_base and knowledge_base_id: + if not self.knowledge_service: + raise ChatServiceError("Knowledge base features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via Knowledge Base {knowledge_base_id}") + + # Use knowledge base chat service + return await self.knowledge_service.chat_with_knowledge_base( + conversation_id=conversation_id, + message=message, + knowledge_base_id=knowledge_base_id, + stream=stream, + temperature=temperature, + max_tokens=max_tokens + ) + elif use_langgraph: + if not self.langgraph_service: + raise ChatServiceError("LangGraph agent features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via LangGraph Agent") + + # Get conversation history for LangGraph agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Use LangGraph agent service + agent_result = await self.langgraph_service.chat(message, chat_history) + + if agent_result["success"]: + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Save assistant response + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + message_metadata={"intermediate_steps": agent_result["intermediate_steps"]} + ) + + return ChatResponse( + message=MessageResponse( + id=assistant_message.id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + conversation_id=conversation_id, + created_at=assistant_message.created_at, + metadata=assistant_message.metadata + ) + ) + else: + raise ChatServiceError(f"LangGraph Agent error: {agent_result.get('error', 'Unknown error')}") + elif use_agent: + if not self.agent_service: + raise ChatServiceError("Agent features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via Agent") + + # Get conversation history for agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Use agent service + agent_result = await self.agent_service.chat(message, chat_history) + + if agent_result["success"]: + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Save assistant response + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + message_metadata={"tool_calls": agent_result["tool_calls"]} + ) + + return ChatResponse( + message=MessageResponse( + id=assistant_message.id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + conversation_id=conversation_id, + created_at=assistant_message.created_at, + metadata=assistant_message.metadata + ) + ) + else: + raise ChatServiceError(f"Agent error: {agent_result.get('error', 'Unknown error')}") + else: + logger.info(f"Processing chat request for conversation {conversation_id} via LangChain") + + # Delegate to LangChain service + return await self.langchain_service.chat( + conversation_id=conversation_id, + message=message, + stream=stream, + temperature=temperature, + max_tokens=max_tokens + ) + + async def chat_stream( + self, + conversation_id: int, + message: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + use_agent: bool = False, + use_langgraph: bool = False, + use_knowledge_base: bool = False, + knowledge_base_id: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Send a message and get streaming AI response using LangChain, Agent, or Knowledge Base.""" + if use_knowledge_base and knowledge_base_id: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via Knowledge Base {knowledge_base_id}") + + # Use knowledge base chat service streaming + async for content in self.knowledge_service.chat_stream_with_knowledge_base( + conversation_id=conversation_id, + message=message, + knowledge_base_id=knowledge_base_id, + temperature=temperature, + max_tokens=max_tokens + ): + # Create stream chunk for compatibility with existing API + stream_chunk = StreamChunk( + content=content, + role=MessageRole.ASSISTANT + ) + yield json.dumps(stream_chunk.dict(), ensure_ascii=False) + elif use_langgraph: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via LangGraph Agent") + + # Get conversation history for LangGraph agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Save user message first + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Use LangGraph agent service streaming + full_response = "" + intermediate_steps = [] + + async for chunk in self.langgraph_service.chat_stream(message, chat_history): + if chunk["type"] == "response": + full_response = chunk["content"] + intermediate_steps = chunk.get("intermediate_steps", []) + + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + + elif chunk["type"] == "error": + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + return + else: + # For other types (status, step, etc.), pass through + yield json.dumps(chunk, ensure_ascii=False) + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={"intermediate_steps": intermediate_steps} + ) + elif use_agent: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via Agent") + + # Get conversation history for agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Save user message first + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Use agent service streaming + full_response = "" + tool_calls = [] + + async for chunk in self.agent_service.chat_stream(message, chat_history): + if chunk["type"] == "response": + full_response = chunk["content"] + tool_calls = chunk.get("tool_calls", []) + + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + + elif chunk["type"] == "error": + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + return + else: + # For other types (status, tool_start, etc.), pass through + yield json.dumps(chunk, ensure_ascii=False) + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={"tool_calls": tool_calls} + ) + else: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via LangChain") + + # Delegate to LangChain service and wrap response in JSON format + async for content in self.langchain_service.chat_stream( + conversation_id=conversation_id, + message=message, + temperature=temperature, + max_tokens=max_tokens + ): + # Create stream chunk for compatibility with existing API + stream_chunk = StreamChunk( + content=content, + role=MessageRole.ASSISTANT + ) + yield json.dumps(stream_chunk.dict(), ensure_ascii=False) + + async def get_available_models(self) -> List[str]: + """Get list of available models from LangChain.""" + logger.info("Getting available models via LangChain") + + # Delegate to LangChain service + return await self.langchain_service.get_available_models() + + def update_model_config( + self, + model: Optional[str] = None, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ): + """Update LLM configuration via LangChain.""" + logger.info(f"Updating model config via LangChain: model={model}, temperature={temperature}, max_tokens={max_tokens}") + + # Delegate to LangChain service + self.langchain_service.update_model_config( + model=model, + temperature=temperature, + max_tokens=max_tokens + ) \ No newline at end of file diff --git a/backend/th_agenter/services/conversation.py b/backend/th_agenter/services/conversation.py new file mode 100644 index 0000000..f69ca00 --- /dev/null +++ b/backend/th_agenter/services/conversation.py @@ -0,0 +1,260 @@ +"""Conversation service.""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import desc, func, or_ + +from ..models.conversation import Conversation +from ..models.message import Message, MessageRole +from ..utils.schemas import ConversationCreate, ConversationUpdate +from ..utils.exceptions import ConversationNotFoundError, DatabaseError +from ..utils.logger import get_logger +from ..core.context import UserContext + +logger = get_logger("conversation_service") + + +class ConversationService: + """Service for managing conversations and messages.""" + + def __init__(self, db: Session): + self.db = db + + def create_conversation( + self, + user_id: int, + conversation_data: ConversationCreate + ) -> Conversation: + """Create a new conversation.""" + logger.info(f"Creating new conversation for user {user_id}") + + try: + conversation = Conversation( + **conversation_data.dict(), + user_id=user_id + ) + + # Set audit fields + conversation.set_audit_fields(user_id=user_id, is_update=False) + + self.db.add(conversation) + self.db.commit() + self.db.refresh(conversation) + + logger.info(f"Successfully created conversation {conversation.id} for user {user_id}") + return conversation + + except Exception as e: + logger.error(f"Failed to create conversation: {str(e)}", exc_info=True) + self.db.rollback() + raise DatabaseError(f"Failed to create conversation: {str(e)}") + + def get_conversation(self, conversation_id: int) -> Optional[Conversation]: + """Get a conversation by ID.""" + try: + user_id = UserContext.get_current_user_id() + conversation = self.db.query(Conversation).filter( + Conversation.id == conversation_id, + Conversation.user_id == user_id + ).first() + + if not conversation: + logger.warning(f"Conversation {conversation_id} not found") + + return conversation + + except Exception as e: + logger.error(f"Failed to get conversation {conversation_id}: {str(e)}", exc_info=True) + raise DatabaseError(f"Failed to get conversation: {str(e)}") + + def get_user_conversations( + self, + skip: int = 0, + limit: int = 50, + search_query: Optional[str] = None, + include_archived: bool = False, + order_by: str = "updated_at", + order_desc: bool = True + ) -> List[Conversation]: + """Get user's conversations with search and filtering.""" + user_id = UserContext.get_current_user_id() + query = self.db.query(Conversation).filter( + Conversation.user_id == user_id + ) + + # Filter archived conversations + if not include_archived: + query = query.filter(Conversation.is_archived == False) + + # Search functionality + if search_query and search_query.strip(): + search_term = f"%{search_query.strip()}%" + query = query.filter( + or_( + Conversation.title.ilike(search_term), + Conversation.system_prompt.ilike(search_term) + ) + ) + + # Ordering + order_column = getattr(Conversation, order_by, Conversation.updated_at) + if order_desc: + query = query.order_by(desc(order_column)) + else: + query = query.order_by(order_column) + + return query.offset(skip).limit(limit).all() + + def update_conversation( + self, + conversation_id: int, + conversation_update: ConversationUpdate + ) -> Optional[Conversation]: + """Update a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return None + + update_data = conversation_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(conversation, field, value) + + self.db.commit() + self.db.refresh(conversation) + return conversation + + def delete_conversation(self, conversation_id: int) -> bool: + """Delete a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + self.db.delete(conversation) + self.db.commit() + return True + + def get_conversation_messages( + self, + conversation_id: int, + skip: int = 0, + limit: int = 100 + ) -> List[Message]: + """Get messages from a conversation.""" + return self.db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).offset(skip).limit(limit).all() + + def add_message( + self, + conversation_id: int, + content: str, + role: MessageRole, + message_metadata: Optional[dict] = None, + context_documents: Optional[list] = None, + prompt_tokens: Optional[int] = None, + completion_tokens: Optional[int] = None, + total_tokens: Optional[int] = None + ) -> Message: + """Add a message to a conversation.""" + message = Message( + conversation_id=conversation_id, + content=content, + role=role, + message_metadata=message_metadata, + context_documents=context_documents, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens + ) + + # Set audit fields + message.set_audit_fields() + + self.db.add(message) + self.db.commit() + self.db.refresh(message) + return message + + def get_conversation_history( + self, + conversation_id: int, + limit: int = 20 + ) -> List[Message]: + """Get recent conversation history for context.""" + return self.db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(desc(Message.created_at)).limit(limit).all()[::-1] # Reverse to get chronological order + + def update_conversation_timestamp(self, conversation_id: int) -> None: + """Update conversation's updated_at timestamp.""" + conversation = self.get_conversation(conversation_id) + if conversation: + # SQLAlchemy will automatically update the updated_at field + self.db.commit() + + def get_user_conversations_count( + self, + search_query: Optional[str] = None, + include_archived: bool = False + ) -> int: + """Get total count of user's conversations.""" + user_id = UserContext.get_current_user_id() + query = self.db.query(func.count(Conversation.id)).filter( + Conversation.user_id == user_id + ) + + if not include_archived: + query = query.filter(Conversation.is_archived == False) + + if search_query and search_query.strip(): + search_term = f"%{search_query.strip()}%" + query = query.filter( + or_( + Conversation.title.ilike(search_term), + Conversation.system_prompt.ilike(search_term) + ) + ) + + return query.scalar() or 0 + + def archive_conversation(self, conversation_id: int) -> bool: + """Archive a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + conversation.is_archived = True + self.db.commit() + return True + + def unarchive_conversation(self, conversation_id: int) -> bool: + """Unarchive a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + conversation.is_archived = False + self.db.commit() + return True + + def delete_all_conversations(self) -> bool: + """Delete all conversations for the current user.""" + try: + user_id = UserContext.get_current_user_id() + # Get all conversations for the user + conversations = self.db.query(Conversation).filter( + Conversation.user_id == user_id + ).all() + + # Delete each conversation + for conversation in conversations: + self.db.delete(conversation) + + self.db.commit() + logger.info(f"Successfully deleted all conversations for user {user_id}") + return True + + except Exception as e: + logger.error(f"Failed to delete all conversations: {str(e)}", exc_info=True) + self.db.rollback() + raise DatabaseError(f"Failed to delete all conversations: {str(e)}") \ No newline at end of file diff --git a/backend/th_agenter/services/conversation_context.py b/backend/th_agenter/services/conversation_context.py new file mode 100644 index 0000000..dc13d3c --- /dev/null +++ b/backend/th_agenter/services/conversation_context.py @@ -0,0 +1,310 @@ +from typing import Dict, Any, List, Optional +import json +from datetime import datetime +from sqlalchemy.orm import Session +from th_agenter.models.conversation import Conversation +from th_agenter.models.message import Message +from th_agenter.db.database import get_db + +class ConversationContextService: + """ + 对话上下文管理服务 + 用于管理智能问数的对话历史和上下文信息 + """ + + def __init__(self): + self.context_cache = {} # 内存缓存对话上下文 + + async def create_conversation(self, user_id: int, title: str = "智能问数对话") -> int: + """ + 创建新的对话 + + Args: + user_id: 用户ID + title: 对话标题 + + Returns: + 新创建的对话ID + """ + try: + db = next(get_db()) + + conversation = Conversation( + user_id=user_id, + title=title, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + db.add(conversation) + db.commit() + db.refresh(conversation) + + # 初始化对话上下文 + self.context_cache[conversation.id] = { + 'conversation_id': conversation.id, + 'user_id': user_id, + 'file_list': [], + 'selected_files': [], + 'query_history': [], + 'created_at': datetime.utcnow().isoformat() + } + + return conversation.id + + except Exception as e: + print(f"创建对话失败: {e}") + raise + finally: + db.close() + + async def get_conversation_context(self, conversation_id: int) -> Optional[Dict[str, Any]]: + """ + 获取对话上下文 + + Args: + conversation_id: 对话ID + + Returns: + 对话上下文信息 + """ + # 先从缓存中查找 + if conversation_id in self.context_cache: + return self.context_cache[conversation_id] + + # 从数据库加载 + try: + db = next(get_db()) + + conversation = db.query(Conversation).filter( + Conversation.id == conversation_id + ).first() + + if not conversation: + return None + + # 加载消息历史 + messages = db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).all() + + # 重建上下文 + context = { + 'conversation_id': conversation_id, + 'user_id': conversation.user_id, + 'file_list': [], + 'selected_files': [], + 'query_history': [], + 'created_at': conversation.created_at.isoformat() + } + + # 从消息中提取查询历史 + for message in messages: + if message.role == 'user': + context['query_history'].append({ + 'query': message.content, + 'timestamp': message.created_at.isoformat() + }) + elif message.role == 'assistant' and message.metadata: + # 从助手消息的元数据中提取文件信息 + try: + metadata = json.loads(message.metadata) if isinstance(message.metadata, str) else message.metadata + if 'selected_files' in metadata: + context['selected_files'] = metadata['selected_files'] + if 'file_list' in metadata: + context['file_list'] = metadata['file_list'] + except (json.JSONDecodeError, TypeError): + pass + + # 缓存上下文 + self.context_cache[conversation_id] = context + + return context + + except Exception as e: + print(f"获取对话上下文失败: {e}") + return None + finally: + db.close() + + async def update_conversation_context( + self, + conversation_id: int, + file_list: List[Dict[str, Any]] = None, + selected_files: List[Dict[str, Any]] = None, + query: str = None + ) -> bool: + """ + 更新对话上下文 + + Args: + conversation_id: 对话ID + file_list: 文件列表 + selected_files: 选中的文件 + query: 用户查询 + + Returns: + 更新是否成功 + """ + try: + # 获取或创建上下文 + context = await self.get_conversation_context(conversation_id) + if not context: + return False + + # 更新上下文信息 + if file_list is not None: + context['file_list'] = file_list + + if selected_files is not None: + context['selected_files'] = selected_files + + if query is not None: + context['query_history'].append({ + 'query': query, + 'timestamp': datetime.utcnow().isoformat() + }) + + # 更新缓存 + self.context_cache[conversation_id] = context + + return True + + except Exception as e: + print(f"更新对话上下文失败: {e}") + return False + + async def save_message( + self, + conversation_id: int, + role: str, + content: str, + metadata: Dict[str, Any] = None + ) -> bool: + """ + 保存消息到数据库 + + Args: + conversation_id: 对话ID + role: 消息角色 (user/assistant) + content: 消息内容 + metadata: 元数据 + + Returns: + 保存是否成功 + """ + try: + db = next(get_db()) + + message = Message( + conversation_id=conversation_id, + role=role, + content=content, + metadata=json.dumps(metadata) if metadata else None, + created_at=datetime.utcnow() + ) + + db.add(message) + db.commit() + + # 更新对话的最后更新时间 + conversation = db.query(Conversation).filter( + Conversation.id == conversation_id + ).first() + + if conversation: + conversation.updated_at = datetime.utcnow() + db.commit() + + return True + + except Exception as e: + print(f"保存消息失败: {e}") + return False + finally: + db.close() + + async def reset_conversation_context(self, conversation_id: int) -> bool: + """ + 重置对话上下文 + + Args: + conversation_id: 对话ID + + Returns: + 重置是否成功 + """ + try: + # 清除缓存 + if conversation_id in self.context_cache: + context = self.context_cache[conversation_id] + # 保留基本信息,清除文件和查询历史 + context.update({ + 'file_list': [], + 'selected_files': [], + 'query_history': [] + }) + + return True + + except Exception as e: + print(f"重置对话上下文失败: {e}") + return False + + async def get_conversation_history(self, conversation_id: int) -> List[Dict[str, Any]]: + """ + 获取对话历史消息 + + Args: + conversation_id: 对话ID + + Returns: + 消息历史列表 + """ + try: + db = next(get_db()) + + messages = db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).all() + + history = [] + for message in messages: + msg_data = { + 'id': message.id, + 'role': message.role, + 'content': message.content, + 'timestamp': message.created_at.isoformat() + } + + if message.metadata: + try: + metadata = json.loads(message.metadata) if isinstance(message.metadata, str) else message.metadata + msg_data['metadata'] = metadata + except (json.JSONDecodeError, TypeError): + pass + + history.append(msg_data) + + return history + + except Exception as e: + print(f"获取对话历史失败: {e}") + return [] + finally: + db.close() + + def clear_cache(self, conversation_id: int = None): + """ + 清除缓存 + + Args: + conversation_id: 特定对话ID,如果为None则清除所有缓存 + """ + if conversation_id: + self.context_cache.pop(conversation_id, None) + else: + self.context_cache.clear() + +# 全局实例 +conversation_context_service = ConversationContextService() \ No newline at end of file diff --git a/backend/th_agenter/services/database_config_service.py b/backend/th_agenter/services/database_config_service.py new file mode 100644 index 0000000..b2d8155 --- /dev/null +++ b/backend/th_agenter/services/database_config_service.py @@ -0,0 +1,324 @@ +"""数据库配置服务""" + +import json +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_ +from cryptography.fernet import Fernet +import base64 +import os + +from ..models.database_config import DatabaseConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool + +# Try to import pymysql for MySQL support +try: + import pymysql + PYMYSQL_AVAILABLE = True +except ImportError: + pymysql = None + PYMYSQL_AVAILABLE = False + +logger = get_logger("database_config_service") + + +class DatabaseConfigService: + """数据库配置管理服务""" + + def __init__(self, db_session: Session): + self.db = db_session + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + # 初始化加密密钥 + self.encryption_key = self._get_or_create_encryption_key() + self.cipher = Fernet(self.encryption_key) + def _get_or_create_encryption_key(self) -> bytes: + """获取或创建加密密钥""" + key_file = "db/db_config_key.key" + if os.path.exists(key_file): + print('find db_config_key') + with open(key_file, 'rb') as f: + return f.read() + + else: + print('not find db_config_key') + key = Fernet.generate_key() + with open(key_file, 'wb') as f: + f.write(key) + return key + + def _encrypt_password(self, password: str) -> str: + """加密密码""" + return self.cipher.encrypt(password.encode()).decode() + + def _decrypt_password(self, encrypted_password: str) -> str: + """解密密码""" + return self.cipher.decrypt(encrypted_password.encode()).decode() + + async def create_config(self, user_id: int, config_data: Dict[str, Any]) -> DatabaseConfig: + """创建数据库配置""" + try: + # 验证配置 + required_fields = ['name', 'db_type', 'host', 'port', 'database', 'username', 'password'] + for field in required_fields: + if field not in config_data: + raise ValidationError(f"缺少必需字段: {field}") + + + # 测试连接 + test_config = { + 'host': config_data['host'], + 'port': config_data['port'], + 'database': config_data['database'], + 'username': config_data['username'], + 'password': config_data['password'] + } + if 'postgresql' == config_data['db_type']: + test_result = await self.postgresql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + if not test_result.success: + raise ValidationError(f"数据库连接测试失败: {test_result.error}") + elif 'mysql' == config_data['db_type']: + test_result = await self.mysql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + if not test_result.success: + raise ValidationError(f"数据库连接测试失败: {test_result.error}") + # 如果设置为默认配置,先取消其他默认配置 + if config_data.get('is_default', False): + self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.created_by == user_id, DatabaseConfig.is_default == True) + ).update({'is_default': False}) + + # 创建配置 + db_config = DatabaseConfig( + created_by=user_id, + name=config_data['name'], + db_type=config_data['db_type'], + host=config_data['host'], + port=config_data['port'], + database=config_data['database'], + username=config_data['username'], + password=self._encrypt_password(config_data['password']), + is_active=config_data.get('is_active', True), + is_default=config_data.get('is_default', False), + connection_params=config_data.get('connection_params') + ) + + self.db.add(db_config) + self.db.commit() + self.db.refresh(db_config) + + logger.info(f"创建数据库配置成功: {db_config.name} (ID: {db_config.id})") + return db_config + + except Exception as e: + self.db.rollback() + logger.error(f"创建数据库配置失败: {str(e)}") + raise + + def get_user_configs(self, user_id: int, active_only: bool = True) -> List[DatabaseConfig]: + """获取用户的数据库配置列表""" + query = self.db.query(DatabaseConfig).filter(DatabaseConfig.created_by == user_id) + if active_only: + query = query.filter(DatabaseConfig.is_active == True) + return query.order_by(DatabaseConfig.created_at.desc()).all() + + def get_config_by_id(self, config_id: int, user_id: int) -> Optional[DatabaseConfig]: + """根据ID获取配置""" + return self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == config_id, DatabaseConfig.created_by == user_id) + ).first() + + def get_default_config(self, user_id: int) -> Optional[DatabaseConfig]: + """获取用户的默认配置""" + return self.db.query(DatabaseConfig).filter( + and_( + DatabaseConfig.created_by == user_id, + # DatabaseConfig.is_default == True, + DatabaseConfig.is_active == True + ) + ).first() + + async def test_connection(self, config_id: int, user_id: int) -> Dict[str, Any]: + """测试数据库连接""" + config = self.get_config_by_id(config_id, user_id) + if not config: + raise NotFoundError("数据库配置不存在") + + test_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': self._decrypt_password(config.password) + } + + result = await self.postgresql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + + return { + 'success': result.success, + 'message': result.result.get('message') if result.success else result.error, + 'details': result.result if result.success else None + } + + async def connect_and_get_tables(self, config_id: int, user_id: int) -> Dict[str, Any]: + """连接数据库并获取表列表""" + config = self.get_config_by_id(config_id, user_id) + if not config: + raise NotFoundError("数据库配置不存在") + + connection_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': self._decrypt_password(config.password) + } + + if 'postgresql' == config.db_type: + # 连接数据库 + connect_result = await self.postgresql_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=str(user_id) + ) + elif 'mysql' == config.db_type: + # 连接数据库 + connect_result = await self.mysql_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=str(user_id) + ) + + if not connect_result.success: + return { + 'success': False, + 'message': connect_result.error + } + # 连接信息已保存到PostgreSQLMCPTool的connections中 + return { + 'success': True, + 'data': connect_result.result, + 'config_name': config.name + } + + async def get_table_data(self, table_name: str, user_id: int, db_type: str, limit: int = 100) -> Dict[str, Any]: + """获取表数据预览(复用已建立的连接)""" + try: + user_id_str = str(user_id) + + # 根据db_type选择相应的数据库工具 + if db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + return { + 'success': False, + 'message': f'不支持的数据库类型: {db_type}' + } + + # 检查是否已有连接 + if user_id_str not in db_tool.connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接数据库' + } + + # 直接使用已建立的连接执行查询 + sql_query = f"SELECT * FROM {table_name}" + result = await db_tool.execute( + operation="execute_query", + user_id=user_id_str, + sql_query=sql_query, + limit=limit + ) + + if not result.success: + return { + 'success': False, + 'message': result.error + } + + return { + 'success': True, + 'data': result.result, + 'db_type': db_type + } + + except Exception as e: + logger.error(f"获取表数据失败: {str(e)}", exc_info=True) + return { + 'success': False, + 'message': f'获取表数据失败: {str(e)}' + } + + def disconnect_database(self, user_id: int) -> Dict[str, Any]: + """断开数据库连接""" + try: + # 从PostgreSQLMCPTool断开连接 + self.postgresql_tool.execute( + operation="disconnect", + user_id=str(user_id) + ) + + # 从本地连接管理中移除 + if user_id in self.user_connections: + del self.user_connections[user_id] + + return { + 'success': True, + 'message': '数据库连接已断开' + } + except Exception as e: + return { + 'success': False, + 'message': f'断开连接失败: {str(e)}' + } + + def get_config_by_type(self, user_id: int, db_type: str) -> Optional[DatabaseConfig]: + """根据数据库类型获取用户配置""" + return self.db.query(DatabaseConfig).filter( + and_( + DatabaseConfig.created_by == user_id, + DatabaseConfig.db_type == db_type, + DatabaseConfig.is_active == True + ) + ).first() + + async def create_or_update_config(self, user_id: int, config_data: Dict[str, Any]) -> DatabaseConfig: + """创建或更新数据库配置(保证db_type唯一性)""" + try: + # 检查是否已存在该类型的配置 + existing_config = self.get_config_by_type(user_id, config_data['db_type']) + + if existing_config: + # 更新现有配置 + for key, value in config_data.items(): + if key == 'password': + setattr(existing_config, key, self._encrypt_password(value)) + elif hasattr(existing_config, key): + setattr(existing_config, key, value) + + self.db.commit() + self.db.refresh(existing_config) + logger.info(f"更新数据库配置成功: {existing_config.name} (ID: {existing_config.id})") + return existing_config + else: + # 创建新配置 + return await self.create_config(user_id, config_data) + + except Exception as e: + self.db.rollback() + logger.error(f"创建或更新数据库配置失败: {str(e)}") + raise \ No newline at end of file diff --git a/backend/th_agenter/services/document.py b/backend/th_agenter/services/document.py new file mode 100644 index 0000000..fe31a7c --- /dev/null +++ b/backend/th_agenter/services/document.py @@ -0,0 +1,301 @@ +"""Document service.""" + +import os +import logging +import hashlib +import mimetypes +from pathlib import Path +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from fastapi import UploadFile + +from ..models.knowledge_base import Document, KnowledgeBase +from ..core.config import get_settings +from ..utils.file_utils import FileUtils +from .storage import storage_service +from .document_processor import get_document_processor +from ..utils.schemas import DocumentChunk + +logger = logging.getLogger(__name__) +settings = get_settings() + + +class DocumentService: + """Document service for managing documents in knowledge bases.""" + + def __init__(self, db: Session): + self.db = db + self.file_utils = FileUtils() + + async def upload_document(self, file: UploadFile, kb_id: int) -> Document: + """Upload a document to knowledge base.""" + try: + # Validate knowledge base exists + kb = self.db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first() + if not kb: + raise ValueError(f"Knowledge base {kb_id} not found") + + # Validate file + if not file.filename: + raise ValueError("No filename provided") + + # Validate file extension + file_extension = Path(file.filename).suffix.lower() + if file_extension not in settings.file.allowed_extensions: + raise ValueError(f"File type {file_extension} not allowed") + + # Upload file using storage service + storage_info = await storage_service.upload_file(file, kb_id) + + # Create document record + document = Document( + knowledge_base_id=kb_id, + filename=os.path.basename(storage_info["file_path"]), + original_filename=file.filename, + file_path=storage_info.get("full_path", storage_info["file_path"]), # Use absolute path if available + file_size=storage_info["size"], + file_type=file_extension, + mime_type=storage_info["mime_type"], + is_processed=False + ) + + # Set audit fields + document.set_audit_fields() + + self.db.add(document) + self.db.commit() + self.db.refresh(document) + + logger.info(f"Uploaded document: {file.filename} to KB {kb_id} (Doc ID: {document.id})") + return document + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to upload document: {e}") + raise + + def get_document(self, doc_id: int, kb_id: int = None) -> Optional[Document]: + """Get document by ID, optionally filtered by knowledge base.""" + query = self.db.query(Document).filter(Document.id == doc_id) + if kb_id is not None: + query = query.filter(Document.knowledge_base_id == kb_id) + return query.first() + + def get_documents(self, kb_id: int, skip: int = 0, limit: int = 50) -> List[Document]: + """Get documents in knowledge base.""" + return ( + self.db.query(Document) + .filter(Document.knowledge_base_id == kb_id) + .offset(skip) + .limit(limit) + .all() + ) + + def list_documents(self, kb_id: int, skip: int = 0, limit: int = 50) -> tuple[List[Document], int]: + """List documents in knowledge base with total count.""" + # Get total count + total = self.db.query(Document).filter(Document.knowledge_base_id == kb_id).count() + + # Get documents with pagination + documents = ( + self.db.query(Document) + .filter(Document.knowledge_base_id == kb_id) + .offset(skip) + .limit(limit) + .all() + ) + + return documents, total + + def delete_document(self, doc_id: int, kb_id: int = None) -> bool: + """Delete document.""" + try: + document = self.get_document(doc_id, kb_id) + if not document: + return False + + # Delete file from storage + try: + storage_service.delete_file(document.file_path) + logger.info(f"Deleted file: {document.file_path}") + except Exception as e: + logger.warning(f"Failed to delete file {document.file_path}: {e}") + + # TODO: Remove from vector database + # This should be implemented when vector database service is ready + get_document_processor().delete_document_from_vector_store(kb_id,doc_id) + # Delete database record + self.db.delete(document) + self.db.commit() + + logger.info(f"Deleted document: {document.filename} (ID: {doc_id})") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to delete document {doc_id}: {e}") + raise + + async def process_document(self, doc_id: int, kb_id: int = None) -> Dict[str, Any]: + """Process document (extract text and create embeddings).""" + try: + document = self.get_document(doc_id, kb_id) + if not document: + raise ValueError(f"Document {doc_id} not found") + + if document.is_processed: + logger.info(f"Document {doc_id} already processed") + return { + "document_id": doc_id, + "status": "already_processed", + "message": "文档已处理" + } + + # 更新文档状态为处理中 + document.processing_error = None + self.db.commit() + + # 调用文档处理器进行处理 + result = get_document_processor().process_document( + document_id=doc_id, + file_path=document.file_path, + knowledge_base_id=document.knowledge_base_id + ) + + # 如果处理成功,更新文档状态 + if result["status"] == "success": + document.is_processed = True + document.chunk_count = result.get("chunks_count", 0) + self.db.commit() + self.db.refresh(document) + logger.info(f"Processed document: {document.filename} (ID: {doc_id})") + + return result + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to process document {doc_id}: {e}") + + # Update document with error + try: + document = self.get_document(doc_id) + if document: + document.processing_error = str(e) + self.db.commit() + except Exception as db_error: + logger.error(f"Failed to update document error status: {db_error}") + + return { + "document_id": doc_id, + "status": "failed", + "error": str(e), + "message": "文档处理失败" + } + + async def _extract_text(self, document: Document) -> str: + """Extract text content from document.""" + try: + if document.is_text_file: + # Read text files directly + with open(document.file_path, 'r', encoding='utf-8') as f: + return f.read() + + elif document.is_pdf_file: + # TODO: Implement PDF text extraction using PyPDF2 or similar + # For now, return placeholder + return f"PDF content from {document.original_filename}" + + elif document.is_office_file: + # TODO: Implement Office file text extraction using python-docx, openpyxl, etc. + # For now, return placeholder + return f"Office document content from {document.original_filename}" + + else: + raise ValueError(f"Unsupported file type: {document.file_type}") + + except Exception as e: + logger.error(f"Failed to extract text from {document.file_path}: {e}") + raise + + def update_document_status(self, doc_id: int, is_processed: bool, error: Optional[str] = None) -> bool: + """Update document processing status.""" + try: + document = self.get_document(doc_id) + if not document: + return False + + document.is_processed = is_processed + document.processing_error = error + + self.db.commit() + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to update document status {doc_id}: {e}") + raise + + def search_documents(self, kb_id: int, query: str, limit: int = 5) -> List[Dict[str, Any]]: + """Search documents in knowledge base using vector similarity.""" + try: + # 使用文档处理器进行相似性搜索 + results = get_document_processor().search_similar_documents(kb_id, query, limit) + return results + except Exception as e: + logger.error(f"Failed to search documents in KB {kb_id}: {e}") + return [] + + def get_document_stats(self, kb_id: int) -> Dict[str, Any]: + """Get document statistics for knowledge base.""" + documents = self.get_documents(kb_id, limit=1000) # Get all documents + + total_count = len(documents) + processed_count = len([doc for doc in documents if doc.is_processed]) + total_size = sum(doc.file_size for doc in documents) + + file_types = {} + for doc in documents: + file_type = doc.file_type + file_types[file_type] = file_types.get(file_type, 0) + 1 + + return { + "total_documents": total_count, + "processed_documents": processed_count, + "pending_documents": total_count - processed_count, + "total_size_bytes": total_size, + "total_size_mb": round(total_size / (1024 * 1024), 2), + "file_types": file_types + } + + def get_document_chunks(self, doc_id: int) -> List[DocumentChunk]: + """Get document chunks for a specific document.""" + try: + # Get document to find knowledge base ID + document = self.db.query(Document).filter(Document.id == doc_id).first() + if not document: + logger.error(f"Document {doc_id} not found") + return [] + + # Get chunks from document processor + chunks_data = get_document_processor().get_document_chunks(document.knowledge_base_id, doc_id) + + # Convert to DocumentChunk objects + chunks = [] + for chunk_data in chunks_data: + chunk = DocumentChunk( + id=chunk_data["id"], + content=chunk_data["content"], + metadata=chunk_data["metadata"], + page_number=chunk_data.get("page_number"), + chunk_index=chunk_data["chunk_index"], + start_char=chunk_data.get("start_char"), + end_char=chunk_data.get("end_char") + ) + chunks.append(chunk) + + logger.info(f"Retrieved {len(chunks)} chunks for document {doc_id}") + return chunks + + except Exception as e: + logger.error(f"Failed to get chunks for document {doc_id}: {e}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/document_processor.py b/backend/th_agenter/services/document_processor.py new file mode 100644 index 0000000..7b97c63 --- /dev/null +++ b/backend/th_agenter/services/document_processor.py @@ -0,0 +1,1005 @@ +"""文档处理服务,负责文档的分段、向量化和索引""" + +import os +import logging +from typing import List, Dict, Any, Optional +from pathlib import Path +from urllib.parse import quote +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import QueuePool +from langchain_text_splitters import RecursiveCharacterTextSplitter + +logger = logging.getLogger(__name__) + +# Try to import pdfplumber with exception handling +try: + import pdfplumber + PDFPLUMBER_AVAILABLE = True +except ImportError: + logger.warning("pdfplumber not available. PDF processing features will be disabled.") + pdfplumber = None + PDFPLUMBER_AVAILABLE = False + +# Try to import langchain_core and langchain_postgres with exception handling +try: + from langchain_core.documents import Document + from langchain_postgres import PGVector + LANGCHAIN_CORE_AVAILABLE = True + LANGCHAIN_POSTGRES_AVAILABLE = True +except ImportError as e: + logger.warning(f"Some langchain modules not available: {e}. Document processing features may be limited.") + Document = None + PGVector = None + LANGCHAIN_CORE_AVAILABLE = False + LANGCHAIN_POSTGRES_AVAILABLE = False +from typing import List +# 旧的ZhipuEmbeddings类已移除,现在统一使用EmbeddingFactory创建embedding实例 + +from ..core.config import settings +from ..utils.file_utils import FileUtils +from ..models.knowledge_base import Document as DocumentModel +from ..db.database import get_db + +# Try to import document loaders with exception handling +try: + from langchain_community.document_loaders import ( + TextLoader, + PyPDFLoader, + Docx2txtLoader, + UnstructuredMarkdownLoader + ) + DOCUMENT_LOADERS_AVAILABLE = True +except ImportError: + logger.warning("langchain_community.document_loaders not available. Document processing features will be disabled.") + TextLoader = None + PyPDFLoader = None + Docx2txtLoader = None + UnstructuredMarkdownLoader = None + DOCUMENT_LOADERS_AVAILABLE = False + + +class PGVectorConnectionPool: + """PGVector连接池管理器""" + + def __init__(self): + self.engine = None + self.SessionLocal = None + self._init_connection_pool() + + def _init_connection_pool(self): + """初始化连接池""" + if settings.vector_db.type == "pgvector": + # 构建连接字符串,对密码进行URL编码以处理特殊字符(如@符号) + encoded_password = quote(settings.vector_db.pgvector_password, safe="") + connection_string = ( + f"postgresql://{settings.vector_db.pgvector_user}:" + f"{encoded_password}@" + f"{settings.vector_db.pgvector_host}:" + f"{settings.vector_db.pgvector_port}/" + f"{settings.vector_db.pgvector_database}" + ) + + # 创建SQLAlchemy引擎,配置连接池 + self.engine = create_engine( + connection_string, + poolclass=QueuePool, + pool_size=5, # 连接池大小 + max_overflow=10, # 最大溢出连接数 + pool_pre_ping=True, # 连接前ping检查 + pool_recycle=3600, # 连接回收时间(秒) + echo=False # 是否打印SQL语句 + ) + + # 创建会话工厂 + self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) + logger.info(f"PGVector连接池已初始化: {settings.vector_db.pgvector_host}:{settings.vector_db.pgvector_port}") + + def get_session(self): + """获取数据库会话""" + if self.SessionLocal is None: + raise RuntimeError("连接池未初始化") + return self.SessionLocal() + + def execute_query(self, query: str, params: tuple = None): + """执行查询并返回结果""" + session = self.get_session() + try: + result = session.execute(text(query), params or {}) + return result.fetchall() + finally: + session.close() + + +class DocumentProcessor: + """文档处理器,负责文档的加载、分段和向量化""" + + def __init__(self): + # 初始化语义分割器配置 + self.semantic_splitter_enabled = settings.file.semantic_splitter_enabled + self.text_splitter = RecursiveCharacterTextSplitter( + chunk_size=settings.file.chunk_size, + chunk_overlap=settings.file.chunk_overlap, + length_function=len, + separators=["\n\n", "\n", " ", ""] + ) + + # 初始化嵌入模型 - 根据配置选择提供商 + self._init_embeddings() + + # 初始化连接池(仅对PGVector) + self.pgvector_pool = None + + # PostgreSQL pgvector连接配置 + print('settings.vector_db.type=============', settings.vector_db.type) + if settings.vector_db.type == "pgvector": + # 新版本PGVector使用psycopg3连接字符串 + # 对密码进行URL编码以处理特殊字符(如@符号) + encoded_password = quote(settings.vector_db.pgvector_password, safe="") + self.connection_string = ( + f"postgresql+psycopg://{settings.vector_db.pgvector_user}:" + f"{encoded_password}@" + f"{settings.vector_db.pgvector_host}:" + f"{settings.vector_db.pgvector_port}/" + f"{settings.vector_db.pgvector_database}" + ) + # 初始化连接池 + self.pgvector_pool = PGVectorConnectionPool() + else: + # 向量数据库存储路径(Chroma兼容) + vector_db_path = settings.vector_db.persist_directory + if not os.path.isabs(vector_db_path): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + vector_db_path = str(backend_dir / vector_db_path) + self.vector_db_path = vector_db_path + + def _init_embeddings(self): + """根据配置初始化embedding模型""" + from .embedding_factory import EmbeddingFactory + self.embeddings = EmbeddingFactory.create_embeddings() + + def load_document(self, file_path: str) -> List[Document]: + """根据文件类型加载文档""" + file_extension = Path(file_path).suffix.lower() + + try: + if file_extension == '.txt': + loader = TextLoader(file_path, encoding='utf-8') + documents = loader.load() + elif file_extension == '.pdf': + # 使用pdfplumber处理PDF文件,更稳定 + documents = self._load_pdf_with_pdfplumber(file_path) + elif file_extension == '.docx': + loader = Docx2txtLoader(file_path) + documents = loader.load() + elif file_extension == '.md': + loader = UnstructuredMarkdownLoader(file_path) + documents = loader.load() + else: + raise ValueError(f"不支持的文件类型: {file_extension}") + + logger.info(f"成功加载文档: {file_path}, 页数: {len(documents)}") + return documents + + except Exception as e: + logger.error(f"加载文档失败 {file_path}: {str(e)}") + raise + + def _load_pdf_with_pdfplumber(self, file_path: str) -> List[Document]: + """使用pdfplumber加载PDF文档""" + documents = [] + try: + with pdfplumber.open(file_path) as pdf: + for page_num, page in enumerate(pdf.pages): + text = page.extract_text() + if text and text.strip(): # 只处理有文本内容的页面 + doc = Document( + page_content=text, + metadata={ + "source": file_path, + "page": page_num + 1 + } + ) + documents.append(doc) + return documents + except Exception as e: + logger.error(f"使用pdfplumber加载PDF失败 {file_path}: {str(e)}") + # 如果pdfplumber失败,回退到PyPDFLoader + try: + loader = PyPDFLoader(file_path) + return loader.load() + except Exception as fallback_e: + logger.error(f"PyPDFLoader回退也失败 {file_path}: {str(fallback_e)}") + raise fallback_e + + def _merge_documents(self, documents: List[Document]) -> Document: + """将多个文档合并成一个文档""" + merged_text = "" + merged_metadata = {} + + for doc in documents: + if merged_text: + merged_text += "\n\n" + merged_text += doc.page_content + # 合并元数据 + merged_metadata.update(doc.metadata) + + return Document(page_content=merged_text, metadata=merged_metadata) + + def _get_semantic_split_points(self, text: str) -> List[str]: + """使用大模型分析文档内容,返回合适的分割点列表""" + try: + from langchain.chat_models import ChatOpenAI + from ..core.config import get_settings + + + + prompt = f""" + # 任务说明 + 请分析文档内容,识别出适合作为分割点的关键位置。分割点应该是能够将文档划分为有意义段落的文本片段。 + + + # 分割规则 + 请严格按照以下规则识别分割点: + + ## 基本要求 + 1. 分割点必须是完整的句子开头或段落开头 + 2. 每个分割后的部分应包含相对完整的语义内容 + 3. 每个分割部分的理想长度控制在500字以内,严禁超过1000字。如果超过了1000字,要强制分段。 + + ## 短段落处理 + 4. 如果某部分长度可能小于50字,应将其与后续内容合并,避免产生过短片段 + + ## 唯一性保证(重要) + 5. 确保每个分割点在文档中具有唯一性: + - 检查文内是否存在相同的文本片段 + - 如果存在重复,需要扩展分割点字符串,直到获得唯一标识 + - 扩展方法:在当前分割点后追加几个字符,形成更长的唯一字符串 + + ## 示例说明 + 原始文档: + "目录: + 第一章 标题一 + 第二章 标题二 + 正文 + 第一章 标题一 + 这是第一章的内容 + + 第二章 标题二 + 这是第二章的内容" + + 错误分割点:"第一章 标题一"(在目录和正文中重复出现) + + 正确分割点:"第一章 标题一\n这是第"(通过追加内容确保唯一性) + + # 输出格式 + - 只返回分割点文本字符串 + - 每个分割点用~~分隔 + - 不要包含任何其他内容或解释 + + 示例输出:分割点1~~分割点2~~分割点3 + + + 文档内容: + {text[:10000]} # 限制输入长度 + """ + from ..core.llm import create_llm + llm = create_llm(temperature=0.2) + + response = llm.invoke(prompt) + + # 解析响应获取分割点列表 + split_points = [point.strip() for point in response.content.split('~~') if point.strip()] + logger.info(f"语义分析得到 {len(split_points)} 个分割点") + return split_points + + except Exception as e: + logger.error(f"获取语义分割点失败: {str(e)}") + return [] + + def _split_by_semantic_points(self, text: str, split_points: List[str]) -> List[str]: + """根据语义分割点切分文本""" + chunks = [] + current_pos = 0 + + # 按顺序查找每个分割点并切分文本 + for point in split_points: + pos = text.find(point, current_pos) + if pos != -1: + # 添加当前位置到分割点位置的文本块 + if pos > current_pos: + chunk = text[current_pos:pos].strip() + if chunk: + chunks.append(chunk) + current_pos = pos + + # 添加最后一个文本块 + if current_pos < len(text): + chunk = text[current_pos:].strip() + if chunk: + chunks.append(chunk) + + return chunks + + def split_documents(self, documents: List[Document]) -> List[Document]: + """将文档分割成小块(含短段落合并和超长强制分割功能)""" + try: + if self.semantic_splitter_enabled and documents: + # 1. 合并文档 + merged_doc = self._merge_documents(documents) + + # 2. 获取语义分割点 + split_points = self._get_semantic_split_points(merged_doc.page_content) + + if split_points: + # 3. 根据语义分割点切分文本 + text_chunks = self._split_by_semantic_points(merged_doc.page_content, split_points) + + # 4. 处理短段落合并和超长强制分割(新增逻辑) + processed_chunks = [] + buffer = "" + for chunk in text_chunks: + # 先检查当前chunk是否超长(超过1000字符) + if len(chunk) > 1000: + # 如果有缓冲内容,先处理缓冲 + if buffer: + processed_chunks.append(buffer) + buffer = "" + + # 对超长chunk进行强制分割 + forced_splits = self._force_split_long_chunk(chunk) + processed_chunks.extend(forced_splits) + else: + # 正常处理短段落合并 + if not buffer: + buffer = chunk + else: + if len(buffer) < 100: + buffer = f"{buffer}\n{chunk}" + else: + processed_chunks.append(buffer) + buffer = chunk + + # 添加最后剩余的缓冲内容 + if buffer: + processed_chunks.append(buffer) + + # 5. 创建Document对象 + chunks = [] + for i, chunk in enumerate(processed_chunks): + doc = Document( + page_content=chunk, + metadata={ + **merged_doc.metadata, + 'chunk_index': i, + 'merged': len(chunk) > 100, # 标记是否经过合并 + 'forced_split': len(chunk) > 1000 # 标记是否经过强制分割 + } + ) + chunks.append(doc) + else: + # 如果获取分割点失败,回退到默认分割器 + logger.warning("语义分割失败,使用默认分割器") + chunks = self.text_splitter.split_documents(documents) + else: + # 使用默认分割器 + chunks = self.text_splitter.split_documents(documents) + + logger.info(f"文档分割完成,共生成 {len(chunks)} 个文档块") + return chunks + + except Exception as e: + logger.error(f"文档分割失败: {str(e)}") + raise + + def _force_split_long_chunk(self, chunk: str) -> List[str]: + """强制分割超长段落(超过1000字符)""" + max_length = 1000 + chunks = [] + + # 先尝试按换行符分割 + if '\n' in chunk: + lines = chunk.split('\n') + current_chunk = "" + for line in lines: + if len(current_chunk) + len(line) + 1 > max_length: + if current_chunk: + chunks.append(current_chunk) + current_chunk = line + else: + chunks.append(line[:max_length]) + current_chunk = line[max_length:] + else: + if current_chunk: + current_chunk += "\n" + line + else: + current_chunk = line + if current_chunk: + chunks.append(current_chunk) + else: + # 没有换行符则直接按长度分割 + chunks = [chunk[i:i + max_length] for i in range(0, len(chunk), max_length)] + + return chunks + + def create_vector_store(self, knowledge_base_id: int, documents: List[Document], document_id: int = None) -> str: + """为知识库创建向量存储""" + try: + if settings.vector_db.type == "pgvector": + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 创建PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + # 创建新版本PGVector实例 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True # 使用JSONB存储元数据 + ) + + # 手动添加文档 + vector_store.add_documents(documents) + + logger.info(f"PostgreSQL pgvector存储创建成功: {collection_name}") + return collection_name + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 创建向量存储 + vector_store = Chroma.from_documents( + documents=documents, + embedding=self.embeddings, + persist_directory=kb_vector_path + ) + + # 持久化向量存储 + vector_store.persist() + + logger.info(f"向量存储创建成功: {kb_vector_path}") + return kb_vector_path + + except Exception as e: + logger.error(f"创建向量存储失败: {str(e)}") + raise + + def add_documents_to_vector_store(self, knowledge_base_id: int, documents: List[Document], document_id: int = None) -> None: + """向现有向量存储添加文档""" + try: + if settings.vector_db.type == "pgvector": + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + try: + # 连接到现有集合 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + # 添加新文档 + vector_store.add_documents(documents) + except Exception as e: + # 如果集合不存在,创建新的向量存储 + logger.warning(f"连接现有向量存储失败,创建新的向量存储: {e}") + self.create_vector_store(knowledge_base_id, documents, document_id) + return + + logger.info(f"文档已添加到PostgreSQL pgvector存储: {collection_name}") + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + # 检查向量存储是否存在 + if not os.path.exists(kb_vector_path): + # 如果不存在,创建新的向量存储 + self.create_vector_store(knowledge_base_id, documents, document_id) + return + + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 加载现有向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 添加新文档 + vector_store.add_documents(documents) + vector_store.persist() + + logger.info(f"文档已添加到向量存储: {kb_vector_path}") + + except Exception as e: + logger.error(f"添加文档到向量存储失败: {str(e)}") + raise + + def process_document(self, document_id: int, file_path: str, knowledge_base_id: int) -> Dict[str, Any]: + """处理单个文档:加载、分段、向量化""" + try: + logger.info(f"开始处理文档 ID: {document_id}, 路径: {file_path}") + + # 1. 加载文档 + documents = self.load_document(file_path) + + # 2. 分割文档 + chunks = self.split_documents(documents) + + # 3. 添加到向量存储 + self.add_documents_to_vector_store(knowledge_base_id, chunks, document_id) + + # 4. 更新文档状态 + with next(get_db()) as db: + document = db.query(DocumentModel).filter(DocumentModel.id == document_id).first() + if document: + document.status = "processed" + document.chunk_count = len(chunks) + db.commit() + + result = { + "document_id": document_id, + "status": "success", + "chunks_count": len(chunks), + "message": "文档处理完成" + } + + logger.info(f"文档处理完成: {result}") + return result + + except Exception as e: + logger.error(f"文档处理失败 ID: {document_id}: {str(e)}") + + # 更新文档状态为失败 + try: + with next(get_db()) as db: + document = db.query(DocumentModel).filter(DocumentModel.id == document_id).first() + if document: + document.status = "failed" + document.error_message = str(e) + db.commit() + except Exception as db_error: + logger.error(f"更新文档状态失败: {str(db_error)}") + + return { + "document_id": document_id, + "status": "failed", + "error": str(e), + "message": "文档处理失败" + } + + def _get_document_ids_from_vector_store(self, knowledge_base_id: int, document_id: int) -> List[str]: + """查询指定document_id的所有向量记录的uuid""" + try: + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + # 使用连接池执行查询 + if self.pgvector_pool: + query = f""" + SELECT uuid FROM langchain_pg_embedding + WHERE collection_id = ( + SELECT uuid FROM langchain_pg_collection + WHERE name = %s + ) AND cmetadata->>'document_id' = %s + """ + + result = self.pgvector_pool.execute_query(query, (collection_name, str(document_id))) + return [row[0] for row in result] if result else [] + else: + logger.warning("PGVector连接池未初始化") + return [] + + except Exception as e: + logger.error(f"查询文档向量记录失败: {str(e)}") + return [] + + def delete_document_from_vector_store(self, knowledge_base_id: int, document_id: int) -> None: + """从向量存储中删除文档""" + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + # 创建新版本PGVector实例 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 直接从数据库查询要删除的文档UUID + try: + from sqlalchemy import text + from sqlalchemy.orm import Session + + # 获取数据库引擎 + engine = vector_store._engine + + with Session(engine) as session: + # 查询匹配document_id的所有记录的ID + query_sql = text( + f"SELECT id FROM langchain_pg_embedding " + f"WHERE cmetadata->>'document_id' = :doc_id" + ) + result = session.execute(query_sql, {"doc_id": str(document_id)}) + ids_to_delete = [row[0] for row in result.fetchall()] + + if ids_to_delete: + # 使用ID删除文档 + vector_store.delete(ids=ids_to_delete) + logger.info(f"成功删除 {len(ids_to_delete)} 个文档块: document_id={document_id}") + else: + logger.warning(f"未找到要删除的文档ID: document_id={document_id}") + + except Exception as query_error: + logger.error(f"查询要删除的文档时出错: {query_error}") + # 如果查询失败,说明文档可能不存在 + logger.warning(f"无法查询到要删除的文档: document_id={document_id}") + return + + logger.info(f"文档已从PostgreSQL pgvector存储中删除: document_id={document_id}") + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储不存在或删除失败: {collection_name}, {str(e)}") + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"向量存储不存在: {kb_vector_path}") + return + + # 加载向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 删除相关文档块(这里需要根据实际的Chroma API来实现) + # 注意:Chroma的删除功能可能需要特定的实现方式 + logger.info(f"文档已从向量存储中删除: document_id={document_id}") + + except Exception as e: + logger.error(f"从向量存储删除文档失败: {str(e)}") + raise + + def get_document_chunks(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """获取文档的所有分段内容 + + 改进说明: + - 避免使用空查询进行相似性搜索,防止触发不必要的embedding API调用 + - 优先使用直接SQL查询,提高性能 + - 确保结果按chunk_index排序 + """ + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 - 使用直接SQL查询避免相似性搜索 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + # 尝试直接SQL查询(推荐方法) + chunks = self._get_chunks_by_sql(knowledge_base_id, document_id) + if chunks: + return chunks + + # 如果SQL查询失败,回退到改进的LangChain方法 + logger.info("SQL查询失败,使用LangChain回退方案") + return self._get_chunks_by_langchain_improved(knowledge_base_id, document_id, collection_name) + + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储访问失败: {collection_name}, {str(e)}") + return [] + else: + # Chroma兼容模式 + return self._get_chunks_chroma(knowledge_base_id, document_id) + + except Exception as e: + logger.error(f"获取文档分段失败 document_id: {document_id}, kb_id: {knowledge_base_id}: {str(e)}") + return [] + + def _get_chunks_by_sql(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """使用SQLAlchemy连接池查询获取文档分段(推荐方法)""" + try: + if not self.pgvector_pool: + logger.error("PGVector连接池未初始化") + return [] + + # 直接SQL查询,避免相似性搜索和embedding计算 + query = """ + SELECT + id, + document, + cmetadata + FROM langchain_pg_embedding + WHERE cmetadata->>'document_id' = :document_id + AND cmetadata->>'knowledge_base_id' = :knowledge_base_id + ORDER BY + CAST(cmetadata->>'chunk_index' AS INTEGER) ASC; + """ + + # 使用连接池执行查询 + session = self.pgvector_pool.get_session() + try: + result = session.execute( + text(query), + { + 'document_id': str(document_id), + 'knowledge_base_id': str(knowledge_base_id) + } + ) + results = result.fetchall() + + chunks = [] + for row in results: + # SQLAlchemy结果行访问 + metadata = row.cmetadata + chunk = { + "id": f"chunk_{document_id}_{metadata.get('chunk_index', 0)}", + "content": row.document, + "metadata": metadata, + "page_number": metadata.get("page"), + "chunk_index": metadata.get("chunk_index", 0), + "start_char": metadata.get("start_char"), + "end_char": metadata.get("end_char") + } + chunks.append(chunk) + + logger.info(f"通过SQLAlchemy连接池查询获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + finally: + session.close() + + except Exception as e: + logger.error(f"SQLAlchemy连接池查询失败: {e}") + return [] + + def _get_chunks_by_langchain_improved(self, knowledge_base_id: int, document_id: int, collection_name: str) -> List[Dict[str, Any]]: + """改进的LangChain查询方法(回退方案)""" + try: + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 使用有意义的查询而不是空查询,避免触发embedding API错误 + # 先尝试获取少量结果来构造查询 + try: + sample_results = vector_store.similarity_search( + query="文档内容", # 使用通用查询词而非空字符串 + k=5, + filter={"document_id": {"$eq": str(document_id)}} + ) + + if sample_results: + # 使用第一个结果的内容片段作为查询 + first_content = sample_results[0].page_content[:50] + results = vector_store.similarity_search( + query=first_content, + k=1000, + filter={"document_id": {"$eq": str(document_id)}} + ) + else: + # 如果没有结果,尝试不使用filter的查询 + results = vector_store.similarity_search( + query="文档", + k=1000 + ) + # 手动过滤结果 + results = [doc for doc in results if doc.metadata.get("document_id") == str(document_id)] + + except Exception as e: + logger.warning(f"改进的相似性搜索失败: {e}") + return [] + + chunks = [] + for i, doc in enumerate(results): + chunk = { + "id": f"chunk_{document_id}_{i}", + "content": doc.page_content, + "metadata": doc.metadata, + "page_number": doc.metadata.get("page"), + "chunk_index": doc.metadata.get("chunk_index", i), + "start_char": doc.metadata.get("start_char"), + "end_char": doc.metadata.get("end_char") + } + chunks.append(chunk) + + # 按chunk_index排序 + chunks.sort(key=lambda x: x.get("chunk_index", 0)) + + logger.info(f"通过改进的LangChain方法获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + except Exception as e: + logger.error(f"LangChain改进方法失败: {e}") + return [] + + def _get_chunks_chroma(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """Chroma存储的处理逻辑""" + try: + from langchain_community.vectorstores import Chroma + + # 构建向量数据库路径 + vector_db_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(vector_db_path): + logger.warning(f"向量数据库不存在: {vector_db_path}") + return [] + + # 加载向量数据库 + vectorstore = Chroma( + persist_directory=vector_db_path, + embedding_function=self.embeddings + ) + + # 获取所有文档的元数据,筛选出指定文档的分段 + collection = vectorstore._collection + all_docs = collection.get(include=["metadatas", "documents"]) + + chunks = [] + chunk_index = 0 + + for i, metadata in enumerate(all_docs["metadatas"]): + if metadata.get("document_id") == str(document_id): + chunk_content = all_docs["documents"][i] + + chunk = { + "id": f"chunk_{document_id}_{chunk_index}", + "content": chunk_content, + "metadata": metadata, + "page_number": metadata.get("page"), + "chunk_index": chunk_index, + "start_char": metadata.get("start_char"), + "end_char": metadata.get("end_char") + } + chunks.append(chunk) + chunk_index += 1 + + logger.info(f"获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + except Exception as e: + logger.error(f"Chroma存储处理失败: {e}") + return [] + + def search_similar_documents(self, knowledge_base_id: int, query: str, k: int = 5) -> List[Dict[str, Any]]: + """在知识库中搜索相似文档""" + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 执行相似性搜索 + results = vector_store.similarity_search_with_score(query, k=k) + + # 格式化结果 + formatted_results = [] + for doc, distance_score in results: + # pgvector使用余弦距离,距离越小相似度越高 + # 将距离转换为0-1之间的相似度分数 + similarity_score = 1.0 / (1.0 + distance_score) + + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": distance_score, # 保留原始距离分数 + "normalized_score": similarity_score, # 归一化相似度分数 + "source": doc.metadata.get('filename', 'unknown'), + "document_id": doc.metadata.get('document_id', 'unknown'), + "chunk_id": doc.metadata.get('chunk_id', 'unknown') + }) + + # 按相似度分数排序(距离越小越相似) + formatted_results.sort(key=lambda x: x['similarity_score']) + + logger.info(f"PostgreSQL pgvector搜索完成,找到 {len(formatted_results)} 个相关文档") + return formatted_results + + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储不存在: {collection_name}, {str(e)}") + return [] + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"向量存储不存在: {kb_vector_path}") + return [] + + # 加载向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 执行相似性搜索 + results = vector_store.similarity_search_with_score(query, k=k) + + # 格式化结果 + formatted_results = [] + for doc, distance_score in results: + # Chroma使用欧几里得距离,距离越小相似度越高 + # 将距离转换为0-1之间的相似度分数 + similarity_score = 1.0 / (1.0 + distance_score) + + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": distance_score, # 保留原始距离分数 + "normalized_score": similarity_score, # 归一化相似度分数 + "source": doc.metadata.get('filename', 'unknown'), + "document_id": doc.metadata.get('document_id', 'unknown'), + "chunk_id": doc.metadata.get('chunk_id', 'unknown') + }) + + # 按相似度分数排序(距离越小越相似) + formatted_results.sort(key=lambda x: x['similarity_score']) + + logger.info(f"搜索完成,找到 {len(formatted_results)} 个相关文档") + return formatted_results + + except Exception as e: + logger.error(f"搜索文档失败: {str(e)}") + return [] # 返回空列表而不是抛出异常 + + +# 全局文档处理器实例(延迟初始化) +document_processor = None + +def get_document_processor(): + """获取文档处理器实例(延迟初始化)""" + global document_processor + if document_processor is None: + document_processor = DocumentProcessor() + return document_processor \ No newline at end of file diff --git a/backend/th_agenter/services/embedding_factory.py b/backend/th_agenter/services/embedding_factory.py new file mode 100644 index 0000000..b55ed23 --- /dev/null +++ b/backend/th_agenter/services/embedding_factory.py @@ -0,0 +1,96 @@ +"""Embedding factory for different providers.""" + +from typing import Optional +from langchain_core.embeddings import Embeddings +from langchain_openai import OpenAIEmbeddings +# Try to import HuggingFace embeddings with exception handling +try: + from langchain_community.embeddings import HuggingFaceEmbeddings +except ImportError: + HuggingFaceEmbeddings = None +from .zhipu_embeddings import ZhipuOpenAIEmbeddings +from ..core.config import settings +from ..utils.logger import get_logger + +logger = get_logger("embedding_factory") + + +class EmbeddingFactory: + """Factory class for creating embedding instances based on provider.""" + + @staticmethod + def create_embeddings( + provider: Optional[str] = None, + model: Optional[str] = None, + dimensions: Optional[int] = None + ) -> Embeddings: + """Create embeddings instance based on provider. + + Args: + provider: Embedding provider (openai, zhipu, deepseek, doubao, moonshot, sentence-transformers) + model: Model name + dimensions: Embedding dimensions + + Returns: + Embeddings instance + """ + # 使用新的embedding配置 + embedding_config = settings.embedding.get_current_config() + provider = provider or settings.embedding.provider + model = model or embedding_config.get("model") + dimensions = dimensions or settings.vector_db.embedding_dimension + + logger.info(f"Creating embeddings with provider: {provider}, model: {model}") + + if provider == "openai": + return EmbeddingFactory._create_openai_embeddings(embedding_config, model, dimensions) + elif provider in ["zhipu", "deepseek", "doubao", "moonshot"]: + return EmbeddingFactory._create_openai_compatible_embeddings(embedding_config, model, dimensions, provider) + elif provider == "sentence-transformers": + if not HuggingFaceEmbeddings: + raise ValueError("HuggingFace embeddings are not available. Please install langchain-community.") + return EmbeddingFactory._create_huggingface_embeddings(model) + else: + raise ValueError(f"Unsupported embedding provider: {provider}") + + @staticmethod + def _create_openai_embeddings(embedding_config: dict, model: str, dimensions: int) -> OpenAIEmbeddings: + """Create OpenAI embeddings.""" + return OpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model if model.startswith("text-embedding") else "text-embedding-ada-002", + dimensions=dimensions if model.startswith("text-embedding-3") else None + ) + + + + @staticmethod + def _create_openai_compatible_embeddings(embedding_config: dict, model: str, dimensions: int, provider: str) -> Embeddings: + """Create OpenAI-compatible embeddings for ZhipuAI, DeepSeek, Doubao, Moonshot.""" + if provider == "zhipu": + return ZhipuOpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model if model.startswith("embedding") else "embedding-3", + dimensions=dimensions + ) + else: + return OpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model, + dimensions=dimensions + ) + + @staticmethod + def _create_huggingface_embeddings(model: str) -> HuggingFaceEmbeddings: + """Create HuggingFace embeddings.""" + if not HuggingFaceEmbeddings: + raise ValueError("HuggingFaceEmbeddings is not available. Please install langchain-community.") + + return HuggingFaceEmbeddings( + model_name=model, + model_kwargs={'device': 'cpu'}, + encode_kwargs={'normalize_embeddings': True} + ) \ No newline at end of file diff --git a/backend/th_agenter/services/excel_metadata_service.py b/backend/th_agenter/services/excel_metadata_service.py new file mode 100644 index 0000000..3989fc0 --- /dev/null +++ b/backend/th_agenter/services/excel_metadata_service.py @@ -0,0 +1,241 @@ +"""Excel metadata extraction service.""" + +import os +import pandas as pd +from typing import Dict, List, Any, Optional, Tuple +from sqlalchemy.orm import Session +from ..models.excel_file import ExcelFile +from ..db.database import get_db +import logging + +logger = logging.getLogger(__name__) + + +class ExcelMetadataService: + """Service for extracting and managing Excel file metadata.""" + + def __init__(self, db: Session): + self.db = db + + def extract_file_metadata(self, file_path: str, original_filename: str, + user_id: int, file_size: int) -> Dict[str, Any]: + """Extract metadata from Excel file.""" + try: + # Determine file type + file_extension = os.path.splitext(original_filename)[1].lower() + + # Read Excel file + if file_extension == '.csv': + # For CSV files, treat as single sheet + df = pd.read_csv(file_path) + sheets_data = {'Sheet1': df} + else: + # For Excel files, read all sheets + sheets_data = pd.read_excel(file_path, sheet_name=None) + + # Extract metadata for each sheet + sheet_names = list(sheets_data.keys()) + columns_info = {} + preview_data = {} + data_types = {} + total_rows = {} + total_columns = {} + + for sheet_name, df in sheets_data.items(): + # Clean column names (remove unnamed columns) + df = df.loc[:, ~df.columns.str.contains('^Unnamed')] + + # Get column information - ensure proper encoding + columns_info[sheet_name] = [str(col) if not isinstance(col, str) else col for col in df.columns.tolist()] + + # Get preview data (first 5 rows) and convert to JSON serializable format + preview_df = df.head(5) + # Convert all values to strings to ensure JSON serialization + preview_values = [] + for row in preview_df.values: + string_row = [] + for value in row: + if pd.isna(value): + string_row.append(None) + elif hasattr(value, 'strftime'): # Handle datetime/timestamp objects + string_row.append(value.strftime('%Y-%m-%d %H:%M:%S')) + else: + # Preserve Chinese characters and other unicode content + if isinstance(value, str): + string_row.append(value) + else: + string_row.append(str(value)) + preview_values.append(string_row) + preview_data[sheet_name] = preview_values + + # Get data types + data_types[sheet_name] = {col: str(dtype) for col, dtype in df.dtypes.items()} + + # Get statistics + total_rows[sheet_name] = len(df) + total_columns[sheet_name] = len(df.columns) + + # Determine default sheet + default_sheet = sheet_names[0] if sheet_names else None + + return { + 'sheet_names': sheet_names, + 'default_sheet': default_sheet, + 'columns_info': columns_info, + 'preview_data': preview_data, + 'data_types': data_types, + 'total_rows': total_rows, + 'total_columns': total_columns, + 'is_processed': True, + 'processing_error': None + } + + except Exception as e: + logger.error(f"Error extracting metadata from {file_path}: {str(e)}") + return { + 'sheet_names': [], + 'default_sheet': None, + 'columns_info': {}, + 'preview_data': {}, + 'data_types': {}, + 'total_rows': {}, + 'total_columns': {}, + 'is_processed': False, + 'processing_error': str(e) + } + + def save_file_metadata(self, file_path: str, original_filename: str, + user_id: int, file_size: int) -> ExcelFile: + """Extract and save Excel file metadata to database.""" + try: + # Extract metadata + metadata = self.extract_file_metadata(file_path, original_filename, user_id, file_size) + + # Determine file type + file_extension = os.path.splitext(original_filename)[1].lower() + + # Create ExcelFile record + excel_file = ExcelFile( + original_filename=original_filename, + file_path=file_path, + file_size=file_size, + file_type=file_extension, + sheet_names=metadata['sheet_names'], + default_sheet=metadata['default_sheet'], + columns_info=metadata['columns_info'], + preview_data=metadata['preview_data'], + data_types=metadata['data_types'], + total_rows=metadata['total_rows'], + total_columns=metadata['total_columns'], + is_processed=metadata['is_processed'], + processing_error=metadata['processing_error'] + ) + + + # Save to database + self.db.add(excel_file) + self.db.commit() + self.db.refresh(excel_file) + + logger.info(f"Saved metadata for file {original_filename} with ID {excel_file.id}") + return excel_file + + except Exception as e: + logger.error(f"Error saving metadata for {original_filename}: {str(e)}") + self.db.rollback() + raise + + def get_user_files(self, user_id: int, skip: int = 0, limit: int = 50) -> Tuple[List[ExcelFile], int]: + """Get Excel files for a user with pagination.""" + try: + # Get total count + total = self.db.query(ExcelFile).filter(ExcelFile.created_by == user_id).count() + + # Get files with pagination + files = (self.db.query(ExcelFile) + .filter(ExcelFile.created_by == user_id) + .order_by(ExcelFile.created_at.desc()) + .offset(skip) + .limit(limit) + .all()) + + return files, total + + except Exception as e: + logger.error(f"Error getting user files for user {user_id}: {str(e)}") + return [], 0 + + def get_file_by_id(self, file_id: int, user_id: int) -> Optional[ExcelFile]: + """Get Excel file by ID and user ID.""" + try: + return (self.db.query(ExcelFile) + .filter(ExcelFile.id == file_id, ExcelFile.created_by == user_id) + .first()) + except Exception as e: + logger.error(f"Error getting file {file_id} for user {user_id}: {str(e)}") + return None + + def delete_file(self, file_id: int, user_id: int) -> bool: + """Delete Excel file record and physical file.""" + try: + # Get file record + excel_file = self.get_file_by_id(file_id, user_id) + if not excel_file: + return False + + # Delete physical file if exists + if os.path.exists(excel_file.file_path): + os.remove(excel_file.file_path) + logger.info(f"Deleted physical file: {excel_file.file_path}") + + # Delete database record + self.db.delete(excel_file) + self.db.commit() + + logger.info(f"Deleted Excel file record with ID {file_id}") + return True + + except Exception as e: + logger.error(f"Error deleting file {file_id}: {str(e)}") + self.db.rollback() + return False + + def update_last_accessed(self, file_id: int, user_id: int) -> bool: + """Update last accessed time for a file.""" + try: + excel_file = self.get_file_by_id(file_id, user_id) + if not excel_file: + return False + + from sqlalchemy.sql import func + excel_file.last_accessed = func.now() + self.db.commit() + + return True + + except Exception as e: + logger.error(f"Error updating last accessed for file {file_id}: {str(e)}") + self.db.rollback() + return False + + def get_file_summary_for_llm(self, user_id: int) -> List[Dict[str, Any]]: + """Get file summary information for LLM context.""" + try: + files = self.db.query(ExcelFile).filter(ExcelFile.user_id == user_id).all() + + summary = [] + for file in files: + file_info = { + 'file_id': file.id, + 'filename': file.original_filename, + 'file_type': file.file_type, + 'sheets': file.get_all_sheets_summary(), + 'upload_time': file.upload_time.isoformat() if file.upload_time else None + } + summary.append(file_info) + + return summary + + except Exception as e: + logger.error(f"Error getting file summary for user {user_id}: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/knowledge_base.py b/backend/th_agenter/services/knowledge_base.py new file mode 100644 index 0000000..774db7f --- /dev/null +++ b/backend/th_agenter/services/knowledge_base.py @@ -0,0 +1,167 @@ +"""Knowledge base service.""" + +import logging +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from ..models.knowledge_base import KnowledgeBase +from ..utils.schemas import KnowledgeBaseCreate, KnowledgeBaseUpdate +from ..core.config import get_settings +from .document_processor import get_document_processor +from ..core.context import UserContext +logger = logging.getLogger(__name__) +settings = get_settings() + + +class KnowledgeBaseService: + """Knowledge base service for managing knowledge bases.""" + + def __init__(self, db: Session): + self.db = db + + def create_knowledge_base(self, kb_data: KnowledgeBaseCreate) -> KnowledgeBase: + """Create a new knowledge base.""" + try: + # Generate collection name for vector database + collection_name = f"kb_{kb_data.name.lower().replace(' ', '_').replace('-', '_')}" + + kb = KnowledgeBase( + name=kb_data.name, + description=kb_data.description, + embedding_model=kb_data.embedding_model, + chunk_size=kb_data.chunk_size, + chunk_overlap=kb_data.chunk_overlap, + vector_db_type=settings.vector_db.type, + collection_name=collection_name + ) + + # Set audit fields + kb.set_audit_fields() + + self.db.add(kb) + self.db.commit() + self.db.refresh(kb) + + logger.info(f"Created knowledge base: {kb.name} (ID: {kb.id})") + return kb + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to create knowledge base: {e}") + raise + + def get_knowledge_base(self, kb_id: int) -> Optional[KnowledgeBase]: + """Get knowledge base by ID.""" + return self.db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first() + + def get_knowledge_base_by_name(self, name: str) -> Optional[KnowledgeBase]: + """Get knowledge base by name.""" + return self.db.query(KnowledgeBase).filter(and_( + KnowledgeBase.name == name, + KnowledgeBase.created_by == UserContext.get_current_user().id + )).first() + + def get_knowledge_bases(self, skip: int = 0, limit: int = 50, active_only: bool = True) -> List[KnowledgeBase]: + """Get list of knowledge bases.""" + + return self.db.query(KnowledgeBase).filter(KnowledgeBase.created_by == UserContext.get_current_user().id) \ + .offset(skip).limit(limit).all() + + + def update_knowledge_base(self, kb_id: int, kb_update: KnowledgeBaseUpdate) -> Optional[KnowledgeBase]: + """Update knowledge base.""" + try: + kb = self.get_knowledge_base(kb_id) + if not kb: + return None + + # Update fields + update_data = kb_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(kb, field, value) + + # Set audit fields + kb.set_audit_fields(is_update=True) + + self.db.commit() + self.db.refresh(kb) + + logger.info(f"Updated knowledge base: {kb.name} (ID: {kb.id})") + return kb + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to update knowledge base {kb_id}: {e}") + raise + + def delete_knowledge_base(self, kb_id: int) -> bool: + """Delete knowledge base.""" + try: + kb = self.get_knowledge_base(kb_id) + if not kb: + return False + + # TODO: Clean up vector database collection + # This should be implemented when vector database service is ready + + self.db.delete(kb) + self.db.commit() + + logger.info(f"Deleted knowledge base: {kb.name} (ID: {kb.id})") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to delete knowledge base {kb_id}: {e}") + raise + + def search_knowledge_bases(self, query: str, skip: int = 0, limit: int = 50) -> List[KnowledgeBase]: + """Search knowledge bases by name or description.""" + search_filter = or_( + KnowledgeBase.name.ilike(f"%{query}%"), + KnowledgeBase.description.ilike(f"%{query}%") + ) + + return ( + self.db.query(KnowledgeBase) + .filter(and_(KnowledgeBase.is_active == True, search_filter)) + .offset(skip) + .limit(limit) + .all() + ) + + async def search(self, kb_id: int, query: str, top_k: int = 5, similarity_threshold: float = 0.7) -> List[Dict[str, Any]]: + """Search in knowledge base using vector similarity.""" + try: + logger.info(f"Searching in knowledge base {kb_id} for: {query}") + + # 使用document_processor进行向量搜索 + search_results = get_document_processor().search_similar_documents( + knowledge_base_id=kb_id, + query=query, + k=top_k + ) + + # 过滤相似度阈值 + filtered_results = [] + for result in search_results: + # 使用已经归一化的相似度分数 + normalized_score = result.get('normalized_score', 0) + + if normalized_score >= similarity_threshold: + filtered_results.append({ + "content": result.get('content', ''), + "source": result.get('source', 'unknown'), + "score": normalized_score, + "metadata": result.get('metadata', {}), + "document_id": result.get('document_id', 'unknown'), + "chunk_id": result.get('chunk_id', 'unknown') + }) + + logger.info(f"Found {len(filtered_results)} relevant documents (threshold: {similarity_threshold})") + return filtered_results + + except Exception as e: + logger.error(f"Search failed for knowledge base {kb_id}: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/knowledge_chat.py b/backend/th_agenter/services/knowledge_chat.py new file mode 100644 index 0000000..3c56acb --- /dev/null +++ b/backend/th_agenter/services/knowledge_chat.py @@ -0,0 +1,387 @@ +"""Knowledge base chat service using LangChain RAG.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from sqlalchemy.orm import Session + +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.runnables import RunnablePassthrough +from langchain_core.output_parsers import StrOutputParser +# Try to import vector stores with exception handling +try: + from langchain_community.vectorstores import Chroma +except ImportError: + Chroma = None + +try: + from langchain_postgres.vectorstores import PGVector +except ImportError: + PGVector = None +from .embedding_factory import EmbeddingFactory + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, MessageResponse +from ..utils.exceptions import ChatServiceError +from ..utils.logger import get_logger +from .conversation import ConversationService +from .document_processor import get_document_processor + +logger = get_logger("knowledge_chat_service") + + +class KnowledgeChatService: + """Knowledge base chat service using LangChain RAG.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + # 获取当前LLM配置 + llm_config = settings.llm.get_current_config() + + # Initialize LangChain ChatOpenAI + self.llm = ChatOpenAI( + model=llm_config["model"], + api_key=llm_config["api_key"], + base_url=llm_config["base_url"], + temperature=llm_config["temperature"], + max_tokens=llm_config["max_tokens"], + streaming=False + ) + + # Streaming LLM for stream responses + self.streaming_llm = ChatOpenAI( + model=llm_config["model"], + api_key=llm_config["api_key"], + base_url=llm_config["base_url"], + temperature=llm_config["temperature"], + max_tokens=llm_config["max_tokens"], + streaming=True + ) + + # Initialize embeddings based on provider + self.embeddings = EmbeddingFactory.create_embeddings() + + logger.info(f"Knowledge Chat Service initialized with model: {self.llm.model_name}") + + def _get_vector_store(self, knowledge_base_id: int) -> Optional[Any]: + """Get vector store for knowledge base.""" + try: + if settings.vector_db.type == "pgvector": + # 使用PGVector + if not PGVector: + logger.error("PGVector module not available. Cannot use pgvector vector store.") + return None + + doc_processor = get_document_processor() + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + vector_store = PGVector( + connection=doc_processor.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + return vector_store + else: + # 兼容Chroma模式 + if not Chroma: + logger.error("Chroma module not available. Cannot use Chroma vector store.") + return None + + import os + kb_vector_path = os.path.join(get_document_processor().vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"Vector store not found for knowledge base {knowledge_base_id}") + return None + + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + return vector_store + + except Exception as e: + logger.error(f"Failed to load vector store for KB {knowledge_base_id}: {str(e)}") + return None + + def _create_rag_chain(self, vector_store, conversation_history: List[Dict[str, str]]): + """Create RAG chain with conversation history.""" + + # Create retriever + retriever = vector_store.as_retriever( + search_type="similarity", + search_kwargs={"k": 5} + ) + + # Create prompt template + system_prompt = """你是一个智能助手,基于提供的上下文信息回答用户问题。 + +上下文信息: +{context} + +请根据上下文信息回答用户的问题。如果上下文信息不足以回答问题,请诚实地说明。 +保持回答准确、有用且简洁。""" + + prompt = ChatPromptTemplate.from_messages([ + ("system", system_prompt), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{question}") + ]) + + # Create chain + def format_docs(docs): + return "\n\n".join(doc.page_content for doc in docs) + + rag_chain = ( + { + "context": retriever | format_docs, + "question": RunnablePassthrough(), + "chat_history": lambda x: conversation_history + } + | prompt + | self.llm + | StrOutputParser() + ) + + return rag_chain, retriever + + def _prepare_conversation_history(self, messages: List) -> List[Dict[str, str]]: + """Prepare conversation history for RAG chain.""" + history = [] + + for msg in messages[:-1]: # Exclude the last message (current user message) + if msg.role == MessageRole.USER: + history.append({"role": "human", "content": msg.content}) + elif msg.role == MessageRole.ASSISTANT: + history.append({"role": "assistant", "content": msg.content}) + + return history + + async def chat_with_knowledge_base( + self, + conversation_id: int, + message: str, + knowledge_base_id: int, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> ChatResponse: + """Chat with knowledge base using RAG.""" + + try: + # Get conversation and validate + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Get vector store + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + raise ChatServiceError(f"Knowledge base {knowledge_base_id} not found or not processed") + + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history + messages = self.conversation_service.get_conversation_messages(conversation_id) + conversation_history = self._prepare_conversation_history(messages) + + # Create RAG chain + rag_chain, retriever = self._create_rag_chain(vector_store, conversation_history) + + # Get relevant documents for context + relevant_docs = retriever.get_relevant_documents(message) + context_documents = [] + + for doc in relevant_docs: + context_documents.append({ + "content": doc.page_content[:500], # Limit content length + "metadata": doc.metadata, + "source": doc.metadata.get("filename", "unknown") + }) + + # Generate response + if stream: + # For streaming, we'll use a different approach + response_content = await self._generate_streaming_response( + rag_chain, message, conversation_id + ) + else: + response_content = await asyncio.to_thread(rag_chain.invoke, message) + + # Save assistant message with context + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=response_content, + role=MessageRole.ASSISTANT, + context_documents=context_documents + ) + + # Create response + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + model_used=self.llm.model_name, + total_tokens=None # TODO: Calculate tokens if needed + ) + + except Exception as e: + logger.error(f"Knowledge base chat failed: {str(e)}") + raise ChatServiceError(f"Knowledge base chat failed: {str(e)}") + + async def _generate_streaming_response( + self, + rag_chain, + message: str, + conversation_id: int + ) -> str: + """Generate streaming response (placeholder for now).""" + # For now, use non-streaming approach + # TODO: Implement proper streaming with RAG chain + return await asyncio.to_thread(rag_chain.invoke, message) + + async def chat_stream_with_knowledge_base( + self, + conversation_id: int, + message: str, + knowledge_base_id: int, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Chat with knowledge base using RAG with streaming response.""" + + try: + + # Get vector store + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + raise ChatServiceError(f"Knowledge base {knowledge_base_id} not found or not processed") + + # Get conversation history + messages = self.conversation_service.get_conversation_messages(conversation_id) + conversation_history = self._prepare_conversation_history(messages) + + # Create RAG chain + rag_chain, retriever = self._create_rag_chain(vector_store, conversation_history) + + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get relevant documents + relevant_docs = retriever.get_relevant_documents(message) + context = "\n\n".join([doc.page_content for doc in relevant_docs]) + + # Create streaming LLM + llm_config = settings.llm.get_current_config() + streaming_llm = ChatOpenAI( + model=llm_config["model"], + temperature=temperature or llm_config["temperature"], + max_tokens=max_tokens or llm_config["max_tokens"], + streaming=True, + api_key=llm_config["api_key"], + base_url=llm_config["base_url"] + ) + + # Create prompt for streaming + prompt = ChatPromptTemplate.from_messages([ + ("system", "你是一个智能助手。请基于以下上下文信息回答用户的问题。如果上下文中没有相关信息,请诚实地说明。\n\n上下文信息:\n{context}"), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{question}") + ]) + + # Prepare chat history for prompt + chat_history_messages = [] + for hist in conversation_history: + if hist["role"] == "human": + chat_history_messages.append(HumanMessage(content=hist["content"])) + elif hist["role"] == "assistant": + chat_history_messages.append(AIMessage(content=hist["content"])) + + # Create streaming chain + streaming_chain = ( + { + "context": lambda x: context, + "chat_history": lambda x: chat_history_messages, + "question": lambda x: x["question"] + } + | prompt + | streaming_llm + | StrOutputParser() + ) + + # Generate streaming response + full_response = "" + async for chunk in streaming_chain.astream({"question": message}): + if chunk: + full_response += chunk + yield chunk + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={ + "knowledge_base_id": knowledge_base_id, + "relevant_docs_count": len(relevant_docs) + } + ) + + except Exception as e: + logger.error(f"Error in knowledge base streaming chat: {str(e)}") + error_message = f"知识库对话出错: {str(e)}" + yield error_message + + # Save error message + self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT + ) + + async def search_knowledge_base( + self, + knowledge_base_id: int, + query: str, + k: int = 5 + ) -> List[Dict[str, Any]]: + """Search knowledge base for relevant documents.""" + + try: + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + return [] + + # Perform similarity search + results = vector_store.similarity_search_with_score(query, k=k) + + formatted_results = [] + for doc, score in results: + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": float(score), + "source": doc.metadata.get("filename", "unknown") + }) + + return formatted_results + + except Exception as e: + logger.error(f"Knowledge base search failed: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/langchain_chat.py b/backend/th_agenter/services/langchain_chat.py new file mode 100644 index 0000000..c04a17f --- /dev/null +++ b/backend/th_agenter/services/langchain_chat.py @@ -0,0 +1,383 @@ +"""LangChain-based chat service.""" + +import json +import asyncio +import os +from typing import AsyncGenerator, Optional, List, Dict, Any +from sqlalchemy.orm import Session + +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.outputs import LLMResult + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, StreamChunk, MessageResponse +from ..utils.exceptions import ChatServiceError, OpenAIError, AuthenticationError, RateLimitError +from ..utils.logger import get_logger +from .conversation import ConversationService + +logger = get_logger("langchain_chat_service") + + +class StreamingCallbackHandler(BaseCallbackHandler): + """Custom callback handler for streaming responses.""" + + def __init__(self): + self.tokens = [] + + def on_llm_new_token(self, token: str, **kwargs) -> None: + """Handle new token from LLM.""" + self.tokens.append(token) + + def get_response(self) -> str: + """Get the complete response.""" + return "".join(self.tokens) + + def clear(self): + """Clear the tokens.""" + self.tokens = [] + + +class LangChainChatService: + """LangChain-based chat service for AI model integration.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + from ..core.llm import create_llm + + # 添加调试日志 + logger.info(f"LLM Provider: {settings.llm.provider}") + + # Initialize LangChain ChatOpenAI + self.llm = create_llm(streaming=False) + + # Streaming LLM for stream responses + self.streaming_llm = create_llm(streaming=True) + + self.streaming_handler = StreamingCallbackHandler() + + logger.info(f"LangChain ChatService initialized with model: {self.llm.model_name}") + + def _prepare_langchain_messages(self, conversation, history: List) -> List: + """Prepare messages for LangChain format.""" + messages = [] + + # Add system message if conversation has system prompt + if hasattr(conversation, 'system_prompt') and conversation.system_prompt: + messages.append(SystemMessage(content=conversation.system_prompt)) + else: + # Default system message + messages.append(SystemMessage( + content="You are a helpful AI assistant. Please provide accurate and helpful responses." + )) + + # Add conversation history + for msg in history[:-1]: # Exclude the last message (current user message) + if msg.role == MessageRole.USER: + messages.append(HumanMessage(content=msg.content)) + elif msg.role == MessageRole.ASSISTANT: + messages.append(AIMessage(content=msg.content)) + + # Add current user message + if history: + last_msg = history[-1] + if last_msg.role == MessageRole.USER: + messages.append(HumanMessage(content=last_msg.content)) + + return messages + + async def chat( + self, + conversation_id: int, + message: str, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> ChatResponse: + """Send a message and get AI response using LangChain.""" + logger.info(f"Processing LangChain chat request for conversation {conversation_id}") + + try: + # Get conversation details + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Add user message to database + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history for context + history = self.conversation_service.get_conversation_history( + conversation_id, limit=20 + ) + + # Prepare messages for LangChain + langchain_messages = self._prepare_langchain_messages(conversation, history) + + # Update LLM parameters if provided + llm_to_use = self.llm + if temperature is not None or max_tokens is not None: + llm_config = settings.llm.get_current_config() + llm_to_use = ChatOpenAI( + model=llm_config["model"], + openai_api_key=llm_config["api_key"], + openai_api_base=llm_config["base_url"], + temperature=temperature if temperature is not None else float(conversation.temperature), + max_tokens=max_tokens if max_tokens is not None else conversation.max_tokens, + streaming=False + ) + + # Call LangChain LLM + response = await llm_to_use.ainvoke(langchain_messages) + + # Extract response content + assistant_content = response.content + + # Add assistant message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=assistant_content, + role=MessageRole.ASSISTANT, + message_metadata={ + "model": llm_to_use.model_name, + "langchain_version": "0.1.0", + "provider": "langchain_openai" + } + ) + + # Update conversation timestamp + self.conversation_service.update_conversation_timestamp(conversation_id) + + logger.info(f"Successfully processed LangChain chat request for conversation {conversation_id}") + + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + total_tokens=None, # LangChain doesn't provide token count by default + model_used=llm_to_use.model_name + ) + + except Exception as e: + logger.error(f"Failed to process LangChain chat request for conversation {conversation_id}: {str(e)}", exc_info=True) + + # Classify error types for better handling + error_type = type(e).__name__ + error_message = self._format_error_message(e) + + # Add error message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT, + message_metadata={ + "error": True, + "error_type": error_type, + "original_error": str(e), + "langchain_error": True + } + ) + + # Re-raise specific exceptions for proper error handling + if "rate limit" in str(e).lower(): + raise RateLimitError(str(e)) + elif "api key" in str(e).lower() or "authentication" in str(e).lower(): + raise AuthenticationError(str(e)) + elif "openai" in str(e).lower(): + raise OpenAIError(str(e)) + + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + total_tokens=0, + model_used=self.llm.model_name + ) + + async def chat_stream( + self, + conversation_id: int, + message: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Send a message and get streaming AI response using LangChain.""" + logger.info(f"Processing LangChain streaming chat request for conversation {conversation_id}") + + try: + # Get conversation details + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Add user message to database + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history for context + history = self.conversation_service.get_conversation_history( + conversation_id, limit=20 + ) + + # Prepare messages for LangChain + langchain_messages = self._prepare_langchain_messages(conversation, history) + + # Update streaming LLM parameters if provided + streaming_llm_to_use = self.streaming_llm + if temperature is not None or max_tokens is not None: + llm_config = settings.llm.get_current_config() + streaming_llm_to_use = ChatOpenAI( + model=llm_config["model"], + openai_api_key=llm_config["api_key"], + openai_api_base=llm_config["base_url"], + temperature=temperature if temperature is not None else float(conversation.temperature), + max_tokens=max_tokens if max_tokens is not None else conversation.max_tokens, + streaming=True + ) + + # Clear previous streaming handler state + self.streaming_handler.clear() + + # Stream response + full_response = "" + async for chunk in streaming_llm_to_use.astream(langchain_messages): + if hasattr(chunk, 'content') and chunk.content: + full_response += chunk.content + yield chunk.content + + # Add complete assistant message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={ + "model": streaming_llm_to_use.model_name, + "langchain_version": "0.1.0", + "provider": "langchain_openai", + "streaming": True + } + ) + + # Update conversation timestamp + self.conversation_service.update_conversation_timestamp(conversation_id) + + logger.info(f"Successfully processed LangChain streaming chat request for conversation {conversation_id}") + + except Exception as e: + logger.error(f"Failed to process LangChain streaming chat request for conversation {conversation_id}: {str(e)}", exc_info=True) + + # Format error message for user + error_message = self._format_error_message(e) + yield error_message + + # Add error message to database + self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT, + message_metadata={ + "error": True, + "error_type": type(e).__name__, + "original_error": str(e), + "langchain_error": True, + "streaming": True + } + ) + + async def get_available_models(self) -> List[str]: + """Get list of available models from LangChain.""" + try: + # LangChain doesn't have a direct method to list models + # Return commonly available OpenAI models + return [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-4", + "gpt-4-turbo-preview", + "gpt-4o", + "gpt-4o-mini" + ] + except Exception as e: + logger.error(f"Failed to get available models: {str(e)}") + return ["gpt-3.5-turbo"] + + def update_model_config( + self, + model: Optional[str] = None, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ): + """Update LLM configuration.""" + from ..core.llm import create_llm + + # 重新创建LLM实例 + self.llm = create_llm( + model=model, + temperature=temperature, + streaming=False + ) + + self.streaming_llm = create_llm( + model=model, + temperature=temperature, + streaming=True + ) + + logger.info(f"Updated LLM configuration: model={model}, temperature={temperature}, max_tokens={max_tokens}") + + def _format_error_message(self, error: Exception) -> str: + """Format error message for user display.""" + error_type = type(error).__name__ + error_str = str(error) + + # Provide user-friendly error messages + if "rate limit" in error_str.lower(): + return "服务器繁忙,请稍后再试。" + elif "api key" in error_str.lower() or "authentication" in error_str.lower(): + return "API认证失败,请检查配置。" + elif "timeout" in error_str.lower(): + return "请求超时,请重试。" + elif "connection" in error_str.lower(): + return "网络连接错误,请检查网络连接。" + elif "model" in error_str.lower() and "not found" in error_str.lower(): + return "指定的模型不可用,请选择其他模型。" + else: + return f"处理请求时发生错误:{error_str}" + + async def _retry_with_backoff(self, func, max_retries: int = 3, base_delay: float = 1.0): + """Retry function with exponential backoff.""" + for attempt in range(max_retries): + try: + return await func() + except Exception as e: + if attempt == max_retries - 1: + raise e + + # Check if error is retryable + if not self._is_retryable_error(e): + raise e + + delay = base_delay * (2 ** attempt) + logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay}s: {str(e)}") + await asyncio.sleep(delay) + + def _is_retryable_error(self, error: Exception) -> bool: + """Check if an error is retryable.""" + error_str = str(error).lower() + retryable_errors = [ + "timeout", + "connection", + "server error", + "internal error", + "rate limit" + ] + return any(err in error_str for err in retryable_errors) \ No newline at end of file diff --git a/backend/th_agenter/services/llm_config_service.py b/backend/th_agenter/services/llm_config_service.py new file mode 100644 index 0000000..0a5b4dd --- /dev/null +++ b/backend/th_agenter/services/llm_config_service.py @@ -0,0 +1,121 @@ +"""LLM配置服务 - 从数据库读取默认配置""" + +from typing import Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import and_ + +from ..models.llm_config import LLMConfig +from ..utils.logger import get_logger +from ..db.database import get_db_session + +logger = get_logger("llm_config_service") + + +class LLMConfigService: + """LLM配置管理服务""" + + def __init__(self, db_session: Optional[Session] = None): + self.db = db_session or get_db_session() + + def get_default_chat_config(self) -> Optional[LLMConfig]: + """获取默认对话模型配置""" + try: + config = self.db.query(LLMConfig).filter( + and_( + LLMConfig.is_default == True, + LLMConfig.is_embedding == False, + LLMConfig.is_active == True + ) + ).first() + + if not config: + logger.warning("未找到默认对话模型配置") + return None + + return config + + except Exception as e: + logger.error(f"获取默认对话模型配置失败: {str(e)}") + return None + + def get_default_embedding_config(self) -> Optional[LLMConfig]: + """获取默认嵌入模型配置""" + try: + config = self.db.query(LLMConfig).filter( + and_( + LLMConfig.is_default == True, + LLMConfig.is_embedding == True, + LLMConfig.is_active == True + ) + ).first() + + if not config: + logger.warning("未找到默认嵌入模型配置") + return None + + return config + + except Exception as e: + logger.error(f"获取默认嵌入模型配置失败: {str(e)}") + return None + + def get_config_by_id(self, config_id: int) -> Optional[LLMConfig]: + """根据ID获取配置""" + try: + return self.db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + except Exception as e: + logger.error(f"获取配置失败: {str(e)}") + return None + + def get_active_configs(self, is_embedding: Optional[bool] = None) -> list: + """获取所有激活的配置""" + try: + query = self.db.query(LLMConfig).filter(LLMConfig.is_active == True) + + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + return query.order_by(LLMConfig.created_at).all() + + except Exception as e: + logger.error(f"获取激活配置失败: {str(e)}") + return [] + + def _get_fallback_chat_config(self) -> Dict[str, Any]: + """获取fallback对话模型配置(从环境变量)""" + from ..core.config import get_settings + settings = get_settings() + return settings.llm.get_current_config() + + def _get_fallback_embedding_config(self) -> Dict[str, Any]: + """获取fallback嵌入模型配置(从环境变量)""" + from ..core.config import get_settings + settings = get_settings() + return settings.embedding.get_current_config() + + def test_config(self, config_id: int, test_message: str = "Hello") -> Dict[str, Any]: + """测试配置连接""" + try: + config = self.get_config_by_id(config_id) + if not config: + return {"success": False, "error": "配置不存在"} + + # 这里可以添加实际的连接测试逻辑 + # 例如发送一个简单的请求来验证配置是否有效 + + return {"success": True, "message": "配置测试成功"} + + except Exception as e: + logger.error(f"测试配置失败: {str(e)}") + return {"success": False, "error": str(e)} + + +# 全局实例 +_llm_config_service = None + +def get_llm_config_service(db_session: Optional[Session] = None) -> LLMConfigService: + """获取LLM配置服务实例""" + global _llm_config_service + if _llm_config_service is None or db_session is not None: + _llm_config_service = LLMConfigService(db_session) + return _llm_config_service \ No newline at end of file diff --git a/backend/th_agenter/services/llm_service.py b/backend/th_agenter/services/llm_service.py new file mode 100644 index 0000000..19b4aad --- /dev/null +++ b/backend/th_agenter/services/llm_service.py @@ -0,0 +1,113 @@ +"""LLM service for workflow execution.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage + +from ..models.llm_config import LLMConfig +from ..utils.logger import get_logger + +logger = get_logger("llm_service") + + +class LLMService: + """LLM服务,用于工作流中的大模型调用""" + + def __init__(self): + pass + + async def chat_completion( + self, + model_config: LLMConfig, + messages: List[Dict[str, str]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> str: + """调用大模型进行对话完成""" + try: + # 创建LangChain ChatOpenAI实例 + llm = ChatOpenAI( + model=model_config.model_name, + api_key=model_config.api_key, + base_url=model_config.base_url, + temperature=temperature or model_config.temperature, + max_tokens=max_tokens or model_config.max_tokens, + streaming=False + ) + + # 转换消息格式 + langchain_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + langchain_messages.append(SystemMessage(content=content)) + elif role == "user": + langchain_messages.append(HumanMessage(content=content)) + elif role == "assistant": + langchain_messages.append(AIMessage(content=content)) + + # 调用LLM + response = await llm.ainvoke(langchain_messages) + + # 返回响应内容 + return response.content + + except Exception as e: + logger.error(f"LLM调用失败: {str(e)}") + raise Exception(f"LLM调用失败: {str(e)}") + + async def chat_completion_stream( + self, + model_config: LLMConfig, + messages: List[Dict[str, str]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """调用大模型进行流式对话完成""" + try: + # 创建LangChain ChatOpenAI实例(流式) + llm = ChatOpenAI( + model=model_config.model_name, + api_key=model_config.api_key, + base_url=model_config.base_url, + temperature=temperature or model_config.temperature, + max_tokens=max_tokens or model_config.max_tokens, + streaming=True + ) + + # 转换消息格式 + langchain_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + langchain_messages.append(SystemMessage(content=content)) + elif role == "user": + langchain_messages.append(HumanMessage(content=content)) + elif role == "assistant": + langchain_messages.append(AIMessage(content=content)) + + # 流式调用LLM + async for chunk in llm.astream(langchain_messages): + if hasattr(chunk, 'content') and chunk.content: + yield chunk.content + + except Exception as e: + logger.error(f"LLM流式调用失败: {str(e)}") + raise Exception(f"LLM流式调用失败: {str(e)}") + + def get_model_info(self, model_config: LLMConfig) -> Dict[str, Any]: + """获取模型信息""" + return { + "id": model_config.id, + "name": model_config.model_name, + "provider": model_config.provider, + "base_url": model_config.base_url, + "temperature": model_config.temperature, + "max_tokens": model_config.max_tokens, + "is_active": model_config.is_active + } \ No newline at end of file diff --git a/backend/th_agenter/services/mcp/__init__.py b/backend/th_agenter/services/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..fe9809f Binary files /dev/null and b/backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/services/mcp/__pycache__/mysql_mcp.cpython-313.pyc b/backend/th_agenter/services/mcp/__pycache__/mysql_mcp.cpython-313.pyc new file mode 100644 index 0000000..6a2803c Binary files /dev/null and b/backend/th_agenter/services/mcp/__pycache__/mysql_mcp.cpython-313.pyc differ diff --git a/backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc b/backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc new file mode 100644 index 0000000..543f8eb Binary files /dev/null and b/backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc differ diff --git a/backend/th_agenter/services/mcp/mysql_mcp.py b/backend/th_agenter/services/mcp/mysql_mcp.py new file mode 100644 index 0000000..b445c96 --- /dev/null +++ b/backend/th_agenter/services/mcp/mysql_mcp.py @@ -0,0 +1,458 @@ +"""MySQL MCP (Model Context Protocol) tool for database operations.""" + +import json +import pymysql +from typing import List, Dict, Any, Optional +from datetime import datetime + +from th_agenter.services.agent.base import BaseTool, ToolParameter, ToolParameterType, ToolResult +from th_agenter.utils.logger import get_logger + +logger = get_logger("mysql_mcp_tool") + + +class MySQLMCPTool(BaseTool): + """MySQL MCP tool for database operations and intelligent querying.""" + + def __init__(self): + super().__init__() + self.connections = {} # 存储用户的数据库连接 + + def get_name(self) -> str: + return "mysql_mcp" + + def get_description(self) -> str: + return "MySQL MCP服务工具,提供数据库连接、表结构查询、SQL执行等功能,支持智能数据问答。" + + def get_parameters(self) -> List[ToolParameter]: + return [ + ToolParameter( + name="operation", + type=ToolParameterType.STRING, + description="操作类型", + required=True, + enum=["connect", "list_tables", "describe_table", "execute_query", "test_connection", "disconnect"] + ), + ToolParameter( + name="connection_config", + type=ToolParameterType.OBJECT, + description="数据库连接配置 {host, port, database, username, password}", + required=False + ), + ToolParameter( + name="user_id", + type=ToolParameterType.STRING, + description="用户ID,用于管理连接", + required=False + ), + ToolParameter( + name="table_name", + type=ToolParameterType.STRING, + description="表名(用于describe_table操作)", + required=False + ), + ToolParameter( + name="sql_query", + type=ToolParameterType.STRING, + description="SQL查询语句(用于execute_query操作)", + required=False + ), + ToolParameter( + name="limit", + type=ToolParameterType.INTEGER, + description="查询结果限制数量,默认100", + required=False, + default=100 + ) + ] + + def _get_tables(self, connection) -> List[Dict[str, Any]]: + """获取数据库表列表""" + cursor = connection.cursor() + try: + cursor.execute(""" + SELECT + table_name, + table_type, + table_schema + FROM information_schema.tables + WHERE table_schema = DATABASE() + ORDER BY table_name; + """) + + tables = [] + for row in cursor.fetchall(): + tables.append({ + "table_name": row[0], + "table_type": row[1], + "table_schema": row[2] + }) + + return tables + finally: + cursor.close() + + def _describe_table(self, connection, table_name: str) -> Dict[str, Any]: + """获取表结构信息""" + cursor = connection.cursor() + try: + # 获取列信息 + cursor.execute(""" + SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale, + column_comment + FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = %s + ORDER BY ordinal_position; + """, (table_name,)) + + columns = [] + for row in cursor.fetchall(): + column_info = { + "column_name": row[0], + "data_type": row[1], + "is_nullable": row[2] == 'YES', + "column_default": row[3], + "character_maximum_length": row[4], + "numeric_precision": row[5], + "numeric_scale": row[6], + "column_comment": row[7] or "" + } + columns.append(column_info) + + # 获取主键信息 + cursor.execute(""" + SELECT column_name + FROM information_schema.key_column_usage + WHERE table_schema = DATABASE() + AND table_name = %s + AND constraint_name = 'PRIMARY' + ORDER BY ordinal_position; + """, (table_name,)) + + primary_keys = [row[0] for row in cursor.fetchall()] + + # 获取外键信息 + cursor.execute(""" + SELECT + column_name, + referenced_table_name, + referenced_column_name + FROM information_schema.key_column_usage + WHERE table_schema = DATABASE() + AND table_name = %s + AND referenced_table_name IS NOT NULL; + """, (table_name,)) + + foreign_keys = [] + for row in cursor.fetchall(): + foreign_keys.append({ + "column_name": row[0], + "referenced_table": row[1], + "referenced_column": row[2] + }) + + # 获取索引信息 + cursor.execute(""" + SELECT + index_name, + column_name, + non_unique + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = %s + ORDER BY index_name, seq_in_index; + """, (table_name,)) + + indexes = [] + for row in cursor.fetchall(): + indexes.append({ + "index_name": row[0], + "column_name": row[1], + "is_unique": row[2] == 0 + }) + + # 获取表注释 + cursor.execute(""" + SELECT table_comment + FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = %s; + """, (table_name,)) + + table_comment = "" + result = cursor.fetchone() + if result: + table_comment = result[0] or "" + + return { + "table_name": table_name, + "columns": columns, + "primary_keys": primary_keys, + "foreign_keys": foreign_keys, + "indexes": indexes, + "table_comment": table_comment + } + + finally: + cursor.close() + + def _execute_query(self, connection, sql_query: str, limit: int = 100) -> Dict[str, Any]: + """执行SQL查询""" + cursor = connection.cursor() + try: + # 添加LIMIT限制(如果查询中没有LIMIT) + if limit and limit > 0 and "LIMIT" not in sql_query.upper(): + sql_query = f"{sql_query.rstrip(';')} LIMIT {limit}" + + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取数据 + rows = cursor.fetchall() + + # 转换为字典列表 + data = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + # 处理特殊数据类型 + if isinstance(value, datetime): + row_dict[columns[i]] = value.isoformat() + else: + row_dict[columns[i]] = value + data.append(row_dict) + return { + "success": True, + "data": data, + "columns": columns, + "row_count": len(data), + "query": sql_query + } + + finally: + cursor.close() + + def _create_connection(self, config: Dict[str, Any]) -> pymysql.Connection: + """创建MySQL数据库连接""" + try: + connection = pymysql.connect( + host=config['host'], + port=int(config.get('port', 3306)), + user=config['username'], + password=config['password'], + database=config['database'], + connect_timeout=10, + charset='utf8mb4' + ) + return connection + except Exception as e: + raise Exception(f"MySQL连接失败: {str(e)}") + + def _test_connection(self, config: Dict[str, Any]) -> Dict[str, Any]: + """测试数据库连接""" + try: + conn = self._create_connection(config) + cursor = conn.cursor() + + # 获取数据库版本信息 + cursor.execute("SELECT VERSION();") + version = cursor.fetchone()[0] + + # 获取数据库引擎信息 + cursor.execute("SHOW ENGINES;") + engines = cursor.fetchall() + has_innodb = any('InnoDB' in str(engine) for engine in engines) + + cursor.close() + conn.close() + + return { + "success": True, + "version": version, + "has_innodb": has_innodb, + "message": "连接测试成功" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "连接测试失败" + } + + + + async def execute(self, **kwargs) -> ToolResult: + """Execute the MySQL MCP tool operation.""" + try: + operation = kwargs.get("operation") + connection_config = kwargs.get("connection_config", {}) + user_id = kwargs.get("user_id") + table_name = kwargs.get("table_name") + sql_query = kwargs.get("sql_query") + limit = kwargs.get("limit", 100) + + logger.info(f"执行MySQL MCP操作: {operation}") + if operation == "test_connection": + if not connection_config: + return ToolResult( + success=False, + error="缺少连接配置参数" + ) + + result = self._test_connection(connection_config) + return ToolResult( + success=result["success"], + result=result, + error=result.get("error") + ) + elif operation == "connect": + if not connection_config: + return ToolResult( + success=False, + error="缺少connection_config参数" + ) + + if not user_id: + return ToolResult( + success=False, + error="缺少user_id参数" + ) + + try: + # 建立MySQL连接 + connection = pymysql.connect( + host=connection_config["host"], + port=int(connection_config["port"]), + user=connection_config["username"], + password=connection_config["password"], + database=connection_config["database"], + charset='utf8mb4', + cursorclass=pymysql.cursors.Cursor + ) + + # 存储连接 + self.connections[user_id] = { + "connection": connection, + "config": connection_config, + "connected_at": datetime.now().isoformat() + } + + # 获取表列表 + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "message": "数据库连接成功", + "database": connection_config["database"], + "tables": tables, + "table_count": len(tables) + } + ) + except Exception as e: + return ToolResult( + success=False, + error=f"连接失败: {str(e)}" + ) + + elif operation == "list_tables": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + connection = self.connections[user_id]["connection"] + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "tables": tables, + "table_count": len(tables) + } + ) + + elif operation == "describe_table": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not table_name: + return ToolResult( + success=False, + error="缺少table_name参数" + ) + + connection = self.connections[user_id]["connection"] + table_info = self._describe_table(connection, table_name) + + return ToolResult( + success=True, + result=table_info + ) + + elif operation == "execute_query": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not sql_query: + return ToolResult( + success=False, + error="缺少sql_query参数" + ) + + connection = self.connections[user_id]["connection"] + query_result = self._execute_query(connection, sql_query, limit) + + return ToolResult( + success=True, + result=query_result + ) + + elif operation == "disconnect": + if user_id and user_id in self.connections: + try: + self.connections[user_id]["connection"].close() + del self.connections[user_id] + return ToolResult( + success=True, + result={"message": "数据库连接已断开"} + ) + except Exception as e: + return ToolResult( + success=False, + error=f"断开连接失败: {str(e)}" + ) + else: + return ToolResult( + success=True, + result={"message": "用户未连接数据库"} + ) + + else: + return ToolResult( + success=False, + result=f"不支持的操作类型: {operation}", + ) + + except Exception as e: + logger.error(f"MySQL MCP工具执行失败: {str(e)}", exc_info=True) + return ToolResult( + success=False, + error=f"工具执行失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/services/mcp/postgresql_mcp.py b/backend/th_agenter/services/mcp/postgresql_mcp.py new file mode 100644 index 0000000..7cf56c8 --- /dev/null +++ b/backend/th_agenter/services/mcp/postgresql_mcp.py @@ -0,0 +1,389 @@ +"""PostgreSQL MCP (Model Context Protocol) tool for database operations.""" + +import json +import psycopg2 +from typing import List, Dict, Any, Optional +from datetime import datetime + +from th_agenter.services.agent.base import BaseTool, ToolParameter, ToolParameterType, ToolResult +from th_agenter.utils.logger import get_logger + +logger = get_logger("postgresql_mcp_tool") + + +class PostgreSQLMCPTool(BaseTool): + """PostgreSQL MCP tool for database operations and intelligent querying.""" + + def __init__(self): + super().__init__() + self.connections = {} # 存储用户的数据库连接 + + def get_name(self) -> str: + return "postgresql_mcp" + + def get_description(self) -> str: + return "PostgreSQL MCP服务工具,提供数据库连接、表结构查询、SQL执行等功能,支持智能数据问答。" + + def get_parameters(self) -> List[ToolParameter]: + return [ + ToolParameter( + name="operation", + type=ToolParameterType.STRING, + description="操作类型", + required=True, + enum=["connect", "list_tables", "describe_table", "execute_query", "test_connection", "disconnect"] + ), + ToolParameter( + name="connection_config", + type=ToolParameterType.OBJECT, + description="数据库连接配置 {host, port, database, username, password}", + required=False + ), + ToolParameter( + name="user_id", + type=ToolParameterType.STRING, + description="用户ID,用于管理连接", + required=False + ), + ToolParameter( + name="table_name", + type=ToolParameterType.STRING, + description="表名(用于describe_table操作)", + required=False + ), + ToolParameter( + name="sql_query", + type=ToolParameterType.STRING, + description="SQL查询语句(用于execute_query操作)", + required=False + ), + ToolParameter( + name="limit", + type=ToolParameterType.INTEGER, + description="查询结果限制数量,默认100", + required=False, + default=100 + ) + ] + + def _create_connection(self, config: Dict[str, Any]) -> psycopg2.extensions.connection: + """创建PostgreSQL数据库连接""" + try: + connection = psycopg2.connect( + host=config['host'], + port=int(config.get('port', 5432)), + user=config['username'], + password=config['password'], + database=config['database'], + connect_timeout=10 + ) + return connection + except Exception as e: + raise Exception(f"PostgreSQL连接失败: {str(e)}") + + def _test_connection(self, config: Dict[str, Any]) -> Dict[str, Any]: + """测试数据库连接""" + try: + conn = self._create_connection(config) + cursor = conn.cursor() + + # 获取数据库版本信息 + cursor.execute("SELECT version();") + version = cursor.fetchone()[0] + + # 检查pgvector扩展 + cursor.execute("SELECT * FROM pg_extension WHERE extname = 'vector';") + has_vector = bool(cursor.fetchall()) + + cursor.close() + conn.close() + + return { + "success": True, + "version": version, + "has_pgvector": has_vector, + "message": "连接测试成功" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "连接测试失败" + } + + def _get_tables(self, connection) -> List[Dict[str, Any]]: + """获取数据库表列表""" + cursor = connection.cursor() + try: + cursor.execute(""" + SELECT + table_name, + table_type, + table_schema + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name; + """) + + tables = [] + for row in cursor.fetchall(): + tables.append({ + "table_name": row[0], + "table_type": row[1], + "table_schema": row[2] + }) + + return tables + finally: + cursor.close() + + def _describe_table(self, connection, table_name: str) -> Dict[str, Any]: + """获取表结构信息""" + cursor = connection.cursor() + try: + # 获取列信息 + cursor.execute(""" + SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale + FROM information_schema.columns + WHERE table_name = %s AND table_schema = 'public' + ORDER BY ordinal_position; + """, (table_name,)) + + columns = [] + for row in cursor.fetchall(): + columns.append({ + "column_name": row[0], + "data_type": row[1], + "is_nullable": row[2], + "column_default": row[3], + "character_maximum_length": row[4], + "numeric_precision": row[5], + "numeric_scale": row[6] + }) + + # 获取主键信息 + cursor.execute(""" + SELECT column_name + FROM information_schema.key_column_usage + WHERE table_name = %s AND table_schema = 'public' + AND constraint_name IN ( + SELECT constraint_name + FROM information_schema.table_constraints + WHERE table_name = %s AND constraint_type = 'PRIMARY KEY' + ); + """, (table_name, table_name)) + + primary_keys = [row[0] for row in cursor.fetchall()] + + # 获取表行数 + cursor.execute(f"SELECT COUNT(*) FROM {table_name};") + row_count = cursor.fetchone()[0] + + return { + "table_name": table_name, + "columns": columns, + "primary_keys": primary_keys, + "row_count": row_count + } + finally: + cursor.close() + + def _execute_query(self, connection, sql_query: str, limit: int = 100) -> Dict[str, Any]: + """执行SQL查询""" + cursor = connection.cursor() + try: + # 添加LIMIT限制(如果查询中没有) + if limit and "LIMIT" not in sql_query.upper(): + sql_query = f"{sql_query.rstrip(';')} LIMIT {limit};" + + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取结果 + if cursor.description: # SELECT查询 + rows = cursor.fetchall() + data = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + # 处理特殊数据类型 + if isinstance(value, datetime): + row_dict[columns[i]] = value.isoformat() + else: + row_dict[columns[i]] = value + data.append(row_dict) + + return { + "success": True, + "data": data, + "columns": columns, + "row_count": len(data), + "query": sql_query + } + else: # INSERT/UPDATE/DELETE查询 + affected_rows = cursor.rowcount + return { + "success": True, + "affected_rows": affected_rows, + "query": sql_query, + "message": f"查询执行成功,影响 {affected_rows} 行" + } + finally: + cursor.close() + + async def execute(self, operation: str, connection_config: Optional[Dict[str, Any]] = None, + user_id: Optional[str] = None, table_name: Optional[str] = None, + sql_query: Optional[str] = None, limit: int = 100) -> ToolResult: + """执行PostgreSQL MCP操作""" + try: + logger.info(f"执行PostgreSQL MCP操作: {operation}") + + if operation == "test_connection": + if not connection_config: + return ToolResult( + success=False, + error="缺少连接配置参数" + ) + + result = self._test_connection(connection_config) + return ToolResult( + success=result["success"], + result=result, + error=result.get("error") + ) + + elif operation == "connect": + if not connection_config or not user_id: + return ToolResult( + success=False, + error="缺少连接配置或用户ID参数" + ) + + try: + connection = self._create_connection(connection_config) + self.connections[user_id] = { + "connection": connection, + "config": connection_config, + "connected_at": datetime.now().isoformat() + } + + # 获取表列表 + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "message": "数据库连接成功", + "database": connection_config["database"], + "tables": tables, + "table_count": len(tables) + } + ) + except Exception as e: + return ToolResult( + success=False, + error=f"连接失败: {str(e)}" + ) + + elif operation == "list_tables": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + connection = self.connections[user_id]["connection"] + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "tables": tables, + "table_count": len(tables) + } + ) + + elif operation == "describe_table": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not table_name: + return ToolResult( + success=False, + error="缺少table_name参数" + ) + + connection = self.connections[user_id]["connection"] + table_info = self._describe_table(connection, table_name) + + return ToolResult( + success=True, + result=table_info + ) + + elif operation == "execute_query": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not sql_query: + return ToolResult( + success=False, + error="缺少sql_query参数" + ) + + connection = self.connections[user_id]["connection"] + query_result = self._execute_query(connection, sql_query, limit) + + return ToolResult( + success=True, + result=query_result + ) + + elif operation == "disconnect": + if user_id and user_id in self.connections: + try: + self.connections[user_id]["connection"].close() + del self.connections[user_id] + return ToolResult( + success=True, + result={"message": "数据库连接已断开"} + ) + except Exception as e: + return ToolResult( + success=False, + error=f"断开连接失败: {str(e)}" + ) + else: + return ToolResult( + success=True, + result={"message": "用户未连接数据库"} + ) + + else: + return ToolResult( + success=False, + error=f"不支持的操作类型: {operation}" + ) + + except Exception as e: + logger.error(f"PostgreSQL MCP工具执行失败: {str(e)}", exc_info=True) + return ToolResult( + success=False, + error=f"工具执行失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/services/mysql_tool_manager.py b/backend/th_agenter/services/mysql_tool_manager.py new file mode 100644 index 0000000..21b7977 --- /dev/null +++ b/backend/th_agenter/services/mysql_tool_manager.py @@ -0,0 +1,51 @@ +"""MySQL MCP工具全局管理器""" + +from typing import Optional +from ..utils.logger import get_logger + +logger = get_logger("mysql_tool_manager") + +# Try to import MySQL MCP tool +try: + from th_agenter.services.mcp.mysql_mcp import MySQLMCPTool + MYSQL_TOOL_AVAILABLE = True +except ImportError as e: + logger.warning(f"MySQL MCP tool not available: {str(e)}. MySQL functionality will be disabled.") + MySQLMCPTool = None + MYSQL_TOOL_AVAILABLE = False + + +class MySQLToolManager: + """MySQL工具全局单例管理器""" + + _instance: Optional['MySQLToolManager'] = None + _mysql_tool: Optional[MySQLMCPTool] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @property + def mysql_tool(self) -> Optional[MySQLMCPTool]: + """获取MySQL工具实例""" + if not MYSQL_TOOL_AVAILABLE: + return None + + if self._mysql_tool is None: + self._mysql_tool = MySQLMCPTool() + logger.info("创建全局MySQL工具实例") + return self._mysql_tool + + def get_tool(self) -> Optional[MySQLMCPTool]: + """获取MySQL工具实例(别名方法)""" + return self.mysql_tool + + +# 全局实例 +mysql_tool_manager = MySQLToolManager() + + +def get_mysql_tool() -> MySQLMCPTool: + """获取全局MySQL工具实例""" + return mysql_tool_manager.get_tool() \ No newline at end of file diff --git a/backend/th_agenter/services/postgresql_tool_manager.py b/backend/th_agenter/services/postgresql_tool_manager.py new file mode 100644 index 0000000..feccbe0 --- /dev/null +++ b/backend/th_agenter/services/postgresql_tool_manager.py @@ -0,0 +1,51 @@ +"""PostgreSQL MCP工具全局管理器""" + +from typing import Optional +from ..utils.logger import get_logger + +logger = get_logger("postgresql_tool_manager") + +# Try to import PostgreSQL MCP tool +try: + from th_agenter.services.mcp.postgresql_mcp import PostgreSQLMCPTool + POSTGRESQL_TOOL_AVAILABLE = True +except ImportError as e: + logger.warning(f"PostgreSQL MCP tool not available: {str(e)}. PostgreSQL functionality will be disabled.") + PostgreSQLMCPTool = None + POSTGRESQL_TOOL_AVAILABLE = False + + +class PostgreSQLToolManager: + """PostgreSQL工具全局单例管理器""" + + _instance: Optional['PostgreSQLToolManager'] = None + _postgresql_tool: Optional[PostgreSQLMCPTool] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @property + def postgresql_tool(self) -> Optional[PostgreSQLMCPTool]: + """获取PostgreSQL工具实例""" + if not POSTGRESQL_TOOL_AVAILABLE: + return None + + if self._postgresql_tool is None: + self._postgresql_tool = PostgreSQLMCPTool() + logger.info("创建全局PostgreSQL工具实例") + return self._postgresql_tool + + def get_tool(self) -> Optional[PostgreSQLMCPTool]: + """获取PostgreSQL工具实例(别名方法)""" + return self.postgresql_tool + + +# 全局实例 +postgresql_tool_manager = PostgreSQLToolManager() + + +def get_postgresql_tool() -> PostgreSQLMCPTool: + """获取全局PostgreSQL工具实例""" + return postgresql_tool_manager.get_tool() \ No newline at end of file diff --git a/backend/th_agenter/services/smart_db_workflow.py b/backend/th_agenter/services/smart_db_workflow.py new file mode 100644 index 0000000..8975b3a --- /dev/null +++ b/backend/th_agenter/services/smart_db_workflow.py @@ -0,0 +1,879 @@ +from typing import Dict, Any, List, Optional +import logging +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor +from langchain_openai import ChatOpenAI +from th_agenter.core.context import UserContext +from .smart_query import DatabaseQueryService +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool +from .table_metadata_service import TableMetadataService +from ..core.config import get_settings + +# 配置日志 +logger = logging.getLogger(__name__) + +class SmartWorkflowError(Exception): + """智能工作流自定义异常""" + pass + +class DatabaseConnectionError(SmartWorkflowError): + """数据库连接异常""" + pass + +class TableSchemaError(SmartWorkflowError): + """表结构获取异常""" + pass + +class SQLGenerationError(SmartWorkflowError): + """SQL生成异常""" + pass + +class QueryExecutionError(SmartWorkflowError): + """查询执行异常""" + pass + + +class SmartDatabaseWorkflowManager: + """ + 智能数据库工作流管理器 + 负责协调数据库连接、表元数据获取、SQL生成、查询执行和AI总结的完整流程 + """ + + def __init__(self, db=None): + self.executor = ThreadPoolExecutor(max_workers=4) + self.database_service = DatabaseQueryService() + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + self.db = db + self.table_metadata_service = TableMetadataService(db) if db else None + + from ..core.llm import create_llm + self.llm = create_llm() + + def _get_database_tool(self, db_type: str): + """根据数据库类型获取对应的数据库工具""" + if db_type.lower() == 'postgresql': + return self.postgresql_tool + elif db_type.lower() == 'mysql': + return self.mysql_tool + else: + raise ValueError(f"不支持的数据库类型: {db_type}") + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + + def _convert_query_result_to_table_data(self, query_result: Dict[str, Any]) -> Dict[str, Any]: + """ + 将数据库查询结果转换为前端表格数据格式 + 参考Excel处理方式,以表格形式返回结果 + """ + try: + data = query_result.get('data', []) + columns = query_result.get('columns', []) + row_count = query_result.get('row_count', 0) + + if not data or not columns: + return { + 'result_type': 'table', + 'columns': [], + 'data': [], + 'total': 0, + 'message': '查询未返回数据' + } + + # 构建列定义 + table_columns = [] + for i, col_name in enumerate(columns): + table_columns.append({ + 'prop': f'col_{i}', + 'label': str(col_name), + 'width': 'auto' + }) + + # 转换数据行 + table_data = [] + for row_index, row in enumerate(data): + row_data = {'_index': str(row_index)} + # 处理字典格式的行数据 + if isinstance(row, dict): + for i, col_name in enumerate(columns): + col_prop = f'col_{i}' + value = row.get(col_name) + # 处理None值和特殊值 + if value is None: + row_data[col_prop] = '' + elif isinstance(value, (int, float, str, bool)): + row_data[col_prop] = str(value) + else: + row_data[col_prop] = str(value) + else: + # 处理列表格式的行数据(兼容性处理) + for i, value in enumerate(row): + col_prop = f'col_{i}' + # 处理None值和特殊值 + if value is None: + row_data[col_prop] = '' + elif isinstance(value, (int, float, str, bool)): + row_data[col_prop] = str(value) + else: + row_data[col_prop] = str(value) + + table_data.append(row_data) + + return { + 'result_type': 'table_data', + 'columns': table_columns, + 'data': table_data, + 'total': row_count, + 'message': f'查询成功,共返回 {row_count} 条记录' + } + + except Exception as e: + logger.error(f"转换查询结果异常: {str(e)}") + return { + 'result_type': 'error', + 'columns': [], + 'data': [], + 'total': 0, + 'message': f'结果转换失败: {str(e)}' + } + + async def process_database_query_stream( + self, + user_query: str, + user_id: int, + database_config_id: int + ): + """ + 流式处理数据库智能问数查询的主要工作流(基于保存的表元数据) + 实时推送每个工作流步骤 + + 新流程: + 1. 根据database_config_id获取数据库配置并创建连接 + 2. 从系统数据库读取表元数据(只包含启用问答的表) + 3. 根据表元数据生成SQL + 4. 执行SQL查询 + 5. 查询数据后处理成表格形式 + 6. 生成数据总结 + 7. 返回结果 + + Args: + user_query: 用户问题 + user_id: 用户ID + database_config_id: 数据库配置ID + + Yields: + 包含工作流步骤或最终结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行流式数据库查询工作流 - 用户ID: {user_id}, 数据库配置ID: {database_config_id}, 查询: {user_query[:50]}...") + + # 步骤1: 根据database_config_id获取数据库配置并创建连接 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'database_connection', + 'status': 'running', + 'message': '正在建立数据库连接...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 获取数据库配置并建立连接 + connection_result = await self._connect_database(user_id, database_config_id) + if not connection_result['success']: + raise DatabaseConnectionError(connection_result['message']) + + step_data.update({ + 'status': 'completed', + 'message': '数据库连接成功', + 'details': {'database': connection_result.get('database_name', 'Unknown')} + }) + yield step_data + + workflow_steps.append({ + 'step': 'database_connection', + 'status': 'completed', + 'message': '数据库连接成功' + }) + + except Exception as e: + error_msg = f'数据库连接失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'database_connection', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤2: 从系统数据库读取表元数据(只包含启用问答的表) + try: + step_data = { + 'type': 'workflow_step', + 'step': 'table_metadata', + 'status': 'running', + 'message': '正在从系统数据库读取表元数据...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 从系统数据库读取已保存的表元数据(只包含启用问答的表) + tables_info = await self._get_saved_tables_metadata(user_id, database_config_id) + + step_data.update({ + 'status': 'completed', + 'message': f'成功读取 {len(tables_info)} 个启用问答的表元数据', + 'details': {'table_count': len(tables_info), 'tables': list(tables_info.keys())} + }) + yield step_data + + workflow_steps.append({ + 'step': 'table_metadata', + 'status': 'completed', + 'message': f'成功读取表元数据' + }) + + except Exception as e: + error_msg = f'获取表元数据失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'table_metadata', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤3: 根据表元数据生成SQL + try: + step_data = { + 'type': 'workflow_step', + 'step': 'sql_generation', + 'status': 'running', + 'message': '正在根据表元数据生成SQL查询...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 根据表元数据选择相关表并生成SQL + target_tables, target_schemas = await self._select_target_table(user_query, tables_info) + step_data = { + 'type': 'workflow_step', + 'step': 'table_selected', + 'status': 'completed', + 'message': f'已经智能选择了相关表: {", ".join(target_tables)}', + 'timestamp': datetime.now().isoformat() + } + + yield step_data + workflow_steps.append({ + 'step': 'table_metadata', + 'status': 'completed', + 'message': f'已经智能选择了相关表: {", ".join(target_tables)}', + }) + sql_query = await self._generate_sql_query(user_query, target_tables, target_schemas) + + step_data.update({ + 'status': 'completed', + 'message': 'SQL查询生成成功', + 'details': { + 'target_tables': target_tables, + 'generated_sql': sql_query[:100] + '...' if len(sql_query) > 100 else sql_query + } + }) + yield step_data + + workflow_steps.append({ + 'step': 'sql_generation', + 'status': 'completed', + 'message': 'SQL语句生成成功' + }) + + except Exception as e: + error_msg = f'SQL生成失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'sql_generation', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤4: 执行SQL查询 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'query_execution', + 'status': 'running', + 'message': '正在执行SQL查询...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + query_result = await self._execute_database_query(user_id, sql_query, database_config_id) + + step_data.update({ + 'status': 'completed', + 'message': f'查询执行成功,返回 {query_result.get("row_count", 0)} 条记录', + 'details': {'row_count': query_result.get('row_count', 0)} + }) + yield step_data + + workflow_steps.append({ + 'step': 'query_execution', + 'status': 'completed', + 'message': '查询执行成功' + }) + + except Exception as e: + error_msg = f'查询执行失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'query_execution', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤5: 查询数据后处理成表格形式(在步骤6中完成) + # 步骤6: 生成数据总结 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'ai_summary', + 'status': 'running', + 'message': '正在生成查询结果总结...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + summary = await self._generate_database_summary(user_query, query_result, ', '.join(target_tables)) + + step_data.update({ + 'status': 'completed', + 'message': '总结生成完成', + 'details': { + 'tables_analyzed': target_tables, + 'summary_length': len(summary) + } + }) + yield step_data + + workflow_steps.append({ + 'step': 'ai_summary', + 'status': 'completed', + 'message': '总结生成完成' + }) + + except Exception as e: + logger.warning(f'生成总结失败: {str(e)}') + summary = '查询执行完成,但生成总结时出现问题。' + + workflow_steps.append({ + 'step': 'ai_summary', + 'status': 'warning', + 'message': '总结生成失败,但查询成功' + }) + + # 步骤7: 返回最终结果,且结果参考excel的处理方式,尽量以表格形式返回 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'result_formatting', + 'status': 'running', + 'message': '正在格式化查询结果...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 转换为表格格式 + table_data = self._convert_query_result_to_table_data(query_result) + + step_data.update({ + 'status': 'completed', + 'message': '结果格式化完成' + }) + yield step_data + + workflow_steps.append({ + 'step': 'result_formatting', + 'status': 'completed', + 'message': '结果格式化完成' + }) + + # 返回最终结果 + final_result = { + 'type': 'final_result', + 'success': True, + 'data': { + **table_data, + 'generated_sql': sql_query, + 'summary': summary, + 'table_name': target_tables, + 'query_result': query_result, + 'metadata_source': 'saved_database' # 标记元数据来源 + }, + 'workflow_steps': workflow_steps, + 'timestamp': datetime.now().isoformat() + } + + yield final_result + logger.info(f"数据库查询工作流完成 - 用户ID: {user_id}") + + except Exception as e: + error_msg = f'结果格式化失败: {str(e)}' + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + except Exception as e: + logger.error(f"数据库查询工作流异常: {str(e)}", exc_info=True) + yield { + 'type': 'error', + 'message': f'系统异常: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def _connect_database(self, user_id: int, database_config_id: int) -> Dict[str, Any]: + """连接数据库(判断用户现有连接)""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + return {'success': False, 'message': '数据库配置不存在'} + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + return {'success': False, 'message': str(e)} + + # 测试连接(如果已经有连接则直接复用) + connection_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': config_service._decrypt_password(config.password) + } + + try: + connection = db_tool._test_connection(connection_config) + if connection['success'] == True: + return { + 'success': True, + 'database_name': config.database, + 'db_type': config.db_type, + 'message': '连接成功' + } + else: + return { + 'success': False, + 'database_name': config.database, + 'db_type': config.db_type, + 'message': '连接失败' + } + except Exception as e: + return { + 'success': False, + 'message': f'连接失败: {str(e)}' + } + + except Exception as e: + logger.error(f"数据库连接异常: {str(e)}") + return {'success': False, 'message': f'连接异常: {str(e)}'} + + async def _get_saved_tables_metadata(self, user_id: int, database_config_id: int) -> Dict[str, Dict[str, Any]]: + """从系统数据库中读取已保存的表元数据""" + try: + if not self.table_metadata_service: + raise TableSchemaError("表元数据服务未初始化") + + # 从数据库中获取表元数据 + saved_metadata = self.table_metadata_service.get_user_table_metadata( + user_id, database_config_id + ) + + if not saved_metadata: + raise TableSchemaError(f"未找到数据库配置ID {database_config_id} 的表元数据,请先在数据库管理页面收集表元数据") + + # 转换为所需格式 + tables_metadata = {} + for meta in saved_metadata: + # 只处理启用问答的表 + if meta.is_enabled_for_qa: + tables_metadata[meta.table_name] = { + 'table_name': meta.table_name, + 'columns': meta.columns_info or [], + 'primary_keys': meta.primary_keys or [], + 'row_count': meta.row_count or 0, + 'table_comment': meta.table_comment or '', + 'qa_description': meta.qa_description or '', + 'business_context': meta.business_context or '', + 'from_saved_metadata': True # 标记来源 + } + + if not tables_metadata: + raise TableSchemaError("没有启用问答的表,请在数据库管理页面启用相关表的问答功能") + + logger.info(f"从系统数据库读取表元数据成功,共 {len(tables_metadata)} 个启用问答的表") + return tables_metadata + + except Exception as e: + logger.error(f"读取保存的表元数据异常: {str(e)}") + raise TableSchemaError(f'读取表元数据失败: {str(e)}') + + async def _get_table_schema(self, user_id: int, table_name: str, database_config_id: int) -> Dict[str, Any]: + """获取指定表结构""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + raise TableSchemaError('数据库配置不存在') + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + raise TableSchemaError(str(e)) + + # 使用对应的数据库工具获取表结构 + schema_result = await db_tool.describe_table(table_name) + + if schema_result.get('success'): + return schema_result.get('schema', {}) + else: + raise TableSchemaError(schema_result.get('error', '获取表结构失败')) + + except Exception as e: + logger.error(f"获取表结构异常: {str(e)}") + raise TableSchemaError(f'获取表结构失败: {str(e)}') + + async def _select_target_table(self, user_query: str, tables_info: Dict[str, Dict]) -> tuple[List[str], List[Dict]]: + """根据用户查询选择相关的表,支持返回多个表""" + try: + if len(tables_info) == 1: + # 只有一个表,直接返回 + table_name = list(tables_info.keys())[0] + return [table_name], [tables_info[table_name]] + + # 多个表时,使用LLM选择相关的表 + tables_summary = [] + for table_name, schema in tables_info.items(): + columns = schema.get('columns', []) + column_names = [col.get('column_name', col.get('name', '')) for col in columns] + qa_desc = schema.get('qa_description', '') + business_ctx = schema.get('business_context', '') + tables_summary.append(f"表名: {table_name}\n字段: {', '.join(column_names[:10])}\n表描述: {qa_desc}\n业务上下文: {business_ctx}") + + prompt = f""" + 用户查询: {user_query} + + 可用的表: + {chr(10).join(tables_summary)} + + 请根据用户查询选择相关的表,可以选择多个表。分析表之间可能的关联关系,返回所有相关的表名,用逗号分隔。 + 可以通过qa_description(表描述),business_context(表的业务上下文),以及column_names几个字段判断要使用哪些表。 + 注意:只返回表名列表,后面不要跟其他的内容。 + 例如直接输出: table1,table2,table3 + """ + + response = await self.llm.ainvoke(prompt) + selected_tables = [t.strip() for t in response.content.strip().split(',')] + + # 验证选择的表是否存在 + valid_tables = [] + valid_schemas = [] + for table in selected_tables: + if table in tables_info: + valid_tables.append(table) + valid_schemas.append(tables_info[table]) + else: + logger.warning(f"LLM选择的表 {table} 不存在") + + if valid_tables: + return valid_tables, valid_schemas + else: + # 如果没有有效的表,选择第一个表 + table_name = list(tables_info.keys())[0] + logger.warning(f"没有找到有效的表,使用默认表 {table_name}") + return [table_name], [tables_info[table_name]] + + except Exception as e: + logger.error(f"选择目标表异常: {str(e)}") + # 出现异常时选择第一个表 + table_name = list(tables_info.keys())[0] + return [table_name], [tables_info[table_name]] + + async def _generate_sql_query(self, user_query: str, table_names: List[str], table_schemas: List[Dict]) -> str: + """生成SQL语句,支持多表关联查询""" + try: + # 构建所有表的结构信息 + tables_info = [] + for table_name, schema in zip(table_names, table_schemas): + columns_info = [] + for col in schema.get('columns', []): + col_info = f"{col['column_name']} ({col['data_type']})" + columns_info.append(col_info) + + table_info = f"表名: {table_name}\n" + table_info += f"表描述: {schema.get('qa_description', '')}\n" + table_info += f"业务上下文: {schema.get('business_context', '')}\n" + table_info += "字段信息:\n" + "\n".join(columns_info) + tables_info.append(table_info) + + schema_text = "\n\n".join(tables_info) + + prompt = f""" + 基于以下表结构,将自然语言查询转换为SQL语句。如果需要关联多个表,请分析表之间的关系,使用合适的JOIN语法: + + {schema_text} + + 用户查询: {user_query} + + 请生成对应的SQL查询语句,要求: + 1. 只返回SQL语句,不要包含其他解释 + 2. 如果查询涉及多个表,需要正确处理表之间的关联关系 + 3. 使用合适的JOIN类型(INNER JOIN、LEFT JOIN等) + 4. 确保SELECT的字段来源明确,必要时使用表名前缀 + """ + + # 使用LLM生成SQL + response = await self.llm.ainvoke(prompt) + sql_query = response.content.strip() + + # 清理SQL语句 + if sql_query.startswith('```sql'): + sql_query = sql_query[6:] + if sql_query.endswith('```'): + sql_query = sql_query[:-3] + + sql_query = sql_query.strip() + + logger.info(f"生成的SQL查询: {sql_query}") + return sql_query + + except Exception as e: + logger.error(f"SQL生成异常: {str(e)}") + raise SQLGenerationError(f'SQL生成失败: {str(e)}') + + async def _execute_database_query(self, user_id: int, sql_query: str, database_config_id: int) -> Dict[str, Any]: + """执行SQL语句""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + raise QueryExecutionError('数据库配置不存在') + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + raise QueryExecutionError(str(e)) + + # 使用对应的数据库工具执行查询 + if str(user_id) in db_tool.connections: + query_result = db_tool._execute_query(db_tool.connections[str(user_id)]['connection'], sql_query) + else: + raise QueryExecutionError('请重新进行数据库连接') + + if query_result.get('success'): + data = query_result.get('data', []) + return { + 'success': True, + 'data': data, + 'row_count': len(data), + 'columns': query_result.get('columns', []), + 'sql_query': sql_query + } + else: + raise QueryExecutionError(query_result.get('error', '查询执行失败')) + + except Exception as e: + logger.error(f"查询执行异常: {str(e)}") + raise QueryExecutionError(f'查询执行失败: {str(e)}') + + async def _generate_database_summary(self, user_query: str, query_result: Dict, tables_str: str) -> str: + """生成AI总结,支持多表查询结果""" + try: + data = query_result.get('data', []) + row_count = query_result.get('row_count', 0) + columns = query_result.get('columns', []) + sql_query = query_result.get('sql_query', '') + + # 构建总结提示词 + prompt = f""" +用户查询: {user_query} +涉及的表: {tables_str} +查询结果: 共 {row_count} 条记录 +查询的字段: {', '.join(columns)} +执行的SQL: {sql_query} + +前几条数据示例: +{str(data[:3]) if data else '无数据'} + +请基于以上信息,用中文生成一个简洁的查询结果总结,包括: +1. 查询涉及的表及其关系 +2. 查询的主要发现和数据特征 +3. 如果有关联查询,说明关联的结果特点 +4. 最后对用户的问题进行回答 + +总结要求: +1. 语言简洁明了 +2. 重点突出查询结果 +3. 如果是多表查询,需要说明表之间的关系 +4. 总结不超过300字 +""" + + # 使用LLM生成总结 + response = await self.llm.ainvoke(prompt) + summary = response.content.strip() + + logger.info(f"生成的总结: {summary[:100]}...") + return summary + + except Exception as e: + logger.error(f"总结生成异常: {str(e)}") + return f"查询完成,共返回 {query_result.get('row_count', 0)} 条记录。涉及的表: {tables_str}" + + async def process_database_query( + self, + user_query: str, + user_id: int, + database_config_id: int, + table_name: Optional[str] = None, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理数据库智能问数查询的主要工作流(基于保存的表元数据) + + 新流程: + 1. 根据database_config_id获取数据库配置 + 2. 创建数据库连接 + 3. 从系统数据库读取表元数据(只包含启用问答的表) + 4. 根据表元数据生成SQL + 5. 执行SQL查询 + 6. 查询数据后处理成表格形式 + 7. 生成数据总结 + 8. 返回结果 + + Args: + user_query: 用户问题 + user_id: 用户ID + database_config_id: 数据库配置ID + table_name: 表名(可选) + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Returns: + 包含查询结果的字典 + """ + try: + logger.info(f"开始执行数据库查询工作流 - 用户ID: {user_id}, 数据库配置ID: {database_config_id}, 查询: {user_query[:50]}...") + + # 步骤1: 根据database_config_id获取数据库配置并创建连接 + connection_result = await self._connect_database(user_id, database_config_id) + if not connection_result['success']: + raise DatabaseConnectionError(connection_result['message']) + + logger.info("数据库连接成功") + + # 步骤2: 从系统数据库读取表元数据(只包含启用问答的表) + tables_info = await self._get_saved_tables_metadata(user_id, database_config_id) + + logger.info(f"表元数据读取完成 - 共{len(tables_info)}个启用问答的表") + + # 步骤3: 根据表元数据选择相关表并生成SQL + target_tables, target_schemas = await self._select_target_table(user_query, tables_info) + sql_query = await self._generate_sql_query(user_query, target_tables, target_schemas) + + logger.info(f"SQL生成完成 - 目标表: {', '.join(target_tables)}") + + # 步骤4: 执行SQL查询 + query_result = await self._execute_database_query(user_id, sql_query, database_config_id) + logger.info("查询执行完成") + + # 步骤5: 查询数据后处理成表格形式 + table_data = self._convert_query_result_to_table_data(query_result) + + # 步骤6: 生成数据总结 + summary = await self._generate_database_summary(user_query, query_result, ', '.join(target_tables)) + + # 步骤7: 返回结果 + return { + 'success': True, + 'data': { + **table_data, + 'generated_sql': sql_query, + 'summary': summary, + 'table_names': target_tables, + 'query_result': query_result, + 'metadata_source': 'saved_database' # 标记元数据来源 + } + } + + except SmartWorkflowError as e: + logger.error(f"数据库工作流异常: {str(e)}") + return { + 'success': False, + 'error': str(e), + 'error_type': type(e).__name__ + } + except Exception as e: + logger.error(f"数据库工作流未知异常: {str(e)}", exc_info=True) + return { + 'success': False, + 'error': f'系统异常: {str(e)}', + 'error_type': 'SystemError' + } \ No newline at end of file diff --git a/backend/th_agenter/services/smart_excel_workflow.py b/backend/th_agenter/services/smart_excel_workflow.py new file mode 100644 index 0000000..afb2f47 --- /dev/null +++ b/backend/th_agenter/services/smart_excel_workflow.py @@ -0,0 +1,1391 @@ +from typing import Dict, Any, List, Optional, Union +import pandas as pd +import os +import tempfile +import json +import logging +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor +# Try to import langchain_core modules +try: + from langchain_core.runnables import RunnableLambda + from langchain_core.messages import HumanMessage + from langchain_core.prompts import PromptTemplate + LANGCHAIN_CORE_AVAILABLE = True +except ImportError: + RunnableLambda = None + HumanMessage = None + PromptTemplate = None + LANGCHAIN_CORE_AVAILABLE = False + +# Try to import langchain_experimental modules +try: + from langchain_experimental.agents import create_pandas_dataframe_agent + LANGCHAIN_EXPERIMENTAL_AVAILABLE = True +except ImportError: + create_pandas_dataframe_agent = None + LANGCHAIN_EXPERIMENTAL_AVAILABLE = False + +# Try to import langchain_community modules +try: + from langchain_community.chat_models import ChatZhipuAI + LANGCHAIN_COMMUNITY_AVAILABLE = True +except ImportError: + ChatZhipuAI = None + LANGCHAIN_COMMUNITY_AVAILABLE = False + +# Try to import langchain_openai modules +try: + from langchain_openai import ChatOpenAI + LANGCHAIN_OPENAI_AVAILABLE = True +except ImportError: + ChatOpenAI = None + LANGCHAIN_OPENAI_AVAILABLE = False + +# Try to import langchain_classic modules +try: + from langchain_classic.chains import LLMChain + LANGCHAIN_CLASSIC_AVAILABLE = True +except ImportError: + LLMChain = None + LANGCHAIN_CLASSIC_AVAILABLE = False +from th_agenter.core.context import UserContext +from .smart_query import ExcelAnalysisService +from .excel_metadata_service import ExcelMetadataService +from ..core.config import get_settings +from pathlib import Path + +# 配置日志 +logger = logging.getLogger(__name__) + +class SmartWorkflowError(Exception): + """智能工作流自定义异常""" + pass + +class FileLoadError(SmartWorkflowError): + """文件加载异常""" + pass + +class FileSelectionError(SmartWorkflowError): + """文件选择异常""" + pass + +class CodeExecutionError(SmartWorkflowError): + """代码执行异常""" + pass + + +class SmartExcelWorkflowManager: + """ + 智能工作流管理器 + 负责协调文件选择、代码生成和执行的完整流程 + """ + + def __init__(self, db=None): + self.executor = ThreadPoolExecutor(max_workers=4) + self.excel_service = ExcelAnalysisService() + self.db = db + if db: + self.metadata_service = ExcelMetadataService(db) + else: + self.metadata_service = None + + from ..core.llm import create_llm + # 禁用流式响应,避免pandas代理兼容性问题 + self.llm = create_llm(streaming=False) + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + + def _convert_dataframe_to_markdown(self, df_string: str) -> str: + """ + 将DataFrame的字符串表示转换为Markdown表格格式 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + Markdown格式的表格字符串 + """ + try: + lines = df_string.strip().split('\n') + + # 查找表格数据的开始位置 + table_start = -1 + for i, line in enumerate(lines): + if '|' in line or (len(line.split()) > 1 and any(char.isdigit() for char in line)): + table_start = i + break + + if table_start == -1: + return df_string # 如果找不到表格,返回原始字符串 + + # 提取表格行 + table_lines = [] + for line in lines[table_start:]: + if line.strip() and not line.startswith('Name:') and not line.startswith('dtype:'): + table_lines.append(line.strip()) + + if not table_lines: + return df_string + + # 处理第一行作为表头 + if table_lines: + # 检查是否已经是表格格式 + if '|' in table_lines[0]: + # 已经是表格格式,直接返回 + markdown_lines = [] + for i, line in enumerate(table_lines): + if i == 1 and not line.startswith('|'): + # 添加分隔行 + cols = table_lines[0].count('|') + 1 + separator = '|' + '---|' * (cols - 1) + '---|' + markdown_lines.append(separator) + markdown_lines.append(line) + return '\n'.join(markdown_lines) + else: + # 转换为Markdown表格格式 + markdown_lines = [] + + # 处理表头 + if len(table_lines) > 0: + # 假设第一行是索引和数据的混合 + first_line = table_lines[0] + parts = first_line.split() + + if len(parts) > 1: + # 创建表头 + header = '| 索引 | ' + ' | '.join(parts[1:]) + ' |' + markdown_lines.append(header) + + # 创建分隔行 + separator = '|' + '---|' * len(parts) + '' + markdown_lines.append(separator) + + # 处理数据行 + for line in table_lines[1:]: + if line.strip(): + parts = line.split() + if len(parts) > 0: + row = '| ' + ' | '.join(parts) + ' |' + markdown_lines.append(row) + + if markdown_lines: + return '\n'.join(markdown_lines) + + return df_string # 如果转换失败,返回原始字符串 + + except Exception as e: + logger.warning(f"DataFrame转Markdown失败: {str(e)}") + return df_string # 转换失败时返回原始字符串 + + def _convert_dataframe_to_markdown(self, df_string: str) -> str: + """ + 将DataFrame的字符串表示转换为Markdown表格格式 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + Markdown格式的表格字符串 + """ + try: + lines = df_string.strip().split('\n') + + # 查找表格数据的开始位置 + table_start = -1 + for i, line in enumerate(lines): + if '|' in line or (len(line.split()) > 1 and any(char.isdigit() for char in line)): + table_start = i + break + + if table_start == -1: + return df_string # 如果找不到表格,返回原始字符串 + + # 提取表格行 + table_lines = [] + for line in lines[table_start:]: + if line.strip() and not line.startswith('Name:') and not line.startswith('dtype:'): + table_lines.append(line.strip()) + + if not table_lines: + return df_string + + # 处理第一行作为表头 + if table_lines: + # 检查是否已经是表格格式 + if '|' in table_lines[0]: + # 已经是表格格式,直接返回 + markdown_lines = [] + for i, line in enumerate(table_lines): + if i == 1 and not line.startswith('|'): + # 添加分隔行 + cols = table_lines[0].count('|') + 1 + separator = '|' + '---|' * (cols - 1) + '---|' + markdown_lines.append(separator) + markdown_lines.append(line) + return '\n'.join(markdown_lines) + else: + # 转换为Markdown表格格式 + markdown_lines = [] + + # 处理表头 + if len(table_lines) > 0: + # 假设第一行是索引和数据的混合 + first_line = table_lines[0] + parts = first_line.split() + + if len(parts) > 1: + # 创建表头 + header = '| 索引 | ' + ' | '.join(parts[1:]) + ' |' + markdown_lines.append(header) + + # 创建分隔行 + separator = '|' + '---|' * len(parts) + '' + markdown_lines.append(separator) + + # 处理数据行 + for line in table_lines[1:]: + if line.strip(): + parts = line.split() + if len(parts) > 0: + row = '| ' + ' | '.join(parts) + ' |' + markdown_lines.append(row) + + if markdown_lines: + return '\n'.join(markdown_lines) + + return df_string # 如果转换失败,返回原始字符串 + + except Exception as e: + logger.warning(f"DataFrame转Markdown失败: {str(e)}") + return df_string # 转换失败时返回原始字符串 + + def _convert_dataframe_to_table_data(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + 将DataFrame转换为前端Table组件可用的结构化数据 + + Args: + df: pandas DataFrame + + Returns: + 包含columns和data的字典 + """ + try: + # 获取列信息 + columns = [] + for col in df.columns: + columns.append({ + 'prop': str(col), + 'label': str(col), + 'width': 'auto' + }) + + # 获取数据 + data = [] + for index, row in df.iterrows(): + row_data = {'_index': str(index)} + for col in df.columns: + # 处理各种数据类型 + value = row[col] + if pd.isna(value): + row_data[str(col)] = '' + elif isinstance(value, (int, float)): + row_data[str(col)] = value + else: + row_data[str(col)] = str(value) + data.append(row_data) + + return { + 'columns': columns, + 'data': data, + 'total': len(df) + } + + except Exception as e: + logger.warning(f"DataFrame转Table数据失败: {str(e)}") + return { + 'columns': [{'prop': 'result', 'label': '结果'}], + 'data': [{'result': str(df)}], + 'total': 1 + } + + async def process_excel_query_stream( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理智能问数查询的主要工作流 + 实时推送每个工作流步骤 + + Args: + user_query: 用户问题 + user_id: 用户ID + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Yields: + 包含工作流步骤或最终结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行流式智能查询工作流 - 用户ID: {user_id}, 查询: {user_query[:50]}...") + + # 步骤1: 加载文件列表 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'running', + 'message': '正在加载用户文件列表...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + if is_new_conversation or conversation_id is None: + file_list = await self._load_user_file_list(user_id) + if not file_list: + raise FileLoadError('未找到可用的Excel文件,请先上传文件') + else: + file_list = await self._load_user_file_list(user_id) + + step_completed = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'completed', + 'message': f'成功加载{len(file_list)}个文件', + 'details': {'file_count': len(file_list)}, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"文件加载完成 - 共{len(file_list)}个文件") + + except FileLoadError as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤2: 智能文件选择 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'running', + 'message': '正在分析问题并选择相关文件...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + selected_files = await self._select_relevant_files(user_query, file_list) + + if not selected_files: + raise FileSelectionError('未找到与问题相关的Excel文件') + selected_files_names = names_str = ", ".join([file["filename"] for file in selected_files]) + step_completed = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'completed', + 'message': f'选择了{len(selected_files)}个相关文件:{selected_files_names}', + 'details': {'selection_count': len(selected_files)}, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"文件选择完成 - 选择了{len(selected_files)}个文件") + + except FileSelectionError as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤3: 数据加载 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'running', + 'message': '正在加载Excel数据...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + dataframes = await self._load_selected_dataframes(selected_files, user_id) + + step_completed = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'completed', + 'message': f'成功加载{len(dataframes)}个数据表', + 'details': { + 'dataframe_count': len(dataframes), + 'total_rows': sum(len(df) for df in dataframes.values()) + }, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"数据加载完成 - 共{len(dataframes)}个数据表") + + except Exception as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤4: 代码执行 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'running', + 'message': '正在生成并执行Python代码...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + result = await self._execute_smart_query(user_query, dataframes, selected_files) + + step_completed = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'completed', + 'message': '成功执行Python代码分析', + 'details': { + 'result_type': result.get('result_type'), + 'data_count': result.get('total', 0) + }, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info("查询执行完成") + + # 发送最终结果 + yield { + 'type': 'final_result', + 'success': True, + 'data': result, + 'workflow_steps': workflow_steps + } + + except CodeExecutionError as e: + error_msg = f'代码执行失败: {str(e)}' + step_failed = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + logger.error(error_msg) + + yield { + 'type': 'final_result', + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + except SmartWorkflowError as e: + logger.error(f"智能工作流异常: {str(e)}") + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + except Exception as e: + logger.error(f"智能工作流未知异常: {str(e)}", exc_info=True) + yield { + 'type': 'final_result', + 'success': False, + 'message': f'系统异常: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def process_smart_query( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理智能问数查询的主要工作流 + + Args: + user_query: 用户问题 + user_id: 用户ID + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Returns: + 包含查询结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行智能查询工作流 - 用户ID: {user_id}, 查询: {user_query[:50]}...") + + # 步骤1: 加载文件列表 + try: + if is_new_conversation or conversation_id is None: + file_list = await self._load_user_file_list(user_id) + if not file_list: + raise FileLoadError('未找到可用的Excel文件,请先上传文件') + else: + file_list = await self._load_user_file_list(user_id) + + workflow_steps.append({ + 'step': 'file_loading', + 'status': 'completed', + 'message': f'成功加载{len(file_list)}个文件', + 'details': {'file_count': len(file_list)} + }) + logger.info(f"文件加载完成 - 共{len(file_list)}个文件") + + except FileLoadError as e: + workflow_steps.append({ + 'step': 'file_loading', + 'status': 'failed', + 'message': str(e) + }) + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + + # 步骤2: 智能文件选择 + try: + selected_files = await self._select_relevant_files(user_query, file_list) + + if not selected_files: + raise FileSelectionError('未找到与问题相关的Excel文件') + + workflow_steps.append({ + 'step': 'file_selection', + 'status': 'completed', + 'message': f'选择了{len(selected_files)}个相关文件', + 'selected_files': [f['filename'] for f in selected_files], + 'details': {'selection_count': len(selected_files)} + }) + logger.info(f"文件选择完成 - 选中{len(selected_files)}个文件") + + except FileSelectionError as e: + workflow_steps.append({ + 'step': 'file_selection', + 'status': 'failed', + 'message': str(e) + }) + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + + # 步骤3: 加载DataFrame + try: + dataframes = await self._load_selected_dataframes(selected_files, user_id) + + if not dataframes: + raise FileLoadError('无法加载选中的Excel文件数据') + + workflow_steps.append({ + 'step': 'dataframe_loading', + 'status': 'completed', + 'message': f'成功加载{len(dataframes)}个数据表', + 'details': { + 'dataframe_count': len(dataframes), + 'total_rows': sum(len(df) for df in dataframes.values()) + } + }) + logger.info(f"DataFrame加载完成 - {len(dataframes)}个数据表") + + except Exception as e: + error_msg = f'数据加载失败: {str(e)}' + workflow_steps.append({ + 'step': 'dataframe_loading', + 'status': 'failed', + 'message': error_msg + }) + logger.error(error_msg) + return { + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + + # 步骤4: 执行查询 + try: + result = await self._execute_smart_query(user_query, dataframes, selected_files) + + workflow_steps.append({ + 'step': 'code_execution', + 'status': 'completed', + 'message': '成功执行pandas代码分析', + 'details': { + 'result_type': result.get('result_type'), + 'data_count': result.get('total', 0) + } + }) + logger.info("查询执行完成") + + return { + 'success': True, + 'data': result, + 'workflow_steps': workflow_steps + } + + except CodeExecutionError as e: + error_msg = f'代码执行失败: {str(e)}' + workflow_steps.append({ + 'step': 'code_execution', + 'status': 'failed', + 'message': error_msg + }) + logger.error(error_msg) + return { + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + + except SmartWorkflowError as e: + logger.error(f"智能工作流异常: {str(e)}") + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}", exc_info=True) + workflow_steps.append({ + 'step': 'error', + 'status': 'failed', + 'message': f'系统错误: {str(e)}' + }) + return { + 'success': False, + 'message': f'工作流执行失败: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def _load_user_file_list(self, user_id: int) -> List[Dict[str, Any]]: + """ + 加载用户的所有文件列表信息 + """ + try: + # 从数据库获取用户的文件元数据 + file_metadata = [] + if self.metadata_service: + files, total = await self._run_in_executor( + self.metadata_service.get_user_files, user_id + ) + file_metadata = files + else: + logger.warning("metadata_service未初始化,跳过数据库文件查询") + + # 检查持久化目录中的文件 + persistent_dir = os.path.join("backend", "data", 'uploads', f"excel_{user_id}") + persistent_files = [] + if os.path.exists(persistent_dir): + persistent_files = [f for f in os.listdir(persistent_dir) + if f.endswith('.pkl')] + + file_list = [] + + # 合并数据库和持久化文件信息 + for metadata in file_metadata: + # 获取默认sheet的信息 + default_sheet = metadata.default_sheet or (metadata.sheet_names[0] if metadata.sheet_names else None) + columns = metadata.columns_info.get(default_sheet, []) if metadata.columns_info and default_sheet else [] + row_count = metadata.total_rows.get(default_sheet, 0) if metadata.total_rows and default_sheet else 0 + column_count = metadata.total_columns.get(default_sheet, 0) if metadata.total_columns and default_sheet else 0 + + file_info = { + 'id': metadata.id, + 'filename': metadata.original_filename, + 'file_path': metadata.file_path, + 'columns': columns, + 'row_count': row_count, + 'column_count': column_count, + 'description': f'Excel文件,包含{str(len(metadata.sheet_names))}个工作表' if metadata.sheet_names else '', + 'created_at': metadata.created_at.isoformat() if metadata.created_at else None + } + file_list.append(file_info) + + return file_list + + except Exception as e: + print(f"加载文件列表失败: {e}") + return [] + + async def _select_relevant_files( + self, + user_query: str, + file_list: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + 根据用户问题智能选择相关的Excel文件 + + Args: + user_query: 用户问题 + file_list: 可用文件列表 + + Returns: + 选中的文件列表 + + Raises: + FileSelectionError: 文件选择过程中发生错误 + """ + if not file_list: + logger.warning("文件列表为空,无法进行文件选择") + raise FileSelectionError("没有可用的文件进行选择") + + # 如果只有一个文件,直接返回 + if len(file_list) == 1: + logger.info("只有一个文件,直接选择") + return file_list + + try: + logger.info(f"开始智能文件选择 - 可用文件数: {len(file_list)}") + + # 构建文件选择提示 + file_descriptions = [] + for i, file_info in enumerate(file_list): + # 确保数值类型转换为字符串 + row_count = str(file_info.get('row_count', 'unknown')) + column_count = str(file_info.get('column_count', 'unknown')) + columns = file_info.get('columns', []) + # 确保列名都是字符串类型 + column_names = ', '.join(str(col) for col in columns) + + desc = f""" + 文件{i+1}: {file_info['filename']} + - 行数: {row_count} + - 列数: {column_count} + - 列名: {column_names} + - 描述: {file_info.get('description', '无描述')} + """ + file_descriptions.append(desc) + file_des_str = ' \n'.join(file_descriptions) + prompt = f""" + 用户问题: {user_query} + + 可用的Excel文件: + {file_des_str} + + 请分析用户问题,选择最相关的Excel文件来回答问题。 + 如果问题涉及多个文件的数据关联,可以选择多个文件。 + 如果问题只涉及特定类型的数据,只选择相关的文件。 + + 请返回JSON格式的结果,包含选中文件的索引(从1开始): + {{"selected_files": [1, 2, ...], "reason": "选择理由"}} + """ + + # 调用LLM进行文件选择 + response = await self._run_in_executor( + self.llm.invoke, [HumanMessage(content=prompt)] + ) + + # 解析LLM响应 + try: + import re + json_match = re.search(r'\{.*\}', response.content, re.DOTALL) + if json_match: + result = json.loads(json_match.group()) + selected_indices = result.get('selected_files', []) + reason = result.get('reason', '未提供理由') + + # 转换索引为实际文件 + selected_files = [] + for idx in selected_indices: + if 1 <= idx <= len(file_list): + selected_files.append(file_list[idx - 1]) + + if not selected_files: + logger.warning("LLM选择结果为空,回退到选择所有文件") + return file_list + + logger.info(f"成功选择{len(selected_files)}个文件: {[f['filename'] for f in selected_files]}") + logger.info(f"选择理由: {reason}") + return selected_files + else: + logger.warning("无法解析LLM响应中的JSON,回退到选择所有文件") + return file_list + + except (json.JSONDecodeError, KeyError) as e: + logger.warning(f"解析LLM响应失败: {str(e)},回退到选择所有文件") + return file_list + + except Exception as e: + raise e + logger.error(f"文件选择过程中发生错误: {str(e)}") + # 出错时返回所有文件作为备选方案 + logger.info("回退到选择所有文件") + return file_list + + async def _load_selected_dataframes( + self, + selected_files: List[Dict[str, Any]], + user_id: int + ) -> Dict[str, pd.DataFrame]: + """ + 加载选中的Excel文件为DataFrame + 使用新的持久化目录结构和文件匹配逻辑 + """ + dataframes = {} + + # 构建用户专属目录路径 + # base_dir = os.path.join("backend", "data", f"excel_{user_id}") + current_user_id = UserContext.get_current_user().id + backend_dir = Path(__file__).parent.parent.parent # 获取backend目录 + base_dir = backend_dir / "data/uploads" / f'excel_{current_user_id}' + if not os.path.exists(base_dir): + logger.warning(f"用户目录不存在: {base_dir}") + return dataframes + + try: + # 获取目录中所有文件 + all_files = os.listdir(base_dir) + + for file_info in selected_files: + filename = file_info.get('filename', '') + if not filename: + logger.warning(f"文件信息缺少filename: {file_info}") + continue + + # 查找匹配的文件(格式:{uuid}_{original_filename}) + matching_files = [] + for file in all_files: + if file.endswith(f"_{filename}") or file.endswith(f"_{filename}.pkl"): + matching_files.append(file) + + if not matching_files: + logger.warning(f"未找到匹配的文件: {filename}") + continue + + # 如果有多个匹配文件,选择最新的 + if len(matching_files) > 1: + matching_files.sort(key=lambda x: os.path.getmtime(os.path.join(base_dir, x)), reverse=True) + logger.info(f"找到多个匹配文件,选择最新的: {matching_files[0]}") + + selected_file = matching_files[0] + file_path = os.path.join(base_dir, selected_file) + + try: + # 优先加载pickle文件 + if selected_file.endswith('.pkl'): + df = await self._run_in_executor(pd.read_pickle, file_path) + logger.info(f"成功从pickle加载文件: {selected_file}") + else: + # 如果没有pickle文件,尝试加载原始文件 + if selected_file.endswith(('.xlsx', '.xls')): + df = await self._run_in_executor(pd.read_excel, file_path) + elif selected_file.endswith('.csv'): + df = await self._run_in_executor(pd.read_csv, file_path) + else: + logger.warning(f"不支持的文件格式: {selected_file}") + continue + logger.info(f"成功从原始文件加载: {selected_file}") + + # 使用原始文件名作为key + dataframes[filename] = df + logger.info(f"成功加载DataFrame: {filename}, 形状: {df.shape}") + + except Exception as e: + logger.error(f"加载文件失败 {selected_file}: {e}") + continue + + except Exception as e: + logger.error(f"加载DataFrames时发生错误: {e}") + raise FileLoadError(f"无法加载选中的文件: {e}") + + if not dataframes: + raise FileLoadError("没有成功加载任何文件") + + return dataframes + + def _parse_dataframe_string_to_table_data(self, df_string: str, subindex: int = -2) -> Dict[str, Any]: + """ + 将字符串格式的DataFrame转换为表格数据 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + 包含columns和data的字典 + """ + try: + # 按行分割字符串 + lines = df_string.strip().split('\n') + + # 去掉最后两行(如因为最后两行可能是 "[12 rows x 11 columns]和空行") + + if len(lines) >= 2 and subindex == -2 : + lines = lines[:subindex] + + if len(lines) < 2: + # 如果行数不足,返回原始字符串 + return { + 'columns': [{'prop': 'result', 'label': '结果', 'width': 'auto'}], + 'data': [{'result': df_string}], + 'total': 1 + } + + # 第一行是列名 + header_line = lines[0].strip() + # 解析列名(去掉索引列) + columns_raw = header_line.split() + if columns_raw and columns_raw[0].isdigit() == False: + # 如果第一列不是数字,说明包含了列名 + column_names = columns_raw + else: + # 否则使用默认列名 + column_names = [f'Column_{i}' for i in range(len(columns_raw))] + + # 构建列定义 + columns = [] + for i, col_name in enumerate(column_names): + columns.append({ + 'prop': f'col_{i}', + 'label': str(col_name), + 'width': 'auto' + }) + + # 解析数据行 + data = [] + for line in lines[1:]: + if line.strip(): + # 分割数据行 + row_values = line.strip().split() + if row_values: + row_data = {} + # 第一个值通常是索引 + if len(row_values) > 0 and row_values[0].isdigit(): + row_data['_index'] = row_values[0] + values = row_values[1:] + else: + values = row_values + + # 填充列数据 + for i, value in enumerate(values): + if i < len(columns): + col_prop = f'col_{i}' + # 处理NaN值 + if value.lower() == 'nan': + row_data[col_prop] = '' + else: + row_data[col_prop] = value + + data.append(row_data) + + return { + 'columns': columns, + 'data': data, + 'total': len(data) + } + + except Exception as e: + logger.warning(f"解析DataFrame字符串失败: {str(e)}") + return { + 'columns': [{'prop': 'result', 'label': '结果', 'width': 'auto'}], + 'data': [{'result': df_string}], + 'total': 1 + } + + async def _execute_smart_query( + self, + user_query: str, + dataframes: Dict[str, pd.DataFrame], + selected_files: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + 执行智能查询,生成和运行pandas代码 + + Args: + user_query: 用户查询 + dataframes: 加载的数据框字典 + selected_files: 选中的文件信息 + + Returns: + 查询结果字典 + + Raises: + CodeExecutionError: 代码执行失败 + """ + if not dataframes: + raise CodeExecutionError("没有可用的数据文件") + + logger.info(f"开始执行智能查询: {user_query[:50]}...") + + try: + # 如果有多个DataFrame,合并或选择主要的一个 + if len(dataframes) == 1: + main_df = list(dataframes.values())[0] + main_filename = list(dataframes.keys())[0] + else: + # 多个文件时,选择行数最多的作为主DataFrame + main_filename = max(dataframes.keys(), key=lambda k: len(dataframes[k])) + main_df = dataframes[main_filename] + + logger.info(f"选择主数据文件: {main_filename}, 行数: {len(main_df)}, 列数: {len(main_df.columns)}") + + # 验证数据框 + if main_df.empty: + raise CodeExecutionError(f"主数据文件 {main_filename} 为空") + + # 使用PythonAstREPLTool替代pandas代理 + try: + from langchain_experimental.tools import PythonAstREPLTool + from langchain_core.output_parsers import JsonOutputKeyToolsParser + from langchain_core.prompts import ChatPromptTemplate + + # 准备数据框字典,支持多个文件 + df_locals = {} + var_name_to_filename = {} # 变量名到文件名的映射 + for filename, df in dataframes.items(): + # 使用简化的变量名 + var_name = f"df_{len(df_locals) + 1}" if len(dataframes) > 1 else "df" + df_locals[var_name] = df + var_name_to_filename[var_name] = filename + + # 创建Python代码执行工具 + python_tool = PythonAstREPLTool(locals=df_locals) + + logger.info(f"创建Python工具成功,可用数据框: {list(df_locals.keys())}") + + except Exception as e: + raise CodeExecutionError(f"创建Python工具失败: {str(e)}") + + # 构建数据集信息(包含文件名和前5行数据) + dataset_info = [] + for var_name, df in df_locals.items(): + filename = var_name_to_filename[var_name] + + # 基本信息 + basic_info = f"- {var_name} (来源文件: {filename}): {len(df)}行 x {len(df.columns)}列" + + # 列名信息 + columns_info = f" 列名: {', '.join(str(col) for col in df.columns.tolist())}" + + # 前5行数据预览 + try: + preview_df = df.head(5) + preview_data = [] + for idx, row in preview_df.iterrows(): + row_data = [] + for col in df.columns: + value = row[col] + # 处理空值和特殊值 + if pd.isna(value): + row_data.append('NaN') + elif isinstance(value, (int, float)): + row_data.append(str(value)) + else: + # 限制字符串长度避免过长 + str_value = str(value) + if len(str_value) > 20: + str_value = str_value[:17] + '...' + row_data.append(str_value) + preview_data.append(f" 行{idx}: {', '.join(row_data)}") + + preview_info = f" 前5行数据预览:\n{chr(10).join(preview_data)}" + except Exception as e: + preview_info = f" 前5行数据预览: 无法生成预览 ({str(e)})" + + # 组合完整信息 + dataset_info.append(f"{basic_info}\n{columns_info}\n{preview_info}") + + # 构建系统提示 + system_prompt = f""" + 你所有可以访问的数据来自于传递给您的python_tool里的locals里的pandas数据信(可能有多个)。 + pandas数据集详细信息(文件来源、列名信息和数据预览)如下: + {chr(10).join(dataset_info)} + 请根据用户提出的问题,结合给出的数据集的详细信息,直接编写Python相关代码来计算pandas中的值。要求: + 1. 只返回代码,不返回其他内容 + 2. 只允许使用pandas和内置库 + 3. 确保代码能够直接执行并返回结果,包括import必要的内置库 + 4. 返回的结果应该是详细的、完整的数据,而不仅仅是简单答案 + 5. 结果应该包含足够的上下文信息,让用户能够验证和理解答案 + 6. 优先返回DataFrame格式的结果,便于展示为表格 + 7. 务必不要再去写代码查看数据集的结构,提示词里已经给出了每个数据集的结构信息,直接根据提示词里的结构信息进行判断。 + 8. 要求代码中最后一次print的结果,必需是最后的正确结果(用户所需要的数据) + + 示例: + - 如果问"哪个项目合同额最高",不仅要返回项目名称,还要返回跟该项目其他有用的信息,比如合同额,合同时间,项目类型等(如果表格有该这些字段信息) + - 如果问"销售额最高的产品",要返回产品名称、销售额、销售数量、市场占比等完整信息(如果表格有该这些字段信息) + """ + + # 创建提示模板 + prompt = ChatPromptTemplate([ + ("system", system_prompt), + ("user", "{question}") + ]) + + # 创建解析器 + parser = JsonOutputKeyToolsParser(key_name=python_tool.name, first_tool_only=True) + + # 绑定工具到LLM + llm_with_tools = self.llm.bind_tools([python_tool]) + + def debug_print(x): + print('中间结果:', x) + return x + + debug_node = RunnableLambda(debug_print) + # 创建执行链 + llm_chain = prompt | llm_with_tools | debug_node| parser | debug_node| python_tool| debug_node + + # 执行查询 + try: + logger.debug("开始执行Python工具查询") + result = await self._run_in_executor(llm_chain.invoke, {"question": user_query}) + logger.debug(f"查询执行完成,结果: {str(result)[:200]}...") + except Exception as e: + error_msg = f"Python工具执行失败: {str(e)}" + logger.error(error_msg) + raise CodeExecutionError(error_msg) + + # 处理结果 + + try: + # 检查结果是否为pandas DataFrame + print('result type:',type(result)) + parse_result = '' + if isinstance(result, pd.DataFrame): + # 转换为表格数据 + table_data = self._convert_dataframe_to_table_data(result) + + data = table_data['data'] + columns = table_data['columns'] + total = table_data['total'] + result_type = 'table_data' + logger.info(f"处理DataFrame结果: {len(result)}行 x {len(result.columns)}列") + parse_result = table_data + # PythonAstREPLTool返回的是字符串结果 + elif isinstance(result, str): + # 尝试解析结果中的数据 + result_lines = result.strip().split('\n') + + # 检查是否是DataFrame的字符串表示 + if any('DataFrame' in line or ('|' in line and len([l for l in result_lines if '|' in l]) > 1) for line in result_lines): + + table_data = self._parse_dataframe_string_to_table_data(result) + data = table_data['data'] + columns = table_data['columns'] + total = 1 + result_type = 'table_data' + parse_result = table_data + elif ('rows' in result_lines[-1] and 'columns' in result_lines[-1]): + # 尝试解析DataFrame字符串为表格数据 + table_data = self._parse_dataframe_string_to_table_data(result) + if 'data' in table_data and 'columns' in table_data: + data = table_data['data'] + columns = table_data['columns'] + total = 1 + result_type = 'table_data' + parse_result = table_data + else: + total = 1 + result_type = 'text' + parse_result = table_data + + else: + # 简单的数值或文本结果 + # 尝试解析DataFrame字符串为表格数据 + table_data = self._parse_dataframe_string_to_table_data(result, 0) + if 'data' in table_data and 'columns' in table_data: + data = table_data['data'] + columns = table_data['columns'] + total = table_data['total'] + total = 1 + result_type = 'table_data' + parse_result = table_data + else: + total = 1 + result_type = 'text' + parse_result = table_data + elif isinstance(result, (int, float, bool)): + data = result + columns = result + total = 1 + result_type = 'scalar' + parse_result = result + else: + # 处理其他类型的结果 + data = result + columns = result + total = 1 + result_type = 'other' + parse_result = result + logger.info(f"结果处理完成: {result_type}, 数据行数: {total}") + + except Exception as e: + error_msg = f"结果处理失败: {str(e)}" + logger.error(error_msg) + raise CodeExecutionError(error_msg) + + # 生成总结 + try: + summary = await self._generate_query_summary(user_query, parse_result, main_df) + except Exception as e: + logger.warning(f"生成总结失败: {str(e)}") + summary = f"基于数据分析完成查询,共处理{len(main_df)}行数据。" + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'result_type': result_type, + 'summary': summary, + 'used_files': list(dataframes.keys()), + 'generated_code': f"# 基于文件: {', '.join(dataframes.keys())}\n# 查询: {user_query}\n# 使用LangChain Python工具执行", + 'data_info': { + 'source_files': list(dataframes.keys()), + 'dataframes': {name: {'rows': len(df), 'columns': len(df.columns), 'column_names': [str(col) for col in df.columns.tolist()]} for name, df in dataframes.items()} + } + } + + except CodeExecutionError: + raise + except Exception as e: + error_msg = f"查询执行过程中发生未知错误: {str(e)}" + logger.error(error_msg, exc_info=True) + raise CodeExecutionError(error_msg) + + async def _generate_query_summary( + self, + query: str, + result: Any, + df: pd.DataFrame + ) -> str: + """ + 生成查询结果的AI总结 + """ + try: + logger.debug("开始生成查询总结") + + # 安全地获取数据集信息 + try: + dataset_info = f""" + 数据集信息: + - 总行数: {len(df)} + - 总列数: {len(df.columns)} + - 列名: {', '.join(str(col) for col in df.columns.tolist())} + """ + except Exception as e: + logger.warning(f"获取数据集信息失败: {str(e)}") + dataset_info = "数据集信息: 无法获取" + + # 安全地处理查询结果 + try: + if isinstance(result, pd.DataFrame): + if len(result) > 0: + result_preview = result.head(3).to_string(max_cols=5, max_rows=3) + else: + result_preview = "查询结果为空" + else: + result_preview = str(result) # 限制长度避免过长 + except Exception as e: + logger.warning(f"生成结果预览失败: {str(e)}") + result_preview = "无法生成结果预览" + + prompt = f""" + 用户问题: {query} + + {dataset_info} + + 查询结果: {result_preview}... + + 系统已经根据用户提问查询出了结果,请根据结果生成一个简洁的中文总结,说明: + 1. 查询的主要发现 + 2. 数据的关键特征 + 3. 结果的业务含义 + + 总结应该在100字以内,通俗易懂。 + """ + + try: + response = await self._run_in_executor( + self.llm.invoke, [HumanMessage(content=prompt)] + ) + + summary = response.content.strip() + + # 验证总结长度 + if len(summary) > 200: + logger.warning("AI生成的总结过长,进行截取") + summary = summary[:200] + "..." + + logger.debug("查询总结生成完成") + return summary + + except Exception as e: + logger.warning(f"LLM总结生成失败: {str(e)}") + # 生成基础总结 + if isinstance(result, pd.DataFrame): + return f"基于{len(df)}行数据完成了关于'{query}'的分析,返回了{len(result)}条结果。" + else: + return f"基于{len(df)}行数据完成了关于'{query}'的分析查询。" + + except Exception as e: + logger.error(f"生成查询总结时发生错误: {str(e)}") + # 如果所有方法都失败,返回最基础的总结 + try: + return f"基于数据分析完成查询,共处理{len(df)}行数据。" + except: + return "完成了数据分析查询。" \ No newline at end of file diff --git a/backend/th_agenter/services/smart_query.py b/backend/th_agenter/services/smart_query.py new file mode 100644 index 0000000..5bda85f --- /dev/null +++ b/backend/th_agenter/services/smart_query.py @@ -0,0 +1,757 @@ +import pandas as pd +import tempfile + +# Try to import database modules +try: + import pymysql + PYMYSQL_AVAILABLE = True +except ImportError: + pymysql = None + PYMYSQL_AVAILABLE = False + +try: + import psycopg2 + PSYCOPG2_AVAILABLE = True +except ImportError: + psycopg2 = None + PSYCOPG2_AVAILABLE = False +import os +from typing import Dict, Any, List +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor + +# Try to import langchain_experimental modules +try: + from langchain_experimental.agents import create_pandas_dataframe_agent + LANGCHAIN_EXPERIMENTAL_AVAILABLE = True +except ImportError: + create_pandas_dataframe_agent = None + LANGCHAIN_EXPERIMENTAL_AVAILABLE = False + +# Try to import langchain_community modules +try: + from langchain_community.chat_models import ChatZhipuAI + LANGCHAIN_COMMUNITY_AVAILABLE = True +except ImportError: + ChatZhipuAI = None + LANGCHAIN_COMMUNITY_AVAILABLE = False + +# Try to import langchain_core modules +try: + from langchain_core.messages import HumanMessage + LANGCHAIN_CORE_AVAILABLE = True +except ImportError: + HumanMessage = None + LANGCHAIN_CORE_AVAILABLE = False + +# 在 SmartQueryService 类中添加方法 + +from .table_metadata_service import TableMetadataService + +class SmartQueryService: + """ + 智能问数服务基类 + """ + def __init__(self): + self.executor = ThreadPoolExecutor(max_workers=4) + self.table_metadata_service = None + + def set_db_session(self, db_session): + """设置数据库会话""" + self.table_metadata_service = TableMetadataService(db_session) + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + +class ExcelAnalysisService(SmartQueryService): + """ + Excel数据分析服务 + """ + def __init__(self): + super().__init__() + self.user_dataframes = {} # 存储用户的DataFrame + + def analyze_dataframe(self, df: pd.DataFrame, filename: str) -> Dict[str, Any]: + """ + 分析DataFrame并返回基本信息 + """ + try: + # 基本统计信息 + rows, columns = df.shape + + # 列信息 + column_info = [] + for col in df.columns: + col_info = { + 'name': col, + 'dtype': str(df[col].dtype), + 'null_count': int(df[col].isnull().sum()), + 'unique_count': int(df[col].nunique()) + } + + # 如果是数值列,添加统计信息 + if pd.api.types.is_numeric_dtype(df[col]): + df.fillna({col:0}) #数值列,将空值补0 + col_info.update({ + 'mean': float(df[col].mean()) if not df[col].isnull().all() else None, + 'std': float(df[col].std()) if not df[col].isnull().all() else None, + 'min': float(df[col].min()) if not df[col].isnull().all() else None, + 'max': float(df[col].max()) if not df[col].isnull().all() else None + }) + + column_info.append(col_info) + + # 数据预览(前5行) + preview_data = df.head().fillna('').to_dict('records') + + # 数据质量检查 + quality_issues = [] + + # 检查缺失值 + missing_cols = df.columns[df.isnull().any()].tolist() + if missing_cols: + quality_issues.append({ + 'type': 'missing_values', + 'description': f'以下列存在缺失值: {", ".join(map(str, missing_cols))}', + 'columns': missing_cols + }) + + # 检查重复行 + duplicate_count = df.duplicated().sum() + if duplicate_count > 0: + quality_issues.append({ + 'type': 'duplicate_rows', + 'description': f'发现 {duplicate_count} 行重复数据', + 'count': int(duplicate_count) + }) + + return { + 'filename': filename, + 'rows': rows, + 'columns': columns, + 'column_names': [str(col) for col in df.columns.tolist()], + 'column_info': column_info, + 'preview': preview_data, + 'quality_issues': quality_issues, + 'memory_usage': f"{df.memory_usage(deep=True).sum() / 1024 / 1024:.2f} MB" + } + + except Exception as e: + print(e) + raise Exception(f"DataFrame分析失败: {str(e)}") + + def _create_pandas_agent(self, df: pd.DataFrame): + """ + 创建pandas代理 + """ + try: + if not LANGCHAIN_COMMUNITY_AVAILABLE: + raise Exception("langchain_community not available. Cannot create pandas agent.") + if not LANGCHAIN_EXPERIMENTAL_AVAILABLE: + raise Exception("langchain_experimental not available. Cannot create pandas agent.") + + # 使用智谱AI作为LLM + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.1 + ) + + # 创建pandas代理 + agent = create_pandas_dataframe_agent( + llm=llm, + df=df, + verbose=True, + return_intermediate_steps=True, + handle_parsing_errors=True, + max_iterations=3, + early_stopping_method="force", + allow_dangerous_code=True # 允许执行代码以支持数据分析 + ) + + return agent + + except Exception as e: + raise Exception(f"创建pandas代理失败: {str(e)}") + + def _execute_pandas_query(self, agent, query: str) -> Dict[str, Any]: + """ + 执行pandas查询 + """ + try: + # 执行查询 + # 使用invoke方法来处理有多个输出键的情况 + agent_result = agent.invoke({"input": query}) + # 提取主要结果 + result = agent_result.get('output', agent_result) + + # 解析结果 + if isinstance(result, pd.DataFrame): + # 如果结果是DataFrame + data = result.fillna('').to_dict('records') + columns = result.columns.tolist() + total = len(result) + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'result_type': 'dataframe' + } + else: + # 如果结果是其他类型(字符串、数字等) + return { + 'data': [{'result': str(result)}], + 'columns': ['result'], + 'total': 1, + 'result_type': 'scalar' + } + + except Exception as e: + raise Exception(f"pandas查询执行失败: {str(e)}") + + async def execute_natural_language_query( + self, + query: str, + user_id: int, + page: int = 1, + page_size: int = 20 + ) -> Dict[str, Any]: + """ + 执行自然语言查询 + """ + try: + # 查找用户的临时文件 + temp_dir = tempfile.gettempdir() + user_files = [f for f in os.listdir(temp_dir) + if f.startswith(f"excel_{user_id}_") and f.endswith('.pkl')] + + if not user_files: + return { + 'success': False, + 'message': '未找到上传的Excel文件,请先上传文件' + } + + # 使用最新的文件 + latest_file = sorted(user_files)[-1] + file_path = os.path.join(temp_dir, latest_file) + + # 加载DataFrame + df = pd.read_pickle(file_path) + + # 创建pandas代理 + agent = self._create_pandas_agent(df) + + # 执行查询 + query_result = await self._run_in_executor( + self._execute_pandas_query, agent, query + ) + + # 分页处理 + total = query_result['total'] + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + + paginated_data = query_result['data'][start_idx:end_idx] + + # 生成AI总结 + summary = await self._generate_summary(query, query_result, df) + + return { + 'success': True, + 'data': { + 'data': paginated_data, + 'columns': query_result['columns'], + 'total': total, + 'page': page, + 'page_size': page_size, + 'generated_code': f"# 基于自然语言查询: {query}\n# 使用LangChain Pandas代理执行", + 'summary': summary, + 'result_type': query_result['result_type'] + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"查询执行失败: {str(e)}" + } + + async def _generate_summary(self, query: str, result: Dict[str, Any], df: pd.DataFrame) -> str: + """ + 生成AI总结 + """ + try: + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.3 + ) + + # 构建总结提示 + prompt = f""" + 用户查询: {query} + + 数据集信息: + - 总行数: {len(df)} + - 总列数: {len(df.columns)} + - 列名: {', '.join(str(col) for col in df.columns.tolist())} + + 查询结果: + - 结果类型: {result['result_type']} + - 结果行数: {result['total']} + - 结果列数: {len(result['columns'])} + + 请基于以上信息,用中文生成一个简洁的分析总结,包括: + 1. 查询的主要目的 + 2. 关键发现 + 3. 数据洞察 + 4. 建议的后续分析方向 + + 总结应该专业、准确、易懂,控制在200字以内。 + """ + + response = await self._run_in_executor( + lambda: llm.invoke([HumanMessage(content=prompt)]) + ) + + return response.content + + except Exception as e: + return f"查询已完成,但生成总结时出现错误: {str(e)}" + +class DatabaseQueryService(SmartQueryService): + """ + 数据库查询服务 + """ + def __init__(self): + super().__init__() + self.user_connections = {} # 存储用户的数据库连接信息 + + def _create_connection(self, config: Dict[str, str]): + """ + 创建数据库连接 + """ + db_type = config['type'].lower() + + try: + if db_type == 'mysql': + if not PYMYSQL_AVAILABLE: + raise Exception("MySQL数据库连接功能不可用,请安装pymysql模块") + connection = pymysql.connect( + host=config['host'], + port=int(config['port']), + user=config['username'], + password=config['password'], + database=config['database'], + charset='utf8mb4' + ) + elif db_type == 'postgresql': + if not PSYCOPG2_AVAILABLE: + raise Exception("PostgreSQL数据库连接功能不可用,请安装psycopg2模块") + connection = psycopg2.connect( + host=config['host'], + port=int(config['port']), + user=config['username'], + password=config['password'], + database=config['database'] + ) + + return connection + + except Exception as e: + raise Exception(f"数据库连接失败: {str(e)}") + + async def test_connection(self, config: Dict[str, str]) -> bool: + """ + 测试数据库连接 + """ + try: + connection = await self._run_in_executor(self._create_connection, config) + connection.close() + return True + except Exception: + return False + + async def connect_database(self, config: Dict[str, str], user_id: int) -> Dict[str, Any]: + """ + 连接数据库并获取表列表 + """ + try: + connection = await self._run_in_executor(self._create_connection, config) + + # 获取表列表 + tables = await self._run_in_executor(self._get_tables, connection, config['type']) + + # 存储连接信息 + self.user_connections[user_id] = { + 'config': config, + 'connection': connection, + 'connected_at': datetime.now() + } + + return { + 'success': True, + 'data': { + 'tables': tables, + 'database_type': config['type'], + 'database_name': config['database'] + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"数据库连接失败: {str(e)}" + } + + def _get_tables(self, connection, db_type: str) -> List[str]: + """ + 获取数据库表列表 + """ + cursor = connection.cursor() + + try: + if db_type.lower() == 'mysql': + cursor.execute("SHOW TABLES") + tables = [row[0] for row in cursor.fetchall()] + elif db_type.lower() == 'postgresql': + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + """) + tables = [row[0] for row in cursor.fetchall()] + + elif db_type.lower() == 'sqlserver': + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + """) + tables = [row[0] for row in cursor.fetchall()] + else: + tables = [] + + return tables + + finally: + cursor.close() + + async def get_table_schema(self, table_name: str, user_id: int) -> Dict[str, Any]: + """ + 获取表结构 + """ + try: + if user_id not in self.user_connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接' + } + + connection = self.user_connections[user_id]['connection'] + db_type = self.user_connections[user_id]['config']['type'] + + schema = await self._run_in_executor( + self._get_table_schema, connection, table_name, db_type + ) + + return { + 'success': True, + 'data': { + 'schema': schema, + 'table_name': table_name + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"获取表结构失败: {str(e)}" + } + + def _get_table_schema(self, connection, table_name: str, db_type: str) -> List[Dict[str, Any]]: + """ + 获取表结构信息 + """ + cursor = connection.cursor() + + try: + if db_type.lower() == 'mysql': + cursor.execute(f"DESCRIBE {table_name}") + columns = cursor.fetchall() + schema = [{ + 'column_name': col[0], + 'data_type': col[1], + 'is_nullable': 'YES' if col[2] == 'YES' else 'NO', + 'column_key': col[3], + 'column_default': col[4] + } for col in columns] + elif db_type.lower() == 'postgresql': + cursor.execute(""" + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s + ORDER BY ordinal_position + """, (table_name,)) + columns = cursor.fetchall() + schema = [{ + 'column_name': col[0], + 'data_type': col[1], + 'is_nullable': col[2], + 'column_default': col[3] + } for col in columns] + + else: + schema = [] + + return schema + + finally: + cursor.close() + + async def execute_natural_language_query( + self, + query: str, + table_name: str, + user_id: int, + page: int = 1, + page_size: int = 20 + ) -> Dict[str, Any]: + """ + 执行自然语言数据库查询 + """ + try: + if user_id not in self.user_connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接' + } + + connection = self.user_connections[user_id]['connection'] + + # 这里应该集成MCP服务来将自然语言转换为SQL + # 目前先使用简单的实现 + sql_query = await self._convert_to_sql(query, table_name, connection) + + # 执行SQL查询 + result = await self._run_in_executor( + self._execute_sql_query, connection, sql_query, page, page_size + ) + + # 生成AI总结 + summary = await self._generate_db_summary(query, result, table_name) + + result['generated_code'] = sql_query + result['summary'] = summary + + return { + 'success': True, + 'data': result + } + + except Exception as e: + return { + 'success': False, + 'message': f"数据库查询执行失败: {str(e)}" + } + + async def _convert_to_sql(self, query: str, table_name: str, connection) -> str: + """ + 将自然语言转换为SQL查询 + TODO: 集成MCP服务 + """ + # 这是一个简化的实现,实际应该使用MCP服务 + # 根据常见的查询模式生成SQL + + query_lower = query.lower() + + if '所有' in query or '全部' in query or 'all' in query_lower: + return f"SELECT * FROM {table_name} LIMIT 100" + elif '统计' in query or '总数' in query or 'count' in query_lower: + return f"SELECT COUNT(*) as total_count FROM {table_name}" + elif '最近' in query or 'recent' in query_lower: + return f"SELECT * FROM {table_name} ORDER BY id DESC LIMIT 10" + elif '分组' in query or 'group' in query_lower: + # 简单的分组查询,需要根据实际表结构调整 + return f"SELECT COUNT(*) as count FROM {table_name} GROUP BY id LIMIT 10" + else: + # 默认查询 + return f"SELECT * FROM {table_name} LIMIT 20" + + def _execute_sql_query(self, connection, sql_query: str, page: int, page_size: int) -> Dict[str, Any]: + """ + 执行SQL查询 + """ + cursor = connection.cursor() + + try: + # 执行查询 + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取所有结果 + all_results = cursor.fetchall() + total = len(all_results) + + # 分页 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_results = all_results[start_idx:end_idx] + + # 转换为字典格式 + data = [] + for row in paginated_results: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + row_dict[columns[i]] = value + data.append(row_dict) + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'page': page, + 'page_size': page_size + } + + finally: + cursor.close() + + async def _generate_db_summary(self, query: str, result: Dict[str, Any], table_name: str) -> str: + """ + 生成数据库查询总结 + """ + try: + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.3 + ) + + prompt = f""" + 用户查询: {query} + 目标表: {table_name} + + 查询结果: + - 结果行数: {result['total']} + - 结果列数: {len(result['columns'])} + - 列名: {', '.join(result['columns'])} + + 请基于以上信息,用中文生成一个简洁的数据库查询分析总结,包括: + 1. 查询的主要目的 + 2. 关键数据发现 + 3. 数据特征分析 + 4. 建议的后续查询方向 + + 总结应该专业、准确、易懂,控制在200字以内。 + """ + + response = await self._run_in_executor( + lambda: llm.invoke([HumanMessage(content=prompt)]) + ) + + return response.content + + except Exception as e: + return f"查询已完成,但生成总结时出现错误: {str(e)}" + + # 在 SmartQueryService 类中添加方法 + + from .table_metadata_service import TableMetadataService + + class SmartQueryService: + def __init__(self): + super().__init__() + self.table_metadata_service = None + + def set_db_session(self, db_session): + """设置数据库会话""" + self.table_metadata_service = TableMetadataService(db_session) + + async def get_database_context(self, user_id: int, query: str) -> str: + """获取数据库上下文信息用于问答""" + if not self.table_metadata_service: + return "" + + try: + # 获取用户的表元数据 + table_metadata_list = self.table_metadata_service.get_user_table_metadata(user_id) + + if not table_metadata_list: + return "" + + # 构建数据库上下文 + context_parts = [] + context_parts.append("=== 数据库表信息 ===") + + for metadata in table_metadata_list: + table_info = [] + table_info.append(f"表名: {metadata.table_name}") + + if metadata.table_comment: + table_info.append(f"表描述: {metadata.table_comment}") + + if metadata.qa_description: + table_info.append(f"业务说明: {metadata.qa_description}") + + # 添加列信息 + if metadata.columns_info: + columns = [] + for col in metadata.columns_info: + col_desc = f"{col['column_name']} ({col['data_type']})" + if col.get('column_comment'): + col_desc += f" - {col['column_comment']}" + columns.append(col_desc) + table_info.append(f"字段: {', '.join(columns)}") + + # 添加示例数据 + if metadata.sample_data: + table_info.append(f"示例数据: {metadata.sample_data[:2]}") + + table_info.append(f"总行数: {metadata.row_count}") + + context_parts.append("\n".join(table_info)) + context_parts.append("---") + + return "\n".join(context_parts) + + except Exception as e: + logger.error(f"获取数据库上下文失败: {str(e)}") + return "" + + async def execute_smart_query(self, query: str, user_id: int, **kwargs) -> Dict[str, Any]: + """执行智能查询(集成表元数据)""" + try: + # 获取数据库上下文 + db_context = await self.get_database_context(user_id, query) + + # 构建增强的提示词 + enhanced_prompt = f""" + {db_context} + + 用户问题: {query} + + 请基于上述数据库表信息,生成相应的SQL查询语句。 + 注意: + 1. 使用准确的表名和字段名 + 2. 考虑数据类型和约束 + 3. 参考示例数据理解数据格式 + 4. 生成高效的查询语句 + """ + + # 调用原有的查询逻辑 + return await super().execute_smart_query(enhanced_prompt, user_id, **kwargs) + + except Exception as e: + logger.error(f"智能查询失败: {str(e)}") + return { + 'success': False, + 'message': f"查询失败: {str(e)}" + } \ No newline at end of file diff --git a/backend/th_agenter/services/smart_workflow.py b/backend/th_agenter/services/smart_workflow.py new file mode 100644 index 0000000..fce1d9b --- /dev/null +++ b/backend/th_agenter/services/smart_workflow.py @@ -0,0 +1,83 @@ +from typing import Dict, Any, List, Optional, Union +import logging +from .smart_excel_workflow import SmartExcelWorkflowManager +from .smart_db_workflow import SmartDatabaseWorkflowManager + +logger = logging.getLogger(__name__) + +# 异常类已迁移到各自的工作流文件中 + +class SmartWorkflowManager: + """ + 智能工作流管理器 + 统一入口,委托给具体的Excel或数据库工作流管理器 + """ + + def __init__(self, db=None): + self.db = db + self.excel_workflow = SmartExcelWorkflowManager(db) + self.database_workflow = SmartDatabaseWorkflowManager(db) + + async def process_excel_query_stream( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理Excel智能问数查询,委托给Excel工作流管理器 + """ + async for result in self.excel_workflow.process_excel_query_stream( + user_query, user_id, conversation_id, is_new_conversation + ): + yield result + + async def process_database_query_stream( + self, + user_query: str, + user_id: int, + database_config_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理数据库智能问数查询,委托给数据库工作流管理器 + """ + async for result in self.database_workflow.process_database_query_stream( + user_query, user_id, database_config_id + ): + yield result + + async def process_smart_query( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理智能问数查询的主要工作流(非流式版本) + 委托给Excel工作流管理器 + """ + return await self.excel_workflow.process_smart_query( + user_query=user_query, + user_id=user_id, + conversation_id=conversation_id, + is_new_conversation=is_new_conversation + ) + + async def process_database_query( + self, + user_query: str, + user_id: int, + database_config_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理数据库智能问数查询,委托给数据库工作流管理器 + """ + return await self.database_workflow.process_database_query( + user_query, user_id, database_config_id, None, conversation_id, is_new_conversation + ) \ No newline at end of file diff --git a/backend/th_agenter/services/storage.py b/backend/th_agenter/services/storage.py new file mode 100644 index 0000000..b1cb6db --- /dev/null +++ b/backend/th_agenter/services/storage.py @@ -0,0 +1,299 @@ +"""File storage service supporting local and S3 storage.""" + +import os +import uuid +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Optional, BinaryIO, Dict, Any +from fastapi import UploadFile + +# Try to import boto3 and related modules for S3 storage +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + S3_AVAILABLE = True +except ImportError: + boto3 = None + ClientError = None + NoCredentialsError = None + S3_AVAILABLE = False + +from ..core.config import settings +from ..utils.file_utils import FileUtils + + +class StorageBackend(ABC): + """Abstract storage backend interface.""" + + @abstractmethod + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file and return storage info.""" + pass + + @abstractmethod + async def delete_file(self, file_path: str) -> bool: + """Delete file from storage.""" + pass + + @abstractmethod + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get file access URL.""" + pass + + @abstractmethod + async def file_exists(self, file_path: str) -> bool: + """Check if file exists.""" + pass + + +class LocalStorageBackend(StorageBackend): + """Local file system storage backend.""" + + def __init__(self, base_path: str): + self.base_path = Path(base_path) + self.base_path.mkdir(parents=True, exist_ok=True) + + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file to local storage.""" + full_path = self.base_path / file_path + + # Create directory if it doesn't exist + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Write file + with open(full_path, "wb") as f: + content = await file.read() + f.write(content) + + # Get file info + file_info = FileUtils.get_file_info(str(full_path)) + + return { + "file_path": file_path, + "full_path": str(full_path), + "size": file_info["size_bytes"], + "mime_type": file_info["mime_type"], + "storage_type": "local" + } + + async def delete_file(self, file_path: str) -> bool: + """Delete file from local storage.""" + full_path = self.base_path / file_path + return FileUtils.delete_file(str(full_path)) + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get local file URL (for development).""" + # In production, you might want to serve files through a web server + full_path = self.base_path / file_path + if full_path.exists(): + return f"/files/{file_path}" + return None + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists in local storage.""" + full_path = self.base_path / file_path + return full_path.exists() + + +if S3_AVAILABLE: + class S3StorageBackend(StorageBackend): + """Amazon S3 storage backend.""" + + def __init__( + self, + bucket_name: str, + aws_access_key_id: Optional[str] = None, + aws_secret_access_key: Optional[str] = None, + aws_region: str = "us-east-1", + endpoint_url: Optional[str] = None + ): + self.bucket_name = bucket_name + self.aws_region = aws_region + + # Initialize S3 client + session = boto3.Session( + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=aws_region + ) + + self.s3_client = session.client( + 's3', + endpoint_url=endpoint_url # For S3-compatible services like MinIO + ) + + # Verify bucket exists or create it + self._ensure_bucket_exists() + + def _ensure_bucket_exists(self): + """Ensure S3 bucket exists.""" + try: + self.s3_client.head_bucket(Bucket=self.bucket_name) + except ClientError as e: + error_code = int(e.response['Error']['Code']) + if error_code == 404: + # Bucket doesn't exist, create it + try: + if self.aws_region == 'us-east-1': + self.s3_client.create_bucket(Bucket=self.bucket_name) + else: + self.s3_client.create_bucket( + Bucket=self.bucket_name, + CreateBucketConfiguration={'LocationConstraint': self.aws_region} + ) + except ClientError as create_error: + raise Exception(f"Failed to create S3 bucket: {create_error}") + else: + raise Exception(f"Failed to access S3 bucket: {e}") + + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file to S3.""" + try: + # Read file content + content = await file.read() + + # Determine content type + content_type = FileUtils.get_mime_type(file.filename) or 'application/octet-stream' + + # Upload to S3 + self.s3_client.put_object( + Bucket=self.bucket_name, + Key=file_path, + Body=content, + ContentType=content_type, + Metadata={ + 'original_filename': file.filename or 'unknown', + 'upload_timestamp': str(int(os.time.time())) + } + ) + + return { + "file_path": file_path, + "bucket": self.bucket_name, + "size": len(content), + "mime_type": content_type, + "storage_type": "s3" + } + except (ClientError, NoCredentialsError) as e: + raise Exception(f"Failed to upload file to S3: {e}") + + async def delete_file(self, file_path: str) -> bool: + """Delete file from S3.""" + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=file_path + ) + return True + except ClientError: + return False + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get presigned URL for S3 file.""" + try: + url = self.s3_client.generate_presigned_url( + 'get_object', + Params={'Bucket': self.bucket_name, 'Key': file_path}, + ExpiresIn=3600 # 1 hour + ) + return url + except ClientError: + return None + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists in S3.""" + try: + self.s3_client.head_object( + Bucket=self.bucket_name, + Key=file_path + ) + return True + except ClientError: + return False + + +class StorageService: + """统一的存储服务管理器""" + + def __init__(self): + self.storage_type = settings.storage.storage_type + + if self.storage_type == 's3': + if S3_AVAILABLE: + self.backend = S3StorageBackend( + bucket_name=settings.storage.s3_bucket_name, + aws_access_key_id=settings.storage.aws_access_key_id, + aws_secret_access_key=settings.storage.aws_secret_access_key, + aws_region=settings.storage.aws_region, + endpoint_url=settings.storage.s3_endpoint_url + ) + else: + # Fallback to local storage if S3 is not available + from ..utils.logger import get_logger + logger = get_logger("storage_service") + logger.warning("S3 storage not available. Falling back to local storage.") + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + upload_dir = settings.storage.upload_directory + if not os.path.isabs(upload_dir): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + upload_dir = str(backend_dir / upload_dir) + self.backend = LocalStorageBackend(upload_dir) + else: + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + upload_dir = settings.storage.upload_directory + if not os.path.isabs(upload_dir): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + upload_dir = str(backend_dir / upload_dir) + self.backend = LocalStorageBackend(upload_dir) + + def generate_file_path(self, knowledge_base_id: int, filename: str) -> str: + """Generate unique file path for storage.""" + # Sanitize filename + safe_filename = FileUtils.sanitize_filename(filename) + + # Generate unique identifier + file_id = str(uuid.uuid4()) + + # Create path: kb_{id}/{file_id}_{filename} + return f"kb_{knowledge_base_id}/{file_id}_{safe_filename}" + + async def upload_file( + self, + file: UploadFile, + knowledge_base_id: int + ) -> Dict[str, Any]: + """Upload file using configured storage backend.""" + file_path = self.generate_file_path(knowledge_base_id, file.filename) + return await self.backend.upload_file(file, file_path) + + async def delete_file(self, file_path: str) -> bool: + """Delete file using configured storage backend.""" + return await self.backend.delete_file(file_path) + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get file access URL.""" + return await self.backend.get_file_url(file_path) + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists.""" + return await self.backend.file_exists(file_path) + + +# Global storage service instance +storage_service = StorageService() \ No newline at end of file diff --git a/backend/th_agenter/services/table_metadata_service.py b/backend/th_agenter/services/table_metadata_service.py new file mode 100644 index 0000000..c34fab8 --- /dev/null +++ b/backend/th_agenter/services/table_metadata_service.py @@ -0,0 +1,446 @@ +"""表元数据管理服务""" + +import json +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, func +from datetime import datetime + +from ..models.table_metadata import TableMetadata +from ..models.database_config import DatabaseConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool + +logger = get_logger("table_metadata_service") + + +class TableMetadataService: + """表元数据管理服务""" + + def __init__(self, db_session: Session): + self.db = db_session + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + + async def collect_and_save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_names: List[str] + ) -> Dict[str, Any]: + """收集并保存表元数据""" + try: + # 获取数据库配置 + db_config = self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == database_config_id, DatabaseConfig.created_by == user_id) + ).first() + + if not db_config: + raise NotFoundError("数据库配置不存在") + + # 根据数据库类型选择相应的工具 + if db_config.db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_config.db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + raise Exception(f"不支持的数据库类型: {db_config.db_type}") + + # 检查是否已有连接,如果没有则建立连接 + user_id_str = str(user_id) + if user_id_str not in db_tool.connections: + connection_config = { + 'host': db_config.host, + 'port': db_config.port, + 'database': db_config.database, + 'username': db_config.username, + 'password': self._decrypt_password(db_config.password) + } + + # 连接数据库 + connect_result = await db_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=user_id_str + ) + + if not connect_result.success: + raise Exception(f"数据库连接失败: {connect_result.error}") + + logger.info(f"为用户 {user_id} 建立了新的{db_config.db_type}数据库连接") + else: + logger.info(f"复用用户 {user_id} 的现有{db_config.db_type}数据库连接") + + collected_tables = [] + failed_tables = [] + + for table_name in table_names: + try: + # 收集表元数据 + metadata = await self._collect_single_table_metadata( + user_id, table_name, db_config.db_type + ) + + # 保存或更新元数据 + table_metadata = await self._save_table_metadata( + user_id, database_config_id, table_name, metadata + ) + + collected_tables.append({ + 'table_name': table_name, + 'metadata_id': table_metadata.id, + 'columns_count': len(metadata['columns_info']), + 'sample_rows': len(metadata['sample_data']) + }) + + except Exception as e: + print(e) + logger.error(f"收集表 {table_name} 元数据失败: {str(e)}") + failed_tables.append({ + 'table_name': table_name, + 'error': str(e) + }) + + return { + 'success': True, + 'collected_tables': collected_tables, + 'failed_tables': failed_tables, + 'total_collected': len(collected_tables), + 'total_failed': len(failed_tables) + } + + except Exception as e: + logger.error(f"收集表元数据失败: {str(e)}") + return { + 'success': False, + 'message': str(e) + } + + async def _collect_single_table_metadata( + self, + user_id: int, + table_name: str, + db_type: str + ) -> Dict[str, Any]: + """收集单个表的元数据""" + + # 根据数据库类型选择相应的工具 + if db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + raise Exception(f"不支持的数据库类型: {db_type}") + + # 获取表结构 + schema_result = await db_tool.execute( + operation="describe_table", + user_id=str(user_id), + table_name=table_name + ) + + if not schema_result.success: + raise Exception(f"获取表结构失败: {schema_result.error}") + + schema_data = schema_result.result + + # 获取示例数据(前5条) + sample_result = await db_tool.execute( + operation="execute_query", + user_id=str(user_id), + sql_query=f"SELECT * FROM {table_name} LIMIT 5", + limit=5 + ) + + sample_data = [] + if sample_result.success: + sample_data = sample_result.result.get('data', []) + + # 获取行数统计 + count_result = await db_tool.execute( + operation="execute_query", + user_id=str(user_id), + sql_query=f"SELECT COUNT(*) as total_rows FROM {table_name}", + limit=1 + ) + + row_count = 0 + if count_result.success and count_result.result.get('data'): + row_count = count_result.result['data'][0].get('total_rows', 0) + + return { + 'columns_info': schema_data.get('columns', []), + 'primary_keys': schema_data.get('primary_keys', []), + 'foreign_keys': schema_data.get('foreign_keys', []), + 'indexes': schema_data.get('indexes', []), + 'sample_data': sample_data, + 'row_count': row_count, + 'table_comment': schema_data.get('table_comment', '') + } + + async def _save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_name: str, + metadata: Dict[str, Any] + ) -> TableMetadata: + """保存表元数据""" + + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.columns_info = metadata['columns_info'] + existing.primary_keys = metadata['primary_keys'] + existing.foreign_keys = metadata['foreign_keys'] + existing.indexes = metadata['indexes'] + existing.sample_data = metadata['sample_data'] + existing.row_count = metadata['row_count'] + existing.table_comment = metadata['table_comment'] + existing.last_synced_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(existing) + return existing + else: + # 创建新记录 + table_metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', + table_type='BASE TABLE', + table_comment=metadata['table_comment'], + columns_info=metadata['columns_info'], + primary_keys=metadata['primary_keys'], + foreign_keys=metadata['foreign_keys'], + indexes=metadata['indexes'], + sample_data=metadata['sample_data'], + row_count=metadata['row_count'], + is_enabled_for_qa=True, + last_synced_at=datetime.utcnow() + ) + + self.db.add(table_metadata) + self.db.commit() + self.db.refresh(table_metadata) + return table_metadata + + async def save_table_metadata_config( + self, + user_id: int, + database_config_id: int, + table_names: List[str] + ) -> Dict[str, Any]: + """保存表元数据配置(简化版,只保存基本信息)""" + try: + # 获取数据库配置 + db_config = self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == database_config_id, DatabaseConfig.user_id == user_id) + ).first() + + if not db_config: + raise NotFoundError("数据库配置不存在") + + saved_tables = [] + failed_tables = [] + + for table_name in table_names: + try: + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.user_id == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.is_enabled_for_qa = True + existing.last_synced_at = datetime.utcnow() + saved_tables.append({ + 'table_name': table_name, + 'action': 'updated' + }) + else: + # 创建新记录 + metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', # 默认值 + table_type='table', # 默认值 + table_comment='', + columns_count=0, # 后续可通过collect接口更新 + row_count=0, # 后续可通过collect接口更新 + is_enabled_for_qa=True, + qa_description='', + business_context='', + sample_data='{}', + column_info='{}', + last_synced_at=datetime.utcnow() + ) + + self.db.add(metadata) + saved_tables.append({ + 'table_name': table_name, + 'action': 'created' + }) + + except Exception as e: + logger.error(f"保存表 {table_name} 配置失败: {str(e)}") + failed_tables.append({ + 'table_name': table_name, + 'error': str(e) + }) + + # 提交事务 + self.db.commit() + + return { + 'saved_tables': saved_tables, + 'failed_tables': failed_tables, + 'total_saved': len(saved_tables), + 'total_failed': len(failed_tables) + } + + except Exception as e: + self.db.rollback() + logger.error(f"保存表元数据配置失败: {str(e)}") + raise e + + def get_user_table_metadata( + self, + user_id: int, + database_config_id: Optional[int] = None + ) -> List[TableMetadata]: + """获取用户的表元数据列表""" + query = self.db.query(TableMetadata).filter(TableMetadata.created_by == user_id) + + if database_config_id: + query = query.filter(TableMetadata.database_config_id == database_config_id) + else: + raise NotFoundError("数据库配置不存在") + return query.filter(TableMetadata.is_enabled_for_qa == True).all() + + def get_table_metadata_by_name( + self, + user_id: int, + database_config_id: int, + table_name: str + ) -> Optional[TableMetadata]: + """根据表名获取表元数据""" + return self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + def update_table_qa_settings( + self, + user_id: int, + metadata_id: int, + settings: Dict[str, Any] + ) -> bool: + """更新表的问答设置""" + try: + metadata = self.db.query(TableMetadata).filter( + and_( + TableMetadata.id == metadata_id, + TableMetadata.created_by == user_id + ) + ).first() + + if not metadata: + return False + + if 'is_enabled_for_qa' in settings: + metadata.is_enabled_for_qa = settings['is_enabled_for_qa'] + if 'qa_description' in settings: + metadata.qa_description = settings['qa_description'] + if 'business_context' in settings: + metadata.business_context = settings['business_context'] + + self.db.commit() + return True + + except Exception as e: + logger.error(f"更新表问答设置失败: {str(e)}") + self.db.rollback() + return False + + def save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_name: str, + columns_info: List[Dict[str, Any]], + primary_keys: List[str], + row_count: int, + table_comment: str = '' + ) -> TableMetadata: + """保存单个表的元数据""" + try: + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.columns_info = columns_info + existing.primary_keys = primary_keys + existing.row_count = row_count + existing.table_comment = table_comment + existing.last_synced_at = datetime.utcnow() + self.db.commit() + return existing + else: + # 创建新记录 + metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', + table_type='BASE TABLE', + table_comment=table_comment, + columns_info=columns_info, + primary_keys=primary_keys, + row_count=row_count, + is_enabled_for_qa=True, + last_synced_at=datetime.utcnow() + ) + + self.db.add(metadata) + self.db.commit() + self.db.refresh(metadata) + return metadata + + except Exception as e: + logger.error(f"保存表元数据失败: {str(e)}") + self.db.rollback() + raise e + + def _decrypt_password(self, encrypted_password: str) -> str: + """解密密码(需要实现加密逻辑)""" + # 这里需要实现与DatabaseConfigService相同的解密逻辑 + # 暂时返回原始密码 + return encrypted_password \ No newline at end of file diff --git a/backend/th_agenter/services/tools/__init__.py b/backend/th_agenter/services/tools/__init__.py new file mode 100644 index 0000000..e2077e3 --- /dev/null +++ b/backend/th_agenter/services/tools/__init__.py @@ -0,0 +1,36 @@ +"""Agent tools package.""" + +from .weather import WeatherQueryTool +from .search import TavilySearchTool +from .datetime_tool import DateTimeTool + +# Try to import PostgreSQL MCP tool +try: + from th_agenter.services.mcp.postgresql_mcp import PostgreSQLMCPTool +except ImportError: + PostgreSQLMCPTool = None + +# Try to import MySQL MCP tool +try: + from th_agenter.services.mcp.mysql_mcp import MySQLMCPTool +except ImportError: + MySQLMCPTool = None + + +# Try to import LangChain native tools if available +try: + from .langchain_native_tools import LANGCHAIN_NATIVE_TOOLS +except ImportError: + LANGCHAIN_NATIVE_TOOLS = [] + +__all__ = [ + 'WeatherQueryTool', + 'TavilySearchTool', + 'DateTimeTool', +] + ([ + 'PostgreSQLMCPTool', +] if PostgreSQLMCPTool is not None else []) + ([ + 'MySQLMCPTool', +] if MySQLMCPTool is not None else []) + [ + 'LANGCHAIN_NATIVE_TOOLS' +] \ No newline at end of file diff --git a/backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4ed3947 Binary files /dev/null and b/backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/services/tools/__pycache__/datetime_tool.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/datetime_tool.cpython-313.pyc new file mode 100644 index 0000000..8e9b667 Binary files /dev/null and b/backend/th_agenter/services/tools/__pycache__/datetime_tool.cpython-313.pyc differ diff --git a/backend/th_agenter/services/tools/__pycache__/search.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/search.cpython-313.pyc new file mode 100644 index 0000000..cdbd3be Binary files /dev/null and b/backend/th_agenter/services/tools/__pycache__/search.cpython-313.pyc differ diff --git a/backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc new file mode 100644 index 0000000..b6056dd Binary files /dev/null and b/backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc differ diff --git a/backend/th_agenter/services/tools/datetime_tool.py b/backend/th_agenter/services/tools/datetime_tool.py new file mode 100644 index 0000000..65bf120 --- /dev/null +++ b/backend/th_agenter/services/tools/datetime_tool.py @@ -0,0 +1,186 @@ +from pydantic import BaseModel, Field +from typing import Optional, Type, Literal, ClassVar +import datetime +import pytz +import logging + +logger = logging.getLogger("datetime_tool") + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. DateTimeTool will be disabled.") + BaseTool = None + +# 定义输入参数模型(使用Pydantic替代原get_parameters()) +class DateTimeInput(BaseModel): + operation: Literal["current_time", "timezone_convert", "date_diff", "add_time", "format_date"] = Field( + description="操作类型: current_time(当前时间), timezone_convert(时区转换), " + "date_diff(日期差), add_time(时间加减), format_date(格式化日期)" + ) + timezone: Optional[str] = Field( + default="UTC", + description="时区名称 (e.g., 'UTC', 'Asia/Shanghai')" + ) + date_string: Optional[str] = Field( + description="日期字符串 (格式: YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)" + ) + target_timezone: Optional[str] = Field( + description="目标时区(用于时区转换)" + ) + days: Optional[int] = Field( + default=0, + description="要加减的天数" + ) + hours: Optional[int] = Field( + default=0, + description="要加减的小时数" + ) + format: Optional[str] = Field( + default="%Y-%m-%d %H:%M:%S", + description="日期格式字符串 (e.g., '%Y-%m-%d %H:%M:%S')" + ) + +class DateTimeTool(BaseTool): + """日期时间操作工具(支持时区转换、日期计算等)""" + + name: ClassVar[str] = "datetime_tool" + description: ClassVar[str] = """执行日期时间相关操作,包括: + - 获取当前时间 + - 时区转换 + - 计算日期差 + - 日期时间加减 + - 格式化日期 + 使用时必须指定operation参数确定操作类型。""" + args_schema: Type[BaseModel] = DateTimeInput + + def _parse_datetime(self, date_string: str, timezone_str: str = "UTC") -> datetime.datetime: + """解析日期字符串(私有方法)""" + tz = pytz.timezone(timezone_str) + formats = [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%Y/%m/%d %H:%M:%S", + "%Y/%m/%d", + "%d/%m/%Y %H:%M:%S", + "%d/%m/%Y", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y" + ] + + for fmt in formats: + try: + dt = datetime.datetime.strptime(date_string, fmt) + return tz.localize(dt) + except ValueError: + continue + raise ValueError(f"无法解析日期字符串: {date_string}") + + def _run(self, + operation: str, + timezone: str = "UTC", + date_string: Optional[str] = None, + target_timezone: Optional[str] = None, + days: int = 0, + hours: int = 0, + format: str = "%Y-%m-%d %H:%M:%S") -> dict: + """同步执行日期时间操作""" + logger.info(f"执行日期时间操作: {operation}") + + try: + if operation == "current_time": + tz = pytz.timezone(timezone) + now = datetime.datetime.now(tz) + return { + "status": "success", + "result": { + "formatted": now.strftime(format), + "iso": now.isoformat(), + "timestamp": now.timestamp(), + "timezone": timezone + }, + "summary": f"当前时间 ({timezone}): {now.strftime(format)}" + } + + elif operation == "timezone_convert": + if not date_string or not target_timezone: + raise ValueError("必须提供date_string和target_timezone参数") + + source_dt = self._parse_datetime(date_string, timezone) + target_dt = source_dt.astimezone(pytz.timezone(target_timezone)) + + return { + "status": "success", + "result": { + "source": source_dt.strftime(format), + "target": target_dt.strftime(format), + "source_tz": timezone, + "target_tz": target_timezone + }, + "summary": f"时区转换: {source_dt.strftime(format)} → {target_dt.strftime(format)}" + } + + elif operation == "date_diff": + if not date_string: + raise ValueError("必须提供date_string参数") + + target_dt = self._parse_datetime(date_string, timezone) + current_dt = datetime.datetime.now(pytz.timezone(timezone)) + delta = target_dt - current_dt + + return { + "status": "success", + "result": { + "days": delta.days, + "hours": delta.seconds // 3600, + "total_seconds": delta.total_seconds(), + "is_future": delta.days > 0 + }, + "summary": f"日期差: {abs(delta.days)}天 {delta.seconds//3600}小时" + } + + elif operation == "add_time": + base_dt = self._parse_datetime(date_string, timezone) if date_string \ + else datetime.datetime.now(pytz.timezone(timezone)) + new_dt = base_dt + datetime.timedelta(days=days, hours=hours) + + return { + "status": "success", + "result": { + "original": base_dt.strftime(format), + "new": new_dt.strftime(format), + "delta": f"{days}天 {hours}小时" + }, + "summary": f"时间计算: {base_dt.strftime(format)} + {days}天 {hours}小时 = {new_dt.strftime(format)}" + } + + elif operation == "format_date": + dt = self._parse_datetime(date_string, timezone) if date_string \ + else datetime.datetime.now(pytz.timezone(timezone)) + formatted = dt.strftime(format) + + return { + "status": "success", + "result": { + "original": dt.isoformat(), + "formatted": formatted + }, + "summary": f"格式化结果: {formatted}" + } + + else: + raise ValueError(f"未知操作类型: {operation}") + + except Exception as e: + logger.error(f"操作失败: {str(e)}") + return { + "status": "error", + "message": str(e), + "operation": operation + } + + async def _arun(self, **kwargs): + """异步执行""" + return self._run(**kwargs) \ No newline at end of file diff --git a/backend/th_agenter/services/tools/search.py b/backend/th_agenter/services/tools/search.py new file mode 100644 index 0000000..9c91512 --- /dev/null +++ b/backend/th_agenter/services/tools/search.py @@ -0,0 +1,92 @@ +"""基于TavilySearch的搜索工具""" + +from th_agenter.core.config import get_settings +from th_agenter.utils.logger import get_logger + +logger = get_logger("search_tool") + +from pydantic import BaseModel, Field, PrivateAttr +from typing import Optional, Type, ClassVar +import logging + +logger = logging.getLogger(__name__) + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. SearchTool will be disabled.") + BaseTool = None + +# Try to import TavilySearchResults +try: + from langchain_community.tools.tavily_search import TavilySearchResults +except ImportError: + logger.warning("langchain_community not available. TavilySearchTool will be disabled.") + TavilySearchResults = None + + +# 定义输入参数模型(替代原get_parameters()) +class SearchInput(BaseModel): + query: str = Field(description="搜索查询内容") + max_results: Optional[int] = Field( + default=5, + description="返回结果的最大数量(默认:5)" + ) + topic: Optional[str] = Field( + default="general", + description="搜索主题,可选值:general, academic, news, places" + ) + + +class TavilySearchTool(BaseTool): + name:ClassVar[str] = "tavily_search_tool" + description:ClassVar[str] = """使用Tavily搜索引擎进行网络搜索,可以获取最新信息。 + 输入应该包含搜索查询(query),可选参数包括max_results和topic。""" # 替代get_description() + args_schema: Type[BaseModel] = SearchInput # 用Pydantic模型定义参数 + _tavily_api_key: str = PrivateAttr() + _search_client: TavilySearchResults = PrivateAttr() + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._tavily_api_key = get_settings().tool.tavily_api_key + if not self._tavily_api_key: + raise ValueError("Tavily API key not found in settings") + + # 初始化Tavily客户端 + self._search_client = TavilySearchResults( + tavily_api_key=self._tavily_api_key + ) + + def _run(self, query: str, max_results: int = 5, topic: str = "general"): + try: + logger.info(f"执行搜索:{query}") + # 调用Tavily(LangChain已内置Tavily工具,这里直接使用) + results = self._search_client.run({ + "query": query, + "max_results": max_results, + "topic": topic + }) + + # 格式化结果(根据Tavily的实际返回结构调整) + if isinstance(results, list): + return { + "status": "success", + "results": [ + { + "title": r.get("title", ""), + "url": r.get("url", ""), + "content": r.get("content", "")[:200] + "..." + } for r in results + ] + } + else: + return {"status": "error", "message": "Unexpected result format"} + + except Exception as e: + logger.error(f"搜索失败: {str(e)}") + return {"status": "error", "message": str(e)} + + async def _arun(self, **kwargs): + """异步版本""" + """直接调用同步版本""" + return self._run(**kwargs) # 直接委托给同步方法 \ No newline at end of file diff --git a/backend/th_agenter/services/tools/weather.py b/backend/th_agenter/services/tools/weather.py new file mode 100644 index 0000000..3d345c4 --- /dev/null +++ b/backend/th_agenter/services/tools/weather.py @@ -0,0 +1,85 @@ +from pydantic import BaseModel, Field, PrivateAttr +from typing import Optional, Type, ClassVar +import requests +import logging +from th_agenter.core.config import get_settings + +logger = logging.getLogger("weather_tool") + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. WeatherQueryTool will be disabled.") + BaseTool = None + +# 定义输入参数模型(替代原get_parameters()) +class WeatherInput(BaseModel): + location: str = Field( + description="城市名称,例如:'北京',只能是单个城市", + examples=["北京", "上海", "New York"] + ) + +class WeatherQueryTool(BaseTool): + """心知天气API查询工具(LangChain标准版)""" + name: ClassVar[str] = "weather_query_tool" + description: ClassVar[str] = """通过心知天气API查询实时天气数据。获取指定城市的当前天气信息,包括温度、湿度、天气状况等。""" + args_schema: Type[BaseModel] = WeatherInput # 参数规范 + # 使用PrivateAttr声明不参与验证的私有属性 + _api_key: str = PrivateAttr() + _base_params: dict = PrivateAttr() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._api_key = get_settings().tool.weather_api_key + if not self._api_key: + raise ValueError("Weather API key not found in settings") + + # 基础请求参数 + self._base_params = { + "key": self._api_key, + "language": "zh-Hans", + "unit": "c" + } + + def _run(self, location: str) -> dict: + """同步执行天气查询""" + try: + logger.info(f"查询天气 - 城市: {location}") + + # 构建API请求 + url = "https://api.seniverse.com/v3/weather/now.json" + params = {**self._base_params, "location": location} + + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + # 处理API响应 + if 'results' not in data: + error_msg = data.get('status', 'API返回格式异常') + raise ValueError(f"天气API错误: {error_msg}") + + weather = data['results'][0]['now'] + return { + "status": "success", + "location": location, + "temperature": weather["temperature"], + "condition": weather["text"], + "humidity": weather.get("humidity", "N/A"), + "wind": weather.get("wind_direction", "N/A"), + "full_data": weather + } + + except requests.exceptions.RequestException as e: + logger.error(f"网络请求失败: {str(e)}") + return {"status": "error", "message": f"网络错误: {str(e)}"} + except Exception as e: + logger.error(f"查询失败: {str(e)}") + return {"status": "error", "message": str(e)} + + async def _arun(self, location: str) -> dict: + """异步执行(示例实现)""" + # 实际项目中可以用aiohttp替换requests + return self._run(location) \ No newline at end of file diff --git a/backend/th_agenter/services/user.py b/backend/th_agenter/services/user.py new file mode 100644 index 0000000..a5c456f --- /dev/null +++ b/backend/th_agenter/services/user.py @@ -0,0 +1,282 @@ +"""User service for managing user operations.""" + +from typing import Optional, List, Tuple +from sqlalchemy.orm import Session +from sqlalchemy import or_, and_, desc + +from ..models.user import User +from ..utils.schemas import UserCreate, UserUpdate +from ..utils.exceptions import DatabaseError, ValidationError +from ..utils.logger import get_logger +from .auth import AuthService + +logger = get_logger(__name__) + + +class UserService: + """Service for user management operations.""" + + def __init__(self, db: Session): + self.db = db + + def get_password_hash(self, password: str) -> str: + """Hash a password.""" + return AuthService.get_password_hash(password) + + def verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return AuthService.verify_password(plain_password, hashed_password) + + def get_user_by_id(self, user_id: int) -> Optional[User]: + """Get user by ID.""" + try: + # Use options to avoid loading problematic relationships + from sqlalchemy.orm import noload + return self.db.query(User).options( + noload(User.roles) + ).filter(User.id == user_id).first() + except Exception as e: + logger.error(f"Error getting user by ID {user_id}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def get_user(self, user_id: int) -> Optional[User]: + """Get user by ID (alias for get_user_by_id).""" + return self.get_user_by_id(user_id) + + def get_user_by_email(self, email: str) -> Optional[User]: + """Get user by email.""" + try: + return self.db.query(User).filter(User.email == email).first() + except Exception as e: + logger.error(f"Error getting user by email {email}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def get_user_by_username(self, username: str) -> Optional[User]: + """Get user by username.""" + try: + return self.db.query(User).filter(User.username == username).first() + except Exception as e: + logger.error(f"Error getting user by username {username}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def create_user(self, user_data: UserCreate) -> User: + """Create a new user.""" + try: + # Validate input + if len(user_data.password) < 6: + raise ValidationError("Password must be at least 6 characters long") + if len(user_data.password) > 72: + raise ValidationError("Password cannot be longer than 72 characters") + + # Hash password + hashed_password = self.get_password_hash(user_data.password) + + # Create user + db_user = User( + username=user_data.username, + email=user_data.email, + hashed_password=hashed_password, + full_name=user_data.full_name, + is_active=True + ) + + # Set audit fields + db_user.set_audit_fields() + + self.db.add(db_user) + self.db.commit() + self.db.refresh(db_user) + + logger.info(f"User created successfully: {user_data.username}") + return db_user + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error creating user {user_data.username}: {e}") + raise DatabaseError(f"Failed to create user: {str(e)}") + + def update_user(self, user_id: int, user_update: UserUpdate) -> Optional[User]: + """Update user information.""" + try: + user = self.get_user_by_id(user_id) + if not user: + return None + + # Update fields + update_data = user_update.dict(exclude_unset=True) + + if "password" in update_data: + update_data["hashed_password"] = self.get_password_hash(update_data.pop("password")) + + for field, value in update_data.items(): + setattr(user, field, value) + + # Skip audit fields for now due to database schema mismatch + # user.set_audit_fields(is_update=True) + + self.db.commit() + self.db.refresh(user) + + logger.info(f"User updated successfully: {user.username}") + return user + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating user {user_id}: {e}") + raise DatabaseError(f"Failed to update user: {str(e)}") + + def get_users(self, skip: int = 0, limit: int = 100) -> List[User]: + """Get all users with pagination.""" + try: + return self.db.query(User).offset(skip).limit(limit).all() + except Exception as e: + logger.error(f"Error getting users: {e}") + raise DatabaseError(f"Failed to get users: {str(e)}") + + def get_users_with_filters( + self, + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + role_id: Optional[int] = None, + is_active: Optional[bool] = None + ) -> Tuple[List[User], int]: + """Get users with filters and return total count.""" + try: + query = self.db.query(User).order_by(desc('created_at')) + + # Apply filters + if search: + search_term = f"%{search}%" + query = query.filter( + or_( + User.username.ilike(search_term), + User.email.ilike(search_term), + User.full_name.ilike(search_term) + ) + ) + + if role_id is not None: + from ..models.permission import UserRole + query = query.join(UserRole).filter(UserRole.role_id == role_id) + + if is_active is not None: + query = query.filter(User.is_active == is_active) + + # Get total count + total = query.count() + + # Apply pagination + users = query.offset(skip).limit(limit).all() + + return users, total + + except Exception as e: + logger.error(f"Error getting users with filters: {e}") + raise DatabaseError(f"Failed to get users: {str(e)}") + + def delete_user(self, user_id: int) -> bool: + """Delete a user.""" + try: + user = self.get_user_by_id(user_id) + if not user: + return False + + # Manually delete related records to avoid cascade issues + from sqlalchemy import text + + # Delete user_roles records + self.db.execute(text("DELETE FROM user_roles WHERE user_id = :user_id"), {"user_id": user_id}) + + # Now delete the user + self.db.delete(user) + self.db.commit() + + logger.info(f"User deleted successfully: {user.username}") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting user {user_id}: {e}") + raise DatabaseError(f"Failed to delete user: {str(e)}") + + def authenticate_user(self, username: str, password: str) -> Optional[User]: + """Authenticate user with username and password.""" + try: + user = self.get_user_by_username(username) + if not user: + return None + + if not self.verify_password(password, user.hashed_password): + return None + + if not user.is_active: + return None + + return user + + except Exception as e: + logger.error(f"Error authenticating user {username}: {e}") + return None + + def change_password(self, user_id: int, current_password: str, new_password: str) -> bool: + """Change user password.""" + try: + user = self.get_user_by_id(user_id) + if not user: + raise ValidationError("User not found") + + # Verify current password + if not self.verify_password(current_password, user.hashed_password): + raise ValidationError("Current password is incorrect") + + # Validate new password + if len(new_password) < 6: + raise ValidationError("New password must be at least 6 characters long") + + # Hash new password + hashed_password = self.get_password_hash(new_password) + + # Update password + user.hashed_password = hashed_password + self.db.commit() + + logger.info(f"Password changed successfully for user: {user.username}") + return True + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error changing password for user {user_id}: {e}") + raise DatabaseError(f"Failed to change password: {str(e)}") + + def reset_password(self, user_id: int, new_password: str) -> bool: + """Reset user password (admin only, no current password required).""" + try: + user = self.get_user_by_id(user_id) + if not user: + raise ValidationError("User not found") + + # Validate new password + if len(new_password) < 6: + raise ValidationError("New password must be at least 6 characters long") + + # Hash new password + hashed_password = self.get_password_hash(new_password) + + # Update password + user.hashed_password = hashed_password + self.db.commit() + + logger.info(f"Password reset successfully for user: {user.username}") + return True + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error resetting password for user {user_id}: {e}") + raise DatabaseError(f"Failed to reset password: {str(e)}") \ No newline at end of file diff --git a/backend/th_agenter/services/workflow_engine.py b/backend/th_agenter/services/workflow_engine.py new file mode 100644 index 0000000..edb200e --- /dev/null +++ b/backend/th_agenter/services/workflow_engine.py @@ -0,0 +1,924 @@ +"""Workflow execution engine.""" + +import asyncio +import json +import time +from datetime import datetime +from typing import Dict, Any, Optional, List +from sqlalchemy.orm import Session + +from ..models.workflow import Workflow, WorkflowExecution, NodeExecution, ExecutionStatus, NodeType +from ..models.llm_config import LLMConfig +from ..services.llm_service import LLMService + +from ..db.database import get_db +from ..utils.logger import get_logger + +logger = get_logger("workflow_engine") + + +class WorkflowEngine: + """工作流执行引擎""" + + def __init__(self, db: Session): + self.db = db + self.llm_service = LLMService() + + + async def execute_workflow(self, workflow: Workflow, input_data: Optional[Dict[str, Any]] = None, + user_id: int = None, db: Session = None) -> 'WorkflowExecutionResponse': + """执行工作流""" + from ..schemas.workflow import WorkflowExecutionResponse + + if db: + self.db = db + + # 创建执行记录 + execution = WorkflowExecution( + workflow_id=workflow.id, + status=ExecutionStatus.RUNNING, + input_data=input_data or {}, + executor_id=user_id, + started_at=datetime.now().isoformat() + ) + execution.set_audit_fields(user_id) + + self.db.add(execution) + self.db.commit() + self.db.refresh(execution) + + + + try: + # 解析工作流定义 + definition = workflow.definition + nodes = {node['id']: node for node in definition['nodes']} + connections = definition['connections'] + + # 构建节点依赖图 + node_graph = self._build_node_graph(nodes, connections) + + # 执行工作流 + result = await self._execute_nodes(execution, nodes, node_graph, input_data or {}) + + # 更新执行状态 + execution.status = ExecutionStatus.COMPLETED + execution.output_data = result + execution.completed_at = datetime.now().isoformat() + + + + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}") + execution.status = ExecutionStatus.FAILED + execution.error_message = str(e) + execution.completed_at = datetime.now().isoformat() + + + + execution.set_audit_fields(user_id, is_update=True) + self.db.commit() + self.db.refresh(execution) + + return WorkflowExecutionResponse.from_orm(execution) + + async def execute_workflow_stream(self, workflow: 'Workflow', input_data: Optional[Dict[str, Any]] = None, + user_id: int = None, db: Session = None): + """流式执行工作流,实时推送节点状态""" + from ..schemas.workflow import WorkflowExecutionResponse + from typing import AsyncGenerator + + if db: + self.db = db + + # 创建执行记录 + execution = WorkflowExecution( + workflow_id=workflow.id, + status=ExecutionStatus.RUNNING, + input_data=input_data or {}, + executor_id=user_id, + started_at=datetime.now().isoformat() + ) + execution.set_audit_fields(user_id) + + self.db.add(execution) + self.db.commit() + self.db.refresh(execution) + + # 发送工作流开始执行的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'started', + 'data': { + "workflow_id": workflow.id, + "workflow_name": workflow.name, + "input_data": input_data or {}, + "started_at": execution.started_at + }, + 'timestamp': datetime.now().isoformat() + } + + try: + # 解析工作流定义 + definition = workflow.definition + nodes = {node['id']: node for node in definition['nodes']} + connections = definition['connections'] + + # 构建节点依赖图 + node_graph = self._build_node_graph(nodes, connections) + + # 执行工作流(流式版本) + result = None + async for step_data in self._execute_nodes_stream(execution, nodes, node_graph, input_data or {}): + yield step_data + # 如果是最终结果,保存它 + if step_data.get('type') == 'workflow_result': + result = step_data.get('data', {}) + + # 更新执行状态 + execution.status = ExecutionStatus.COMPLETED + execution.output_data = result + execution.completed_at = datetime.now().isoformat() + + # 发送工作流完成的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'completed', + 'data': { + "output_data": result, + "completed_at": execution.completed_at + }, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}") + execution.status = ExecutionStatus.FAILED + execution.error_message = str(e) + execution.completed_at = datetime.now().isoformat() + + # 发送工作流失败的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'failed', + 'data': { + "error_message": str(e), + "completed_at": execution.completed_at + }, + 'timestamp': datetime.now().isoformat() + } + + execution.set_audit_fields(user_id, is_update=True) + self.db.commit() + self.db.refresh(execution) + + def _build_node_graph(self, nodes: Dict[str, Any], connections: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: + """构建节点依赖图""" + graph = {} + + for node_id, node in nodes.items(): + graph[node_id] = { + 'node': node, + 'inputs': [], # 输入节点 + 'outputs': [] # 输出节点 + } + + for connection in connections: + # 支持两种字段名格式:from/to 和 from_node/to_node + from_node = connection.get('from') or connection.get('from_node') + to_node = connection.get('to') or connection.get('to_node') + + if from_node in graph and to_node in graph: + graph[from_node]['outputs'].append(to_node) + graph[to_node]['inputs'].append(from_node) + + return graph + + async def _execute_nodes(self, execution: WorkflowExecution, nodes: Dict[str, Any], + node_graph: Dict[str, Dict[str, Any]], workflow_input: Dict[str, Any]) -> Dict[str, Any]: + """执行节点""" + # 找到开始节点 + start_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'start'] + + if not start_nodes: + raise ValueError("未找到开始节点") + + if len(start_nodes) > 1: + raise ValueError("存在多个开始节点") + + start_node_id = start_nodes[0] + + # 执行上下文 + context = { + 'workflow_input': workflow_input, + 'node_outputs': {} + } + + # 从开始节点开始执行 + await self._execute_node_recursive(execution, start_node_id, node_graph, context) + + # 找到结束节点的输出作为工作流结果 + end_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'end'] + + if end_nodes: + end_node_id = end_nodes[0] + return context['node_outputs'].get(end_node_id, {}) + + return {} + + async def _execute_nodes_stream(self, execution: WorkflowExecution, nodes: Dict[str, Any], + node_graph: Dict[str, Dict[str, Any]], workflow_input: Dict[str, Any]): + """流式执行节点,实时推送节点状态""" + # 找到开始节点 + start_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'start'] + + if not start_nodes: + raise ValueError("未找到开始节点") + + if len(start_nodes) > 1: + raise ValueError("存在多个开始节点") + + start_node_id = start_nodes[0] + + # 执行上下文 + context = { + 'workflow_input': workflow_input, + 'node_outputs': {} + } + + # 从开始节点开始执行 + async for step_data in self._execute_node_recursive_stream(execution, start_node_id, node_graph, context): + yield step_data + + # 找到结束节点的输出作为工作流结果 + end_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'end'] + + if end_nodes: + end_node_id = end_nodes[0] + result = context['node_outputs'].get(end_node_id, {}) + else: + result = {} + + # 发送最终结果 + yield { + 'type': 'workflow_result', + 'execution_id': execution.id, + 'data': result, + 'timestamp': datetime.now().isoformat() + } + + async def _execute_node_recursive_stream(self, execution: WorkflowExecution, node_id: str, + node_graph: Dict[str, Dict[str, Any]], context: Dict[str, Any]): + """递归执行节点(流式版本)""" + if node_id in context['node_outputs']: + # 节点已执行过 + return + + node_info = node_graph[node_id] + node = node_info['node'] + + # 等待所有输入节点完成 + for input_node_id in node_info['inputs']: + async for step_data in self._execute_node_recursive_stream(execution, input_node_id, node_graph, context): + yield step_data + + # 发送节点开始执行的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'started', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'started_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + + try: + # 执行当前节点 + output = await self._execute_single_node(execution, node, context) + context['node_outputs'][node_id] = output + + # 发送节点完成的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'completed', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'output': output, + 'completed_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + # 发送节点失败的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'failed', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'error_message': str(e), + 'failed_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + raise + + # 执行所有输出节点 + for output_node_id in node_info['outputs']: + async for step_data in self._execute_node_recursive_stream(execution, output_node_id, node_graph, context): + yield step_data + + async def _execute_node_recursive(self, execution: WorkflowExecution, node_id: str, + node_graph: Dict[str, Dict[str, Any]], context: Dict[str, Any]): + """递归执行节点""" + if node_id in context['node_outputs']: + # 节点已执行过 + return + + node_info = node_graph[node_id] + node = node_info['node'] + + # 等待所有输入节点完成 + for input_node_id in node_info['inputs']: + await self._execute_node_recursive(execution, input_node_id, node_graph, context) + + # 执行当前节点 + output = await self._execute_single_node(execution, node, context) + context['node_outputs'][node_id] = output + + # 执行所有输出节点 + for output_node_id in node_info['outputs']: + await self._execute_node_recursive(execution, output_node_id, node_graph, context) + + async def _execute_single_node(self, execution: WorkflowExecution, node: Dict[str, Any], + context: Dict[str, Any]) -> Dict[str, Any]: + """执行单个节点""" + node_id = node['id'] + node_type = node['type'] + node_name = node['name'] + + # 创建节点执行记录 + node_execution = NodeExecution( + workflow_execution_id=execution.id, + node_id=node_id, + node_type=NodeType(node_type), + node_name=node_name, + status=ExecutionStatus.RUNNING, + started_at=datetime.now().isoformat() + ) + self.db.add(node_execution) + self.db.commit() + self.db.refresh(node_execution) + + start_time = time.time() + + try: + # 准备输入数据 + input_data = self._prepare_node_input(node, context) + + # 为前端显示准备输入数据 + display_input_data = input_data.copy() + + # 对于开始节点,显示的输入应该是workflow_input + if node_type == 'start': + display_input_data = input_data['workflow_input'] + elif node_type == 'llm': + # 对于LLM节点,先执行变量替换以获取处理后的提示词 + config = input_data['node_config'] + prompt_template = config.get('prompt', '') + enable_variable_substitution = config.get('enable_variable_substitution', True) + + if enable_variable_substitution: + processed_prompt = self._substitute_variables(prompt_template, input_data) + else: + processed_prompt = prompt_template + + display_input_data = { + 'original_prompt': prompt_template, + 'processed_prompt': processed_prompt, + 'model_config': config, + 'resolved_inputs': input_data.get('resolved_inputs', {}) + } + + node_execution.input_data = display_input_data + self.db.commit() + + + + # 根据节点类型执行 + if node_type == 'start': + output_data = await self._execute_start_node(node, input_data) + elif node_type == 'end': + output_data = await self._execute_end_node(node, input_data) + elif node_type == 'llm': + output_data = await self._execute_llm_node(node, input_data) + elif node_type == 'condition': + output_data = await self._execute_condition_node(node, input_data) + elif node_type == 'code': + output_data = await self._execute_code_node(node, input_data) + elif node_type == 'http': + output_data = await self._execute_http_node(node, input_data) + else: + raise ValueError(f"不支持的节点类型: {node_type}") + + # 更新执行状态 + end_time = time.time() + node_execution.status = ExecutionStatus.COMPLETED + node_execution.output_data = output_data + node_execution.completed_at = datetime.now().isoformat() + node_execution.duration_ms = int((end_time - start_time) * 1000) + + self.db.commit() + + + + return output_data + + except Exception as e: + logger.error(f"节点 {node_id} 执行失败: {str(e)}") + end_time = time.time() + node_execution.status = ExecutionStatus.FAILED + node_execution.error_message = str(e) + node_execution.completed_at = datetime.now().isoformat() + node_execution.duration_ms = int((end_time - start_time) * 1000) + self.db.commit() + + + + raise + + def _prepare_node_input(self, node: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: + """准备节点输入数据""" + # 基础输入数据 + input_data = { + 'workflow_input': context['workflow_input'], + 'node_config': node.get('config', {}), + 'previous_outputs': context['node_outputs'] + } + + # 处理节点参数配置 + node_parameters = node.get('parameters', {}) + if node_parameters and 'inputs' in node_parameters: + resolved_inputs = {} + + for param in node_parameters['inputs']: + param_name = param.get('name') + param_source = param.get('source', 'default') + param_default = param.get('default_value') + variable_name = param.get('variable_name', '') + + # 优先使用variable_name,如果存在的话 + if variable_name: + resolved_value = self._resolve_variable_value(variable_name, context) + resolved_inputs[param_name] = resolved_value if resolved_value is not None else param_default + elif param_source == 'workflow': + # 从工作流输入获取 + source_param_name = param.get('source_param_name', param_name) + resolved_inputs[param_name] = context['workflow_input'].get(source_param_name, param_default) + elif param_source == 'node': + # 从其他节点输出获取 + source_node_id = param.get('source_node_id') + source_param_name = param.get('source_param_name', 'data') + + if source_node_id and source_node_id in context['node_outputs']: + source_output = context['node_outputs'][source_node_id] + if isinstance(source_output, dict): + resolved_inputs[param_name] = source_output.get(source_param_name, param_default) + else: + resolved_inputs[param_name] = source_output + else: + resolved_inputs[param_name] = param_default + else: + # 使用默认值 + resolved_inputs[param_name] = param_default + + # 将解析后的参数添加到输入数据 + input_data['resolved_inputs'] = resolved_inputs + + return input_data + + def _resolve_variable_value(self, variable_name: str, context: Dict[str, Any]) -> Any: + """解析变量值,支持格式如 "node_id.output.field_name" 或更深层路径""" + try: + # 解析变量名格式:node_id.output.field_name 或 node_id.field1.field2.field3 + parts = variable_name.split('.') + if len(parts) >= 2: + source_node_id = parts[0] + + # 从previous_outputs中获取源节点的输出 + if source_node_id in context['node_outputs']: + source_output = context['node_outputs'][source_node_id] + + if isinstance(source_output, dict): + # 从第二个部分开始遍历路径 + current_value = source_output + for field_name in parts[1:]: + if isinstance(current_value, dict) and field_name in current_value: + current_value = current_value[field_name] + else: + # 如果路径不存在,返回None + return None + + return current_value + + return None + except Exception as e: + logger.warning(f"解析变量值失败: {variable_name}, 错误: {str(e)}") + return None + + async def _execute_start_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行开始节点""" + # 开始节点的输入和输出应该一致,都是workflow_input + workflow_input = input_data['workflow_input'] + return { + 'success': True, + 'message': '工作流开始', + 'data': workflow_input, + 'user_input': workflow_input # 添加用户输入显示 + } + + async def _execute_end_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行结束节点""" + previous_outputs = input_data.get('previous_outputs', {}) + + # 处理结束节点的输出参数配置 + node_parameters = node.get('parameters', {}) + output_params = node_parameters.get('outputs', []) + + result_data = {} + + # 根据输出参数配置获取对应的值 + for param in output_params: + param_name = param.get('name') + variable_name = param.get('variable_name') + + if variable_name: + # 解析variable_name,格式如: "node_1759022611056.output.response" + try: + parts = variable_name.split('.') + if len(parts) >= 3: + source_node_id = parts[0] + output_type = parts[1] # 通常是"output" + field_name = parts[2] # 具体的字段名,如"response" + + # 从前一个节点的输出中获取值 + if source_node_id in previous_outputs: + source_output = previous_outputs[source_node_id] + if isinstance(source_output, dict): + # 首先尝试从根级别获取字段(如LLM节点的response字段) + if field_name in source_output: + result_data[param_name] = source_output[field_name] + # 如果根级别没有,再尝试从data字段中获取 + elif 'data' in source_output and isinstance(source_output['data'], dict): + if field_name in source_output['data']: + result_data[param_name] = source_output['data'][field_name] + else: + result_data[param_name] = None + else: + result_data[param_name] = None + else: + result_data[param_name] = source_output + else: + result_data[param_name] = None + else: + # 格式不正确,使用默认值 + result_data[param_name] = param.get('default_value') + except Exception as e: + logger.warning(f"解析variable_name失败: {variable_name}, 错误: {str(e)}") + result_data[param_name] = param.get('default_value') + else: + # 没有variable_name,使用默认值 + result_data[param_name] = param.get('default_value') + + # 如果没有配置输出参数,返回简化的前一个节点输出(保持向后兼容) + if not output_params: + simplified_outputs = {} + for node_id, output in previous_outputs.items(): + if isinstance(output, dict): + simplified_outputs[node_id] = { + 'success': output.get('success', False), + 'message': output.get('message', ''), + 'data': output.get('data', {}) if not isinstance(output.get('data'), dict) or node_id not in str(output.get('data', {})) else {} + } + else: + simplified_outputs[node_id] = output + result_data = simplified_outputs + + return { + 'success': True, + 'message': '工作流结束', + 'data': result_data + } + + async def _execute_llm_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行LLM节点""" + config = input_data['node_config'] + + # 获取LLM配置 + model_id = config.get('model_id') + if not model_id: + # 兼容前端的model字段(可能是ID或名称) + model_value = config.get('model_name', config.get('model')) + if model_value: + # 如果是整数,直接作为ID使用 + if isinstance(model_value, int): + model_id = model_value + else: + # 如果是字符串,按名称查询 + llm_config = self.db.query(LLMConfig).filter(LLMConfig.model_name == model_value).first() + if llm_config: + model_id = llm_config.id + + if not model_id: + raise ValueError("未指定有效的大模型配置") + + llm_config = self.db.query(LLMConfig).filter(LLMConfig.id == model_id).first() + if not llm_config: + raise ValueError(f"大模型配置 {model_id} 不存在") + + # 准备提示词 + prompt_template = config.get('prompt', '') + + # 检查是否启用变量替换 + enable_variable_substitution = config.get('enable_variable_substitution', True) + + if enable_variable_substitution: + # 使用增强的变量替换 + prompt = self._substitute_variables(prompt_template, input_data) + else: + prompt = prompt_template + + # 记录处理后的提示词到输入数据中,用于前端显示 + input_data['processed_prompt'] = prompt + input_data['original_prompt'] = prompt_template + + # 调用LLM服务 + try: + response = await self.llm_service.chat_completion( + model_config=llm_config, + messages=[{"role": "user", "content": prompt}], + temperature=config.get('temperature', 0.7), + max_tokens=config.get('max_tokens') + ) + + return { + 'success': True, + 'response': response, + 'prompt': prompt, + 'model': llm_config.model_name, + 'tokens_used': getattr(response, 'usage', {}).get('total_tokens', 0) if hasattr(response, 'usage') else 0 + } + + except Exception as e: + logger.error(f"LLM调用失败: {str(e)}") + raise ValueError(f"LLM调用失败: {str(e)}") + + def _substitute_variables(self, template: str, input_data: Dict[str, Any]) -> str: + """变量替换函数""" + import re + + # 获取解析后的输入参数 + resolved_inputs = input_data.get('resolved_inputs', {}) + + # 获取工作流输入数据 + # input_data['workflow_input'] 包含了用户输入的参数 + workflow_input = input_data.get('workflow_input', {}) + + # 构建变量上下文 + variable_context = {} + + # 首先添加解析后的参数 + variable_context.update(resolved_inputs) + + # 添加工作流输入的顶层字段 + variable_context.update(workflow_input) + + # 如果 workflow_input 包含 user_input 字段,将其内容提升到顶层 + if 'user_input' in workflow_input and isinstance(workflow_input['user_input'], dict): + variable_context.update(workflow_input['user_input']) + + # 添加前一个节点的输出(简化访问) + for node_id, output in input_data.get('previous_outputs', {}).items(): + if isinstance(output, dict): + # 添加节点输出的直接访问 + variable_context[f'node_{node_id}'] = output.get('data', output) + # 如果输出有response字段,也添加直接访问 + if 'response' in output: + variable_context[f'node_{node_id}_response'] = output['response'] + + # 调试日志:打印变量上下文 + logger.info(f"变量替换上下文: {variable_context}") + logger.info(f"原始模板: {template}") + + # 使用正则表达式替换变量 {{variable_name}} 和 {variable_name} + def replace_variable(match): + var_name = match.group(1) + replacement = variable_context.get(var_name, match.group(0)) + logger.info(f"替换变量 {match.group(0)} -> {replacement}") + return str(replacement) + + # 首先替换 {{variable_name}} 格式的变量 + result = re.sub(r'\{\{([^}]+)\}\}', replace_variable, template) + # 然后替换 {variable_name} 格式的变量 + result = re.sub(r'\{([^}]+)\}', replace_variable, result) + + logger.info(f"替换后结果: {result}") + return result + + async def _execute_condition_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行条件节点""" + config = input_data['node_config'] + condition = config.get('condition', '') + + # 简单的条件评估(生产环境需要更安全的实现) + try: + # 构建评估上下文 + eval_context = { + 'input': input_data['workflow_input'], + 'previous': input_data['previous_outputs'] + } + + # 评估条件 + result = eval(condition, {"__builtins__": {}}, eval_context) + + return { + 'success': True, + 'condition': condition, + 'result': bool(result) + } + + except Exception as e: + logger.error(f"条件评估失败: {str(e)}") + raise ValueError(f"条件评估失败: {str(e)}") + + async def _execute_code_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行代码节点""" + config = input_data['node_config'] + language = config.get('language', 'python') + code = config.get('code', '') + + if language == 'python': + # 执行Python代码 + execution_result = await self._execute_python_code(code, input_data) + + # 处理输出参数配置 + node_parameters = node.get('parameters', {}) + if node_parameters and 'outputs' in node_parameters: + output_params = node_parameters['outputs'] + code_result = execution_result.get('result', {}) + + # 根据输出参数配置构建最终输出 + final_output = { + 'success': execution_result['success'], + 'code': execution_result['code'], + 'input_parameters': execution_result.get('input_parameters', {}) + } + + # 如果代码返回的是字典,根据输出参数配置提取对应字段 + if isinstance(code_result, dict): + for output_param in output_params: + param_name = output_param.get('name') + if param_name and param_name in code_result: + final_output[param_name] = code_result[param_name] + else: + # 如果代码返回的不是字典,且只有一个输出参数,直接使用返回值 + if len(output_params) == 1: + param_name = output_params[0].get('name') + if param_name: + final_output[param_name] = code_result + + return final_output + else: + # 如果没有输出参数配置,返回原始结果 + return execution_result + else: + raise ValueError(f"不支持的代码语言: {language}") + + async def _execute_python_code(self, code: str, input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行Python代码""" + try: + # 构建执行上下文 + safe_builtins = { + 'len': len, + 'str': str, + 'int': int, + 'float': float, + 'bool': bool, + 'list': list, + 'dict': dict, + 'tuple': tuple, + 'set': set, + 'range': range, + 'enumerate': enumerate, + 'zip': zip, + 'sum': sum, + 'min': min, + 'max': max, + 'abs': abs, + 'round': round, + 'sorted': sorted, + 'reversed': reversed, + 'print': print, + '__import__': __import__, + } + + # 导入常用模块 + import json + import datetime + import math + import re + + exec_context = { + '__builtins__': safe_builtins, + 'json': json, # 允许使用json模块 + 'datetime': datetime, # 允许使用datetime模块 + 'math': math, # 允许使用math模块 + 're': re, # 允许使用re模块 + } + + # 执行代码以定义函数 + exec(code, exec_context) + + # 检查是否定义了main函数 + if 'main' not in exec_context: + raise ValueError("代码中必须定义一个main函数") + + main_function = exec_context['main'] + + # 获取已解析的输入参数 + resolved_inputs = input_data.get('resolved_inputs', {}) + + # 调用main函数并传递参数 + if resolved_inputs: + # 使用解析后的输入参数调用main函数 + result = main_function(**resolved_inputs) + else: + # 如果没有输入参数,直接调用main函数 + result = main_function() + + return { + 'success': True, + 'result': result, + 'code': code, + 'input_parameters': resolved_inputs + } + + except Exception as e: + logger.error(f"Python代码执行失败: {str(e)}") + raise ValueError(f"Python代码执行失败: {str(e)}") + + async def _execute_http_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行HTTP请求节点""" + import aiohttp + + config = input_data['node_config'] + method = config.get('method', 'GET').upper() + url = config.get('url', '') + headers = config.get('headers', {}) + body = config.get('body') + + try: + async with aiohttp.ClientSession() as session: + async with session.request( + method=method, + url=url, + headers=headers, + data=body + ) as response: + response_text = await response.text() + + return { + 'success': True, + 'status_code': response.status, + 'response': response_text, + 'headers': dict(response.headers) + } + + except Exception as e: + logger.error(f"HTTP请求失败: {str(e)}") + raise ValueError(f"HTTP请求失败: {str(e)}") + + +# 工作流引擎实例 +def get_workflow_engine(db: Session = None) -> WorkflowEngine: + """获取工作流引擎实例""" + if db is None: + db = next(get_db()) + return WorkflowEngine(db) \ No newline at end of file diff --git a/backend/th_agenter/services/zhipu_embeddings.py b/backend/th_agenter/services/zhipu_embeddings.py new file mode 100644 index 0000000..249976f --- /dev/null +++ b/backend/th_agenter/services/zhipu_embeddings.py @@ -0,0 +1,74 @@ +"""Custom ZhipuAI Embeddings using OpenAI compatible API.""" + +import asyncio +from typing import List, Optional +from openai import OpenAI +from langchain_core.embeddings import Embeddings +from ..core.config import settings +from ..utils.logger import get_logger + +logger = get_logger("zhipu_embeddings") + + +class ZhipuOpenAIEmbeddings(Embeddings): + """ZhipuAI Embeddings using OpenAI compatible API.""" + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = "https://open.bigmodel.cn/api/paas/v4", + model: str = "embedding-3", + dimensions: int = 1024 + ): + self.api_key = api_key or settings.embedding.zhipu_api_key + self.base_url = base_url + self.model = model + self.dimensions = dimensions + + self.client = OpenAI( + api_key=self.api_key, + base_url=self.base_url + ) + + logger.info(f"ZhipuOpenAI Embeddings initialized with model: {self.model}") + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed search docs.""" + try: + embeddings = [] + for text in texts: + response = self.client.embeddings.create( + model=self.model, + input=text, + dimensions=self.dimensions, + encoding_format="float" + ) + embeddings.append(response.data[0].embedding) + return embeddings + except Exception as e: + logger.error(f"Error embedding documents: {e}") + raise + + def embed_query(self, text: str) -> List[float]: + """Embed query text.""" + try: + response = self.client.embeddings.create( + model=self.model, + input=text, + dimensions=self.dimensions, + encoding_format="float" + ) + return response.data[0].embedding + except Exception as e: + logger.error(f"Error embedding query: {e}") + raise + + async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + """Async embed search docs.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.embed_documents, texts) + + async def aembed_query(self, text: str) -> List[float]: + """Async embed query text.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.embed_query, text) \ No newline at end of file diff --git a/backend/th_agenter/utils/__init__.py b/backend/th_agenter/utils/__init__.py new file mode 100644 index 0000000..0f68c12 --- /dev/null +++ b/backend/th_agenter/utils/__init__.py @@ -0,0 +1,30 @@ +"""Utilities package for the chat agent application.""" + +from .logger import get_logger, setup_logger +from .exceptions import ( + ChatAgentException, + ValidationError, + AuthenticationError, + AuthorizationError, + NotFoundError, + ConversationNotFoundError, + UserNotFoundError, + ChatServiceError, + OpenAIError, + DatabaseError +) + +__all__ = [ + "get_logger", + "setup_logger", + "ChatAgentException", + "ValidationError", + "AuthenticationError", + "AuthorizationError", + "NotFoundError", + "ConversationNotFoundError", + "UserNotFoundError", + "ChatServiceError", + "OpenAIError", + "DatabaseError" +] \ No newline at end of file diff --git a/backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..39a423d Binary files /dev/null and b/backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc new file mode 100644 index 0000000..9b73bbe Binary files /dev/null and b/backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc differ diff --git a/backend/th_agenter/utils/__pycache__/file_utils.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/file_utils.cpython-313.pyc new file mode 100644 index 0000000..fe15b49 Binary files /dev/null and b/backend/th_agenter/utils/__pycache__/file_utils.cpython-313.pyc differ diff --git a/backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000..01bc75c Binary files /dev/null and b/backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc differ diff --git a/backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000..2a33bdd Binary files /dev/null and b/backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc differ diff --git a/backend/th_agenter/utils/exceptions.py b/backend/th_agenter/utils/exceptions.py new file mode 100644 index 0000000..dc705c6 --- /dev/null +++ b/backend/th_agenter/utils/exceptions.py @@ -0,0 +1,170 @@ +"""Custom exceptions and error handlers for the chat agent application.""" + +from typing import Any, Dict, Optional +from fastapi import HTTPException, Request +from fastapi.responses import JSONResponse +from starlette.status import ( + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_500_INTERNAL_SERVER_ERROR, +) + +from .logger import get_logger + +logger = get_logger("exceptions") + + +class ChatAgentException(Exception): + """Base exception for chat agent application.""" + + def __init__( + self, + message: str, + status_code: int = HTTP_500_INTERNAL_SERVER_ERROR, + details: Optional[Dict[str, Any]] = None + ): + self.message = message + self.status_code = status_code + self.details = details or {} + super().__init__(self.message) + + +class ValidationError(ChatAgentException): + """Validation error exception.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + super().__init__(message, HTTP_422_UNPROCESSABLE_ENTITY, details) + + +class AuthenticationError(ChatAgentException): + """Authentication error exception.""" + + def __init__(self, message: str = "Authentication failed"): + super().__init__(message, HTTP_401_UNAUTHORIZED) + + +class AuthorizationError(ChatAgentException): + """Authorization error exception.""" + + def __init__(self, message: str = "Access denied"): + super().__init__(message, HTTP_403_FORBIDDEN) + + +class NotFoundError(ChatAgentException): + """Resource not found exception.""" + + def __init__(self, message: str = "Resource not found"): + super().__init__(message, HTTP_404_NOT_FOUND) + + +class ConversationNotFoundError(NotFoundError): + """Conversation not found exception.""" + + def __init__(self, conversation_id: str): + super().__init__(f"Conversation with ID {conversation_id} not found") + + +class UserNotFoundError(NotFoundError): + """User not found exception.""" + + def __init__(self, user_id: str): + super().__init__(f"User with ID {user_id} not found") + + +class ChatServiceError(ChatAgentException): + """Chat service error exception.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + super().__init__(message, HTTP_500_INTERNAL_SERVER_ERROR, details) + + +class OpenAIError(ChatServiceError): + """OpenAI API error exception.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + super().__init__(f"OpenAI API error: {message}", details) + + +class RateLimitError(ChatAgentException): + """Rate limit exceeded error.""" + pass + + +class DatabaseError(ChatAgentException): + """Database operation error exception.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + super().__init__(f"Database error: {message}", details) + + +# Error handlers +async def chat_agent_exception_handler(request: Request, exc: ChatAgentException) -> JSONResponse: + """Handle ChatAgentException and its subclasses.""" + logger.error( + f"ChatAgentException: {exc.message}", + extra={ + "status_code": exc.status_code, + "details": exc.details, + "path": request.url.path, + "method": request.method + } + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "message": exc.message, + "type": exc.__class__.__name__, + "details": exc.details + } + } + ) + + +async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + """Handle HTTPException.""" + logger.warning( + f"HTTPException: {exc.detail}", + extra={ + "status_code": exc.status_code, + "path": request.url.path, + "method": request.method + } + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "message": exc.detail, + "type": "HTTPException" + } + } + ) + + +async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Handle general exceptions.""" + logger.error( + f"Unhandled exception: {str(exc)}", + extra={ + "exception_type": exc.__class__.__name__, + "path": request.url.path, + "method": request.method + }, + exc_info=True + ) + + return JSONResponse( + status_code=HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": { + "message": "Internal server error", + "type": "InternalServerError" + } + } + ) \ No newline at end of file diff --git a/backend/th_agenter/utils/file_utils.py b/backend/th_agenter/utils/file_utils.py new file mode 100644 index 0000000..79b36ce --- /dev/null +++ b/backend/th_agenter/utils/file_utils.py @@ -0,0 +1,191 @@ +"""File utilities.""" + +import os +import re +import hashlib +import mimetypes +from pathlib import Path +from typing import Optional, List, Dict, Any + + +class FileUtils: + """Utility class for file operations.""" + + # Allowed file extensions for document upload + ALLOWED_EXTENSIONS = { + '.txt', '.md', '.csv', # Text files + '.pdf', # PDF files + '.docx', '.doc', # Word documents + '.xlsx', '.xls', # Excel files + '.pptx', '.ppt', # PowerPoint files + '.rtf', # Rich text format + '.odt', '.ods', '.odp' # OpenDocument formats + } + + # MIME type mappings + MIME_TYPE_MAPPING = { + '.txt': 'text/plain', + '.md': 'text/markdown', + '.csv': 'text/csv', + '.pdf': 'application/pdf', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.doc': 'application/msword', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.ppt': 'application/vnd.ms-powerpoint', + '.rtf': 'application/rtf', + '.odt': 'application/vnd.oasis.opendocument.text', + '.ods': 'application/vnd.oasis.opendocument.spreadsheet', + '.odp': 'application/vnd.oasis.opendocument.presentation' + } + + @staticmethod + def sanitize_filename(filename: str) -> str: + """Sanitize filename to remove dangerous characters.""" + # Remove or replace dangerous characters + filename = re.sub(r'[<>:"/\\|?*]', '_', filename) + + # Remove leading/trailing spaces and dots + filename = filename.strip(' .') + + # Ensure filename is not empty + if not filename: + filename = 'unnamed_file' + + # Limit filename length + if len(filename) > 255: + name, ext = os.path.splitext(filename) + filename = name[:255-len(ext)] + ext + + return filename + + @staticmethod + def get_file_hash(file_path: str, algorithm: str = 'md5') -> str: + """Calculate file hash.""" + hash_func = hashlib.new(algorithm) + + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_func.update(chunk) + + return hash_func.hexdigest() + + @staticmethod + def get_file_info(file_path: str) -> Dict[str, Any]: + """Get comprehensive file information.""" + path = Path(file_path) + + if not path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + stat = path.stat() + + # Get MIME type + mime_type, encoding = mimetypes.guess_type(str(path)) + + return { + 'filename': path.name, + 'extension': path.suffix.lower(), + 'size_bytes': stat.st_size, + 'size_mb': round(stat.st_size / (1024 * 1024), 2), + 'mime_type': mime_type, + 'encoding': encoding, + 'created_at': stat.st_ctime, + 'modified_at': stat.st_mtime, + 'is_file': path.is_file(), + 'is_readable': os.access(path, os.R_OK) + } + + @staticmethod + def validate_file_extension(filename: str, allowed_extensions: Optional[List[str]] = None) -> bool: + """Validate file extension.""" + if allowed_extensions is None: + allowed_extensions = list(FileUtils.ALLOWED_EXTENSIONS) + + extension = Path(filename).suffix.lower() + return extension in allowed_extensions + + @staticmethod + def validate_file_size(file_size: int, max_size: int) -> bool: + """Validate file size.""" + return file_size <= max_size + + @staticmethod + def create_directory(directory_path: str) -> bool: + """Create directory if it doesn't exist.""" + try: + Path(directory_path).mkdir(parents=True, exist_ok=True) + return True + except Exception: + return False + + @staticmethod + def delete_file(file_path: str) -> bool: + """Safely delete a file.""" + try: + path = Path(file_path) + if path.exists() and path.is_file(): + path.unlink() + return True + return False + except Exception: + return False + + @staticmethod + def get_mime_type(filename: str) -> Optional[str]: + """Get MIME type for filename.""" + extension = Path(filename).suffix.lower() + return FileUtils.MIME_TYPE_MAPPING.get(extension) + + @staticmethod + def format_file_size(size_bytes: int) -> str: + """Format file size in human readable format.""" + if size_bytes == 0: + return "0 B" + + size_names = ["B", "KB", "MB", "GB", "TB"] + i = 0 + size = float(size_bytes) + + while size >= 1024.0 and i < len(size_names) - 1: + size /= 1024.0 + i += 1 + + return f"{size:.1f} {size_names[i]}" + + @staticmethod + def is_text_file(filename: str) -> bool: + """Check if file is a text file.""" + extension = Path(filename).suffix.lower() + return extension in {'.txt', '.md', '.csv', '.rtf'} + + @staticmethod + def is_pdf_file(filename: str) -> bool: + """Check if file is a PDF.""" + extension = Path(filename).suffix.lower() + return extension == '.pdf' + + @staticmethod + def is_office_file(filename: str) -> bool: + """Check if file is an Office document.""" + extension = Path(filename).suffix.lower() + return extension in {'.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt', '.odt', '.ods', '.odp'} + + @staticmethod + def get_file_category(filename: str) -> str: + """Get file category based on extension.""" + extension = Path(filename).suffix.lower() + + if extension in {'.txt', '.md', '.csv', '.rtf'}: + return 'text' + elif extension == '.pdf': + return 'pdf' + elif extension in {'.docx', '.doc', '.odt'}: + return 'document' + elif extension in {'.xlsx', '.xls', '.ods'}: + return 'spreadsheet' + elif extension in {'.pptx', '.ppt', '.odp'}: + return 'presentation' + else: + return 'unknown' \ No newline at end of file diff --git a/backend/th_agenter/utils/logger.py b/backend/th_agenter/utils/logger.py new file mode 100644 index 0000000..92c0b54 --- /dev/null +++ b/backend/th_agenter/utils/logger.py @@ -0,0 +1,68 @@ +"""Logging configuration for the chat agent application.""" + +import logging +import sys +from pathlib import Path +from typing import Optional + + +def setup_logger( + name: str = "th_agenter", + level: str = "DEBUG", + log_file: Optional[str] = None +) -> logging.Logger: + """Setup logger with console and optional file output. + + Args: + name: Logger name + level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + log_file: Optional log file path + + Returns: + Configured logger instance + """ + logger = logging.getLogger(name) + + # Clear existing handlers + logger.handlers.clear() + + # Set level + logger.setLevel(getattr(logging, level.upper())) + + # Create formatter + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # File handler (optional) + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = logging.FileHandler(log_path, encoding="utf-8") + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + + +# Default logger instance +logger = setup_logger() + + +def get_logger(name: str) -> logging.Logger: + """Get a logger instance with the specified name. + + Args: + name: Logger name + + Returns: + Logger instance + """ + return logging.getLogger(f"th_agenter.{name}") \ No newline at end of file diff --git a/backend/th_agenter/utils/node_parameters.py b/backend/th_agenter/utils/node_parameters.py new file mode 100644 index 0000000..3d950cd --- /dev/null +++ b/backend/th_agenter/utils/node_parameters.py @@ -0,0 +1,217 @@ +""" +节点参数默认配置工具 +""" +from typing import Dict, List +from ..schemas.workflow import NodeInputOutput, NodeParameter, ParameterType, NodeType + + +def get_default_node_parameters(node_type: NodeType) -> NodeInputOutput: + """获取节点类型的默认输入输出参数""" + + if node_type == NodeType.START: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="workflow_input", + type=ParameterType.OBJECT, + description="工作流初始输入数据", + required=False, + source="input" + ) + ], + outputs=[ + NodeParameter( + name="data", + type=ParameterType.OBJECT, + description="开始节点输出数据" + ) + ] + ) + + elif node_type == NodeType.END: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="final_result", + type=ParameterType.OBJECT, + description="最终结果数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="workflow_result", + type=ParameterType.OBJECT, + description="工作流最终输出" + ) + ] + ) + + elif node_type == NodeType.LLM: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="prompt_variables", + type=ParameterType.OBJECT, + description="Prompt中使用的变量", + required=False, + source="node" + ), + NodeParameter( + name="user_input", + type=ParameterType.STRING, + description="用户输入文本", + required=False, + source="input" + ) + ], + outputs=[ + NodeParameter( + name="response", + type=ParameterType.STRING, + description="LLM生成的回复" + ), + NodeParameter( + name="tokens_used", + type=ParameterType.NUMBER, + description="使用的token数量" + ) + ] + ) + + elif node_type == NodeType.CODE: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="input_data", + type=ParameterType.OBJECT, + description="代码执行的输入数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="result", + type=ParameterType.OBJECT, + description="代码执行结果" + ), + NodeParameter( + name="output", + type=ParameterType.STRING, + description="代码输出内容" + ) + ] + ) + + elif node_type == NodeType.HTTP: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="url_params", + type=ParameterType.OBJECT, + description="URL参数", + required=False, + source="node" + ), + NodeParameter( + name="request_body", + type=ParameterType.OBJECT, + description="请求体数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="response_data", + type=ParameterType.OBJECT, + description="HTTP响应数据" + ), + NodeParameter( + name="status_code", + type=ParameterType.NUMBER, + description="HTTP状态码" + ) + ] + ) + + elif node_type == NodeType.CONDITION: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="condition_data", + type=ParameterType.OBJECT, + description="条件判断的输入数据", + required=True, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="result", + type=ParameterType.BOOLEAN, + description="条件判断结果" + ), + NodeParameter( + name="branch", + type=ParameterType.STRING, + description="执行分支(true/false)" + ) + ] + ) + + else: + # 默认参数 + return NodeInputOutput( + inputs=[ + NodeParameter( + name="input", + type=ParameterType.OBJECT, + description="节点输入数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="output", + type=ParameterType.OBJECT, + description="节点输出数据" + ) + ] + ) + + +def validate_parameter_connections(nodes: List[Dict], connections: List[Dict]) -> List[str]: + """验证节点参数连接的有效性""" + errors = [] + node_dict = {node['id']: node for node in nodes} + + for node in nodes: + if 'parameters' not in node or not node['parameters']: + continue + + for input_param in node['parameters'].get('inputs', []): + if input_param.get('source') == 'node': + source_node_id = input_param.get('source_node_id') + source_field = input_param.get('source_field') + + if not source_node_id: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 缺少来源节点ID") + continue + + if source_node_id not in node_dict: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 引用了不存在的节点 {source_node_id}") + continue + + source_node = node_dict[source_node_id] + if 'parameters' in source_node and source_node['parameters']: + source_outputs = source_node['parameters'].get('outputs', []) + output_fields = [output['name'] for output in source_outputs] + + if source_field and source_field not in output_fields: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 引用了节点 {source_node['name']} 不存在的输出字段 {source_field}") + + return errors \ No newline at end of file diff --git a/backend/th_agenter/utils/schemas.py b/backend/th_agenter/utils/schemas.py new file mode 100644 index 0000000..2edf80a --- /dev/null +++ b/backend/th_agenter/utils/schemas.py @@ -0,0 +1,375 @@ +"""Pydantic schemas for API requests and responses.""" + +from typing import Optional, List, Any, Dict, TYPE_CHECKING +from datetime import datetime +from pydantic import BaseModel, Field +from enum import Enum + +if TYPE_CHECKING: + from ..schemas.permission import RoleResponse + + +class MessageRole(str, Enum): + """Message role enumeration.""" + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + + +class MessageType(str, Enum): + """Message type enumeration.""" + TEXT = "text" + IMAGE = "image" + FILE = "file" + AUDIO = "audio" + + +# Base schemas +class BaseResponse(BaseModel): + """Base response schema.""" + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# User schemas +class UserBase(BaseModel): + """User base schema.""" + username: str = Field(..., min_length=3, max_length=50) + email: str = Field(..., max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + + +class UserCreate(UserBase): + """User creation schema.""" + password: str = Field(..., min_length=6) + + +class UserUpdate(BaseModel): + """User update schema.""" + username: Optional[str] = Field(None, min_length=3, max_length=50) + email: Optional[str] = Field(None, max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + password: Optional[str] = Field(None, min_length=6) + is_active: Optional[bool] = None + department_id: Optional[int] = None + + +class UserResponse(BaseResponse, UserBase): + """User response schema.""" + is_active: bool + department_id: Optional[int] = None + roles: Optional[List['RoleResponse']] = Field(default=[], description="用户角色列表") + permissions: Optional[List[Dict[str, Any]]] = Field(default=[], description="用户权限列表") + is_superuser: Optional[bool] = Field(default=False, description="是否为超级管理员") + + @classmethod + def from_orm(cls, obj): + """从ORM对象创建响应对象,安全处理关系属性.""" + # 获取基本字段 + data = { + 'id': obj.id, + 'username': obj.username, + 'email': obj.email, + 'full_name': obj.full_name, + 'is_active': obj.is_active, + 'department_id': obj.department_id, + 'created_at': obj.created_at, + 'updated_at': obj.updated_at, + 'created_by': obj.created_by, + 'updated_by': obj.updated_by, + } + + # 安全处理roles关系 + try: + # 尝试访问roles,如果成功则包含,否则使用空列表 + if hasattr(obj, 'roles') and obj.roles is not None: + from ..schemas.permission import RoleResponse + data['roles'] = [RoleResponse.from_orm(role) for role in obj.roles] + else: + data['roles'] = [] + except Exception: + # 如果访问roles失败(DetachedInstanceError),使用空列表 + data['roles'] = [] + + # 添加权限信息 + try: + # 获取数据库会话 + from sqlalchemy.orm import object_session + session = object_session(obj) + + if obj.has_role('SUPER_ADMIN'): + # 超级管理员拥有所有权限 + if session: + from ..models.permission import Permission + all_permissions = session.query(Permission).filter(Permission.is_active == True).all() + data['permissions'] = [{'code': perm.code, 'name': perm.name} for perm in all_permissions] + else: + data['permissions'] = [{'code': '*', 'name': '所有权限'}] + else: + # 从角色获取权限 + permissions = set() + for role in obj.roles: + if role.is_active: + for perm in role.permissions: + if perm.is_active: + permissions.add((perm.code, perm.name)) + data['permissions'] = [{'code': code, 'name': name} for code, name in permissions] + except Exception as e: + data['permissions'] = [] + + # 添加超级管理员状态 + try: + data['is_superuser'] = obj.is_superuser() + except Exception: + data['is_superuser'] = False + + return cls(**data) + + +# Authentication schemas +class LoginRequest(BaseModel): + """Login request schema.""" + email: str = Field(..., max_length=100) + password: str = Field(..., min_length=6) + + +class Token(BaseModel): + """Token response schema.""" + access_token: str + token_type: str + expires_in: int + + +# Conversation schemas +class ConversationBase(BaseModel): + """Conversation base schema.""" + title: str = Field(..., min_length=1, max_length=200) + system_prompt: Optional[str] = None + model_name: str = Field(default="gpt-3.5-turbo", max_length=100) + temperature: str = Field(default="0.7", max_length=10) + max_tokens: int = Field(default=2048, ge=1, le=8192) + knowledge_base_id: Optional[int] = None + + +class ConversationCreate(ConversationBase): + """Conversation creation schema.""" + pass + + +class ConversationUpdate(BaseModel): + """Conversation update schema.""" + title: Optional[str] = Field(None, min_length=1, max_length=200) + system_prompt: Optional[str] = None + model_name: Optional[str] = Field(None, max_length=100) + temperature: Optional[str] = Field(None, max_length=10) + max_tokens: Optional[int] = Field(None, ge=1, le=8192) + is_archived: Optional[bool] = None + + +class ConversationResponse(BaseResponse, ConversationBase): + """Conversation response schema.""" + user_id: int + is_archived: bool + message_count: int = 0 + last_message_at: Optional[datetime] = None + + +# Message schemas +class MessageBase(BaseModel): + """Message base schema.""" + content: str = Field(..., min_length=1) + role: MessageRole + message_type: MessageType = MessageType.TEXT + metadata: Optional[Dict[str, Any]] = Field(None, alias="message_metadata") + + +class MessageCreate(MessageBase): + """Message creation schema.""" + conversation_id: int + + +class MessageResponse(BaseResponse, MessageBase): + """Message response schema.""" + conversation_id: int + context_documents: Optional[List[Dict[str, Any]]] = None + prompt_tokens: Optional[int] = None + completion_tokens: Optional[int] = None + total_tokens: Optional[int] = None + + class Config: + from_attributes = True + populate_by_name = True + + +# Chat schemas +class ChatRequest(BaseModel): + """Chat request schema.""" + message: str = Field(..., min_length=1, max_length=10000) + stream: bool = Field(default=False) + use_knowledge_base: bool = Field(default=False) + knowledge_base_id: Optional[int] = Field(None, description="Knowledge base ID for RAG mode") + use_agent: bool = Field(default=False, description="Enable agent mode with tool calling capabilities") + use_langgraph: bool = Field(default=False, description="Enable LangGraph agent mode with advanced tool calling") + temperature: Optional[float] = Field(None, ge=0.0, le=2.0) + max_tokens: Optional[int] = Field(None, ge=1, le=8192) + + +class ChatResponse(BaseModel): + """Chat response schema.""" + user_message: MessageResponse + assistant_message: MessageResponse + total_tokens: Optional[int] = None + model_used: str + + +class StreamChunk(BaseModel): + """Stream chunk schema.""" + content: str + role: MessageRole = MessageRole.ASSISTANT + finish_reason: Optional[str] = None + tokens_used: Optional[int] = None + + +# Knowledge Base schemas +class KnowledgeBaseBase(BaseModel): + """Knowledge base base schema.""" + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = None + embedding_model: str = Field(default="sentence-transformers/all-MiniLM-L6-v2") + chunk_size: int = Field(default=1000, ge=100, le=5000) + chunk_overlap: int = Field(default=200, ge=0, le=1000) + + +class KnowledgeBaseCreate(KnowledgeBaseBase): + """Knowledge base creation schema.""" + pass + + +class KnowledgeBaseUpdate(BaseModel): + """Knowledge base update schema.""" + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = None + embedding_model: Optional[str] = None + chunk_size: Optional[int] = Field(None, ge=100, le=5000) + chunk_overlap: Optional[int] = Field(None, ge=0, le=1000) + is_active: Optional[bool] = None + + +class KnowledgeBaseResponse(BaseResponse, KnowledgeBaseBase): + """Knowledge base response schema.""" + is_active: bool + vector_db_type: str + collection_name: Optional[str] + document_count: int = 0 + active_document_count: int = 0 + + +# Document schemas +class DocumentBase(BaseModel): + """Document base schema.""" + filename: str + original_filename: str + file_type: str + file_size: int + + +class DocumentUpload(BaseModel): + """Document upload schema.""" + knowledge_base_id: int + process_immediately: bool = Field(default=True) + + +class DocumentResponse(BaseResponse, DocumentBase): + """Document response schema.""" + knowledge_base_id: int + file_path: str + mime_type: Optional[str] + is_processed: bool + processing_error: Optional[str] + chunk_count: int = 0 + embedding_model: Optional[str] + file_size_mb: float + + +class DocumentListResponse(BaseModel): + """Document list response schema.""" + documents: List[DocumentResponse] + total: int + page: int + page_size: int + + +class DocumentProcessingStatus(BaseModel): + """Document processing status schema.""" + document_id: int + status: str # 'pending', 'processing', 'completed', 'failed' + progress: float = Field(default=0.0, ge=0.0, le=100.0) + error_message: Optional[str] = None + chunks_created: int = 0 + estimated_time_remaining: Optional[int] = None # seconds + + +# Error schemas +# Document chunk schemas +class DocumentChunk(BaseModel): + """Document chunk schema.""" + id: str + content: str + metadata: Dict[str, Any] = Field(default_factory=dict) + page_number: Optional[int] = None + chunk_index: int + start_char: Optional[int] = None + end_char: Optional[int] = None + + +class DocumentChunksResponse(BaseModel): + """Document chunks response schema.""" + document_id: int + document_name: str + total_chunks: int + chunks: List[DocumentChunk] + + +class ErrorResponse(BaseModel): + """Error response schema.""" + error: str + detail: Optional[str] = None + code: Optional[str] = None + +# 通用返回结构 +class NormalResponse(BaseModel): + success: bool + message: str + +class ExcelPreviewRequest(BaseModel): + file_id: str + page: int = 1 + page_size: int = 20 + +class FileListResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + + +# 解决前向引用问题 +def rebuild_models(): + """重建模型以解决前向引用问题.""" + try: + from ..schemas.permission import RoleResponse + UserResponse.model_rebuild() + except ImportError: + # 如果无法导入RoleResponse,跳过重建 + pass + + +# 在模块加载时尝试重建模型 +rebuild_models() \ No newline at end of file diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..6c46038 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,19 @@ +# ======================================== +# 前端环境变量配置示例 +# ======================================== +# 注意:实际的环境变量配置现在统一在 backend/.env 文件中 +# 这个文件仅作为参考,实际部署时需要将这些变量设置到运行环境中 + +# API Configuration +VITE_API_BASE_URL=http://localhost:8000/api + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..2b9066c --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,16 @@ +# API Configuration +# VITE_API_BASE_URL=http://localhost:3000/api +# 开发环境变量 +VITE_API_BASE_URL=/api +VITE_API_TARGET=http://localhost:8001 # 本地后端地址 + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..56f02f8 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,19 @@ +# ======================================== +# 前端环境变量配置示例 +# ======================================== +# 注意:实际的环境变量配置现在统一在 backend/.env 文件中 +# 这个文件仅作为参考,实际部署时需要将这些变量设置到运行环境中 + +# API Configuration +VITE_API_BASE_URL=http://localhost:8000/api + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts new file mode 100644 index 0000000..23ae575 --- /dev/null +++ b/frontend/auto-imports.d.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const ElMessage: typeof import('element-plus/es')['ElMessage'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const axios: typeof import('axios')['default'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const createPinia: typeof import('pinia')['createPinia'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const effectScope: typeof import('vue')['effectScope'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const h: typeof import('vue')['h'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useId: typeof import('vue')['useId'] + const useLink: typeof import('vue-router')['useLink'] + const useModel: typeof import('vue')['useModel'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/frontend/components.d.ts b/frontend/components.d.ts new file mode 100644 index 0000000..cde4d8b --- /dev/null +++ b/frontend/components.d.ts @@ -0,0 +1,70 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + AgentManagement: typeof import('./src/components/AgentManagement.vue')['default'] + AgentWorkflow: typeof import('./src/components/AgentWorkflow.vue')['default'] + CreativeStudio: typeof import('./src/components/CreativeStudio.vue')['default'] + DepartmentManagement: typeof import('./src/components/system/DepartmentManagement.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElCollapse: typeof import('element-plus/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElLink: typeof import('element-plus/es')['ElLink'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElSlider: typeof import('element-plus/es')['ElSlider'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + KnowledgeManagement: typeof import('./src/components/KnowledgeManagement.vue')['default'] + LLMConfigManagement: typeof import('./src/components/system/LLMConfigManagement.vue')['default'] + MainLayout: typeof import('./src/components/MainLayout.vue')['default'] + NodeParameterConfig: typeof import('./src/components/NodeParameterConfig.vue')['default'] + NotFound: typeof import('./src/components/NotFound.vue')['default'] + ParameterInputDialog: typeof import('./src/components/ParameterInputDialog.vue')['default'] + ProfileDialog: typeof import('./src/components/ProfileDialog.vue')['default'] + RoleManagement: typeof import('./src/components/system/RoleManagement.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SmartQuery: typeof import('./src/components/SmartQuery.vue')['default'] + UserManagement: typeof import('./src/components/system/UserManagement.vue')['default'] + WorkflowEditor: typeof import('./src/components/WorkflowEditor.vue')['default'] + } + export interface ComponentCustomProperties { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..2ea5417 --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string + readonly VITE_APP_TITLE: string + readonly VITE_APP_VERSION: string + readonly VITE_ENABLE_MOCK: string + readonly VITE_UPLOAD_MAX_SIZE: string + readonly VITE_SUPPORTED_FILE_TYPES: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..39158b8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,107 @@ + + + + + + + + + + TH智能对话助手 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
正在加载 TH-Agenter...
+
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3ec45a4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4493 @@ +{ + "name": "chat-agent-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-agent-frontend", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.6.0", + "dayjs": "^1.11.10", + "element-plus": "^2.4.2", + "highlight.js": "^11.9.0", + "lodash-es": "^4.17.21", + "markdown-it": "^13.0.2", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "vue": "^3.3.8", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.53.0", + "eslint-plugin-vue": "^9.18.1", + "prettier": "^3.0.3", + "sass": "^1.91.0", + "typescript": "~5.2.0", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.5.0", + "vue-tsc": "^1.8.22" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.20.tgz", + "integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.20", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz", + "integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz", + "integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.20", + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz", + "integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-8.0.0.tgz", + "integrity": "sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.20.tgz", + "integrity": "sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.20.tgz", + "integrity": "sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz", + "integrity": "sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/runtime-core": "3.5.20", + "@vue/shared": "3.5.20", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.20.tgz", + "integrity": "sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "vue": "3.5.20" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.20.tgz", + "integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.4.0.tgz", + "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.11.1", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.1.tgz", + "integrity": "sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.13", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.91.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.7.tgz", + "integrity": "sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.5", + "fast-glob": "^3.3.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "minimatch": "^9.0.3", + "unimport": "^3.4.0", + "unplugin": "^1.5.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.25.2", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz", + "integrity": "sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.5", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "minimatch": "^9.0.3", + "resolve": "^1.22.2", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.20.tgz", + "integrity": "sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-sfc": "3.5.20", + "@vue/runtime-dom": "3.5.20", + "@vue/server-renderer": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..be8b6db --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "chat-agent-frontend", + "version": "0.1.0", + "description": "Vue.js frontend for TH-Agenter application", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.6.0", + "dayjs": "^1.11.10", + "element-plus": "^2.4.2", + "highlight.js": "^11.9.0", + "lodash-es": "^4.17.21", + "markdown-it": "^13.0.2", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "vue": "^3.3.8", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.53.0", + "eslint-plugin-vue": "^9.18.1", + "prettier": "^3.0.3", + "sass": "^1.91.0", + "typescript": "~5.2.0", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.5.0", + "vue-tsc": "^1.8.22" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a498f4f --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..219f222 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,35 @@ +import { api } from './request' +import type { User, UserLogin, UserCreate, AuthTokens, ApiResponse, LoginRequest } from '@/types' + +// Auth API +export const authApi = { + // User login + login(data: LoginRequest) { + return api.post('/auth/login', data) + }, + + // User register + register(data: UserCreate) { + return api.post('/auth/register', data) + }, + + // Refresh token + refreshToken(refreshToken: string) { + return api.post('/auth/refresh', { + refresh_token: refreshToken + }) + }, + + // Get current user info + getCurrentUser() { + return api.get('/auth/me') + }, + + // Logout (optional - mainly client-side) + logout() { + // Clear tokens from localStorage + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + return Promise.resolve() + } +} \ No newline at end of file diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts new file mode 100644 index 0000000..789eb21 --- /dev/null +++ b/frontend/src/api/chat.ts @@ -0,0 +1,109 @@ +import { api } from './request' +import type { + Conversation, + ConversationCreate, + ConversationUpdate, + Message, + ChatRequest, + ChatResponse, + PaginationParams +} from '@/types' + +// Chat API +export const chatApi = { + // Conversations + getConversations(params?: PaginationParams & { + search?: string + include_archived?: boolean + order_by?: string + order_desc?: boolean + }) { + return api.get('/chat/conversations', { params }) + }, + + getConversationsCount(params?: { + search?: string + include_archived?: boolean + }) { + return api.get<{ count: number }>('/chat/conversations/count', { params }) + }, + + createConversation(data: ConversationCreate) { + return api.post('/chat/conversations', data) + }, + + getConversation(conversationId: string) { + return api.get(`/chat/conversations/${conversationId}`) + }, + + updateConversation(conversationId: string, data: ConversationUpdate) { + return api.put(`/chat/conversations/${conversationId}`, data) + }, + + deleteConversation(conversationId: string) { + return api.delete(`/chat/conversations/${conversationId}`) + }, + + deleteAllConversations() { + return api.delete('/chat/conversations') + }, + + archiveConversation(conversationId: string) { + return api.put(`/chat/conversations/${conversationId}/archive`) + }, + + unarchiveConversation(conversationId: string) { + return api.put(`/chat/conversations/${conversationId}/unarchive`) + }, + + // Messages + getMessages(conversationId: string, params?: PaginationParams) { + return api.get(`/chat/conversations/${conversationId}/messages`, { params }) + }, + + // Chat + sendMessage(conversationId: string, data: ChatRequest) { + return api.post(`/chat/conversations/${conversationId}/chat`, data) + }, + + // Stream chat (for Server-Sent Events) + sendMessageStream(data: ChatRequest) { + const baseURL = import.meta.env.VITE_API_BASE_URL || '/api' + const token = localStorage.getItem('access_token') + + return new EventSource(`${baseURL}/chat/stream`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + // Note: EventSource doesn't support POST with body directly + // This would need to be implemented differently, possibly using fetch with ReadableStream + }) + }, + + // Alternative stream implementation using fetch + async sendMessageStreamFetch(conversationId: string, data: ChatRequest): Promise | null> { + const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api' + const token = localStorage.getItem('access_token') + + try { + const response = await fetch(`${baseURL}/chat/conversations/${conversationId}/chat/stream`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return response.body + } catch (error) { + console.error('Stream request failed:', error) + throw error + } + } +} \ No newline at end of file diff --git a/frontend/src/api/departments.ts b/frontend/src/api/departments.ts new file mode 100644 index 0000000..fbbbcec --- /dev/null +++ b/frontend/src/api/departments.ts @@ -0,0 +1,97 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// Department types +export interface Department { + id: number + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active: boolean + created_at: string + updated_at: string + manager?: { + id: number + username: string + full_name?: string + } + children?: Department[] + user_count?: number +} + +export interface DepartmentCreate { + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active?: boolean + sort_order?: number +} + +export interface DepartmentUpdate { + name?: string + code?: string + description?: string + parent_id?: number + manager_id?: number + is_active?: boolean + sort_order?: number +} + +// Departments API +export const departmentsApi = { + // Get all departments + getDepartments(params?: { + skip?: number + limit?: number + search?: string + parent_id?: number + is_active?: boolean + include_children?: boolean + }) { + return api.get<{ + departments: Department[] + total: number + page: number + page_size: number + }>('/admin/departments/', { params }) + }, + + // Get department tree + getDepartmentTree() { + return api.get('/admin/departments/tree') + }, + + // Create new department + createDepartment(data: DepartmentCreate) { + return api.post('/admin/departments/', data) + }, + + // Get department by ID + getDepartmentById(departmentId: number) { + return api.get(`/admin/departments/${departmentId}`) + }, + + // Update department by ID + updateDepartmentById(departmentId: number, data: DepartmentUpdate) { + return api.put(`/admin/departments/${departmentId}`, data) + }, + + // Delete department by ID + deleteDepartmentById(departmentId: number) { + return api.delete(`/admin/departments/${departmentId}`) + }, + + // Get department users + getDepartmentUsers(departmentId: number, params?: PaginationParams) { + return api.get<{ + users: any[] + total: number + page: number + page_size: number + }>(`/admin/departments/${departmentId}/users`, { params }) + } +} \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..ee99084 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,14 @@ +// Export all API modules +export { authApi } from './auth' +export { usersApi } from './users' +export { chatApi } from './chat' +export { knowledgeApi } from './knowledge' +export { rolesApi } from './roles' +export { departmentsApi } from './departments' +export { userDepartmentsApi } from './userDepartments' +export { llmConfigApi } from './llmConfig' +export { workflowApi } from './workflow' +export { api } from './request' + +// Re-export request instance as default +export { default } from './request' \ No newline at end of file diff --git a/frontend/src/api/knowledge.ts b/frontend/src/api/knowledge.ts new file mode 100644 index 0000000..68ba993 --- /dev/null +++ b/frontend/src/api/knowledge.ts @@ -0,0 +1,71 @@ +import { api } from './request' +import type { + KnowledgeBase, + KnowledgeBaseCreate, + KnowledgeBaseUpdate, + Document, + DocumentUpload, + DocumentListResponse, + DocumentChunksResponse, + SearchRequest, + SearchResult, + PaginationParams +} from '@/types' + +// Knowledge Base API +export const knowledgeApi = { + // Knowledge Bases + getKnowledgeBases(params?: PaginationParams) { + return api.get('/knowledge-bases/', { params }) + }, + + createKnowledgeBase(data: KnowledgeBaseCreate) { + return api.post('/knowledge-bases/', data) + }, + + getKnowledgeBase(knowledgeBaseId: string) { + return api.get(`/knowledge-bases/${knowledgeBaseId}`) + }, + + updateKnowledgeBase(knowledgeBaseId: string, data: KnowledgeBaseUpdate) { + return api.put(`/knowledge-bases/${knowledgeBaseId}`, data) + }, + + deleteKnowledgeBase(knowledgeBaseId: string) { + return api.delete(`/knowledge-bases/${knowledgeBaseId}`) + }, + + // Documents + getDocuments(knowledgeBaseId: string, params?: PaginationParams) { + return api.get(`/knowledge-bases/${knowledgeBaseId}/documents`, { params }) + }, + + uploadDocument(knowledgeBaseId: string, file: File) { + const formData = new FormData() + formData.append('file', file) + formData.append('knowledge_base_id', knowledgeBaseId) + + return api.upload(`/knowledge-bases/${knowledgeBaseId}/documents`, formData) + }, + + deleteDocument(knowledgeBaseId: string, documentId: string) { + return api.delete(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}`) + }, + + processDocument(knowledgeBaseId: string, documentId: string) { + return api.post(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}/process`) + }, + + getDocumentChunks(knowledgeBaseId: string, documentId: string) { + return api.get(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}/chunks`) + }, + + // Search + searchKnowledgeBase(data: SearchRequest) { + return api.post(`/knowledge-bases/${data.knowledge_base_id}/search`, { + query: data.query, + top_k: data.top_k, + score_threshold: data.score_threshold + }) + } +} \ No newline at end of file diff --git a/frontend/src/api/llmConfig.ts b/frontend/src/api/llmConfig.ts new file mode 100644 index 0000000..4b483f6 --- /dev/null +++ b/frontend/src/api/llmConfig.ts @@ -0,0 +1,156 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// LLM Configuration types +export interface LLMConfig { + id: number + name: string + provider: string + model_name: string + api_key: string + base_url?: string + temperature: number + max_tokens: number + top_p: number + description?: string + is_active: boolean + is_default: boolean + is_embedding: boolean + extra_config?: Record + created_at: string + updated_at: string +} + +export interface LLMConfigCreate { + name: string + provider: string + model_name: string + api_key: string + base_url?: string + temperature?: number + max_tokens?: number + top_p?: number + description?: string + is_active?: boolean + is_default?: boolean + is_embedding?: boolean + extra_config?: Record +} + +export interface LLMConfigUpdate { + name?: string + provider?: string + model_name?: string + api_key?: string + base_url?: string + temperature?: number + max_tokens?: number + top_p?: number + description?: string + is_active?: boolean + is_default?: boolean + is_embedding?: boolean + extra_config?: Record +} + +export interface LLMConfigTestRequest { + message?: string +} + +export interface LLMConfigTestResponse { + success: boolean + response_time: number + model_info?: string + error?: string + test_message?: any +} + +// LLM Configuration API +export const llmConfigApi = { + // Get all LLM configurations + getLLMConfigs(params?: { + skip?: number + limit?: number + search?: string + provider?: string + is_active?: boolean + is_embedding?: boolean + }) { + return api.get('/admin/llm-configs/', { params }) + }, + + // Get LLM configuration by ID + getLLMConfig(configId: number) { + return api.get(`/admin/llm-configs/${configId}`) + }, + + // Create new LLM configuration + createLLMConfig(data: LLMConfigCreate) { + return api.post('/admin/llm-configs/', data) + }, + + // Update LLM configuration + updateLLMConfig(configId: number, data: LLMConfigUpdate) { + return api.put(`/admin/llm-configs/${configId}`, data) + }, + + // Delete LLM configuration + deleteLLMConfig(configId: number) { + return api.delete(`/admin/llm-configs/${configId}`) + }, + + // Test LLM configuration + testLLMConfig(configId: number, data?: LLMConfigTestRequest) { + return api.post(`/admin/llm-configs/${configId}/test`, data || {}) + }, + + // Get embedding type configurations + getEmbeddingConfigs(params?: { + skip?: number + limit?: number + is_active?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_embedding: true } + }) + }, + + // Get chat model configurations + getChatConfigs(params?: { + skip?: number + limit?: number + is_active?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_embedding: false } + }) + }, + + // Get default configuration + getDefaultConfig(isEmbedding: boolean = false) { + return api.get('/admin/llm-configs/default', { + params: { is_embedding: isEmbedding } + }) + }, + + // Get active configurations + getActiveConfigs(params?: { + skip?: number + limit?: number + is_embedding?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_active: true } + }) + }, + + // Set as default configuration + setAsDefault(configId: number) { + return api.post(`/admin/llm-configs/${configId}/set-default`) + }, + + // Toggle configuration status + toggleStatus(configId: number) { + return api.post(`/admin/llm-configs/${configId}/toggle-status`) + } +} \ No newline at end of file diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..2f6e2d6 --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,146 @@ +import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' +import type { ApiResponse } from '@/types' + +// Create axios instance +const request = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api', + timeout: 30000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// Request interceptor +request.interceptors.request.use( + (config: AxiosRequestConfig) => { + // Add auth token + const token = localStorage.getItem('access_token') + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + console.error('Request error:', error) + return Promise.reject(error) + } +) + +// 防止重复处理401错误的标志 +let isHandling401 = false + +// Response interceptor +request.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response + + // Handle successful response + if (data.success !== false) { + return response + } + + // Handle business logic errors + const message = data.message || data.error || 'Request failed' + ElMessage.error(message) + return Promise.reject(new Error(message)) + }, + async (error) => { + const { response, message } = error + + if (response) { + const { status, data } = response + + switch (status) { + case 401: + // 防止重复处理401错误 + if (!isHandling401) { + isHandling401 = true + + // 延迟处理,避免并发请求重复跳转 + setTimeout(() => { + console.log('认证失败,跳转到登录页面') + ElMessage.error('登录已过期,请重新登录') + + // 强制跳转到登录页面,添加expired参数标识token过期 + router.replace('/login?expired=true') + isHandling401 = false + }, 100) + } + break + + case 403: + ElMessage.error('没有权限访问该资源') + break + + case 404: + ElMessage.error('请求的资源不存在') + break + + case 400: + // Bad request errors - 支持detail字段 + const badRequestMsg = data?.detail || data?.message || data?.error || '请求参数错误' + ElMessage.error(badRequestMsg) + break + + case 422: + // Validation errors + const errorMessage = data?.detail?.[0]?.msg || data?.message || '请求参数错误' + ElMessage.error(errorMessage) + break + + case 500: + ElMessage.error('服务器内部错误') + break + + default: + const msg = data?.message || data?.error || `请求失败 (${status})` + ElMessage.error(msg) + } + } else if (message.includes('timeout')) { + ElMessage.error('请求超时,请稍后重试') + } else if (message.includes('Network Error')) { + ElMessage.error('网络连接失败,请检查网络') + } else { + ElMessage.error('请求失败,请稍后重试') + } + + return Promise.reject(error) + } +) + +// Request methods +export const api = { + get(url: string, config?: AxiosRequestConfig): Promise>> { + return request.get(url, config) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.put(url, data, config) + }, + + patch(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.patch(url, data, config) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise>> { + return request.delete(url, config) + }, + + upload(url: string, formData: FormData, config?: AxiosRequestConfig): Promise>> { + return request.post(url, formData, { + ...config, + headers: { + 'Content-Type': 'multipart/form-data', + ...config?.headers + } + }) + } +} + +export default request \ No newline at end of file diff --git a/frontend/src/api/roles.ts b/frontend/src/api/roles.ts new file mode 100644 index 0000000..5a1f024 --- /dev/null +++ b/frontend/src/api/roles.ts @@ -0,0 +1,80 @@ +import { api } from './request' +import type { Role, ApiResponse } from '@/types' + +// Role interfaces +export interface RoleCreate { + name: string + code: string + description?: string + sort_order?: number + is_active?: boolean +} + +export interface RoleUpdate { + name?: string + code?: string + description?: string + sort_order?: number + is_active?: boolean +} + +export interface UserRoleAssign { + user_id: number + role_ids: number[] +} + + + +// Roles API +export const rolesApi = { + // Get user roles by user ID + getUserRoles(userId: number) { + return api.get(`/admin/roles/user-roles/user/${userId}`) + }, + + // Get all roles + getRoles(params?: { + skip?: number + limit?: number + search?: string + is_active?: boolean + }) { + return api.get('/admin/roles/', { params }) + }, + + // Get role by ID + getRole(roleId: number) { + return api.get(`/admin/roles/${roleId}`) + }, + + // Create new role + createRole(data: RoleCreate) { + return api.post('/admin/roles/', data) + }, + + // Update role + updateRole(roleId: number, data: RoleUpdate) { + return api.put(`/admin/roles/${roleId}`, data) + }, + + // Delete role + deleteRole(roleId: number) { + return api.delete(`/admin/roles/${roleId}`) + }, + + // Assign permissions to role + assignRolePermissions(roleId: number, data: RolePermissionAssign) { + return api.post(`/admin/roles/${roleId}/permissions`, data) + }, + + // Get role permissions + getRolePermissions(roleId: number) { + return api.get(`/admin/roles/${roleId}/permissions`) + }, + + // Assign roles to user + assignUserRoles(data: UserRoleAssign) { + return api.post('/admin/roles/user-roles/assign', data) + }, + +} \ No newline at end of file diff --git a/frontend/src/api/userDepartments.ts b/frontend/src/api/userDepartments.ts new file mode 100644 index 0000000..9eeca47 --- /dev/null +++ b/frontend/src/api/userDepartments.ts @@ -0,0 +1,114 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// User Department types +export interface UserDepartment { + id: number + user_id: number + department_id: number + is_primary: boolean + is_active: boolean + created_at: string + updated_at: string +} + +export interface UserDepartmentWithDetails extends UserDepartment { + user_name?: string + user_email?: string + department_name?: string + department_code?: string +} + +export interface DepartmentUserList { + department_id: number + department_name: string + department_code: string + users: UserDepartmentWithDetails[] + total_users: number + active_users: number +} + +export interface UserDepartmentCreate { + user_id: number + department_id: number + is_primary?: boolean + is_active?: boolean +} + +export interface UserDepartmentUpdate { + is_primary?: boolean + is_active?: boolean +} + +// User Departments API +export const userDepartmentsApi = { + // Create user-department association + createUserDepartment(data: UserDepartmentCreate) { + return api.post('/admin/user-departments/', data) + }, + + // Get all departments for a user + getUserDepartments(userId: number, activeOnly: boolean = true) { + return api.get<{ + user_id: number + user_name: string + user_email: string + departments: UserDepartmentWithDetails[] + total_departments: number + active_departments: number + }>(`/admin/user-departments/user/${userId}`, { + params: { active_only: activeOnly } + }) + }, + + // Get all users in a department + getDepartmentUsers(departmentId: number, activeOnly: boolean = true) { + return api.get(`/admin/user-departments/department/${departmentId}`, { + params: { active_only: activeOnly } + }) + }, + + // Update user-department association + updateUserDepartment(userId: number, departmentId: number, data: UserDepartmentUpdate) { + return api.put(`/admin/user-departments/user/${userId}/department/${departmentId}`, data) + }, + + // Remove user from department + removeUserFromDepartment(userId: number, departmentId: number) { + return api.delete(`/admin/user-departments/user/${userId}/department/${departmentId}`) + }, + + // Set user's primary department + setUserPrimaryDepartment(userId: number, departmentId: number) { + return api.put(`/admin/user-departments/user/${userId}/primary-department`, { + department_id: departmentId + }) + }, + + // Bulk create user-department associations + bulkCreateUserDepartments(data: { + user_ids: number[] + department_id: number + is_primary?: boolean + is_active?: boolean + }) { + return api.post('/admin/user-departments/bulk', data) + }, + + // Get user's department tree + getUserDepartmentTree(userId: number) { + return api.get<{ + user_id: number + department_tree: any[] + }>(`/admin/user-departments/user/${userId}/tree`) + }, + + // Get all user IDs that have department associations + getUsersWithDepartments(activeOnly: boolean = true) { + return api.get<{ + user_ids: number[] + }>('/admin/user-departments/users-with-departments', { + params: { active_only: activeOnly } + }) + } +} \ No newline at end of file diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 0000000..6b3a3ce --- /dev/null +++ b/frontend/src/api/users.ts @@ -0,0 +1,81 @@ +import { api } from './request' +import type { User, UserUpdate, UserCreate, PaginationParams } from '@/types' + +// Users API +export const usersApi = { + // Get current user profile + getProfile() { + return api.get('/users/profile') + }, + + // Update current user profile + updateProfile(data: UserUpdate) { + return api.put('/users/profile', data) + }, + + // Delete current user account + deleteAccount() { + return api.delete('/users/profile') + }, + + // Change password + changePassword(data: { current_password: string; new_password: string }) { + return api.put('/users/change-password', data) + }, + + // Admin: Get all users with pagination and filters + getUsers(params?: { + skip?: number + limit?: number + search?: string + department_id?: number + role_id?: number + is_active?: boolean + }) { + return api.get<{ + users: User[] + total: number + page: number + page_size: number + }>('/users/', { params }) + }, + + // Admin: Create new user + createUser(data: UserCreate & { + department_id?: number + is_admin?: boolean + is_active?: boolean + }) { + return api.post('/users/', data) + }, + + // Admin: Get user by ID + getUserById(userId: number) { + return api.get(`/users/${userId}`) + }, + + // Admin: Update user by ID + updateUserById(userId: number, data: UserUpdate & { + password?: string + department_id?: number + is_admin?: boolean + is_active?: boolean + }) { + return api.put(`/users/${userId}`, data) + }, + + // Admin: Delete user by ID + deleteUserById(userId: number) { + return api.delete(`/users/${userId}`) + }, + + // Admin: Update user status + updateUserStatus(userId: number, is_active: boolean) { + return api.put(`/users/${userId}`, { is_active }) + }, + + // Admin: Reset user password + resetUserPassword(userId: number, newPassword: string) { + return api.put(`/users/${userId}/reset-password`, { new_password: newPassword }) + } +} \ No newline at end of file diff --git a/frontend/src/api/workflow.ts b/frontend/src/api/workflow.ts new file mode 100644 index 0000000..ec665c7 --- /dev/null +++ b/frontend/src/api/workflow.ts @@ -0,0 +1,166 @@ +import { api } from './request' +import type { PaginationParams, NodeInputOutput } from '@/types' + +// 工作流类型定义 +export interface WorkflowNode { + id: string + type: 'start' | 'end' | 'llm' | 'condition' | 'code' | 'http' + name: string + description?: string + position: { + x: number + y: number + } + config: Record + parameters?: NodeInputOutput +} + +export interface WorkflowConnection { + id: string + from: string + to: string +} + +export interface WorkflowDefinition { + nodes: WorkflowNode[] + connections: WorkflowConnection[] +} + +export interface Workflow { + id: number + name: string + description?: string + version: string + status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' + definition: WorkflowDefinition + created_at: string + updated_at: string + created_by: number + updated_by: number +} + +export interface WorkflowCreate { + name: string + description?: string + version?: string + definition: WorkflowDefinition +} + +export interface WorkflowUpdate { + name?: string + description?: string + version?: string + definition?: WorkflowDefinition +} + +export interface WorkflowExecution { + id: number + workflow_id: number + status: 'pending' | 'running' | 'completed' | 'failed' + input_data?: Record + output_data?: Record + error_message?: string + started_at: string + completed_at?: string + executor_id: number + node_executions?: NodeExecution[] + created_at: string + updated_at: string +} + +export interface WorkflowExecuteRequest { + input_data?: Record +} + +export interface NodeExecution { + id: number + node_id: string + node_type: string + node_name: string + status: 'pending' | 'running' | 'completed' | 'failed' + input_data?: Record + output_data?: Record + error_message?: string + started_at?: string + completed_at?: string + duration_ms?: number +} + +// 工作流API +export const workflowApi = { + // 获取工作流列表 + getWorkflows(params?: PaginationParams & { + search?: string + status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' + created_by?: number + }) { + return api.get<{ + workflows: Workflow[] + total: number + page: number + size: number + }>('/workflows/', { params }) + }, + + // 获取工作流详情 + getWorkflow(workflowId: number) { + return api.get(`/workflows/${workflowId}`) + }, + + // 创建工作流 + createWorkflow(data: WorkflowCreate) { + return api.post('/workflows/', data) + }, + + // 更新工作流 + updateWorkflow(workflowId: number, data: WorkflowUpdate) { + return api.put(`/workflows/${workflowId}`, data) + }, + + // 删除工作流 + deleteWorkflow(workflowId: number) { + return api.delete(`/workflows/${workflowId}`) + }, + + // 激活工作流 + activateWorkflow(workflowId: number) { + return api.post(`/workflows/${workflowId}/activate`) + }, + + // 停用工作流 + deactivateWorkflow(workflowId: number) { + return api.post(`/workflows/${workflowId}/deactivate`) + }, + + // 执行工作流 + executeWorkflow(workflowId: number, data: WorkflowExecuteRequest) { + return api.post(`/workflows/${workflowId}/execute`, data) + }, + + // 获取工作流执行历史 + getWorkflowExecutions(workflowId: number, params?: PaginationParams) { + return api.get<{ + executions: WorkflowExecution[] + total: number + page: number + size: number + }>(`/workflows/${workflowId}/executions`, { params }) + }, + + // 获取执行详情 + getExecution(executionId: number) { + return api.get(`/workflows/executions/${executionId}`) + }, + + // 获取节点执行详情 + getNodeExecutions(executionId: number) { + return api.get<{ + node_executions: NodeExecution[] + }>(`/workflows/executions/${executionId}/nodes`) + }, + + // 停止执行 + stopExecution(executionId: number) { + return api.post(`/workflows/executions/${executionId}/stop`) + } +} \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..d615841 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/AgentManagement.vue b/frontend/src/components/AgentManagement.vue new file mode 100644 index 0000000..05b0cd7 --- /dev/null +++ b/frontend/src/components/AgentManagement.vue @@ -0,0 +1,1430 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/AgentWorkflow.vue b/frontend/src/components/AgentWorkflow.vue new file mode 100644 index 0000000..e7d5b0d --- /dev/null +++ b/frontend/src/components/AgentWorkflow.vue @@ -0,0 +1,845 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/CreativeStudio.vue b/frontend/src/components/CreativeStudio.vue new file mode 100644 index 0000000..c747f20 --- /dev/null +++ b/frontend/src/components/CreativeStudio.vue @@ -0,0 +1,1151 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/KnowledgeManagement.vue b/frontend/src/components/KnowledgeManagement.vue new file mode 100644 index 0000000..2a17755 --- /dev/null +++ b/frontend/src/components/KnowledgeManagement.vue @@ -0,0 +1,1726 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/MainLayout.vue b/frontend/src/components/MainLayout.vue new file mode 100644 index 0000000..dc6be41 --- /dev/null +++ b/frontend/src/components/MainLayout.vue @@ -0,0 +1,1019 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/NodeParameterConfig.vue b/frontend/src/components/NodeParameterConfig.vue new file mode 100644 index 0000000..0422e31 --- /dev/null +++ b/frontend/src/components/NodeParameterConfig.vue @@ -0,0 +1,894 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/NotFound.vue b/frontend/src/components/NotFound.vue new file mode 100644 index 0000000..bf70a7c --- /dev/null +++ b/frontend/src/components/NotFound.vue @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ParameterInputDialog.vue b/frontend/src/components/ParameterInputDialog.vue new file mode 100644 index 0000000..55e437e --- /dev/null +++ b/frontend/src/components/ParameterInputDialog.vue @@ -0,0 +1,427 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ProfileDialog.vue b/frontend/src/components/ProfileDialog.vue new file mode 100644 index 0000000..6ff41c4 --- /dev/null +++ b/frontend/src/components/ProfileDialog.vue @@ -0,0 +1,315 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/SmartQuery.vue b/frontend/src/components/SmartQuery.vue new file mode 100644 index 0000000..ec9484f --- /dev/null +++ b/frontend/src/components/SmartQuery.vue @@ -0,0 +1,4062 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/WorkflowEditor.vue b/frontend/src/components/WorkflowEditor.vue new file mode 100644 index 0000000..e8bd339 --- /dev/null +++ b/frontend/src/components/WorkflowEditor.vue @@ -0,0 +1,3967 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/DepartmentManagement.vue b/frontend/src/components/system/DepartmentManagement.vue new file mode 100644 index 0000000..ed5d1e1 --- /dev/null +++ b/frontend/src/components/system/DepartmentManagement.vue @@ -0,0 +1,1009 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/LLMConfigManagement.vue b/frontend/src/components/system/LLMConfigManagement.vue new file mode 100644 index 0000000..2f16bb6 --- /dev/null +++ b/frontend/src/components/system/LLMConfigManagement.vue @@ -0,0 +1,1909 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/RoleManagement.vue b/frontend/src/components/system/RoleManagement.vue new file mode 100644 index 0000000..48367bf --- /dev/null +++ b/frontend/src/components/system/RoleManagement.vue @@ -0,0 +1,1076 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/UserManagement.vue b/frontend/src/components/system/UserManagement.vue new file mode 100644 index 0000000..1d12b98 --- /dev/null +++ b/frontend/src/components/system/UserManagement.vue @@ -0,0 +1,1011 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..36100cc --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,23 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import 'nprogress/nprogress.css' + +import App from './App.vue' +import router from './router' +// import '@/styles/index.scss' + +const app = createApp(App) + +// Register Element Plus icons +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) + +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..687843d --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,182 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/stores/user' +import NProgress from 'nprogress' + +const routes: Array = [ + { + path: '/login', + name: 'Login', + component: () => import('../views/Login.vue'), + meta: { requiresAuth: false } + }, + { + path: '/register', + name: 'Register', + component: () => import('../views/Register.vue'), + meta: { requiresAuth: false } + }, + { + path: '/', + name: 'MainLayout', + component: () => import('../components/MainLayout.vue'), + meta: { requiresAuth: false }, + redirect: '/chat', + children: [ + { + path: 'chat', + name: 'Chat', + component: () => import('../views/Chat.vue'), + meta: { requiresAuth: false } + }, + { + path: 'knowledge', + name: 'Knowledge', + component: () => import('../components/KnowledgeManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'workflow', + name: 'Workflow', + redirect: '/workflow/list', + meta: { requiresAuth: false }, + children: [ + { + path: 'list', + name: 'WorkflowList', + component: () => import('../views/Flow/WorkflowList.vue'), + meta: { requiresAuth: false } + }, + { + path: 'editor/:id?', + name: 'WorkflowEditor', + component: () => import('../components/WorkflowEditor.vue'), + meta: { requiresAuth: false } + } + ] + }, + { + path: 'agent', + name: 'Agent', + component: () => import('../components/AgentManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'mcp-service', + name: 'MCPService', + component: () => import('../views/MCPServiceManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'smart-query', + name: 'SmartQuery', + component: () => import('../components/SmartQuery.vue'), + meta: { requiresAuth: false } + }, + { + path: 'creation', + name: 'Creation', + component: () => import('../components/CreativeStudio.vue'), + meta: { requiresAuth: false } + }, + { + path: 'profile', + name: 'Profile', + component: () => import('../views/Profile.vue'), + meta: { requiresAuth: false } + }, + { + path: 'system', + name: 'System', + component: () => import('../views/SystemManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false }, + redirect: '/system/users', + children: [ + { + path: 'users', + name: 'SystemUsers', + component: () => import('../components/system/UserManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + { + path: 'departments', + name: 'SystemDepartments', + component: () => import('../components/system/DepartmentManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + { + path: 'roles', + name: 'SystemRoles', + component: () => import('../components/system/RoleManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + + { + path: 'llm-configs', + name: 'SystemLLMConfigs', + component: () => import('../components/system/LLMConfigManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + } + ] + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('../components/NotFound.vue') + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// Navigation guards +router.beforeEach(async (to, from, next) => { + NProgress.start() + + // Set page title + if (to.meta.title) { + document.title = `${to.meta.title} - TH-Agenter` + } + + const userStore = useUserStore() + + // Initialize user if token exists + if (!userStore.user && localStorage.getItem('access_token')) { + try { + await userStore.initializeUser() + } catch (error) { + console.log('Failed to initialize user') + } + } + + // Check authentication requirement + if (to.meta.requiresAuth && !userStore.isAuthenticated) { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + // Check admin requirement + if (to.meta.requiresAdmin && !userStore.isAdmin) { + next({ name: 'Chat' }) + return + } + + // Redirect authenticated users away from login/register + // 但如果是因为token过期跳转过来的,则允许访问登录页面 + if ((to.name === 'Login' || to.name === 'Register') && userStore.isAuthenticated && !to.query.expired) { + next({ name: 'Chat' }) + return + } + + next() +}) + +router.afterEach(() => { + NProgress.done() +}) + +export default router \ No newline at end of file diff --git a/frontend/src/services/sse.ts b/frontend/src/services/sse.ts new file mode 100644 index 0000000..54a6786 --- /dev/null +++ b/frontend/src/services/sse.ts @@ -0,0 +1,290 @@ +/** + * SSE (Server-Sent Events) 服务,用于实时接收工作流执行状态 + */ + +export interface WorkflowExecutionMessage { + type: 'workflow_status' | 'node_status' | 'workflow_result' | 'error' + execution_id?: number + node_id?: string + status?: string + data?: any + timestamp?: string + message?: string +} + +export interface WorkflowExecutionCallbacks { + onWorkflowStarted?: (data: any) => void + onWorkflowCompleted?: (data: any) => void + onWorkflowFailed?: (data: any) => void + onNodeStarted?: (nodeId: string, data: any) => void + onNodeCompleted?: (nodeId: string, data: any) => void + onNodeFailed?: (nodeId: string, data: any) => void + onWorkflowResult?: (data: any) => void + onError?: (error: string) => void +} + +export class WorkflowSSEService { + private callbacks: WorkflowExecutionCallbacks = {} + private isConnected = false + private abortController: AbortController | null = null + private currentExecutionId: number | null = null + + /** + * 开始监听工作流执行 + */ + async startWorkflowExecution(workflowId: number, inputData: any = {}): Promise { + // 如果已经有连接,先关闭 + if (this.abortController) { + this.disconnect() + } + + try { + // 获取认证token + const token = localStorage.getItem('access_token') + if (!token) { + throw new Error('未找到认证token') + } + + // 执行工作流并处理流式响应 + await this.executeWorkflow(workflowId, inputData, token) + + } catch (error) { + console.error('启动工作流执行失败:', error) + this.callbacks.onError?.(error instanceof Error ? error.message : '启动工作流执行失败') + throw error + } + } + + /** + * 执行工作流 + */ + private async executeWorkflow(workflowId: number, inputData: any, token: string): Promise { + const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' + const url = `${baseUrl}/workflows/${workflowId}/execute-stream` + + // 创建AbortController来管理请求 + this.abortController = new AbortController() + + // 构建符合后端期望的数据结构 + const requestData = { + input_data: inputData + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(requestData), + signal: this.abortController.signal + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // 处理流式响应 + const reader = response.body?.getReader() + if (!reader) { + throw new Error('无法获取响应流') + } + + this.isConnected = true + this.processStream(reader) + } + + /** + * 处理流式响应 + */ + private async processStream(reader: ReadableStreamDefaultReader): Promise { + const decoder = new TextDecoder() + let buffer = '' + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }) + + // 处理完整的消息 + const lines = buffer.split('\n') + buffer = lines.pop() || '' // 保留不完整的行 + + for (const line of lines) { + if (line.trim()) { + try { + // 解析SSE格式的数据 + if (line.startsWith('data: ')) { + const jsonData = line.substring(6) + const message: WorkflowExecutionMessage = JSON.parse(jsonData) + this.handleMessage(message) + } + } catch (error) { + console.error('解析SSE消息失败:', error, line) + } + } + } + } + } catch (error) { + console.error('处理SSE流失败:', error) + this.callbacks.onError?.(error instanceof Error ? error.message : '处理SSE流失败') + } finally { + this.isConnected = false + reader.releaseLock() + } + } + + + + /** + * 处理接收到的消息 + */ + private handleMessage(message: WorkflowExecutionMessage): void { + console.log('收到SSE消息:', message) + + switch (message.type) { + case 'workflow_start': + // 处理工作流开始消息 + this.callbacks.onWorkflowStarted?.(message) + break + case 'execution_update': + // 处理执行更新消息 + this.handleExecutionUpdate(message) + break + case 'workflow_status': + this.handleWorkflowStatus(message) + break + case 'node_status': + this.handleNodeStatus(message) + break + case 'workflow_result': + this.handleWorkflowResult(message) + break + case 'error': + this.callbacks.onError?.(message.message || '工作流执行错误') + break + default: + console.warn('未知的消息类型:', message.type) + } + } + + /** + * 处理工作流状态消息 + */ + private handleWorkflowStatus(message: WorkflowExecutionMessage): void { + if (message.execution_id) { + this.currentExecutionId = message.execution_id + } + + switch (message.status) { + case 'started': + this.callbacks.onWorkflowStarted?.(message.data) + break + case 'completed': + this.callbacks.onWorkflowCompleted?.(message.data) + break + case 'failed': + this.callbacks.onWorkflowFailed?.(message.data) + break + } + } + + /** + * 处理节点状态消息 + */ + private handleNodeStatus(message: WorkflowExecutionMessage): void { + if (!message.node_id) return + + switch (message.status) { + case 'started': + this.callbacks.onNodeStarted?.(message.node_id, message.data) + break + case 'completed': + this.callbacks.onNodeCompleted?.(message.node_id, message.data) + break + case 'failed': + this.callbacks.onNodeFailed?.(message.node_id, message.data) + break + } + } + + /** + * 处理工作流结果消息 + */ + private handleWorkflowResult(message: WorkflowExecutionMessage): void { + this.callbacks.onWorkflowResult?.(message.data) + } + + /** + * 处理执行更新消息 + */ + private handleExecutionUpdate(message: any): void { + const { update_type, data } = message + + switch (update_type) { + case 'node_status': + // 转换为node_status格式并处理 + const nodeMessage = { + type: 'node_status', + node_id: data.node_id, + status: data.status, + data: data.data + } + this.handleNodeStatus(nodeMessage) + break + case 'workflow_status': + // 转换为workflow_status格式并处理 + const workflowMessage = { + type: 'workflow_status', + status: data.status, + data: data.data + } + this.handleWorkflowStatus(workflowMessage) + break + default: + console.warn('未知的执行更新类型:', update_type) + } + } + + /** + * 设置回调函数 + */ + setCallbacks(callbacks: WorkflowExecutionCallbacks): void { + this.callbacks = { ...this.callbacks, ...callbacks } + } + + /** + * 断开连接 + */ + disconnect(): void { + if (this.abortController) { + this.abortController.abort() + this.abortController = null + } + this.isConnected = false + this.currentExecutionId = null + } + + /** + * 获取连接状态 + */ + isConnectedToWorkflow(): boolean { + return this.isConnected + } + + /** + * 获取当前执行ID + */ + getCurrentExecutionId(): number | null { + return this.currentExecutionId + } +} + +// 创建单例实例 +export const workflowSSEService = new WorkflowSSEService() \ No newline at end of file diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts new file mode 100644 index 0000000..83becd9 --- /dev/null +++ b/frontend/src/stores/chat.ts @@ -0,0 +1,704 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { chatApi } from '@/api' +import type { + Conversation, + ConversationCreate, + ConversationUpdate, + Message, + ChatRequest +} from '@/types' + +export const useChatStore = defineStore('chat', () => { + // State + const conversations = ref([]) + const currentConversation = ref(null) + const messages = ref([]) + const isLoading = ref(false) + const isStreaming = ref(false) + const isLoadingMessages = ref(false) + const searchQuery = ref('') + const includeArchived = ref(false) + const totalCount = ref(0) + const currentPage = ref(1) + const pageSize = ref(20) + + // Getters + const sortedConversations = computed(() => { + return [...conversations.value].sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ) + }) + + const activeConversations = computed(() => { + return conversations.value.filter(conv => !conv.is_archived) + }) + + const archivedConversations = computed(() => { + return conversations.value.filter(conv => conv.is_archived) + }) + + // Actions + const loadConversations = async (params?: { + search?: string + include_archived?: boolean + page?: number + limit?: number + order_by?: string + order_desc?: boolean + }) => { + try { + isLoading.value = true + const queryParams = { + search: params?.search || searchQuery.value, + include_archived: params?.include_archived ?? includeArchived.value, + skip: ((params?.page || currentPage.value) - 1) * pageSize.value, + limit: params?.limit || pageSize.value, + order_by: params?.order_by || 'updated_at', + order_desc: params?.order_desc ?? true + } + + const response = await chatApi.getConversations(queryParams) + conversations.value = response.data.data || response.data || [] + + // Load total count + const countResponse = await chatApi.getConversationsCount({ + search: queryParams.search, + include_archived: queryParams.include_archived + }) + totalCount.value = countResponse.data.count || countResponse.data.data?.count || 0 + } catch (error: any) { + console.error('Load conversations failed:', error) + ElMessage.error('加载对话列表失败') + } finally { + isLoading.value = false + } + } + + const createConversation = async (data: ConversationCreate) => { + try { + isLoading.value = true + console.log('Creating conversation with data:', data) + const response = await chatApi.createConversation(data) + console.log('Create conversation response:', response) + + const newConversation = response.data + console.log('New conversation:', newConversation) + + conversations.value.unshift(newConversation) + currentConversation.value = newConversation + messages.value = [] + + // ElMessage.success('创建对话成功') + return newConversation + } catch (error: any) { + console.error('Create conversation failed:', error) + console.error('Error response:', error.response?.data) + console.error('Error status:', error.response?.status) + ElMessage.error('创建对话失败: ' + (error.response?.data?.detail || error.message)) + return null + } finally { + isLoading.value = false + } + } + + const loadConversation = async (conversationId: string) => { + try { + isLoading.value = true + const response = await chatApi.getConversation(conversationId) + currentConversation.value = response.data.data || response.data + + // Update conversation in list if exists + const index = conversations.value.findIndex(conv => conv.id.toString() === conversationId) + if (index !== -1) { + conversations.value[index] = currentConversation.value + } + + return currentConversation.value + } catch (error: any) { + console.error('Load conversation failed:', error) + ElMessage.error('加载对话失败') + return null + } finally { + isLoading.value = false + } + } + + const updateConversation = async (conversationId: string, data: ConversationUpdate) => { + try { + const response = await chatApi.updateConversation(conversationId, data) + const updatedConversation = response.data.data || response.data + + // Update in list + const index = conversations.value.findIndex(conv => conv.id.toString() === conversationId) + if (index !== -1) { + conversations.value[index] = updatedConversation + } + + // Update current if it's the same + if (currentConversation.value?.id.toString() === conversationId) { + currentConversation.value = updatedConversation + } + + ElMessage.success('对话更新成功') + return updatedConversation + } catch (error: any) { + console.error('Update conversation failed:', error) + ElMessage.error('更新对话失败') + return null + } + } + + const deleteConversation = async (conversationId: string) => { + try { + await chatApi.deleteConversation(conversationId) + + // Remove from list + conversations.value = conversations.value.filter(conv => conv.id.toString() !== conversationId) + + // Clear current if it's the same + if (currentConversation.value?.id.toString() === conversationId) { + currentConversation.value = null + messages.value = [] + } + + ElMessage.success('对话删除成功') + return true + } catch (error: any) { + console.error('Delete conversation failed:', error) + ElMessage.error('删除对话失败') + return false + } + } + + const deleteAllConversations = async () => { + try { + await chatApi.deleteAllConversations() + + // Clear all conversations + conversations.value = [] + + // Clear current conversation and messages + currentConversation.value = null + messages.value = [] + + // ElMessage.success('所有对话已删除') + return true + } catch (error: any) { + console.error('Delete all conversations failed:', error) + ElMessage.error('删除所有对话失败') + return false + } + } + + const loadMessages = async (conversationId: string, forceReload = false) => { + try { + isLoadingMessages.value = true + const response = await chatApi.getMessages(conversationId) + const loadedMessages = response.data.data || response.data || [] + + // 只有在强制重新加载或消息为空时才替换消息数组 + console.log(forceReload) + if (forceReload || messages.value.length === 0) { + messages.value = loadedMessages + } + // messages.value = loadedMessages + return loadedMessages + } catch (error: any) { + console.error('Load messages failed:', error) + ElMessage.error('加载消息失败') + return [] + } finally { + isLoadingMessages.value = false + } + } + + const sendMessage = async (data: ChatRequest) => { + try { + console.log('sendMessage called with data:', data) + isStreaming.value = true + + // Add user message to local state immediately + const userMessage: Message = { + id: `temp-${Date.now()}`, + conversation_id: data.conversation_id || '', + role: 'user', + content: data.message, + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(userMessage) + + console.log('Calling chatApi.sendMessage...') + // Convert conversation_id to string for API call + const conversationId = String(data.conversation_id!) + const response = await chatApi.sendMessage(conversationId, data) + console.log('API response:', response) + const { user_message, assistant_message } = response.data + + // Remove temp user message and add real messages + messages.value = messages.value.filter(msg => msg.id !== userMessage.id) + + // Add both user and assistant messages from response + messages.value.push(user_message) + messages.value.push(assistant_message) + + // Update current conversation if needed + if (currentConversation.value) { + const index = conversations.value.findIndex(conv => conv.id === currentConversation.value!.id) + if (index !== -1) { + // Update conversation in list + conversations.value[index] = { ...conversations.value[index], updated_at: new Date().toISOString() } + } + } + + return { user_message, assistant_message } + } catch (error: any) { + console.error('Send message failed:', error) + console.error('Error details:', error.response?.data || error.message) + ElMessage.error('发送消息失败: ' + (error.response?.data?.detail || error.message)) + + // Remove temp user message on error + messages.value = messages.value.filter(msg => !msg.id.startsWith('temp-')) + + return null + } finally { + isStreaming.value = false + } + } + + const sendMessageStream = async (data: ChatRequest, onChunk?: (chunk: string) => void) => { + try { + // 防止重复调用 + if (isStreaming.value) { + console.warn('Already streaming, ignoring duplicate call') + return + } + + isStreaming.value = true + + // Add user message immediately + const userMessage: Message = { + id: Date.now(), + conversation_id: data.conversation_id || 0, + role: 'user', + content: data.message, + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(userMessage) + + // Add assistant message placeholder + const assistantMessage: Message = { + id: Date.now() + 1, + conversation_id: data.conversation_id || 0, + role: 'assistant', + content: '', + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(assistantMessage) + + // Convert conversation_id to string for API call + const conversationId = String(data.conversation_id!) + const stream = await chatApi.sendMessageStreamFetch(conversationId, data) + if (!stream) { + throw new Error('Failed to get stream') + } + + const reader = stream.getReader() + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n') + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6).trim() + if (data === '[DONE]') { + break + } + + try { + const parsed = JSON.parse(data) + + // 处理智能体模式的流式响应 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + // 初始化智能体数据结构 + if (!messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data = { + status: 'thinking', + steps: [], + current_tool: null + } + } + + const agentData = messages.value[lastMessageIndex].agent_data + + if (parsed.type === 'thinking') { + // 处理思考过程 + console.log('🤔 Thinking:', parsed.content) + agentData.status = 'thinking' + agentData.steps.push({ + id: `thinking_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'thinking', + content: parsed.content, + timestamp: new Date().toISOString(), + node_name: parsed.node_name || 'agent', + raw_output: parsed.raw_output + }) + + } else if (parsed.type === 'tools_end') { + // 处理工具执行完成 + console.log('🔧 Tools end:', parsed.content) + agentData.status = 'tool_calling' + agentData.steps.push({ + id: `tools_end_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'tools_end', + content: parsed.content, + timestamp: new Date().toISOString(), + node_name: parsed.node_name || 'tools', + tool_name: parsed.tool_name, + tool_output: parsed.tool_output + }) + + } else if (parsed.type === 'response_start') { + // 处理开始输出 + console.log('💬 Response start:', parsed.content) + agentData.status = 'responding' + agentData.steps.push({ + id: `response_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response_start', + content: '正在输出', + timestamp: new Date().toISOString() + }) + + } else if (parsed.type === 'response') { + // 处理流式响应内容,同时更新消息内容和steps中的response步骤 + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content = parsed.content + + // 在agent_data.steps中创建或更新response步骤 + const steps = agentData.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 更新已存在的response步骤内容 + steps[existingResponseIndex] = { + ...steps[existingResponseIndex], + content: parsed.content + } + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + + } else if (parsed.type === 'complete') { + // 处理完成状态 + console.log('✅ Complete:', parsed.content) + agentData.status = 'completed' + + // 更新最后一个response_start步骤为complete + const lastResponseStartIndex = agentData.steps.findLastIndex(step => step.type === 'response_start') + if (lastResponseStartIndex >= 0) { + agentData.steps[lastResponseStartIndex] = { + ...agentData.steps[lastResponseStartIndex], + type: 'complete', + content: '本次对话过程完成' + } + } else { + agentData.steps.push({ + id: `complete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'complete', + content: '本次对话过程完成', + timestamp: new Date().toISOString() + }) + } + + // 设置最终消息内容 + messages.value[lastMessageIndex].content = parsed.content + + } else if (parsed.type === 'status') { + // 处理状态消息 + console.log('📊 Status:', parsed.content) + messages.value[lastMessageIndex].status = parsed.content + } + } + + // 处理其他类型的流式数据 + if (parsed.type === 'content') { + // 处理内容块 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + messages.value[lastMessageIndex].content += parsed.content + } + } else if (parsed.type === 'tool' || parsed.type === 'tool_result') { + // 工具执行相关消息 + console.log('Tool execution:', parsed.content) + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + // 显示工具执行状态 + messages.value[lastMessageIndex].status = parsed.content + } + } else if (parsed.type === 'content') { + // 流式内容块,累积到消息内容 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content += parsed.content + // 清除状态显示 + messages.value[lastMessageIndex].status = undefined + + // 更新智能体状态为生成中 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'generating' + } + } + onChunk?.(parsed.content) + } else if (parsed.type === 'response' && parsed.content) { + // 处理流式响应内容 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (parsed.done) { + // 完成时设置最终内容 + messages.value[lastMessageIndex].content = parsed.content + // 清除状态显示 + messages.value[lastMessageIndex].status = undefined + + // 更新智能体状态为完成 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'completed' + + // 移除已存在的response步骤,确保只有一个最终response + const steps = messages.value[lastMessageIndex].agent_data.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 替换已存在的response步骤 + steps[existingResponseIndex] = responseStep + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + } + + // 兼容旧的思考过程数据 + if (messages.value[lastMessageIndex].thinking_data) { + messages.value[lastMessageIndex].thinking_data.status = 'completed' + messages.value[lastMessageIndex].thinking_data.current_step = '思考完成' + } + + onChunk?.(parsed.content) + } else { + // 流式过程中,累积更新content实现打字机效果 + messages.value[lastMessageIndex].content = parsed.content + + // 更新智能体状态为生成中 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'responding' + + // 在流式过程中也要创建/更新response步骤 + const steps = messages.value[lastMessageIndex].agent_data.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 更新已存在的response步骤内容 + steps[existingResponseIndex] = { + ...steps[existingResponseIndex], + content: parsed.content + } + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + } + + onChunk?.(parsed.content) + } + } + } else if (parsed.content) { + // 兼容旧格式的流式响应 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content += parsed.content + } + onChunk?.(parsed.content) + } + + // 检查是否流式传输完成 + if (parsed.done === true || parsed.finish_reason) { + // 流式传输完成 + break + } + } catch (e) { + console.warn('Failed to parse SSE data:', data) + } + } + } + } + } finally { + reader.releaseLock() + } + + } catch (error: any) { + console.error('Stream message failed:', error) + ElMessage.error('发送消息失败: ' + (error.response?.data?.detail || error.message)) + + // Remove temp messages on error + messages.value = messages.value.filter(msg => msg.id < Date.now() - 1000) + } finally { + isStreaming.value = false + } + } + + const setCurrentConversation = async (conversationId: number) => { + try { + const conversation = await loadConversation(conversationId.toString()) + if (conversation) { + // 只有在消息为空且切换到不同对话时才加载消息 + // 如果当前有消息且是同一个对话,不要重新加载以保持消息连续性 + const shouldLoadMessages = messages.value.length === 0 && + (!currentConversation.value || currentConversation.value.id !== conversationId) + + if (shouldLoadMessages) { + await loadMessages(conversationId.toString()) + } + } + return conversation + } catch (error) { + console.error('Set current conversation failed:', error) + return null + } + } + + const clearCurrentConversation = () => { + currentConversation.value = null + messages.value = [] + } + + const clearMessages = () => { + messages.value = [] + } + + const addMessage = (message: Message) => { + messages.value.push(message) + } + + const archiveConversation = async (conversationId: string) => { + try { + await chatApi.archiveConversation(conversationId) + const conversation = conversations.value.find(c => c.id.toString() === conversationId) + if (conversation) { + conversation.is_archived = true + } + ElMessage.success('对话已归档') + } catch (error: any) { + console.error('Archive conversation failed:', error) + ElMessage.error('归档对话失败') + } + } + + const unarchiveConversation = async (conversationId: string) => { + try { + await chatApi.unarchiveConversation(conversationId) + const conversation = conversations.value.find(c => c.id.toString() === conversationId) + if (conversation) { + conversation.is_archived = false + } + ElMessage.success('对话已取消归档') + } catch (error: any) { + console.error('Unarchive conversation failed:', error) + ElMessage.error('取消归档对话失败') + } + } + + const setSearchQuery = (query: string) => { + searchQuery.value = query + } + + const setIncludeArchived = (include: boolean) => { + includeArchived.value = include + } + + const setCurrentPage = (page: number) => { + currentPage.value = page + } + + return { + // State + conversations, + currentConversation, + messages, + isLoading, + isStreaming, + isLoadingMessages, + searchQuery, + includeArchived, + totalCount, + currentPage, + pageSize, + + // Getters + sortedConversations, + activeConversations, + archivedConversations, + + // Actions + loadConversations, + createConversation, + loadConversation, + updateConversation, + deleteConversation, + deleteAllConversations, + loadMessages, + sendMessage, + sendMessageStream, + setCurrentConversation, + clearCurrentConversation, + clearMessages, + addMessage, + archiveConversation, + unarchiveConversation, + setSearchQuery, + setIncludeArchived, + setCurrentPage + } +}) \ No newline at end of file diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts new file mode 100644 index 0000000..ec1b93d --- /dev/null +++ b/frontend/src/stores/index.ts @@ -0,0 +1,4 @@ +// Export all stores +export { useUserStore } from './user' +export { useChatStore } from './chat' +export { useKnowledgeStore } from './knowledge' \ No newline at end of file diff --git a/frontend/src/stores/knowledge.ts b/frontend/src/stores/knowledge.ts new file mode 100644 index 0000000..b0ae8af --- /dev/null +++ b/frontend/src/stores/knowledge.ts @@ -0,0 +1,312 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { knowledgeApi } from '@/api' +import type { + KnowledgeBase, + KnowledgeBaseCreate, + KnowledgeBaseUpdate, + Document, + SearchRequest, + SearchResult +} from '@/types' + +export const useKnowledgeStore = defineStore('knowledge', () => { + // State + const knowledgeBases = ref([]) + const currentKnowledgeBase = ref(null) + const documents = ref([]) + const searchResults = ref([]) + const isLoading = ref(false) + const isLoadingDocuments = ref(false) + const isUploading = ref(false) + const isSearching = ref(false) + + // Getters + const activeKnowledgeBases = computed(() => { + return knowledgeBases.value.filter(kb => kb.is_active) + }) + + const sortedKnowledgeBases = computed(() => { + return [...knowledgeBases.value].sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ) + }) + + const documentsByStatus = computed(() => { + const grouped = { + pending: [] as Document[], + processing: [] as Document[], + completed: [] as Document[], + failed: [] as Document[] + } + + documents.value.forEach(doc => { + if (doc.status in grouped) { + grouped[doc.status as keyof typeof grouped].push(doc) + } + }) + + return grouped + }) + + // Actions + const loadKnowledgeBases = async () => { + try { + isLoading.value = true + const response = await knowledgeApi.getKnowledgeBases() + knowledgeBases.value = response.data || [] + } catch (error: any) { + console.error('Load knowledge bases failed:', error) + ElMessage.error('加载知识库列表失败') + } finally { + isLoading.value = false + } + } + + const createKnowledgeBase = async (data: KnowledgeBaseCreate) => { + try { + isLoading.value = true + const response = await knowledgeApi.createKnowledgeBase(data) + const newKnowledgeBase = response.data + + knowledgeBases.value.unshift(newKnowledgeBase) + currentKnowledgeBase.value = newKnowledgeBase + + ElMessage.success('创建知识库成功') + return newKnowledgeBase + } catch (error: any) { + console.error('Create knowledge base failed:', error) + ElMessage.error('创建知识库失败') + return null + } finally { + isLoading.value = false + } + } + + const loadKnowledgeBase = async (knowledgeBaseId: string) => { + try { + isLoading.value = true + const response = await knowledgeApi.getKnowledgeBase(knowledgeBaseId) + currentKnowledgeBase.value = response.data + + // Update in list if exists + const index = knowledgeBases.value.findIndex(kb => kb.id.toString() === knowledgeBaseId) + if (index !== -1) { + knowledgeBases.value[index] = currentKnowledgeBase.value + } + + return currentKnowledgeBase.value + } catch (error: any) { + console.error('Load knowledge base failed:', error) + ElMessage.error('加载知识库失败') + return null + } finally { + isLoading.value = false + } + } + + const updateKnowledgeBase = async (knowledgeBaseId: string, data: KnowledgeBaseUpdate) => { + try { + const response = await knowledgeApi.updateKnowledgeBase(knowledgeBaseId, data) + const updatedKnowledgeBase = response.data + + // Update in list + const index = knowledgeBases.value.findIndex(kb => kb.id.toString() === knowledgeBaseId) + if (index !== -1) { + knowledgeBases.value[index] = updatedKnowledgeBase + } + + // Update current if it's the same + if (currentKnowledgeBase.value?.id.toString() === knowledgeBaseId) { + currentKnowledgeBase.value = updatedKnowledgeBase + } + + ElMessage.success('知识库更新成功') + return updatedKnowledgeBase + } catch (error: any) { + console.error('Update knowledge base failed:', error) + ElMessage.error('更新知识库失败') + return null + } + } + + const deleteKnowledgeBase = async (knowledgeBaseId: string) => { + try { + await knowledgeApi.deleteKnowledgeBase(knowledgeBaseId) + + // Remove from list + knowledgeBases.value = knowledgeBases.value.filter(kb => kb.id.toString() !== knowledgeBaseId) + + // Clear current if it's the same + if (currentKnowledgeBase.value?.id.toString() === knowledgeBaseId) { + currentKnowledgeBase.value = null + documents.value = [] + } + + ElMessage.success('知识库删除成功') + return true + } catch (error: any) { + console.error('Delete knowledge base failed:', error) + ElMessage.error('删除知识库失败') + return false + } + } + + const loadDocuments = async (knowledgeBaseId: string) => { + try { + isLoadingDocuments.value = true + const response = await knowledgeApi.getDocuments(knowledgeBaseId) + // 后端返回的是 DocumentListResponse 格式: {documents: [], total, page, page_size} + const documentList = response.data?.documents || response.data || [] + documents.value = documentList + return documents.value + } catch (error: any) { + console.error('Load documents failed:', error) + ElMessage.error('加载文档列表失败') + return [] + } finally { + isLoadingDocuments.value = false + } + } + + const uploadDocument = async (knowledgeBaseId: string, file: File) => { + try { + isUploading.value = true + const response = await knowledgeApi.uploadDocument(knowledgeBaseId, file) + const newDocument = response.data + + documents.value.unshift(newDocument) + + ElMessage.success('文档上传成功') + return newDocument + } catch (error: any) { + console.error('Upload document failed:', error) + ElMessage.error('文档上传失败') + return null + } finally { + isUploading.value = false + } + } + + const deleteDocument = async (knowledgeBaseId: string, documentId: string) => { + try { + await knowledgeApi.deleteDocument(knowledgeBaseId, documentId) + + // Remove from list + documents.value = documents.value.filter(doc => doc.id.toString() !== documentId) + + ElMessage.success('文档删除成功') + return true + } catch (error: any) { + console.error('Delete document failed:', error) + ElMessage.error('删除文档失败') + return false + } + } + + const processDocument = async (knowledgeBaseId: string, documentId: string) => { + try { + const response = await knowledgeApi.processDocument(knowledgeBaseId, documentId) + const updatedDocument = response.data + + // Update in list + const index = documents.value.findIndex(doc => doc.id.toString() === documentId) + if (index !== -1) { + documents.value[index] = updatedDocument + } + + ElMessage.success('文档处理已开始') + return updatedDocument + } catch (error: any) { + console.error('Process document failed:', error) + ElMessage.error('文档处理失败') + return null + } + } + + const searchKnowledgeBase = async (data: SearchRequest) => { + try { + isSearching.value = true + const response = await knowledgeApi.searchKnowledgeBase(data) + searchResults.value = response.data || [] + return searchResults.value + } catch (error: any) { + console.error('Search knowledge base failed:', error) + ElMessage.error('搜索失败') + return [] + } finally { + isSearching.value = false + } + } + + const clearSearchResults = () => { + searchResults.value = [] + } + + const clearCurrentKnowledgeBase = () => { + currentKnowledgeBase.value = null + documents.value = [] + searchResults.value = [] + } + + const getDocumentChunks = async (documentId: string) => { + try { + // 从当前文档列表中找到对应的知识库ID + const document = documents.value.find(doc => doc.id.toString() === documentId) + if (!document || !document.knowledge_base_id) { + throw new Error('Document not found or missing knowledge base ID') + } + + const response = await knowledgeApi.getDocumentChunks(document.knowledge_base_id.toString(), documentId) + return response.data?.chunks || response.data || [] + } catch (error: any) { + console.error('Get document chunks failed:', error) + ElMessage.error('获取文档分段失败') + return [] + } + } + + const updateDocumentStatus = (documentId: string, status: Document['status'], chunks?: number) => { + const index = documents.value.findIndex(doc => doc.id.toString() === documentId) + if (index !== -1) { + documents.value[index].status = status + if (chunks !== undefined) { + documents.value[index].chunks = chunks + } + } + } + + return { + // State + knowledgeBases, + currentKnowledgeBase, + documents, + searchResults, + isLoading, + isLoadingDocuments, + isUploading, + isSearching, + + // Getters + activeKnowledgeBases, + sortedKnowledgeBases, + documentsByStatus, + + // Actions + loadKnowledgeBases, + createKnowledgeBase, + loadKnowledgeBase, + updateKnowledgeBase, + deleteKnowledgeBase, + loadDocuments, + uploadDocument, + deleteDocument, + processDocument, + getDocumentChunks, + searchKnowledgeBase, + clearSearchResults, + clearCurrentKnowledgeBase, + updateDocumentStatus + } +}) \ No newline at end of file diff --git a/frontend/src/stores/menu.ts b/frontend/src/stores/menu.ts new file mode 100644 index 0000000..17ef010 --- /dev/null +++ b/frontend/src/stores/menu.ts @@ -0,0 +1,113 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useUserStore } from './user' + +export interface MenuItem { + key: string + label: string + icon: string + route: string + expandable?: boolean + children?: MenuItem[] + requires_admin?: boolean +} + +export const useMenuStore = defineStore('menu', () => { + // State + const userStore = useUserStore() + + // Getters + const upperNavItems = computed(() => { + const items: MenuItem[] = [ + { + key: 'chat', + label: '智能问答', + icon: 'ChatDotRound', + route: '/chat' + }, + { + key: 'smart-query', + label: '智能问数', + icon: 'DataAnalysis', + route: '/smart-query' + }, + { + key: 'creation', + label: '智能创作', + icon: 'EditPen', + route: '/creation' + } + ] + + return items + }) + + const lowerNavItems = computed(() => { + const items: MenuItem[] = [ + { + key: 'knowledge', + label: '知识库', + icon: 'Collection', + route: '/knowledge' + }, + { + key: 'workflow', + label: '工作流编排', + icon: 'Connection', + route: '/workflow' + }, + { + key: 'agent', + label: '智能体管理', + icon: 'Cpu', + route: '/agent' + }, + { + key: 'mcp-service', + label: 'MCP服务管理', + icon: 'Link', + route: '/mcp-service' + }, + ] + + // 如果是超级管理员,添加系统管理菜单 + if (userStore.user?.is_superuser) { + items.push({ + key: 'system', + label: '系统管理', + icon: 'Setting', + route: '/system', + expandable: true, + children: [ + { + key: 'users', + label: '用户管理', + icon: 'User', + route: '/system/users' + }, + { + key: 'roles', + label: '角色管理', + icon: 'UserFilled', + route: '/system/roles' + }, + { + key: 'llm-configs', + label: '大模型管理', + icon: 'Cpu', + route: '/system/llm-configs' + } + ], + requires_admin: true + }) + } + + return items + }) + + return { + // Getters + upperNavItems, + lowerNavItems + } +}) \ No newline at end of file diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..e41e31c --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,241 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { authApi, usersApi, rolesApi } from '@/api' +import type { User, UserLogin, UserCreate, UserUpdate, LoginRequest, Role } from '@/types' + +export const useUserStore = defineStore('user', () => { + // State + const user = ref(null) + const isLoading = ref(false) + + // Getters + const isAuthenticated = computed(() => { + // 主要检查token是否存在,用户信息可以稍后获取 + const hasToken = !!localStorage.getItem('access_token') + console.log('检查认证状态:', { hasToken, hasUser: !!user.value }) + return hasToken + }) + + const isAdmin = computed(() => { + if (!user.value) return false + + // Check if user has admin role + if (user.value.roles) { + return user.value.roles.some(role => + role.is_active && (role.code === 'SUPER_ADMIN' || role.code === 'ADMIN' || role.code === 'AAA') + ) + } + + return false + }) + + // Actions + const login = async (credentials: LoginRequest) => { + try { + isLoading.value = true + console.log('开始登录流程...') + + const response = await authApi.login(credentials) + const { access_token, token_type } = response.data + console.log('登录API调用成功,获取到token') + + // Store tokens + localStorage.setItem('access_token', access_token) + // Note: Backend doesn't return refresh_token yet, using access_token as placeholder + localStorage.setItem('refresh_token', access_token) + console.log('Token已保存到localStorage') + + // Get user info + try { + await getCurrentUser() + console.log('用户信息获取成功,登录流程完成') + return true + } catch (userError: any) { + console.error('获取用户信息失败,但登录token有效:', userError) + // 即使获取用户信息失败,如果token有效,仍然认为登录成功 + ElMessage.warning('登录成功,但获取用户信息失败,请刷新页面') + return true + } + } catch (error: any) { + console.error('Login failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '登录失败') + return false + } finally { + isLoading.value = false + } + } + + const register = async (userData: UserCreate) => { + try { + isLoading.value = true + const response = await authApi.register(userData) + + ElMessage.success('注册成功,请登录') + return true + } catch (error: any) { + console.error('Registration failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '注册失败') + return false + } finally { + isLoading.value = false + } + } + + const getCurrentUser = async () => { + try { + const response = await authApi.getCurrentUser() + user.value = response.data + + // Get user roles + if (user.value) { + await getUserRoles() + } + + return user.value + } catch (error: any) { + console.error('Get current user failed:', error) + + // Don't clear tokens on any error, let the user re-login to update tokens + if (error.response && error.response.status === 401) { + console.log('Authentication failed, but keeping tokens for re-login') + user.value = null + } else { + console.log('Non-authentication error, keeping tokens and user state') + } + + throw error + } + } + + const getUserRoles = async () => { + try { + if (!user.value) return + + const response = await rolesApi.getUserRoles(user.value.id) + if (user.value) { + user.value.roles = response.data + } + } catch (error: any) { + console.error('Get user roles failed:', error) + // Don't throw error, just log it as roles are optional + } + } + + const updateProfile = async (userData: UserUpdate) => { + try { + isLoading.value = true + const response = await usersApi.updateProfile(userData) + user.value = response.data + + ElMessage.success('个人资料更新成功') + return true + } catch (error: any) { + console.error('Update profile failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '更新失败') + return false + } finally { + isLoading.value = false + } + } + + const changePassword = async (passwordData: { current_password: string; new_password: string }) => { + try { + isLoading.value = true + await usersApi.changePassword(passwordData) + + ElMessage.success('密码修改成功') + return true + } catch (error: any) { + console.error('Change password failed:', error) + // API拦截器已经处理了错误消息显示,这里不需要重复显示 + throw error // 重新抛出错误,让调用方知道操作失败 + } finally { + isLoading.value = false + } + } + + const logout = async () => { + try { + await authApi.logout() + user.value = null + ElMessage.success('已退出登录') + } catch (error) { + console.error('Logout error:', error) + } + } + + const refreshToken = async () => { + try { + const refreshToken = localStorage.getItem('refresh_token') + if (!refreshToken) { + throw new Error('No refresh token available') + } + + const response = await authApi.refreshToken(refreshToken) + const { access_token, token_type } = response.data + + localStorage.setItem('access_token', access_token) + // Note: Backend doesn't return refresh_token yet, using access_token as placeholder + localStorage.setItem('refresh_token', access_token) + + return true + } catch (error) { + console.error('Token refresh failed:', error) + await logout() + return false + } + } + + const deleteAccount = async () => { + try { + isLoading.value = true + await usersApi.deleteAccount() + await logout() + + ElMessage.success('账户已删除') + return true + } catch (error: any) { + console.error('Delete account failed:', error) + ElMessage.error(error.message || '删除账户失败') + return false + } finally { + isLoading.value = false + } + } + + // Initialize user on store creation + const initializeUser = async () => { + const token = localStorage.getItem('access_token') + if (token && !user.value) { + try { + await getCurrentUser() + } catch (error) { + // Token is invalid, will be cleared in getCurrentUser + console.log('Failed to initialize user with stored token') + } + } + } + + return { + // State + user, + isLoading, + + // Getters + isAuthenticated, + isAdmin, + + // Actions + login, + register, + getCurrentUser, + getUserRoles, + updateProfile, + changePassword, + logout, + refreshToken, + deleteAccount, + initializeUser + } +}) \ No newline at end of file diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss new file mode 100644 index 0000000..6aa4f5f --- /dev/null +++ b/frontend/src/styles/index.scss @@ -0,0 +1,229 @@ +// Global styles for openAgent + +// Custom CSS variables +:root { + // Colors + --chat-primary-color: #409eff; + --chat-success-color: #67c23a; + --chat-warning-color: #e6a23c; + --chat-danger-color: #f56c6c; + --chat-info-color: #909399; + + // Chat specific colors + --chat-bg-color: #f5f7fa; + --chat-sidebar-bg: #ffffff; + --chat-message-user-bg: #409eff; + --chat-message-assistant-bg: #f0f2f5; + --chat-message-system-bg: #e1f3d8; + + // Spacing + --chat-padding-sm: 8px; + --chat-padding-md: 16px; + --chat-padding-lg: 24px; + --chat-padding-xl: 32px; + + // Border radius + --chat-border-radius-sm: 4px; + --chat-border-radius-md: 8px; + --chat-border-radius-lg: 12px; + + // Shadows + --chat-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1); + --chat-shadow-medium: 0 4px 8px rgba(0, 0, 0, 0.15); + --chat-shadow-heavy: 0 8px 16px rgba(0, 0, 0, 0.2); + + // Transitions + --chat-transition-fast: 0.2s ease; + --chat-transition-normal: 0.3s ease; + --chat-transition-slow: 0.5s ease; +} + +// Reset and base styles +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--chat-bg-color); + line-height: 1.6; +} + +#app { + height: 100vh; + overflow: hidden; +} + +// Chat specific styles +.chat-layout { + height: 100vh; + display: flex; + overflow: hidden; +} + +.chat-sidebar { + width: 280px; + background: var(--chat-sidebar-bg); + border-right: 1px solid #e4e7ed; + display: flex; + flex-direction: column; +} + +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--chat-padding-md); + background: var(--chat-bg-color); +} + +.chat-input { + padding: var(--chat-padding-md); + border-top: 1px solid #e4e7ed; + background: #ffffff; +} + +.message-item { + margin-bottom: var(--chat-padding-md); + display: flex; + align-items: flex-start; + gap: var(--chat-padding-sm); +} + +.message-item.user { + flex-direction: row-reverse; +} + +.message-content { + max-width: 70%; + padding: var(--chat-padding-sm) var(--chat-padding-md); + border-radius: var(--chat-border-radius-lg); + word-wrap: break-word; + line-height: 1.5; +} + +.message-content.user { + background: var(--chat-message-user-bg); + color: white; +} + +.message-content.assistant { + background: var(--chat-message-assistant-bg); + color: #303133; +} + +.message-content.system { + background: var(--chat-message-system-bg); + color: #303133; + font-style: italic; +} + +// Input styles +.el-input { + &.el-input--prefix, + &.el-input--suffix { + .el-input__inner { + padding-left: 36px; + padding-right: 36px; + } + } + + &.el-input--prefix { + .el-input__inner { + padding-left: 36px; + } + } + + &.el-input--suffix { + .el-input__inner { + padding-right: 36px; + } + } + + // Search input styles with non-white background + .el-input__inner { + background-color: #f0f2f5; // Use the same background as assistant messages + border: 1px solid #dcdfe6; + transition: all 0.3s ease; + + &:focus { + background-color: #ffffff; + border-color: var(--chat-primary-color); + box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); + } + } +} + +// Utility classes +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.flex-1 { + flex: 1; +} + +.w-full { + width: 100%; +} + +.h-full { + height: 100%; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.text-center { + text-align: center; +} + +.cursor-pointer { + cursor: pointer; +} + +// Responsive design +@media (max-width: 768px) { + .chat-sidebar { + width: 100%; + position: absolute; + z-index: 1000; + height: 100%; + transform: translateX(-100%); + transition: transform 0.3s ease; + } + + .chat-sidebar.open { + transform: translateX(0); + } + + .message-content { + max-width: 85%; + } +} \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..dcd933a --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,440 @@ +// API Response Types +export interface ApiResponse { + success: boolean + data?: T + message?: string + error?: string +} + +// User Types +export interface User { + id: number + username: string + email: string + full_name?: string + is_active: boolean + is_admin: boolean + department_id?: number + avatar_url?: string + bio?: string + created_at: string + updated_at: string + roles?: Role[] +} + +// Role Types +export interface Role { + id: number + name: string + code: string + description?: string + is_active: boolean + permissions?: Permission[] +} + +export interface Permission { + id: number + name: string + code: string + description?: string + category: string +} + +// Department Types +export interface Department { + id: number + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active: boolean + created_at: string + updated_at: string + manager?: { + id: number + username: string + full_name?: string + } + children?: Department[] + user_count?: number +} + +export interface UserCreate { + username: string + email: string + password: string + full_name?: string +} + +export interface UserUpdate { + username?: string + email?: string + full_name?: string + bio?: string + avatar_url?: string +} + +export interface UserLogin { + username: string + password: string +} + +export interface LoginRequest { + email: string + password: string +} + +export interface AuthTokens { + access_token: string + refresh_token: string + token_type: string +} + +// Conversation Types +export interface Conversation { + id: number + title: string + user_id: number + knowledge_base_id?: number + system_prompt?: string + model_name: string + temperature: string + max_tokens: number + is_archived: boolean + created_at: string + updated_at: string + message_count?: number + last_message_at?: string +} + +export interface ConversationCreate { + title: string + knowledge_base_id?: number + system_prompt?: string + model_name?: string + temperature?: string + max_tokens?: number +} + +export interface ConversationUpdate { + title?: string + knowledge_base_id?: number + system_prompt?: string + model_name?: string + temperature?: string + max_tokens?: number + is_archived?: boolean +} + +// Message Types +export type MessageRole = 'user' | 'assistant' | 'system' +export type MessageType = 'text' | 'image' | 'file' | 'audio' + +export interface Message { + id: number + conversation_id: number + role: MessageRole + content: string + message_type: MessageType + metadata?: Record + context_documents?: string[] + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + created_at: string + updated_at: string + status?: string // 用于显示流式状态,如"正在思考..."、"正在使用工具..."等 + thinking_data?: ThinkingData // LangGraph智能体思考过程数据 +} + +// LangGraph思考过程数据结构 +export interface ThinkingData { + steps: ThinkingStep[] + tool_calls: ToolCall[] + current_step: string + status: 'thinking' | 'tool_calling' | 'completed' | 'error' +} + +export interface ThinkingStep { + id: number + type: 'thinking' | 'tool_start' | 'tool_end' + content: string + timestamp: string + node_name?: string + tool_call?: ToolCall +} + +export interface ToolCall { + id: number + name: string + input: Record + output: any + status: 'running' | 'completed' | 'error' + timestamp: string +} + +export interface MessageCreate { + content: string + message_type?: MessageType + metadata?: Record +} + +export interface ChatRequest { + message: string + conversation_id?: number + stream?: boolean + use_knowledge_base?: boolean + knowledge_base_id?: number + use_agent?: boolean + use_langgraph?: boolean + temperature?: number + max_tokens?: number +} + +export interface ChatResponse { + user_message: Message + assistant_message: Message + total_tokens?: number + model_used: string +} + +// Knowledge Base Types +export interface KnowledgeBase { + id: number + name: string + description?: string + embedding_model: string + chunk_size: number + chunk_overlap: number + is_active: boolean + vector_db_type: string + collection_name: string + created_at: string + updated_at: string + document_count?: number +} + +export interface KnowledgeBaseCreate { + name: string + description?: string + embedding_model?: string + chunk_size?: number + chunk_overlap?: number +} + +export interface KnowledgeBaseUpdate { + name?: string + description?: string + embedding_model?: string + chunk_size?: number + chunk_overlap?: number + is_active?: boolean +} + +// Document Types +export type DocumentStatus = 'pending' | 'processing' | 'completed' | 'failed' + +export interface Document { + id: number + knowledge_base_id: number + filename: string + original_filename: string + file_path: string + file_size: number + file_type: string + mime_type: string + status: DocumentStatus + processing_error?: string + content?: string + metadata?: Record + chunk_count?: number + embedding_info?: Record + created_at: string + updated_at: string +} + +export interface DocumentUpload { + file: File + knowledge_base_id: number +} + +export interface DocumentListResponse { + documents: Document[] + total: number + page: number + page_size: number +} + +// Document Chunk Types +export interface DocumentChunk { + id: string + content: string + metadata: Record + page_number?: number + chunk_index: number + start_char?: number + end_char?: number +} + +export interface DocumentChunksResponse { + document_id: number + document_name: string + total_chunks: number + chunks: DocumentChunk[] +} + +export interface SearchRequest { + query: string + knowledge_base_id: number + top_k?: number + score_threshold?: number +} + +export interface SearchResult { + content: string + metadata: Record + score: number + document_id: number +} + +// UI State Types +export interface ChatState { + conversations: Conversation[] + currentConversation: Conversation | null + messages: Message[] + isLoading: boolean + isStreaming: boolean +} + +export interface KnowledgeState { + knowledgeBases: KnowledgeBase[] + currentKnowledgeBase: KnowledgeBase | null + documents: Document[] + isLoading: boolean +} + +export interface UserState { + user: User | null + isAuthenticated: boolean + isLoading: boolean +} + +// Form Types +export interface LoginForm { + username: string + password: string + remember: boolean +} + +export interface RegisterForm { + username: string + email: string + password: string + confirmPassword: string + fullName: string +} + +export interface ProfileForm { + username: string + email: string + fullName: string + bio: string +} + +// Component Props Types +export interface MessageItemProps { + message: Message + isStreaming?: boolean +} + +export interface ConversationItemProps { + conversation: Conversation + isActive?: boolean +} + +export interface KnowledgeBaseItemProps { + knowledgeBase: KnowledgeBase + isActive?: boolean +} + +export interface DocumentItemProps { + document: Document +} + +// Workflow Types +export interface NodeParameter { + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + description?: string + required: boolean + default_value?: any + source?: 'input' | 'node' | 'variable' + source_node_id?: string + source_field?: string +} + +export interface NodeInputOutput { + inputs: NodeParameter[] + outputs: NodeParameter[] +} + +export interface WorkflowNode { + id: string + type: string + name: string + description?: string + x: number + y: number + config: Record + parameters?: NodeInputOutput + result?: { + success: boolean + data?: any + error?: string + } +} + +export interface WorkflowConnection { + id: string + from: string + to: string + fromPoint: string + toPoint: string +} + +export interface Workflow { + id: number + name: string + description?: string + definition: { + nodes: WorkflowNode[] + connections: WorkflowConnection[] + } + status: string + is_active: boolean + created_at: string + updated_at: string +} + +// Utility Types +export type LoadingState = 'idle' | 'loading' | 'success' | 'error' + +export interface PaginationParams { + page?: number + size?: number + skip?: number + limit?: number +} + +export interface SortParams { + sort_by?: string + sort_order?: 'asc' | 'desc' +} + +export interface FilterParams { + search?: string + status?: string + is_active?: boolean + [key: string]: any +} \ No newline at end of file diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts new file mode 100644 index 0000000..f38dc62 --- /dev/null +++ b/frontend/src/utils/date.ts @@ -0,0 +1,72 @@ +/** + * 日期时间工具函数 + */ + +/** + * 格式化日期时间 + * @param date 日期字符串或Date对象 + * @param format 格式化模板,默认为 'YYYY-MM-DD HH:mm:ss' + * @returns 格式化后的日期字符串 + */ +export function formatDateTime(date: string | Date | null | undefined, format: string = 'YYYY-MM-DD HH:mm:ss'): string { + if (!date) return '-' + + const d = typeof date === 'string' ? new Date(date) : date + + if (isNaN(d.getTime())) return '-' + + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hours = String(d.getHours()).padStart(2, '0') + const minutes = String(d.getMinutes()).padStart(2, '0') + const seconds = String(d.getSeconds()).padStart(2, '0') + + return format + .replace('YYYY', String(year)) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds) +} + +/** + * 格式化日期(不包含时间) + * @param date 日期字符串或Date对象 + * @returns 格式化后的日期字符串 + */ +export function formatDate(date: string | Date | null | undefined): string { + return formatDateTime(date, 'YYYY-MM-DD') +} + +/** + * 格式化时间(不包含日期) + * @param date 日期字符串或Date对象 + * @returns 格式化后的时间字符串 + */ +export function formatTime(date: string | Date | null | undefined): string { + return formatDateTime(date, 'HH:mm:ss') +} + +/** + * 获取相对时间描述 + * @param date 日期字符串或Date对象 + * @returns 相对时间描述,如"刚刚"、"5分钟前"等 + */ +export function getRelativeTime(date: string | Date | null | undefined): string { + if (!date) return '-' + + const d = typeof date === 'string' ? new Date(date) : date + if (isNaN(d.getTime())) return '-' + + const now = new Date() + const diff = now.getTime() - d.getTime() + + if (diff < 60000) return '刚刚' + if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前` + if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前` + if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前` + + return formatDate(date) +} \ No newline at end of file diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..2024a88 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,315 @@ +// Utility functions for TH-Agenter + +/** + * Format time to readable string + */ +export const formatTime = (date: string | Date): string => { + const d = new Date(date) + return d.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }) +} + +/** + * Format date to readable string + */ +export const formatDate = (date: string | Date, format: 'full' | 'date' | 'time' | 'relative' = 'relative'): string => { + const d = new Date(date) + const now = new Date() + const diff = now.getTime() - d.getTime() + + if (format === 'relative') { + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (seconds < 60) { + return '刚刚' + } else if (minutes < 60) { + return `${minutes}分钟前` + } else if (hours < 24) { + return `${hours}小时前` + } else if (days < 7) { + return `${days}天前` + } else { + return d.toLocaleDateString('zh-CN') + } + } + + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + } + + switch (format) { + case 'full': + return d.toLocaleString('zh-CN', options) + case 'date': + return d.toLocaleDateString('zh-CN') + case 'time': + return d.toLocaleTimeString('zh-CN') + default: + return d.toLocaleString('zh-CN', options) + } +} + +/** + * Format file size to readable string + */ +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * Debounce function + */ +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout + + return (...args: Parameters) => { + clearTimeout(timeout) + timeout = setTimeout(() => func.apply(null, args), wait) + } +} + +/** + * Throttle function + */ +export const throttle = any>( + func: T, + limit: number +): ((...args: Parameters) => void) => { + let inThrottle: boolean + + return (...args: Parameters) => { + if (!inThrottle) { + func.apply(null, args) + inThrottle = true + setTimeout(() => inThrottle = false, limit) + } + } +} + +/** + * Generate random ID + */ +export const generateId = (length: number = 8): string => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return result +} + +/** + * Copy text to clipboard + */ +export const copyToClipboard = async (text: string): Promise => { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + return true + } else { + // Fallback for older browsers + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + const result = document.execCommand('copy') + document.body.removeChild(textArea) + return result + } + } catch (error) { + console.error('Failed to copy text:', error) + return false + } +} + +/** + * Validate email format + */ +export const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} + +/** + * Validate password strength + */ +export const validatePassword = (password: string): { + isValid: boolean + errors: string[] +} => { + const errors: string[] = [] + + if (password.length < 8) { + errors.push('密码长度至少8位') + } + + if (!/[A-Z]/.test(password)) { + errors.push('密码必须包含至少一个大写字母') + } + + if (!/[a-z]/.test(password)) { + errors.push('密码必须包含至少一个小写字母') + } + + if (!/\d/.test(password)) { + errors.push('密码必须包含至少一个数字') + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.push('密码必须包含至少一个特殊字符') + } + + return { + isValid: errors.length === 0, + errors + } +} + +/** + * Truncate text with ellipsis + */ +export const truncateText = (text: string, maxLength: number): string => { + if (text.length <= maxLength) { + return text + } + return text.slice(0, maxLength) + '...' +} + +/** + * Get file extension from filename + */ +export const getFileExtension = (filename: string): string => { + return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2) +} + +/** + * Check if file type is supported + */ +export const isSupportedFileType = (filename: string, supportedTypes: string[]): boolean => { + const extension = getFileExtension(filename).toLowerCase() + return supportedTypes.includes(extension) +} + +/** + * Parse error message from API response + */ +export const parseErrorMessage = (error: any): string => { + if (typeof error === 'string') { + return error + } + + if (error?.response?.data?.message) { + return error.response.data.message + } + + if (error?.response?.data?.error) { + return error.response.data.error + } + + if (error?.response?.data?.detail) { + if (Array.isArray(error.response.data.detail)) { + return error.response.data.detail[0]?.msg || '请求参数错误' + } + return error.response.data.detail + } + + if (error?.message) { + return error.message + } + + return '未知错误' +} + +/** + * Sleep function for delays + */ +export const sleep = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +/** + * Check if running in development mode + */ +export const isDevelopment = (): boolean => { + return import.meta.env.MODE === 'development' +} + +/** + * Get API base URL + */ +export const getApiBaseUrl = (): string => { + return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api' +} + +/** + * Format conversation title + */ +export const formatConversationTitle = (title: string, maxLength: number = 30): string => { + if (!title || title.trim() === '') { + return '新对话' + } + return truncateText(title.trim(), maxLength) +} + +/** + * Get message type icon + */ +export const getMessageTypeIcon = (type: string): string => { + const icons: Record = { + text: 'ChatDotRound', + image: 'Picture', + file: 'Document', + audio: 'Microphone' + } + return icons[type] || 'ChatDotRound' +} + +/** + * Get document status color + */ +export const getDocumentStatusColor = (status: string): string => { + const colors: Record = { + pending: 'warning', + processing: 'primary', + completed: 'success', + failed: 'danger' + } + return colors[status] || 'info' +} + +/** + * Get document status text + */ +export const getDocumentStatusText = (status: string): string => { + const texts: Record = { + pending: '待处理', + processing: '处理中', + completed: '已完成', + failed: '处理失败' + } + return texts[status] || '未知状态' +} \ No newline at end of file diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue new file mode 100644 index 0000000..542efaf --- /dev/null +++ b/frontend/src/views/Chat.vue @@ -0,0 +1,1903 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Flow/WorkflowList.vue b/frontend/src/views/Flow/WorkflowList.vue new file mode 100644 index 0000000..e18a6c3 --- /dev/null +++ b/frontend/src/views/Flow/WorkflowList.vue @@ -0,0 +1,563 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Knowledge.vue b/frontend/src/views/Knowledge.vue new file mode 100644 index 0000000..3be2b4b --- /dev/null +++ b/frontend/src/views/Knowledge.vue @@ -0,0 +1,724 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..919a3f2 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,184 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/MCPServiceManagement.vue b/frontend/src/views/MCPServiceManagement.vue new file mode 100644 index 0000000..c5c9df1 --- /dev/null +++ b/frontend/src/views/MCPServiceManagement.vue @@ -0,0 +1,676 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue new file mode 100644 index 0000000..e03982e --- /dev/null +++ b/frontend/src/views/Profile.vue @@ -0,0 +1,374 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..159652d --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,203 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/SystemManagement.vue b/frontend/src/views/SystemManagement.vue new file mode 100644 index 0000000..1de667f --- /dev/null +++ b/frontend/src/views/SystemManagement.vue @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..cac055b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": [ + "vite/client", + "element-plus/global" + ], + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false + } +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..b10a208 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,85 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const base = process.argv.includes('--base') + ? process.argv[process.argv.indexOf('--base') + 1] + : '/'; // 默认值 + return { + base, + plugins: [ + vue(), + AutoImport({ + resolvers: [ElementPlusResolver()], + imports: [ + 'vue', + 'vue-router', + 'pinia', + { + axios: [ + ['default', 'axios'] + ] + } + ], + dts: true + }), + Components({ + resolvers: [ElementPlusResolver()] + }) + ], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + secure: false, + rewrite: (path) => path, + configure: (proxy, options) => { + proxy.on('proxyReq', (proxyReq, req, res) => { + // 确保Authorization头部被正确传递 + if (req.headers.authorization) { + proxyReq.setHeader('Authorization', req.headers.authorization); + } + }); + } + } + } + }, + build: { + outDir: 'dist', + sourcemap: false, + chunkSizeWarningLimit: 1600, + rollupOptions: { + output: { + manualChunks: { + 'vue-vendor': ['vue', 'vue-router', 'pinia', 'element-plus'], + } + } + } + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";"` + } + } + }, + // 定义环境变量 + define: { + __VITE_ENV__: JSON.stringify(env) + } + } +}) \ No newline at end of file