Skip to content

Commit 219dc6a

Browse files
authored
Merge pull request #25 from jihe520/docker
feat: apikeydialog
2 parents 9d219ce + 1921f9d commit 219dc6a

33 files changed

+1120
-57
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# name: Build and Push Docker Image
2+
3+
# on:
4+
# push:
5+
# branches:
6+
# - main
7+
# paths:
8+
# - 'backend/**'
9+
# - 'frontend/**'
10+
# - 'docker-compose.yml'
11+
# - '.github/workflows/docker-build-push.yml'
12+
# pull_request:
13+
# branches:
14+
# - main
15+
# paths:
16+
# - 'backend/**'
17+
# - 'frontend/**'
18+
# - 'docker-compose.yml'
19+
# - '.github/workflows/docker-build-push.yml'
20+
21+
# env:
22+
# REGISTRY: docker.io
23+
# BACKEND_IMAGE_NAME: mathmodelagent-backend
24+
# FRONTEND_IMAGE_NAME: mathmodelagent-frontend
25+
26+
# jobs:
27+
# build-backend:
28+
# runs-on: ubuntu-latest
29+
# outputs:
30+
# image-tag: ${{ steps.meta.outputs.tags }}
31+
# image-digest: ${{ steps.build.outputs.digest }}
32+
33+
# steps:
34+
# - name: Checkout code
35+
# uses: actions/checkout@v4
36+
37+
# - name: Set up Docker Buildx
38+
# uses: docker/setup-buildx-action@v3
39+
40+
# - name: Log in to Docker Hub
41+
# if: github.event_name != 'pull_request'
42+
# uses: docker/login-action@v3
43+
# with:
44+
# registry: ${{ env.REGISTRY }}
45+
# username: ${{ secrets.DOCKER_USERNAME }}
46+
# password: ${{ secrets.DOCKER_PASSWORD }}
47+
48+
# - name: Extract metadata for backend
49+
# id: meta
50+
# uses: docker/metadata-action@v5
51+
# with:
52+
# images: ${{ env.REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/${{ env.BACKEND_IMAGE_NAME }}
53+
# tags: |
54+
# type=ref,event=branch
55+
# type=ref,event=pr
56+
# type=sha,prefix={{branch}}-
57+
# type=raw,value=latest,enable={{is_default_branch}}
58+
59+
# - name: Build and push backend Docker image
60+
# id: build
61+
# uses: docker/build-push-action@v5
62+
# with:
63+
# context: ./backend
64+
# file: ./backend/Dockerfile
65+
# push: ${{ github.event_name != 'pull_request' }}
66+
# tags: ${{ steps.meta.outputs.tags }}
67+
# labels: ${{ steps.meta.outputs.labels }}
68+
# cache-from: type=gha
69+
# cache-to: type=gha,mode=max
70+
# platforms: linux/amd64,linux/arm64
71+
72+
# build-frontend:
73+
# runs-on: ubuntu-latest
74+
# outputs:
75+
# image-tag: ${{ steps.meta.outputs.tags }}
76+
# image-digest: ${{ steps.build.outputs.digest }}
77+
78+
# steps:
79+
# - name: Checkout code
80+
# uses: actions/checkout@v4
81+
82+
# - name: Set up Docker Buildx
83+
# uses: docker/setup-buildx-action@v3
84+
85+
# - name: Log in to Docker Hub
86+
# if: github.event_name != 'pull_request'
87+
# uses: docker/login-action@v3
88+
# with:
89+
# registry: ${{ env.REGISTRY }}
90+
# username: ${{ secrets.DOCKER_USERNAME }}
91+
# password: ${{ secrets.DOCKER_PASSWORD }}
92+
93+
# - name: Extract metadata for frontend
94+
# id: meta
95+
# uses: docker/metadata-action@v5
96+
# with:
97+
# images: ${{ env.REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/${{ env.FRONTEND_IMAGE_NAME }}
98+
# tags: |
99+
# type=ref,event=branch
100+
# type=ref,event=pr
101+
# type=sha,prefix={{branch}}-
102+
# type=raw,value=latest,enable={{is_default_branch}}
103+
104+
# - name: Build and push frontend Docker image
105+
# id: build
106+
# uses: docker/build-push-action@v5
107+
# with:
108+
# context: ./frontend
109+
# file: ./frontend/Dockerfile
110+
# push: ${{ github.event_name != 'pull_request' }}
111+
# tags: ${{ steps.meta.outputs.tags }}
112+
# labels: ${{ steps.meta.outputs.labels }}
113+
# cache-from: type=gha
114+
# cache-to: type=gha,mode=max
115+
# platforms: linux/amd64,linux/arm64
116+
117+
# security-scan:
118+
# runs-on: ubuntu-latest
119+
# needs: [build-backend, build-frontend]
120+
# if: github.event_name != 'pull_request'
121+
122+
# strategy:
123+
# matrix:
124+
# component: [backend, frontend]
125+
126+
# steps:
127+
# - name: Run Trivy vulnerability scanner
128+
# uses: aquasecurity/trivy-action@master
129+
# with:
130+
# image-ref: ${{ needs[format('build-{0}', matrix.component)].outputs.image-tag }}
131+
# format: 'sarif'
132+
# output: 'trivy-results-${{ matrix.component }}.sarif'
133+
134+
# - name: Upload Trivy scan results to GitHub Security tab
135+
# uses: github/codeql-action/upload-sarif@v3
136+
# if: always()
137+
# with:
138+
# sarif_file: 'trivy-results-${{ matrix.component }}.sarif'

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ docker-compose up -d
135135

