143 lines
8.1 KiB
Python
143 lines
8.1 KiB
Python
"""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':'订单信息暂时无法读取,请稍后重试。'}
|