diff --git a/backend/app/api/copilot.py b/backend/app/api/copilot.py index a5d0cb0d..8abc6ed2 100644 --- a/backend/app/api/copilot.py +++ b/backend/app/api/copilot.py @@ -289,6 +289,10 @@ NODE_REFERENCE = """\ 4. 不要用 transform 解析 agent / llm 的文字:口径卡读 cell(nodes.查库节点id, 行, '列名') 或 vars..字段 5. 数据在两个库、要按行对齐时:两个 tool 节点各自聚合到相同的键和粒度 → merge 按键合并 → 口径卡算派生指标 → report。 同一个库写成一条 SQL;只组合几个单值用口径卡的 cell();不要用 code 节点合并(出处会断,喂给口径卡时核对不了) +6. 问题里的「今天、昨天、上周、本月、去年」这类相对日期一律按运行当天算:SQL 用数据源方言里取当天的写法(CURRENT_DATE、 + CURDATE()、date('now')、SYSDATE),agent 先调 current_time 拿到当天日期再写条件(agent 的 prompt 里写明这一点、tools 绑上 + current_time)。不要用表里最大的日期(MAX(日期))去推「上周」「本月」:数据没更新到那段时间时,推出来的是另一段,报告还会 + 把它当成上周来写。想让报告说清数据最新到哪天,另取一列 MAX(日期) AS 数据最新日期 模型字段(llm / agent / supervisor / report 的 config.model): - **不要填**。留空表示跟随当前供应商的默认模型,这几乎总是对的。 diff --git a/backend/app/engine/evidence.py b/backend/app/engine/evidence.py index 02f3c2da..f11bdec5 100644 --- a/backend/app/engine/evidence.py +++ b/backend/app/engine/evidence.py @@ -50,6 +50,7 @@ import unicodedata from collections import Counter from collections.abc import Callable, Iterable from dataclasses import dataclass, field +from datetime import date, timedelta from decimal import ROUND_HALF_UP, Context, Decimal from typing import Any, Literal, TypedDict @@ -3306,6 +3307,24 @@ SEE_AND_CODE_RULES = ( "(「状态为 9」「类型 1」):码值是没有出处的数字,系统会当成裸数字打回重写。" ) +#: 相对日期(「今天、昨天、上周、本月、去年」)一律按运行当天算(period_rule)。部署上撞过:数据只更新到两周前,查库时拿表里 +#: 最大的日期往前推七天当「上周」,写作者照着写成「上周产量……」。写作者不知道今天是哪天,就分不出数据有没有覆盖问的那段, +#: 所以把运行当天和上周的起止日期直接告诉它:没覆盖时先照实说、写明数据最新到哪天;最近一段的数可以给,但写具体起止日期 +PERIOD_RULE = ( + "相对日期:今天是 {today}({weekday}),上周是 {week_start} 至 {week_end}。问题里的「今天、昨天、上周、本月、去年」" + "这类说法一律按今天算,不按数据里最新的日期推。查到的数据没覆盖问的那段时间时,先照实说没覆盖、数据最新到哪天" + "(写具体日期);可以接着给出数据里最近一段的数,但写明它的起止日期,不要把它叫作「上周」「本月」这类相对的说法。" +) +_WEEKDAYS = ("周一", "周二", "周三", "周四", "周五", "周六", "周日") + + +def period_rule(today: date) -> str: + """写作规则里讲相对日期的那一条(PERIOD_RULE),按运行当天 today 填好:上周是上周一到上周日。""" + monday = today - timedelta(days=today.weekday() + 7) + return PERIOD_RULE.format(today=today.isoformat(), weekday=_WEEKDAYS[today.weekday()], + week_start=monday.isoformat(), week_end=(monday + timedelta(days=6)).isoformat()) + + #: 句中的 [[see:]]:前面是介词、后面接着句子的写法(「以 [[see:Q1]] 为依据」「根据 [[see:Q1]],」「详见 [[see:Q1]]」)。 #: 只认这几种拿得准的:「以」前面是「所、可、加……」的是另一个词(所以、可以),不算;「依据 [[see:…]]。」可能是 #: 「这是结论的依据」,不算。认出来就交回写作者重写(report.REWRITE_ONLY:重写不好也不判失败) diff --git a/backend/app/engine/nodes/report.py b/backend/app/engine/nodes/report.py index cb9b5b4f..322fe5ab 100644 --- a/backend/app/engine/nodes/report.py +++ b/backend/app/engine/nodes/report.py @@ -38,6 +38,7 @@ import re import time from collections import Counter from collections.abc import Iterable, Mapping +from datetime import date, datetime from typing import Any from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage @@ -56,6 +57,7 @@ from app.engine.evidence import ( ENTITY_KINDS, MARKER_RULES, SEE_AND_CODE_RULES, + period_rule, StreamRenderer, WriterCatalog, build_catalog, @@ -634,12 +636,17 @@ def _repair_request(violations: list[dict[str, Any]], failed_rule: str | None = ) +def run_today() -> date: + """写作规则里的「今天」(period_rule):服务端所在时区的日期,和查库时 CURRENT_DATE 一类按运行当天现算的写法同一天。""" + return datetime.now().astimezone().date() + + def _messages(ctx: NodeContext, state: GraphState, writer: WriterCatalog, claims: str = "off", failed: str | None = None) -> list[BaseMessage]: system = ctx.render_str(ctx.cfg("system", ""), state) instructions = ctx.render_str(ctx.cfg("instructions", ""), state).strip() \ or "根据下面的证据写一份简洁的报告,先总后分。" - rules = f"{MARKER_RULES}\n{SEE_AND_CODE_RULES}" + rules = f"{MARKER_RULES}\n{SEE_AND_CODE_RULES}\n{period_rule(run_today())}" if writer.guarded: # 写作者看不全证据(目录有省略、有来源的结构化结论为空):「没有查到」这类结论没有依据 rules = f"{rules}\n{ABSENCE_RULE}" diff --git a/backend/tests/test_relative_dates.py b/backend/tests/test_relative_dates.py new file mode 100644 index 00000000..ced2ff07 --- /dev/null +++ b/backend/tests/test_relative_dates.py @@ -0,0 +1,60 @@ +"""「今天、昨天、上周、本月、去年」一律按运行当天算。 + +部署上撞过:问「上周各条线的产量」,数据只更新到两周前;查库时拿表里最大的日期往前推七天当「上周」,报告照着写成 +「上周产量……」,读的人以为是上周的数。现在: +- 助手的节点手册(画布、问数据共用)的搭图规则里有一条:相对日期按运行当天算(SQL 用当天的写法,agent 先调 + current_time),不用 MAX(日期) 去推; +- 报告写作规则告诉写作者运行当天是哪天、上周是哪几天:数据没覆盖问的那段时间时先照实说、写明数据最新到哪天;可以给出 + 最近一段的数,但写具体起止日期,不叫它「上周」。 +数据、日期都是虚构的。 +""" +from __future__ import annotations + +from datetime import date + +from app.api.copilot import NODE_REFERENCE +from app.engine.evidence import period_rule +from tests.test_report_node import engine_up, finish, node, script # noqa: F401 + + +def _build_rules() -> str: + start = NODE_REFERENCE.index("搭图规则(有数字结论时必须遵守)") + return NODE_REFERENCE[start:NODE_REFERENCE.index("\n\n模型字段", start)] + + +def test_the_copilot_reckons_relative_dates_from_the_run_day(): + rules = _build_rules() + assert "今天、昨天、上周、本月、去年" in rules and "运行当天" in rules + assert "CURRENT_DATE" in rules and "current_time" in rules # SQL 用当天的写法;agent 先取当天日期 + assert "MAX(日期)" in rules and "不要" in rules # 点名不要拿数据里最大的日期去推 + + +def test_the_writer_rule_names_the_run_day_and_last_week(): + rule = period_rule(date(2031, 3, 12)) # 虚构的一天:周三 + assert "2031-03-12(周三)" in rule + assert "上周是 2031-03-03 至 2031-03-09" in rule # 上周一到上周日 + assert "不按数据里最新的日期推" in rule + assert "数据最新到哪天" in rule and "起止日期" in rule and "不要把它叫作「上周」" in rule + + +def test_last_week_on_a_monday_and_a_sunday(): + assert "上周是 2031-03-03 至 2031-03-09" in period_rule(date(2031, 3, 10)) # 周一 + assert "上周是 2031-03-03 至 2031-03-09" in period_rule(date(2031, 3, 16)) # 周日:本周还没过完,上周照旧是前一周 + + +async def test_the_report_prompt_carries_the_run_day(monkeypatch): + from app.engine.nodes import report as report_mod + + monkeypatch.setattr(report_mod, "run_today", lambda: date(2031, 3, 12)) + seen = script(monkeypatch, "没有数据。") + graph = { + "nodes": [ + node("start", "input", fields=[{"name": "q", "default": "上周的产量是多少"}]), + node("write", "report", instructions="回答:{{ input.q }}"), + node("out", "output", fields=[{"name": "答案", "value": "{{ nodes.write.text }}"}]), + ], + "edges": [{"source": "start", "target": "write"}, {"source": "write", "target": "out"}], + } + await finish(graph) + prompt = "\n".join(str(m.content) for m in seen[0]) + assert "今天是 2031-03-12(周三)" in prompt and "上周是 2031-03-03 至 2031-03-09" in prompt