diff --git a/AGENTS.md b/AGENTS.md index 4753b64..c1b25ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Use `am.py` for CLI/WSGI, `logistics_chat/` for business code, and `static/css/` | Knowledge | `logistics_chat/knowledge.py`: `knowledge_list`, `search_knowledge`; `docs/knowledge.md` | | Pricing | `logistics_chat/pricing.py`: `RATES`, `calc_freight`; `quote_intent.py`: `parse_intent`; `quote_service.py`: `answer_quote`; `quote_engine.py`: `quantity`, `render_rates`; `quote_answers.py`, `quote_fees.py`: local business terms and fees | | US quote data | `logistics_chat/quote_workbook.py`: local detail export; `quote_data.py`: versioned SQLite imports; `quote_engine.py`: matching/calculation; `quote_service.py`: conversational reply; `scripts/import_quote_data.py` | +| Common customer questions | `logistics_chat/shipping_faq.py`: pre-shipment material; `quote_allocations.py`: per-warehouse weights; `quote_context.py`: chronological edits; `order_explanations.py`: business status, latest actual nodes and operation boundaries | | Tracking | `logistics_chat/tracking.py`: `extract_tracking_no`, `build_track`, `CARD_PREFIX` | | Order test data | `logistics_chat/order_data.py`: `lookup_order`, `count_orders`, `answer_query`, `import_records`; `scripts/import_order_test_data.py` | | Persistence | `logistics_chat/db.py`: `init_db`, `save_message`; `logistics_chat/history_service.py` | diff --git a/docs/architecture.md b/docs/architecture.md index ccf84c8..e7bb8bd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -141,6 +141,10 @@ tests/ # 按业务模块组织的回归 订单列表使用与逐单查询相同的服务端租户交集,每次最多展示20票并保护其会话历史;未知筛选条件不默认为全部订单。改址/取消尚无业务写入接口,只明确引导人工,不声称修改成功。输入明确的转人工指令复用现有申请接口,失败保留草稿,不增加SSE事件类型。 +`quote_allocations.py` 只识别明确分配给各仓的重量、箱数及箱规,逐仓计费及计算每箱最低重量;仅给总重和多个仓码时保留比较语义,不擅自分配重量。`quote_context.py` 在当前会话的有效上下文内按时间顺序应用更正,保留已算出的总重:重量纠正会清除旧分箱明细,单改箱数时统一箱重可沿用,混合箱重需重新确认;“总重不变”优先于旧每箱重量,新尺寸不沿用旧体积。按方报价不把按公斤的材质费直接乘CBM,金额未确定时明确单列待核算项。 + +`shipping_faq.py` 从激活版本回答交仓、标签、报关资料及查验费用问题;具体船期和计划发货日期需人工核对,不使用旧表日期承诺必达。`order_explanations.py` 只解释授权订单的业务状态和实际节点,不把预计节点当实际位置,不推断逐箱签收或亚马逊上架。文字事实回复与订单卡片同样绑定客户及范围;催单或拦截不声称已提交。本轮不扩展售后理赔规则。 + 先按 README 安装项目依赖。Windows 使用 `.\.venv\Scripts\python.exe -m pytest -v` 或同解释器的 `-m unittest discover -s tests -v`;Linux/macOS 使用 `.venv/bin/python`。业务测试依赖 Flask,不能用未安装依赖的全局 Python 运行整套测试。模型请求全部模拟,不代表真实 DeepSeek 联调。 路径、函数或资产移动后同步更新根 `AGENTS.md`、本文、知识资料和 README 目录树。仅改导航时检查路径、符号、命令;改业务时补充相关成功和失败分支,前端改动检查实际页面。 diff --git a/logistics_chat/order_data.py b/logistics_chat/order_data.py index c9f78d3..1e7a55d 100644 --- a/logistics_chat/order_data.py +++ b/logistics_chat/order_data.py @@ -351,6 +351,9 @@ def is_tracking_request(message): def is_order_followup(message): """Reuse an order only for an explicit reference or a short detail question.""" + from .order_explanations import detail_question + if detail_question(message) and not re.search(r'报价|价格|多少钱|公斤|\bKG\b|海派|海卡',message,re.I): + return True if is_lookup_help(message): return False if is_tracking_request(message): @@ -375,6 +378,10 @@ def is_order_followup(message): def answer_query(message, conv_id): references = extract_reference(message) tenants = customer_tenant_ids() + from .order_explanations import general_explanation, detail_question, explain_order + if not references: + explanation=general_explanation(message) + if explanation:return {'t':explanation} count_intent = parse_count_query(message) if not references else None if count_intent is not None: return answer_count_query(count_intent,tenants) @@ -413,6 +420,8 @@ def answer_query(message, conv_id): if not reference: if mutation: return order_login_response() if not tenants else {'t':'当前支持查询订单与物流轨迹;改址、取消等操作请联系人工客服办理,尚未修改订单。请提供入仓码。'} + if detail_question(message) and not is_lookup_help(message): + return order_login_response() if not tenants else {'t':'请提供入仓码,我先核对该票订单的实际物流记录。'} if is_tracking_request(message) or (not is_lookup_help(message) and re.search(r'(?:查|搜|看|找).*(?:订单|运单|物流轨迹|入仓码|入仓号)|(?:入仓码|入仓号).*(?:查|轨迹|状态)|订单状态|物流轨迹|shipment\s*id|' r'(?:列出|搜索|所有|全部|多少|我的).*(?:订单|运单|入仓)|(?:订单|运单).*(?:多少|列表|查询|在哪|到哪|进度)|' r'^\s*(?:查单|查询|查一下|帮我查一下)[。?!?!\s]*$|' @@ -434,4 +443,7 @@ def answer_query(message, conv_id): return {'t':f'已查到订单“{reference}”。当前支持查询订单与物流轨迹;改址、取消等操作请联系人工客服办理,尚未修改订单。','_customer_data':True} if re.search(r'运费|费用|多少钱|价格', message): return {'t':'该订单未记录实际运费,请联系人工客服核实。','_customer_data':True} - return {'card': build_card(matches[0])} + card=build_card(matches[0]) + explanation=explain_order(message,matches[0],card) + if explanation:return {'t':explanation,'_customer_data':True} + return {'card':card} diff --git a/logistics_chat/order_explanations.py b/logistics_chat/order_explanations.py new file mode 100644 index 0000000..9e27431 --- /dev/null +++ b/logistics_chat/order_explanations.py @@ -0,0 +1,42 @@ +"""Explain recorded order facts without inferring delivery or operational writes.""" + +import re + + +def detail_question(message): + if re.search(r'报价|运费|海派|海卡|\d+\s*(?:KG|公斤)',message,re.I) and not re.search(r'这票|该订单|我的订单',message): + return False + return bool(re.search(r'上架|没更新|未更新|没有更新|不更新|少了?.*(?:箱|件)|到了?几箱|逐箱|每箱.*签收|' + r'已完成.*(?:签收|送达)|(?:签收|送达).*已完成|催单|催一下|拦截|' + r'(?:这票|该订单|我的货).*(?:发货|发走|出库|开船)',message)) + + +def general_explanation(message): + if re.search(r'我的|这票|这单|这个订单|该订单',message):return None + if re.search(r'已完成|履约中|上架',message) and re.search(r'是什么意思|是否代表|是不是|等于|有什么区别',message): + return '“履约中”“头程订单已完成”属于业务状态;出库、开船、到港等以实际物流节点为准。业务完成不等于收件人签收,签收也不等于亚马逊已上架;上架需以亚马逊入库记录核实。' + return None + + +def explain_order(message,row,card): + if not detail_question(message):return None + reference=row.get('inbound_code') or row['order_no'] + lines=[f'入仓码 {reference}。' if row.get('inbound_code') else f'订单号 {reference}。'] + if re.search(r'催单|催一下|拦截',message): + return lines[0]+'催单或拦截需联系人工客服办理;当前仅查询订单,尚未提交催单或拦截申请。' + lines.append(f'登记的业务状态为“{card["status"]}”。') + actual=card.get('nodes',[]) if card.get('node_kind')=='logistics' else [] + if actual: + latest=actual[0] + lines.append('最近一条实际物流记录:'+' '.join(str(latest.get(k) or '') for k in ('time','text','place')).strip()+'。') + else:lines.append('当前没有已记录的实际物流节点,不能根据业务状态推断货物位置。') + if re.search(r'上架|已完成|签收|送达',message): + lines.append('头程业务完成不等于收件人签收;当前节点不能用于确认亚马逊上架。上架结果需核对亚马逊入库记录。') + if re.search(r'没更新|未更新|没有更新|不更新',message): + lines.append('未更新期间的实际位置和原因无法从现有记录确定,可提供入仓码联系人工核查。') + if re.search(r'发货|发走|出库|开船',message): + lines.append('计划发货或开船安排需联系人工核对,不能根据业务状态承诺未来发货日期。') + if re.search(r'少了?.*(?:箱|件)|到了?几箱|逐箱|每箱.*签收',message): + if row.get('package_count') is not None:lines.append(f'登记总件数为{row["package_count"]}件。') + lines.append('当前没有逐箱签收明细,不能把整票业务状态当作每箱均已送达,请联系人工核实各箱情况。') + return '\n'.join(lines) diff --git a/logistics_chat/quote_allocations.py b/logistics_chat/quote_allocations.py new file mode 100644 index 0000000..2466db5 --- /dev/null +++ b/logistics_chat/quote_allocations.py @@ -0,0 +1,38 @@ +"""Explicit weight-to-warehouse assignments; unassigned comparisons stay separate.""" + +from decimal import Decimal +import re + + +def allocations(text,codes,weight_pattern,to_kg): + if len(codes)<2:return None + code='('+'|'.join(re.escape(code) for code in sorted(codes,key=len,reverse=True))+r')(?![A-Za-z0-9])' + forward=weight_pattern.pattern+r'\s*(?:货物)?\s*(?:发到|发往|送到|发|到|去)?\s*'+code + reverse=code+r'\s*(?:[::]|分配|发|重)?\s*'+weight_pattern.pattern + entries=[];used=[] + for pattern,backwards in ((forward,False),(reverse,True)): + for match in re.finditer(pattern,text,re.I): + if any(match.start()start for start,end in used):continue + values=match.groups() + destination,value,unit=values if backwards else (values[2],values[0],values[1]) + piece_weight=to_kg(value,unit) + weight_start=match.start(2 if backwards else 1) + carton=re.search(r'(第)?(\d+)\s*(?:箱|件)\s*(每(?:箱|件))?\s*$',text[:weight_start]) + boxes=(1 if carton.group(1) else int(carton.group(2))) if carton else None + pieces=([{'count':boxes,'weight':str(piece_weight),'dims':None,'dim_unit':'cm','position':weight_start}] + if carton and (boxes==1 or carton.group(3)) else None) + weight=piece_weight*boxes if pieces else piece_weight + entries.append({'warehouse':destination.upper(),'weight':weight,'boxes':boxes,'packages':pieces}) + used.append(match.span()) + if len(entries)<2:return None + totals={} + for entry in entries: + code=entry['warehouse'] + if code not in totals: + totals[code]=entry + continue + total=totals[code] + total['weight']+=entry['weight'] + total['boxes']=total['boxes']+entry['boxes'] if total['boxes'] is not None and entry['boxes'] is not None else None + total['packages']=(total['packages']+entry['packages']) if total['packages'] and entry['packages'] else None + return [{**entry,'weight':str(entry['weight'])} for entry in totals.values()] diff --git a/logistics_chat/quote_context.py b/logistics_chat/quote_context.py new file mode 100644 index 0000000..72ea6fe --- /dev/null +++ b/logistics_chat/quote_context.py @@ -0,0 +1,54 @@ +"""Apply quote edits in chronological order, including derived carton quantities.""" + +from copy import deepcopy +from decimal import Decimal + + +QUANTITY_QUESTION='箱数已更改,请确认新的总重或各箱重量,原分箱明细不能直接套用。' + + +def merge_intent(current,previous): + result=deepcopy(current) + excluded={'mode','text','errors','dim_unit','quantity_note','quantity_uncertain','preserve_total'} + if current.get('warehouse') or current.get('unknown_warehouses'): + excluded.update(('postal','zone','zones')) + elif current.get('postal'): + excluded.update(('warehouse','warehouses','unknown_warehouses','zone','zones')) + elif current.get('zone'): + excluded.update(('warehouse','warehouses','unknown_warehouses','postal')) + if any(current.get(k) for k in ('warehouse','postal','zone')) and not current.get('unknown_warehouses'): + excluded.add('unknown_warehouses') + if any(current.get(k) for k in ('warehouse','unknown_warehouses','postal','zone')) and not current.get('allocations'): + excluded.add('allocations') + if current.get('allocations'):excluded.update(('packages','boxes','dims','cbm')) + if current['transport'] or current['kinds'] or current['services']:excluded.update(('air_only','air_requested')) + if current['air_only'] or current['tax']=='excluded':excluded.add('kinds') + if current['weight'] is not None: + if not current.get('packages'):excluded.add('packages') + if not current.get('allocations'):excluded.add('allocations') + if current['dims']:excluded.add('cbm') + if current['cbm'] is not None:excluded.add('dims') + for key,value in previous.items(): + if result.get(key) is None and key not in excluded:result[key]=deepcopy(value) + if current['dims'] is None and 'dims' not in excluded and previous['dims']: + result['dim_unit']=previous['dim_unit'] + pieces=result.get('packages') or [] + if current['dims']: + for piece in pieces:piece.update(dims=current['dims'],dim_unit=current['dim_unit']) + elif current['cbm'] is not None: + for piece in pieces:piece.update(dims=None,dim_unit='cm') + if previous.get('quantity_uncertain') and current['weight'] is None and not current.get('preserve_total'): + result['quantity_uncertain']=True + result['errors'].append(QUANTITY_QUESTION) + if pieces and result['boxes']!=sum(p['count'] for p in pieces): + if current.get('preserve_total'): + result['packages']=None + elif len(pieces)==1: + pieces[0]['count']=result['boxes'] + result['weight']=str(Decimal(pieces[0]['weight'])*result['boxes']) + result['quantity_note']=f'按每箱{pieces[0]["weight"]}KG不变,更新为{result["boxes"]}箱。' + else: + result['packages']=None + result['quantity_uncertain']=True + result['errors'].append(QUANTITY_QUESTION) + return result diff --git a/logistics_chat/quote_engine.py b/logistics_chat/quote_engine.py index 606007c..7e4c7e8 100644 --- a/logistics_chat/quote_engine.py +++ b/logistics_chat/quote_engine.py @@ -20,6 +20,10 @@ def broad_zone(postal): def variants(intent, book): + if intent.get('allocations'): + return [{**destination,'allocation':part} for part in intent['allocations'] + for destination in variants({**intent,'allocations':None,'warehouse':part['warehouse'], + 'warehouses':None,'postal':None},book)] if len(intent.get('warehouses') or [])>1: return [variant for warehouse in intent['warehouses'] for variant in variants({**intent,'warehouse':warehouse,'warehouses':None,'postal':None},book)] @@ -123,21 +127,27 @@ def render_rates(intent, book, destinations, remote_fees): lines.append(f"美国 {number(intent['weight'])}KG 基础运费:") else: lines.append('美国线路报价:') - if len(intent.get('warehouses') or [])>1: + if intent.get('quantity_note'):lines.append(intent['quantity_note']) + if intent.get('allocations'): + lines.append('按各目的仓分票计算,箱规需逐票核对:') + elif len(intent.get('warehouses') or [])>1: lines.append('以下按各目的仓分别比较;实际分票请提供每个仓的重量。') if intent.get('air_requested') and not intent.get('air_only'): lines.append('当前资料没有美国空运费率,下面列出海运方案。') for destination, fees in zip(destinations, remote_fees): - rates = match_rates(intent, book, destination) + part=destination.get('allocation') + billing=({**intent,'weight':part['weight'],'packages':part.get('packages'),'dims':None,'cbm':None,'boxes':part.get('boxes')} + if part else intent) + rates = match_rates(billing, book, destination) if intent.get('mode') in ('cheapest','fastest') and rates: if intent['mode']=='cheapest': - eligible=[r for r in rates if quantity(intent,r) is not None] + eligible=[r for r in rates if quantity(billing,r) is not None] best={} for rate in eligible: key=(destination['zone'] or rate['zone'],rate['unit'],rate['tax']) - value=quantity(intent,rate)*Decimal(rate['price'])+charge_details(intent,rate,fees,book)['cny'] + value=quantity(billing,rate)*Decimal(rate['price'])+charge_details(billing,rate,fees,book)['cny'] best[key]=min(best.get(key,value),value) - rates=[r for r in eligible if quantity(intent,r)*Decimal(r['price'])+charge_details(intent,r,fees,book)['cny']==best[(destination['zone'] or r['zone'],r['unit'],r['tax'])]] or rates + rates=[r for r in eligible if quantity(billing,r)*Decimal(r['price'])+charge_details(billing,r,fees,book)['cny']==best[(destination['zone'] or r['zone'],r['unit'],r['tax'])]] or rates lines.append('按相同区域、计费单位及税费方式比较的较低价方案:') else: def eta_key(rate): @@ -163,13 +173,13 @@ def render_rates(intent, book, destinations, remote_fees): continue parts = [] for rate in rows: - amount = quantity(intent,rate) + amount = quantity(billing,rate) price = Decimal(rate['price']) rate_text = f"{rate['zone']} " if rate['zone'] and not destination['zone'] else '' rate_text += f"{number(price)}元/{'KG' if unit == 'kg' else 'CBM'}" if amount is not None: total = amount*price - details=charge_details(intent,rate,fees,book) + details=charge_details(billing,rate,fees,book) total+=details['cny'] rate_text += f",{number(amount,3)}{'KG' if unit == 'kg' else 'CBM'} = {number(total)}元" if details['items']:rate_text+='('+ ';'.join(details['items'])+')' diff --git a/logistics_chat/quote_fees.py b/logistics_chat/quote_fees.py index 651fef0..ca0e65d 100644 --- a/logistics_chat/quote_fees.py +++ b/logistics_chat/quote_fees.py @@ -83,11 +83,13 @@ def charge_details(intent,rate,remote,book): fee=charged*Decimal(remote['fee']);result['cny']+=fee result['items'].append(f'偏远费{remote["fee"]}元/KG×{charged:g}KG={fee:g}元') else:result['notes'].append('遥远地址的附加费需单独确认,当前金额未包含该项。') - if rate['unit']=='kg' and intent.get('material')=='metal': + if intent.get('material')=='metal': per_kg=material_fee(book) - if per_kg is not None: + if per_kg is not None and rate['unit']=='kg': fee=charged*per_kg;result['cny']+=fee result['items'].append(f'铁铝附加费{per_kg:g}元/KG×{charged:g}KG={fee:g}元') + elif per_kg is not None: + result['notes'].append(f'铁铝附加费为{per_kg:g}元/KG,当前按方运费未包含该项;需确认适用计费重量后单列,不能把CBM直接乘公斤附加费。') if rate['kind']=='parcel': limits=limit_fees(intent,book) result['cny']+=limits['cny'];result['items']+=limits['items'];result['notes']+=limits['notes'] diff --git a/logistics_chat/quote_intent.py b/logistics_chat/quote_intent.py index d5b0234..8aaeb66 100644 --- a/logistics_chat/quote_intent.py +++ b/logistics_chat/quote_intent.py @@ -49,6 +49,8 @@ def request_mode(text): # Business topics outrank generic verbs such as “怎么算”. if re.search(r'保险|保价|投保|insured|insurance',text,re.I):return 'insurance' if re.search(r'纯电池|带电|内置电池|液体|粉末|食品|医疗用品|仿牌',text):return 'acceptance' + from .shipping_faq import preparation_topic + if preparation_topic(text):return 'preparation' if re.search(r'最便宜|便宜的|最省钱|最划算|最低价|cheapest|least expensive',text,re.I):return 'cheapest' if re.search(r'最快|快的|fastest',text,re.I):return 'fastest' if re.search(r'一件代发|散箱入库|整柜卸货|卸货费|仓租|仓储|贴标费',text):return 'fulfillment' @@ -63,6 +65,7 @@ def request_mode(text): def parse_intent(message,warehouse_codes): text=normalize(message) + text=re.sub(r'(?:而?不是|并非|不要按|不是按)\s*(?:每[箱件]|单[箱件])?\s*'+WEIGHT.pattern,' ',text,flags=re.I) parsed=parse_shipping_intent(text) country='US' if US.search(text) else ('other' if shipping_region(text) is not None else None) candidates=re.findall(r'(?1:parsed['dims']=None + elif assigned: + weight=str(sum(Decimal(a['weight']) for a in assigned)) + if set(a['warehouse'] for a in assigned)!=set(warehouses):errors.append('请分别提供每个目的仓的重量,避免遗漏分票。') + total=re.search(r'(?:总重|合计重量)\s*'+WEIGHT.pattern,text,re.I) + if total and kg(*total.groups())!=Decimal(weight):errors.append('总重与各目的仓分配重量不一致,请确认。') elif len(weights)>1: errors.append('请明确总重,或分别提供每箱重量,避免把不同重量混在一起。') + if assigned and packages and Decimal(weight)!=sum(Decimal(a['weight']) for a in assigned): + errors.append('分仓重量与分箱重量不一致,请分别确认每仓的箱数和重量。') if any(kg(*w.groups())<=0 for w in weights):errors.append('重量必须大于 0,请确认有效重量。') labeled=re.search(r'(?:邮编|zip(?:\s*code)?|post\s*code)\s*[::为是]?\s*([0-9-]+)',text,re.I) postal=None @@ -125,7 +142,7 @@ def parse_intent(message,warehouse_codes): sea=bool(re.search(r'海运|海派|海卡|海铁|\bsea\b',text,re.I)) english=not re.search(r'[\u4e00-\u9fff]',message) and bool(re.search(r'[A-Za-z]{3,}\s+[A-Za-z]',message)) return {'country':country,'weight':weight,'dims':parsed['dims'],'dim_unit':detect_dim_unit(text), - 'cbm':cbm.group(1) if cbm else None,'boxes':count,'packages':packages or None,'origin':origin, + 'cbm':cbm.group(1) if cbm else None,'boxes':count,'packages':packages or None,'allocations':assigned,'origin':origin, 'services':services or None,'kinds':kinds or None,'transport':parsed['channel'], 'air_only':True if air and not sea else None,'air_requested':True if air else None, 'warehouse':warehouses[0] if warehouses else None,'warehouses':warehouses or None, @@ -135,10 +152,11 @@ def parse_intent(message,warehouse_codes): 'material':('ordinary' if re.search(r'普通货|普货|塑料|木制|非铁铝',text) else ('metal' if re.search(r'铁制|钢制|铝制|铁铝|不锈钢',text) else None)), 'address_type':('non_fba' if re.search(r'私人|住宅|商业地址|非亚马逊',text) else ('fba' if warehouses else None)), - 'text':text} + 'preserve_total':True if re.search(r'总重不变|重量不变',text) else None,'text':text} def is_followup(message,intent): + if re.search(r'和刚才一样|其他不变|其它不变|每箱不变',message):return True if TOPIC_BOUNDARY.search(message):return False text=message.strip().strip(',,。.!!?? ') if intent.get('errors') and re.search(r'邮编|重量|箱|kg',message,re.I):return True diff --git a/logistics_chat/quote_service.py b/logistics_chat/quote_service.py index 5e26eeb..ed93d7a 100644 --- a/logistics_chat/quote_service.py +++ b/logistics_chat/quote_service.py @@ -10,45 +10,33 @@ from .quote_engine import US, QUOTE_WORDS, parse_intent, is_followup, variants, def intent_with_context(message, conv_id, book): - from copy import deepcopy + from .quote_context import merge_intent + codes = {row['code'] for row in book['warehouses']} codes.update(value.upper() for rate in book['rates'] for value in rate['destinations']) intent = parse_intent(message,codes) - def destination_reset(value): - if value.get('warehouse') or value.get('unknown_warehouses'):return {'postal','zone','zones'} - if value.get('postal'):return {'warehouse','warehouses','unknown_warehouses','zone','zones'} - if value.get('zone'):return {'warehouse','warehouses','unknown_warehouses','postal'} - return set() - excluded=destination_reset(intent) - selected_destination=bool(excluded) - if selected_destination and not intent.get('unknown_warehouses'):excluded.add('unknown_warehouses') - if intent['transport'] or intent['kinds'] or intent['services']:excluded.update(('air_only','air_requested')) - if intent['air_only'] or intent['tax']=='excluded':excluded.add('kinds') - if intent.get('weight') is not None and not intent.get('packages'):excluded.add('packages') - explicit_dims=bool(intent['dims']) + earlier_intents=[] if conv_id and is_followup(message,intent): connection=connect() try: recent=connection.execute("SELECT content FROM messages WHERE conv_id=? AND role='user' ORDER BY id DESC LIMIT 10",(conv_id,)).fetchall() - finally:connection.close() + finally: + connection.close() for (previous,) in recent: if re.search(r'理赔|赔偿|破损|丢失|退件|投诉|查单|轨迹|签收',previous):break earlier=parse_intent(previous,codes) if earlier['country']=='other':break if not (QUOTE_WORDS.search(previous) or US.search(previous) or is_followup(previous,earlier) or any(earlier.get(key) for key in ('weight','dims','cbm','warehouse','postal'))):break - if not selected_destination and destination_reset(earlier): - excluded.update(destination_reset(earlier));selected_destination=True - if intent['dims'] is None and earlier['dims']: - intent['dims']=earlier['dims'];intent['dim_unit']=earlier['dim_unit'] - for key,value in earlier.items(): - if key in ('mode','text','errors','dim_unit','dims'):continue - if intent.get(key) is None and key not in excluded:intent[key]=deepcopy(value) + earlier_intents.append(earlier) if not is_followup(previous,earlier):break - if explicit_dims and intent.get('packages'): - for piece in intent['packages']: - piece['dims']=intent['dims'];piece['dim_unit']=intent['dim_unit'] - if (intent['warehouse'] or intent.get('unknown_warehouses')) and intent['country'] is None:intent['country']='US' + if earlier_intents: + state=earlier_intents[-1] + for update in list(reversed(earlier_intents[:-1]))+[intent]: + state=merge_intent(update,state) + intent=state + if (intent['warehouse'] or intent.get('unknown_warehouses')) and intent['country'] is None: + intent['country']='US' return intent @@ -56,7 +44,8 @@ def answer_quote(message, conv_id): if re.search(r'理赔|赔偿|破损|丢失|退件|投诉|查单|轨迹|签收', message): return None # Avoid a new read for ordinary chat; short followups need their local context. - if not (US.search(message) or QUOTE_WORDS.search(message) or conv_id or + from .shipping_faq import preparation_topic + if not (US.search(message) or QUOTE_WORDS.search(message) or preparation_topic(message) or conv_id or re.search(r'(?