feat(aia): Protocol Agent MVP complete with one-click generation and Word export
- Add one-click research protocol generation with streaming output - Implement Word document export via Pandoc integration - Add dynamic dual-panel layout with resizable split pane - Implement collapsible content for StatePanel stages - Add conversation history management with title auto-update - Fix scroll behavior, markdown rendering, and UI layout issues - Simplify conversation creation logic for reliability
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- 健康检查
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
@@ -63,6 +63,8 @@ from services.dc_executor import validate_code, execute_pandas_code
|
||||
# 新增:统一文档处理器(RAG 引擎使用)
|
||||
from services.document_processor import DocumentProcessor, convert_to_markdown
|
||||
from services.pdf_markdown_processor import PdfMarkdownProcessor, extract_pdf_to_markdown
|
||||
# 新增:文档导出服务(Markdown → Word)
|
||||
from services.doc_export_service import check_pandoc_available, convert_markdown_to_docx, create_protocol_docx
|
||||
|
||||
# 兼容:nougat 相关(已废弃,保留空实现避免报错)
|
||||
def check_nougat_available(): return False
|
||||
@@ -243,6 +245,19 @@ class FillnaMiceRequest(BaseModel):
|
||||
random_state: int = 42
|
||||
|
||||
|
||||
class MarkdownToDocxRequest(BaseModel):
|
||||
"""Markdown转Word请求模型"""
|
||||
content: str # Markdown 内容
|
||||
use_template: bool = True # 是否使用模板
|
||||
title: str = "临床研究方案" # 文档标题
|
||||
|
||||
|
||||
class ProtocolToDocxRequest(BaseModel):
|
||||
"""研究方案转Word请求模型"""
|
||||
sections: Dict[str, str] # 章节内容
|
||||
title: str = "临床研究方案" # 文档标题
|
||||
|
||||
|
||||
# ==================== API路由 ====================
|
||||
|
||||
@app.get("/")
|
||||
@@ -2106,6 +2121,160 @@ async def operation_fillna_mice(request: FillnaMiceRequest):
|
||||
}, status_code=400)
|
||||
|
||||
|
||||
# ==================== Word 导出 API ====================
|
||||
|
||||
@app.get("/api/pandoc/status")
|
||||
async def pandoc_status():
|
||||
"""
|
||||
检查 Pandoc 可用性
|
||||
|
||||
Returns:
|
||||
{
|
||||
"available": bool,
|
||||
"version": str,
|
||||
"message": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
result = check_pandoc_available()
|
||||
logger.info(f"Pandoc 状态检查: {result}")
|
||||
return JSONResponse(content=result)
|
||||
except Exception as e:
|
||||
logger.error(f"Pandoc 状态检查失败: {str(e)}")
|
||||
return JSONResponse(content={
|
||||
"available": False,
|
||||
"version": None,
|
||||
"message": f"检查失败: {str(e)}"
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/convert/docx")
|
||||
async def convert_to_docx(request: MarkdownToDocxRequest):
|
||||
"""
|
||||
Markdown 转 Word 接口
|
||||
|
||||
将 Markdown 文本转换为 Word 文档(.docx)
|
||||
|
||||
Args:
|
||||
request: MarkdownToDocxRequest
|
||||
- content: Markdown 内容
|
||||
- use_template: 是否使用模板(默认 True)
|
||||
- title: 文档标题
|
||||
|
||||
Returns:
|
||||
Word 文档二进制数据(application/vnd.openxmlformats-officedocument.wordprocessingml.document)
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始转换 Markdown → Word, 内容长度: {len(request.content)} 字符")
|
||||
|
||||
# 执行转换
|
||||
result = convert_markdown_to_docx(
|
||||
markdown_text=request.content,
|
||||
use_template=request.use_template
|
||||
)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error(f"转换失败: {result.get('error', 'Unknown error')}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=result.get("error", "转换失败")
|
||||
)
|
||||
|
||||
# 读取生成的文件
|
||||
output_path = result["output_path"]
|
||||
with open(output_path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.remove(output_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理临时文件失败: {e}")
|
||||
|
||||
logger.info(f"Markdown → Word 转换成功, 文件大小: {len(content)} bytes")
|
||||
|
||||
# 返回文件
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="document.docx"'
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Markdown → Word 转换失败: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"转换失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/protocol/export/docx")
|
||||
async def export_protocol_to_docx(request: ProtocolToDocxRequest):
|
||||
"""
|
||||
研究方案导出为 Word 接口
|
||||
|
||||
将分章节的研究方案内容导出为格式化的 Word 文档
|
||||
|
||||
Args:
|
||||
request: ProtocolToDocxRequest
|
||||
- sections: 章节内容字典
|
||||
- title: 文档标题
|
||||
|
||||
Returns:
|
||||
Word 文档二进制数据
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始导出研究方案, 章节数: {len(request.sections)}")
|
||||
|
||||
# 执行转换
|
||||
result = create_protocol_docx(
|
||||
sections=request.sections,
|
||||
title=request.title
|
||||
)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error(f"导出失败: {result.get('error', 'Unknown error')}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=result.get("error", "导出失败")
|
||||
)
|
||||
|
||||
# 读取生成的文件
|
||||
output_path = result["output_path"]
|
||||
with open(output_path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.remove(output_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理临时文件失败: {e}")
|
||||
|
||||
logger.info(f"研究方案导出成功, 文件大小: {len(content)} bytes")
|
||||
|
||||
# 返回文件
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="research_protocol.docx"'
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"研究方案导出失败: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"导出失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== 启动配置 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user