402 lines
12 KiB
Python
402 lines
12 KiB
Python
"""
|
||
AI 助手 API 路由
|
||
"""
|
||
|
||
from typing import Optional, List
|
||
from pathlib import Path
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from pydantic import BaseModel
|
||
from datetime import datetime
|
||
|
||
from app.database import get_db
|
||
from app.models.intent import Intent, AICheckRecord
|
||
from app.services.ai_service import get_ai_service
|
||
|
||
router = APIRouter(prefix="/ai", tags=["AI助手"])
|
||
|
||
|
||
# ============ Pydantic Schemas ============
|
||
|
||
class GenerateRequest(BaseModel):
|
||
"""AI生成请求"""
|
||
prompt: str
|
||
context: Optional[str] = None
|
||
|
||
|
||
class GenerateResponse(BaseModel):
|
||
"""AI生成响应"""
|
||
content: str
|
||
success: bool
|
||
error: Optional[str] = None
|
||
|
||
|
||
class CheckRequest(BaseModel):
|
||
"""AI检查请求"""
|
||
content: str
|
||
intent_id: Optional[int] = None # 如果提供,会保存检查记录
|
||
requirements: Optional[List[str]] = None
|
||
|
||
|
||
class CheckResult(BaseModel):
|
||
"""AI检查结果"""
|
||
passed: bool
|
||
score: int
|
||
issues: List[str]
|
||
suggestions: List[str]
|
||
raw_response: Optional[str] = None
|
||
|
||
|
||
class CheckResponse(BaseModel):
|
||
"""AI检查响应"""
|
||
result: CheckResult
|
||
record_id: Optional[int] = None # 如果保存了记录
|
||
success: bool
|
||
error: Optional[str] = None
|
||
|
||
|
||
class IntentGenerateRequest(BaseModel):
|
||
"""意图编制生成请求"""
|
||
test_type: str # 测试类型:功能测试、性能测试、安全测试等
|
||
test_target: str # 测试目标描述
|
||
additional_requirements: Optional[str] = None
|
||
|
||
|
||
class PlanGenerateRequest(BaseModel):
|
||
"""测试规划生成请求"""
|
||
requirement_text: str # 测试需求文本(前端文本框内容)
|
||
source_name: Optional[str] = "用户输入" # 来源名称
|
||
|
||
|
||
class ExtractStepsRequest(BaseModel):
|
||
"""提取测试步骤请求"""
|
||
content: str # 意图编制内容
|
||
title: Optional[str] = "意图编制"
|
||
|
||
|
||
class ExtractedStepData(BaseModel):
|
||
"""提取的步骤数据"""
|
||
id: int
|
||
name: str
|
||
purpose: str
|
||
deviceParams: dict # {设备类别: {参数名: 参数值}}
|
||
|
||
|
||
class ExtractStepsResponse(BaseModel):
|
||
"""提取测试步骤响应"""
|
||
success: bool
|
||
steps: List[ExtractedStepData]
|
||
error: Optional[str] = None
|
||
|
||
|
||
# ============ API Endpoints ============
|
||
|
||
@router.post("/generate", response_model=GenerateResponse)
|
||
async def generate_content(request: GenerateRequest):
|
||
"""AI 生成内容"""
|
||
try:
|
||
ai_service = get_ai_service()
|
||
content = await ai_service.generate(
|
||
prompt=request.prompt,
|
||
context=request.context
|
||
)
|
||
return GenerateResponse(content=content, success=True)
|
||
except Exception as e:
|
||
return GenerateResponse(
|
||
content="",
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
|
||
|
||
@router.post("/check", response_model=CheckResponse)
|
||
async def check_content(
|
||
request: CheckRequest,
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""AI 检查内容"""
|
||
try:
|
||
ai_service = get_ai_service()
|
||
result = await ai_service.check(
|
||
content=request.content,
|
||
requirements=request.requirements
|
||
)
|
||
|
||
check_result = CheckResult(
|
||
passed=result.get("passed", False),
|
||
score=result.get("score", 0),
|
||
issues=result.get("issues", []),
|
||
suggestions=result.get("suggestions", []),
|
||
raw_response=result.get("raw_response")
|
||
)
|
||
|
||
record_id = None
|
||
|
||
# 如果提供了 intent_id,保存检查记录
|
||
if request.intent_id:
|
||
# 验证意图存在
|
||
query = select(Intent).where(Intent.id == request.intent_id)
|
||
intent_result = await db.execute(query)
|
||
intent = intent_result.scalar_one_or_none()
|
||
|
||
if intent:
|
||
record = AICheckRecord(
|
||
intent_id=request.intent_id,
|
||
check_result=result,
|
||
suggestions="\n".join(result.get("suggestions", []))
|
||
)
|
||
db.add(record)
|
||
await db.commit()
|
||
await db.refresh(record)
|
||
record_id = record.id
|
||
|
||
return CheckResponse(
|
||
result=check_result,
|
||
record_id=record_id,
|
||
success=True
|
||
)
|
||
except Exception as e:
|
||
return CheckResponse(
|
||
result=CheckResult(passed=False, score=0, issues=[str(e)], suggestions=[]),
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
|
||
|
||
@router.post("/generate-intent", response_model=GenerateResponse)
|
||
async def generate_intent_content(request: IntentGenerateRequest):
|
||
"""AI 生成意图编制初始内容"""
|
||
prompt = f"""请帮我生成一份测试意图编制文档的初始内容。
|
||
|
||
测试类型:{request.test_type}
|
||
测试目标:{request.test_target}
|
||
{"额外要求:" + request.additional_requirements if request.additional_requirements else ""}
|
||
|
||
请按照以下格式生成内容:
|
||
|
||
## 1. 测试目标
|
||
[详细描述测试的目标和预期达成的效果]
|
||
|
||
## 2. 测试范围
|
||
[明确测试的边界和覆盖范围]
|
||
|
||
## 3. 测试条件
|
||
### 3.1 前置条件
|
||
[列出测试开始前需要满足的条件]
|
||
|
||
### 3.2 测试环境
|
||
[描述测试所需的硬件、软件环境]
|
||
|
||
### 3.3 测试数据
|
||
[描述测试所需的数据准备]
|
||
|
||
## 4. 测试用例概述
|
||
[列出主要的测试场景和用例]
|
||
|
||
## 5. 预期结果
|
||
[描述测试成功的判定标准]
|
||
|
||
## 6. 风险与注意事项
|
||
[列出可能的风险和需要注意的事项]
|
||
"""
|
||
|
||
context = """你是一位专业的软件测试工程师,擅长编写测试意图编制文档。
|
||
请生成规范、完整、专业的测试意图编制内容。"""
|
||
|
||
try:
|
||
ai_service = get_ai_service()
|
||
content = await ai_service.generate(prompt=prompt, context=context)
|
||
return GenerateResponse(content=content, success=True)
|
||
except Exception as e:
|
||
return GenerateResponse(
|
||
content="",
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
|
||
|
||
@router.post("/generate-plan", response_model=GenerateResponse)
|
||
async def generate_test_plan(request: PlanGenerateRequest):
|
||
"""
|
||
根据前端输入的测试需求文本,调用 planner 生成测试规划。
|
||
直接返回生成的 Markdown 内容(不保存文件)。
|
||
"""
|
||
from planner.planning_agent.planner import build_plan_from_text
|
||
|
||
try:
|
||
# 直接调用规划生成函数,返回 Markdown 内容
|
||
md_content = build_plan_from_text(
|
||
requirement_text=request.requirement_text,
|
||
source_doc=request.source_name or "用户输入"
|
||
)
|
||
|
||
if not md_content:
|
||
return GenerateResponse(
|
||
content="",
|
||
success=False,
|
||
error="生成内容为空,请检查输入内容或后端服务"
|
||
)
|
||
|
||
return GenerateResponse(content=md_content, success=True)
|
||
except Exception as e:
|
||
import traceback
|
||
traceback.print_exc()
|
||
return GenerateResponse(
|
||
content="",
|
||
success=False,
|
||
error=f"规划生成失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/extract-steps", response_model=ExtractStepsResponse)
|
||
async def extract_steps_from_intent(request: ExtractStepsRequest):
|
||
"""
|
||
从意图编制内容中提取测试步骤和参数。
|
||
使用 LLM 解析文本并返回结构化的步骤数据。
|
||
"""
|
||
try:
|
||
ai_service = get_ai_service()
|
||
|
||
# 构建提取 prompt
|
||
extract_prompt = f"""请从以下测试规划内容中提取测试步骤和仪器参数。
|
||
|
||
请严格按照以下 JSON 格式返回,不要添加任何其他内容:
|
||
```json
|
||
{{
|
||
"steps": [
|
||
{{
|
||
"id": 1,
|
||
"name": "步骤名称",
|
||
"purpose": "步骤目的",
|
||
"deviceParams": {{
|
||
"程控电源参数": {{
|
||
"输出电压": "28V",
|
||
"输出电流": "6A"
|
||
}},
|
||
"频谱分析仪参数": {{
|
||
"中心频率": "2.7GHz",
|
||
"扫宽Span": "500MHz"
|
||
}}
|
||
}}
|
||
}}
|
||
]
|
||
}}
|
||
```
|
||
|
||
设备类别必须使用以下名称之一:
|
||
- 程控电源参数
|
||
- 功率探头参数
|
||
- 频谱分析仪参数
|
||
- 矢量网络分析仪参数
|
||
- 矢量信号分析仪参数
|
||
- 矢量信号源参数
|
||
- 示波器参数
|
||
- 信号源基础参数
|
||
|
||
测试规划内容:
|
||
{request.content}
|
||
"""
|
||
|
||
context = """你是一个专业的测试工程师,擅长从测试规划文档中提取结构化的测试步骤和仪器参数。
|
||
请仔细分析文档,提取每个测试步骤及其涉及的仪器配置参数。
|
||
只返回 JSON 格式的结果,不要添加任何解释或其他文字。"""
|
||
|
||
response_text = await ai_service.generate(prompt=extract_prompt, context=context)
|
||
|
||
# 解析 JSON 响应
|
||
import json
|
||
import re
|
||
|
||
# 尝试从响应中提取 JSON
|
||
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response_text)
|
||
if json_match:
|
||
json_str = json_match.group(1)
|
||
else:
|
||
# 尝试直接解析整个响应
|
||
json_str = response_text.strip()
|
||
# 清理可能的 markdown 代码块标记
|
||
json_str = re.sub(r'^```\w*\n?', '', json_str)
|
||
json_str = re.sub(r'\n?```$', '', json_str)
|
||
|
||
try:
|
||
parsed = json.loads(json_str)
|
||
steps_data = parsed.get("steps", [])
|
||
except json.JSONDecodeError:
|
||
# 如果解析失败,返回空列表
|
||
return ExtractStepsResponse(
|
||
success=False,
|
||
steps=[],
|
||
error="无法解析 LLM 返回的 JSON 格式"
|
||
)
|
||
|
||
# 构建响应,过滤掉空值
|
||
def filter_device_params(params: dict) -> dict:
|
||
"""过滤掉空的设备参数"""
|
||
if not params or not isinstance(params, dict):
|
||
return {}
|
||
filtered = {}
|
||
for device_category, device_params in params.items():
|
||
if not device_params or not isinstance(device_params, dict):
|
||
continue
|
||
# 过滤掉空值的参数
|
||
non_empty_params = {
|
||
k: v for k, v in device_params.items()
|
||
if v and isinstance(v, str) and v.strip()
|
||
}
|
||
if non_empty_params:
|
||
filtered[device_category] = non_empty_params
|
||
return filtered
|
||
|
||
steps = []
|
||
for step in steps_data:
|
||
filtered_params = filter_device_params(step.get("deviceParams", {}))
|
||
steps.append(ExtractedStepData(
|
||
id=step.get("id", len(steps) + 1),
|
||
name=step.get("name", f"步骤 {len(steps) + 1}"),
|
||
purpose=step.get("purpose", ""),
|
||
deviceParams=filtered_params
|
||
))
|
||
|
||
return ExtractStepsResponse(
|
||
success=True,
|
||
steps=steps
|
||
)
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
traceback.print_exc()
|
||
return ExtractStepsResponse(
|
||
success=False,
|
||
steps=[],
|
||
error=f"步骤提取失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/check-records/{intent_id}")
|
||
async def get_check_records(
|
||
intent_id: int,
|
||
db: AsyncSession = Depends(get_db)
|
||
):
|
||
"""获取意图的所有检查记录"""
|
||
query = select(AICheckRecord).where(
|
||
AICheckRecord.intent_id == intent_id
|
||
).order_by(AICheckRecord.checked_at.desc())
|
||
|
||
result = await db.execute(query)
|
||
records = result.scalars().all()
|
||
|
||
return {
|
||
"intent_id": intent_id,
|
||
"total": len(records),
|
||
"records": [
|
||
{
|
||
"id": r.id,
|
||
"check_result": r.check_result,
|
||
"suggestions": r.suggestions,
|
||
"checked_at": r.checked_at.isoformat()
|
||
}
|
||
for r in records
|
||
]
|
||
}
|