From 41b350b87cbaccaa89044c2f000f902df43bbd9e Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Mon, 19 Oct 2026 16:53:46 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E6=97=A5=E6=9C=9F=E4=B8=80?= =?UTF-8?q?=E5=BE=8B=E6=8C=89=E8=BF=90=E8=A1=8C=E5=BD=93=E5=A4=A9=E7=AE=97?= =?UTF-8?q?=EF=BC=9A=E5=8A=A9=E6=89=8B=E8=8A=82=E7=82=B9=E6=89=8B=E5=86=8C?= =?UTF-8?q?=E7=9A=84=E6=90=AD=E5=9B=BE=E8=A7=84=E5=88=99=EF=BC=88=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E3=80=81=E9=97=AE=E6=95=B0=E6=8D=AE=E5=85=B1=E7=94=A8?= =?UTF-8?q?=EF=BC=89=E5=8A=A0=E7=AC=AC=206=20=E6=9D=A1=E2=80=94=E2=80=94?= =?UTF-8?q?=E3=80=8C=E4=BB=8A=E5=A4=A9=E3=80=81=E6=98=A8=E5=A4=A9=E3=80=81?= =?UTF-8?q?=E4=B8=8A=E5=91=A8=E3=80=81=E6=9C=AC=E6=9C=88=E3=80=81=E5=8E=BB?= =?UTF-8?q?=E5=B9=B4=E3=80=8D=E6=8C=89=E8=BF=90=E8=A1=8C=E5=BD=93=E5=A4=A9?= =?UTF-8?q?=E7=AE=97=EF=BC=8CSQL=20=E7=94=A8=E6=96=B9=E8=A8=80=E9=87=8C?= =?UTF-8?q?=E5=8F=96=E5=BD=93=E5=A4=A9=E7=9A=84=E5=86=99=E6=B3=95=E3=80=81?= =?UTF-8?q?agent=20=E5=85=88=E8=B0=83=20current=5Ftime=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E7=94=A8=20MAX(=E6=97=A5=E6=9C=9F)=20=E5=8E=BB=E6=8E=A8?= =?UTF-8?q?=E3=80=8C=E4=B8=8A=E5=91=A8=E3=80=8D=EF=BC=8C=E8=A6=81=E8=AF=B4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9C=80=E6=96=B0=E5=88=B0=E5=93=AA=E5=A4=A9?= =?UTF-8?q?=E5=B0=B1=E5=8F=A6=E5=8F=96=E4=B8=80=E5=88=97=EF=BC=9B=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E5=86=99=E4=BD=9C=E8=A7=84=E5=88=99=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=EF=BC=88period=5Frule=EF=BC=89=EF=BC=8C=E5=91=8A?= =?UTF-8?q?=E8=AF=89=E5=86=99=E4=BD=9C=E8=80=85=E4=BB=8A=E5=A4=A9=E6=98=AF?= =?UTF-8?q?=E5=93=AA=E5=A4=A9=E3=80=81=E4=B8=8A=E5=91=A8=E6=98=AF=E5=93=AA?= =?UTF-8?q?=E5=87=A0=E5=A4=A9=EF=BC=9A=E6=95=B0=E6=8D=AE=E6=B2=A1=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E9=97=AE=E7=9A=84=E9=82=A3=E6=AE=B5=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=97=B6=E5=85=88=E7=85=A7=E5=AE=9E=E8=AF=B4=E3=80=81=E5=86=99?= =?UTF-8?q?=E6=98=8E=E6=95=B0=E6=8D=AE=E6=9C=80=E6=96=B0=E5=88=B0=E5=93=AA?= =?UTF-8?q?=E5=A4=A9=EF=BC=8C=E6=9C=80=E8=BF=91=E4=B8=80=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E6=95=B0=E5=8F=AF=E4=BB=A5=E7=BB=99=E4=BD=86=E5=86=99=E5=85=B7?= =?UTF-8?q?=E4=BD=93=E8=B5=B7=E6=AD=A2=E6=97=A5=E6=9C=9F=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=8F=AB=E5=AE=83=E3=80=8C=E4=B8=8A=E5=91=A8=E3=80=8D=E3=80=8C?= =?UTF-8?q?=E6=9C=AC=E6=9C=88=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/copilot.py | 4 ++ backend/app/engine/evidence.py | 19 +++++++++ backend/app/engine/nodes/report.py | 9 ++++- backend/tests/test_relative_dates.py | 60 ++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_relative_dates.py 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