从邮件中选出正确的日期
供应商邮件里会出现很多日期:下单日期、被推迟的日期、看起来像日期的发票号,以及藏在某处的新交货日期。选择,而不是生成:代码用正则表达式找出每个日期并规范化,Valen 选出哪一个是新的交货日期,代码再把规范化后的值写入订单。模型从不书写日期,所以永远不会写出格式错误的日期。
工作原理
| 问题 | 类型 | 作用 |
|---|---|---|
has_new_date:供应商给出了新的交货日期 | predicate | 处理空值情况:完全没有日期,或确认原日期不变 |
delivery_date:哪个日期是新的交货日期? | 在代码找到的日期中做选择题 | 选出对应片段 |
每个候选项的描述都引用了它周围的文字,让模型结合上下文判断日期。真值表:
has_new_date | delivery_date | 动作 |
|---|---|---|
| 低于 0.3,或没有找到日期 | — | no_new_date |
| 0.7 及以上 | 最高 ≥ 0.8 且差距 ≥ 0.2 | update_order,写入规范化后的日期 |
| 其他 | 其他 | review |
运行
先下载一次示例包。caches/value_extraction.json 中录制的回答让脚本无需 API key 即可运行;加 --replay off 则改为调用免费托管 API。
Bash
curl -fLO https://valen-team.github.io/cookbook/valen-cookbook.zip
python -m zipfile -e valen-cookbook.zip .
cd valen-cookbook && python -m pip install -r requirements.txt
python value_extraction.py
python value_extraction.py --replay off --base-url https://api.valen-team.workers.dev/v1
代码
value_extraction.py
"""Select instead of generate: code finds the dates, Valen picks the one the business needs. Apache-2.0."""
from datetime import date
import json
import re
from common import HERE, choice_status, client, emit, parser, request
MONTHS = {m: i + 1 for i, m in enumerate(["january", "february", "march", "april", "may", "june", "july",
"august", "september", "october", "november", "december"])}
PATTERN = re.compile(r"\b(\d{4})-(\d{2})-(\d{2})\b|\b(\d{1,2}) (" + "|".join(MONTHS) + r")\b|\b("
+ "|".join(MONTHS) + r") (\d{1,2})\b", re.IGNORECASE)
def candidates(text, year):
"""Every date written in the message, normalized by code; the model never writes a date."""
found = []
for m in PATTERN.finditer(text):
if m.group(1):
value = date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
else:
day, month = (m.group(4), m.group(5)) if m.group(4) else (m.group(7), m.group(6))
value = date(year, MONTHS[month.lower()], int(day))
found.append({"span": m.group(0), "iso": value.isoformat(),
"context": text[max(0, m.start() - 60):m.end() + 40].replace("\n", " ")})
return found
def run(predict, email):
found = candidates(email["text"], email["year"])
asked = [{"name": "has_new_date", "type": "predicate",
"instructions": "The supplier gives a new delivery date in this email."}]
if found:
asked.append({"name": "delivery_date", "type": "choice",
"instructions": "Which date is the new date the order will be delivered?",
"choices": [{"value": c["span"], "description": f"The date in: “{c['context']}”"}
for c in found]})
answers = request(predict, email["text"], asked)
has_new = answers["has_new_date"]["probability"]
result = {"id": email["id"], "candidates": [c["span"] for c in found], "has_new_date": has_new}
if has_new < 0.3 or not found:
return {**result, "status": "no_new_date"}
picked = answers["delivery_date"]
confidence, certain = choice_status(picked)
status = "update_order" if certain and has_new >= 0.7 else "review"
iso = next(c["iso"] for c in found if c["span"] == picked["choice"])
return {**result, "status": status, "delivery_date": iso, "span": picked["choice"], "confidence": confidence}
def main():
args = parser(__doc__).parse_args()
emails = json.loads((HERE / "value_extraction_data.json").read_text())
with client(args) as predict:
emit([run(predict, email) for email in emails], args)
if __name__ == "__main__":
main()
value_extraction_data.json
[
{"id": "po-1182", "year": 2026, "text": "Hi Mei,\n\nThanks for your order placed on 2 March. Unfortunately the glaze kiln is down, so we can't ship on March 9 as promised. The new delivery date is 16 March. Invoice 2026-03-02 stays the same.\n\nBest,\nTomas"},
{"id": "po-1190", "year": 2026, "text": "Hello,\n\nQuick update on PO 1190: everything is on track and will arrive on 2026-03-20 as originally agreed. Our office is closed on 18 March for a holiday.\n\nRegards,\nPriya"},
{"id": "po-1204", "year": 2026, "text": "Hi team,\n\nThe courier lost the first pallet. We are re-shipping today; you should have it by April 7 instead of 31 March. Sorry about this. We'll credit the freight on the 15 April statement.\n\nAnna"},
{"id": "po-1211", "year": 2026, "text": "Dear Mei,\n\nWe received your payment, thank you. Delivery is being planned and we'll confirm a date next week.\n\nKind regards,\nTomas"}
]
输出
PO 1182 有四个候选日期,其中包括发票号 2026-03-02;Valen 选出了 "16 March"。PO 1190 确认原日期不变,所以 has_new_date 为 0.004,订单保持原样。
录制的输出
[
{
"id": "po-1182",
"candidates": [
"2 March",
"March 9",
"16 March",
"2026-03-02"
],
"has_new_date": 0.9980370964696507,
"status": "update_order",
"delivery_date": "2026-03-16",
"span": "16 March",
"confidence": 0.970832077353691
},
{
"id": "po-1190",
"candidates": [
"2026-03-20",
"18 March"
],
"has_new_date": 0.00443016482795233,
"status": "no_new_date"
},
{
"id": "po-1204",
"candidates": [
"April 7",
"31 March",
"15 April"
],
"has_new_date": 0.9780033681094145,
"status": "update_order",
"delivery_date": "2026-04-07",
"span": "April 7",
"confidence": 0.9725751976466991
},
{
"id": "po-1211",
"candidates": [],
"has_new_date": 0.19682010421693014,
"status": "no_new_date"
}
]
改成你自己的
- 同样的结构也适用于金额、订单号、姓名和地址:用代码找出候选项,问 Valen 业务上指的是哪一个,再用代码规范化。
- 年份、时区或发件人的地区设置都在代码里补上;模型只负责选择。