136136
启动后端
137137

138-
*启动 redis*
138+
> [!CAUTION]
139+
> 启动 Redis, 下载和运行问 AI
139140
140141
```bash
141142
cd backend # 切换到 backend 目录下

backend/.env.dev.example

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ WRITER_API_KEY=
2020
WRITER_MODEL=
2121
# WRITER_BASE_URL=
2222

23-
DEFAULT_API_KEY=
24-
DEFAULT_MODEL=
25-
# DEFAULT_BASE_URL=
26-
2723
# 模型最大问答次数
2824
MAX_CHAT_TURNS=60
2925
# 思考反思次数
@@ -39,6 +35,7 @@ LOG_LEVEL=DEBUG
3935
DEBUG=true
4036
# 确保安装 Redis
4137
# 如果是docker: REDIS_URL=redis://redis:6379/0
38+
# 本地部署 : redis://localhost:6379/0
4239
REDIS_URL=redis://localhost:6379/0
4340
REDIS_MAX_CONNECTIONS=20
4441
CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost:3000

backend/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ RUN --mount=type=cache,target=/root/.cache/uv \
2222

2323
EXPOSE 8000
2424

25+
# 直接使用 uvicorn,因为依赖已安装到系统 Python
2526
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--ws-ping-interval", "60", "--ws-ping-timeout", "120"]

backend/app/config/setting.py

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,31 @@ def parse_cors(value: str) -> list[str]:
1818
class Settings(BaseSettings):
1919
ENV: str
2020

21-
COORDINATOR_API_KEY: str
22-
COORDINATOR_MODEL: str
21+
COORDINATOR_API_KEY: Optional[str] = None
22+
COORDINATOR_MODEL: Optional[str] = None
2323
COORDINATOR_BASE_URL: Optional[str] = None
2424

25-
MODELER_API_KEY: str
26-
MODELER_MODEL: str
25+
MODELER_API_KEY: Optional[str] = None
26+
MODELER_MODEL: Optional[str] = None
2727
MODELER_BASE_URL: Optional[str] = None
2828

29-
CODER_API_KEY: str
30-
CODER_MODEL: str
29+
CODER_API_KEY: Optional[str] = None
30+
CODER_MODEL: Optional[str] = None
3131
CODER_BASE_URL: Optional[str] = None
3232

33-
WRITER_API_KEY: str
34-
WRITER_MODEL: str
33+
WRITER_API_KEY: Optional[str] = None
34+
WRITER_MODEL: Optional[str] = None
3535
WRITER_BASE_URL: Optional[str] = None
3636

37-
DEFAULT_API_KEY: str
38-
DEFAULT_MODEL: str
39-
DEFAULT_BASE_URL: Optional[str] = None
40-
41-
MAX_CHAT_TURNS: int
42-
MAX_RETRIES: int
37+
MAX_CHAT_TURNS: int = 60
38+
MAX_RETRIES: int = 5
4339
E2B_API_KEY: Optional[str] = None
44-
LOG_LEVEL: str
45-
DEBUG: bool
46-
REDIS_URL: str
47-
REDIS_MAX_CONNECTIONS: int
48-
CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)]
49-
SERVER_HOST: str = "http://localhost:8000" # 默认值
40+
LOG_LEVEL: str = "DEBUG"
41+
DEBUG: bool = True
42+
REDIS_URL: str = "redis://redis:6379/0"
43+
REDIS_MAX_CONNECTIONS: int = 10
44+
CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)] = "*"
45+
SERVER_HOST: str = "http://localhost:8000"
5046
OPENALEX_EMAIL: Optional[str] = None
5147

5248
model_config = SettingsConfigDict(
@@ -55,13 +51,6 @@ class Settings(BaseSettings):
5551
extra="allow",
5652
)
5753

58-
def get_deepseek_config(self) -> dict:
59-
return {
60-
"api_key": self.DEEPSEEK_API_KEY,
61-
"model": self.DEEPSEEK_MODEL,
62-
"base_url": self.DEEPSEEK_BASE_URL,
63-
}
64-
6554
@classmethod
6655
def from_env(cls, env: str = None):
6756
env = env or os.getenv("ENV", "dev")

backend/app/core/agents/coordinator_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ async def run(self, ques_all: str) -> CoordinatorToModeler:
3030
)
3131
json_str = response.choices[0].message.content
3232

33-
if not json_str.startswith("```json"):
34-
logger.info(f"拒绝回答用户非数学建模请求:{json_str}")
35-
raise ValueError(f"拒绝回答用户非数学建模请求:{json_str}")
33+
# if not json_str.startswith("```json"):
34+
# logger.info(f"拒绝回答用户非数学建模请求:{json_str}")
35+
# raise ValueError(f"拒绝回答用户非数学建模请求:{json_str}")
3636

