中期成果提交:PPT自动生成 Agent(感知-规划-行动-记忆闭环 + aura-ppt 渲染引擎 + Web 交互界面 + 测试用例 + AI 使用日志)

This commit is contained in:
T1-AISUYISIN
2026-08-31 09:53:11 +08:00
commit 883a89682b
50 changed files with 10355 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""LLM 接入集成测试:规划/标题/意图/禁止降级/长文档两级规划。
用法:python tests/e2e_llm.py
"""
import os
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
os.environ["PLAN_CHUNK_CHARS"] = "2500" # 压低阈值,确保长文档用例真正走分段通读
from src.agent import llm # noqa: E402
from src.agent.perception import GenerateConfig # noqa: E402
from src.agent import planning as P # noqa: E402
from src.agent.planning import build_plan, PlanningError # noqa: E402
from src.agent.chat import parse_intent_llm, parse_intent # noqa: E402
SAMPLE = """# 项目背景
为提升知识周转效率启动平台建设,前期调研两个月
# 本期成果
部署完成基础服务,导入文档120份
混合检索准确率较基线提升明显
完成三个部门试点接入
# 下一步
扩大试点范围,建立运营机制
"""
failures = []
def check(name, cond, extra=""):
print(f" [{'OK' if cond else 'NG'}] {name} {extra}")
if not cond:
failures.append(name)
def make_long_doc(sections=40):
"""构造约 1.5-2 万字符的多节长文档(含数字,验证分段通读与页数硬约束)。"""
parts = []
for i in range(1, sections + 1):
parts.append(f"# 第{i}章节 阶段性工作汇报")
parts.append(f"本阶段完成任务{i}的设计与开发,投入人力3人,周期2周")
parts.append(f"完成度达到{i*2}%,质量抽检合格率98%")
parts.append("- 关键交付物已通过评审")
parts.append("- 遗留问题已登记跟踪")
parts.append("下一步将推进与业务系统的对接联调")
return "\n".join(parts)
def main():
print("== 0. 禁止降级:LLM 失败必须报错而非照搬 ==")
cfg_nf = GenerateConfig(title="降级测试", user_id=1, content=SAMPLE,
page_min=5, page_max=8)
orig_chat = llm.chat_json
try:
llm.chat_json = lambda *a, **k: None
try:
build_plan(cfg_nf)
check("no silent fallback", False, "未抛出 PlanningError")
except PlanningError as e:
check("no silent fallback", True, str(e)[:50])
finally:
llm.chat_json = orig_chat
ok = llm.ensure_backend()
check("llm backend", ok)
print("== 1. LLM 内容规划(短文档直通) ==")
t = time.time()
cfg = GenerateConfig(title="知识平台周报", user_id=1, content=SAMPLE,
scene="report", language="zh", page_min=6, page_max=9)
r = build_plan(cfg)
plan = r.plan
slides = plan["slides"]
print(f" mode={r.mode} pages={len(slides)} 耗时={time.time()-t:.0f}s")
check("llm mode used", r.mode == "llm")
check("page count in range", 6 <= len(slides) <= 9, f"n={len(slides)}")
check("cover first / end last", slides[0]["type"] == "cover" and slides[-1]["type"] == "end")
notes_n = sum(1 for s in slides if s.get("notes"))
check("per-slide notes", notes_n >= len(slides) - 2, f"notes={notes_n}")
generic = sum(1 for s in slides if s["title"] in ("",) or s["title"].startswith("要点"))
check("no generic titles", generic == 0)
def avg_len(sl):
bl = [len(b) for x in sl if x["type"] == "content" for b in x.get("content", [])]
return sum(bl) / max(len(bl), 1)
check("full-sentence bullets (short doc)", avg_len(slides) >= 12,
f"avg={avg_len(slides):.0f}")
for s in slides:
print(f" - [{s['type']:8s}] {s['title'][:24]} notes={'Y' if s.get('notes') else '-'}")
print("== 2. 长文档两级规划(分段通读→汇总大纲) ==")
long_doc = make_long_doc()
t = time.time()
cfg2 = GenerateConfig(title="季度工作总结", user_id=1, content=long_doc,
scene="report", language="zh", page_min=10, page_max=15)
r2 = build_plan(cfg2)
slides2 = r2.plan["slides"]
n2 = len(slides2)
print(f" mode={r2.mode} pages={n2} 源={len(long_doc)}字 耗时={time.time()-t:.0f}s")
check("long doc page hard cap", n2 <= 15 and n2 >= 4, f"n={n2}")
notes2 = sum(1 for s in slides2 if s.get("notes"))
check("long doc has notes", notes2 >= n2 - 2, f"notes={notes2}")
covered = sum(1 for i in range(1, 41) if str(i) in "".join(
s["title"] + "".join(s["content"]) for s in slides2))
check("late sections represented", covered >= 3, f"covered_sections={covered}")
check("full-sentence bullets (long doc)", avg_len(slides2) >= 12,
f"avg={avg_len(slides2):.0f}")
print("== 3. 智能标题 ==")
t = time.time()
data = llm.chat_json(
"根据以下 PPT 内容拟 3 个标题(每个不超过 20 字)。"
'输出 JSON {"titles": ["..."]}。\n\n' + SAMPLE)
titles = [str(x).strip() for x in (data or {}).get("titles", []) if str(x).strip()]
print(f" titles={titles} 耗时={time.time()-t:.0f}s")
check("title suggest", len(titles) >= 1)
print("== 4. LLM 意图解析(规则无法处理的句式) ==")
msg = '第2页末尾添加一条要点:风险与依赖已同步全组'
rule = parse_intent(msg, {})
llm_it = parse_intent_llm(msg, plan)
check("rule cannot parse (no quotes)", rule["type"] == "unknown", str(rule))
check("llm parses add", llm_it and llm_it.get("type") == "edit_page"
and llm_it.get("op") == "add" and llm_it.get("page_no") == 2,
str(llm_it))
msg2 = "把标题改成 AI 知识平台建设汇报"
it2 = parse_intent_llm(msg2, plan)
check("llm parses title", it2 and it2.get("type") == "change_title"
and "知识平台" in it2.get("title", ""), str(it2))
print("== 5. 渲染 LLM plan(含 notes ==")
import json
import tempfile
from src.engine_bridge import render_plan, verify_output
tmp = Path(tempfile.mkdtemp(prefix="llm_plan_"))
p = tmp / "plan.json"
p.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
out = tmp / "out.pptx"
render_plan(p, out)
vok, detail = verify_output(out, p)
check("render+verify llm plan", vok, detail[:60])
print("\n" + ("LLM E2E ALL PASSED" if not failures else "FAILURES: " + "; ".join(failures)))
sys.exit(1 if failures else 0)
if __name__ == "__main__":
main()