aps-agent/server/agent_core/assistant.py

323 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

# ============================================================
# 通用排产助理(moduleId: core-assistant, 可重生 ✅)
# 像 Kimi:什么都能聊;带多轮上下文;排产是强项。
# ============================================================
from __future__ import annotations
import re
from datetime import datetime
from typing import Any
from server.agent_core.context import (
format_history_for_llm,
normalize_history,
resolve_followup,
wants_folder_listing,
)
from server.agent_core.providers import get_provider
from server.aps_domain.readiness import analyze_project_data, check_readiness
from server.contracts import AgentReply, UIBlock
World = dict[str, Any]
HistoryItem = dict[str, str]
_WEEKDAYS = "一二三四五六日"
_SYSTEM = """你是「工业智核」智能助手,对话能力对标 Kimi / 豆包:用户什么都会问,你都尽量答。
身份:日常问答 + 工厂排产专家。
铁律:
1. **先看【对话上文】**,理解用户是在接上一句(比如「是的」「那个」),再回答。
2. **先正面回答这句话**。问几点报时间;问文件夹就列文件;问排产就讲排产。
3. 禁止说:「跟排产无关帮不上」「换个排产问题」「没配大模型像 Kimi 才行」。
4. 说人话、短句;少用黑话,必须用时立刻白话解释。
5. 不要每轮粘贴功能清单。用 Markdown 排版。
6. 排产数字只能来自【项目上下文】;文件夹内容来自【文件夹清单】。
7. 有【当前时间】时,问几点必须用它。
"""
def _now_text() -> str:
n = datetime.now()
wd = _WEEKDAYS[n.weekday()]
return n.strftime(f"%Y年%m月%d日 星期{wd} %H:%M")
def _world_brief(world: World) -> str:
active = [o for o in (world.get("flexOrders") or [])
if (o.get("status") or "") not in ("DONE", "CANCELLED")]
mats = world.get("flexMaterials") or []
routes = world.get("flexRoutings") or []
equip = [e for e in (world.get("flexEquipment") or []) if e.get("status") == "RUNNING"]
cal = world.get("flexCalendar") or []
report = check_readiness(world)
s = report["summary"]
lines = [
f"待排订单 {len(active)} 张,物料 {len(mats)} 种,工艺步骤 {len(routes)} 条,"
f"能干活的设备 {len(equip)} 台,班次安排 {len(cal)} 条。",
f"其中能直接开排约 {s.get('ready')} 张,卡住 {s.get('blocked')} 张,"
f"工时未填 {s.get('timePending')} 步。",
]
for o in active[:8]:
lines.append(
f"- 订单 {o.get('orderNo')}:产品 {o.get('productCode')},"
f"数量 {o.get('quantity')},交期 {o.get('dueDate') or o.get('deliveryDate') or '未填'}"
)
return "\n".join(lines)
def _knowledge_snippets(query: str, top_k: int = 3) -> str:
try:
from server.knowledge import get_knowledge
from server.knowledge.retrieval import hybrid_search
units = get_knowledge().iter_search_units()
hits = hybrid_search(units, query, top_k=top_k, min_score=0.04)
except Exception:
return ""
if not hits:
return ""
parts = []
for h in hits:
title = h.get("title") or h.get("assetId") or "资料"
body = (h.get("content") or "")[:400]
parts.append(f"《{title}》\n{body}")
return "\n\n".join(parts)
def _is_greeting(t: str) -> bool:
return bool(re.search(r"^(你好|您好|嗨|hi|hello|在吗)[\s!!.。??]*$", t, re.I))
def _is_time_query(t: str) -> bool:
return bool(re.search(
r"(现在|当前)?(几点|什么时候了|几时了)|现在几点|几点了|今日日期|今天(几号|日期|星期几)|现在是几号",
t,
))
def _is_definition(t: str) -> bool:
return bool(re.search(
r"(什么叫|什么是|啥叫|啥是|解释(一下|下)?|怎么理解|啥意思|定义).{0,12}排产"
r"|排产.{0,6}(什么意思|是什么|啥意思|怎么理解)",
t,
))
def _wants_inventory(t: str) -> bool:
return bool(re.search(
r"分析|盘点|现状|手头|缺什么|缺啥|齐不齐|能不能开排|现在(能不能|可以)排|"
r"看看(现在|项目|数据|订单)|有多少(单|订单|料)|数据内容|项目里面的数据",
t,
)) or "【文件解析】" in t
def _simple_math(t: str) -> str | None:
m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([+\-*/×÷])\s*(\d+(?:\.\d+)?)\s*[=??]?\s*", t)
if not m:
return None
a, op, b = float(m.group(1)), m.group(2), float(m.group(3))
try:
if op == "+":
r = a + b
elif op == "-":
r = a - b
elif op in ("*", "×"):
r = a * b
elif op in ("/", "÷"):
if b == 0:
return "除数不能是 0 哟。"
r = a / b
else:
return None
except Exception:
return None
if abs(r - round(r)) < 1e-9:
return f"**{int(round(r))}**"
return f"**{r:.4g}**"
def _explain_scheduling() -> str:
return (
"**排产是啥?**\n\n"
"用大白话讲:工厂里有很多订单要做,每张单要经过几道工序、用哪台设备、"
"什么时候开工完工——**把这些事提前安排清楚,就叫排产**。\n\n"
"好比家里做饭:先想清楚先炒哪个菜、用哪口锅、几点能上桌。\n\n"
"**排产一般要弄清三件事:**\n"
"- **做什么**:哪些订单、做什么产品、做多少、啥时候要\n"
"- **怎么做**:几道工序、每道大概多久\n"
"- **谁来做**:哪些设备/人能干这些活、哪天有空\n\n"
"你要是想亲手试一版,跟我说一声「带我排一版」就行。"
)
def _explain_can_schedule() -> str:
return (
"**会,这就是我的本职工作。**\n\n"
"你把订单和现场情况告诉我,我可以帮你看缺不缺、排出一版计划。\n"
"日常问题(几点了、算个数、闲聊)也都能聊。\n\n"
"想现在就试?直接说「带我排一版」或「直接排一版」。"
)
def _answer_time() -> str:
return f"现在是 **{_now_text()}**(按你这台电脑的本地时间)。"
def _readiness_block(world: World) -> UIBlock:
report = check_readiness(world)
return UIBlock(
blockId="assistant-readiness", type="readiness",
props={"summary": report["summary"], "orders": report["orders"][:12],
"globalIssues": report["globalIssues"]},
)
def _offline_reply(
world: World, query: str, *, session_id: str | None = None,
) -> tuple[str, bool]:
t = query.strip()
if _is_greeting(t):
return (
"你好,我在。\n\n"
"啥都能问:几点了、算个数、看项目文件夹、排产排程……怎么说都行。\n\n"
"想排产的话,说「带我排一版」我就上手。",
False,
)
if _is_time_query(t):
return _answer_time(), False
if wants_folder_listing(t):
from server.aps_domain.folder_pack import analyze_work_dir
report = analyze_work_dir(world, session_id)
return report.get("markdown") or report.get("error") or "目录解析完成。", False
if _is_definition(t):
return _explain_scheduling(), False
if re.search(r"你会排产|会不会排|能不能排产|你会排吗", t):
return _explain_can_schedule(), False
if re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下|你会什么", t):
return (
"我是工业智核里的助手:**日常问题也能聊**,排产是我最拿手的。\n\n"
"可以说「现在几点了」「解析项目文件夹」「帮我分析项目数据」「带我排一版」。",
False,
)
math = _simple_math(t)
if math:
return f"算下来是 {math}。", False
if re.search(r"讲个笑话|来个笑话|说个笑话", t):
return (
"好,来一个车间版冷笑话:\n\n"
"计划员问设备:「你今天忙不忙?」\n"
"设备说:「不忙,我排期排到下下下个月了,早就躺平了。」",
False,
)
if re.search(r"天气|下雨|降温|热不热", t):
return (
"我这边看不到窗外实时天气,手机天气最准。\n\n"
"要是担心交期被耽误,把单号跟我说,我帮你看计划。",
False,
)
if _wants_inventory(t):
return analyze_project_data(world, t), True
# 祈使开排不应落到助理菜单(意图层应已拦截;此处再兜底一句口令)
if re.search(
r"给我排产|我要你排产|你给我排|帮我排产|开始排产|直接排|马上排|赶紧排|排产吧",
t,
):
return (
"明白,你是要**直接开排**。\n\n"
"请再说一次「给我排产」或点快捷指令「跑一版柔性排产」——"
"我会按工程目录/当前数据开排,不再让你选菜单。",
False,
)
if re.search(r"排产|订单|交期|设备|工艺|物料|工时", t) and re.search(
r"什么|怎么|如何|吗|呢|?|\?", t,
):
return (
f"你问的是「{t[:40]}」。\n\n"
"若要**直接开排**,说「给我排产」;\n"
"若要看齐不齐,说「帮我看看现在能不能排」;\n"
"若要看文件夹,说「解析项目文件夹」。",
False,
)
# 开放问题:结合上文语气,不再甩「没配 Kimi」
return (
f"关于「{t[:48]}」,我先按你的话理解一下。\n\n"
"你要是想看**项目文件夹里有啥文件**,直接说「解析项目文件夹」;\n"
"想看**排产数据齐不齐**,说「帮我分析一下当前项目里面的数据」;\n"
"想**开排**,说「带我排一版」。\n\n"
"也可以把具体想法再说详细一点,我按你的目标接着做。",
False,
)
async def reply(
world: World,
query: str,
*,
history: list[Any] | None = None,
session_id: str | None = None,
) -> AgentReply:
"""带多轮上下文的通用作答。"""
hist = normalize_history(history)
# 历史不含本轮用户句时,resolve 用 hist;前端传来的 history 通常不含当前句
follow = resolve_followup(query, hist)
if follow.get("kind") == "deny" and follow.get("reply"):
return AgentReply(text=str(follow["reply"]))
q = str(follow.get("text") or query or "").strip() or "你好"
note = str(follow.get("note") or "")
# 短确认被改写成「直接排一版 / 带我排一版」等 → 交给工作流口令识别更稳
# 此处若已是明确排产口令,仍可由 assistant 引导;gateway 会在 resolve 后重新 recognize。
# 助理层只负责问答类。
if _is_time_query(q) or _is_greeting(q) or _is_definition(q) \
or re.search(r"你会排产|会不会排|能不能排产|你会排吗", q) \
or wants_folder_listing(q):
text, with_card = _offline_reply(world, q, session_id=session_id)
if note and follow.get("kind") == "affirm":
text = f"(明白,你是说「{q}」)\n\n" + text
return AgentReply(text=text, blocks=[_readiness_block(world)] if with_card else [])
provider = get_provider()
if provider.enabled:
brief = _world_brief(world)
knowledge = _knowledge_snippets(q)
hist_blob = format_history_for_llm(hist)
folder_hint = ""
if wants_folder_listing(q):
from server.aps_domain.folder_pack import analyze_work_dir
folder_hint = analyze_work_dir(world, session_id).get("markdown") or ""
user = (
f"【当前时间】{_now_text()}\n\n"
+ (f"【对话上文】\n{hist_blob}\n\n" if hist_blob else "")
+ (f"【上下文备注】{note}\n\n" if note else "")
+ f"【项目上下文】\n{brief}\n\n"
+ (f"【文件夹清单】\n{folder_hint}\n\n" if folder_hint else "")
+ (f"【厂内资料摘录】\n{knowledge}\n\n" if knowledge else "")
+ f"【用户本轮】\n{q}\n\n"
"请结合上文正面回答。用户说「是的」时,按上一轮建议执行或确认,不要反问「你问的是是的」。"
)
text = await provider.chat_text(_SYSTEM, user, timeout=45.0)
if text and text.strip():
blocks = [_readiness_block(world)] if _wants_inventory(q) else []
return AgentReply(text=text.strip(), blocks=blocks)
text, with_card = _offline_reply(world, q, session_id=session_id)
if note and follow.get("kind") == "affirm":
text = f"(明白,按你说的「{q}」来)\n\n" + text
blocks = [_readiness_block(world)] if with_card else []
return AgentReply(text=text, blocks=blocks)