297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
package service
|
||
|
||
import "strings"
|
||
|
||
// ToolContinuationSignals 聚合工具续链相关信号,避免重复遍历 input。
|
||
type ToolContinuationSignals struct {
|
||
HasFunctionCallOutput bool
|
||
HasFunctionCallOutputMissingCallID bool
|
||
HasToolCallContext bool
|
||
HasItemReference bool
|
||
HasItemReferenceForAllCallIDs bool
|
||
FunctionCallOutputCallIDs []string
|
||
}
|
||
|
||
// FunctionCallOutputValidation 汇总 function_call_output 关联性校验结果。
|
||
type FunctionCallOutputValidation struct {
|
||
HasFunctionCallOutput bool
|
||
HasToolCallContext bool
|
||
HasFunctionCallOutputMissingCallID bool
|
||
HasItemReferenceForAllCallIDs bool
|
||
}
|
||
|
||
// NeedsToolContinuation 判定请求是否需要工具调用续链处理。
|
||
// 满足以下任一信号即视为续链:previous_response_id、input 内包含 function_call_output/item_reference、
|
||
// 或显式声明 tools/tool_choice。
|
||
func NeedsToolContinuation(reqBody map[string]any) bool {
|
||
if reqBody == nil {
|
||
return false
|
||
}
|
||
if hasNonEmptyString(reqBody["previous_response_id"]) {
|
||
return true
|
||
}
|
||
if hasToolsSignal(reqBody) {
|
||
return true
|
||
}
|
||
if hasToolChoiceSignal(reqBody) {
|
||
return true
|
||
}
|
||
input, ok := reqBody["input"].([]any)
|
||
if !ok {
|
||
return false
|
||
}
|
||
for _, item := range input {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
itemType, _ := itemMap["type"].(string)
|
||
if itemType == "function_call_output" || itemType == "item_reference" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// AnalyzeToolContinuationSignals 单次遍历 input,提取 function_call_output/tool_call/item_reference 相关信号。
|
||
func AnalyzeToolContinuationSignals(reqBody map[string]any) ToolContinuationSignals {
|
||
signals := ToolContinuationSignals{}
|
||
if reqBody == nil {
|
||
return signals
|
||
}
|
||
input, ok := reqBody["input"].([]any)
|
||
if !ok {
|
||
return signals
|
||
}
|
||
|
||
var callIDs map[string]struct{}
|
||
var referenceIDs map[string]struct{}
|
||
|
||
for _, item := range input {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
itemType, _ := itemMap["type"].(string)
|
||
switch itemType {
|
||
case "tool_call", "function_call":
|
||
callID, _ := itemMap["call_id"].(string)
|
||
if strings.TrimSpace(callID) != "" {
|
||
signals.HasToolCallContext = true
|
||
}
|
||
case "function_call_output":
|
||
signals.HasFunctionCallOutput = true
|
||
callID, _ := itemMap["call_id"].(string)
|
||
callID = strings.TrimSpace(callID)
|
||
if callID == "" {
|
||
signals.HasFunctionCallOutputMissingCallID = true
|
||
continue
|
||
}
|
||
if callIDs == nil {
|
||
callIDs = make(map[string]struct{})
|
||
}
|
||
callIDs[callID] = struct{}{}
|
||
case "item_reference":
|
||
signals.HasItemReference = true
|
||
idValue, _ := itemMap["id"].(string)
|
||
idValue = strings.TrimSpace(idValue)
|
||
if idValue == "" {
|
||
continue
|
||
}
|
||
if referenceIDs == nil {
|
||
referenceIDs = make(map[string]struct{})
|
||
}
|
||
referenceIDs[idValue] = struct{}{}
|
||
}
|
||
}
|
||
|
||
if len(callIDs) == 0 {
|
||
return signals
|
||
}
|
||
signals.FunctionCallOutputCallIDs = make([]string, 0, len(callIDs))
|
||
allReferenced := len(referenceIDs) > 0
|
||
for callID := range callIDs {
|
||
signals.FunctionCallOutputCallIDs = append(signals.FunctionCallOutputCallIDs, callID)
|
||
if allReferenced {
|
||
if _, ok := referenceIDs[callID]; !ok {
|
||
allReferenced = false
|
||
}
|
||
}
|
||
}
|
||
signals.HasItemReferenceForAllCallIDs = allReferenced
|
||
return signals
|
||
}
|
||
|
||
// ValidateFunctionCallOutputContext 为 handler 提供低开销校验结果:
|
||
// 1) 无 function_call_output 直接返回
|
||
// 2) 若已存在 tool_call/function_call 上下文则提前返回
|
||
// 3) 仅在无工具上下文时才构建 call_id / item_reference 集合
|
||
func ValidateFunctionCallOutputContext(reqBody map[string]any) FunctionCallOutputValidation {
|
||
result := FunctionCallOutputValidation{}
|
||
if reqBody == nil {
|
||
return result
|
||
}
|
||
input, ok := reqBody["input"].([]any)
|
||
if !ok {
|
||
return result
|
||
}
|
||
|
||
for _, item := range input {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
itemType, _ := itemMap["type"].(string)
|
||
switch itemType {
|
||
case "function_call_output":
|
||
result.HasFunctionCallOutput = true
|
||
case "tool_call", "function_call":
|
||
callID, _ := itemMap["call_id"].(string)
|
||
if strings.TrimSpace(callID) != "" {
|
||
result.HasToolCallContext = true
|
||
}
|
||
}
|
||
if result.HasFunctionCallOutput && result.HasToolCallContext {
|
||
return result
|
||
}
|
||
}
|
||
|
||
if !result.HasFunctionCallOutput || result.HasToolCallContext {
|
||
return result
|
||
}
|
||
|
||
callIDs := make(map[string]struct{})
|
||
referenceIDs := make(map[string]struct{})
|
||
for _, item := range input {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
itemType, _ := itemMap["type"].(string)
|
||
switch itemType {
|
||
case "function_call_output":
|
||
callID, _ := itemMap["call_id"].(string)
|
||
callID = strings.TrimSpace(callID)
|
||
if callID == "" {
|
||
result.HasFunctionCallOutputMissingCallID = true
|
||
continue
|
||
}
|
||
callIDs[callID] = struct{}{}
|
||
case "item_reference":
|
||
idValue, _ := itemMap["id"].(string)
|
||
idValue = strings.TrimSpace(idValue)
|
||
if idValue == "" {
|
||
continue
|
||
}
|
||
referenceIDs[idValue] = struct{}{}
|
||
}
|
||
}
|
||
|
||
if len(callIDs) == 0 || len(referenceIDs) == 0 {
|
||
return result
|
||
}
|
||
allReferenced := true
|
||
for callID := range callIDs {
|
||
if _, ok := referenceIDs[callID]; !ok {
|
||
allReferenced = false
|
||
break
|
||
}
|
||
}
|
||
result.HasItemReferenceForAllCallIDs = allReferenced
|
||
return result
|
||
}
|
||
|
||
// HasFunctionCallOutput 判断 input 是否包含 function_call_output,用于触发续链校验。
|
||
func HasFunctionCallOutput(reqBody map[string]any) bool {
|
||
return AnalyzeToolContinuationSignals(reqBody).HasFunctionCallOutput
|
||
}
|
||
|
||
// HasToolCallContext 判断 input 是否包含带 call_id 的 tool_call/function_call,
|
||
// 用于判断 function_call_output 是否具备可关联的上下文。
|
||
func HasToolCallContext(reqBody map[string]any) bool {
|
||
return AnalyzeToolContinuationSignals(reqBody).HasToolCallContext
|
||
}
|
||
|
||
// FunctionCallOutputCallIDs 提取 input 中 function_call_output 的 call_id 集合。
|
||
// 仅返回非空 call_id,用于与 item_reference.id 做匹配校验。
|
||
func FunctionCallOutputCallIDs(reqBody map[string]any) []string {
|
||
return AnalyzeToolContinuationSignals(reqBody).FunctionCallOutputCallIDs
|
||
}
|
||
|
||
// HasFunctionCallOutputMissingCallID 判断是否存在缺少 call_id 的 function_call_output。
|
||
func HasFunctionCallOutputMissingCallID(reqBody map[string]any) bool {
|
||
return AnalyzeToolContinuationSignals(reqBody).HasFunctionCallOutputMissingCallID
|
||
}
|
||
|
||
// HasItemReferenceForCallIDs 判断 item_reference.id 是否覆盖所有 call_id。
|
||
// 用于仅依赖引用项完成续链场景的校验。
|
||
func HasItemReferenceForCallIDs(reqBody map[string]any, callIDs []string) bool {
|
||
if reqBody == nil || len(callIDs) == 0 {
|
||
return false
|
||
}
|
||
input, ok := reqBody["input"].([]any)
|
||
if !ok {
|
||
return false
|
||
}
|
||
referenceIDs := make(map[string]struct{})
|
||
for _, item := range input {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
itemType, _ := itemMap["type"].(string)
|
||
if itemType != "item_reference" {
|
||
continue
|
||
}
|
||
idValue, _ := itemMap["id"].(string)
|
||
idValue = strings.TrimSpace(idValue)
|
||
if idValue == "" {
|
||
continue
|
||
}
|
||
referenceIDs[idValue] = struct{}{}
|
||
}
|
||
if len(referenceIDs) == 0 {
|
||
return false
|
||
}
|
||
for _, callID := range callIDs {
|
||
if _, ok := referenceIDs[strings.TrimSpace(callID)]; !ok {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// hasNonEmptyString 判断字段是否为非空字符串。
|
||
func hasNonEmptyString(value any) bool {
|
||
stringValue, ok := value.(string)
|
||
return ok && strings.TrimSpace(stringValue) != ""
|
||
}
|
||
|
||
// hasToolsSignal 判断 tools 字段是否显式声明(存在且不为空)。
|
||
func hasToolsSignal(reqBody map[string]any) bool {
|
||
raw, exists := reqBody["tools"]
|
||
if !exists || raw == nil {
|
||
return false
|
||
}
|
||
if tools, ok := raw.([]any); ok {
|
||
return len(tools) > 0
|
||
}
|
||
return false
|
||
}
|
||
|
||
// hasToolChoiceSignal 判断 tool_choice 是否显式声明(非空或非 nil)。
|
||
func hasToolChoiceSignal(reqBody map[string]any) bool {
|
||
raw, exists := reqBody["tool_choice"]
|
||
if !exists || raw == nil {
|
||
return false
|
||
}
|
||
switch value := raw.(type) {
|
||
case string:
|
||
return strings.TrimSpace(value) != ""
|
||
case map[string]any:
|
||
return len(value) > 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|