Files
012-kaopeilian/backend/app/api/v1/admin_portal/prompts.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

638 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI 提示词管理 API
"""
import os
import json
from typing import Optional, List
import pymysql
from fastapi import APIRouter, Depends, HTTPException, status, Query
from .auth import get_current_admin, require_superadmin, get_db_connection, AdminUserInfo
from .schemas import (
AIPromptCreate,
AIPromptUpdate,
AIPromptResponse,
AIPromptVersionResponse,
TenantPromptResponse,
TenantPromptUpdate,
ResponseModel,
)
router = APIRouter(prefix="/prompts", tags=["提示词管理"])
def log_operation(cursor, admin: AdminUserInfo, tenant_id: int, tenant_code: str,
operation_type: str, resource_type: str, resource_id: int,
resource_name: str, old_value: dict = None, new_value: dict = None):
"""记录操作日志"""
cursor.execute(
"""
INSERT INTO operation_logs
(admin_user_id, admin_username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name, old_value, new_value)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(admin.id, admin.username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name,
json.dumps(old_value, ensure_ascii=False) if old_value else None,
json.dumps(new_value, ensure_ascii=False) if new_value else None)
)
@router.get("", response_model=List[AIPromptResponse], summary="获取提示词列表")
async def list_prompts(
module: Optional[str] = Query(None, description="模块筛选"),
is_active: Optional[bool] = Query(None, description="是否启用"),
admin: AdminUserInfo = Depends(get_current_admin),
):
"""
获取所有 AI 提示词模板
- **module**: 模块筛选course, exam, practice, ability
- **is_active**: 是否启用
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
conditions = []
params = []
if module:
conditions.append("module = %s")
params.append(module)
if is_active is not None:
conditions.append("is_active = %s")
params.append(is_active)
where_clause = " AND ".join(conditions) if conditions else "1=1"
cursor.execute(
f"""
SELECT * FROM ai_prompts
WHERE {where_clause}
ORDER BY module, id
""",
params
)
rows = cursor.fetchall()
result = []
for row in rows:
# 解析 JSON 字段
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
output_schema = None
if row.get("output_schema"):
try:
output_schema = json.loads(row["output_schema"])
except:
pass
result.append(AIPromptResponse(
id=row["id"],
code=row["code"],
name=row["name"],
description=row["description"],
module=row["module"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
output_schema=output_schema,
model_recommendation=row["model_recommendation"],
max_tokens=row["max_tokens"],
temperature=float(row["temperature"]) if row["temperature"] else 0.7,
is_system=row["is_system"],
is_active=row["is_active"],
version=row["version"],
created_at=row["created_at"],
updated_at=row["updated_at"],
))
return result
finally:
conn.close()
@router.get("/{prompt_id}", response_model=AIPromptResponse, summary="获取提示词详情")
async def get_prompt(
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取提示词详情"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
row = cursor.fetchone()
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 解析 JSON 字段
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
output_schema = None
if row.get("output_schema"):
try:
output_schema = json.loads(row["output_schema"])
except:
pass
return AIPromptResponse(
id=row["id"],
code=row["code"],
name=row["name"],
description=row["description"],
module=row["module"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
output_schema=output_schema,
model_recommendation=row["model_recommendation"],
max_tokens=row["max_tokens"],
temperature=float(row["temperature"]) if row["temperature"] else 0.7,
is_system=row["is_system"],
is_active=row["is_active"],
version=row["version"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
finally:
conn.close()
@router.post("", response_model=AIPromptResponse, summary="创建提示词")
async def create_prompt(
data: AIPromptCreate,
admin: AdminUserInfo = Depends(require_superadmin),
):
"""
创建新的提示词模板
需要超级管理员权限
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 检查编码是否已存在
cursor.execute("SELECT id FROM ai_prompts WHERE code = %s", (data.code,))
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="提示词编码已存在",
)
# 创建提示词
cursor.execute(
"""
INSERT INTO ai_prompts
(code, name, description, module, system_prompt, user_prompt_template,
variables, output_schema, model_recommendation, max_tokens, temperature,
is_system, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, FALSE, %s)
""",
(data.code, data.name, data.description, data.module,
data.system_prompt, data.user_prompt_template,
json.dumps(data.variables) if data.variables else None,
json.dumps(data.output_schema) if data.output_schema else None,
data.model_recommendation, data.max_tokens, data.temperature,
admin.id)
)
prompt_id = cursor.lastrowid
# 记录操作日志
log_operation(
cursor, admin, None, None,
"create", "prompt", prompt_id, data.name,
new_value=data.model_dump()
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.put("/{prompt_id}", response_model=AIPromptResponse, summary="更新提示词")
async def update_prompt(
prompt_id: int,
data: AIPromptUpdate,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""
更新提示词模板
更新会自动保存版本历史
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取原提示词
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
old_prompt = cursor.fetchone()
if not old_prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 保存版本历史(如果系统提示词或用户提示词有变化)
if data.system_prompt or data.user_prompt_template:
new_version = old_prompt["version"] + 1
cursor.execute(
"""
INSERT INTO ai_prompt_versions
(prompt_id, version, system_prompt, user_prompt_template, variables,
output_schema, change_summary, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""",
(prompt_id, old_prompt["version"],
old_prompt["system_prompt"], old_prompt["user_prompt_template"],
old_prompt["variables"], old_prompt["output_schema"],
f"版本 {old_prompt['version']} 备份",
admin.id)
)
else:
new_version = old_prompt["version"]
# 构建更新语句
update_fields = []
update_values = []
if data.name is not None:
update_fields.append("name = %s")
update_values.append(data.name)
if data.description is not None:
update_fields.append("description = %s")
update_values.append(data.description)
if data.system_prompt is not None:
update_fields.append("system_prompt = %s")
update_values.append(data.system_prompt)
if data.user_prompt_template is not None:
update_fields.append("user_prompt_template = %s")
update_values.append(data.user_prompt_template)
if data.variables is not None:
update_fields.append("variables = %s")
update_values.append(json.dumps(data.variables))
if data.output_schema is not None:
update_fields.append("output_schema = %s")
update_values.append(json.dumps(data.output_schema))
if data.model_recommendation is not None:
update_fields.append("model_recommendation = %s")
update_values.append(data.model_recommendation)
if data.max_tokens is not None:
update_fields.append("max_tokens = %s")
update_values.append(data.max_tokens)
if data.temperature is not None:
update_fields.append("temperature = %s")
update_values.append(data.temperature)
if data.is_active is not None:
update_fields.append("is_active = %s")
update_values.append(data.is_active)
if not update_fields:
return await get_prompt(prompt_id, admin)
# 更新版本号
if data.system_prompt or data.user_prompt_template:
update_fields.append("version = %s")
update_values.append(new_version)
update_fields.append("updated_by = %s")
update_values.append(admin.id)
update_values.append(prompt_id)
cursor.execute(
f"UPDATE ai_prompts SET {', '.join(update_fields)} WHERE id = %s",
update_values
)
# 记录操作日志
log_operation(
cursor, admin, None, None,
"update", "prompt", prompt_id, old_prompt["name"],
old_value={"version": old_prompt["version"]},
new_value=data.model_dump(exclude_unset=True)
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.get("/{prompt_id}/versions", response_model=List[AIPromptVersionResponse], summary="获取提示词版本历史")
async def get_prompt_versions(
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取提示词的版本历史"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT * FROM ai_prompt_versions
WHERE prompt_id = %s
ORDER BY version DESC
""",
(prompt_id,)
)
rows = cursor.fetchall()
result = []
for row in rows:
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
result.append(AIPromptVersionResponse(
id=row["id"],
prompt_id=row["prompt_id"],
version=row["version"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
change_summary=row["change_summary"],
created_at=row["created_at"],
))
return result
finally:
conn.close()
@router.post("/{prompt_id}/rollback/{version}", response_model=AIPromptResponse, summary="回滚提示词版本")
async def rollback_prompt_version(
prompt_id: int,
version: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""回滚到指定版本的提示词"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取指定版本
cursor.execute(
"""
SELECT * FROM ai_prompt_versions
WHERE prompt_id = %s AND version = %s
""",
(prompt_id, version)
)
version_row = cursor.fetchone()
if not version_row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="版本不存在",
)
# 获取当前提示词
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
current = cursor.fetchone()
if not current:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 保存当前版本到历史
new_version = current["version"] + 1
cursor.execute(
"""
INSERT INTO ai_prompt_versions
(prompt_id, version, system_prompt, user_prompt_template, variables,
output_schema, change_summary, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""",
(prompt_id, current["version"],
current["system_prompt"], current["user_prompt_template"],
current["variables"], current["output_schema"],
f"回滚前备份(版本 {current['version']}",
admin.id)
)
# 回滚
cursor.execute(
"""
UPDATE ai_prompts
SET system_prompt = %s, user_prompt_template = %s, variables = %s,
output_schema = %s, version = %s, updated_by = %s
WHERE id = %s
""",
(version_row["system_prompt"], version_row["user_prompt_template"],
version_row["variables"], version_row["output_schema"],
new_version, admin.id, prompt_id)
)
# 记录操作日志
log_operation(
cursor, admin, None, None,
"rollback", "prompt", prompt_id, current["name"],
old_value={"version": current["version"]},
new_value={"version": new_version, "rollback_from": version}
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.get("/tenants/{tenant_id}", response_model=List[TenantPromptResponse], summary="获取租户自定义提示词")
async def get_tenant_prompts(
tenant_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取租户的自定义提示词列表"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT tp.*, ap.code as prompt_code, ap.name as prompt_name
FROM tenant_prompts tp
JOIN ai_prompts ap ON tp.prompt_id = ap.id
WHERE tp.tenant_id = %s
ORDER BY ap.module, ap.id
""",
(tenant_id,)
)
rows = cursor.fetchall()
return [
TenantPromptResponse(
id=row["id"],
tenant_id=row["tenant_id"],
prompt_id=row["prompt_id"],
prompt_code=row["prompt_code"],
prompt_name=row["prompt_name"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
is_active=row["is_active"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
for row in rows
]
finally:
conn.close()
@router.put("/tenants/{tenant_id}/{prompt_id}", response_model=ResponseModel, summary="更新租户自定义提示词")
async def update_tenant_prompt(
tenant_id: int,
prompt_id: int,
data: TenantPromptUpdate,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""创建或更新租户的自定义提示词"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 验证租户存在
cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,))
tenant = cursor.fetchone()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 验证提示词存在
cursor.execute("SELECT name FROM ai_prompts WHERE id = %s", (prompt_id,))
prompt = cursor.fetchone()
if not prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 检查是否已有自定义
cursor.execute(
"""
SELECT id FROM tenant_prompts
WHERE tenant_id = %s AND prompt_id = %s
""",
(tenant_id, prompt_id)
)
existing = cursor.fetchone()
if existing:
# 更新
update_fields = []
update_values = []
if data.system_prompt is not None:
update_fields.append("system_prompt = %s")
update_values.append(data.system_prompt)
if data.user_prompt_template is not None:
update_fields.append("user_prompt_template = %s")
update_values.append(data.user_prompt_template)
if data.is_active is not None:
update_fields.append("is_active = %s")
update_values.append(data.is_active)
if update_fields:
update_fields.append("updated_by = %s")
update_values.append(admin.id)
update_values.append(existing["id"])
cursor.execute(
f"UPDATE tenant_prompts SET {', '.join(update_fields)} WHERE id = %s",
update_values
)
else:
# 创建
cursor.execute(
"""
INSERT INTO tenant_prompts
(tenant_id, prompt_id, system_prompt, user_prompt_template, is_active, created_by)
VALUES (%s, %s, %s, %s, %s, %s)
""",
(tenant_id, prompt_id, data.system_prompt, data.user_prompt_template,
data.is_active if data.is_active is not None else True, admin.id)
)
# 记录操作日志
log_operation(
cursor, admin, tenant_id, tenant["code"],
"update", "tenant_prompt", prompt_id, prompt["name"],
new_value=data.model_dump(exclude_unset=True)
)
conn.commit()
return ResponseModel(message="自定义提示词已保存")
finally:
conn.close()
@router.delete("/tenants/{tenant_id}/{prompt_id}", response_model=ResponseModel, summary="删除租户自定义提示词")
async def delete_tenant_prompt(
tenant_id: int,
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""删除租户的自定义提示词(恢复使用默认)"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
DELETE FROM tenant_prompts
WHERE tenant_id = %s AND prompt_id = %s
""",
(tenant_id, prompt_id)
)
if cursor.rowcount == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="自定义提示词不存在",
)
conn.commit()
return ResponseModel(message="自定义提示词已删除,将使用默认模板")
finally:
conn.close()