添加ai智能体标书接口,修改ai_agent仅暴露一个方法供外部调用
This commit is contained in:
56
AI_Agent.py
56
AI_Agent.py
@ -8,11 +8,10 @@ from docx import Document
|
|||||||
from shutil import which
|
from shutil import which
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from config import OLLAMA_BASE_URL, OLLAMA_MODEL
|
||||||
|
|
||||||
INPUT_WORD = r"C:\Users\YC\Desktop\1.docx" # 你的招标文件
|
INPUT_WORD = r"C:\Users\YC\Desktop\1.docx" # 你的招标文件
|
||||||
OUTPUT_WORD = r"C:\Users\YC\Desktop\投标文件-最终版.docx" # 最终输出路径
|
OUTPUT_WORD = r"C:\Users\YC\Desktop\投标文件-最终版.docx" # 最终输出路径
|
||||||
OLLAMA_MODEL = "alibayram/Qwen3-30B-A3B-Instruct-2507:latest" # 当前最强本地模型
|
|
||||||
OLLAMA_BASE_URL = "http://192.168.110.5:11434"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Ollama 本地调用(支持 128K 上下文 + 长输出)===================
|
# ==================== Ollama 本地调用(支持 128K 上下文 + 长输出)===================
|
||||||
@ -82,7 +81,7 @@ def word_to_md(word_path: str) -> str:
|
|||||||
if not pandoc_cmd:
|
if not pandoc_cmd:
|
||||||
common = [
|
common = [
|
||||||
os.path.expanduser(r"~\AppData\Local\Pandoc\pandoc.exe"),
|
os.path.expanduser(r"~\AppData\Local\Pandoc\pandoc.exe"),
|
||||||
r"C:\Program Files\Pandoc\pandoc.exe",
|
r"./util/pandoc.exe",
|
||||||
]
|
]
|
||||||
for p in common:
|
for p in common:
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
@ -255,26 +254,37 @@ def update_word_toc(docx_path: str):
|
|||||||
print(f"Word目录自动更新失败(可手动右键更新):{e}")
|
print(f"Word目录自动更新失败(可手动右键更新):{e}")
|
||||||
|
|
||||||
|
|
||||||
# ==================== 主流程 ====================
|
# ==================== 统一对外暴露的方法(唯一入口)===================
|
||||||
def main():
|
def generate_tender_from_input(input_word_path: str, output_word_path: str) -> bool:
|
||||||
print("启动本地 Qwen3-30B 投标文件生成器(128K上下文版)\n")
|
"""
|
||||||
|
统一对外暴露的投标文件生成方法
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_word_path: 输入招标文件路径(.docx/.doc)
|
||||||
|
output_word_path: 输出投标文件路径(.docx)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 生成成功返回True,失败返回False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("启动投标文件生成器(统一入口)\n")
|
||||||
os.makedirs("output", exist_ok=True)
|
os.makedirs("output", exist_ok=True)
|
||||||
|
|
||||||
# 1. 转换招标文件
|
# 1. 转换招标文件
|
||||||
tender_md = word_to_md(INPUT_WORD)
|
tender_md = word_to_md(input_word_path)
|
||||||
tender_text = Path(tender_md).read_text(encoding="utf-8")
|
tender_text = Path(tender_md).read_text(encoding="utf-8")
|
||||||
|
|
||||||
# 2. 生成超级详细目录
|
# 2. 生成超级详细目录
|
||||||
outline = generate_full_outline(tender_md)
|
outline = generate_full_outline(tender_md)
|
||||||
|
|
||||||
# 3. 分批生成正文(超长内容
|
# 3. 分批生成正文
|
||||||
content = batch_fill_content(outline, tender_text)
|
content = batch_fill_content(outline, tender_text)
|
||||||
content = expand_to_50000_words(content)
|
content = expand_to_50000_words(content)
|
||||||
|
|
||||||
# 4. 合成最终 Markdown
|
# 4. 合成最终 Markdown
|
||||||
final_md = f"""# 【投标单位全称】
|
final_md = f"""# 【投标单位全称】
|
||||||
|
|
||||||
## {Path(INPUT_WORD).stem} - 投标文件
|
## {Path(input_word_path).stem} - 投标文件
|
||||||
|
|
||||||
{outline}
|
{outline}
|
||||||
|
|
||||||
@ -296,8 +306,9 @@ def main():
|
|||||||
success = False
|
success = False
|
||||||
pandoc_cmd = which("pandoc") or which("pandoc.exe")
|
pandoc_cmd = which("pandoc") or which("pandoc.exe")
|
||||||
if pandoc_cmd and os.path.exists(pandoc_cmd):
|
if pandoc_cmd and os.path.exists(pandoc_cmd):
|
||||||
cmd = [pandoc_cmd, final_md_path, "-o", OUTPUT_WORD, "--reference-doc=template.docx"] if os.path.exists(
|
cmd = [pandoc_cmd, final_md_path, "-o", output_word_path,
|
||||||
"template.docx") else [pandoc_cmd, final_md_path, "-o", OUTPUT_WORD]
|
"--reference-doc=template.docx"] if os.path.exists(
|
||||||
|
"template.docx") else [pandoc_cmd, final_md_path, "-o", output_word_path]
|
||||||
if subprocess.run(cmd, capture_output=True).returncode == 0:
|
if subprocess.run(cmd, capture_output=True).returncode == 0:
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
@ -316,12 +327,27 @@ def main():
|
|||||||
doc.add_heading(l[5:], 3)
|
doc.add_heading(l[5:], 3)
|
||||||
elif l:
|
elif l:
|
||||||
doc.add_paragraph(l)
|
doc.add_paragraph(l)
|
||||||
doc.save(OUTPUT_WORD)
|
doc.save(output_word_path)
|
||||||
|
|
||||||
update_word_toc(OUTPUT_WORD)
|
update_word_toc(output_word_path)
|
||||||
print(f"\n大功告成!投标文件已生成:")
|
print(f"\n大功告成!投标文件已生成:")
|
||||||
print(f" {OUTPUT_WORD}")
|
print(f" {output_word_path}")
|
||||||
print(f" 总字数约:{len(final_md) // 2} 字")
|
print(f" 总字数约:{len(final_md) // 2} 字")
|
||||||
|
|
||||||
|
# 清理临时生成的md文件(可选,保留也可以)
|
||||||
|
if os.path.exists(tender_md):
|
||||||
|
os.remove(tender_md)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"投标文件生成失败:{str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 原有主流程(保持不变,方便单独运行)===================
|
||||||
|
def main():
|
||||||
|
generate_tender_from_input(INPUT_WORD, OUTPUT_WORD)
|
||||||
os.startfile(OUTPUT_WORD)
|
os.startfile(OUTPUT_WORD)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -64,3 +64,7 @@ class DetectionResponse(BaseModel):
|
|||||||
originalImgSize: List[int]
|
originalImgSize: List[int]
|
||||||
targets: List[dict]
|
targets: List[dict]
|
||||||
processing_errors: List[str] = []
|
processing_errors: List[str] = []
|
||||||
|
|
||||||
|
# 智能体相关
|
||||||
|
OLLAMA_MODEL = "alibayram/Qwen3-30B-A3B-Instruct-2507:latest" # 当前最强本地模型
|
||||||
|
OLLAMA_BASE_URL = "http://192.168.110.5:11434"
|
||||||
175
main.py
175
main.py
@ -1,18 +1,30 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import shutil
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException, UploadFile, File
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel, HttpUrl
|
from pydantic import BaseModel, HttpUrl
|
||||||
|
|
||||||
|
from AI_Agent import generate_tender_from_input
|
||||||
from config import DetectionResponse
|
from config import DetectionResponse
|
||||||
from process import detect_large_image_from_url
|
from process import detect_large_image_from_url
|
||||||
|
|
||||||
|
# 配置文件存储路径
|
||||||
|
UPLOAD_DIR = Path("uploaded_files")
|
||||||
|
OUTPUT_DIR = Path("generated_tenders")
|
||||||
|
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# 全局检测管理器
|
# 全局检测管理器
|
||||||
detector_manager = None
|
detector_manager = None
|
||||||
|
|
||||||
@ -28,9 +40,17 @@ async def lifespan(app: FastAPI):
|
|||||||
print(f"初始化失败:{str(e)}")
|
print(f"初始化失败:{str(e)}")
|
||||||
raise
|
raise
|
||||||
yield
|
yield
|
||||||
|
# 程序关闭时清理临时文件(可选)
|
||||||
|
print("清理临时文件...")
|
||||||
|
for file in UPLOAD_DIR.glob("*"):
|
||||||
|
try:
|
||||||
|
if file.is_file():
|
||||||
|
file.unlink()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"清理文件 {file} 失败:{e}")
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan, title="目标检测API", version="1.0.0")
|
app = FastAPI(lifespan=lifespan, title="目标检测与投标文件生成API", version="1.0.0")
|
||||||
|
|
||||||
# 配置跨域请求
|
# 配置跨域请求
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@ -51,6 +71,15 @@ class DetectionProcessRequest(BaseModel):
|
|||||||
url: HttpUrl
|
url: HttpUrl
|
||||||
|
|
||||||
|
|
||||||
|
class TenderGenerateResponse(BaseModel):
|
||||||
|
"""投标文件生成响应模型"""
|
||||||
|
status: str
|
||||||
|
message: str
|
||||||
|
relative_path: str
|
||||||
|
file_name: str
|
||||||
|
file_size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/detect_image", response_model=DetectionResponse)
|
@app.post("/detect_image", response_model=DetectionResponse)
|
||||||
async def run_detection_image(request: DetectionRequest):
|
async def run_detection_image(request: DetectionRequest):
|
||||||
# 解析检测类型
|
# 解析检测类型
|
||||||
@ -114,12 +143,154 @@ async def get_supported_types():
|
|||||||
return {"supported_types": []}
|
return {"supported_types": []}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/generate_tender", response_model=TenderGenerateResponse)
|
||||||
|
async def generate_tender_file(file: UploadFile = File(...)):
|
||||||
|
"""
|
||||||
|
上传招标文件(Word格式),生成投标文件
|
||||||
|
支持的文件格式:.docx
|
||||||
|
返回生成文件的相对路径,用于下载接口
|
||||||
|
"""
|
||||||
|
upload_path = None
|
||||||
|
|
||||||
|
# 验证文件类型
|
||||||
|
if not file.filename.endswith((".docx", ".doc")):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="仅支持Word文件(.docx或.doc格式)"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 保存上传的文件
|
||||||
|
timestamp = int(time.time())
|
||||||
|
file_ext = Path(file.filename).suffix
|
||||||
|
upload_filename = f"tender_{timestamp}{file_ext}"
|
||||||
|
upload_path = UPLOAD_DIR / upload_filename
|
||||||
|
|
||||||
|
# 保存文件内容
|
||||||
|
with open(upload_path, "wb") as f:
|
||||||
|
shutil.copyfileobj(file.file, f)
|
||||||
|
|
||||||
|
print(f"已接收上传文件:{upload_path}")
|
||||||
|
|
||||||
|
# 生成输出文件名和路径
|
||||||
|
output_filename = f"投标文件_生成版_{timestamp}.docx"
|
||||||
|
output_path = OUTPUT_DIR / output_filename
|
||||||
|
|
||||||
|
# 调用AI_Agent的统一入口方法(仅这一个调用)
|
||||||
|
print("开始生成投标文件...")
|
||||||
|
generate_success = generate_tender_from_input(
|
||||||
|
input_word_path=str(upload_path),
|
||||||
|
output_word_path=str(output_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not generate_success:
|
||||||
|
raise Exception("投标文件生成核心流程执行失败")
|
||||||
|
|
||||||
|
# 验证生成的文件是否存在
|
||||||
|
if not output_path.exists() or not output_path.is_file():
|
||||||
|
raise Exception("投标文件生成后未找到目标文件")
|
||||||
|
|
||||||
|
# 计算文件大小
|
||||||
|
file_size = output_path.stat().st_size
|
||||||
|
|
||||||
|
# 构建相对路径(相对于项目根目录)
|
||||||
|
relative_path = str(output_path.relative_to(Path.cwd()))
|
||||||
|
|
||||||
|
print(f"投标文件生成成功:{output_path}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "投标文件生成完成",
|
||||||
|
"relative_path": relative_path,
|
||||||
|
"file_name": output_filename,
|
||||||
|
"file_size": file_size
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"生成投标文件失败:{str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=error_msg
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# 关闭上传文件流
|
||||||
|
await file.close()
|
||||||
|
# 清理上传的原始文件,节省空间
|
||||||
|
if upload_path and upload_path.exists():
|
||||||
|
try:
|
||||||
|
upload_path.unlink()
|
||||||
|
print(f"已清理上传文件:{upload_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"清理上传文件失败:{e}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/download_file")
|
||||||
|
async def download_generated_file(relative_path: str):
|
||||||
|
"""
|
||||||
|
根据相对路径下载生成的投标文件
|
||||||
|
Args:
|
||||||
|
relative_path: 生成文件的相对路径(从/generate_tender接口获取)
|
||||||
|
"""
|
||||||
|
# 解析绝对路径,防止路径穿越攻击
|
||||||
|
try:
|
||||||
|
abs_path = Path(relative_path).resolve()
|
||||||
|
|
||||||
|
# 验证路径是否在允许的输出目录内
|
||||||
|
if not abs_path.is_relative_to(OUTPUT_DIR.resolve()):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="访问禁止:文件路径不在允许范围内"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not abs_path.exists() or not abs_path.is_file():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="文件不存在或已被删除"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回文件下载响应
|
||||||
|
return FileResponse(
|
||||||
|
path=abs_path,
|
||||||
|
filename=abs_path.name,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"文件下载失败:{str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"文件下载失败:{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/list_generated_files")
|
||||||
|
async def list_generated_files():
|
||||||
|
"""列出所有生成的投标文件(可选接口)"""
|
||||||
|
files = []
|
||||||
|
for file in OUTPUT_DIR.glob("*.docx"):
|
||||||
|
files.append({
|
||||||
|
"file_name": file.name,
|
||||||
|
"relative_path": str(file.relative_to(Path.cwd())),
|
||||||
|
"file_size": file.stat().st_size,
|
||||||
|
"created_time": file.stat().st_ctime
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"total": len(files),
|
||||||
|
"files": files
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
# 补充必要的导入(在主函数中导入,避免启动时依赖冲突)
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--host", default="0.0.0.0")
|
parser.add_argument("--host", default="0.0.0.0")
|
||||||
parser.add_argument("--port", type=int, default=8000)
|
parser.add_argument("--port", type=int, default=8000)
|
||||||
parser.add_argument("--reload", action="store_true")
|
parser.add_argument("--reload", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
uvicorn.run("main:app", host=args.host, port=args.port, reload=args.reload)
|
uvicorn.run("main:app", host=args.host, port=args.port, reload=args.reload)
|
||||||
Reference in New Issue
Block a user