Anchor weather and other realtime questions on runner /v1/search results (or an explicit refuse constraint) so the model cannot invent dates or temperatures from training memory.
204 lines
6.6 KiB
Go
204 lines
6.6 KiB
Go
package intent
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
// isSecurityViolation 检查是否命中敏感安全与权限红线
|
||
func isSecurityViolation(input string) bool {
|
||
lower := strings.ToLower(input)
|
||
patterns := []string{
|
||
"修改身份", "身份修改", "修改职位", "职位修改", "修改岗位", "岗位修改",
|
||
"修改姓名", "姓名修改", "修改名字", "名字修改",
|
||
"修改部门", "部门修改",
|
||
"修改资质", "资质修改",
|
||
"改成批准人", "改成审核人", "改成编制人",
|
||
"工号改成", "修改工号", "工号修改",
|
||
"修改设置", "修改配置", "系统设置", "系统配置",
|
||
"修改网关", "网关配置", "网关密钥", "大模型网关", "模型网关",
|
||
"修改密码", "重置密码", "提升权限", "越权",
|
||
"修改向量库", "向量库连接", "向量库配置", "修改向量库连接",
|
||
"修改知识库连接", "知识库连接", "知识库配置",
|
||
}
|
||
for _, p := range patterns {
|
||
if strings.Contains(lower, p) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
secRegexes := []string{
|
||
`把.*(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质).*(?:修改|改|换|重置|设为|变为)`,
|
||
`(?:修改|改|换|重置|设为).*(?:我的)?(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质|网关|向量库|知识库连接|密钥|apikey|api_key)`,
|
||
`.*(?:向量库连接|修改向量库|知识库连接|大模型网关密钥|网关密钥).*`,
|
||
}
|
||
for _, r := range secRegexes {
|
||
if matched, _ := regexp.MatchString(r, input); matched {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// isPureUICommand 检查是否为纯前端UI视窗布局与主题控制指令
|
||
func isPureUICommand(input string) bool {
|
||
patterns := []string{
|
||
"打开工具栏", "关闭工具栏",
|
||
"折叠侧边栏", "展开侧边栏", "隐藏侧边栏", "显示侧边栏",
|
||
"折叠左侧栏", "展开左侧栏", "折叠右侧栏", "展开右侧栏",
|
||
"折叠工作台", "展开工作台",
|
||
"切换全屏", "全屏显示", "退出全屏",
|
||
"打开控制台", "关闭控制台",
|
||
"切换主题", "换成深色", "换成浅色",
|
||
"打开抽屉", "展开抽屉", "关闭抽屉",
|
||
"深色模式", "浅色模式", "深色主题", "浅色主题",
|
||
}
|
||
for _, p := range patterns {
|
||
if strings.Contains(input, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// resolveTargetDoc 优先从输入中提取显式受控文档名称,提取不到时回退至上下文的 ActiveDocID
|
||
func resolveTargetDoc(input string, userCtx *ContextSnapshot) string {
|
||
if doc := extractDocName(input); doc != "" {
|
||
return doc
|
||
}
|
||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||
return userCtx.ActiveDocID
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// isRejectReasonQuery 判定是否为询问驳回/退回原因(而非发起驳回执行)
|
||
func isRejectReasonQuery(input string) bool {
|
||
markers := []string{
|
||
"原因", "为何", "为什么", "怎么回事", "什么情况", "什么意见",
|
||
"哪点", "哪些问题", "整改", "意见是什么", "理由", "怎么改", "如何修改",
|
||
"查一下驳回", "看看驳回",
|
||
}
|
||
for _, m := range markers {
|
||
if strings.Contains(input, m) {
|
||
return true
|
||
}
|
||
}
|
||
return strings.Contains(input, "被驳回")
|
||
}
|
||
|
||
// extractFactSlots 从文本中提取结构化事实槽位 (支持补丁合并与补充信息)
|
||
func extractFactSlots(input string) map[string]string {
|
||
slots := make(map[string]string)
|
||
|
||
marketPrefixes := []string{
|
||
"目标市场不仅是", "目标市场不仅有", "目标市场还包括", "目标市场确定为", "目标市场确定是",
|
||
"目标市场改成", "目标市场改为", "目标市场是", "目标市场为",
|
||
"市场改成", "市场改为", "市场是", "市场为", "目标市场除了国内还想考虑",
|
||
}
|
||
for _, p := range marketPrefixes {
|
||
if idx := strings.Index(input, p); idx != -1 {
|
||
val := strings.TrimSpace(input[idx+len(p):])
|
||
val = strings.Trim(val, "::,,。 ")
|
||
if val != "" {
|
||
slots["target_market"] = val
|
||
slots[SlotFactKey] = "target_market"
|
||
slots[SlotFactValue] = val
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
budgetPrefixes := []string{"预算改成", "预算改为", "预算是", "预算为", "预算只有", "预算大概", "预算"}
|
||
for _, p := range budgetPrefixes {
|
||
if idx := strings.Index(input, p); idx != -1 && slots["budget"] == "" {
|
||
val := strings.TrimSpace(input[idx+len(p):])
|
||
val = strings.Trim(val, "::,,。 ")
|
||
if val != "" {
|
||
slots["budget"] = val
|
||
slots[SlotFactKey] = "budget"
|
||
slots[SlotFactValue] = val
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
usePrefixes := []string{"用途是", "用途为", "预期用途是", "场景是", "场景为"}
|
||
for _, p := range usePrefixes {
|
||
if idx := strings.Index(input, p); idx != -1 && slots["intended_use"] == "" {
|
||
val := strings.TrimSpace(input[idx+len(p):])
|
||
val = strings.Trim(val, "::,,。 ")
|
||
if val != "" {
|
||
slots["intended_use"] = val
|
||
slots[SlotFactKey] = "intended_use"
|
||
slots[SlotFactValue] = val
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
return slots
|
||
}
|
||
|
||
func isGreetingOrHelp(input string) bool {
|
||
greetings := []string{"你好", "您好", "早上好", "下午好", "晚上好", "早啊", "在吗", "hi", "hello", "你是谁"}
|
||
for _, g := range greetings {
|
||
if strings.Contains(strings.ToLower(input), g) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func sortCandidates(c []CandidateIntent) {
|
||
for i := 0; i < len(c)-1; i++ {
|
||
for j := i + 1; j < len(c); j++ {
|
||
if c[j].Confidence > c[i].Confidence {
|
||
c[i], c[j] = c[j], c[i]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// IsSmallTalkOrGeneralQuestion 判定是否为通用非业务问句(闲聊/常识提问)。
|
||
func IsSmallTalkOrGeneralQuestion(input string) bool {
|
||
trimmed := strings.TrimSpace(input)
|
||
if trimmed == "" || utf8.RuneCountInString(trimmed) < 3 {
|
||
return false
|
||
}
|
||
if hasStrongBusinessLexicon(trimmed) {
|
||
return false
|
||
}
|
||
questionMarkers := []string{
|
||
"怎么样", "好吗", "什么是", "为什么", "怎么", "哪些", "多少",
|
||
"吗", "呢", "?", "?",
|
||
}
|
||
for _, m := range questionMarkers {
|
||
if strings.Contains(trimmed, m) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// HasStrongBusinessLexicon 是否含强业务词典(项目/法规/文档治理等),供闲聊与实时检索判定复用。
|
||
func HasStrongBusinessLexicon(input string) bool {
|
||
return hasStrongBusinessLexicon(input)
|
||
}
|
||
|
||
func hasStrongBusinessLexicon(input string) bool {
|
||
markers := []string{
|
||
"项目", "阶段", "推进", "法规", "注册", "文档", "审核", "批准", "驳回",
|
||
"报价", "需求", "型检", "成熟度", "工作流", "交付", "立项", "送审", "认领",
|
||
"方案成型", "V0.1", "V0.5", "V1.0", "v0.1", "v0.5", "v1.0",
|
||
"进度", "风险", "预算", "目标市场", "受控",
|
||
}
|
||
for _, m := range markers {
|
||
if strings.Contains(input, m) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|