diff --git a/server/src/app/services/ontology.py b/server/src/app/services/ontology.py index 7fdf411..97e68b7 100644 --- a/server/src/app/services/ontology.py +++ b/server/src/app/services/ontology.py @@ -325,7 +325,11 @@ class SemanticOntologyService: entities = self._extract_entities(query, compact_query, reference) rule_scenario, scenario_score = self._detect_scenario(compact_query) - time_range, _time_score = self._extract_time_range(query, compact_query) + time_range, _time_score = self._extract_time_range( + query, + compact_query, + context_json=context_json, + ) context_scenario = self._resolve_context_scenario(context_json) if rule_scenario == "unknown" and context_scenario is not None: rule_scenario = context_scenario @@ -1175,16 +1179,22 @@ class SemanticOntologyService: self, query: str, compact_query: str, + *, + context_json: dict[str, Any], ) -> tuple[OntologyTimeRange, float]: - today = datetime.now(UTC).date() + today = self._resolve_reference_today(context_json) - direct_mappings = { - "今天": self._single_day_range(today, "今天", "day"), - "昨日": self._single_day_range(today - timedelta(days=1), "昨日", "day"), - "昨天": self._single_day_range(today - timedelta(days=1), "昨天", "day"), - "明天": self._single_day_range(today + timedelta(days=1), "明天", "day"), - } - for keyword, value in direct_mappings.items(): + direct_mappings = [ + ("大前天", self._single_day_range(today - timedelta(days=3), "大前天", "day")), + ("前天", self._single_day_range(today - timedelta(days=2), "前天", "day")), + ("昨日", self._single_day_range(today - timedelta(days=1), "昨日", "day")), + ("昨天", self._single_day_range(today - timedelta(days=1), "昨天", "day")), + ("今天", self._single_day_range(today, "今天", "day")), + ("明天", self._single_day_range(today + timedelta(days=1), "明天", "day")), + ("后天", self._single_day_range(today + timedelta(days=2), "后天", "day")), + ("大后天", self._single_day_range(today + timedelta(days=3), "大后天", "day")), + ] + for keyword, value in direct_mappings: if keyword in query: return value, 0.10 @@ -1263,6 +1273,29 @@ class SemanticOntologyService: return OntologyTimeRange(), 0.0 + @staticmethod + def _resolve_reference_today(context_json: dict[str, Any]) -> date: + client_now_iso = str(context_json.get("client_now_iso") or "").strip() + if not client_now_iso: + return datetime.now(UTC).date() + + normalized = client_now_iso.replace("Z", "+00:00") + try: + client_now = datetime.fromisoformat(normalized) + except ValueError: + return datetime.now(UTC).date() + + if client_now.tzinfo is None: + client_now = client_now.replace(tzinfo=UTC) + + try: + offset_minutes = int(context_json.get("client_timezone_offset_minutes") or 0) + except (TypeError, ValueError): + offset_minutes = 0 + + local_now = client_now - timedelta(minutes=offset_minutes) + return local_now.date() + @staticmethod def _single_day_range(target: date, raw: str, granularity: str) -> OntologyTimeRange: return OntologyTimeRange( diff --git a/server/src/app/services/user_agent.py b/server/src/app/services/user_agent.py index 97097d8..4f3a5e2 100644 --- a/server/src/app/services/user_agent.py +++ b/server/src/app/services/user_agent.py @@ -595,6 +595,7 @@ class UserAgentService: payload, can_proceed=can_proceed, draft_payload=draft_payload, + missing_slot_labels=[SLOT_LABELS.get(key, key) for key in missing_slot_keys], ) return UserAgentReviewPayload( @@ -936,7 +937,7 @@ class UserAgentService: emphasis="secondary", ), UserAgentReviewAction( - label="修改", + label="修改识别信息", action_type="edit_review", description="打开结构化模板,按已识别字段逐项修改。", emphasis="secondary", @@ -958,9 +959,9 @@ class UserAgentService: location = slots.get("location") customer = slots.get("customer_name") - summary = "系统识别出您想要发起一笔报销。" + summary = "我先按你当前提供的信息整理出一笔报销。" if expense_type and expense_type.value: - summary = f"系统识别出您想要报销{expense_type.value}。" + summary = f"我理解你这次想报销{expense_type.value}。" details: list[str] = [] if customer and customer.value: details.append(f"客户名称:{customer.value}") @@ -970,8 +971,6 @@ class UserAgentService: details.append(f"地点:{location.value}") if amount and amount.value: details.append(f"金额:{amount.value}") - if claim_groups and len(claim_groups) > 1: - details.append(f"建议拆分为 {len(claim_groups)} 张报销单") if details: return f"{summary} {';'.join(details)}。" return summary @@ -993,12 +992,15 @@ class UserAgentService: review_action = str(payload.context_json.get("review_action") or "").strip() if review_action == "save_draft": if draft_payload is not None and draft_payload.claim_no: - return f"相关识别信息已在右侧展示,请核对。当前已先保存到草稿 {draft_payload.claim_no},缺失信息后续可继续补充。" - return "相关识别信息已在右侧展示,请核对。当前信息未补齐,已按你的要求先保存草稿。" + return ( + f"我已经把本轮识别结果整理好了,右侧可以继续核对。" + f"当前先替你保存到草稿 {draft_payload.claim_no},后面把缺的信息补齐就可以继续。" + ) + return "我已经把本轮识别结果整理好了,右侧可以继续核对。当前信息还没补全,我先按你的要求保存为草稿。" if review_action == "next_step": - return "相关识别信息已在右侧展示,请核对。当前信息已满足继续流转条件,可进入下一步。" + return "我已经把识别到的关键信息整理好了,右侧是本轮识别结果。你确认无误后,可以直接进入下一步。" if review_action == "edit_review": - return "相关识别信息已在右侧展示,请核对。我已根据你的修改更新识别结果,请继续确认。" + return "我已经按你修改后的内容重新识别了一遍。右侧是最新结果,下方还有待补信息和注意事项,你继续确认即可。" return review_payload.body_message or None def _build_review_body_message( @@ -1007,12 +1009,21 @@ class UserAgentService: *, can_proceed: bool, draft_payload: UserAgentDraftPayload | None, + missing_slot_labels: list[str], ) -> str: if can_proceed: - return "相关识别信息已在右侧展示,请核对。确认无误后可点击“下一步”。" + return "我已经把识别结果整理在右侧了。当前关键信息基本齐全,你核对无误后可以直接点“下一步”继续处理。" + missing_hint = "、".join(missing_slot_labels[:4]) + missing_message = f"当前还缺少 {missing_hint}。" if missing_hint else "当前仍有信息待补充。" if draft_payload is not None and draft_payload.claim_no: - return f"相关识别信息已在右侧展示,请核对。当前信息还未补齐,可修改后继续,或先保存到草稿 {draft_payload.claim_no}。" - return "相关识别信息已在右侧展示,请核对。当前信息还未补齐,可点击“修改”继续补充,或先“保存草稿”。" + return ( + f"我先根据你当前提供的信息完成了初步识别,右侧是识别结果。{missing_message}" + f"如果现在还拿不全,也可以先保存到草稿 {draft_payload.claim_no},后面再补。" + ) + return ( + f"我先根据你当前提供的信息完成了初步识别,右侧是识别结果。{missing_message}" + "你可以继续补充;如果暂时不方便提供,也可以先保存草稿。" + ) @staticmethod def _can_proceed_review( @@ -1413,12 +1424,8 @@ class UserAgentService: time_range = payload.ontology.time_range if time_range.start_date and time_range.end_date: if time_range.start_date == time_range.end_date: - if time_range.raw and time_range.raw != time_range.start_date: - return f"{time_range.start_date}(原文:{time_range.raw})" return time_range.start_date normalized = f"{time_range.start_date} 至 {time_range.end_date}" - if time_range.raw and time_range.raw != normalized: - return f"{normalized}(原文:{time_range.raw})" return normalized if time_range.raw: return time_range.raw @@ -1493,7 +1500,7 @@ class UserAgentService: if edited_value: raw_value = str(review_form_values.get("time_range_raw") or edited_value).strip() return self._build_slot_value( - value=edited_value if raw_value == edited_value else f"{edited_value}(原文:{raw_value})", + value=edited_value, raw_value=raw_value, normalized_value=edited_value, source="user_form", @@ -1509,9 +1516,8 @@ class UserAgentService: else f"{time_range.start_date} 至 {time_range.end_date}" ) raw_value = str(time_range.raw or "").strip() - value = normalized_value if not raw_value or raw_value == normalized_value else f"{normalized_value}(原文:{raw_value})" return self._build_slot_value( - value=value, + value=normalized_value, raw_value=raw_value, normalized_value=normalized_value, source="user_text",