deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/persistence/research_templates/sql.py
2026-09-07 18:24:55 +08:00

222 lines
8.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SQLAlchemy-backed research template storage."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.persistence.research_templates.model import ResearchTemplateRow
_UNSET = object()
# 新机器内置范例模板(表为空时由 seed_defaults() 写入;作为给用户的范例,内容务求规范好用)。
# article_type_key 取自默认文章类型(news/analysis/feature/report,见 20260517_02 迁移)。
DEFAULT_RESEARCH_TEMPLATES: list[dict] = [
{
"name": "网摘",
"description": "汇编近期网络热点信息,分条摘录核心要点并标注来源,快速掌握全貌",
"article_type_key": "news",
"material_source": "general",
"word_count": 1000,
"audience": "关注热点动态的一般读者",
"strict_mode": False,
"topic": (
"围绕【主题】检索并汇编近一周网络上的重要信息与热点动态,"
"逐条摘录核心事实、关键数据与原文出处,帮助读者快速掌握全貌。"
),
"outline": (
"一、热点速览(用一句话清单列出本期要点)\n"
"二、重点摘录\n"
" 1.【事件/话题标题】——核心事实、关键数据、信息来源\n"
" 2.【事件/话题标题】——……\n"
" 3.(按重要性依次罗列)\n"
"三、关联动态与背景补充(串联相关信息、补足来龙去脉)\n"
"四、小结与观察(提炼趋势或值得关注的看点)"
),
"sort_order": 0,
},
{
"name": "周报",
"description": "按周梳理工作进展、数据指标与问题风险,输出结构化周度汇报",
"article_type_key": "report",
"material_source": "general",
"word_count": 1200,
"audience": "团队成员与上级主管",
"strict_mode": True,
"topic": (
"汇总本周【项目/团队】的工作进展,撰写周报:"
"覆盖关键成果、数据指标、问题风险与下周计划,简明、客观、可量化。"
),
"outline": (
"一、本周概览(一句话总结 + 关键指标对比上周)\n"
"二、重点进展\n"
" 1. 已完成事项与成果(结论先行,附支撑数据)\n"
" 2. 关键数据 / 里程碑达成情况\n"
"三、问题与风险(逐条列出问题、影响面及应对措施)\n"
"四、下周计划与资源需求(目标、负责人、所需支持)"
),
"sort_order": 1,
},
{
"name": "要讯",
"description": "提炼最重要的资讯,精炼成简报式要点,突出核心事实与潜在影响",
"article_type_key": "analysis",
"material_source": "general",
"word_count": 600,
"audience": "需快速掌握要点的决策者",
"strict_mode": True,
"topic": (
"围绕【主题/领域】提炼近期最重要的资讯,编写要讯:"
"每条聚焦核心事实、关键背景与潜在影响,文字精炼、判断审慎。"
),
"outline": (
"一、头条要讯(1–3 条,每条用一句话点明核心)\n"
"二、要点详述(逐条展开)\n"
" · 事实:发生了什么、何时何地、涉及谁\n"
" · 背景:为何重要、与既有态势的关系\n"
" · 影响:可能带来的后果与连锁反应\n"
"三、研判与建议(给出倾向性判断与可行动建议)"
),
"sort_order": 2,
},
]
class ResearchTemplateRepository:
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._sf = session_factory
@staticmethod
def _row_to_dict(row: ResearchTemplateRow) -> dict:
d = row.to_dict()
for k in ("created_at", "updated_at"):
if isinstance(d.get(k), datetime):
d[k] = d[k].isoformat()
return d
async def create(
self,
*,
name: str,
description: str | None = None,
article_type_key: str | None = None,
outline: str | None = None,
material_source: str = "general",
word_count: int = 800,
audience: str | None = None,
strict_mode: bool = False,
topic: str | None = None,
sort_order: int = 0,
) -> dict:
now = datetime.now(UTC)
row = ResearchTemplateRow(
id=str(uuid.uuid4()),
name=name,
description=description,
article_type_key=article_type_key,
outline=outline,
material_source=material_source,
word_count=word_count,
audience=audience,
strict_mode=strict_mode,
topic=topic,
sort_order=sort_order,
created_at=now,
updated_at=now,
)
async with self._sf() as session:
session.add(row)
await session.commit()
await session.refresh(row)
return self._row_to_dict(row)
async def update(
self,
template_id: str,
*,
name=_UNSET,
description=_UNSET,
article_type_key=_UNSET,
outline=_UNSET,
material_source=_UNSET,
word_count=_UNSET,
audience=_UNSET,
strict_mode=_UNSET,
topic=_UNSET,
sort_order=_UNSET,
) -> dict | None:
async with self._sf() as session:
row = await session.get(ResearchTemplateRow, template_id)
if row is None:
return None
if name is not _UNSET:
row.name = name
if description is not _UNSET:
row.description = description
if article_type_key is not _UNSET:
row.article_type_key = article_type_key
if outline is not _UNSET:
row.outline = outline
if material_source is not _UNSET:
row.material_source = material_source
if word_count is not _UNSET:
row.word_count = word_count
if audience is not _UNSET:
row.audience = audience
if strict_mode is not _UNSET:
row.strict_mode = strict_mode
if topic is not _UNSET:
row.topic = topic
if sort_order is not _UNSET:
row.sort_order = sort_order
row.updated_at = datetime.now(UTC)
await session.commit()
await session.refresh(row)
return self._row_to_dict(row)
async def get(self, template_id: str) -> dict | None:
async with self._sf() as session:
row = await session.get(ResearchTemplateRow, template_id)
if row is None:
return None
return self._row_to_dict(row)
async def list(self) -> list[dict]:
stmt = select(ResearchTemplateRow).order_by(
ResearchTemplateRow.sort_order.asc(), ResearchTemplateRow.created_at.asc()
)
async with self._sf() as session:
result = await session.execute(stmt)
return [self._row_to_dict(r) for r in result.scalars()]
async def delete(self, template_id: str) -> bool:
async with self._sf() as session:
row = await session.get(ResearchTemplateRow, template_id)
if row is None:
return False
await session.delete(row)
await session.commit()
return True
async def seed_defaults(self) -> int:
"""新机器/空表时写入内置范例模板(网摘/周报/要讯)。
幂等:仅当表为空时插入;已有任何模板则跳过(避免覆盖用户配置或重复播种)。
返回插入条数。
"""
async with self._sf() as session:
existing = await session.execute(select(ResearchTemplateRow.id).limit(1))
if existing.first() is not None:
return 0
now = datetime.now(UTC)
for t in DEFAULT_RESEARCH_TEMPLATES:
session.add(
ResearchTemplateRow(id=str(uuid.uuid4()), created_at=now, updated_at=now, **t)
)
await session.commit()
return len(DEFAULT_RESEARCH_TEMPLATES)