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:
2026-01-25 19:16:36 +08:00
parent 4d7d97ca19
commit 303dd78c54
332 changed files with 6204 additions and 617 deletions

View File

@@ -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__":