添加ai智能体标书接口,修改ai_agent仅暴露一个方法供外部调用
This commit is contained in:
136
AI_Agent.py
136
AI_Agent.py
@ -8,11 +8,10 @@ from docx import Document
|
||||
from shutil import which
|
||||
import requests
|
||||
|
||||
from config import OLLAMA_BASE_URL, OLLAMA_MODEL
|
||||
|
||||
INPUT_WORD = r"C:\Users\YC\Desktop\1.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 上下文 + 长输出)===================
|
||||
@ -82,7 +81,7 @@ def word_to_md(word_path: str) -> str:
|
||||
if not pandoc_cmd:
|
||||
common = [
|
||||
os.path.expanduser(r"~\AppData\Local\Pandoc\pandoc.exe"),
|
||||
r"C:\Program Files\Pandoc\pandoc.exe",
|
||||
r"./util/pandoc.exe",
|
||||
]
|
||||
for p in common:
|
||||
if os.path.exists(p):
|
||||
@ -110,7 +109,7 @@ def word_to_md(word_path: str) -> str:
|
||||
# ==================== 新版:两步生成超级目录(永不超时)===================
|
||||
def generate_full_outline(tender_md: str) -> str:
|
||||
tender_text = Path(tender_md).read_text(encoding="utf-8")
|
||||
print(f"招标文件共 {len(tender_text)//2} 字,开始两阶段生成四级目录...")
|
||||
print(f"招标文件共 {len(tender_text) // 2} 字,开始两阶段生成四级目录...")
|
||||
|
||||
# 第一步:先让模型只看前 6 万字,生成一个【简洁但完整】的三级目录(超快,10秒内出)
|
||||
prompt1 = f"""请仔细阅读以下招标文件核心内容,只输出一个简洁但完整的三级目录(一级用“一、”,二级用“1、”,三级用“1.1、”)。
|
||||
@ -143,7 +142,7 @@ def generate_full_outline(tender_md: str) -> str:
|
||||
# 每 8个三级标题为一组,展开四级(稳到爆)
|
||||
full_outline = outline_skeleton + "\n"
|
||||
for i in range(0, len(level3_titles), 8):
|
||||
batch = level3_titles[i:i+8]
|
||||
batch = level3_titles[i:i + 8]
|
||||
batch_text = "\n".join(batch)
|
||||
|
||||
prompt2 = f"""你是一位招投标专家,请把下面这几个三级标题分别展开成 10~18 个专业四级标题(格式必须是 1.1.1、1.1.2、……)。
|
||||
@ -157,7 +156,7 @@ def generate_full_outline(tender_md: str) -> str:
|
||||
|
||||
直接输出四级标题:"""
|
||||
|
||||
print(f" 正在展开第 {i//8 + 1} 组四级标题({len(batch)}个)...")
|
||||
print(f" 正在展开第 {i // 8 + 1} 组四级标题({len(batch)}个)...")
|
||||
level4_text = call_llm([{"role": "user", "content": prompt2}],
|
||||
temperature=0.2, max_tokens=20000)
|
||||
full_outline += "\n" + level4_text + "\n"
|
||||
@ -165,7 +164,7 @@ def generate_full_outline(tender_md: str) -> str:
|
||||
|
||||
# 保存并返回
|
||||
Path("output/四级目录.md").write_text(full_outline, encoding="utf-8")
|
||||
print(f"超级四级目录生成成功!总计约 {len(full_outline)//2} 字(再也不怕超时了!)")
|
||||
print(f"超级四级目录生成成功!总计约 {len(full_outline) // 2} 字(再也不怕超时了!)")
|
||||
return full_outline
|
||||
|
||||
|
||||
@ -255,26 +254,37 @@ def update_word_toc(docx_path: str):
|
||||
print(f"Word目录自动更新失败(可手动右键更新):{e}")
|
||||
|
||||
|
||||
# ==================== 主流程 ====================
|
||||
def main():
|
||||
print("启动本地 Qwen3-30B 投标文件生成器(128K上下文版)\n")
|
||||
os.makedirs("output", exist_ok=True)
|
||||
# ==================== 统一对外暴露的方法(唯一入口)===================
|
||||
def generate_tender_from_input(input_word_path: str, output_word_path: str) -> bool:
|
||||
"""
|
||||
统一对外暴露的投标文件生成方法
|
||||
|
||||
# 1. 转换招标文件
|
||||
tender_md = word_to_md(INPUT_WORD)
|
||||
tender_text = Path(tender_md).read_text(encoding="utf-8")
|
||||
Args:
|
||||
input_word_path: 输入招标文件路径(.docx/.doc)
|
||||
output_word_path: 输出投标文件路径(.docx)
|
||||
|
||||
# 2. 生成超级详细目录
|
||||
outline = generate_full_outline(tender_md)
|
||||
Returns:
|
||||
bool: 生成成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
print("启动投标文件生成器(统一入口)\n")
|
||||
os.makedirs("output", exist_ok=True)
|
||||
|
||||
# 3. 分批生成正文(超长内容
|
||||
content = batch_fill_content(outline, tender_text)
|
||||
content = expand_to_50000_words(content)
|
||||
# 1. 转换招标文件
|
||||
tender_md = word_to_md(input_word_path)
|
||||
tender_text = Path(tender_md).read_text(encoding="utf-8")
|
||||
|
||||
# 4. 合成最终 Markdown
|
||||
final_md = f"""# 【投标单位全称】
|
||||
# 2. 生成超级详细目录
|
||||
outline = generate_full_outline(tender_md)
|
||||
|
||||
## {Path(INPUT_WORD).stem} - 投标文件
|
||||
# 3. 分批生成正文
|
||||
content = batch_fill_content(outline, tender_text)
|
||||
content = expand_to_50000_words(content)
|
||||
|
||||
# 4. 合成最终 Markdown
|
||||
final_md = f"""# 【投标单位全称】
|
||||
|
||||
## {Path(input_word_path).stem} - 投标文件
|
||||
|
||||
{outline}
|
||||
|
||||
@ -287,41 +297,57 @@ def main():
|
||||
- 类似业绩证明材料
|
||||
- 偏离表
|
||||
"""
|
||||
final_md_path = "output/最终投标文件.md"
|
||||
Path(final_md_path).write_text(final_md, encoding="utf-8")
|
||||
print(f"\n最终 Markdown 生成成功!总计约 {len(final_md) // 2} 字")
|
||||
final_md_path = "output/最终投标文件.md"
|
||||
Path(final_md_path).write_text(final_md, encoding="utf-8")
|
||||
print(f"\n最终 Markdown 生成成功!总计约 {len(final_md) // 2} 字")
|
||||
|
||||
# 5. 转 Word(三保险)
|
||||
print("正在转换为 Word 文档...")
|
||||
success = False
|
||||
pandoc_cmd = which("pandoc") or which("pandoc.exe")
|
||||
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(
|
||||
"template.docx") else [pandoc_cmd, final_md_path, "-o", OUTPUT_WORD]
|
||||
if subprocess.run(cmd, capture_output=True).returncode == 0:
|
||||
success = True
|
||||
# 5. 转 Word(三保险)
|
||||
print("正在转换为 Word 文档...")
|
||||
success = False
|
||||
pandoc_cmd = which("pandoc") or which("pandoc.exe")
|
||||
if pandoc_cmd and os.path.exists(pandoc_cmd):
|
||||
cmd = [pandoc_cmd, final_md_path, "-o", output_word_path,
|
||||
"--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:
|
||||
success = True
|
||||
|
||||
if not success:
|
||||
print("Pandoc 失败,使用 python-docx 强制生成...")
|
||||
doc = Document()
|
||||
for line in final_md.split('\n'):
|
||||
l = line.strip()
|
||||
if l.startswith("# "):
|
||||
doc.add_heading(l[2:], 0)
|
||||
elif l.startswith("## "):
|
||||
doc.add_heading(l[3:], 1)
|
||||
elif l.startswith("### "):
|
||||
doc.add_heading(l[4:], 2)
|
||||
elif l.startswith("#### "):
|
||||
doc.add_heading(l[5:], 3)
|
||||
elif l:
|
||||
doc.add_paragraph(l)
|
||||
doc.save(OUTPUT_WORD)
|
||||
if not success:
|
||||
print("Pandoc 失败,使用 python-docx 强制生成...")
|
||||
doc = Document()
|
||||
for line in final_md.split('\n'):
|
||||
l = line.strip()
|
||||
if l.startswith("# "):
|
||||
doc.add_heading(l[2:], 0)
|
||||
elif l.startswith("## "):
|
||||
doc.add_heading(l[3:], 1)
|
||||
elif l.startswith("### "):
|
||||
doc.add_heading(l[4:], 2)
|
||||
elif l.startswith("#### "):
|
||||
doc.add_heading(l[5:], 3)
|
||||
elif l:
|
||||
doc.add_paragraph(l)
|
||||
doc.save(output_word_path)
|
||||
|
||||
update_word_toc(OUTPUT_WORD)
|
||||
print(f"\n大功告成!投标文件已生成:")
|
||||
print(f" {OUTPUT_WORD}")
|
||||
print(f" 总字数约:{len(final_md) // 2} 字")
|
||||
update_word_toc(output_word_path)
|
||||
print(f"\n大功告成!投标文件已生成:")
|
||||
print(f" {output_word_path}")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@ -64,3 +64,7 @@ class DetectionResponse(BaseModel):
|
||||
originalImgSize: List[int]
|
||||
targets: List[dict]
|
||||
processing_errors: List[str] = []
|
||||
|
||||
# 智能体相关
|
||||
OLLAMA_MODEL = "alibayram/Qwen3-30B-A3B-Instruct-2507:latest" # 当前最强本地模型
|
||||
OLLAMA_BASE_URL = "http://192.168.110.5:11434"
|
||||
177
main.py
177
main.py
@ -1,18 +1,30 @@
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
import shutil
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import uvicorn
|
||||
from PIL import Image
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
from AI_Agent import generate_tender_from_input
|
||||
from config import DetectionResponse
|
||||
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
|
||||
|
||||
@ -28,9 +40,17 @@ async def lifespan(app: FastAPI):
|
||||
print(f"初始化失败:{str(e)}")
|
||||
raise
|
||||
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(
|
||||
@ -51,6 +71,15 @@ class DetectionProcessRequest(BaseModel):
|
||||
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)
|
||||
async def run_detection_image(request: DetectionRequest):
|
||||
# 解析检测类型
|
||||
@ -114,12 +143,154 @@ async def get_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__":
|
||||
import argparse
|
||||
|
||||
# 补充必要的导入(在主函数中导入,避免启动时依赖冲突)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
parser.add_argument("--reload", action="store_true")
|
||||
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