matching.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. """Literal subject/policy comparisons used to prioritize, never approve, recommendations."""
  2. from datetime import date
  3. from .dialogue import user_history
  4. MATCH_PROMPT = '''
  5. 另输出match_basis数组,提供主体侧依据与政策条款的比较:
  6. [{dimension:需求/地区/行业/资质/主体/时间/其他,fact_evidence_id:subject_facts编号,
  7. policy_evidence_id:本批evidence编号,status:supports/conflicts/unknown,explanation:简短比较结论}]。
  8. supports表示有针对性的相关线索,不表示已满足全部资格;conflicts用于明确冲突;缺失信息或无法推出结论用unknown。
  9. 优先比较已获取的工商信息、荣誉及用户自述个人信息,不得仅以“是企业”“存续”“都在青浦”作为推荐理由。
  10. 公司的荣誉认定与原文资质要求可形成较强线索,但需核对有效期及其他条件;过期或日期未知不能写成现行有效资质。
  11. 经营范围仅为登记范围,不能直接证明实际项目、行业收入占比或其他资格。注册地址不等于项目地点、户籍或纳税地。
  12. 用户只想找惠企政策时,结合已有企业信息寻找确有关系的方向,不凭空猜测其正计划融资、招人或投资。
  13. 泛问“有什么政策”本身不算具体需求匹配,不得仅凭这句话填写需求supports;需比较实际工商或荣誉线索。
  14. 个人自述的地区、就业或创业情况可以比较相应条件;不存在的个人字段不得猜测。
  15. 没有足够主体事实支持针对性比较时返回空数组。纯政策了解无需做用户资格比较,可返回空数组。
  16. reason必须概括具体的主体事实与政策对应关系以及关键缺口,不得只复述原问题或泛称相关。
  17. 严格区分两个数组:conditions是资格核验,dimension仅地区/主体/行业/时间/其他条件,status仅满足/不满足/待确认;
  18. match_basis是相关依据比较,dimension仅需求/地区/行业/资质/主体/时间/其他,status仅supports/conflicts/unknown。
  19. 两者枚举不可混用;需求和资质不是conditions的dimension,英文状态不能用于conditions。
  20. '''
  21. def subject_facts(handoff):
  22. facts = [{'path': '/question', 'text': handoff['original_question'], 'origin': 'user',
  23. 'provenance': {'type': 'user_statement'}}]
  24. facts.extend({'path': f'/history/{i}/question', 'text': text, 'origin': 'user',
  25. 'provenance': {'type': 'user_statement', 'context': 'same_thread_history'}}
  26. for i, text in enumerate(user_history(handoff)))
  27. facts.extend({'path': '/attachments/' + row['id'], 'text': row['excerpt'], 'origin': 'attachment',
  28. 'provenance': {'type': 'user_attachment', 'verified': False, 'excerpt': True}}
  29. for row in (handoff.get('conversation_context') or {}).get('attachments', []))
  30. profile = (handoff.get('profile') or {}) if handoff.get('needs_company') else {}
  31. source = profile.get('source') or {}
  32. provenance = {k: source[k] for k in ('api_code', 'fetched_at') if k in source}
  33. def visit(value, path, origin, metadata):
  34. if isinstance(value, dict):
  35. for key, child in value.items():
  36. visit(child, path + '/' + str(key).replace('~', '~0').replace('/', '~1'), origin, metadata)
  37. elif value is not None and str(value).strip():
  38. facts.append({'path': path, 'text': str(value), 'origin': origin, **metadata})
  39. for field in ('Area', 'Address', 'Scope', 'EconKind', 'Status', 'StartDate', 'RegistCapi'):
  40. visit(profile.get('data', {}).get(field), '/company/' + field, 'company', {'provenance': provenance})
  41. honors = (handoff.get('honors') or {}) if handoff.get('needs_company') else {}
  42. for index, row in enumerate(honors.get('records', [])):
  43. data = row['data']
  44. current = False
  45. try:
  46. start = date.fromisoformat(str(data.get('BegingDate'))[:10])
  47. end = date.fromisoformat(str(data.get('DeadLine'))[:10])
  48. current = start <= date.today() <= end
  49. except ValueError:
  50. pass
  51. source = row.get('source') or {}
  52. metadata = {'provenance': {k: source[k] for k in ('api_code', 'fetched_at', 'page_index') if k in source},
  53. 'current_validity_evidenced': current}
  54. for field in ('Name', 'Level', 'BegingDate', 'DeadLine'):
  55. visit(data.get(field), f'/honors/{index}/{field}', 'honor', metadata)
  56. return facts
  57. def validate_basis(value, policy_units, facts):
  58. if not isinstance(value, list):
  59. raise ValueError('invalid_match_basis')
  60. result = []
  61. for row in value:
  62. if not isinstance(row, dict):
  63. raise ValueError('invalid_match_dimension')
  64. row = dict(row)
  65. if not isinstance(row.get('dimension'), str):
  66. raise ValueError('invalid_match_dimension')
  67. row['dimension'] = {'其他条件': '其他', '资格': '资质'}.get(row.get('dimension'), row.get('dimension'))
  68. if row.get('dimension') not in ('需求', '地区', '行业', '资质', '主体', '时间', '其他'):
  69. raise ValueError('invalid_match_dimension')
  70. if row.get('status') not in ('supports', 'conflicts', 'unknown'):
  71. raise ValueError('invalid_match_status')
  72. if any(not isinstance(row.get(k), str) or not row[k].strip() for k in ('fact_quote', 'policy_quote', 'explanation')):
  73. raise ValueError('invalid_match_text')
  74. fact = next((f for f in facts if row.get('fact_path') == f['path'] and row['fact_quote'] in f['text']), None)
  75. if fact is None or not any(row.get('policy_path') == unit['path'] and row['policy_quote'] in unit['text'] for unit in policy_units):
  76. raise ValueError('invented_match_evidence')
  77. clean = {k: row[k] for k in ('dimension', 'status', 'fact_path', 'fact_quote', 'policy_path', 'policy_quote', 'explanation')}
  78. clean['fact_origin'] = fact['origin']
  79. clean['provenance'] = fact['provenance']
  80. if fact['origin'] == 'attachment' and clean['status'] == 'supports':
  81. clean['status'] = 'unknown'
  82. clean['explanation'] += '用户附件尚未独立核实,不能据此确认资格。'
  83. if fact['origin'] == 'honor' and not fact.get('current_validity_evidenced') and clean['status'] == 'supports':
  84. clean['status'] = 'unknown'
  85. clean['explanation'] += '该荣誉当前有效性没有完整日期证据,不能视为当前资质符合。'
  86. result.append(clean)
  87. return result
  88. def priority_key(item):
  89. """Evidence coverage, not an approval score; stable ties keep recall order."""
  90. supported = [b for b in item.get('match_basis', []) if b['status'] == 'supports']
  91. strong = {b['dimension'] for b in supported if b['dimension'] in ('需求', '行业', '资质')}
  92. company_or_honor = {b['dimension'] for b in supported if b['fact_origin'] in ('company', 'honor')
  93. and b['dimension'] in ('行业', '资质')}
  94. return ('需求' in strong), len(company_or_honor), len(strong), len({b['dimension'] for b in supported})