mirror of
https://github.com/jihe520/MathModelAgent.git
synced 2026-10-02 02:44:56 +08:00
refactor: 重构注释规范、修复类型错误、添加 lint hook 和 CLAUDE.md (#66)
- 后端按 Google Python Style Guide 重构注释,修复 ruff 和 pyright 错误(161→0) - 前端按 Vue.js Style Guide 重构注释,修复 biome 错误 - 添加 PostToolUse lint hook(ruff + biome 自动检查) - 更新 CLAUDE.md 覆盖 Commands/Testing/Structure/CodeStyle/GitWorkflow/Boundaries - 修复 tsconfig.json 废弃的 baseUrl 配置
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook: 根据修改的文件路径运行对应的 lint 检查
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# 读取 stdin JSON,用 sed 提取 file_path(避免 jq 吞掉 Windows 反斜杠)
|
||||
input=$(cat)
|
||||
file=$(echo "$input" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
# 将 Windows 反斜杠转为正斜杠,并合并连续斜杠
|
||||
file=$(echo "$file" | sed 's/\\/\//g' | tr -s '/')
|
||||
[[ -z "$file" ]] && exit 0
|
||||
file=$(echo "$file" | xargs)
|
||||
|
||||
# 去掉 REPO_ROOT 前缀得到相对路径
|
||||
# REPO_ROOT 格式:/c/Users/...(Git Bash),文件路径格式:C:/Users/...(Windows)
|
||||
norm_root="$REPO_ROOT"
|
||||
if [[ "$norm_root" =~ ^/([a-zA-Z])/(.*) ]]; then
|
||||
drive="${BASH_REMATCH[1]}"
|
||||
rest="${BASH_REMATCH[2]}"
|
||||
drive="${drive^^}"
|
||||
norm_root="${drive}:/${rest}"
|
||||
fi
|
||||
if [[ "$file" == "$norm_root"/* ]]; then
|
||||
file="${file#$norm_root/}"
|
||||
elif [[ "$file" == "$norm_root"* ]]; then
|
||||
file="${file#$norm_root}"
|
||||
file="${file#/}"
|
||||
elif [[ "$file" == "$REPO_ROOT"/* ]]; then
|
||||
file="${file#$REPO_ROOT/}"
|
||||
elif [[ "$file" == "$REPO_ROOT"* ]]; then
|
||||
file="${file#$REPO_ROOT}"
|
||||
file="${file#/}"
|
||||
fi
|
||||
|
||||
run_check() {
|
||||
local output
|
||||
local exit_code=0
|
||||
output=$("$@" 2>&1) || exit_code=$?
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
echo "$output" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
case "$file" in
|
||||
backend/*.py)
|
||||
cd "$REPO_ROOT/backend" || exit 0
|
||||
export PYTHONIOENCODING=utf-8
|
||||
run_check ./.venv/Scripts/python.exe -m ruff check app/
|
||||
;;
|
||||
frontend/src/*.vue|frontend/src/*.ts|frontend/src/*/*.vue|frontend/src/*/*.ts|frontend/src/*/*/*.vue|frontend/src/*/*/*.ts|frontend/src/*/*/*/*.vue|frontend/src/*/*/*/*.ts)
|
||||
cd "$REPO_ROOT/frontend" || exit 0
|
||||
run_check npx biome check "$file"
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(powershell -Command \"\\(Get-ChildItem -Path 'frontend\\\\src' -Recurse -Filter '*.vue'\\).Count\")",
|
||||
"Bash(powershell -Command \"\\(Get-ChildItem -Path 'frontend\\\\src' -Recurse -Filter '*.ts'\\).Count\")",
|
||||
"Bash(powershell *)",
|
||||
"Bash(.\\\\backend\\\\.venv\\\\Scripts\\\\python.exe -m ruff check backend/app/)",
|
||||
"Bash(npx biome *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -m ' *)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "bash",
|
||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hook_lint.sh\"",
|
||||
"timeout": 180,
|
||||
"statusMessage": "Running lint checks..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,4 +6,5 @@ backend/app/.DS_Store
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.idea/
|
||||
.idea/
|
||||
settings.local.json
|
||||
@@ -2,121 +2,177 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
## 项目概述
|
||||
|
||||
MathModelAgent is a mathematical modeling automation system designed specifically for math competitions. It automates the entire process of mathematical modeling, code generation, and paper writing to produce competition-ready submissions.
|
||||
MathModelAgent 是数学建模竞赛自动化系统,通过多 Agent 协作完成建模、代码生成和论文撰写。核心工作流:CoordinatorAgent 分析问题 → ModelerAgent 建模 → CoderAgent 编码执行 → WriterAgent 撰写论文。
|
||||
|
||||
## Common Commands
|
||||
## Commands
|
||||
|
||||
### 后端
|
||||
|
||||
### Backend Development
|
||||
```bash
|
||||
cd backend
|
||||
# Install dependencies using uv
|
||||
|
||||
# 安装依赖
|
||||
uv sync
|
||||
|
||||
# Activate virtual environment
|
||||
source .venv/bin/activate # macOS/Linux
|
||||
venv\Scripts\activate.bat # Windows
|
||||
|
||||
# Start development server
|
||||
# 启动开发服务器(需要先启动 Redis)
|
||||
ENV=DEV uvicorn app.main:app --host 0.0.0.0 --port 8000 --ws-ping-interval 60 --ws-ping-timeout 120 --reload
|
||||
|
||||
# Lint and format (using ruff)
|
||||
ruff check .
|
||||
ruff format .
|
||||
# Lint(使用虚拟环境中的 ruff)
|
||||
.\.venv\Scripts\python.exe -m ruff check app/
|
||||
.\.venv\Scripts\python.exe -m ruff format app/
|
||||
|
||||
# 类型检查
|
||||
npx pyright app/
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
### 前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
# Install dependencies
|
||||
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
|
||||
# Start development server
|
||||
# 启动开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# Build for production
|
||||
# 构建
|
||||
pnpm run build
|
||||
|
||||
# Lint
|
||||
npx biome check src/
|
||||
npx biome check --write src/ # 自动修复
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose up
|
||||
|
||||
# Run in background
|
||||
docker-compose up -d
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
docker-compose up -d # 后台启动
|
||||
docker-compose down # 停止
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
## 项目结构
|
||||
|
||||
### Project Structure
|
||||
- **frontend/**: Vue 3 + TypeScript + Vite web application
|
||||
- **backend/**: FastAPI-based Python backend with mathematical modeling logic
|
||||
- **docker-compose.yml**: Container orchestration for development and deployment
|
||||
```
|
||||
backend/
|
||||
app/
|
||||
core/
|
||||
agents/ # Agent 实现(继承 Agent 基类)
|
||||
agent.py # Agent 基类:对话历史、轮次控制、记忆压缩
|
||||
coordinator_agent.py # 任务分解
|
||||
modeler_agent.py # 数学建模
|
||||
coder_agent.py # 代码生成与执行
|
||||
writer_agent.py # 论文撰写
|
||||
llm/ # LLM 调用层(LiteLLM 封装)
|
||||
prompts/ # 各 Agent 的 prompt 模板
|
||||
flows.py # 编排逻辑(问题拆分、子任务管理)
|
||||
workflow.py # 工作流主入口
|
||||
routers/ # FastAPI 路由(REST + WebSocket)
|
||||
schemas/ # Pydantic 模型(请求/响应/枚举)
|
||||
services/ # Redis 管理、WebSocket 管理
|
||||
tools/ # 代码解释器(本地 Jupyter / E2B 云端)
|
||||
utils/ # 工具函数
|
||||
config/ # 配置(Pydantic Settings)
|
||||
|
||||
### Core Components
|
||||
frontend/
|
||||
src/
|
||||
apis/ # 后端 API 调用封装
|
||||
components/ # 通用组件 + shadcn-vue UI 库(components/ui/ 不要修改)
|
||||
pages/ # 页面组件(chat/、task/、login/)
|
||||
stores/ # Pinia 状态管理
|
||||
utils/ # 工具函数、类型定义、WebSocket 客户端
|
||||
```
|
||||
|
||||
#### Backend Architecture
|
||||
- **FastAPI Application**: Async web framework with WebSocket support
|
||||
- **Multi-Agent System**:
|
||||
- `ModelerAgent`: Analyzes problems and creates mathematical models
|
||||
- `CoderAgent`: Generates and executes Python code using Jupyter notebooks
|
||||
- `WriterAgent`: Composes academic papers from modeling results
|
||||
- **Code Interpreters**:
|
||||
- Local Jupyter-based interpreter (saves notebooks for editing)
|
||||
- Cloud interpreters (E2B, Daytona) for remote execution
|
||||
- **Task Management**: Redis-based queuing and status tracking
|
||||
- **LLM Integration**: LiteLLM for multi-model support across different agents
|
||||
## Code Style
|
||||
|
||||
#### Frontend Architecture
|
||||
- **Vue 3 Composition API** with TypeScript
|
||||
- **Pinia** for state management
|
||||
- **Tailwind CSS** for styling
|
||||
- **WebSocket Client**: Real-time task progress updates
|
||||
- **Multi-page Application**: Chat interface, task details, and configuration
|
||||
### 后端(Python)
|
||||
|
||||
### Key Workflow
|
||||
1. User uploads data files and provides problem description
|
||||
2. System creates modeling task with unique task_id
|
||||
3. Agents work sequentially:
|
||||
- Modeler analyzes problem and proposes mathematical approach
|
||||
- Coder implements solution in Jupyter notebooks
|
||||
- Writer generates formatted academic paper
|
||||
4. Real-time progress updates via WebSocket
|
||||
5. Results saved to `backend/project/work_dir/{task_id}/`
|
||||
- 模块级、类级、公共方法均使用 Google 风格 docstring(Args/Returns/Raises)
|
||||
- 类型注解:使用 `str | None` 而非 `Optional[str]`
|
||||
- 异步:全程 async/await,FastAPI 路由均为 async def
|
||||
- 注释:中文,解释 WHY 而非 WHAT
|
||||
|
||||
### Configuration Files
|
||||
- **Backend**: `pyproject.toml` for Python dependencies, `.env.dev` for environment
|
||||
- **Frontend**: `package.json` for Node.js dependencies, `.env.development` for config
|
||||
- **Docker**: `docker-compose.yml` orchestrates Redis, backend, and frontend services
|
||||
```python
|
||||
"""模块级 docstring:描述模块用途。"""
|
||||
|
||||
### Development Environment Requirements
|
||||
- Python 3.12+ (with uv for package management)
|
||||
- Node.js with pnpm
|
||||
- Redis server
|
||||
- Docker (optional, for containerized deployment)
|
||||
class ExampleAgent:
|
||||
"""类级 docstring:简述职责。"""
|
||||
|
||||
## Important Development Notes
|
||||
async def run(self, prompt: str, system_prompt: str) -> str:
|
||||
"""执行任务并返回结果。
|
||||
|
||||
### Backend Development
|
||||
- All new API routes should go in `backend/app/routers/` and be registered in `main.py`
|
||||
- Core business logic belongs in `backend/app/core/`
|
||||
- Utility functions should be placed in `backend/app/utils/`
|
||||
- Use async/await patterns throughout for FastAPI compatibility
|
||||
- Redis is used for task state management and WebSocket message broadcasting
|
||||
Args:
|
||||
prompt: 用户输入。
|
||||
system_prompt: 系统提示词。
|
||||
|
||||
### Frontend Development
|
||||
- Page components in `frontend/src/pages/` with `index.vue` as entry point
|
||||
- Reusable components in `frontend/src/components/`
|
||||
- API calls through `frontend/src/utils/request.ts` (axios wrapper)
|
||||
- Use Vue 3 Composition API with `<script setup lang="ts">` syntax
|
||||
- WebSocket connection for real-time task updates
|
||||
Returns:
|
||||
处理结果文本。
|
||||
"""
|
||||
```
|
||||
|
||||
### Testing and Quality
|
||||
- Backend: Use ruff for linting and formatting (configured in pyproject.toml)
|
||||
- Frontend: TypeScript compilation and Biome for linting
|
||||
- Generated files are stored in `backend/project/work_dir/` for inspection
|
||||
### 前端(Vue 3 + TypeScript)
|
||||
|
||||
- SFC 使用 `<script setup lang="ts">`
|
||||
- 代码按逻辑分组,用注释分隔:`// ---- Props ----`、`// ---- State ----`、`// ---- Computed ----`、`// ---- Methods ----`
|
||||
- TypeScript 接口和 API 函数使用 JSDoc `/** */` 注释
|
||||
- UI 库组件(`components/ui/`)为 shadcn-vue 生成代码,不要修改
|
||||
- 格式:tab 缩进,双引号,Biome 管理 lint 和格式化
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
/** 组件属性 */
|
||||
interface Props {
|
||||
/** 消息类型 */
|
||||
type: "agent" | "user";
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { type: "user" });
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
const rendered = computed(() => marked.parse(props.content));
|
||||
</script>
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
|
||||
提交信息格式:`<type>: <描述>`,type 包括:
|
||||
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复
|
||||
- `refactor`: 重构
|
||||
- `chore`: 杂项变更
|
||||
- `enhance`: 增强
|
||||
- `docs`: 文档
|
||||
|
||||
示例:`feat: 添加 OpenAlex API Key 支持并更新相关配置`
|
||||
|
||||
## Boundaries
|
||||
|
||||
### 自动化 Lint Hook
|
||||
|
||||
每次 Edit/Write 文件后,PostToolUse hook 自动触发:
|
||||
- `backend/**/*.py` → `ruff check app/`
|
||||
- `frontend/src/**/*.{vue,ts}` → `biome check <file>`
|
||||
|
||||
hook 脚本位于 `.claude/hook_lint.sh`,配置位于 `.claude/settings.json`。
|
||||
|
||||
### 不要修改的内容
|
||||
|
||||
- `frontend/src/components/ui/` — shadcn-vue 第三方 UI 库组件
|
||||
- 已有的 `# type: ignore` 注释 — 这些是经过验证的类型抑制,非遗留问题
|
||||
- `.env` 相关文件中的实际配置值
|
||||
|
||||
### 运行环境
|
||||
|
||||
- Python 3.12+,包管理用 uv(非 pip)
|
||||
- Node.js,包管理用 pnpm(版本见 packageManager 字段)
|
||||
- Redis 必须运行(任务队列和 WebSocket 广播)
|
||||
- 后端虚拟环境路径:`backend/.venv/`
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
from pydantic import AnyUrl, BeforeValidator, computed_field, field_validator, Field
|
||||
"""应用配置模块,基于 pydantic-settings 管理环境变量和全局配置。"""
|
||||
|
||||
from pydantic import BeforeValidator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
import os
|
||||
from typing import Annotated, Optional
|
||||
|
||||
|
||||
def parse_cors(value: str) -> list[str]:
|
||||
"""
|
||||
Parses the CORS settings from a string to a list of URLs.
|
||||
"""将 CORS 配置字符串解析为 URL 列表。
|
||||
|
||||
Args:
|
||||
value: 逗号分隔的 URL 字符串,或 "*" 表示允许所有来源。
|
||||
|
||||
Returns:
|
||||
解析后的 URL 列表。
|
||||
"""
|
||||
if value == "*":
|
||||
return ["*"]
|
||||
@@ -16,7 +23,8 @@ def parse_cors(value: str) -> list[str]:
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
ENV: str
|
||||
"""全局应用配置,从环境变量和 .env 文件加载。"""
|
||||
ENV: str = "dev"
|
||||
|
||||
COORDINATOR_API_KEY: Optional[str] = None
|
||||
COORDINATOR_MODEL: Optional[str] = None
|
||||
@@ -38,8 +46,8 @@ class Settings(BaseSettings):
|
||||
WRITER_BASE_URL: Optional[str] = None
|
||||
WRITER_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
MAX_CHAT_TURNS: int = 150
|
||||
MAX_RETRIES: int = 5
|
||||
MAX_CHAT_TURNS: Optional[int] = None
|
||||
MAX_RETRIES: Optional[int] = None
|
||||
E2B_API_KEY: Optional[str] = None
|
||||
LOG_LEVEL: str = "DEBUG"
|
||||
DEBUG: bool = True
|
||||
@@ -47,6 +55,8 @@ class Settings(BaseSettings):
|
||||
REDIS_MAX_CONNECTIONS: int = 10
|
||||
CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)] = "*"
|
||||
SERVER_HOST: str = "http://localhost:8000"
|
||||
DEEPSEEK_MODEL: Optional[str] = None
|
||||
DEEPSEEK_BASE_URL: Optional[str] = None
|
||||
OPENALEX_EMAIL: Optional[str] = None
|
||||
OPENALEX_API_KEY: Optional[str] = None
|
||||
|
||||
@@ -57,10 +67,15 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env: str = None):
|
||||
def from_env(cls, env: str | None = None):
|
||||
"""根据环境名称加载对应配置。
|
||||
|
||||
Args:
|
||||
env: 环境名称(如 dev、prod),默认从 ENV 环境变量获取。
|
||||
"""
|
||||
env = env or os.getenv("ENV", "dev")
|
||||
env_file = f".env.{env.lower()}"
|
||||
return cls(_env_file=env_file, _env_file_encoding="utf-8")
|
||||
return cls(_env_file=env_file, _env_file_encoding="utf-8") # type: ignore[call-arg]
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
"""Agent 基类模块,提供对话管理和记忆压缩功能。"""
|
||||
|
||||
from typing import Any
|
||||
from app.core.llm.llm import LLM, simple_chat
|
||||
from app.utils.log_util import logger
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
|
||||
# TODO: Memory 的管理
|
||||
# TODO: 评估任务完成情况,rethinking
|
||||
|
||||
|
||||
class Agent:
|
||||
"""Agent 基类,管理对话历史、轮次控制和记忆压缩。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int = 30, # 单个agent最大对话轮次
|
||||
max_chat_turns: int | None = None, # 单个agent最大对话轮次,None表示无限制
|
||||
max_memory: int = 12, # 最大记忆轮次
|
||||
) -> None:
|
||||
self.task_id = task_id
|
||||
@@ -21,15 +25,16 @@ class Agent:
|
||||
self.current_chat_turns = 0 # 当前对话轮次计数器
|
||||
self.max_memory = max_memory # 最大记忆轮次
|
||||
|
||||
async def run(self, prompt: str, system_prompt: str, sub_title: str) -> str:
|
||||
"""
|
||||
执行agent的对话并返回结果和总结
|
||||
async def run(self, prompt: str, system_prompt: str, sub_title: str) -> Any:
|
||||
"""执行 Agent 对话并返回模型响应。
|
||||
|
||||
Args:
|
||||
prompt: 输入的提示
|
||||
prompt: 用户输入的提示。
|
||||
system_prompt: 系统提示词。
|
||||
sub_title: 子任务标题。
|
||||
|
||||
Returns:
|
||||
str: 模型的响应
|
||||
模型的响应文本。
|
||||
"""
|
||||
try:
|
||||
logger.info(f"{self.__class__.__name__}:开始:执行对话")
|
||||
@@ -55,6 +60,11 @@ class Agent:
|
||||
return error_msg
|
||||
|
||||
async def append_chat_history(self, msg: dict) -> None:
|
||||
"""向对话历史追加消息,并在必要时触发记忆清理。
|
||||
|
||||
Args:
|
||||
msg: 消息字典,需包含 role 和 content 字段。
|
||||
"""
|
||||
ic(f"添加消息: role={msg.get('role')}, 当前历史长度={len(self.chat_history)}")
|
||||
self.chat_history.append(msg)
|
||||
ic(f"添加后历史长度={len(self.chat_history)}")
|
||||
@@ -67,7 +77,7 @@ class Agent:
|
||||
ic("跳过内存清理(tool消息)")
|
||||
|
||||
async def clear_memory(self):
|
||||
"""当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩"""
|
||||
"""当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩。"""
|
||||
ic(f"检查内存清理: 当前={len(self.chat_history)}, 最大={self.max_memory}")
|
||||
|
||||
if len(self.chat_history) <= self.max_memory:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""代码手 Agent 模块,负责生成和执行 Python 代码完成建模任务。"""
|
||||
|
||||
from app.core.agents.agent import Agent
|
||||
from app.config.setting import settings
|
||||
from app.utils.log_util import logger
|
||||
@@ -17,16 +19,16 @@ from app.core.functions import coder_tools
|
||||
# TODO: 引入创新方案:
|
||||
|
||||
|
||||
# 代码强
|
||||
class CoderAgent(Agent): # 同样继承自Agent类
|
||||
class CoderAgent(Agent):
|
||||
"""代码手 Agent,通过 LLM 生成代码并在解释器中执行,支持错误反思和重试。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
work_dir: str, # 工作目录
|
||||
max_chat_turns: int = settings.MAX_CHAT_TURNS, # 最大聊天次数
|
||||
max_retries: int = settings.MAX_RETRIES, # 最大反思次数
|
||||
code_interpreter: BaseCodeInterpreter = None,
|
||||
max_chat_turns: int | None = settings.MAX_CHAT_TURNS, # 最大聊天次数,None表示无限制
|
||||
max_retries: int | None = settings.MAX_RETRIES, # 最大反思次数,None表示无限制
|
||||
code_interpreter: BaseCodeInterpreter | None = None,
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
self.work_dir = work_dir
|
||||
@@ -35,8 +37,18 @@ class CoderAgent(Agent): # 同样继承自Agent类
|
||||
self.system_prompt = CODER_PROMPT
|
||||
self.code_interpreter = code_interpreter
|
||||
|
||||
async def run(self, prompt: str, subtask_title: str) -> CoderToWriter:
|
||||
async def run(self, prompt: str, subtask_title: str) -> CoderToWriter: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""执行代码手子任务,生成并运行代码。
|
||||
|
||||
Args:
|
||||
prompt: 子任务描述。
|
||||
subtask_title: 子任务标题,用于分段输出。
|
||||
|
||||
Returns:
|
||||
CoderToWriter 对象,包含代码执行结果和生成的图片列表。
|
||||
"""
|
||||
logger.info(f"{self.__class__.__name__}:开始:执行子任务: {subtask_title}")
|
||||
assert self.code_interpreter is not None, "code_interpreter 未初始化"
|
||||
self.code_interpreter.add_section(subtask_title)
|
||||
|
||||
# 如果是第一次运行,则添加系统提示
|
||||
@@ -62,7 +74,7 @@ class CoderAgent(Agent): # 同样继承自Agent类
|
||||
last_error_message = ""
|
||||
|
||||
while True:
|
||||
if retry_count >= self.max_retries:
|
||||
if self.max_retries is not None and retry_count >= self.max_retries:
|
||||
logger.error(f"超过最大尝试次数: {self.max_retries}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
@@ -70,11 +82,11 @@ class CoderAgent(Agent): # 同样继承自Agent类
|
||||
)
|
||||
logger.warning(f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}")
|
||||
return CoderToWriter(
|
||||
coder_response=f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}",
|
||||
code_response=f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}",
|
||||
created_images=[])
|
||||
|
||||
|
||||
if self.current_chat_turns >= self.max_chat_turns:
|
||||
|
||||
if self.max_chat_turns is not None and self.current_chat_turns >= self.max_chat_turns:
|
||||
logger.error(f"超过最大聊天次数: {self.max_chat_turns}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
@@ -179,7 +191,7 @@ class CoderAgent(Agent): # 同样继承自Agent类
|
||||
# 没有工具调用,表示任务完成
|
||||
logger.info("没有工具调用,任务完成")
|
||||
return CoderToWriter(
|
||||
coder_response=response.choices[0].message.content,
|
||||
code_response=response.choices[0].message.content,
|
||||
created_images=await self.code_interpreter.get_created_images(
|
||||
subtask_title
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""协调者 Agent 模块,负责识别用户意图并拆解数学建模问题。"""
|
||||
|
||||
from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import COORDINATOR_PROMPT
|
||||
@@ -8,6 +10,7 @@ from app.schemas.A2A import CoordinatorToModeler
|
||||
|
||||
|
||||
class CoordinatorAgent(Agent):
|
||||
"""协调者 Agent,判断用户输入是否为数学建模问题并拆解为结构化问题列表。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -17,15 +20,21 @@ class CoordinatorAgent(Agent):
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
self.system_prompt = COORDINATOR_PROMPT
|
||||
|
||||
async def run(self, ques_all: str) -> CoordinatorToModeler:
|
||||
"""用户输入问题 使用LLM 格式化 questions"""
|
||||
async def run(self, ques_all: str) -> CoordinatorToModeler: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""解析用户输入的问题并格式化为结构化 JSON。
|
||||
|
||||
Args:
|
||||
ques_all: 用户输入的完整题目信息。
|
||||
|
||||
Returns:
|
||||
CoordinatorToModeler 对象,包含结构化问题和问题数量。
|
||||
"""
|
||||
await self.append_chat_history(
|
||||
{"role": "system", "content": self.system_prompt}
|
||||
)
|
||||
await self.append_chat_history({"role": "user", "content": ques_all})
|
||||
max_retries = 3
|
||||
attempt = 0
|
||||
while attempt <= max_retries:
|
||||
while True:
|
||||
try:
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
@@ -44,21 +53,14 @@ class CoordinatorAgent(Agent):
|
||||
ques_count = questions["ques_count"]
|
||||
logger.info(f"questions:{questions}")
|
||||
return CoordinatorToModeler(questions=questions, ques_count=ques_count)
|
||||
|
||||
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
attempt += 1
|
||||
logger.warning(f"解析失败 (尝试 {attempt}/{max_retries}): {str(e)}")
|
||||
|
||||
if attempt > max_retries:
|
||||
logger.error(f"超过最大重试次数,放弃解析")
|
||||
raise RuntimeError(f"无法解析模型响应: {str(e)}")
|
||||
|
||||
logger.warning(f"解析失败 (尝试 {attempt}): {str(e)}")
|
||||
|
||||
# 添加错误反馈提示
|
||||
error_prompt = f"⚠️ 上次响应格式错误: {str(e)}。请严格输出JSON格式"
|
||||
await self.append_chat_history({
|
||||
"role": "system",
|
||||
"role": "system",
|
||||
"content": self.system_prompt + "\n" + error_prompt
|
||||
})
|
||||
|
||||
# 永远不会执行到这里
|
||||
raise RuntimeError("意外的流程终止")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""建模手 Agent 模块,负责分析问题并制定数学建模方案。"""
|
||||
|
||||
from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import MODELER_PROMPT
|
||||
@@ -5,11 +7,18 @@ from app.schemas.A2A import CoordinatorToModeler, ModelerToCoder
|
||||
from app.utils.log_util import logger
|
||||
import json
|
||||
import re
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
|
||||
|
||||
def repair_json(json_str: str) -> dict | None:
|
||||
"""Try to repair malformed JSON from LLM output."""
|
||||
"""尝试修复 LLM 输出的格式错误的 JSON。
|
||||
|
||||
Args:
|
||||
json_str: 可能包含格式错误的 JSON 字符串。
|
||||
|
||||
Returns:
|
||||
修复后的字典,无法修复时返回 None。
|
||||
"""
|
||||
json_str = json_str.replace("```json", "").replace("```", "").strip()
|
||||
|
||||
# Try direct parse first
|
||||
@@ -43,6 +52,7 @@ def repair_json(json_str: str) -> dict | None:
|
||||
|
||||
|
||||
class ModelerAgent(Agent):
|
||||
"""建模手 Agent,分析问题类型并制定建模方案、求解方法和可视化策略。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -52,7 +62,15 @@ class ModelerAgent(Agent):
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
self.system_prompt = MODELER_PROMPT
|
||||
|
||||
async def run(self, coordinator_to_modeler: CoordinatorToModeler) -> ModelerToCoder:
|
||||
async def run(self, coordinator_to_modeler: CoordinatorToModeler) -> ModelerToCoder: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""根据协调者拆解的问题生成建模方案。
|
||||
|
||||
Args:
|
||||
coordinator_to_modeler: 协调者传递的结构化问题信息。
|
||||
|
||||
Returns:
|
||||
ModelerToCoder 对象,包含各问题的建模解决方案。
|
||||
"""
|
||||
await self.append_chat_history(
|
||||
{"role": "system", "content": self.system_prompt}
|
||||
)
|
||||
@@ -63,8 +81,8 @@ class ModelerAgent(Agent):
|
||||
}
|
||||
)
|
||||
|
||||
max_parse_retries = 3
|
||||
for attempt in range(max_parse_retries):
|
||||
attempt = 0
|
||||
while True:
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
agent_name=self.__class__.__name__,
|
||||
@@ -79,8 +97,9 @@ class ModelerAgent(Agent):
|
||||
ic(questions_solution)
|
||||
return ModelerToCoder(questions_solution=questions_solution)
|
||||
|
||||
attempt += 1
|
||||
logger.warning(
|
||||
f"JSON 解析失败 (第{attempt + 1}次),请求模型重新生成"
|
||||
f"JSON 解析失败 (第{attempt}次),请求模型重新生成"
|
||||
)
|
||||
await self.append_chat_history(
|
||||
{"role": "assistant", "content": json_str}
|
||||
@@ -91,7 +110,3 @@ class ModelerAgent(Agent):
|
||||
"content": "你返回的JSON格式有误,请严格按照JSON格式重新输出,注意字符串值内的双引号必须转义为\\\",不要包含未转义的特殊字符。",
|
||||
}
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"经过{max_parse_retries}次尝试仍无法解析JSON,请检查模型输出"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""写作手 Agent 模块,负责基于建模结果撰写学术论文。"""
|
||||
|
||||
from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import get_writer_prompt
|
||||
@@ -8,23 +10,25 @@ from app.services.redis_manager import redis_manager
|
||||
from app.schemas.response import SystemMessage, WriterMessage
|
||||
import json
|
||||
from app.core.functions import writer_tools
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from app.schemas.A2A import WriterResponse
|
||||
|
||||
|
||||
# 长文本
|
||||
# TODO: 并行 parallel
|
||||
# TODO: 获取当前文件下的文件
|
||||
# TODO: 引用cites tool
|
||||
class WriterAgent(Agent): # 同样继承自Agent类
|
||||
|
||||
|
||||
class WriterAgent(Agent):
|
||||
"""写作手 Agent,基于建模和代码执行结果撰写竞赛论文。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int = 10, # 添加最大对话轮次限制
|
||||
comp_template: CompTemplate = CompTemplate,
|
||||
max_chat_turns: int | None = None, # 最大对话轮次,None表示无限制
|
||||
comp_template: CompTemplate = CompTemplate.CHINA,
|
||||
format_output: FormatOutPut = FormatOutPut.Markdown,
|
||||
scholar: OpenAlexScholar = None,
|
||||
scholar: OpenAlexScholar | None = None,
|
||||
max_memory: int = 25, # 添加最大记忆轮次
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns, max_memory)
|
||||
@@ -35,11 +39,11 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
self.system_prompt = get_writer_prompt(format_output)
|
||||
self.available_images: list[str] = []
|
||||
|
||||
async def run(
|
||||
async def run( # type: ignore[reportIncompatibleMethodOverride]
|
||||
self,
|
||||
prompt: str,
|
||||
available_images: list[str] = None,
|
||||
sub_title: str = None,
|
||||
available_images: list[str] | None = None,
|
||||
sub_title: str | None = None,
|
||||
) -> WriterResponse:
|
||||
"""
|
||||
执行写作任务
|
||||
@@ -85,6 +89,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
)
|
||||
|
||||
footnotes = []
|
||||
response_content: str = ""
|
||||
|
||||
if (
|
||||
hasattr(response.choices[0].message, "tool_calls")
|
||||
@@ -105,7 +110,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
WriterMessage(
|
||||
input={"query": query},
|
||||
content=query,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -114,6 +119,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
ic(response.choices[0].message.model_dump())
|
||||
|
||||
try:
|
||||
assert self.scholar is not None, "scholar 未初始化"
|
||||
papers = await self.scholar.search_papers(query)
|
||||
except Exception as e:
|
||||
error_msg = f"搜索文献失败: {str(e)}"
|
||||
@@ -122,6 +128,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
response_content=error_msg, footnotes=footnotes
|
||||
)
|
||||
# TODO: pass to frontend
|
||||
assert self.scholar is not None, "scholar 未初始化"
|
||||
papers_str = self.scholar.papers_to_str(papers)
|
||||
logger.info(f"搜索文献结果\n{papers_str}")
|
||||
await self.append_chat_history(
|
||||
@@ -147,9 +154,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
|
||||
return WriterResponse(response_content=response_content, footnotes=footnotes)
|
||||
|
||||
async def summarize(self) -> str:
|
||||
"""
|
||||
总结对话内容
|
||||
"""
|
||||
"""总结对话内容,生成任务执行摘要。"""
|
||||
try:
|
||||
await self.append_chat_history(
|
||||
{"role": "user", "content": "请简单总结以上完成什么任务取得什么结果:"}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""工作流程定义模块,管理建模任务的求解和写作流程。"""
|
||||
|
||||
from app.models.user_output import UserOutput
|
||||
from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
from app.core.agents.modeler_agent import ModelerToCoder
|
||||
|
||||
|
||||
class Flows:
|
||||
"""管理数学建模任务的求解流程和写作流程。"""
|
||||
def __init__(self, questions: dict[str, str | int]):
|
||||
self.flows: dict[str, dict] = {}
|
||||
self.questions: dict[str, str | int] = questions
|
||||
|
||||
def set_flows(self, ques_count: int):
|
||||
"""根据问题数量设置流程节点。
|
||||
|
||||
Args:
|
||||
ques_count: 问题数量。
|
||||
"""
|
||||
ques_str = [f"ques{i}" for i in range(1, ques_count + 1)]
|
||||
seq = [
|
||||
"firstPage",
|
||||
@@ -26,6 +34,15 @@ class Flows:
|
||||
def get_solution_flows(
|
||||
self, questions: dict[str, str | int], modeler_response: ModelerToCoder
|
||||
):
|
||||
"""生成求解阶段的流程配置。
|
||||
|
||||
Args:
|
||||
questions: 包含各问题描述的字典。
|
||||
modeler_response: 建模手的响应,包含各问题的解决方案。
|
||||
|
||||
Returns:
|
||||
求解流程配置字典,键为任务名,值包含 coder_prompt 等信息。
|
||||
"""
|
||||
questions_quesx = {
|
||||
key: value
|
||||
for key, value in questions.items()
|
||||
@@ -61,6 +78,16 @@ class Flows:
|
||||
def get_write_flows(
|
||||
self, user_output: UserOutput, config_template: dict, bg_ques_all: str
|
||||
):
|
||||
"""生成写作阶段的流程配置。
|
||||
|
||||
Args:
|
||||
user_output: 用户输出对象,包含已求解的结果。
|
||||
config_template: 论文模板配置。
|
||||
bg_ques_all: 问题背景和题目信息。
|
||||
|
||||
Returns:
|
||||
写作流程配置字典,键为章节名,值为写作提示。
|
||||
"""
|
||||
model_build_solve = user_output.get_model_build_solve()
|
||||
flows = {
|
||||
"firstPage": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["firstPage"]},撰写标题,摘要,关键词""",
|
||||
@@ -118,7 +145,7 @@ class Flows:
|
||||
"""获取问题1,2...的键"""
|
||||
return list(self.get_questions_quesx().keys())
|
||||
|
||||
def get_questions_quesx(self) -> dict[str, str]:
|
||||
def get_questions_quesx(self) -> dict[str, str | int]:
|
||||
"""获取问题1,2,3...的键值对"""
|
||||
# 获取所有以 "ques" 开头的键值对
|
||||
questions_quesx = {
|
||||
@@ -129,6 +156,14 @@ class Flows:
|
||||
return questions_quesx
|
||||
|
||||
def get_seq(self, ques_count: int) -> dict[str, str]:
|
||||
"""获取论文章节顺序。
|
||||
|
||||
Args:
|
||||
ques_count: 问题数量。
|
||||
|
||||
Returns:
|
||||
以章节名为键的有序字典。
|
||||
"""
|
||||
ques_str = [f"ques{i}" for i in range(1, ques_count + 1)]
|
||||
seq = [
|
||||
"firstPage",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""工具函数定义模块,为各 Agent 提供可用的工具 schema。"""
|
||||
|
||||
coder_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
|
||||
+59
-36
@@ -1,3 +1,6 @@
|
||||
"""LLM 交互模块,封装大语言模型的调用、重试和消息发送。"""
|
||||
|
||||
from typing import Any
|
||||
from app.utils.common_utils import transform_link, split_footnotes
|
||||
from app.utils.log_util import logger
|
||||
import time
|
||||
@@ -9,21 +12,22 @@ from app.schemas.response import (
|
||||
CoordinatorMessage,
|
||||
)
|
||||
from app.services.redis_manager import redis_manager
|
||||
from litellm import acompletion
|
||||
import litellm
|
||||
from litellm import acompletion # type: ignore[import-unresolved]
|
||||
import litellm # type: ignore[import-unresolved]
|
||||
from app.schemas.enums import AgentType
|
||||
from app.utils.track import agent_metrics
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
|
||||
litellm.callbacks = [agent_metrics]
|
||||
|
||||
class LLM:
|
||||
"""大语言模型封装类,提供对话调用、重试和工具调用验证功能。"""
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str,
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
api_key: str | None = None,
|
||||
model: str | None = None,
|
||||
base_url: str | None = None,
|
||||
task_id: str = "",
|
||||
max_tokens: int | None = None,
|
||||
):
|
||||
self.api_key = api_key
|
||||
@@ -33,7 +37,15 @@ class LLM:
|
||||
self.max_tokens = max_tokens
|
||||
self.task_id = task_id
|
||||
|
||||
def _validate_config(self, agent_name: AgentType) -> None:
|
||||
def _validate_config(self, agent_name: str) -> None:
|
||||
"""验证 LLM 配置是否完整。
|
||||
|
||||
Args:
|
||||
agent_name: Agent 类型名称,用于错误提示。
|
||||
|
||||
Raises:
|
||||
ValueError: 模型 ID 或 API Key 未配置时抛出。
|
||||
"""
|
||||
if not self.model or not str(self.model).strip():
|
||||
raise ValueError(f"{agent_name} 未配置模型 ID,请设置对应的 *_MODEL")
|
||||
if not self.api_key or not str(self.api_key).strip():
|
||||
@@ -41,15 +53,15 @@ class LLM:
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
history: list = None,
|
||||
tools: list = None,
|
||||
tool_choice: str = None,
|
||||
max_retries: int = 8, # 添加最大重试次数
|
||||
history: list | None = None,
|
||||
tools: list | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_retries: int | None = None, # 最大重试次数,None表示无限制
|
||||
retry_delay: float = 1.0, # 添加重试延迟
|
||||
top_p: float | None = None, # 添加top_p参数,
|
||||
agent_name: AgentType = AgentType.SYSTEM, # CoderAgent or WriterAgent
|
||||
agent_name: str = "SystemAgent", # CoderAgent or WriterAgent
|
||||
sub_title: str | None = None,
|
||||
) -> str:
|
||||
) -> Any:
|
||||
logger.info(f"subtitle是:{sub_title}")
|
||||
self._validate_config(agent_name)
|
||||
|
||||
@@ -78,9 +90,9 @@ class LLM:
|
||||
litellm.enable_json_schema_validation = True #加入json格式验证
|
||||
|
||||
# TODO: stream 输出
|
||||
for attempt in range(max_retries):
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
# completion = self.client.chat.completions.create(**kwargs)
|
||||
response = await acompletion(**kwargs)
|
||||
logger.info(f"API返回: {response}")
|
||||
if not response or not hasattr(response, "choices"):
|
||||
@@ -89,12 +101,12 @@ class LLM:
|
||||
await self.send_message(response, agent_name, sub_title)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"第{attempt + 1}次重试: {str(e)}")
|
||||
if attempt < max_retries - 1: # 如果不是最后一次尝试
|
||||
time.sleep(retry_delay * (attempt + 1)) # 指数退避
|
||||
continue
|
||||
logger.debug(f"请求参数: {kwargs}")
|
||||
raise # 如果所有重试都失败,则抛出异常
|
||||
attempt += 1
|
||||
logger.error(f"第{attempt}次重试: {str(e)}")
|
||||
if max_retries is not None and attempt >= max_retries:
|
||||
logger.debug(f"请求参数: {kwargs}")
|
||||
raise
|
||||
time.sleep(retry_delay * min(attempt, 10)) # 指数退避,上限10秒
|
||||
|
||||
def _validate_and_fix_tool_calls(self, history: list) -> list:
|
||||
"""验证并修复工具调用完整性"""
|
||||
@@ -154,9 +166,9 @@ class LLM:
|
||||
cleaned_msg = {k: v for k, v in msg.items() if k != "tool_calls"}
|
||||
if cleaned_msg.get("content"):
|
||||
fixed_history.append(cleaned_msg)
|
||||
ic(f" 🔧 移除所有tool_calls,保留消息内容")
|
||||
ic(" 🔧 移除所有tool_calls,保留消息内容")
|
||||
else:
|
||||
ic(f" 🗑️ 完全移除空的tool_calls消息")
|
||||
ic(" 🗑️ 完全移除空的tool_calls消息")
|
||||
|
||||
# 如果是tool响应消息,检查是否是孤立的
|
||||
elif isinstance(msg, dict) and msg.get("role") == "tool":
|
||||
@@ -175,7 +187,7 @@ class LLM:
|
||||
|
||||
if found_call:
|
||||
fixed_history.append(msg)
|
||||
ic(f" ✅ 保留有效的tool响应")
|
||||
ic(" ✅ 保留有效的tool响应")
|
||||
else:
|
||||
ic(f" 🗑️ 移除孤立的tool响应: {tool_call_id}")
|
||||
|
||||
@@ -188,31 +200,43 @@ class LLM:
|
||||
if len(fixed_history) != len(history):
|
||||
ic(f"🔧 修复完成: {len(history)} -> {len(fixed_history)} 条消息")
|
||||
else:
|
||||
ic(f"✅ 验证通过,无需修复")
|
||||
ic("✅ 验证通过,无需修复")
|
||||
|
||||
return fixed_history
|
||||
|
||||
async def send_message(self, response, agent_name, sub_title=None):
|
||||
"""将 LLM 响应通过 Redis 发送给前端。
|
||||
|
||||
Args:
|
||||
response: LLM 返回的响应对象。
|
||||
agent_name: Agent 类型。
|
||||
sub_title: 子任务标题。
|
||||
"""
|
||||
logger.info(f"subtitle是:{sub_title}")
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# tool_call 响应的 content 为 None,跳过消息发送
|
||||
if content is None:
|
||||
return
|
||||
|
||||
agent_msg: Any = None
|
||||
match agent_name:
|
||||
case AgentType.CODER:
|
||||
agent_msg: CoderMessage = CoderMessage(content=content)
|
||||
agent_msg = CoderMessage(content=content)
|
||||
case AgentType.WRITER:
|
||||
# 处理 Markdown 格式的图片语法
|
||||
content, _ = split_footnotes(content)
|
||||
content = transform_link(self.task_id, content)
|
||||
agent_msg: WriterMessage = WriterMessage(
|
||||
agent_msg = WriterMessage(
|
||||
content=content,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
case AgentType.MODELER:
|
||||
agent_msg: ModelerMessage = ModelerMessage(content=content)
|
||||
agent_msg = ModelerMessage(content=content)
|
||||
case AgentType.SYSTEM:
|
||||
agent_msg: SystemMessage = SystemMessage(content=content)
|
||||
agent_msg = SystemMessage(content=content)
|
||||
case AgentType.COORDINATOR:
|
||||
agent_msg: CoordinatorMessage = CoordinatorMessage(content=content)
|
||||
agent_msg = CoordinatorMessage(content=content)
|
||||
case _:
|
||||
raise ValueError(f"不支持的agent类型: {agent_name}")
|
||||
|
||||
@@ -235,15 +259,14 @@ class LLM:
|
||||
|
||||
|
||||
async def simple_chat(model: LLM, history: list) -> str:
|
||||
"""
|
||||
Description of the function.
|
||||
"""使用 LLM 进行简单的单轮对话。
|
||||
|
||||
Args:
|
||||
model (LLM): 模型
|
||||
history (list): 构造好的历史记录(包含system_prompt,user_prompt)
|
||||
model: LLM 实例。
|
||||
history: 构造好的历史记录(包含 system_prompt 和 user_prompt)。
|
||||
|
||||
Returns:
|
||||
return_type: Description of the return value.
|
||||
模型的响应文本。
|
||||
"""
|
||||
kwargs = {
|
||||
"api_key": model.api_key,
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""LLM 工厂模块,根据配置创建各 Agent 使用的 LLM 实例。"""
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.llm.llm import LLM
|
||||
|
||||
|
||||
class LLMFactory:
|
||||
"""LLM 工厂类,根据配置创建协调者、建模手、代码手和写作手的 LLM 实例。"""
|
||||
task_id: str
|
||||
|
||||
def __init__(self, task_id: str) -> None:
|
||||
self.task_id = task_id
|
||||
|
||||
def get_all_llms(self) -> tuple[LLM, LLM, LLM, LLM]:
|
||||
"""创建所有 Agent 的 LLM 实例。
|
||||
|
||||
Returns:
|
||||
包含 (coordinator_llm, modeler_llm, coder_llm, writer_llm) 的元组。
|
||||
"""
|
||||
coordinator_llm = LLM(
|
||||
api_key=settings.COORDINATOR_API_KEY,
|
||||
model=settings.COORDINATOR_MODEL,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""代码手 Agent 的系统提示词。"""
|
||||
|
||||
import platform
|
||||
|
||||
CODER_PROMPT = f"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""协调者 Agent 的系统提示词。"""
|
||||
|
||||
FORMAT_QUESTIONS_PROMPT = """
|
||||
用户将提供给你一段题目信息,**请你不要更改题目信息,完整将用户输入的内容**,以 JSON 的形式输出,输出的 JSON 需遵守以下的格式:
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""建模手 Agent 的系统提示词。"""
|
||||
|
||||
MODELER_PROMPT = """
|
||||
# Role
|
||||
你是一名数学建模竞赛经验丰富、善于思考的建模手,负责为每个问题制定建模方案和可视化策略。
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
"""共享的提示词工具函数。"""
|
||||
|
||||
|
||||
def get_reflection_prompt(error_message, code) -> str:
|
||||
"""生成代码错误反思提示词。
|
||||
|
||||
Args:
|
||||
error_message: 错误信息。
|
||||
code: 出错的代码。
|
||||
|
||||
Returns:
|
||||
反思提示词字符串。
|
||||
"""
|
||||
return f"""The code execution encountered an error:
|
||||
{error_message}
|
||||
|
||||
@@ -20,6 +32,15 @@ Please provide an explanation of what went wrong and Remenber call the function
|
||||
|
||||
|
||||
def get_completion_check_prompt(prompt, text_to_gpt) -> str:
|
||||
"""生成任务完成检查提示词。
|
||||
|
||||
Args:
|
||||
prompt: 原始任务描述。
|
||||
text_to_gpt: 最新执行结果。
|
||||
|
||||
Returns:
|
||||
完成检查提示词字符串。
|
||||
"""
|
||||
return f"""
|
||||
Please analyze the current state and determine if the task is fully completed:
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
"""写作手 Agent 的系统提示词。"""
|
||||
|
||||
from app.schemas.enums import FormatOutPut
|
||||
|
||||
|
||||
def get_writer_prompt(
|
||||
format_output: FormatOutPut = FormatOutPut.Markdown,
|
||||
):
|
||||
"""根据输出格式生成写作手的系统提示词。
|
||||
|
||||
Args:
|
||||
format_output: 输出格式(Markdown 或 LaTeX)。
|
||||
|
||||
Returns:
|
||||
写作手系统提示词字符串。
|
||||
"""
|
||||
return f"""
|
||||
# Role
|
||||
你是一名数学建模竞赛论文写作专家,擅长技术文档撰写和学术文献综合。
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""工作流模块,编排多 Agent 协作完成数学建模任务。"""
|
||||
|
||||
from app.core.agents import WriterAgent, CoderAgent, CoordinatorAgent, ModelerAgent
|
||||
from app.schemas.request import Problem
|
||||
from app.schemas.response import SystemMessage
|
||||
@@ -14,22 +16,31 @@ from app.core.llm.llm_factory import LLMFactory
|
||||
|
||||
|
||||
class WorkFlow:
|
||||
"""工作流基类。"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def execute(self) -> str:
|
||||
def execute(self) -> None:
|
||||
"""执行工作流。"""
|
||||
# RichPrinter.workflow_start()
|
||||
# RichPrinter.workflow_end()
|
||||
pass
|
||||
|
||||
|
||||
class MathModelWorkFlow(WorkFlow):
|
||||
"""数学建模工作流,协调协调者、建模手、代码手和写作手完成完整建模任务。"""
|
||||
task_id: str #
|
||||
work_dir: str # worklow work dir
|
||||
ques_count: int = 0 # 问题数量
|
||||
questions: dict[str, str | int] = {} # 问题
|
||||
|
||||
async def execute(self, problem: Problem):
|
||||
async def execute(self, problem: Problem): # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""执行数学建模工作流。
|
||||
|
||||
Args:
|
||||
problem: 包含题目信息、模板配置等的 Problem 对象。
|
||||
"""
|
||||
self.task_id = problem.task_id
|
||||
self.work_dir = create_work_dir(self.task_id)
|
||||
|
||||
@@ -82,6 +93,8 @@ class MathModelWorkFlow(WorkFlow):
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
assert settings.OPENALEX_EMAIL is not None, "OPENALEX_EMAIL 未配置"
|
||||
assert settings.OPENALEX_API_KEY is not None, "OPENALEX_API_KEY 未配置"
|
||||
scholar = OpenAlexScholar(
|
||||
task_id=self.task_id,
|
||||
email=settings.OPENALEX_EMAIL,
|
||||
@@ -138,7 +151,7 @@ class MathModelWorkFlow(WorkFlow):
|
||||
)
|
||||
|
||||
writer_prompt = flows.get_writer_prompt(
|
||||
key, coder_response.code_response, code_interpreter, config_template
|
||||
key, coder_response.code_response or "", code_interpreter, config_template
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
"""MathModelAgent 应用入口,配置 FastAPI 应用和中间件。"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import os
|
||||
from app.routers import modeling_router, ws_router, common_router, files_router
|
||||
from app.utils.log_util import logger
|
||||
from app.config.setting import settings
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.utils.cli import get_ascii_banner, center_cli_str
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""用户输出管理模块,负责论文结果的拼接、引用处理和保存。"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from app.utils.data_recorder import DataRecorder
|
||||
@@ -7,6 +9,7 @@ import uuid
|
||||
|
||||
|
||||
class UserOutput:
|
||||
"""管理建模任务的输出结果,处理引用编号、脚注和最终论文拼接。"""
|
||||
def __init__(
|
||||
self, work_dir: str, ques_count: int, data_recorder: DataRecorder | None = None
|
||||
):
|
||||
@@ -46,16 +49,23 @@ class UserOutput:
|
||||
]
|
||||
|
||||
def set_res(self, key: str, writer_response: WriterResponse):
|
||||
"""设置指定章节的写作结果。
|
||||
|
||||
Args:
|
||||
key: 章节标识(如 eda、ques1)。
|
||||
writer_response: 写作手的响应对象。
|
||||
"""
|
||||
self.res[key] = {
|
||||
"response_content": writer_response.response_content,
|
||||
"footnotes": writer_response.footnotes,
|
||||
}
|
||||
|
||||
def get_res(self):
|
||||
"""获取所有章节的写作结果。"""
|
||||
return self.res
|
||||
|
||||
def get_model_build_solve(self) -> str:
|
||||
"""获取模型求解"""
|
||||
"""获取模型求解结果的摘要字符串。"""
|
||||
model_build_solve = ",".join(
|
||||
f"{key}-{value}"
|
||||
for key, value in self.res.items()
|
||||
@@ -65,6 +75,14 @@ class UserOutput:
|
||||
return model_build_solve
|
||||
|
||||
def replace_references_with_uuid(self, text: str) -> str:
|
||||
"""将文本中的引用标记替换为 UUID,用于去重和排序。
|
||||
|
||||
Args:
|
||||
text: 包含引用标记的文本。
|
||||
|
||||
Returns:
|
||||
替换引用为 UUID 后的文本。
|
||||
"""
|
||||
# 匹配引用内容,格式为 {[^数字]: 引用内容}
|
||||
# 修改正则表达式,匹配大括号包裹的引用格式
|
||||
references = re.findall(r"\{\[\^(\d+)\]:\s*(.*?)\}", text, re.DOTALL)
|
||||
@@ -104,6 +122,14 @@ class UserOutput:
|
||||
return text
|
||||
|
||||
def sort_text_with_footnotes(self, replace_res: dict) -> dict:
|
||||
"""按章节顺序排列文本并将 UUID 替换为连续编号。
|
||||
|
||||
Args:
|
||||
replace_res: 已替换 UUID 的结果字典。
|
||||
|
||||
Returns:
|
||||
按顺序编号后的结果字典。
|
||||
"""
|
||||
sort_res = {}
|
||||
ref_index = 1
|
||||
|
||||
@@ -124,6 +150,14 @@ class UserOutput:
|
||||
return sort_res
|
||||
|
||||
def append_footnotes_to_text(self, text: str) -> str:
|
||||
"""在文本末尾追加参考文献列表。
|
||||
|
||||
Args:
|
||||
text: 论文正文。
|
||||
|
||||
Returns:
|
||||
附带参考文献的完整文本。
|
||||
"""
|
||||
text += "\n\n ## 参考文献"
|
||||
# 将脚注转换为列表并按 number 排序
|
||||
sorted_footnotes = sorted(self.footnotes.items(), key=lambda x: x[1]["number"])
|
||||
@@ -132,6 +166,7 @@ class UserOutput:
|
||||
return text
|
||||
|
||||
def get_result_to_save(self) -> str:
|
||||
"""获取最终拼接的论文全文,包含引用处理和参考文献。"""
|
||||
replace_res = {}
|
||||
|
||||
for key, value in self.res.items():
|
||||
@@ -149,9 +184,8 @@ class UserOutput:
|
||||
full_res = self.append_footnotes_to_text(full_res_1)
|
||||
return full_res
|
||||
|
||||
def save_result(
|
||||
self,
|
||||
):
|
||||
def save_result(self):
|
||||
"""将结果保存为 res.json 和 res.md 文件。"""
|
||||
with open(os.path.join(self.work_dir, "res.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(self.res, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""通用路由模块,提供配置查询、消息获取和健康检查等接口。"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
@@ -12,6 +14,17 @@ router = APIRouter()
|
||||
|
||||
|
||||
def _require_safe_task_id(task_id: str) -> str:
|
||||
"""验证并返回安全的任务 ID。
|
||||
|
||||
Args:
|
||||
task_id: 待验证的任务 ID。
|
||||
|
||||
Returns:
|
||||
验证通过的任务 ID。
|
||||
|
||||
Raises:
|
||||
HTTPException: 任务 ID 非法时返回 400。
|
||||
"""
|
||||
try:
|
||||
return ensure_safe_task_id(task_id)
|
||||
except ValueError as exc:
|
||||
@@ -19,6 +32,14 @@ def _require_safe_task_id(task_id: str) -> str:
|
||||
|
||||
|
||||
def _load_task_messages_from_file(task_id: str) -> list[dict]:
|
||||
"""从文件加载指定任务的历史消息。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
|
||||
Returns:
|
||||
消息列表,文件不存在时返回空列表。
|
||||
"""
|
||||
safe_task_id = _require_safe_task_id(task_id)
|
||||
message_file = Path("logs/messages") / f"{safe_task_id}.json"
|
||||
if not message_file.exists():
|
||||
@@ -71,7 +92,7 @@ async def track(task_id: str):
|
||||
|
||||
@router.get("/status")
|
||||
async def get_service_status():
|
||||
"""获取各个服务的状态"""
|
||||
"""获取后端和 Redis 的运行状态。"""
|
||||
status = {
|
||||
"backend": {"status": "running", "message": "Backend service is running"},
|
||||
"redis": {"status": "unknown", "message": "Redis connection status unknown"}
|
||||
@@ -80,7 +101,7 @@ async def get_service_status():
|
||||
# 检查Redis连接状态
|
||||
try:
|
||||
redis_client = await redis_manager.get_client()
|
||||
await redis_client.ping()
|
||||
await redis_client.ping() # type: ignore[reportGeneralTypeIssues]
|
||||
status["redis"] = {"status": "running", "message": "Redis connection is healthy"}
|
||||
except Exception as e:
|
||||
logger.error(f"Redis connection failed: {str(e)}")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""文件管理路由模块,提供文件下载、列表和目录打开等接口。"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from app.utils.common_utils import get_current_files, get_work_dir
|
||||
import os
|
||||
import subprocess
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from fastapi import HTTPException
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""建模任务路由模块,提供任务创建、API 验证和配置管理等接口。"""
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, File, Form, UploadFile
|
||||
from app.core.workflow import MathModelWorkFlow
|
||||
from app.schemas.enums import CompTemplate, FormatOutPut
|
||||
@@ -14,10 +16,10 @@ from app.utils.common_utils import (
|
||||
import os
|
||||
import asyncio
|
||||
from fastapi import HTTPException
|
||||
from icecream import ic
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from app.schemas.request import ExampleRequest
|
||||
from pydantic import BaseModel
|
||||
import litellm
|
||||
import litellm # type: ignore[import-unresolved]
|
||||
from app.config.setting import settings
|
||||
import requests
|
||||
|
||||
@@ -201,6 +203,7 @@ async def modeling(
|
||||
logger.info(f"开始处理上传的文件,工作目录: {work_dir}")
|
||||
for file in files:
|
||||
try:
|
||||
assert file.filename is not None
|
||||
data_file_path = os.path.join(work_dir, file.filename)
|
||||
logger.info(f"保存文件: {file.filename} -> {data_file_path}")
|
||||
|
||||
@@ -243,6 +246,14 @@ async def run_modeling_task_async(
|
||||
comp_template: CompTemplate,
|
||||
format_output: FormatOutPut,
|
||||
):
|
||||
"""异步执行建模任务。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
ques_all: 完整题目信息。
|
||||
comp_template: 竞赛模板类型。
|
||||
format_output: 输出格式。
|
||||
"""
|
||||
logger.info(f"run modeling task for task_id: {task_id}")
|
||||
|
||||
problem = Problem(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""WebSocket 路由模块,提供实时任务消息推送。"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
@@ -48,7 +50,7 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
|
||||
|
||||
# 建立 WebSocket 连接
|
||||
await ws_manager.connect(websocket)
|
||||
websocket.timeout = 500
|
||||
# websocket.timeout 在 Starlette WebSocket 中不可用,已移除
|
||||
logger.debug(f"WebSocket connection status: {websocket.client}")
|
||||
|
||||
# 订阅 Redis 频道
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
"""Agent 间通信数据模型定义。"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Any
|
||||
|
||||
|
||||
class CoordinatorToModeler(BaseModel):
|
||||
"""协调者传递给建模手的数据结构。"""
|
||||
questions: dict
|
||||
ques_count: int
|
||||
|
||||
|
||||
class ModelerToCoder(BaseModel):
|
||||
"""建模手传递给代码手的数据结构。"""
|
||||
questions_solution: dict[str, str]
|
||||
|
||||
|
||||
class CoderToWriter(BaseModel):
|
||||
"""代码手传递给写作手的数据结构。"""
|
||||
code_response: str | None = None
|
||||
code_output: str | None = None
|
||||
created_images: list[str] | None = None
|
||||
|
||||
|
||||
class WriterResponse(BaseModel):
|
||||
"""写作手的响应数据结构。"""
|
||||
response_content: Any
|
||||
footnotes: list[tuple[str, str]] | None = None
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
"""枚举类型定义模块。"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CompTemplate(str, Enum):
|
||||
CHINA: str = "CHINA"
|
||||
AMERICAN: str = "AMERICAN"
|
||||
"""竞赛模板类型。"""
|
||||
CHINA = "CHINA"
|
||||
AMERICAN = "AMERICAN"
|
||||
|
||||
|
||||
class FormatOutPut(str, Enum):
|
||||
Markdown: str = "Markdown"
|
||||
LaTeX: str = "LaTeX"
|
||||
"""输出格式类型。"""
|
||||
Markdown = "Markdown"
|
||||
LaTeX = "LaTeX"
|
||||
|
||||
|
||||
class AgentType(str, Enum):
|
||||
"""Agent 类型标识。"""
|
||||
COORDINATOR = "CoordinatorAgent"
|
||||
MODELER = "ModelerAgent"
|
||||
CODER = "CoderAgent"
|
||||
@@ -20,6 +25,7 @@ class AgentType(str, Enum):
|
||||
|
||||
|
||||
class AgentStatus(str, Enum):
|
||||
"""Agent 执行状态。"""
|
||||
START = "start"
|
||||
WORKING = "working"
|
||||
DONE = "done"
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""请求数据模型定义。"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from app.schemas.enums import CompTemplate, FormatOutPut
|
||||
|
||||
|
||||
class ExampleRequest(BaseModel):
|
||||
"""示例建模请求。"""
|
||||
example_id: str
|
||||
source: str
|
||||
|
||||
|
||||
class Problem(BaseModel):
|
||||
"""建模问题描述。"""
|
||||
task_id: str
|
||||
ques_all: str = ""
|
||||
comp_template: CompTemplate = CompTemplate.CHINA
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""响应数据模型定义,包括消息类型和代码执行结果。"""
|
||||
|
||||
from typing import Literal, Union
|
||||
from app.schemas.enums import AgentType
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -5,6 +7,7 @@ from uuid import uuid4
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""消息基类。"""
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
msg_type: Literal[
|
||||
"system", "agent", "user", "tool"
|
||||
@@ -13,23 +16,23 @@ class Message(BaseModel):
|
||||
|
||||
|
||||
class ToolMessage(Message):
|
||||
msg_type: str = "tool"
|
||||
msg_type: Literal["system", "agent", "user", "tool"] = "tool"
|
||||
tool_name: Literal["execute_code", "search_scholar"]
|
||||
input: dict
|
||||
output: list
|
||||
input: dict | None = None
|
||||
output: list | None = None
|
||||
|
||||
|
||||
class SystemMessage(Message):
|
||||
msg_type: str = "system"
|
||||
msg_type: Literal["system", "agent", "user", "tool"] = "system"
|
||||
type: Literal["info", "warning", "success", "error"] = "info"
|
||||
|
||||
|
||||
class UserMessage(Message):
|
||||
msg_type: str = "user"
|
||||
msg_type: Literal["system", "agent", "user", "tool"] = "user"
|
||||
|
||||
|
||||
class AgentMessage(Message):
|
||||
msg_type: str = "agent"
|
||||
msg_type: Literal["system", "agent", "user", "tool"] = "agent"
|
||||
agent_type: AgentType # CoordinatorAgent | ModelerAgent | CoderAgent | WriterAgent
|
||||
|
||||
|
||||
@@ -42,20 +45,21 @@ class CoordinatorMessage(AgentMessage):
|
||||
|
||||
|
||||
class CodeExecution(BaseModel):
|
||||
"""代码执行结果基类。"""
|
||||
res_type: Literal["stdout", "stderr", "result", "error"]
|
||||
msg: str | None = None
|
||||
|
||||
|
||||
class StdOutModel(CodeExecution):
|
||||
res_type: str = "stdout"
|
||||
res_type: Literal["stdout", "stderr", "result", "error"] = "stdout"
|
||||
|
||||
|
||||
class StdErrModel(CodeExecution):
|
||||
res_type: str = "stderr"
|
||||
res_type: Literal["stdout", "stderr", "result", "error"] = "stderr"
|
||||
|
||||
|
||||
class ResultModel(CodeExecution):
|
||||
res_type: str = "result"
|
||||
res_type: Literal["stdout", "stderr", "result", "error"] = "result"
|
||||
format: Literal[
|
||||
"text",
|
||||
"html",
|
||||
@@ -71,7 +75,7 @@ class ResultModel(CodeExecution):
|
||||
|
||||
|
||||
class ErrorModel(CodeExecution):
|
||||
res_type: str = "error"
|
||||
res_type: Literal["stdout", "stderr", "result", "error"] = "error"
|
||||
name: str
|
||||
value: str
|
||||
traceback: str
|
||||
@@ -82,13 +86,13 @@ OutputItem = Union[StdOutModel, StdErrModel, ResultModel, ErrorModel]
|
||||
|
||||
|
||||
class ScholarMessage(ToolMessage):
|
||||
tool_name: str = "search_scholar"
|
||||
tool_name: Literal["execute_code", "search_scholar"] = "search_scholar"
|
||||
input: dict | None = None # query
|
||||
output: list[str] | None = None # cites
|
||||
|
||||
|
||||
class InterpreterMessage(ToolMessage):
|
||||
tool_name: str = "execute_code"
|
||||
tool_name: Literal["execute_code", "search_scholar"] = "execute_code"
|
||||
input: dict | None = None # code
|
||||
output: list[OutputItem] | None = None # code_results
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""工具调用结果数据模型。"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class ToolResult(BaseModel):
|
||||
"""工具调用的通用返回结构。"""
|
||||
success: bool
|
||||
message: Optional[str] = None
|
||||
data: Optional[Any] = None
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Redis 管理模块,提供消息发布/订阅和持久化存储。"""
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
from typing import Optional
|
||||
import json
|
||||
@@ -8,6 +10,7 @@ from app.utils.log_util import logger
|
||||
|
||||
|
||||
class RedisManager:
|
||||
"""Redis 连接管理器,负责消息发布/订阅和任务消息持久化。"""
|
||||
def __init__(self):
|
||||
self.redis_url = settings.REDIS_URL
|
||||
self._client: Optional[aioredis.Redis] = None
|
||||
@@ -23,7 +26,7 @@ class RedisManager:
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
)
|
||||
try:
|
||||
await self._client.ping()
|
||||
await self._client.ping() # type: ignore[reportGeneralTypeIssues]
|
||||
logger.info(f"Redis 连接建立成功: {self.redis_url}")
|
||||
return self._client
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""WebSocket 连接管理模块。"""
|
||||
|
||||
from fastapi import WebSocket
|
||||
|
||||
|
||||
class WebSocketManager:
|
||||
"""WebSocket 连接管理器,维护活跃连接并提供消息广播功能。"""
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""配置模板获取测试脚本。"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -7,6 +9,7 @@ from app.schemas.enums import CompTemplate
|
||||
|
||||
|
||||
def test_get_config_template():
|
||||
"""测试获取中国竞赛模板配置。"""
|
||||
from app.utils.common_utils import get_config_template
|
||||
|
||||
comp_template = CompTemplate.CHINA
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
"""通用工具函数单元测试。"""
|
||||
|
||||
import unittest
|
||||
|
||||
from app.utils.common_utils import split_footnotes
|
||||
|
||||
|
||||
class TestCommonUtils(unittest.TestCase):
|
||||
"""测试 common_utils 模块的核心函数。"""
|
||||
|
||||
def test_split_footnotes(self):
|
||||
"""测试脚注分离功能。"""
|
||||
text = "Example[^1]\n\n[^1]: Footnote content"
|
||||
main, notes = split_footnotes(text)
|
||||
self.assertEqual(main, "Example")
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
"""E2B 代码解释器集成测试。"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import unittest
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.tools.e2b_interpreter import E2BCodeInterpreter
|
||||
from app.tools.notebook_serializer import NotebookSerializer
|
||||
from app.utils.common_utils import create_work_dir
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ModuleNotFoundError: # Fallback if python-dotenv is not installed
|
||||
def load_dotenv(*args, **kwargs):
|
||||
return None
|
||||
|
||||
try:
|
||||
from app.tools.e2b_interpreter import E2BCodeInterpreter
|
||||
except ModuleNotFoundError:
|
||||
E2BCodeInterpreter = None
|
||||
from app.utils.common_utils import create_task_id, create_work_dir
|
||||
|
||||
|
||||
|
||||
class TestE2BCodeInterpreter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
load_dotenv()
|
||||
"""E2B 代码解释器测试用例。"""
|
||||
|
||||
def setUp(self):
|
||||
"""初始化测试环境,创建沙箱和代码解释器。"""
|
||||
if E2BCodeInterpreter is None:
|
||||
self.skipTest("e2b_code_interpreter not available")
|
||||
_, dirs = create_work_dir("20250312-104132-d3625cab")
|
||||
notebook = NotebookSerializer(dirs["jupyter"])
|
||||
self.task_id = "20250312-104132-d3625cab"
|
||||
self.work_dir = create_work_dir(self.task_id)
|
||||
notebook_path = os.path.join(self.work_dir, "jupyter")
|
||||
notebook = NotebookSerializer(notebook_path)
|
||||
|
||||
self.code_interpreter = E2BCodeInterpreter(
|
||||
self.task_id, self.work_dir, notebook
|
||||
)
|
||||
|
||||
def test_execute_code(self):
|
||||
"""测试在 E2B 沙箱中执行 Python 代码。"""
|
||||
if not os.getenv("E2B_API_KEY"):
|
||||
self.skipTest("E2B_API_KEY not set")
|
||||
|
||||
|
||||
+26
-31
@@ -1,3 +1,5 @@
|
||||
"""工具基类模块,提供工具注册和调用的基础设施。"""
|
||||
|
||||
from typing import Dict, Any, List, Callable
|
||||
import inspect
|
||||
from app.schemas.tool_result import ToolResult
|
||||
@@ -9,16 +11,16 @@ def tool(
|
||||
parameters: Dict[str, Dict[str, Any]],
|
||||
required: List[str],
|
||||
) -> Callable:
|
||||
"""Tool registration decorator
|
||||
"""工具注册装饰器,为函数生成工具 schema。
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
description: Tool description
|
||||
parameters: Tool parameter definitions
|
||||
required: List of required parameters
|
||||
name: 工具名称。
|
||||
description: 工具描述。
|
||||
parameters: 工具参数定义。
|
||||
required: 必需参数列表。
|
||||
|
||||
Returns:
|
||||
Decorator function
|
||||
装饰器函数。
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@@ -47,66 +49,59 @@ def tool(
|
||||
|
||||
|
||||
class BaseTool:
|
||||
"""Base tool class, providing common tool calling methods"""
|
||||
"""工具基类,提供工具注册、查询和调用的通用方法。"""
|
||||
|
||||
name: str = ""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base tool class"""
|
||||
pass
|
||||
self._tools_cache = None
|
||||
|
||||
def get_tools(self) -> List[Dict[str, Any]]:
|
||||
"""Get all registered tools
|
||||
|
||||
Returns:
|
||||
List of tools
|
||||
"""
|
||||
"""获取所有已注册的工具 schema 列表。"""
|
||||
if self._tools_cache is not None:
|
||||
return self._tools_cache
|
||||
|
||||
tools = []
|
||||
for _, method in inspect.getmembers(self, inspect.ismethod):
|
||||
if hasattr(method, "_tool_schema"):
|
||||
tools.append(method._tool_schema)
|
||||
schema = getattr(method, "_tool_schema", None)
|
||||
if schema is not None:
|
||||
tools.append(schema)
|
||||
|
||||
self._tools_cache = tools
|
||||
return tools
|
||||
|
||||
def has_function(self, function_name: str) -> bool:
|
||||
"""Check if specified function exists
|
||||
"""检查指定名称的工具是否存在。
|
||||
|
||||
Args:
|
||||
function_name: Function name
|
||||
function_name: 工具函数名称。
|
||||
|
||||
Returns:
|
||||
Whether the tool exists
|
||||
工具是否存在。
|
||||
"""
|
||||
for _, method in inspect.getmembers(self, inspect.ismethod):
|
||||
if (
|
||||
hasattr(method, "_function_name")
|
||||
and method._function_name == function_name
|
||||
):
|
||||
fn_name = getattr(method, "_function_name", None)
|
||||
if fn_name == function_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def invoke_function(self, function_name: str, **kwargs) -> ToolResult:
|
||||
"""Invoke specified tool
|
||||
"""调用指定的工具函数。
|
||||
|
||||
Args:
|
||||
function_name: Function name
|
||||
**kwargs: Parameters
|
||||
function_name: 工具函数名称。
|
||||
**kwargs: 传递给工具的参数。
|
||||
|
||||
Returns:
|
||||
Invocation result
|
||||
工具调用结果。
|
||||
|
||||
Raises:
|
||||
ValueError: Raised when tool doesn't exist
|
||||
ValueError: 工具不存在时抛出。
|
||||
"""
|
||||
for _, method in inspect.getmembers(self, inspect.ismethod):
|
||||
if (
|
||||
hasattr(method, "_function_name")
|
||||
and method._function_name == function_name
|
||||
):
|
||||
fn_name = getattr(method, "_function_name", None)
|
||||
if fn_name == function_name:
|
||||
return await method(**kwargs)
|
||||
|
||||
raise ValueError(f"Tool '{function_name}' not found")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# base_interpreter.py
|
||||
"""代码解释器抽象基类模块。"""
|
||||
|
||||
import abc
|
||||
import re
|
||||
from app.tools.notebook_serializer import NotebookSerializer
|
||||
@@ -11,6 +12,7 @@ from app.schemas.response import (
|
||||
|
||||
|
||||
class BaseCodeInterpreter(abc.ABC):
|
||||
"""代码解释器抽象基类,定义代码执行、输出管理和资源清理的接口。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""E2B 云端代码解释器模块,通过 E2B 沙箱执行 Python 代码。"""
|
||||
|
||||
import os
|
||||
from e2b_code_interpreter import AsyncSandbox
|
||||
from e2b_code_interpreter import AsyncSandbox # type: ignore[import-unresolved]
|
||||
from app.schemas.response import (
|
||||
ErrorModel,
|
||||
OutputItem,
|
||||
@@ -17,6 +19,7 @@ from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
|
||||
|
||||
class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
"""基于 E2B 沙箱的云端代码解释器。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -70,6 +73,7 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
# 使用官方推荐的 files.write 方法
|
||||
assert self.sbx is not None
|
||||
await self.sbx.files.write(f"/home/user/{file}", content)
|
||||
logger.info(f"成功上传文件到沙箱: {file}")
|
||||
except Exception as e:
|
||||
@@ -159,20 +163,20 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
# 1. 文本格式
|
||||
if str(result):
|
||||
content_to_display.append(
|
||||
ResultModel(type="result", format="text", msg=str(result))
|
||||
ResultModel(res_type="result", format="text", msg=str(result))
|
||||
)
|
||||
# 2. HTML格式
|
||||
if result._repr_html_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="html", msg=result._repr_html_()
|
||||
res_type="result", format="html", msg=result._repr_html_()
|
||||
)
|
||||
)
|
||||
# 3. Markdown格式
|
||||
if result._repr_markdown_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result",
|
||||
res_type="result",
|
||||
format="markdown",
|
||||
msg=result._repr_markdown_(),
|
||||
)
|
||||
@@ -181,42 +185,42 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
if result._repr_png_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="png", msg=result._repr_png_()
|
||||
res_type="result", format="png", msg=result._repr_png_()
|
||||
)
|
||||
)
|
||||
# 5. JPEG图片
|
||||
if result._repr_jpeg_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="jpeg", msg=result._repr_jpeg_()
|
||||
res_type="result", format="jpeg", msg=result._repr_jpeg_()
|
||||
)
|
||||
)
|
||||
# 6. SVG
|
||||
if result._repr_svg_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="svg", msg=result._repr_svg_()
|
||||
res_type="result", format="svg", msg=result._repr_svg_()
|
||||
)
|
||||
)
|
||||
# 7. PDF
|
||||
if result._repr_pdf_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="pdf", msg=result._repr_pdf_()
|
||||
res_type="result", format="pdf", msg=result._repr_pdf_()
|
||||
)
|
||||
)
|
||||
# 8. LaTeX
|
||||
if result._repr_latex_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result", format="latex", msg=result._repr_latex_()
|
||||
res_type="result", format="latex", msg=result._repr_latex_()
|
||||
)
|
||||
)
|
||||
# 9. JSON
|
||||
if result._repr_json_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result",
|
||||
res_type="result",
|
||||
format="json",
|
||||
msg=json.dumps(result._repr_json_()),
|
||||
)
|
||||
@@ -225,7 +229,7 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
if result._repr_javascript_():
|
||||
content_to_display.append(
|
||||
ResultModel(
|
||||
type="result",
|
||||
res_type="result",
|
||||
format="javascript",
|
||||
msg=result._repr_javascript_(),
|
||||
)
|
||||
@@ -324,9 +328,10 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
async def download_all_files_from_sandbox(self) -> None:
|
||||
"""从沙箱中下载所有文件并与本地同步"""
|
||||
try:
|
||||
assert self.sbx is not None
|
||||
# 获取沙箱中的文件列表
|
||||
sandbox_files = await self.sbx.files.list("/home/user")
|
||||
sandbox_files_dict = {f.name: f for f in sandbox_files}
|
||||
sandbox_files_dict = {f.name: f for f in sandbox_files} # noqa: F841
|
||||
|
||||
# 获取本地文件列表
|
||||
local_files = set()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# interpreter_factory.py
|
||||
"""代码解释器工厂模块,根据配置创建本地或远程解释器。"""
|
||||
|
||||
from typing import Literal
|
||||
from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
from app.tools.e2b_interpreter import E2BCodeInterpreter
|
||||
from app.tools.local_interpreter import LocalCodeInterpreter
|
||||
from app.tools.notebook_serializer import NotebookSerializer
|
||||
@@ -15,6 +17,21 @@ async def create_interpreter(
|
||||
notebook_serializer: NotebookSerializer,
|
||||
timeout=3000,
|
||||
):
|
||||
"""创建代码解释器实例。
|
||||
|
||||
Args:
|
||||
kind: 解释器类型,"remote" 使用 E2B 沙箱,"local" 使用本地 Jupyter。
|
||||
task_id: 任务 ID。
|
||||
work_dir: 工作目录。
|
||||
notebook_serializer: Notebook 序列化器。
|
||||
timeout: 超时时间(秒)。
|
||||
|
||||
Returns:
|
||||
初始化完成的代码解释器实例。
|
||||
|
||||
Raises:
|
||||
ValueError: 未知的解释器类型时抛出。
|
||||
"""
|
||||
if not settings.E2B_API_KEY:
|
||||
logger.info("默认使用本地解释器")
|
||||
kind = "local"
|
||||
@@ -22,16 +39,17 @@ async def create_interpreter(
|
||||
logger.info("使用远程解释器")
|
||||
kind = "remote"
|
||||
|
||||
interp: BaseCodeInterpreter
|
||||
if kind == "remote":
|
||||
interp: E2BCodeInterpreter = await E2BCodeInterpreter.create(
|
||||
interp = await E2BCodeInterpreter.create(
|
||||
task_id=task_id,
|
||||
work_dir=work_dir,
|
||||
notebook_serializer=notebook_serializer,
|
||||
)
|
||||
await interp.initialize(timeout=timeout)
|
||||
await interp.initialize(timeout=timeout) # type: ignore[reportCallIssue]
|
||||
return interp
|
||||
elif kind == "local":
|
||||
interp: LocalCodeInterpreter = LocalCodeInterpreter(
|
||||
interp = LocalCodeInterpreter(
|
||||
task_id=task_id,
|
||||
work_dir=work_dir,
|
||||
notebook_serializer=notebook_serializer,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""本地代码解释器模块,通过本地 Jupyter 内核执行 Python 代码。"""
|
||||
|
||||
from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
from app.tools.notebook_serializer import NotebookSerializer
|
||||
import jupyter_client
|
||||
@@ -13,6 +15,7 @@ from app.schemas.response import (
|
||||
|
||||
|
||||
class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
"""基于本地 Jupyter 内核的代码解释器。"""
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -32,7 +35,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
)
|
||||
self._pre_execute_code()
|
||||
|
||||
def _pre_execute_code(self):
|
||||
def _pre_execute_code(self): # type: ignore[reportIncompatibleMethodOverride]
|
||||
init_code = (
|
||||
f"import os\n"
|
||||
f"work_dir = r'{self.work_dir}'\n"
|
||||
@@ -82,7 +85,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
text_to_gpt.append(self._truncate_text(f"[{mark}]\n{out_str}"))
|
||||
# 添加text到notebook
|
||||
content_to_display.append(
|
||||
ResultModel(type="result", format="text", msg=out_str)
|
||||
ResultModel(res_type="result", format="text", msg=out_str)
|
||||
)
|
||||
self.notebook_serializer.add_code_cell_output_to_notebook(out_str)
|
||||
|
||||
@@ -99,14 +102,14 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
if "png" in mark:
|
||||
self.notebook_serializer.add_image_to_notebook(out_str, "image/png")
|
||||
content_to_display.append(
|
||||
ResultModel(type="result", format="png", msg=out_str)
|
||||
ResultModel(res_type="result", format="png", msg=out_str)
|
||||
)
|
||||
else:
|
||||
self.notebook_serializer.add_image_to_notebook(
|
||||
out_str, "image/jpeg"
|
||||
)
|
||||
content_to_display.append(
|
||||
ResultModel(type="result", format="jpeg", msg=out_str)
|
||||
ResultModel(res_type="result", format="jpeg", msg=out_str)
|
||||
)
|
||||
|
||||
elif mark == "error":
|
||||
@@ -131,7 +134,9 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
)
|
||||
|
||||
def execute_code_(self, code) -> list[tuple[str, str]]:
|
||||
msg_id = self.kc.execute(code)
|
||||
assert self.kc is not None
|
||||
assert self.km is not None
|
||||
self.kc.execute(code)
|
||||
logger.info(f"执行代码: {code}")
|
||||
# Get the output of the code
|
||||
msg_list = []
|
||||
@@ -144,7 +149,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
and iopub_msg["content"].get("execution_state") == "idle"
|
||||
):
|
||||
break
|
||||
except:
|
||||
except Exception:
|
||||
if self.interrupt_signal:
|
||||
self.km.interrupt_kernel()
|
||||
self.interrupt_signal = False
|
||||
@@ -211,6 +216,8 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
|
||||
async def cleanup(self):
|
||||
# 关闭内核
|
||||
assert self.kc is not None
|
||||
assert self.km is not None
|
||||
self.kc.shutdown()
|
||||
logger.info("关闭内核")
|
||||
self.km.shutdown_kernel()
|
||||
@@ -220,6 +227,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
|
||||
def restart_jupyter_kernel(self):
|
||||
"""Restart the Jupyter kernel and recreate the work directory."""
|
||||
assert self.kc is not None
|
||||
self.kc.shutdown()
|
||||
self.km, self.kc = jupyter_client.manager.start_new_kernel(
|
||||
kernel_name="python3"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Jupyter Notebook 序列化模块,管理 notebook 的创建和内容追加。"""
|
||||
|
||||
import nbformat
|
||||
from nbformat import v4 as nbf
|
||||
import ansi2html
|
||||
import ansi2html # type: ignore[import-unresolved]
|
||||
import os
|
||||
|
||||
|
||||
class NotebookSerializer:
|
||||
"""Jupyter Notebook 序列化器,负责创建和维护 .ipynb 文件。"""
|
||||
def __init__(self, work_dir=None, notebook_name="notebook.ipynb"):
|
||||
self.nb = nbf.new_notebook()
|
||||
self.notebook_path = None
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""OpenAlex 学术文献搜索模块。"""
|
||||
|
||||
import requests
|
||||
from typing import List, Dict, Any
|
||||
from app.services.redis_manager import redis_manager
|
||||
@@ -5,12 +7,15 @@ from app.schemas.response import ScholarMessage
|
||||
|
||||
|
||||
class OpenAlexScholar:
|
||||
def __init__(self, task_id: str, email: str = None, api_key: str = None):
|
||||
"""Initialize OpenAlex client.
|
||||
"""OpenAlex 学术文献搜索客户端。"""
|
||||
|
||||
def __init__(self, task_id: str, email: str | None = None, api_key: str | None = None):
|
||||
"""初始化 OpenAlex 客户端。
|
||||
|
||||
Args:
|
||||
email: Optional email for better API service
|
||||
api_key: Optional OpenAlex API key
|
||||
task_id: 任务 ID。
|
||||
email: 可选的邮箱地址,用于获取更好的 API 服务。
|
||||
api_key: 可选的 OpenAlex API Key。
|
||||
"""
|
||||
self.base_url = "https://api.openalex.org"
|
||||
self.email = email
|
||||
@@ -18,7 +23,11 @@ class OpenAlexScholar:
|
||||
self.task_id = task_id
|
||||
|
||||
def _get_request_url(self, endpoint: str) -> str:
|
||||
"""Construct request URL with email parameter if provided."""
|
||||
"""构建请求 URL。
|
||||
|
||||
Args:
|
||||
endpoint: API 端点路径。
|
||||
"""
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
return f"{self.base_url}/{endpoint}"
|
||||
@@ -52,14 +61,14 @@ class OpenAlexScholar:
|
||||
return " ".join(words).strip()
|
||||
|
||||
async def search_papers(self, query: str, limit: int = 8) -> List[Dict[str, Any]]:
|
||||
"""Search for papers using OpenAlex API.
|
||||
"""使用 OpenAlex API 搜索学术论文。
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
limit: Maximum number of results to return
|
||||
query: 搜索关键词。
|
||||
limit: 最大返回结果数。
|
||||
|
||||
Returns:
|
||||
List of papers with their details
|
||||
包含论文详细信息的字典列表。
|
||||
"""
|
||||
# 构建基础 URL
|
||||
base_url = self._get_request_url("works")
|
||||
@@ -87,6 +96,7 @@ class OpenAlexScholar:
|
||||
}
|
||||
|
||||
# 让 requests 处理参数编码和 URL 构建
|
||||
response: requests.Response | None = None
|
||||
try:
|
||||
print(f"请求 URL: {base_url} 参数: {params}")
|
||||
response = requests.get(base_url, params=params, headers=headers)
|
||||
@@ -96,11 +106,11 @@ class OpenAlexScholar:
|
||||
results = response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"HTTP 错误: {e}")
|
||||
if response.status_code == 403:
|
||||
if response is not None and response.status_code == 403:
|
||||
print(
|
||||
"提示: 403错误通常意味着您需要提供有效的邮箱地址或者遵循礼貌池(polite pool)规则"
|
||||
)
|
||||
if hasattr(response, "text"):
|
||||
if response is not None and hasattr(response, "text"):
|
||||
print(f"响应内容: {response.text}")
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -165,7 +175,7 @@ class OpenAlexScholar:
|
||||
return papers
|
||||
|
||||
def papers_to_str(self, papers: List[Dict[str, Any]]) -> str:
|
||||
"""将文献列表转换为字符串"""
|
||||
"""将文献列表转换为可读字符串。"""
|
||||
result = ""
|
||||
for paper in papers:
|
||||
result += "\n" + "=" * 80
|
||||
@@ -181,7 +191,7 @@ class OpenAlexScholar:
|
||||
return result
|
||||
|
||||
def _format_citation(self, work: Dict[str, Any]) -> str:
|
||||
"""Format citation in a readable format."""
|
||||
"""将论文数据格式化为引用字符串。"""
|
||||
# 获取所有作者
|
||||
authors = [
|
||||
authorship.get("author", {}).get("display_name")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Rich 终端美化输出模块。"""
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
@@ -8,6 +10,7 @@ from app.utils.log_util import logger
|
||||
|
||||
|
||||
class RichPrinter:
|
||||
"""Rich 终端美化打印工具,提供面板、表格和 Agent 消息的格式化输出。"""
|
||||
# 类属性:全局样式配置
|
||||
_styles = {
|
||||
"success": {"emoji": "✅", "color": "green", "prefix": "成功"},
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
"""CLI 显示工具模块,提供 ASCII 横幅和终端居中显示功能。"""
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
def center_cli_str(text: str, width: int | None = None):
|
||||
"""将多行文本在终端中居中显示。
|
||||
|
||||
Args:
|
||||
text: 待居中的多行文本。
|
||||
width: 终端宽度,默认自动检测。
|
||||
"""
|
||||
import shutil
|
||||
|
||||
width = width or shutil.get_terminal_size().columns
|
||||
@@ -12,6 +21,14 @@ def center_cli_str(text: str, width: int | None = None):
|
||||
|
||||
|
||||
def get_ascii_banner(center: bool = True) -> str:
|
||||
"""获取项目 ASCII 横幅。
|
||||
|
||||
Args:
|
||||
center: 是否居中显示。
|
||||
|
||||
Returns:
|
||||
ASCII 横幅字符串。
|
||||
"""
|
||||
text = dedent(
|
||||
r"""
|
||||
===============================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""通用工具函数模块,提供任务 ID 生成、文件操作和文档转换等功能。"""
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import hashlib
|
||||
@@ -5,15 +7,14 @@ import tomllib
|
||||
from app.schemas.enums import CompTemplate
|
||||
from app.utils.log_util import logger
|
||||
import re
|
||||
import pypandoc
|
||||
import pypandoc # type: ignore[import-unresolved]
|
||||
from app.config.setting import settings
|
||||
from icecream import ic
|
||||
|
||||
TASK_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
|
||||
|
||||
def create_task_id() -> str:
|
||||
"""生成任务ID"""
|
||||
"""生成基于时间戳和随机哈希的唯一任务 ID。"""
|
||||
# 生成时间戳和随机hash
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
random_hash = hashlib.md5(str(datetime.datetime.now()).encode()).hexdigest()[:8]
|
||||
@@ -21,6 +22,17 @@ def create_task_id() -> str:
|
||||
|
||||
|
||||
def ensure_safe_task_id(task_id: str) -> str:
|
||||
"""验证任务 ID 的合法性,防止路径遍历攻击。
|
||||
|
||||
Args:
|
||||
task_id: 待验证的任务 ID。
|
||||
|
||||
Returns:
|
||||
验证通过的任务 ID。
|
||||
|
||||
Raises:
|
||||
ValueError: 任务 ID 不合法时抛出。
|
||||
"""
|
||||
normalized = (task_id or "").strip()
|
||||
if not normalized or not TASK_ID_PATTERN.fullmatch(normalized):
|
||||
raise ValueError("非法 task_id")
|
||||
@@ -28,6 +40,14 @@ def ensure_safe_task_id(task_id: str) -> str:
|
||||
|
||||
|
||||
def create_work_dir(task_id: str) -> str:
|
||||
"""为指定任务创建工作目录。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
|
||||
Returns:
|
||||
工作目录路径。
|
||||
"""
|
||||
# 设置主工作目录和子目录
|
||||
work_dir = os.path.join("project", "work_dir", task_id)
|
||||
|
||||
@@ -42,6 +62,17 @@ def create_work_dir(task_id: str) -> str:
|
||||
|
||||
|
||||
def get_work_dir(task_id: str) -> str:
|
||||
"""获取指定任务的工作目录路径。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
|
||||
Returns:
|
||||
工作目录路径。
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 工作目录不存在时抛出。
|
||||
"""
|
||||
work_dir = os.path.join("project", "work_dir", task_id)
|
||||
if os.path.exists(work_dir):
|
||||
return work_dir
|
||||
@@ -50,23 +81,48 @@ def get_work_dir(task_id: str) -> str:
|
||||
raise FileNotFoundError(f"工作目录不存在: {work_dir}")
|
||||
|
||||
|
||||
# TODO: 是不是应该将 Prompt 写成一个 class
|
||||
# TODO: 是不是应该将 Prompt 写成一个 class
|
||||
def get_config_template(comp_template: CompTemplate = CompTemplate.CHINA) -> dict:
|
||||
"""获取论文模板配置。
|
||||
|
||||
Args:
|
||||
comp_template: 竞赛模板类型。
|
||||
|
||||
Returns:
|
||||
模板配置字典。
|
||||
"""
|
||||
if comp_template == CompTemplate.CHINA:
|
||||
return load_toml(os.path.join("app", "config", "md_template.toml"))
|
||||
return {}
|
||||
|
||||
|
||||
def load_toml(path: str) -> dict:
|
||||
"""加载 TOML 配置文件。
|
||||
|
||||
Args:
|
||||
path: TOML 文件路径。
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def load_markdown(path: str) -> str:
|
||||
"""加载 Markdown 文件内容。
|
||||
|
||||
Args:
|
||||
path: Markdown 文件路径。
|
||||
"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def get_current_files(folder_path: str, type: str = "all") -> list[str]:
|
||||
"""获取指定目录下的文件列表。
|
||||
|
||||
Args:
|
||||
folder_path: 目录路径。
|
||||
type: 文件类型过滤(all/md/ipynb/data/image)。
|
||||
"""
|
||||
files = os.listdir(folder_path)
|
||||
if type == "all":
|
||||
return files
|
||||
@@ -82,10 +138,16 @@ def get_current_files(folder_path: str, type: str = "all") -> list[str]:
|
||||
return [
|
||||
file for file in files if file.endswith(".png") or file.endswith(".jpg")
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
# 判断content是否包含图片 xx.png,对其处理为 
|
||||
def transform_link(task_id: str, content: str):
|
||||
"""将 Markdown 中的图片链接转换为静态资源 URL。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID,用于构建 URL 路径。
|
||||
content: 包含图片链接的 Markdown 文本。
|
||||
"""
|
||||
content = re.sub(
|
||||
r"!\[(.*?)\]\((.*?\.(?:png|jpg|jpeg|gif|bmp|webp))\)",
|
||||
lambda match: f"})",
|
||||
@@ -94,8 +156,12 @@ def transform_link(task_id: str, content: str):
|
||||
return content
|
||||
|
||||
|
||||
# TODO: fix 公式显示
|
||||
def md_2_docx(task_id: str):
|
||||
"""将 Markdown 论文转换为 DOCX 格式。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
"""
|
||||
work_dir = get_work_dir(task_id)
|
||||
md_path = os.path.join(work_dir, "res.md")
|
||||
docx_path = os.path.join(work_dir, "res.docx")
|
||||
@@ -119,6 +185,14 @@ def md_2_docx(task_id: str):
|
||||
|
||||
|
||||
def split_footnotes(text: str) -> tuple[str, list[tuple[str, str]]]:
|
||||
"""从文本中分离正文和脚注。
|
||||
|
||||
Args:
|
||||
text: 包含脚注的完整文本。
|
||||
|
||||
Returns:
|
||||
(正文, 脚注列表) 的元组,脚注格式为 (编号, 内容)。
|
||||
"""
|
||||
main_text = re.sub(
|
||||
r"\n\[\^\d+\]:.*?(?=\n\[\^|\n\n|\Z)", "", text, flags=re.DOTALL
|
||||
).strip()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""数据记录模块,记录 Agent 的对话历史和 Token 使用情况。"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from app.utils.log_util import logger
|
||||
@@ -5,9 +7,8 @@ from typing import Any, Dict
|
||||
|
||||
|
||||
# TODO: 记录数据
|
||||
# data analysis : save all data and result
|
||||
# agent-histroy, token usgae, , cost , workflow cost , res
|
||||
class DataRecorder:
|
||||
"""记录 Agent 对话历史、Token 使用量和 API 调用费用。"""
|
||||
def __init__(self, log_work_dir: str = ""):
|
||||
self.total_cost = 0.0
|
||||
self.agents_chat_history = {}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""日志初始化模块,配置 loguru 日志格式和输出。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from loguru import logger as _logger
|
||||
from loguru import logger as _logger # type: ignore[import-unresolved]
|
||||
|
||||
|
||||
class LoggerInitializer:
|
||||
"""日志初始化器,配置控制台和文件日志输出。"""
|
||||
def __init__(self):
|
||||
self.log_path = os.path.join(os.getcwd(), "logs")
|
||||
self.__ensure_log_directory_exists()
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
import litellm
|
||||
"""LLM 调用指标收集模块。"""
|
||||
|
||||
from litellm.integrations.custom_logger import CustomLogger # type: ignore[import-unresolved]
|
||||
|
||||
|
||||
class AgentMetrics(CustomLogger):
|
||||
#### ASYNC ####
|
||||
"""LLM 调用指标收集器,记录成功的 API 调用信息。"""
|
||||
|
||||
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
try:
|
||||
# response_cost = kwargs.get("response_cost", 0)
|
||||
# print("streaming response_cost", response_cost)
|
||||
print("agent_name", kwargs["litellm_params"]["metadata"]["agent_name"])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
print(f"On Async Failure")
|
||||
print("On Async Failure")
|
||||
|
||||
|
||||
# 全局指标收集器实例
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import Toaster from '@/components/ui/toast/Toaster.vue'
|
||||
import Toaster from "@/components/ui/toast/Toaster.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,66 +1,84 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 验证 API Key 请求参数
|
||||
/** 验证 API Key 请求参数 */
|
||||
export interface ValidateApiKeyRequest {
|
||||
api_key: string;
|
||||
base_url?: string;
|
||||
model_id: string;
|
||||
api_key: string;
|
||||
base_url?: string;
|
||||
model_id: string;
|
||||
}
|
||||
|
||||
// 验证 API Key 响应
|
||||
/** 验证 API Key 响应 */
|
||||
export interface ValidateApiKeyResponse {
|
||||
valid: boolean;
|
||||
message: string;
|
||||
valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 保存 API 配置请求参数
|
||||
/** 保存 API 配置请求参数 */
|
||||
export interface SaveApiConfigRequest {
|
||||
coordinator: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
modeler: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
coder: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
writer: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
openalex_email: string;
|
||||
coordinator: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
modeler: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
coder: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
writer: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
openalex_email: string;
|
||||
}
|
||||
|
||||
/** 验证 OpenAlex Email 请求参数 */
|
||||
export interface ValidateOpenalexEmailRequest {
|
||||
email: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/** 验证 OpenAlex Email 响应 */
|
||||
export interface ValidateOpenalexEmailResponse {
|
||||
valid: boolean;
|
||||
message: string;
|
||||
valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 验证 API Key
|
||||
/**
|
||||
* 验证 API Key 是否有效
|
||||
* @param params 验证请求参数
|
||||
*/
|
||||
export function validateApiKey(params: ValidateApiKeyRequest) {
|
||||
return request.post<ValidateApiKeyResponse>("/validate-api-key", params);
|
||||
return request.post<ValidateApiKeyResponse>("/validate-api-key", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 OpenAlex Email 是否有效
|
||||
* @param params 验证请求参数
|
||||
*/
|
||||
export function validateOpenalexEmail(params: ValidateOpenalexEmailRequest) {
|
||||
return request.post<ValidateOpenalexEmailResponse>("/validate-openalex-email", params);
|
||||
return request.post<ValidateOpenalexEmailResponse>(
|
||||
"/validate-openalex-email",
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// 保存 API 配置
|
||||
/**
|
||||
* 保存 API 配置到后端
|
||||
* @param params API 配置参数
|
||||
*/
|
||||
export function saveApiConfig(params: SaveApiConfigRequest) {
|
||||
return request.post<{ success: boolean; message: string }>("/save-api-config", params);
|
||||
}
|
||||
return request.post<{ success: boolean; message: string }>(
|
||||
"/save-api-config",
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import request from "@/utils/request";
|
||||
import type { Message } from "@/utils/response";
|
||||
|
||||
/** 健康检查 */
|
||||
export function getHelloWorld() {
|
||||
return request.get<{ message: string }>("/");
|
||||
}
|
||||
|
||||
// 获取论文顺序
|
||||
/** 获取论文写作顺序 */
|
||||
export function getWriterSeque() {
|
||||
return request.get<{ writer_seque: string[] }>("/writer_seque");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务的历史消息
|
||||
* @param task_id 任务ID
|
||||
*/
|
||||
export function getTaskMessages(task_id: string) {
|
||||
return request.get<Message[]>("/messages", {
|
||||
params: {
|
||||
@@ -18,7 +23,10 @@ export function getTaskMessages(task_id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打开工作目录
|
||||
* @param task_id 任务ID
|
||||
*/
|
||||
export function openFolderAPI(task_id: string) {
|
||||
return request.get<{ message: string }>("/open_folder", {
|
||||
params: {
|
||||
@@ -27,7 +35,11 @@ export function openFolderAPI(task_id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提交样例任务
|
||||
* @param example_id 样例ID
|
||||
* @param source 来源
|
||||
*/
|
||||
export function exampleAPI(example_id: string, source: string) {
|
||||
return request.post<{
|
||||
task_id: string;
|
||||
@@ -38,7 +50,7 @@ export function exampleAPI(example_id: string, source: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取服务状态
|
||||
/** 获取后端和 Redis 服务状态 */
|
||||
export function getServiceStatus() {
|
||||
return request.get<{
|
||||
backend: { status: string; message: string };
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取任务工作区文件列表
|
||||
* @param task_id 任务ID
|
||||
*/
|
||||
export function getFiles(task_id: string) {
|
||||
return request.get<{
|
||||
files: {
|
||||
filename: string;
|
||||
file_type: string;
|
||||
}[]
|
||||
}>("/files", {
|
||||
params: { task_id },
|
||||
});
|
||||
return request.get<{
|
||||
files: {
|
||||
filename: string;
|
||||
file_type: string;
|
||||
}[];
|
||||
}>("/files", {
|
||||
params: { task_id },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取单个文件下载链接
|
||||
* @param task_id 任务ID
|
||||
* @param filename 文件名
|
||||
*/
|
||||
export async function getFileDownloadUrl(task_id: string, filename: string) {
|
||||
return await request.get<{ download_url: string }>(`/download_url`, {
|
||||
params: {
|
||||
task_id,
|
||||
filename,
|
||||
}
|
||||
})
|
||||
return await request.get<{ download_url: string }>("/download_url", {
|
||||
params: {
|
||||
task_id,
|
||||
filename,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,9 +34,9 @@ export async function getFileDownloadUrl(task_id: string, filename: string) {
|
||||
* @param task_id 任务ID
|
||||
*/
|
||||
export async function getAllFilesDownloadUrl(task_id: string) {
|
||||
return await request.get<{ download_url: string }>(`/download_all_url`, {
|
||||
params: {
|
||||
task_id,
|
||||
}
|
||||
})
|
||||
}
|
||||
return await request.get<{ download_url: string }>("/download_all_url", {
|
||||
params: {
|
||||
task_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// ... existing code ...
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 提交数学建模任务
|
||||
* @param problem 问题描述
|
||||
* @param files 上传的数据文件
|
||||
*/
|
||||
export function submitModelingTask(
|
||||
problem: {
|
||||
ques_all: string;
|
||||
@@ -12,7 +16,7 @@ export function submitModelingTask(
|
||||
const formData = new FormData();
|
||||
// 添加问题数据
|
||||
formData.append("ques_all", problem.ques_all);
|
||||
formData.append("comp_template", 'CHINA');
|
||||
formData.append("comp_template", "CHINA");
|
||||
formData.append("format_output", problem.format_output || "Markdown");
|
||||
|
||||
if (files) {
|
||||
|
||||
@@ -4,92 +4,90 @@
|
||||
|
||||
/* ... */
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
|
||||
--border:220 13% 91%;
|
||||
--input:220 13% 91%;
|
||||
--ring:262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background:224 71.4% 4.1%;
|
||||
--foreground:210 20% 98%;
|
||||
|
||||
--card:224 71.4% 4.1%;
|
||||
--card-foreground:210 20% 98%;
|
||||
|
||||
--popover:224 71.4% 4.1%;
|
||||
--popover-foreground:210 20% 98%;
|
||||
|
||||
--primary:263.4 70% 50.4%;
|
||||
--primary-foreground:210 20% 98%;
|
||||
|
||||
--secondary:215 27.9% 16.9%;
|
||||
--secondary-foreground:210 20% 98%;
|
||||
|
||||
--muted:215 27.9% 16.9%;
|
||||
--muted-foreground:217.9 10.6% 64.9%;
|
||||
|
||||
--accent:215 27.9% 16.9%;
|
||||
--accent-foreground:210 20% 98%;
|
||||
|
||||
--destructive:0 62.8% 30.6%;
|
||||
--destructive-foreground:210 20% 98%;
|
||||
|
||||
--border:215 27.9% 16.9%;
|
||||
--input:215 27.9% 16.9%;
|
||||
--ring:263.4 70% 50.4%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 263.4 70% 50.4%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import NotebookArea from '@/components/NotebookArea.vue'
|
||||
import NotebookArea from "@/components/NotebookArea.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,69 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTaskStore } from '@/stores/task'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useTaskStore } from "@/stores/task";
|
||||
import { computed } from "vue";
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
// ---- Reactive State ----
|
||||
|
||||
// 获取最新的CoordinatorMessage
|
||||
const taskStore = useTaskStore();
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
/** 获取最新的协调者消息 */
|
||||
const latestCoordinatorMessage = computed(() => {
|
||||
const messages = taskStore.coordinatorMessages
|
||||
return messages.length > 0 ? messages[messages.length - 1] : null
|
||||
})
|
||||
const messages = taskStore.coordinatorMessages;
|
||||
return messages.length > 0 ? messages[messages.length - 1] : null;
|
||||
});
|
||||
|
||||
// 解析CoordinatorMessage的JSON内容
|
||||
/** 解析协调者消息的 JSON 内容 */
|
||||
const coordinatorData = computed(() => {
|
||||
if (!latestCoordinatorMessage.value?.content) return null
|
||||
if (!latestCoordinatorMessage.value?.content) return null;
|
||||
|
||||
try {
|
||||
const content = latestCoordinatorMessage.value.content
|
||||
// 移除可能的```json标记
|
||||
const cleanContent = content.replace(/```json\n?/, '').replace(/```$/, '').trim()
|
||||
return JSON.parse(cleanContent)
|
||||
} catch (error) {
|
||||
console.error('解析CoordinatorMessage失败:', error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
try {
|
||||
const content = latestCoordinatorMessage.value.content;
|
||||
// 移除可能的```json标记
|
||||
const cleanContent = content
|
||||
.replace(/```json\n?/, "")
|
||||
.replace(/```$/, "")
|
||||
.trim();
|
||||
return JSON.parse(cleanContent);
|
||||
} catch (error) {
|
||||
console.error("解析CoordinatorMessage失败:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取最新的ModelerMessage
|
||||
/** 获取最新的建模者消息 */
|
||||
const latestModelerMessage = computed(() => {
|
||||
const messages = taskStore.modelerMessages
|
||||
return messages.length > 0 ? messages[messages.length - 1] : null
|
||||
})
|
||||
const messages = taskStore.modelerMessages;
|
||||
return messages.length > 0 ? messages[messages.length - 1] : null;
|
||||
});
|
||||
|
||||
// 解析ModelerMessage的JSON内容
|
||||
/** 解析建模者消息的 JSON 内容 */
|
||||
const modelerData = computed(() => {
|
||||
if (!latestModelerMessage.value?.content) return null
|
||||
if (!latestModelerMessage.value?.content) return null;
|
||||
|
||||
try {
|
||||
const content = latestModelerMessage.value.content
|
||||
// 移除可能的```json标记
|
||||
const cleanContent = content.replace(/```json\n?/, '').replace(/```$/, '').trim()
|
||||
return JSON.parse(cleanContent)
|
||||
} catch (error) {
|
||||
console.error('解析ModelerMessage失败:', error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
try {
|
||||
const content = latestModelerMessage.value.content;
|
||||
// 移除可能的```json标记
|
||||
const cleanContent = content
|
||||
.replace(/```json\n?/, "")
|
||||
.replace(/```$/, "")
|
||||
.trim();
|
||||
return JSON.parse(cleanContent);
|
||||
} catch (error) {
|
||||
console.error("解析ModelerMessage失败:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 生成问题列表
|
||||
/** 从协调者数据中提取问题列表 */
|
||||
const questionsList = computed(() => {
|
||||
if (!coordinatorData.value) return []
|
||||
if (!coordinatorData.value) return [];
|
||||
|
||||
const questions = []
|
||||
for (let i = 1; i <= coordinatorData.value.ques_count; i++) {
|
||||
const quesKey = `ques${i}`
|
||||
if (coordinatorData.value[quesKey]) {
|
||||
questions.push({
|
||||
number: i,
|
||||
content: coordinatorData.value[quesKey]
|
||||
})
|
||||
}
|
||||
}
|
||||
return questions
|
||||
})
|
||||
const questions = [];
|
||||
for (let i = 1; i <= coordinatorData.value.ques_count; i++) {
|
||||
const quesKey = `ques${i}`;
|
||||
if (coordinatorData.value[quesKey]) {
|
||||
questions.push({
|
||||
number: i,
|
||||
content: coordinatorData.value[quesKey],
|
||||
});
|
||||
}
|
||||
}
|
||||
return questions;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,64 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import { renderMarkdown } from '@/utils/markdown';
|
||||
import type { WriterMessage } from '@/utils/response'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { renderMarkdown } from "@/utils/markdown";
|
||||
import type { WriterMessage } from "@/utils/response";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
/** 内容段落数据结构 */
|
||||
interface ContentSection {
|
||||
id: number;
|
||||
content: string;
|
||||
renderedContent: string;
|
||||
sub_title?: string;
|
||||
id: number;
|
||||
content: string;
|
||||
renderedContent: string;
|
||||
sub_title?: string;
|
||||
}
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
const props = defineProps<{
|
||||
messages: WriterMessage[]
|
||||
writerSequence: string[]
|
||||
}>()
|
||||
messages: WriterMessage[];
|
||||
writerSequence: string[];
|
||||
}>();
|
||||
|
||||
// ---- Reactive State ----
|
||||
|
||||
const sections = ref<ContentSection[]>([]);
|
||||
let nextId = 0;
|
||||
|
||||
// 添加新的内容段落
|
||||
// ---- Methods ----
|
||||
|
||||
/** 添加新的内容段落 */
|
||||
const appendContent = async (content: string, sub_title?: string) => {
|
||||
const renderedContent = await renderMarkdown(content);
|
||||
sections.value.push({
|
||||
id: nextId++,
|
||||
content,
|
||||
renderedContent,
|
||||
sub_title
|
||||
});
|
||||
const renderedContent = await renderMarkdown(content);
|
||||
sections.value.push({
|
||||
id: nextId++,
|
||||
content,
|
||||
renderedContent,
|
||||
sub_title,
|
||||
});
|
||||
};
|
||||
|
||||
// 根据 writerSequence 排序内容
|
||||
// ---- Computed ----
|
||||
|
||||
/** 根据 writerSequence 排序内容 */
|
||||
const sortedSections = computed(() => {
|
||||
if (!props.writerSequence.length) return sections.value;
|
||||
if (!props.writerSequence.length) return sections.value;
|
||||
|
||||
return [...sections.value].sort((a, b) => {
|
||||
const aIndex = a.sub_title ? props.writerSequence.indexOf(a.sub_title) : Infinity;
|
||||
const bIndex = b.sub_title ? props.writerSequence.indexOf(b.sub_title) : Infinity;
|
||||
return [...sections.value].sort((a, b) => {
|
||||
const aIndex = a.sub_title
|
||||
? props.writerSequence.indexOf(a.sub_title)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const bIndex = b.sub_title
|
||||
? props.writerSequence.indexOf(b.sub_title)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
if (aIndex === Infinity && bIndex === Infinity) return 0;
|
||||
if (aIndex === Infinity) return 1;
|
||||
if (bIndex === Infinity) return -1;
|
||||
if (
|
||||
aIndex === Number.POSITIVE_INFINITY &&
|
||||
bIndex === Number.POSITIVE_INFINITY
|
||||
)
|
||||
return 0;
|
||||
if (aIndex === Number.POSITIVE_INFINITY) return 1;
|
||||
if (bIndex === Number.POSITIVE_INFINITY) return -1;
|
||||
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
});
|
||||
|
||||
// 监听消息变化
|
||||
watch(() => props.messages, async (messages) => {
|
||||
// 清空现有内容
|
||||
sections.value = [];
|
||||
nextId = 0;
|
||||
// ---- Watch ----
|
||||
|
||||
// 按顺序添加每个消息的内容
|
||||
for (const msg of messages) {
|
||||
if (msg.content) {
|
||||
await appendContent(msg.content, msg.sub_title);
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
/** 监听消息变化,重新渲染内容 */
|
||||
watch(
|
||||
() => props.messages,
|
||||
async (messages) => {
|
||||
// 清空现有内容
|
||||
sections.value = [];
|
||||
nextId = 0;
|
||||
|
||||
// 按顺序添加每个消息的内容
|
||||
for (const msg of messages) {
|
||||
if (msg.content) {
|
||||
await appendContent(msg.content, msg.sub_title);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,83 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { QQ_GROUP, TWITTER, GITHUB_LINK, BILLBILL, XHS, DISCORD } from '@/utils/const'
|
||||
import NavUser from './NavUser.vue'
|
||||
import {
|
||||
BILLBILL,
|
||||
DISCORD,
|
||||
GITHUB_LINK,
|
||||
QQ_GROUP,
|
||||
TWITTER,
|
||||
XHS,
|
||||
} from "@/utils/const";
|
||||
import NavUser from "./NavUser.vue";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
type SidebarProps,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
type SidebarProps,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
const props = defineProps<SidebarProps>()
|
||||
// ---- Props ----
|
||||
|
||||
// This is sample data.
|
||||
const props = defineProps<SidebarProps>();
|
||||
|
||||
// ---- Reactive State ----
|
||||
|
||||
/** 导航菜单数据 */
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: '开始',
|
||||
url: '#',
|
||||
items: [
|
||||
{
|
||||
title: '开始新任务',
|
||||
url: '#',
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '历史任务',
|
||||
url: '#',
|
||||
items: [
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
navMain: [
|
||||
{
|
||||
title: "开始",
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "开始新任务",
|
||||
url: "#",
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "历史任务",
|
||||
url: "#",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const socialMedia = [
|
||||
{
|
||||
name: 'QQ',
|
||||
url: QQ_GROUP,
|
||||
icon: '/qq.svg',
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
url: TWITTER,
|
||||
icon: '/twitter.svg',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: GITHUB_LINK,
|
||||
icon: '/github.svg',
|
||||
},
|
||||
{
|
||||
name: '哔哩哔哩',
|
||||
url: BILLBILL,
|
||||
icon: '/bilibili.svg',
|
||||
},
|
||||
{
|
||||
name: '小红书',
|
||||
url: XHS,
|
||||
icon: '/xiaohongshu.svg',
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
url: DISCORD,
|
||||
icon: '/discord.svg',
|
||||
},
|
||||
]
|
||||
|
||||
{
|
||||
name: "QQ",
|
||||
url: QQ_GROUP,
|
||||
icon: "/qq.svg",
|
||||
},
|
||||
{
|
||||
name: "Twitter",
|
||||
url: TWITTER,
|
||||
icon: "/twitter.svg",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
url: GITHUB_LINK,
|
||||
icon: "/github.svg",
|
||||
},
|
||||
{
|
||||
name: "哔哩哔哩",
|
||||
url: BILLBILL,
|
||||
icon: "/bilibili.svg",
|
||||
},
|
||||
{
|
||||
name: "小红书",
|
||||
url: XHS,
|
||||
icon: "/xiaohongshu.svg",
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
url: DISCORD,
|
||||
icon: "/discord.svg",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { computed } from 'vue'
|
||||
import { AgentType } from '@/utils/enum'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AgentType } from "@/utils/enum";
|
||||
import { marked } from "marked";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
interface BubbleProps {
|
||||
type: 'agent' | 'user'
|
||||
agentType?: AgentType
|
||||
class?: HTMLAttributes['class']
|
||||
content: string
|
||||
type: "agent" | "user";
|
||||
agentType?: AgentType;
|
||||
class?: HTMLAttributes["class"];
|
||||
content: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BubbleProps>(), {
|
||||
type: 'user'
|
||||
})
|
||||
type: "user",
|
||||
});
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
/** 渲染 Markdown 内容 */
|
||||
const renderedContent = computed(() => {
|
||||
return marked.parse(props.content)
|
||||
})
|
||||
return marked.parse(props.content);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import Bubble from './Bubble.vue'
|
||||
import SystemMessage from './SystemMessage.vue'
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Send } from 'lucide-vue-next'
|
||||
import type { Message } from '@/utils/response'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Message } from "@/utils/response";
|
||||
import { Send } from "lucide-vue-next";
|
||||
import { ref } from "vue";
|
||||
import Bubble from "./Bubble.vue";
|
||||
import SystemMessage from "./SystemMessage.vue";
|
||||
|
||||
const props = defineProps<{ messages: Message[] }>()
|
||||
// ---- Props ----
|
||||
|
||||
const inputValue = ref('')
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const scrollRef = ref<HTMLDivElement | null>(null)
|
||||
const props = defineProps<{ messages: Message[] }>();
|
||||
|
||||
// ---- Reactive State ----
|
||||
|
||||
const inputValue = ref("");
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const scrollRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
/** 发送消息(本地处理) */
|
||||
const sendMessage = () => {
|
||||
// 这里只处理本地 user 消息输入,如需和后端交互请在父组件处理
|
||||
if (!inputValue.value.trim()) return
|
||||
// 可以通过 emit 事件让父组件处理 user 消息
|
||||
inputValue.value = ''
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
if (!inputValue.value.trim()) return;
|
||||
inputValue.value = "";
|
||||
inputRef.value?.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ref } from "vue";
|
||||
|
||||
// 控制弹窗显示
|
||||
const showConfirmDialog = ref(false)
|
||||
let resolvePromise: (value: boolean) => void
|
||||
// ---- Reactive State ----
|
||||
|
||||
// 打开确认弹窗(返回Promise)
|
||||
/** 控制弹窗显示 */
|
||||
const showConfirmDialog = ref(false);
|
||||
let resolvePromise: (value: boolean) => void;
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
/** 打开确认弹窗(返回 Promise) */
|
||||
const openConfirmDialog = () => {
|
||||
showConfirmDialog.value = true
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
}
|
||||
showConfirmDialog.value = true;
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// 处理确认操作
|
||||
/** 处理确认操作 */
|
||||
const handleConfirm = () => {
|
||||
showConfirmDialog.value = false
|
||||
resolvePromise(true) // 继续执行
|
||||
}
|
||||
showConfirmDialog.value = false;
|
||||
resolvePromise(true); // 继续执行
|
||||
};
|
||||
|
||||
// 处理取消操作
|
||||
/** 处理取消操作 */
|
||||
const handleCancel = () => {
|
||||
showConfirmDialog.value = false
|
||||
resolvePromise(false) // 返回上传文件
|
||||
}
|
||||
showConfirmDialog.value = false;
|
||||
resolvePromise(false); // 返回上传文件
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({ openConfirmDialog })
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({ openConfirmDialog });
|
||||
</script>
|
||||
<template>
|
||||
<!-- 确认弹窗 -->
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import Tree from '@/components/Tree.vue'
|
||||
import { File } from 'lucide-vue-next'
|
||||
import { useTaskStore } from '@/stores/task'
|
||||
import { Sidebar, SidebarContent, SidebarGroup, SidebarFooter, SidebarProvider } from '@/components/ui/sidebar'
|
||||
const taskStore = useTaskStore()
|
||||
const isLoading = ref(true)
|
||||
// 从消息中提取最新的文件列表
|
||||
const files = taskStore.files as string[]
|
||||
import Tree from "@/components/Tree.vue";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useTaskStore } from "@/stores/task";
|
||||
import { File } from "lucide-vue-next";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
// 将文件列表转换为树形结构
|
||||
// ---- Reactive State ----
|
||||
|
||||
const taskStore = useTaskStore();
|
||||
const isLoading = ref(true);
|
||||
|
||||
/** 从消息中提取最新的文件列表 */
|
||||
const files = taskStore.files as string[];
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
/** 将文件列表转换为树形结构 */
|
||||
const fileTree = computed(() => {
|
||||
// 无论files是否为空,只要计算属性被触发,就认为数据已加载完成
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
return files;
|
||||
});
|
||||
|
||||
// 直接返回文件列表,不做转换,因为Tree组件期望接收string或数组
|
||||
return files
|
||||
})
|
||||
// ---- Lifecycle Hooks ----
|
||||
|
||||
// 添加超时机制,确保即使数据没有加载也会在一定时间后显示内容
|
||||
onMounted(() => {
|
||||
// 3秒后无论如何都取消加载状态
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 3000)
|
||||
})
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
const handleFileClick = (file: string) => {
|
||||
// 处理文件点击
|
||||
console.log('File clicked:', file)
|
||||
}
|
||||
console.log("File clicked:", file);
|
||||
};
|
||||
|
||||
const handleFileDownload = (file: string) => {
|
||||
// 处理文件下载
|
||||
console.log('Download file:', file)
|
||||
}
|
||||
console.log("Download file:", file);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,63 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// 导入图片资源
|
||||
import huashuCupC from '@/assets/example/华数杯2023年C题.png'
|
||||
import wuyiCupC from '@/assets/example/2025五一杯C题.png'
|
||||
import mcmCupC from '@/assets/example/2024高教杯C题.png'
|
||||
import { exampleAPI } from '@/apis/commonApi'
|
||||
import { exampleAPI } from "@/apis/commonApi";
|
||||
import mcmCupC from "@/assets/example/2024高教杯C题.png";
|
||||
import wuyiCupC from "@/assets/example/2025五一杯C题.png";
|
||||
import huashuCupC from "@/assets/example/华数杯2023年C题.png";
|
||||
|
||||
// 定义样例类型
|
||||
// ---- Types ----
|
||||
|
||||
/** 建模样例数据结构 */
|
||||
interface ModelingExample {
|
||||
id: number
|
||||
title: string
|
||||
source: string
|
||||
description: string
|
||||
tags: string[]
|
||||
problemText: string
|
||||
image: string
|
||||
id: number;
|
||||
title: string;
|
||||
source: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
problemText: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// ---- Reactive State ----
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/** 样例列表 */
|
||||
const examples = ref<ModelingExample[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: "母亲身心健康对婴儿成长的影响",
|
||||
source: "2023华数杯C题",
|
||||
description: "研究母亲身心健康对婴儿成长的影响",
|
||||
tags: ["分类问题", "成长", "健康"],
|
||||
problemText: "给定母亲身心健康数据,建立一个预测模型,预测婴儿成长情况。",
|
||||
image: huashuCupC
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "社交媒体平台用户分析问题",
|
||||
source: "2025五一杯C题",
|
||||
description: "分析社交媒体平台用户行为特征",
|
||||
tags: ["社交媒体", "用户行为"],
|
||||
problemText: "分析社交媒体平台用户行为特征,构建用户画像模型。",
|
||||
image: wuyiCupC
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "农作物的种植策略",
|
||||
source: "2024高教杯C题",
|
||||
description: "研究农作物的种植策略",
|
||||
tags: ["种植策略", "农作物", "生长"],
|
||||
problemText: "研究农作物的种植策略,建立一个优化模型,使得农作物产量最大化。",
|
||||
image: mcmCupC
|
||||
}
|
||||
])
|
||||
{
|
||||
id: 1,
|
||||
title: "母亲身心健康对婴儿成长的影响",
|
||||
source: "2023华数杯C题",
|
||||
description: "研究母亲身心健康对婴儿成长的影响",
|
||||
tags: ["分类问题", "成长", "健康"],
|
||||
problemText: "给定母亲身心健康数据,建立一个预测模型,预测婴儿成长情况。",
|
||||
image: huashuCupC,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "社交媒体平台用户分析问题",
|
||||
source: "2025五一杯C题",
|
||||
description: "分析社交媒体平台用户行为特征",
|
||||
tags: ["社交媒体", "用户行为"],
|
||||
problemText: "分析社交媒体平台用户行为特征,构建用户画像模型。",
|
||||
image: wuyiCupC,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "农作物的种植策略",
|
||||
source: "2024高教杯C题",
|
||||
description: "研究农作物的种植策略",
|
||||
tags: ["种植策略", "农作物", "生长"],
|
||||
problemText:
|
||||
"研究农作物的种植策略,建立一个优化模型,使得农作物产量最大化。",
|
||||
image: mcmCupC,
|
||||
},
|
||||
]);
|
||||
|
||||
// 选择样例并跳转到任务创建步骤
|
||||
/** 选择样例并跳转到任务创建步骤 */
|
||||
const selectExample = async (example: ModelingExample) => {
|
||||
const res = await exampleAPI(example.id.toString(), example.source)
|
||||
const task_id = res?.data?.task_id
|
||||
router.push(`/task/${task_id}`)
|
||||
}
|
||||
|
||||
const res = await exampleAPI(example.id.toString(), example.source);
|
||||
const task_id = res?.data?.task_id;
|
||||
router.push(`/task/${task_id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from '@/components/ui/avatar'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import ApiKeyDialog from "@/pages/chat/components/ApiDialog.vue";
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
LogOut,
|
||||
KeyRound,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import ApiKeyDialog from '@/pages/chat/components/ApiDialog.vue'
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
KeyRound,
|
||||
LogOut,
|
||||
} from "lucide-vue-next";
|
||||
import { ref } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: 'San Jin',
|
||||
email: 'mathmodel@mathmodel.com',
|
||||
avatar: 'https://github.com/jihe520.png'
|
||||
})
|
||||
}
|
||||
})
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: "San Jin",
|
||||
email: "mathmodel@mathmodel.com",
|
||||
avatar: "https://github.com/jihe520.png",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { isMobile } = useSidebar()
|
||||
// ---- Reactive State ----
|
||||
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
// API Key 对话框控制
|
||||
const isApiKeyDialogOpen = ref(false)
|
||||
/** API Key 对话框开关状态 */
|
||||
const isApiKeyDialogOpen = ref(false);
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
const openApiKeyDialog = () => {
|
||||
isApiKeyDialogOpen.value = true
|
||||
}
|
||||
|
||||
isApiKeyDialogOpen.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTaskStore } from '@/stores/task'
|
||||
import NotebookCell from '@/components/NotebookCell.vue'
|
||||
import type { NoteCell, CodeCell, ResultCell } from '@/utils/interface'
|
||||
import NotebookCell from "@/components/NotebookCell.vue";
|
||||
import { useTaskStore } from "@/stores/task";
|
||||
import type { CodeCell, NoteCell, ResultCell } from "@/utils/interface";
|
||||
import { computed } from "vue";
|
||||
|
||||
// 使用任务存储
|
||||
const taskStore = useTaskStore()
|
||||
console.log('interpreterMessage:', taskStore.interpreterMessage)
|
||||
// 将代码消息转换为Notebook单元格
|
||||
// ---- Reactive State ----
|
||||
|
||||
const taskStore = useTaskStore();
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
/** 将代码执行消息转换为 Notebook 单元格列表 */
|
||||
const cells = computed<NoteCell[]>(() => {
|
||||
const notebookCells: NoteCell[] = []
|
||||
const notebookCells: NoteCell[] = [];
|
||||
|
||||
// 获取代码执行工具消息,按顺序处理
|
||||
for (const toolMsg of taskStore.interpreterMessage) {
|
||||
console.log('Code execute message:', toolMsg)
|
||||
// 获取代码执行工具消息,按顺序处理
|
||||
for (const toolMsg of taskStore.interpreterMessage) {
|
||||
console.log("Code execute message:", toolMsg);
|
||||
|
||||
// 处理代码输入消息
|
||||
if (toolMsg.input && toolMsg.input.code) {
|
||||
const codeCell: CodeCell = {
|
||||
type: 'code',
|
||||
content: toolMsg.input.code
|
||||
}
|
||||
notebookCells.push(codeCell)
|
||||
}
|
||||
// 处理代码输入消息
|
||||
if (toolMsg.input?.code) {
|
||||
const codeCell: CodeCell = {
|
||||
type: "code",
|
||||
content: toolMsg.input.code,
|
||||
};
|
||||
notebookCells.push(codeCell);
|
||||
}
|
||||
|
||||
// 处理执行结果消息
|
||||
if (toolMsg.output && toolMsg.output.length > 0) {
|
||||
const resultCell: ResultCell = {
|
||||
type: 'result',
|
||||
code_results: toolMsg.output
|
||||
}
|
||||
notebookCells.push(resultCell)
|
||||
}
|
||||
}
|
||||
// 处理执行结果消息
|
||||
if (toolMsg.output && toolMsg.output.length > 0) {
|
||||
const resultCell: ResultCell = {
|
||||
type: "result",
|
||||
code_results: toolMsg.output,
|
||||
};
|
||||
notebookCells.push(resultCell);
|
||||
}
|
||||
}
|
||||
|
||||
return notebookCells
|
||||
})
|
||||
return notebookCells;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,65 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type { CodeExecutionResult } from '@/utils/response'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
import type { NoteCell, CodeCell, ResultCell } from '@/utils/interface'
|
||||
import type { CodeCell, NoteCell, ResultCell } from "@/utils/interface";
|
||||
import { renderMarkdown } from "@/utils/markdown";
|
||||
import type { CodeExecutionResult } from "@/utils/response";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
defineProps<{
|
||||
cell: NoteCell
|
||||
}>()
|
||||
cell: NoteCell;
|
||||
}>();
|
||||
|
||||
// 获取结果格式的CSS类
|
||||
// ---- Methods ----
|
||||
|
||||
/** 获取结果格式对应的 CSS 类 */
|
||||
const getResultClass = (result: CodeExecutionResult) => {
|
||||
switch (result.res_type) {
|
||||
case 'stdout':
|
||||
return 'text-gray-600'
|
||||
case 'stderr':
|
||||
return 'text-orange-600'
|
||||
case 'error':
|
||||
return 'text-red-600'
|
||||
default:
|
||||
return 'text-gray-800'
|
||||
}
|
||||
}
|
||||
switch (result.res_type) {
|
||||
case "stdout":
|
||||
return "text-gray-600";
|
||||
case "stderr":
|
||||
return "text-orange-600";
|
||||
case "error":
|
||||
return "text-red-600";
|
||||
default:
|
||||
return "text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
// 判断结果是否为图片
|
||||
/** 判断结果是否为图片格式 */
|
||||
const isImageResult = (result: CodeExecutionResult) => {
|
||||
return result.res_type === 'result' &&
|
||||
['png', 'jpeg', 'svg'].includes(result.format as string)
|
||||
}
|
||||
return (
|
||||
result.res_type === "result" &&
|
||||
["png", "jpeg", "svg"].includes(result.format as string)
|
||||
);
|
||||
};
|
||||
|
||||
// 判断结果是否为LaTeX
|
||||
/** 判断结果是否为 LaTeX 格式 */
|
||||
const isLatexResult = (result: CodeExecutionResult) => {
|
||||
return result.res_type === 'result' && result.format === 'latex'
|
||||
}
|
||||
return result.res_type === "result" && result.format === "latex";
|
||||
};
|
||||
|
||||
// 判断结果是否为JSON
|
||||
/** 判断结果是否为 JSON 格式 */
|
||||
const isJsonResult = (result: CodeExecutionResult) => {
|
||||
return result.res_type === 'result' && result.format === 'json'
|
||||
}
|
||||
return result.res_type === "result" && result.format === "json";
|
||||
};
|
||||
|
||||
// 格式化JSON显示
|
||||
/** 格式化 JSON 显示 */
|
||||
const formatJson = (jsonString: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch (e) {
|
||||
return jsonString
|
||||
}
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (e) {
|
||||
return jsonString;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染Markdown内容
|
||||
/** 渲染 Markdown 内容 */
|
||||
const renderMarkdownContent = (content: string) => {
|
||||
return renderMarkdown(content)
|
||||
}
|
||||
return renderMarkdown(content);
|
||||
};
|
||||
|
||||
// 类型守卫函数,用于区分单元格类型
|
||||
/** 类型守卫:判断是否为代码单元格 */
|
||||
const isCodeCell = (cell: NoteCell): cell is CodeCell => {
|
||||
return cell.type === 'code'
|
||||
}
|
||||
return cell.type === "code";
|
||||
};
|
||||
|
||||
/** 类型守卫:判断是否为结果单元格 */
|
||||
const isResultCell = (cell: NoteCell): cell is ResultCell => {
|
||||
return cell.type === 'result'
|
||||
}
|
||||
return cell.type === "result";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Search } from "lucide-vue-next";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -16,91 +16,103 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getServiceStatus } from '@/apis/commonApi'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
import { getServiceStatus } from "@/apis/commonApi";
|
||||
import { useToast } from "@/components/ui/toast/use-toast";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
/** 单个服务状态 */
|
||||
interface ServiceStatus {
|
||||
status: 'running' | 'error' | 'unknown'
|
||||
message: string
|
||||
status: "running" | "error" | "unknown";
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 所有服务状态 */
|
||||
interface Services {
|
||||
backend: ServiceStatus
|
||||
redis: ServiceStatus
|
||||
backend: ServiceStatus;
|
||||
redis: ServiceStatus;
|
||||
}
|
||||
|
||||
const { toast } = useToast()
|
||||
// ---- Reactive State ----
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
/** 服务状态数据 */
|
||||
const services = ref<Services>({
|
||||
backend: { status: 'unknown', message: 'Checking...' },
|
||||
redis: { status: 'unknown', message: 'Checking...' }
|
||||
})
|
||||
backend: { status: "unknown", message: "Checking..." },
|
||||
redis: { status: "unknown", message: "Checking..." },
|
||||
});
|
||||
|
||||
let statusInterval: number | null = null
|
||||
let statusInterval: number | null = null;
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
/** 获取状态对应的背景和文字样式 */
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'bg-green-100 text-green-800'
|
||||
case 'error':
|
||||
return 'bg-red-100 text-red-800'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "error":
|
||||
return "bg-red-100 text-red-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取状态指示点的颜色样式 */
|
||||
const getStatusDotClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'bg-green-500'
|
||||
case 'error':
|
||||
return 'bg-red-500'
|
||||
default:
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "bg-green-500";
|
||||
case "error":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-400";
|
||||
}
|
||||
};
|
||||
|
||||
/** 检查服务状态并处理状态变化 */
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const response = await getServiceStatus()
|
||||
const oldStatus = { ...services.value }
|
||||
services.value = response.data as Services
|
||||
try {
|
||||
const response = await getServiceStatus();
|
||||
const oldStatus = { ...services.value };
|
||||
services.value = response.data as Services;
|
||||
|
||||
// 检查是否有服务状态变化为错误
|
||||
Object.keys(response.data).forEach((key) => {
|
||||
const serviceKey = key as keyof Services
|
||||
const newStatus = response.data[serviceKey].status
|
||||
const oldStatusValue = oldStatus[serviceKey].status
|
||||
// 检查是否有服务状态变化为错误
|
||||
for (const key of Object.keys(response.data)) {
|
||||
const serviceKey = key as keyof Services;
|
||||
const newStatus = response.data[serviceKey].status;
|
||||
const oldStatusValue = oldStatus[serviceKey].status;
|
||||
|
||||
if (newStatus === 'error' && oldStatusValue !== 'error') {
|
||||
toast({
|
||||
title: '服务警告',
|
||||
description: `${serviceKey.toUpperCase()} 服务连接失败: ${response.data[serviceKey].message}`,
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to check service status:', error)
|
||||
toast({
|
||||
title: '状态检查失败',
|
||||
description: '无法获取服务状态,请检查网络连接',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (newStatus === "error" && oldStatusValue !== "error") {
|
||||
toast({
|
||||
title: "服务警告",
|
||||
description: `${serviceKey.toUpperCase()} 服务连接失败: ${response.data[serviceKey].message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check service status:", error);
|
||||
toast({
|
||||
title: "状态检查失败",
|
||||
description: "无法获取服务状态,请检查网络连接",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Lifecycle Hooks ----
|
||||
|
||||
onMounted(() => {
|
||||
// 立即检查一次
|
||||
checkStatus()
|
||||
|
||||
// 每30秒检查一次状态
|
||||
statusInterval = setInterval(checkStatus, 30000)
|
||||
})
|
||||
checkStatus();
|
||||
statusInterval = setInterval(checkStatus, 30000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval)
|
||||
}
|
||||
})
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,23 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
interface SystemMessageProps {
|
||||
class?: HTMLAttributes['class']
|
||||
content: string
|
||||
type?: 'info' | 'warning' | 'success' | 'error'
|
||||
class?: HTMLAttributes["class"];
|
||||
content: string;
|
||||
type?: "info" | "warning" | "success" | "error";
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SystemMessageProps>(), {
|
||||
type: 'info'
|
||||
})
|
||||
type: "info",
|
||||
});
|
||||
|
||||
/** 不同消息类型的样式映射 */
|
||||
const typeStyles = {
|
||||
info: 'text-blue-500 dark:text-blue-400 bg-blue-500/5 border-blue-500/10',
|
||||
warning: 'text-yellow-500 dark:text-yellow-400 bg-yellow-500/5 border-yellow-500/10',
|
||||
success: 'text-green-500 dark:text-green-400 bg-green-500/5 border-green-500/10',
|
||||
error: 'text-red-500 dark:text-red-400 bg-red-500/5 border-red-500/10'
|
||||
}
|
||||
info: "text-blue-500 dark:text-blue-400 bg-blue-500/5 border-blue-500/10",
|
||||
warning:
|
||||
"text-yellow-500 dark:text-yellow-400 bg-yellow-500/5 border-yellow-500/10",
|
||||
success:
|
||||
"text-green-500 dark:text-green-400 bg-green-500/5 border-green-500/10",
|
||||
error: "text-red-500 dark:text-red-400 bg-red-500/5 border-red-500/10",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { ChevronRight, File, Folder } from 'lucide-vue-next'
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { ChevronRight, File, Folder } from "lucide-vue-next";
|
||||
const props = defineProps<{
|
||||
item: string | any[]
|
||||
}>()
|
||||
const [name, ...items] = Array.isArray(props.item) ? props.item : [props.item]
|
||||
item: string | unknown[];
|
||||
}>();
|
||||
const [name, ...items] = Array.isArray(props.item) ? props.item : [props.item];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,166 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FileUp } from 'lucide-vue-next'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { saveApiConfig } from "@/apis/apiKeyApi";
|
||||
import { submitModelingTask } from "@/apis/submitModelingApi";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import FileConfirmDialog from './FileConfirmDialog.vue'
|
||||
import { submitModelingTask } from '@/apis/submitModelingApi'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Rocket } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTaskStore } from '@/stores/task'
|
||||
import { useToast } from '@/components/ui/toast'
|
||||
import { useApiKeyStore } from '@/stores/apiKeys'
|
||||
import { saveApiConfig } from '@/apis/apiKeyApi'
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { useApiKeyStore } from "@/stores/apiKeys";
|
||||
import { useTaskStore } from "@/stores/task";
|
||||
import { FileUp } from "lucide-vue-next";
|
||||
import { Rocket } from "lucide-vue-next";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type FileConfirmDialog from "./FileConfirmDialog.vue";
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const { toast } = useToast()
|
||||
const apiKeyStore = useApiKeyStore()
|
||||
const currentStep = ref(1)
|
||||
const fileConfirmDialog = ref<InstanceType<typeof FileConfirmDialog> | null>(null)
|
||||
const fileUploaded = ref(true)
|
||||
// ---- Reactive State ----
|
||||
|
||||
// 表单数据
|
||||
const uploadedFiles = ref<File[]>([])
|
||||
const question = ref('')
|
||||
const taskStore = useTaskStore();
|
||||
const { toast } = useToast();
|
||||
const apiKeyStore = useApiKeyStore();
|
||||
const currentStep = ref(1);
|
||||
const fileConfirmDialog = ref<InstanceType<typeof FileConfirmDialog> | null>(
|
||||
null,
|
||||
);
|
||||
const fileUploaded = ref(true);
|
||||
|
||||
/** 已上传的文件列表 */
|
||||
const uploadedFiles = ref<File[]>([]);
|
||||
|
||||
/** 题目内容 */
|
||||
const question = ref("");
|
||||
|
||||
/** 选项配置 */
|
||||
const selectedOptions = ref({
|
||||
template: '国赛',
|
||||
language: '中文',
|
||||
format: 'Markdown',
|
||||
})
|
||||
template: "国赛",
|
||||
language: "中文",
|
||||
format: "Markdown",
|
||||
});
|
||||
|
||||
/** 选择器配置列表 */
|
||||
const selectConfig = [
|
||||
{
|
||||
key: '模板',
|
||||
label: '选择模板',
|
||||
options: ['国赛', '美赛'],
|
||||
},
|
||||
{
|
||||
key: '语言',
|
||||
label: '选择语言',
|
||||
options: ['中文', '英文'],
|
||||
},
|
||||
{
|
||||
key: '格式',
|
||||
label: '选择格式',
|
||||
options: ['Markdown', 'LaTeX'],
|
||||
},
|
||||
]
|
||||
{
|
||||
key: "模板",
|
||||
label: "选择模板",
|
||||
options: ["国赛", "美赛"],
|
||||
},
|
||||
{
|
||||
key: "语言",
|
||||
label: "选择语言",
|
||||
options: ["中文", "英文"],
|
||||
},
|
||||
{
|
||||
key: "格式",
|
||||
label: "选择格式",
|
||||
options: ["Markdown", "LaTeX"],
|
||||
},
|
||||
];
|
||||
|
||||
// 添加状态控制
|
||||
const showUploadSuccess = ref(false)
|
||||
/** 上传成功提示显示状态 */
|
||||
const showUploadSuccess = ref(false);
|
||||
|
||||
// 提交任务
|
||||
const showSubmitSuccess = ref(false)
|
||||
/** 提交成功提示显示状态 */
|
||||
const showSubmitSuccess = ref(false);
|
||||
|
||||
const taskId = ref<string | null>(null)
|
||||
/** 任务ID */
|
||||
const taskId = ref<string | null>(null);
|
||||
|
||||
// 添加 fileInput 的类型声明
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
/** 文件输入元素引用 */
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 2)
|
||||
currentStep.value++
|
||||
}
|
||||
if (currentStep.value < 2) currentStep.value++;
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 1)
|
||||
currentStep.value--
|
||||
}
|
||||
if (currentStep.value > 1) currentStep.value--;
|
||||
};
|
||||
|
||||
// 修改文件上传处理
|
||||
/** 处理文件上传事件 */
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files.length > 0) {
|
||||
uploadedFiles.value = Array.from(input.files)
|
||||
fileUploaded.value = true
|
||||
showUploadSuccess.value = true // 显示提示
|
||||
setTimeout(() => {
|
||||
showUploadSuccess.value = false // 3秒后自动隐藏
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
uploadedFiles.value = Array.from(input.files);
|
||||
fileUploaded.value = true;
|
||||
showUploadSuccess.value = true; // 显示提示
|
||||
setTimeout(() => {
|
||||
showUploadSuccess.value = false; // 3秒后自动隐藏
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/** 提交建模任务 */
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
try {
|
||||
if (apiKeyStore.isEmpty) {
|
||||
toast({
|
||||
title: "请先配置 API Key",
|
||||
description: "在侧边栏 -> 头像 -> API Key 中配置 API Key",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiKeyStore.isEmpty) {
|
||||
toast({
|
||||
title: '请先配置 API Key',
|
||||
description: '在侧边栏 -> 头像 -> API Key 中配置 API Key',
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
}
|
||||
// 保存 API Key
|
||||
await saveApiConfig({
|
||||
coordinator: apiKeyStore.coordinatorConfig,
|
||||
modeler: apiKeyStore.modelerConfig,
|
||||
coder: apiKeyStore.coderConfig,
|
||||
writer: apiKeyStore.writerConfig,
|
||||
openalex_email: apiKeyStore.openalexEmail,
|
||||
});
|
||||
|
||||
// 保存 API Key
|
||||
await saveApiConfig({
|
||||
coordinator: apiKeyStore.coordinatorConfig,
|
||||
modeler: apiKeyStore.modelerConfig,
|
||||
coder: apiKeyStore.coderConfig,
|
||||
writer: apiKeyStore.writerConfig,
|
||||
openalex_email: apiKeyStore.openalexEmail
|
||||
})
|
||||
if (uploadedFiles.value.length === 0) {
|
||||
if (!fileConfirmDialog.value) return;
|
||||
|
||||
if (uploadedFiles.value.length === 0) {
|
||||
if (!fileConfirmDialog.value) return
|
||||
|
||||
const shouldContinue = await fileConfirmDialog.value.openConfirmDialog()
|
||||
|
||||
if (!shouldContinue) {
|
||||
toast({
|
||||
title: '请先上传文件',
|
||||
description: '请先上传文件',
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
console.log(selectedOptions.value)
|
||||
console.log(question.value)
|
||||
console.log(uploadedFiles.value)
|
||||
const response = await submitModelingTask(
|
||||
{
|
||||
ques_all: question.value,
|
||||
comp_template: selectedOptions.value.template,
|
||||
format_output: selectedOptions.value.format
|
||||
},
|
||||
uploadedFiles.value
|
||||
)
|
||||
const shouldContinue = await fileConfirmDialog.value.openConfirmDialog();
|
||||
|
||||
taskId.value = response?.data?.task_id ?? null
|
||||
taskStore.addUserMessage(question.value)
|
||||
if (!shouldContinue) {
|
||||
toast({
|
||||
title: "请先上传文件",
|
||||
description: "请先上传文件",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(selectedOptions.value);
|
||||
console.log(question.value);
|
||||
console.log(uploadedFiles.value);
|
||||
const response = await submitModelingTask(
|
||||
{
|
||||
ques_all: question.value,
|
||||
comp_template: selectedOptions.value.template,
|
||||
format_output: selectedOptions.value.format,
|
||||
},
|
||||
uploadedFiles.value,
|
||||
);
|
||||
|
||||
showSubmitSuccess.value = true
|
||||
setTimeout(() => {
|
||||
showSubmitSuccess.value = false // 3秒后自动隐藏
|
||||
}, 3000)
|
||||
router.push(`/task/${taskId.value}`)
|
||||
toast({
|
||||
title: '任务提交成功',
|
||||
description: '任务提交成功,编号为:' + taskId.value,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('任务提交失败:', error)
|
||||
toast({
|
||||
title: '任务提交失败',
|
||||
description: '请检查 API Key 是否正确',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
taskId.value = response?.data?.task_id ?? null;
|
||||
taskStore.addUserMessage(question.value);
|
||||
|
||||
showSubmitSuccess.value = true;
|
||||
setTimeout(() => {
|
||||
showSubmitSuccess.value = false; // 3秒后自动隐藏
|
||||
}, 3000);
|
||||
router.push(`/task/${taskId.value}`);
|
||||
toast({
|
||||
title: "任务提交成功",
|
||||
description: `任务提交成功,编号为:${taskId.value}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("任务提交失败:", error);
|
||||
toast({
|
||||
title: "任务提交失败",
|
||||
description: "请检查 API Key 是否正确",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Check, ChevronsUpDown, GalleryVerticalEnd } from 'lucide-vue-next'
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Check, ChevronsUpDown, GalleryVerticalEnd } from "lucide-vue-next";
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { ref } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
const props = defineProps<{
|
||||
versions: string[]
|
||||
defaultVersion: string
|
||||
}>()
|
||||
versions: string[];
|
||||
defaultVersion: string;
|
||||
}>();
|
||||
|
||||
const selectedVersion = ref(props.defaultVersion)
|
||||
// ---- Reactive State ----
|
||||
|
||||
const selectedVersion = ref(props.defaultVersion);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type AlertVariants, alertVariants } from '.'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { type AlertVariants, alertVariants } from ".";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: AlertVariants['variant']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
variant?: AlertVariants["variant"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
export { default as Alert } from './Alert.vue'
|
||||
export { default as AlertDescription } from './AlertDescription.vue'
|
||||
export { default as AlertTitle } from './AlertTitle.vue'
|
||||
export { default as Alert } from "./Alert.vue";
|
||||
export { default as AlertDescription } from "./AlertDescription.vue";
|
||||
export { default as AlertTitle } from "./AlertTitle.vue";
|
||||
|
||||
export const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AlertVariants = VariantProps<typeof alertVariants>
|
||||
export type AlertVariants = VariantProps<typeof alertVariants>;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AvatarRoot } from 'reka-ui'
|
||||
import { avatarVariant, type AvatarVariants } from '.'
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AvatarRoot } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { type AvatarVariants, avatarVariant } from ".";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
size?: AvatarVariants['size']
|
||||
shape?: AvatarVariants['shape']
|
||||
}>(), {
|
||||
size: 'sm',
|
||||
shape: 'circle',
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
size?: AvatarVariants["size"];
|
||||
shape?: AvatarVariants["shape"];
|
||||
}>(),
|
||||
{
|
||||
size: "sm",
|
||||
shape: "circle",
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarFallback, type AvatarFallbackProps } from 'reka-ui'
|
||||
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>()
|
||||
const props = defineProps<AvatarFallbackProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarImageProps } from 'reka-ui'
|
||||
import { AvatarImage } from 'reka-ui'
|
||||
import type { AvatarImageProps } from "reka-ui";
|
||||
import { AvatarImage } from "reka-ui";
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
const props = defineProps<AvatarImageProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
export { default as Avatar } from './Avatar.vue'
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||
export { default as AvatarImage } from './AvatarImage.vue'
|
||||
export { default as Avatar } from "./Avatar.vue";
|
||||
export { default as AvatarFallback } from "./AvatarFallback.vue";
|
||||
export { default as AvatarImage } from "./AvatarImage.vue";
|
||||
|
||||
export const avatarVariant = cva(
|
||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-10 w-10 text-xs',
|
||||
base: 'h-16 w-16 text-2xl',
|
||||
lg: 'h-32 w-32 text-5xl',
|
||||
},
|
||||
shape: {
|
||||
circle: 'rounded-full',
|
||||
square: 'rounded-md',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-10 w-10 text-xs",
|
||||
base: "h-16 w-16 text-2xl",
|
||||
lg: "h-32 w-32 text-5xl",
|
||||
},
|
||||
shape: {
|
||||
circle: "rounded-full",
|
||||
square: "rounded-md",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
||||
export type AvatarVariants = VariantProps<typeof avatarVariant>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MoreHorizontal } from "lucide-vue-next";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Primitive, type PrimitiveProps } from 'reka-ui'
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
as: 'a',
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
as: "a",
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { default as Breadcrumb } from './Breadcrumb.vue'
|
||||
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
|
||||
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
|
||||
export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
|
||||
export { default as BreadcrumbList } from './BreadcrumbList.vue'
|
||||
export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
|
||||
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'
|
||||
export { default as Breadcrumb } from "./Breadcrumb.vue";
|
||||
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue";
|
||||
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
|
||||
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
|
||||
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
|
||||
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue";
|
||||
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Primitive, type PrimitiveProps } from 'reka-ui'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { type ButtonVariants, buttonVariants } from ".";
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: ButtonVariants["variant"];
|
||||
size?: ButtonVariants["size"];
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button',
|
||||
})
|
||||
as: "button",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
export { default as Button } from "./Button.vue";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
xs: 'h-7 rounded px-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
xs: "h-7 rounded px-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { default as Card } from './Card.vue'
|
||||
export { default as CardContent } from './CardContent.vue'
|
||||
export { default as CardDescription } from './CardDescription.vue'
|
||||
export { default as CardFooter } from './CardFooter.vue'
|
||||
export { default as CardHeader } from './CardHeader.vue'
|
||||
export { default as CardTitle } from './CardTitle.vue'
|
||||
export { default as Card } from "./Card.vue";
|
||||
export { default as CardContent } from "./CardContent.vue";
|
||||
export { default as CardDescription } from "./CardDescription.vue";
|
||||
export { default as CardFooter } from "./CardFooter.vue";
|
||||
export { default as CardHeader } from "./CardHeader.vue";
|
||||
export { default as CardTitle } from "./CardTitle.vue";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps<CollapsibleRootProps>()
|
||||
const emits = defineEmits<CollapsibleRootEmits>()
|
||||
const props = defineProps<CollapsibleRootProps>();
|
||||
const emits = defineEmits<CollapsibleRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user