完善模型测试与参考文件历史能力
This commit is contained in:
+1
-1
@@ -33,7 +33,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# 文件上传限制
|
||||
MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024 # 50MB
|
||||
ALLOWED_EXTENSIONS: list = [".docx", ".xlsx", ".xls", ".csv", ".pdf", ".txt", ".md"]
|
||||
ALLOWED_EXTENSIONS: list = [".docx", ".doc", ".xlsx", ".xls", ".xlsm", ".csv", ".pdf", ".txt", ".md", ".json"]
|
||||
|
||||
# 加密(用于 API Key 加密)
|
||||
ENCRYPTION_KEY: str = "change-this-to-a-32-byte-key-in-production!!"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from config import settings
|
||||
|
||||
@@ -24,5 +25,18 @@ async def init_db():
|
||||
from models.ai_model import AiModel
|
||||
from models.document import Document
|
||||
from models.generation_log import GenerationLog
|
||||
from models.reference_file import ReferenceFile
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
dialect_name = conn.dialect.name
|
||||
columns = await conn.run_sync(lambda sync_conn: [column["name"] for column in inspect(sync_conn).get_columns("ai_models")])
|
||||
if "supports_streaming" not in columns:
|
||||
if dialect_name == "sqlite":
|
||||
await conn.execute(text("ALTER TABLE ai_models ADD COLUMN supports_streaming BOOLEAN DEFAULT 0"))
|
||||
else:
|
||||
await conn.execute(text("ALTER TABLE ai_models ADD COLUMN supports_streaming TINYINT(1) DEFAULT 0"))
|
||||
if "enable_reasoning" not in columns:
|
||||
if dialect_name == "sqlite":
|
||||
await conn.execute(text("ALTER TABLE ai_models ADD COLUMN enable_reasoning BOOLEAN DEFAULT 0"))
|
||||
else:
|
||||
await conn.execute(text("ALTER TABLE ai_models ADD COLUMN enable_reasoning TINYINT(1) DEFAULT 0"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, func
|
||||
from sqlalchemy import Boolean, Column, Integer, String, Text, DateTime, func
|
||||
from database import Base
|
||||
|
||||
class AiModel(Base):
|
||||
@@ -9,6 +9,8 @@ class AiModel(Base):
|
||||
api_format = Column(String(20), default="openai", comment="anthropic/openai")
|
||||
api_endpoint = Column(String(500), default="", comment="API接口地址")
|
||||
api_key_encrypted = Column(Text, default="", comment="加密后的API Key")
|
||||
supports_streaming = Column(Boolean, default=False, comment="是否支持流式传输")
|
||||
enable_reasoning = Column(Boolean, default=False, comment="是否开启思考模式")
|
||||
status = Column(String(20), default="enabled", comment="enabled/disabled")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class ReferenceFile(Base):
|
||||
__tablename__ = "reference_files"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
file_name: Mapped[str] = mapped_column(String(255), default="", comment="原始文件名")
|
||||
file_path: Mapped[str] = mapped_column(String(500), default="", comment="MinIO 对象路径")
|
||||
file_size: Mapped[int] = mapped_column(Integer, default=0, comment="文件大小")
|
||||
content_type: Mapped[str] = mapped_column(String(120), default="", comment="文件类型")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
+136
-8
@@ -15,9 +15,10 @@ from models.ai_model import AiModel
|
||||
from models.document import Document
|
||||
from models.generation_log import GenerationLog
|
||||
from models.paragraph import Paragraph
|
||||
from models.reference_file import ReferenceFile
|
||||
from models.template import Template
|
||||
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
||||
from services.ai_service import call_ai
|
||||
from services.ai_service import call_ai, stream_ai_preview
|
||||
from services.file_summary import summarize_minio_files
|
||||
from services.generation_runtime import (
|
||||
build_mock_content,
|
||||
@@ -45,6 +46,17 @@ def _serialize_document(document: Document) -> dict:
|
||||
"updated_at": document.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _serialize_reference_file(file: ReferenceFile) -> dict:
|
||||
return {
|
||||
"id": file.id,
|
||||
"file_name": file.file_name,
|
||||
"file_path": file.file_path,
|
||||
"file_size": file.file_size,
|
||||
"content_type": file.content_type,
|
||||
"created_at": file.created_at,
|
||||
}
|
||||
|
||||
@router.post("/test")
|
||||
async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(get_db)):
|
||||
paragraph = await db.get(Paragraph, body.paragraph_id)
|
||||
@@ -64,6 +76,7 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
||||
content = build_mock_content(paragraph)
|
||||
message = "当前未找到可用模型,返回本地模拟生成结果。"
|
||||
else:
|
||||
setattr(paragraph, "enable_reasoning", bool(model.enable_reasoning))
|
||||
result = await call_ai(paragraph, model, file_summaries)
|
||||
content = result.content
|
||||
message = f"已通过模型 {result.used_model} 生成。"
|
||||
@@ -77,14 +90,101 @@ async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(ge
|
||||
)
|
||||
|
||||
|
||||
@router.post("/test-stream")
|
||||
async def generate_test_stream(body: GenerateTestRequest, db: AsyncSession = Depends(get_db)):
|
||||
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:
|
||||
model = await db.get(AiModel, body.model_id)
|
||||
elif paragraph.model_id:
|
||||
model = await db.get(AiModel, paragraph.model_id)
|
||||
|
||||
if model is None or model.status != "enabled":
|
||||
raise HTTPException(status_code=400, detail="当前段落未配置可用的流式模型")
|
||||
if not model.supports_streaming:
|
||||
raise HTTPException(status_code=400, detail="当前模型未开启流式传输")
|
||||
|
||||
file_summaries = await asyncio.to_thread(summarize_minio_files, body.file_paths or []) if body.file_paths else []
|
||||
setattr(paragraph, "enable_reasoning", bool(model.enable_reasoning))
|
||||
|
||||
async def event_stream():
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps(
|
||||
{
|
||||
"type": "meta",
|
||||
"message": f"正在通过模型 {model.name} 流式生成...",
|
||||
"file_summaries": file_summaries,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
try:
|
||||
async for chunk in stream_ai_preview(paragraph, model, file_summaries):
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps({"type": "delta", "content": chunk}, ensure_ascii=False),
|
||||
}
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps({"type": "done"}, ensure_ascii=False),
|
||||
}
|
||||
except Exception as error:
|
||||
fallback_message = str(error)
|
||||
if "503" in fallback_message or "temporarily unavailable" in fallback_message.lower():
|
||||
try:
|
||||
result = await call_ai(paragraph, model, file_summaries)
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps(
|
||||
{
|
||||
"type": "meta",
|
||||
"message": "流式通道暂时不可用,已自动回退为普通返回。",
|
||||
"file_summaries": file_summaries,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps({"type": "delta", "content": result.raw_text}, ensure_ascii=False),
|
||||
}
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps({"type": "done"}, ensure_ascii=False),
|
||||
}
|
||||
return
|
||||
except Exception as fallback_error:
|
||||
fallback_message = f"{fallback_message};普通调用回退也失败:{fallback_error}"
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": json.dumps({"type": "error", "message": fallback_message}, ensure_ascii=False),
|
||||
}
|
||||
|
||||
return EventSourceResponse(
|
||||
event_stream(),
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_reference_file(file: UploadFile = File(...)):
|
||||
async def upload_reference_file(file: UploadFile = File(...), db: AsyncSession = Depends(get_db)):
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="文件名不能为空")
|
||||
|
||||
ext = os.path.splitext(file.filename)[1].lower()
|
||||
if ext not in settings.ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(status_code=400, detail="文件类型不支持")
|
||||
allowed = " / ".join(settings.ALLOWED_EXTENSIONS)
|
||||
raise HTTPException(status_code=400, detail=f"文件类型不支持:{ext or '无扩展名'}。当前支持:{allowed}")
|
||||
|
||||
content = await file.read()
|
||||
if not content:
|
||||
@@ -100,12 +200,40 @@ async def upload_reference_file(file: UploadFile = File(...)):
|
||||
content,
|
||||
file.content_type or "application/octet-stream",
|
||||
)
|
||||
return Response(
|
||||
data={
|
||||
"file_name": file.filename,
|
||||
"file_path": f"{settings.MINIO_BUCKET_UPLOADS}/{object_name}",
|
||||
}
|
||||
record = ReferenceFile(
|
||||
file_name=file.filename,
|
||||
file_path=f"{settings.MINIO_BUCKET_UPLOADS}/{object_name}",
|
||||
file_size=len(content),
|
||||
content_type=file.content_type or "application/octet-stream",
|
||||
)
|
||||
db.add(record)
|
||||
await db.commit()
|
||||
await db.refresh(record)
|
||||
return Response(
|
||||
data=_serialize_reference_file(record)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/reference-files")
|
||||
async def list_reference_files(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
keyword: str = Query("", description="按文件名搜索"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
stmt = select(ReferenceFile)
|
||||
count_stmt = select(func.count(ReferenceFile.id))
|
||||
if keyword:
|
||||
like_keyword = f"%{keyword.strip()}%"
|
||||
stmt = stmt.where(ReferenceFile.file_name.like(like_keyword))
|
||||
count_stmt = count_stmt.where(ReferenceFile.file_name.like(like_keyword))
|
||||
|
||||
total = (await db.execute(count_stmt)).scalar_one()
|
||||
result = await db.execute(
|
||||
stmt.order_by(ReferenceFile.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
)
|
||||
items = [_serialize_reference_file(item) for item in result.scalars().all()]
|
||||
return Response(data={"items": items, "total": total, "page": page, "page_size": page_size})
|
||||
|
||||
|
||||
@router.post("/full")
|
||||
|
||||
@@ -20,6 +20,8 @@ def _serialize_model(model: AiModel) -> dict:
|
||||
"api_format": model.api_format,
|
||||
"api_endpoint": model.api_endpoint,
|
||||
"api_key_preview": mask_secret(api_key),
|
||||
"supports_streaming": bool(model.supports_streaming),
|
||||
"enable_reasoning": bool(model.enable_reasoning),
|
||||
"status": model.status,
|
||||
"created_at": model.created_at,
|
||||
}
|
||||
@@ -40,6 +42,8 @@ async def create_model(body: AiModelCreate, db: AsyncSession = Depends(get_db)):
|
||||
api_format=body.api_format,
|
||||
api_endpoint=body.api_endpoint,
|
||||
api_key_encrypted=encrypt_text(body.api_key),
|
||||
supports_streaming=body.supports_streaming,
|
||||
enable_reasoning=body.enable_reasoning,
|
||||
status=body.status,
|
||||
)
|
||||
db.add(model)
|
||||
@@ -62,6 +66,10 @@ async def update_model(model_id: int, body: AiModelUpdate, db: AsyncSession = De
|
||||
model.api_format = body.api_format
|
||||
if body.api_endpoint is not None:
|
||||
model.api_endpoint = body.api_endpoint
|
||||
if body.supports_streaming is not None:
|
||||
model.supports_streaming = body.supports_streaming
|
||||
if body.enable_reasoning is not None:
|
||||
model.enable_reasoning = body.enable_reasoning
|
||||
if body.status is not None:
|
||||
model.status = body.status
|
||||
if body.api_key:
|
||||
@@ -95,6 +103,7 @@ async def test_model(model_id: int, db: AsyncSession = Depends(get_db)):
|
||||
need_prompt = False
|
||||
prompt_text = ""
|
||||
output_format = "text"
|
||||
enable_reasoning = bool(model.enable_reasoning)
|
||||
|
||||
try:
|
||||
result = await call_ai(FakeParagraph(), model)
|
||||
|
||||
@@ -50,6 +50,8 @@ class AiModelCreate(BaseModel):
|
||||
api_format: str = "openai"
|
||||
api_endpoint: str = ""
|
||||
api_key: str = ""
|
||||
supports_streaming: bool = False
|
||||
enable_reasoning: bool = False
|
||||
status: str = "enabled"
|
||||
|
||||
|
||||
@@ -59,6 +61,8 @@ class AiModelUpdate(BaseModel):
|
||||
api_format: Optional[str] = None
|
||||
api_endpoint: Optional[str] = None
|
||||
api_key: str = ""
|
||||
supports_streaming: Optional[bool] = None
|
||||
enable_reasoning: Optional[bool] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
class AiModelOut(BaseModel):
|
||||
@@ -68,6 +72,8 @@ class AiModelOut(BaseModel):
|
||||
api_format: str = "openai"
|
||||
api_endpoint: str = ""
|
||||
api_key_preview: str = ""
|
||||
supports_streaming: bool = False
|
||||
enable_reasoning: bool = False
|
||||
status: str = "enabled"
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
@@ -43,12 +43,23 @@ def _ensure_json_content(text: str) -> dict:
|
||||
return {"content": [{"type": "text", "text": stripped}]}
|
||||
|
||||
|
||||
def _format_file_context(file_summaries: list[dict]) -> str:
|
||||
file_blocks: list[str] = []
|
||||
for item in file_summaries:
|
||||
file_name = item.get("file_name") or "未命名文件"
|
||||
summary = item.get("summary") or "文件内容为空。"
|
||||
file_blocks.append(f"文件:{file_name}\n内容:\n{summary}")
|
||||
return "\n\n".join(file_blocks)
|
||||
|
||||
|
||||
def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
|
||||
system_prompt = (
|
||||
"你是一个企业文档撰写助手。"
|
||||
"请严格输出 JSON,不要输出 JSON 之外的说明。"
|
||||
'格式为:{"content":[{"type":"text","text":"..."},{"type":"table","title":"...","headers":["..."],"rows":[["..."]]}]}。'
|
||||
)
|
||||
if getattr(paragraph, "enable_reasoning", False):
|
||||
system_prompt += "你可以先进行充分思考,再给出最终答案,但最终只输出要求的结果内容。"
|
||||
|
||||
user_parts = [f"段落标题:{paragraph.title}"]
|
||||
if paragraph.content:
|
||||
@@ -56,10 +67,7 @@ def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None
|
||||
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("参考文件内容:\n" + _format_file_context(file_summaries))
|
||||
user_parts.append(f"输出格式:{paragraph.output_format}")
|
||||
return system_prompt, "\n\n".join(user_parts)
|
||||
|
||||
@@ -136,6 +144,65 @@ async def _call_openai_compatible(model: AiModel, system_prompt: str, user_promp
|
||||
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
|
||||
|
||||
|
||||
async def _stream_openai_compatible(model: AiModel, system_prompt: str, user_prompt: str):
|
||||
api_key = decrypt_text(model.api_key_encrypted)
|
||||
if not api_key:
|
||||
raise RuntimeError("模型 API Key 不可用")
|
||||
|
||||
payload = {
|
||||
"model": model.name,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"stream": True,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(settings.AI_MAX_RETRIES):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=settings.AI_REQUEST_TIMEOUT, trust_env=False) as client:
|
||||
async with client.stream("POST", _normalize_openai_endpoint(model.api_endpoint), headers=headers, json=payload) as response:
|
||||
if response.status_code in (429, 500, 502, 503, 504):
|
||||
body = await response.aread()
|
||||
raise httpx.HTTPStatusError(
|
||||
f"上游模型流式响应异常: {response.status_code} - {body.decode('utf-8', errors='ignore')[:500]}",
|
||||
request=response.request,
|
||||
response=response,
|
||||
)
|
||||
response.raise_for_status()
|
||||
async for line in response.aiter_lines():
|
||||
if not line or not line.startswith("data:"):
|
||||
continue
|
||||
payload_line = line[5:].strip()
|
||||
if payload_line == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(payload_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
delta_payload = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
delta = delta_payload.get("content", "")
|
||||
reasoning = delta_payload.get("reasoning_content", "")
|
||||
if isinstance(delta, list):
|
||||
delta = "".join(
|
||||
item.get("text", "") if isinstance(item, dict) else str(item)
|
||||
for item in delta
|
||||
)
|
||||
if delta:
|
||||
yield delta
|
||||
if reasoning:
|
||||
yield reasoning
|
||||
return
|
||||
except (httpx.TimeoutException, httpx.HTTPStatusError, httpx.NetworkError) as error:
|
||||
last_error = error
|
||||
if attempt == settings.AI_MAX_RETRIES - 1:
|
||||
break
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
raise RuntimeError(f"模型流式调用失败:{last_error}")
|
||||
|
||||
|
||||
async def _call_anthropic(model: AiModel, system_prompt: str, user_prompt: str) -> AiCallResult:
|
||||
api_key = decrypt_text(model.api_key_encrypted)
|
||||
if not api_key:
|
||||
@@ -162,8 +229,79 @@ 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 _stream_anthropic(model: AiModel, system_prompt: str, user_prompt: str):
|
||||
api_key = decrypt_text(model.api_key_encrypted)
|
||||
if not api_key:
|
||||
raise RuntimeError("模型 API Key 不可用")
|
||||
|
||||
payload = {
|
||||
"model": model.name,
|
||||
"max_tokens": 2048,
|
||||
"system": system_prompt,
|
||||
"messages": [{"role": "user", "content": user_prompt}],
|
||||
"stream": True,
|
||||
}
|
||||
headers = {
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(settings.AI_MAX_RETRIES):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=settings.AI_REQUEST_TIMEOUT, trust_env=False) as client:
|
||||
async with client.stream("POST", _normalize_anthropic_endpoint(model.api_endpoint), headers=headers, json=payload) as response:
|
||||
if response.status_code in (429, 500, 502, 503, 504):
|
||||
body = await response.aread()
|
||||
raise httpx.HTTPStatusError(
|
||||
f"上游模型流式响应异常: {response.status_code} - {body.decode('utf-8', errors='ignore')[:500]}",
|
||||
request=response.request,
|
||||
response=response,
|
||||
)
|
||||
response.raise_for_status()
|
||||
async for line in response.aiter_lines():
|
||||
if not line or not line.startswith("data:"):
|
||||
continue
|
||||
payload_line = line[5:].strip()
|
||||
if payload_line == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(payload_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if chunk.get("type") == "content_block_delta":
|
||||
delta = chunk.get("delta", {}).get("text", "")
|
||||
if delta:
|
||||
yield delta
|
||||
return
|
||||
except (httpx.TimeoutException, httpx.HTTPStatusError, httpx.NetworkError) as error:
|
||||
last_error = error
|
||||
if attempt == settings.AI_MAX_RETRIES - 1:
|
||||
break
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
raise RuntimeError(f"模型流式调用失败:{last_error}")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def build_test_stream_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
|
||||
system_prompt = "你是一个企业文档撰写助手。请直接输出适合预览的正文内容或 Markdown 表格,不要输出 JSON。"
|
||||
if getattr(paragraph, "enable_reasoning", False):
|
||||
system_prompt += "你可以先进行充分思考,再持续输出最终可展示的内容。"
|
||||
_, user_prompt = _build_prompt(paragraph, file_summaries or [])
|
||||
return system_prompt, user_prompt
|
||||
|
||||
|
||||
async def stream_ai_preview(paragraph: Paragraph, model: AiModel, file_summaries: list[dict] | None = None):
|
||||
system_prompt, user_prompt = build_test_stream_prompt(paragraph, file_summaries)
|
||||
if model.api_format == "anthropic":
|
||||
async for chunk in _stream_anthropic(model, system_prompt, user_prompt):
|
||||
yield chunk
|
||||
return
|
||||
async for chunk in _stream_openai_compatible(model, system_prompt, user_prompt):
|
||||
yield chunk
|
||||
|
||||
@@ -63,10 +63,12 @@ def summarize_file_bytes(file_name: str, content: bytes) -> str:
|
||||
return _decode_text(content)[:4000]
|
||||
if suffix == ".csv":
|
||||
return _summarize_csv(content)
|
||||
if suffix in {".xlsx", ".xls"}:
|
||||
if suffix in {".xlsx", ".xls", ".xlsm"}:
|
||||
return _summarize_excel(content, suffix)
|
||||
if suffix == ".docx":
|
||||
return _summarize_docx(content)
|
||||
if suffix == ".doc":
|
||||
return "当前暂不支持直接解析 .doc 旧版 Word 文件正文,建议先另存为 .docx 后再上传。"
|
||||
if suffix == ".pdf":
|
||||
return _summarize_pdf(content)
|
||||
return f"暂不支持解析该文件内容:{file_name}"
|
||||
|
||||
@@ -117,6 +117,7 @@ async def run_generation(document_id: int, template_id: int):
|
||||
if model is None:
|
||||
content = build_mock_content(paragraph)
|
||||
else:
|
||||
setattr(paragraph, "enable_reasoning", bool(model.enable_reasoning))
|
||||
result_data = await call_ai(paragraph, model)
|
||||
content = result_data.content
|
||||
status = "success"
|
||||
|
||||
Reference in New Issue
Block a user