Files
baiyue-logistics-chat/logistics_chat/order_followups.py
T

143 lines
8.1 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.
"""Resolve bounded list references and subset questions from scoped saved replies."""
import json
import re
import sqlite3
from .db import connect
from .order_query import COUNT,NOUN,CONDITION,parse_query
ORDINALS={word:index for index,word in enumerate(
'一 二 三 四 五 六 七 八 九 十 十一 十二 十三 十四 十五 十六 十七 十八 十九 二十'.split(),1)}
RELATIVE=r'^(?:这其中|其中|这些(?:订单|运单)?(?:中|里)?|这几(?:票|条)(?:中|里)?|里面|当中)'
def is_relative_question(message):
return (bool(re.match(RELATIVE,message))
and bool(re.search(COUNT+'|'+NOUN+'|'+CONDITION+r'|轨迹|节点|阶段|到哪|在哪',message,re.I))
and not re.search(r'报价|海运|海派|海卡|空运|运费|费用|价格|KG|公斤|千克|CBM|立方',message,re.I))
def ordinal_request(message):
from .order_data import is_order_followup
text=re.sub(r'[\s,,。.!!??]','',message)
match=re.fullmatch(r'(?:请|帮我|看看|看一下|查询)?第(\d{1,5}|[一二三四五六七八九十]{1,3})'
r'(?:个|票|条|笔)(?:订单|运单)?(?:的)?(.*)',text)
if not match:return None
detail=match[2]
if detail not in ('','呢','详情') and not is_order_followup(detail):return None
number=int(match[1]) if match[1].isdigit() else ORDINALS.get(match[1],0)
return number
def list_step(message):
from .order_data import is_order_followup
text=re.sub(r'[\s,,。.!!??]','',message)
match=re.fullmatch(r'(?:那|再看|看看|查一下|查询)?(下一|后一|上一|前一)'
r'(?:票|个|条|单)(?:订单|运单)?(?:的)?(.*)',text)
if not match:return None
if match[2] not in ('','呢','详情') and not is_order_followup(match[2]):return None
return 1 if match[1] in ('下一','后一') else -1
def shown_references(connection,conv_id,state):
if 'shown' in state:
return [(item['inbound_code'] or '未登记',item['order_no']) for item in state['shown']]
row=connection.execute("SELECT content FROM messages WHERE conv_id=? AND id=? AND role='assistant'",
(conv_id,state['message_id'])).fetchone()
# Older states stored only the reply ID. Accept its fixed, unambiguous row format.
lines=[line for line in (row[0] if row else '').splitlines() if line.startswith('- 入仓码:')]
result=[]
for line in lines:
match=re.match(r'^- 入仓码:([^\r\n;]+);订单号:([^\r\n;]+);(?:业务状态|异常原因):',line)
if not match or any(line.count(label)>1 for label in (';订单号:',';业务状态:',';异常原因:')):
return []
result.append(match.groups())
return result
def selected_row(content,shown):
from .tracking import CARD_PREFIX
try:
card=json.loads(content[len(CARD_PREFIX):])
if card.get('kind')!='order' or not isinstance(card.get('no'),str):return None
matches=[index for index,(_,number) in enumerate(shown,1) if number.casefold()==card['no'].casefold()]
if len(matches)>1:
fields=card.get('fields')
if not isinstance(fields,list):return None
inbound=next((field.get('value') for field in fields if isinstance(field,dict) and field.get('key')=='inbound'),None)
matches=[index for index in matches if shown[index-1][0]==inbound]
return matches[0] if len(matches)==1 else None
except (ValueError,TypeError,AttributeError):
return None
def answer_followup(message,tenants,conv_id):
from .customer_service import order_login_response
from .order_listing import load_state,answer_summary
text=message.strip(' ,,。.!!??')
ordinal=ordinal_request(text)
step=list_step(text)
relative=is_relative_question(text)
reason=bool(re.fullmatch(r'(?:那|这些|这几票|它们|上述|上面)?(?:订单|运单)?'
r'(?:为什么异常|为什么有异常|异常的?原因(?:是什么|有哪些|呢)?|异常是怎么回事)',text))
query_text=re.sub(RELATIVE,'',text)
query_text=re.sub(r'^(?:那么|那)','',query_text)
intent=parse_query(query_text+'订单有多少') if not (re.search(NOUN,query_text,re.I) or re.search(COUNT,query_text,re.I)) else parse_query(query_text)
if relative and intent is None and re.search(COUNT,query_text,re.I):intent=parse_query(query_text+'订单')
short_count=(not re.search(NOUN,text,re.I) and not re.search(COUNT,text,re.I)
and intent is not None and not intent['unsupported']
and any(intent.get(key) for key in ('status','stage','exception','grouped')))
if ordinal is None and step is None and not relative and not reason and not short_count:return None
if not tenants:return order_login_response()
try:
previous=load_state(conv_id)
if step is not None:
if not previous:return {'t':'请先查看订单列表并选择一票,或直接提供入仓码。'}
selected=previous.get('selected_index')
if selected is None:return {'t':'请先在当前列表中选择一票,例如“第一个”,再继续查看上一票或下一票。'}
connection=connect()
try:shown=shown_references(connection,conv_id,previous)
finally:connection.close()
ordinal=selected+step
if not 1<=ordinal<=len(shown):
command='下一页' if step>0 else '上一页'
edge='最后一票' if step>0 else '第一票'
return {'t':f'当前展示列表已到{edge}。可回复“{command}”翻页,或提供入仓码查询其他订单。'}
if ordinal is not None:
if not previous:return {'t':'请先查看需要查询的订单列表,或直接提供入仓码。'}
connection=connect()
try:
shown=shown_references(connection,conv_id,previous)
finally:connection.close()
if not shown:return {'t':'当前列表没有可查询的订单,请重新指定筛选条件,或直接提供入仓码。'}
if not 1<=ordinal<=len(shown):
return {'t':f'刚才的列表展示了{len(shown)}票,请选择其中一票,或回复“查看全部”展开列表后再查询。'}
return {'_order_reference':shown[ordinal-1][1]}
if short_count and not relative and (not previous or previous.get('selected')):
from .order_data import is_order_followup
if is_order_followup(text):return None
return {'t':'请先说明要统计的订单范围,例如“我的异常订单有多少”,或提供入仓码查询单票情况。'}
if reason:
if not previous or previous.get('selected'):return None
intent={**previous['query'],'exception':True,'unsupported':False}
elif relative:
if not previous:return {'t':'请先查询要筛选的订单范围,再继续问“其中”的数量。'}
if previous.get('selected'):
return {'t':'您想统计刚才列表中的订单,还是查询当前这一票?请说明订单范围或提供入仓码。'}
if intent is None or intent['unsupported'] or 'summaries' in intent:
return {'t':'请明确要在当前订单范围内筛选的一个条件,例如“其中异常的有多少票”。'}
for key in ('status','stage'):
if previous['query'][key] and intent.get(key) and previous['query'][key]!=intent[key]:
return {'t':'当前范围与这次条件对应不同状态或节点,请确认要切换筛选条件,还是继续查看原范围。'}
intent={**intent,**{key:intent.get(key) or value for key,value in previous['query'].items()}}
elif intent.get('grouped'):
if not previous or previous.get('selected'):return None
intent={**intent,**previous['query']}
if intent is None or intent['unsupported']:return None
return answer_summary(intent,tenants,conv_id,reasons=reason)
except sqlite3.Error:
return {'t':'订单信息暂时无法读取,请稍后重试。'}