Skip to content

Commit 549d2ab

Browse files
committed
refactor: streamline Gemini tool configuration and parsing logic with new utility methods
1 parent 9c36769 commit 549d2ab

1 file changed

Lines changed: 122 additions & 33 deletions

File tree

backend/internal/infra/llm/gemini.go

Lines changed: 122 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func buildGeminiRequestBody(input GenerateInput) (map[string]interface{}, error)
153153
webSearchTools = append(webSearchTools, map[string]interface{}{"google_search": map[string]interface{}{}})
154154
}
155155
appendToolDeclarations(payload, providerTools, webSearchTools, buildGeminiTools(toolDefinitions))
156-
applyGeminiToolConfigDefaults(payload, len(providerTools)+len(webSearchTools) > 0, len(toolDefinitions) > 0)
156+
applyGeminiToolConfigDefaults(payload, len(providerTools)+len(webSearchTools) > 0)
157157

158158
if len(systemTextParts) > 0 {
159159
payload["systemInstruction"] = map[string]interface{}{
@@ -167,8 +167,8 @@ func buildGeminiRequestBody(input GenerateInput) (map[string]interface{}, error)
167167
return payload, nil
168168
}
169169

170-
func applyGeminiToolConfigDefaults(payload map[string]interface{}, hasServerSideTools bool, hasFunctionDeclarations bool) {
171-
if !hasServerSideTools || !hasFunctionDeclarations {
170+
func applyGeminiToolConfigDefaults(payload map[string]interface{}, hasServerSideTools bool) {
171+
if !hasServerSideTools {
172172
return
173173
}
174174
toolConfig := asMap(payload["toolConfig"])
@@ -925,26 +925,52 @@ func parseGeminiServerToolCalls(parsed map[string]interface{}) []ToolCall {
925925
}
926926
result := make([]ToolCall, 0)
927927
for _, call := range parseGeminiExplicitServerToolCalls(parsed, -1) {
928-
appendUniqueToolCall(&result, call)
928+
appendGeminiServerToolCallForParse(&result, call)
929929
}
930930
for candidateIndex, rawCandidate := range asSlice(parsed["candidates"]) {
931931
candidate := asMap(rawCandidate)
932932
for _, call := range parseGeminiExplicitServerToolCalls(candidate, candidateIndex) {
933-
appendUniqueToolCall(&result, call)
933+
appendGeminiServerToolCallForParse(&result, call)
934934
}
935935
if call, ok := parseGeminiGoogleSearchServerToolCall(candidate, candidateIndex); ok {
936-
appendUniqueToolCall(&result, call)
936+
appendGeminiServerToolCallForParse(&result, call)
937937
}
938938
if call, ok := parseGeminiURLContextServerToolCall(candidate, candidateIndex); ok {
939-
appendUniqueToolCall(&result, call)
939+
appendGeminiServerToolCallForParse(&result, call)
940940
}
941941
for _, call := range parseGeminiCodeExecutionServerToolCalls(candidate, candidateIndex) {
942-
appendUniqueToolCall(&result, call)
942+
appendGeminiServerToolCallForParse(&result, call)
943943
}
944944
}
945945
return result
946946
}
947947

948+
func appendGeminiServerToolCallForParse(calls *[]ToolCall, call ToolCall) {
949+
if calls == nil {
950+
return
951+
}
952+
if isGeminiSingletonServerToolCall(call) {
953+
for idx, existing := range *calls {
954+
if sameToolCallKind(existing, call) {
955+
incoming := call
956+
incoming.ToolCallID = ""
957+
(*calls)[idx] = mergeToolCall(existing, incoming)
958+
return
959+
}
960+
}
961+
}
962+
appendUniqueToolCall(calls, call)
963+
}
964+
965+
func isGeminiSingletonServerToolCall(call ToolCall) bool {
966+
switch normalizeGeminiServerToolName(firstNonEmptyString(call.ToolName, call.ToolType)) {
967+
case "google_search", "url_context":
968+
return true
969+
default:
970+
return false
971+
}
972+
}
973+
948974
func parseGeminiExplicitServerToolCalls(payload map[string]interface{}, candidateIndex int) []ToolCall {
949975
if len(payload) == 0 {
950976
return nil
@@ -963,9 +989,8 @@ func parseGeminiExplicitServerToolCalls(payload map[string]interface{}, candidat
963989
}
964990
}
965991
}
966-
content := asMap(payload["content"])
967-
for partIndex, raw := range asSlice(content["parts"]) {
968-
part := asMap(raw)
992+
for partIndex, raw := range geminiContentParts(payload) {
993+
part := geminiContentPart(raw)
969994
for _, key := range []string{"serverSideToolInvocation", "server_side_tool_invocation", "toolInvocation", "tool_invocation"} {
970995
if call, ok := parseGeminiExplicitServerToolCall(asMap(part[key]), candidateIndex, partIndex); ok {
971996
result = append(result, call)
@@ -1036,7 +1061,7 @@ func normalizeGeminiExplicitServerToolStatus(status string) string {
10361061
}
10371062

10381063
func parseGeminiGoogleSearchServerToolCall(candidate map[string]interface{}, candidateIndex int) (ToolCall, bool) {
1039-
grounding := asMap(candidate["groundingMetadata"])
1064+
grounding := geminiGroundingMetadata(candidate)
10401065
if !hasGeminiSearchGroundingMetadata(grounding) {
10411066
return ToolCall{}, false
10421067
}
@@ -1062,15 +1087,15 @@ func parseGeminiGoogleSearchServerToolCall(candidate map[string]interface{}, can
10621087
}
10631088

10641089
func parseGeminiURLContextServerToolCall(candidate map[string]interface{}, candidateIndex int) (ToolCall, bool) {
1065-
metadata := asMap(candidate["urlContextMetadata"])
1090+
metadata := geminiURLContextMetadata(candidate)
10661091
urlMetadata := asSlice(metadata["urlMetadata"])
10671092
if len(urlMetadata) == 0 {
10681093
return ToolCall{}, false
10691094
}
10701095
urls := make([]string, 0, len(urlMetadata))
10711096
for _, raw := range urlMetadata {
10721097
item := asMap(raw)
1073-
if uri := firstNonEmptyString(getString(item["retrievedUrl"]), getString(item["url"]), getString(item["uri"])); uri != "" {
1098+
if uri := geminiURLContextItemURL(item); uri != "" {
10741099
urls = append(urls, uri)
10751100
}
10761101
}
@@ -1092,12 +1117,11 @@ func parseGeminiURLContextServerToolCall(candidate map[string]interface{}, candi
10921117
}
10931118

10941119
func parseGeminiCodeExecutionServerToolCalls(candidate map[string]interface{}, candidateIndex int) []ToolCall {
1095-
content := asMap(candidate["content"])
1096-
parts := asSlice(content["parts"])
1120+
parts := geminiContentParts(candidate)
10971121
result := make([]ToolCall, 0)
10981122
currentIndex := -1
10991123
for _, raw := range parts {
1100-
part := asMap(raw)
1124+
part := geminiContentPart(raw)
11011125
executableCode := asMap(part["executableCode"])
11021126
if len(executableCode) > 0 {
11031127
currentIndex++
@@ -1132,6 +1156,65 @@ func parseGeminiCodeExecutionServerToolCalls(candidate map[string]interface{}, c
11321156
return result
11331157
}
11341158

1159+
func geminiGroundingMetadata(candidate map[string]interface{}) map[string]interface{} {
1160+
grounding := asMap(candidate["groundingMetadata"])
1161+
if len(grounding) == 0 {
1162+
grounding = asMap(candidate["grounding_metadata"])
1163+
}
1164+
return normalizeGeminiMapAliases(grounding, map[string]string{
1165+
"grounding_chunks": "groundingChunks",
1166+
"grounding_supports": "groundingSupports",
1167+
"retrieval_metadata": "retrievalMetadata",
1168+
"search_entry_point": "searchEntryPoint",
1169+
"web_search_queries": "webSearchQueries",
1170+
})
1171+
}
1172+
1173+
func geminiURLContextMetadata(candidate map[string]interface{}) map[string]interface{} {
1174+
metadata := asMap(candidate["urlContextMetadata"])
1175+
if len(metadata) == 0 {
1176+
metadata = asMap(candidate["url_context_metadata"])
1177+
}
1178+
return normalizeGeminiMapAliases(metadata, map[string]string{
1179+
"url_metadata": "urlMetadata",
1180+
})
1181+
}
1182+
1183+
func geminiContentParts(candidate map[string]interface{}) []interface{} {
1184+
content := asMap(candidate["content"])
1185+
return asSlice(content["parts"])
1186+
}
1187+
1188+
func geminiContentPart(raw interface{}) map[string]interface{} {
1189+
return normalizeGeminiMapAliases(asMap(raw), map[string]string{
1190+
"code_execution_result": "codeExecutionResult",
1191+
"executable_code": "executableCode",
1192+
})
1193+
}
1194+
1195+
func normalizeGeminiMapAliases(item map[string]interface{}, aliases map[string]string) map[string]interface{} {
1196+
if len(item) == 0 || len(aliases) == 0 {
1197+
return item
1198+
}
1199+
normalized := item
1200+
cloned := false
1201+
for alias, canonical := range aliases {
1202+
if _, ok := normalized[canonical]; ok {
1203+
continue
1204+
}
1205+
value, ok := item[alias]
1206+
if !ok {
1207+
continue
1208+
}
1209+
if !cloned {
1210+
normalized = cloneMap(item)
1211+
cloned = true
1212+
}
1213+
normalized[canonical] = value
1214+
}
1215+
return normalized
1216+
}
1217+
11351218
func geminiGroundingSources(grounding map[string]interface{}) []map[string]interface{} {
11361219
chunks := asSlice(grounding["groundingChunks"])
11371220
if len(chunks) == 0 {
@@ -1140,7 +1223,7 @@ func geminiGroundingSources(grounding map[string]interface{}) []map[string]inter
11401223
result := make([]map[string]interface{}, 0, len(chunks))
11411224
for _, raw := range chunks {
11421225
chunk := asMap(raw)
1143-
for _, key := range []string{"web", "retrievedContext"} {
1226+
for _, key := range []string{"web", "retrievedContext", "retrieved_context"} {
11441227
source := asMap(chunk[key])
11451228
uri := firstNonEmptyString(getString(source["uri"]), getString(source["url"]))
11461229
if uri == "" {
@@ -1163,19 +1246,28 @@ func geminiURLContextItems(items []interface{}) []map[string]interface{} {
11631246
result := make([]map[string]interface{}, 0, len(items))
11641247
for _, raw := range items {
11651248
item := asMap(raw)
1166-
url := firstNonEmptyString(getString(item["retrievedUrl"]), getString(item["url"]), getString(item["uri"]))
1249+
url := geminiURLContextItemURL(item)
11671250
if url == "" {
11681251
continue
11691252
}
11701253
next := map[string]interface{}{"url": url}
1171-
if status := strings.TrimSpace(getString(item["urlRetrievalStatus"])); status != "" {
1254+
if status := strings.TrimSpace(firstNonEmptyString(getString(item["urlRetrievalStatus"]), getString(item["url_retrieval_status"]))); status != "" {
11721255
next["status"] = status
11731256
}
11741257
result = append(result, next)
11751258
}
11761259
return result
11771260
}
11781261

1262+
func geminiURLContextItemURL(item map[string]interface{}) string {
1263+
return firstNonEmptyString(
1264+
getString(item["retrievedUrl"]),
1265+
getString(item["retrieved_url"]),
1266+
getString(item["url"]),
1267+
getString(item["uri"]),
1268+
)
1269+
}
1270+
11791271
func compactGeminiServerToolPayload(payload map[string]interface{}) map[string]interface{} {
11801272
if len(payload) == 0 {
11811273
return nil
@@ -1230,20 +1322,20 @@ func parseGeminiCitations(parsed map[string]interface{}) []string {
12301322
citations := make([]string, 0)
12311323
for _, rawCandidate := range asSlice(parsed["candidates"]) {
12321324
candidate := asMap(rawCandidate)
1233-
grounding := asMap(candidate["groundingMetadata"])
1325+
grounding := geminiGroundingMetadata(candidate)
12341326
for _, raw := range asSlice(grounding["groundingChunks"]) {
12351327
chunk := asMap(raw)
1236-
for _, key := range []string{"web", "retrievedContext"} {
1328+
for _, key := range []string{"web", "retrievedContext", "retrieved_context"} {
12371329
source := asMap(chunk[key])
12381330
if uri := firstNonEmptyString(getString(source["uri"]), getString(source["url"])); uri != "" {
12391331
citations = append(citations, uri)
12401332
}
12411333
}
12421334
}
1243-
urlContext := asMap(candidate["urlContextMetadata"])
1335+
urlContext := geminiURLContextMetadata(candidate)
12441336
for _, raw := range asSlice(urlContext["urlMetadata"]) {
12451337
item := asMap(raw)
1246-
if uri := firstNonEmptyString(getString(item["retrievedUrl"]), getString(item["url"]), getString(item["uri"])); uri != "" {
1338+
if uri := geminiURLContextItemURL(item); uri != "" {
12471339
citations = append(citations, uri)
12481340
}
12491341
}
@@ -1256,17 +1348,14 @@ func parseGeminiServerSideToolUsage(parsed map[string]interface{}) map[string]in
12561348
return nil
12571349
}
12581350
result := make(map[string]int64)
1259-
for _, rawCandidate := range asSlice(parsed["candidates"]) {
1260-
candidate := asMap(rawCandidate)
1261-
if hasGeminiSearchGroundingMetadata(asMap(candidate["groundingMetadata"])) {
1351+
for _, call := range parseGeminiServerToolCalls(parsed) {
1352+
switch normalizeGeminiServerToolName(firstNonEmptyString(call.ToolName, call.ToolType)) {
1353+
case "google_search":
12621354
result["google_search"] = 1
1263-
}
1264-
if len(asSlice(asMap(candidate["urlContextMetadata"])["urlMetadata"])) > 0 {
1355+
case "url_context":
12651356
result["url_context"] = 1
1266-
}
1267-
codeExecutionCount := int64(len(parseGeminiCodeExecutionServerToolCalls(candidate, 0)))
1268-
if codeExecutionCount > 0 {
1269-
result["code_execution"] += codeExecutionCount
1357+
case "code_execution":
1358+
result["code_execution"]++
12701359
}
12711360
}
12721361
if len(result) == 0 {

0 commit comments

Comments
 (0)