|
|
@@ -3,7 +3,6 @@ package com.zsjz.ai.module.agent.tools;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.zsjz.ai.common.base.Query;
|
|
|
import com.zsjz.ai.common.enums.SymbolEnum;
|
|
|
-import com.zsjz.ai.common.model.call.entity.CallOpenInfo;
|
|
|
import com.zsjz.ai.common.model.person.dto.CallDataProfileStatDTO;
|
|
|
import com.zsjz.ai.common.model.person.dto.CallOftenTop10DTO;
|
|
|
import com.zsjz.ai.common.model.person.dto.CallRecordSummaryDTO;
|
|
|
@@ -13,9 +12,7 @@ import com.zsjz.ai.common.model.person.dto.TransDataProfileStatDTO;
|
|
|
import com.zsjz.ai.common.model.person.dto.TransOftenTop10DTO;
|
|
|
import com.zsjz.ai.common.model.person.dto.TransTrendDTO;
|
|
|
import com.zsjz.ai.common.model.person.entity.PersonBasicInfo;
|
|
|
-import com.zsjz.ai.common.model.person.entity.PersonGroup;
|
|
|
import com.zsjz.ai.common.model.person.query.CallOftenTop10Query;
|
|
|
-import com.zsjz.ai.common.model.person.query.CallOperPhoneInfoQuery;
|
|
|
import com.zsjz.ai.common.model.person.query.CallRecordSummaryQuery;
|
|
|
import com.zsjz.ai.common.model.person.query.CallStatStationQuery;
|
|
|
import com.zsjz.ai.common.model.person.query.TransOftenTop10Query;
|
|
|
@@ -38,11 +35,9 @@ import java.math.BigDecimal;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.format.DateTimeParseException;
|
|
|
import java.util.ArrayList;
|
|
|
-import java.util.LinkedHashMap;
|
|
|
import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Locale;
|
|
|
-import java.util.Map;
|
|
|
import java.util.Objects;
|
|
|
import java.util.function.Function;
|
|
|
|
|
|
@@ -98,18 +93,15 @@ public class PersonAnalysisTool {
|
|
|
private final DataProfileService dataProfileService;
|
|
|
private final IntimacyService intimacyService;
|
|
|
private final PersonBasicInfoService personBasicInfoService;
|
|
|
- private final PersonGroupService personGroupService;
|
|
|
private final SqlResultStore sqlResultStore;
|
|
|
|
|
|
public PersonAnalysisTool(DataProfileService dataProfileService,
|
|
|
IntimacyService intimacyService,
|
|
|
PersonBasicInfoService personBasicInfoService,
|
|
|
- PersonGroupService personGroupService,
|
|
|
SqlResultStore sqlResultStore) {
|
|
|
this.dataProfileService = dataProfileService;
|
|
|
this.intimacyService = intimacyService;
|
|
|
this.personBasicInfoService = personBasicInfoService;
|
|
|
- this.personGroupService = personGroupService;
|
|
|
this.sqlResultStore = sqlResultStore;
|
|
|
}
|
|
|
|
|
|
@@ -241,38 +233,6 @@ public class PersonAnalysisTool {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 号码运营商归属查询(补齐未匹配到的号码)
|
|
|
- */
|
|
|
- @Tool(name = "get_person_oper_phone_info",
|
|
|
- description = "查询一批本方号码在运营商开户信息表(call_opening_info)中的归属记录(开户人、证件号、地址、"
|
|
|
- + "运营商等原始字段);表里查不到的号码也会补一行(只带号码、姓名、operatorId),便于模型知道哪些号没匹配上。"
|
|
|
- + "★ 问「某人名下号码的运营商/开户信息」「哪些号码在运营商侧没有记录」时用本工具,不要用 execute_sql 手写 SQL。"
|
|
|
- + "phoneList 必传(至少一个号码);personName 建议一起传,用于过滤开户人并回填未匹配行。"
|
|
|
- + "本接口无分页条件,一次最多返回 10000 行。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock getPersonOperPhoneInfo(
|
|
|
- @ToolParam(name = "query", description = "运营商号码归属查询条件,phoneList 必传")
|
|
|
- PersonToolSpecs.OperPhoneInfoSpec query) {
|
|
|
- PersonToolSpecs.OperPhoneInfoSpec spec = query == null ? new PersonToolSpecs.OperPhoneInfoSpec() : query;
|
|
|
- try {
|
|
|
- List<String> phones = cleanList(spec.phoneList);
|
|
|
- if (phones == null) {
|
|
|
- return ToolResults.error("phoneList 不能为空,请至少传一个本方号码");
|
|
|
- }
|
|
|
- CallOperPhoneInfoQuery q = new CallOperPhoneInfoQuery();
|
|
|
- q.setPersonName(trimToNull(spec.personName));
|
|
|
- q.setOperatorId(trimToNull(spec.operatorId));
|
|
|
- q.setPhoneList(phones);
|
|
|
- applyFetchWindow(q);
|
|
|
-
|
|
|
- List<CallOpenInfo> list = dataProfileService.getCallOperPhoneInfo(q);
|
|
|
- return renderList(list, spec.pageSize);
|
|
|
- } catch (Exception e) {
|
|
|
- return fail("运营商号码归属查询", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// ==================== 交易画像(DataProfileService) ====================
|
|
|
|
|
|
/**
|
|
|
@@ -331,7 +291,7 @@ public class PersonAnalysisTool {
|
|
|
q.setStartDate(parseDate(spec.startDate, "startDate"));
|
|
|
q.setEndDate(parseDate(spec.endDate, "endDate"));
|
|
|
if (spec.transCount != null) {
|
|
|
- q.setTransCount(spec.transCount.intValue());
|
|
|
+ q.setTransCount(spec.transCount);
|
|
|
}
|
|
|
if (spec.transAmount != null) {
|
|
|
q.setTransAmount(toBigDecimal(spec.transAmount, "transAmount"));
|
|
|
@@ -373,7 +333,7 @@ public class PersonAnalysisTool {
|
|
|
q.setStartDate(parseDate(spec.startDate, "startDate"));
|
|
|
q.setEndDate(parseDate(spec.endDate, "endDate"));
|
|
|
if (spec.transCount != null) {
|
|
|
- q.setTransCount(spec.transCount.intValue());
|
|
|
+ q.setTransCount(spec.transCount);
|
|
|
}
|
|
|
if (spec.transAmount != null) {
|
|
|
q.setTransAmount(toBigDecimal(spec.transAmount, "transAmount"));
|
|
|
@@ -381,7 +341,7 @@ public class PersonAnalysisTool {
|
|
|
q.setTransCountSymbol(parseSymbol(spec.transCountSymbol, "transCountSymbol"));
|
|
|
q.setTransAmountSymbol(parseSymbol(spec.transAmountSymbol, "transAmountSymbol"));
|
|
|
if (spec.source != null) {
|
|
|
- q.setSource(spec.source.intValue());
|
|
|
+ q.setSource(spec.source);
|
|
|
}
|
|
|
// 排序硬编码(不暴露给模型),否则 XML 的 <if> 不成立会导致结果完全无序
|
|
|
q.setOrderKey(DEFAULT_TRANS_SUMMARY_ORDER_KEY);
|
|
|
@@ -395,46 +355,6 @@ public class PersonAnalysisTool {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 交易对方名称清单(用于下拉/枚举,去重)
|
|
|
- */
|
|
|
- @Tool(name = "get_trans_other_names",
|
|
|
- description = "列出某个对象在交易数据中出现过的全部交易对方名称(去重,对方信息缺失归入「未知对手」)。"
|
|
|
- + "★ 用于「某人都有哪些交易对手」「把交易对手名称列出来做筛选」这类枚举型问题,"
|
|
|
- + "或作为调用 stat_trans_record_summary / stat_trans_amount_trend_* 前的取值参考。"
|
|
|
- + "只返回名称字符串,没有金额次数;要带统计数字请用 stat_person_trans_top10 或 stat_trans_record_summary。"
|
|
|
- + "personName 必传。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock getTransOtherNames(
|
|
|
- @ToolParam(name = "query", description = "交易对方名称查询条件,personName 必传")
|
|
|
- PersonToolSpecs.TransOtherNameSpec query) {
|
|
|
- PersonToolSpecs.TransOtherNameSpec spec = query == null ? new PersonToolSpecs.TransOtherNameSpec() : query;
|
|
|
- try {
|
|
|
- String personName = trimToNull(spec.personName);
|
|
|
- if (personName == null) {
|
|
|
- return ToolResults.error("personName 不能为空");
|
|
|
- }
|
|
|
- Query q = new Query();
|
|
|
- q.setPersonName(personName);
|
|
|
- applyFetchWindow(q);
|
|
|
-
|
|
|
- Page<String> page = dataProfileService.getTransOtherNamePage(q);
|
|
|
- if (page.getTotal() > FETCH_LIMIT) {
|
|
|
- return overLimit(page.getTotal());
|
|
|
- }
|
|
|
- // 元素是纯字符串,直接丢给 ToolResultTable 无法推导列,包成 [{"otherName": 值}] 再走统一出口
|
|
|
- List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
- for (String otherName : page.getRecords()) {
|
|
|
- Map<String, Object> row = new LinkedHashMap<>();
|
|
|
- row.put("otherName", otherName);
|
|
|
- rows.add(row);
|
|
|
- }
|
|
|
- return ToolResultTable.store(sqlResultStore, null, rows, 1,
|
|
|
- ToolResultTable.normalizePageSize(spec.pageSize));
|
|
|
- } catch (Exception e) {
|
|
|
- return fail("交易对方名称查询", e);
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
/**
|
|
|
* 银行卡交易汇总(含账户状态、余额、开卡/销卡时间)
|
|
|
@@ -528,126 +448,10 @@ public class PersonAnalysisTool {
|
|
|
readOnly = true)
|
|
|
public ToolResultBlock statIntimacySummary(
|
|
|
@ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("亲密度总览统计", personName, intimacyService::intimacySummary);
|
|
|
+ return runIntimacy(personName, intimacyService::intimacySummary);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 亲密度 - 大额转账
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_big_trans",
|
|
|
- description = "统计某个对象的大额转账行为:按交易对手聚合 1 万元以上(其中 5 万元以上为一档)的转账,"
|
|
|
- + "返回对手姓名、命中分值、行为标签与「大额交易次数」明细。"
|
|
|
- + "★ 问「某人给谁大额转账」「大额资金往来对象」「单笔或累计大额交易」时用本工具,"
|
|
|
- + "不要用 execute_sql 手写 SQL —— 大额阈值(1 万 / 5 万)与计分档位来自案件亲密度配置。"
|
|
|
- + "personName 必传。要看含金额的原始交易,用 execute_sql 或 stat_trans_record_summary。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacyBigTrans(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("大额转账分析", personName, intimacyService::bigTrans);
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * 亲密度 - 特殊日期特殊转账
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_special_day_trans",
|
|
|
- description = "统计某个对象在敏感日期(案件配置的特殊日期/节假日)内的特殊转账行为:按交易对手聚合,"
|
|
|
- + "返回对手姓名、命中分值、行为标签与「特殊日期特殊转账」次数明细,按次数分档计分。"
|
|
|
- + "★ 问「敏感日期期间谁给谁转过账」「节假日/特殊日期的资金往来」时用本工具,不要用 execute_sql 手写 SQL。"
|
|
|
- + "personName 必传。要看敏感日期内的原始话单式明细请用 stat_call_sensitive_detail(通话)或 execute_sql(交易)。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacySpecialDayTrans(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("特殊日期特殊转账分析", personName, intimacyService::specialDayTrans);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 异卡同号
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_same_phone_binding",
|
|
|
- description = "统计某个对象的「异卡同号」行为:在本方所属人员组中,找出把银行卡预留手机号登记成本方号码的其他人,"
|
|
|
- + "按人返回命中分值、行为标签与「异卡同号」关联卡数明细。"
|
|
|
- + "★ 问「谁的银行卡用了某人的手机号」「异卡同号关联了哪些人」时用本工具,不要用 execute_sql 手写 SQL —— "
|
|
|
- + "该行为依赖人员组的「主体」标识,只有主体人员调用才会返回结果,非主体一律返回空。"
|
|
|
- + "personName 必传。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacySamePhoneBinding(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("异卡同号分析", personName, intimacyService::samePhoneBinding);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 频繁联系(通话次数 TOP)
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_keep_call",
|
|
|
- description = "统计某个对象的频繁联系行为:按通话次数对联系对象排名,取前 20 名并按档位(前 5 / 6~10 / 11~20)计分,"
|
|
|
- + "返回对手姓名、命中分值、行为标签与「联系次数」明细。已过滤 12/95/96/106 开头的服务号与短号。"
|
|
|
- + "★ 问「谁和某人联系最频繁」「通话次数最多的联系人」时用本工具,不要用 execute_sql 手写 SQL —— "
|
|
|
- + "档位分值与短号过滤口径已固化。personName 必传。"
|
|
|
- + "若关心的是「跨月持续联系」,请用 stat_call_continuous_summary。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacyKeepCall(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("频繁联系分析", personName, intimacyService::keepCall);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 特殊日期联系(通话)
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_special_day_call",
|
|
|
- description = "统计某个对象在敏感日期(案件配置的特殊日期/节假日)内的通话联系:按通话次数对联系对象排名,"
|
|
|
- + "取前 20 名并按档位计分,返回对手姓名、命中分值、行为标签与「特殊日期通话次数」明细。"
|
|
|
- + "★ 问「敏感日期期间和谁联系过」「节假日/特殊日期的通话对象」时用本工具,不要用 execute_sql 手写 SQL。"
|
|
|
- + "personName 必传。要看敏感日期内的原始话单请用 stat_call_sensitive_detail。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacySpecialDayCall(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("特殊日期联系分析", personName, intimacyService::specialDayCall);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 频繁交易
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_keep_trans",
|
|
|
- description = "统计某个对象的频繁交易行为:按交易笔数对交易对手排名,取前 20 名并按档位(前 5 / 6~10 / 11~20 / 20 以后)计分,"
|
|
|
- + "返回对手姓名、命中分值、行为标签与「交易次数」明细。"
|
|
|
- + "★ 问「谁和某人交易最频繁」「交易笔数最多的对手」时用本工具,不要用 execute_sql 手写 SQL —— "
|
|
|
- + "档位分值来自案件亲密度配置,缺失时有内置兜底值。personName 必传。"
|
|
|
- + "若要看交易金额维度,请用 stat_person_trans_top10。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacyKeepTrans(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("频繁交易分析", personName, intimacyService::keepTrans);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 同交通工具出行
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_travel_together",
|
|
|
- description = "统计某个对象的同交通工具出行行为:把火车/航班记录与同行轨迹按「出行时间 + 车次/航班号」配对,"
|
|
|
- + "找出与其同行的其他人,按同行次数分档计分,返回同行人姓名、命中分值、行为标签与「同行次数」明细。"
|
|
|
- + "★ 问「某人和谁一起坐过车/飞机」「同行人」「一起出行的次数」时用本工具,不要用 execute_sql 手写 SQL —— "
|
|
|
- + "配对口径(时间+班次)与分档规则已固化。personName 必传。"
|
|
|
- + "注意:本行为需要遍历全案人员做轨迹匹配,返回较慢且结果可能为空(没有同行记录时)。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacyTravelTogether(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("同交通工具出行分析", personName, intimacyService::travelTogether);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 亲密度 - 夜间通话
|
|
|
- */
|
|
|
- @Tool(name = "stat_intimacy_night_call",
|
|
|
- description = "统计某个对象的夜间通话行为:按对方号码聚合夜间通话次数,只保留有夜间通话的月份数 ≥ 2 的号码,"
|
|
|
- + "取次数前 20 名并按档位计分,返回对方号码、命中分值、行为标签与「夜间通话次数」明细。"
|
|
|
- + "★ 问「某人深夜常联系谁」「夜间通话对象」时用本工具,不要用 execute_sql 手写 SQL —— "
|
|
|
- + "夜间时段、跨月阈值与档位分值已固化。personName 必传。"
|
|
|
- + "需要按小时段下钻或自定义夜间时段,请用 stat_call_night_summary / stat_call_night_detail。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock statIntimacyNightCall(
|
|
|
- @ToolParam(name = "personName", description = "本方(机主)姓名,精确匹配,必传,如「张三」") String personName) {
|
|
|
- return runIntimacy("夜间通话分析", personName, intimacyService::nightCall);
|
|
|
- }
|
|
|
|
|
|
// ==================== 人员与人员组(只读列表) ====================
|
|
|
|
|
|
@@ -670,52 +474,7 @@ public class PersonAnalysisTool {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 全案对象人员姓名清单
|
|
|
- */
|
|
|
- @Tool(name = "list_person_names",
|
|
|
- description = "列出当前案件全部对象人员的姓名(仅姓名,最省 token)。"
|
|
|
- + "★ 问「本案有哪些人」「都有谁」这类只需姓名的场景用本工具;"
|
|
|
- + "需要证件号、住址、名下号码等基本信息请用 list_persons。无入参。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock listPersonNames() {
|
|
|
- try {
|
|
|
- List<String> names = personBasicInfoService.getPersonNames();
|
|
|
- // ★ 不能直接把 List<String> 交给 ToolResultTable:它按「POJO 列表」处理,
|
|
|
- // 会把每个姓名当成对象反序列化,抛
|
|
|
- // "Cannot construct instance of java.util.LinkedHashMap ... from String value"
|
|
|
- // (2026-09-20 在 MCP 链路上实测到,空人名时不会触发,所以一直没暴露)。
|
|
|
- // 这里显式包成单列,列名与 list_persons 的姓名字段保持一致。
|
|
|
- List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
- if (names != null) {
|
|
|
- for (String name : names) {
|
|
|
- Map<String, Object> row = new LinkedHashMap<>();
|
|
|
- row.put("name", name);
|
|
|
- rows.add(row);
|
|
|
- }
|
|
|
- }
|
|
|
- return renderList(rows, null);
|
|
|
- } catch (Exception e) {
|
|
|
- return fail("对象人员姓名查询", e);
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * 人员组(团伙/关系组)清单
|
|
|
- */
|
|
|
- @Tool(name = "list_person_groups",
|
|
|
- description = "列出当前案件配置的人员组(关系组):组名、备注,以及组内成员列表(成员姓名、是否主体、主体标识)。"
|
|
|
- + "★ 问「本案分了哪些组」「某人和谁在同一组」「谁是主体人员」时用本工具,不要用 execute_sql 手写 SQL。"
|
|
|
- + "无入参。注意「异卡同号」等亲密度行为依赖人员组的主体标识,可用本工具确认主体是谁。",
|
|
|
- readOnly = true)
|
|
|
- public ToolResultBlock listPersonGroups() {
|
|
|
- try {
|
|
|
- List<PersonGroup> list = personGroupService.getList();
|
|
|
- return renderList(list, null);
|
|
|
- } catch (Exception e) {
|
|
|
- return fail("人员组列表查询", e);
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
// ==================== 内部实现 ====================
|
|
|
|
|
|
@@ -725,7 +484,7 @@ public class PersonAnalysisTool {
|
|
|
* <p>9 个亲密度方法签名完全一致(单 String 入参、返回 {@code List<IntimacyDTO>}),
|
|
|
* 抽出来既避免重复代码,也保证「空姓名报错文案」与「超限判定」在 9 个工具间完全一致。
|
|
|
*/
|
|
|
- private ToolResultBlock runIntimacy(String action, String personName,
|
|
|
+ private ToolResultBlock runIntimacy(String personName,
|
|
|
Function<String, List<IntimacyDTO>> actionFn) {
|
|
|
String name = trimToNull(personName);
|
|
|
if (name == null) {
|
|
|
@@ -734,7 +493,7 @@ public class PersonAnalysisTool {
|
|
|
try {
|
|
|
return renderList(actionFn.apply(name), null);
|
|
|
} catch (Exception e) {
|
|
|
- return fail(action, e);
|
|
|
+ return fail("亲密度总览统计", e);
|
|
|
}
|
|
|
}
|
|
|
|