3737
# 清理 JSON 字符串
3838
json_str = json_str.replace("```json", "").replace("```", "").strip()

backend/app/core/prompts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
CODER_PROMPT = f"""
6161
You are an AI code interpreter specializing in data analysis with Python. Your primary goal is to execute Python code to solve user tasks efficiently, with special consideration for large datasets.
6262
63+
中文回复
64+
6365
**Environment**: {platform.system()}
6466
**Key Skills**: pandas, numpy, seaborn, matplotlib, scikit-learn, xgboost, scipy
6567
**Data Visualization Style**: Nature/Science publication quality
@@ -139,6 +141,8 @@ def get_writer_prompt(
139141
# Role Definition
140142
Professional writer for mathematical modeling competitions with expertise in technical documentation and literature synthesis
141143
144+
中文回复
145+
142146
# Core Tasks
143147
1. Compose competition papers using provided problem statements and solution content
144148
2. Strictly adhere to {format_output} formatting templates

backend/app/routers/modeling_router.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,114 @@
1616
from fastapi import HTTPException
1717
from icecream import ic
1818
from app.schemas.request import ExampleRequest
19+
from pydantic import BaseModel
20+
import litellm
21+
from app.config.setting import settings
1922

2023
router = APIRouter()
2124

2225

26+
class ValidateApiKeyRequest(BaseModel):
27+
api_key: str
28+
base_url: str = "https://api.openai.com/v1"
29+
model_id: str
30+
31+
32+
class ValidateApiKeyResponse(BaseModel):
33+
valid: bool
34+
message: str
35+
36+
37+
class SaveApiConfigRequest(BaseModel):
38+
coordinator: dict
39+
modeler: dict
40+
coder: dict
41+
writer: dict
42+
43+
44+
@router.post("/save-api-config")
45+
async def save_api_config(request: SaveApiConfigRequest):
46+
"""
47+
保存验证成功的 API 配置到 settings
48+
"""
49+
try:
50+
# 更新各个模块的设置
51+
if request.coordinator:
52+
settings.COORDINATOR_API_KEY = request.coordinator.get('apiKey', '')
53+
settings.COORDINATOR_MODEL = request.coordinator.get('modelId', '')
54+
settings.COORDINATOR_BASE_URL = request.coordinator.get('baseUrl', '')
55+
56+
if request.modeler:
57+
settings.MODELER_API_KEY = request.modeler.get('apiKey', '')
58+
settings.MODELER_MODEL = request.modeler.get('modelId', '')
59+
settings.MODELER_BASE_URL = request.modeler.get('baseUrl', '')
60+
61+
if request.coder:
62+
settings.CODER_API_KEY = request.coder.get('apiKey', '')
63+
settings.CODER_MODEL = request.coder.get('modelId', '')
64+
settings.CODER_BASE_URL = request.coder.get('baseUrl', '')
65+
66+
if request.writer:
67+
settings.WRITER_API_KEY = request.writer.get('apiKey', '')
68+
settings.WRITER_MODEL = request.writer.get('modelId', '')
69+
settings.WRITER_BASE_URL = request.writer.get('baseUrl', '')
70+
71+
return {"success": True, "message": "配置保存成功"}
72+
except Exception as e:
73+
logger.error(f"保存配置失败: {str(e)}")
74+
raise HTTPException(status_code=500, detail=f"保存配置失败: {str(e)}")
75+
76+
77+
@router.post("/validate-api-key", response_model=ValidateApiKeyResponse)
78+
async def validate_api_key(request: ValidateApiKeyRequest):
79+
"""
80+
验证 API Key 的有效性
81+
"""
82+
try:
83+
# 使用 litellm 发送测试请求
84+
await litellm.acompletion(
85+
model=request.model_id,
86+
messages=[{"role": "user", "content": "Hi"}],
87+
max_tokens=1,
88+
api_key=request.api_key,
89+
base_url=request.base_url if request.base_url != "https://api.openai.com/v1" else None,
90+
)
91+
92+
return ValidateApiKeyResponse(
93+
valid=True,
94+
message="✓ 模型 API 验证成功"
95+
)
96+
except Exception as e:
97+
error_msg = str(e)
98+
99+
# 解析不同类型的错误
100+
if "401" in error_msg or "Unauthorized" in error_msg:
101+
return ValidateApiKeyResponse(
102+
valid=False,
103+
message="✗ API Key 无效或已过期"
104+
)
105+
elif "404" in error_msg or "Not Found" in error_msg:
106+
return ValidateApiKeyResponse(
107+
valid=False,
108+
message="✗ 模型 ID 不存在或 Base URL 错误"
109+
)
110+
elif "429" in error_msg or "rate limit" in error_msg.lower():
111+
return ValidateApiKeyResponse(
112+
valid=False,
113+
message="✗ 请求过于频繁,请稍后再试"
114+
)
115+
elif "403" in error_msg or "Forbidden" in error_msg:
116+
return ValidateApiKeyResponse(
117+
valid=False,
118+
message="✗ API 权限不足或账户余额不足"
119+
)
120+
else:
121+
return ValidateApiKeyResponse(
122+
valid=False,
123+
message=f"✗ 验证失败: {error_msg[:50]}..."
124+
)
125+
126+
23127
@router.post("/example")
24128
async def exampleModeling(
25129
example_request: ExampleRequest,

0 commit comments

Comments
 (0)