完善模板测试弹窗与多文件解析链路
This commit is contained in:
@@ -7,6 +7,7 @@ cryptography>=42.0.0
|
||||
python-docx>=1.1.0
|
||||
openpyxl>=3.1.0
|
||||
pandas>=2.1.0
|
||||
xlrd>=2.0.1
|
||||
httpx>=0.27.0
|
||||
pydantic>=2.5.0
|
||||
pydantic-settings>=2.1.0
|
||||
@@ -15,3 +16,4 @@ aiofiles>=23.2.0
|
||||
sse-starlette>=2.0.0
|
||||
minio>=7.2.0 # MinIO 对象存储 SDK
|
||||
alembic>=1.13.0
|
||||
pypdf>=5.0.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
@@ -17,6 +18,7 @@ from models.paragraph import Paragraph
|
||||
from models.template import Template
|
||||
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
||||
from services.ai_service import call_ai
|
||||
from services.file_summary import summarize_minio_files
|
||||
from services.generation_runtime import (
|
||||
build_mock_content,
|
||||
generation_progress,
|
||||
@@ -48,6 +50,8 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
||||
paragraph = await db.get(Paragraph, body.paragraph_id)
|
||||
if paragraph is None or paragraph.template_id != body.template_id:
|
||||
raise HTTPException(status_code=404, detail="段落不存在")
|
||||
if body.prompt_text:
|
||||
paragraph.prompt_text = body.prompt_text
|
||||
|
||||
model = None
|
||||
if body.model_id:
|
||||
@@ -55,11 +59,12 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
||||
elif paragraph.model_id:
|
||||
model = await db.get(AiModel, paragraph.model_id)
|
||||
|
||||
file_summaries = await asyncio.to_thread(summarize_minio_files, body.file_paths or []) if body.file_paths else []
|
||||
if model is None or model.status != "enabled":
|
||||
content = build_mock_content(paragraph)
|
||||
message = "当前未找到可用模型,返回本地模拟生成结果。"
|
||||
else:
|
||||
result = await call_ai(paragraph, model)
|
||||
result = await call_ai(paragraph, model, file_summaries)
|
||||
content = result.content
|
||||
message = f"已通过模型 {result.used_model} 生成。"
|
||||
return Response(
|
||||
@@ -67,6 +72,7 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
||||
"paragraph_id": paragraph.id,
|
||||
"content": content,
|
||||
"message": message,
|
||||
"file_summaries": file_summaries,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def _ensure_json_content(text: str) -> dict:
|
||||
return {"content": [{"type": "text", "text": stripped}]}
|
||||
|
||||
|
||||
def _build_prompt(paragraph: Paragraph) -> tuple[str, str]:
|
||||
def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
|
||||
system_prompt = (
|
||||
"你是一个企业文档撰写助手。"
|
||||
"请严格输出 JSON,不要输出 JSON 之外的说明。"
|
||||
@@ -55,6 +55,11 @@ def _build_prompt(paragraph: Paragraph) -> tuple[str, str]:
|
||||
user_parts.append(f"模板上下文:{paragraph.content}")
|
||||
if paragraph.need_prompt and paragraph.prompt_text:
|
||||
user_parts.append(f"附加要求:{paragraph.prompt_text}")
|
||||
if file_summaries:
|
||||
file_blocks = []
|
||||
for item in file_summaries:
|
||||
file_blocks.append(f"文件名:{item['file_name']}\n文件摘要:{item['summary']}")
|
||||
user_parts.append("参考文件内容:\n" + "\n\n".join(file_blocks))
|
||||
user_parts.append(f"输出格式:{paragraph.output_format}")
|
||||
return system_prompt, "\n\n".join(user_parts)
|
||||
|
||||
@@ -157,8 +162,8 @@ async def _call_anthropic(model: AiModel, system_prompt: str, user_prompt: str)
|
||||
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
|
||||
|
||||
|
||||
async def call_ai(paragraph: Paragraph, model: AiModel) -> AiCallResult:
|
||||
system_prompt, user_prompt = _build_prompt(paragraph)
|
||||
async def call_ai(paragraph: Paragraph, model: AiModel, file_summaries: list[dict] | None = None) -> AiCallResult:
|
||||
system_prompt, user_prompt = _build_prompt(paragraph, file_summaries)
|
||||
if model.api_format == "anthropic":
|
||||
return await _call_anthropic(model, system_prompt, user_prompt)
|
||||
return await _call_openai_compatible(model, system_prompt, user_prompt)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from docx import Document
|
||||
|
||||
from services.minio_client import download_object_bytes, split_bucket_path
|
||||
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except Exception: # pragma: no cover
|
||||
PdfReader = None
|
||||
|
||||
|
||||
def _decode_text(content: bytes) -> str:
|
||||
for encoding in ("utf-8", "utf-8-sig", "gbk", "gb18030"):
|
||||
try:
|
||||
return content.decode(encoding)
|
||||
except Exception:
|
||||
continue
|
||||
return content.decode("utf-8", errors="ignore")
|
||||
|
||||
|
||||
def _summarize_docx(content: bytes) -> str:
|
||||
doc = Document(io.BytesIO(content))
|
||||
texts = [paragraph.text.strip() for paragraph in doc.paragraphs if paragraph.text.strip()]
|
||||
return "\n".join(texts[:40])[:4000]
|
||||
|
||||
|
||||
def _summarize_csv(content: bytes) -> str:
|
||||
text = _decode_text(content)
|
||||
reader = csv.reader(io.StringIO(text))
|
||||
rows = list(reader[:20])
|
||||
return "\n".join([" | ".join(row) for row in rows])[:4000]
|
||||
|
||||
|
||||
def _summarize_excel(content: bytes, suffix: str) -> str:
|
||||
excel_buffer = io.BytesIO(content)
|
||||
sheet_map = pd.read_excel(excel_buffer, sheet_name=None) if suffix == ".xlsx" else pd.read_excel(excel_buffer, sheet_name=None, engine="xlrd")
|
||||
parts: list[str] = []
|
||||
for sheet_name, dataframe in list(sheet_map.items())[:5]:
|
||||
preview = dataframe.head(10).fillna("").astype(str)
|
||||
parts.append(f"[工作表] {sheet_name}")
|
||||
parts.append(preview.to_csv(index=False).strip())
|
||||
return "\n".join(parts)[:5000]
|
||||
|
||||
|
||||
def _summarize_pdf(content: bytes) -> str:
|
||||
if PdfReader is None:
|
||||
return "当前环境未安装 PDF 文本解析依赖,无法提取 PDF 正文。"
|
||||
reader = PdfReader(io.BytesIO(content))
|
||||
texts: list[str] = []
|
||||
for page in reader.pages[:10]:
|
||||
texts.append((page.extract_text() or "").strip())
|
||||
return "\n".join(filter(None, texts))[:4000]
|
||||
|
||||
|
||||
def summarize_file_bytes(file_name: str, content: bytes) -> str:
|
||||
suffix = Path(file_name).suffix.lower()
|
||||
if suffix in {".txt", ".md", ".json"}:
|
||||
return _decode_text(content)[:4000]
|
||||
if suffix == ".csv":
|
||||
return _summarize_csv(content)
|
||||
if suffix in {".xlsx", ".xls"}:
|
||||
return _summarize_excel(content, suffix)
|
||||
if suffix == ".docx":
|
||||
return _summarize_docx(content)
|
||||
if suffix == ".pdf":
|
||||
return _summarize_pdf(content)
|
||||
return f"暂不支持解析该文件内容:{file_name}"
|
||||
|
||||
|
||||
def summarize_minio_files(file_paths: list[str]) -> list[dict]:
|
||||
summaries: list[dict] = []
|
||||
for file_path in file_paths:
|
||||
bucket, object_name = split_bucket_path(file_path)
|
||||
content = download_object_bytes(bucket, object_name)
|
||||
file_name = Path(object_name).name
|
||||
summaries.append(
|
||||
{
|
||||
"file_name": file_name,
|
||||
"file_path": file_path,
|
||||
"summary": summarize_file_bytes(file_name, content),
|
||||
}
|
||||
)
|
||||
return summaries
|
||||
Reference in New Issue
Block a user