148 lines
4.5 KiB
Python
148 lines
4.5 KiB
Python
|
|
"""用于把模型生成的 Markdown 摘要做轻量解析并用更产品化的方式展示。
|
|||
|
|
|
|||
|
|
说明:不引入复杂依赖,不要求模型输出 JSON。
|
|||
|
|
通过解析固定标题:
|
|||
|
|
- # 一句话结论
|
|||
|
|
- # 关键决策要点(Top 5)
|
|||
|
|
- # 主要风险(Top 3)
|
|||
|
|
- # 下一步行动清单
|
|||
|
|
- # 需要进一步澄清的问题
|
|||
|
|
|
|||
|
|
另外提供:
|
|||
|
|
- render_opinion_preview:观点一句话预览
|
|||
|
|
- summarize_round_opinions:把一轮的多角色观点压缩为可扫读摘要(用于时间线)
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
import streamlit as st
|
|||
|
|
|
|||
|
|
|
|||
|
|
SECTION_TITLES = [
|
|||
|
|
"一句话结论",
|
|||
|
|
"关键决策要点(Top 5)",
|
|||
|
|
"主要风险(Top 3)",
|
|||
|
|
"下一步行动清单",
|
|||
|
|
"需要进一步澄清的问题",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _split_sections(md: str) -> dict[str, str]:
|
|||
|
|
if not md:
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
pattern = r"^#\s+(.*)$"
|
|||
|
|
lines = md.splitlines()
|
|||
|
|
sections: dict[str, list[str]] = {}
|
|||
|
|
|
|||
|
|
current = None
|
|||
|
|
for line in lines:
|
|||
|
|
m = re.match(pattern, line.strip())
|
|||
|
|
if m:
|
|||
|
|
title = m.group(1).strip()
|
|||
|
|
current = title
|
|||
|
|
sections.setdefault(current, [])
|
|||
|
|
continue
|
|||
|
|
if current is not None:
|
|||
|
|
sections[current].append(line)
|
|||
|
|
|
|||
|
|
return {k: "\n".join(v).strip() for k, v in sections.items()}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_decision_badge(one_liner: str) -> tuple[str, str]:
|
|||
|
|
t = (one_liner or "").strip()
|
|||
|
|
if any(k in t for k in ["不建议", "否决", "不推荐"]):
|
|||
|
|
return ("不建议", "error")
|
|||
|
|
if any(k in t for k in ["谨慎", "有条件", "需要修改"]):
|
|||
|
|
return ("谨慎推进", "warning")
|
|||
|
|
if any(k in t for k in ["推荐", "可行", "建议推进"]):
|
|||
|
|
return ("推荐", "success")
|
|||
|
|
return ("结论", "info")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def render_decision_summary(md: str) -> None:
|
|||
|
|
"""以更产品化的方式渲染结论摘要。"""
|
|||
|
|
sections = _split_sections(md)
|
|||
|
|
|
|||
|
|
one_liner = sections.get("一句话结论", "").strip()
|
|||
|
|
badge_text, badge_kind = _extract_decision_badge(one_liner)
|
|||
|
|
|
|||
|
|
col1, col2 = st.columns([1, 5])
|
|||
|
|
with col1:
|
|||
|
|
if badge_kind == "success":
|
|||
|
|
st.success(badge_text)
|
|||
|
|
elif badge_kind == "warning":
|
|||
|
|
st.warning(badge_text)
|
|||
|
|
elif badge_kind == "error":
|
|||
|
|
st.error(badge_text)
|
|||
|
|
else:
|
|||
|
|
st.info(badge_text)
|
|||
|
|
|
|||
|
|
with col2:
|
|||
|
|
if one_liner:
|
|||
|
|
st.markdown(f"**{one_liner}**")
|
|||
|
|
else:
|
|||
|
|
st.markdown("**(未识别到一句话结论,请检查模型输出格式)**")
|
|||
|
|
|
|||
|
|
st.markdown("---")
|
|||
|
|
|
|||
|
|
c1, c2 = st.columns(2)
|
|||
|
|
|
|||
|
|
with c1:
|
|||
|
|
st.subheader("🎯 关键决策要点")
|
|||
|
|
content = sections.get("关键决策要点(Top 5)", "").strip()
|
|||
|
|
st.markdown(content if content else "(暂无)")
|
|||
|
|
|
|||
|
|
st.subheader("✅ 下一步行动")
|
|||
|
|
actions = sections.get("下一步行动清单", "").strip()
|
|||
|
|
items = [
|
|||
|
|
re.sub(r"^-\s*\[.\]\s*", "", l).strip()
|
|||
|
|
for l in actions.splitlines()
|
|||
|
|
if l.strip().startswith("-")
|
|||
|
|
]
|
|||
|
|
if items:
|
|||
|
|
for i, it in enumerate(items):
|
|||
|
|
st.checkbox(it, value=False, key=f"action_{i}")
|
|||
|
|
else:
|
|||
|
|
st.markdown(actions if actions else "(暂无)")
|
|||
|
|
|
|||
|
|
with c2:
|
|||
|
|
st.subheader("⚠️ 主要风险")
|
|||
|
|
risks = sections.get("主要风险(Top 3)", "").strip()
|
|||
|
|
st.markdown(risks if risks else "(暂无)")
|
|||
|
|
|
|||
|
|
st.subheader("❓ 需要澄清")
|
|||
|
|
qs = sections.get("需要进一步澄清的问题", "").strip()
|
|||
|
|
st.markdown(qs if qs else "(暂无)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def render_opinion_preview(opinion: str, limit: int = 160) -> str:
|
|||
|
|
"""返回用于预览的短文本。"""
|
|||
|
|
if not opinion:
|
|||
|
|
return ""
|
|||
|
|
t = opinion.strip().replace("\n", " ")
|
|||
|
|
if len(t) <= limit:
|
|||
|
|
return t
|
|||
|
|
return t[:limit].rstrip() + "..."
|
|||
|
|
|
|||
|
|
|
|||
|
|
def summarize_round_opinions(opinions: dict, role_descriptions: dict, limit_per_role: int = 60) -> str:
|
|||
|
|
"""把一轮 opinions 生成适合扫读的摘要(不调用模型,避免额外成本与不稳定)。
|
|||
|
|
|
|||
|
|
输出格式示例:
|
|||
|
|
- 📊 产品经理:xxx...
|
|||
|
|
- 💻 技术专家:xxx...
|
|||
|
|
"""
|
|||
|
|
if not opinions:
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
lines: list[str] = []
|
|||
|
|
for role, text in opinions.items():
|
|||
|
|
info = role_descriptions.get(role, {"icon": "👤", "name": role})
|
|||
|
|
preview = render_opinion_preview(str(text or ""), limit=limit_per_role)
|
|||
|
|
if not preview:
|
|||
|
|
preview = "(本轮未生成/生成失败)"
|
|||
|
|
lines.append(f"- {info['icon']} {info['name']}:{preview}")
|
|||
|
|
|
|||
|
|
return "\n".join(lines)
|