Mirrowel commited on
Commit
ba6dcaa
Β·
1 Parent(s): bccb879

fix(antigravity): πŸ› improve function call response pairing with recovery strategies

Browse files

Enhanced the response grouping logic in AntigravityProvider to handle ID mismatches between function calls and their responses more robustly.

- Added three-tier matching strategy: direct ID match, function name match, then order-based fallback
- Function names are now tracked alongside IDs for orphan response recovery
- Responses with "unknown_function" can now be repaired with correct function names
- Placeholder responses are automatically created for completely missing tool responses
- Fixed insertion position tracking to ensure responses are added immediately after their corresponding model message
- Pending groups are now processed in reverse order to prevent index shifting during insertion
- Re-enabled debug logging for response collection and group satisfaction
- Added comprehensive recovery logging for troubleshooting pairing issues

This prevents conversation history corruption when client/proxy systems mutate response IDs or when responses are lost during context processing.

src/rotator_library/providers/antigravity_provider.py CHANGED
@@ -2160,9 +2160,18 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
2160
  to grouped format (model with calls, user with all responses).
2161
 
2162
  IMPORTANT: Preserves ID-based pairing to prevent mismatches.
 
 
 
 
 
2163
  """
2164
  new_contents = []
2165
- pending_groups = [] # List of {"ids": [id1, id2, ...], "call_indices": [...]}
 
 
 
 
2166
  collected_responses = {} # Dict mapping ID -> response_part
2167
 
2168
  for content in contents:
@@ -2182,7 +2191,9 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
2182
  f"Ignoring duplicate - this may indicate malformed conversation history."
2183
  )
2184
  continue
2185
- # lib_logger.debug(f"[Grouping] Collected response for ID: {resp_id}")
 
 
2186
  collected_responses[resp_id] = resp
2187
 
2188
  # Try to satisfy pending groups (newest first)
@@ -2197,10 +2208,10 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
2197
  collected_responses.pop(gid) for gid in group_ids
2198
  ]
2199
  new_contents.append({"parts": group_responses, "role": "user"})
2200
- # lib_logger.debug(
2201
- # f"[Grouping] Satisfied group with {len(group_responses)} responses: "
2202
- # f"ids={group_ids}"
2203
- # )
2204
  pending_groups.pop(i)
2205
  break
2206
  continue
@@ -2213,14 +2224,22 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
2213
  fc.get("functionCall", {}).get("id", "") for fc in func_calls
2214
  ]
2215
  call_ids = [cid for cid in call_ids if cid] # Filter empty IDs
 
 
 
 
 
 
2216
  if call_ids:
2217
  lib_logger.debug(
2218
- f"[Grouping] Created pending group expecting {len(call_ids)} responses: ids={call_ids}"
 
2219
  )
2220
  pending_groups.append(
2221
  {
2222
  "ids": call_ids,
2223
- "call_indices": list(range(len(func_calls))),
 
2224
  }
2225
  )
2226
  else:
@@ -2228,37 +2247,120 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
2228
 
2229
  # Handle remaining groups (shouldn't happen in well-formed conversations)
2230
  # Attempt recovery by matching orphans to unsatisfied calls
 
 
 
2231
  for group in pending_groups:
2232
  group_ids = group["ids"]
 
 
2233
  group_responses = []
2234
 
2235
- for expected_id in group_ids:
 
 
 
 
 
 
 
2236
  if expected_id in collected_responses:
 
2237
  group_responses.append(collected_responses.pop(expected_id))
 
 
 
2238
  elif collected_responses:
2239
- # Recovery: Match with an orphan response
2240
- # This handles cases where client/proxy mutates IDs (e.g. toolu_ -> call_)
2241
- # Get the first available orphan ID to maintain order
2242
- orphan_id = next(iter(collected_responses))
2243
- orphan_resp = collected_responses.pop(orphan_id)
2244
 
2245
- # Fix the ID in the response to match the call
2246
- orphan_resp["functionResponse"]["id"] = expected_id
 
 
 
 
 
 
 
 
 
 
2247
 
2248
- lib_logger.warning(
2249
- f"[Grouping] Auto-repaired ID mismatch: mapped response '{orphan_id}' "
2250
- f"to call '{expected_id}'"
2251
- )
2252
- group_responses.append(orphan_resp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2253
 
2254
- if group_responses:
2255
- new_contents.append({"parts": group_responses, "role": "user"})
 
 
 
 
2256
 
2257
- if len(group_responses) != len(group_ids):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2258
  lib_logger.warning(
2259
- f"[Grouping] Partial group satisfaction after repair: "
2260
- f"expected {len(group_ids)}, got {len(group_responses)} responses"
2261
  )
 
 
 
 
 
 
 
 
 
 
 
2262
 
2263
  # Warn about unmatched responses
2264
  if collected_responses:
 
2160
  to grouped format (model with calls, user with all responses).
2161
 
2162
  IMPORTANT: Preserves ID-based pairing to prevent mismatches.
2163
+ When IDs don't match, attempts recovery by:
2164
+ 1. Matching by function name first
2165
+ 2. Matching by order if names don't match
2166
+ 3. Inserting placeholder responses if responses are missing
2167
+ 4. Inserting responses at the CORRECT position (after their corresponding call)
2168
  """
2169
  new_contents = []
2170
+ # Each pending group tracks:
2171
+ # - ids: expected response IDs
2172
+ # - func_names: expected function names (for orphan matching)
2173
+ # - insert_after_idx: position in new_contents where model message was added
2174
+ pending_groups = []
2175
  collected_responses = {} # Dict mapping ID -> response_part
2176
 
2177
  for content in contents:
 
2191
  f"Ignoring duplicate - this may indicate malformed conversation history."
2192
  )
2193
  continue
2194
+ lib_logger.debug(
2195
+ f"[Grouping] Collected response for ID: {resp_id}"
2196
+ )
2197
  collected_responses[resp_id] = resp
2198
 
2199
  # Try to satisfy pending groups (newest first)
 
2208
  collected_responses.pop(gid) for gid in group_ids
2209
  ]
2210
  new_contents.append({"parts": group_responses, "role": "user"})
2211
+ lib_logger.debug(
2212
+ f"[Grouping] Satisfied group with {len(group_responses)} responses: "
2213
+ f"ids={group_ids}"
2214
+ )
2215
  pending_groups.pop(i)
2216
  break
2217
  continue
 
2224
  fc.get("functionCall", {}).get("id", "") for fc in func_calls
2225
  ]
2226
  call_ids = [cid for cid in call_ids if cid] # Filter empty IDs
2227
+
2228
+ # Also extract function names for orphan matching
2229
+ func_names = [
2230
+ fc.get("functionCall", {}).get("name", "") for fc in func_calls
2231
+ ]
2232
+
2233
  if call_ids:
2234
  lib_logger.debug(
2235
+ f"[Grouping] Created pending group expecting {len(call_ids)} responses: "
2236
+ f"ids={call_ids}, names={func_names}"
2237
  )
2238
  pending_groups.append(
2239
  {
2240
  "ids": call_ids,
2241
+ "func_names": func_names,
2242
+ "insert_after_idx": len(new_contents) - 1,
2243
  }
2244
  )
2245
  else:
 
2247
 
2248
  # Handle remaining groups (shouldn't happen in well-formed conversations)
2249
  # Attempt recovery by matching orphans to unsatisfied calls
2250
+ # Process in REVERSE order of insert_after_idx so insertions don't shift indices
2251
+ pending_groups.sort(key=lambda g: g["insert_after_idx"], reverse=True)
2252
+
2253
  for group in pending_groups:
2254
  group_ids = group["ids"]
2255
+ group_func_names = group.get("func_names", [])
2256
+ insert_idx = group["insert_after_idx"] + 1
2257
  group_responses = []
2258
 
2259
+ lib_logger.debug(
2260
+ f"[Grouping Recovery] Processing unsatisfied group: "
2261
+ f"ids={group_ids}, names={group_func_names}, insert_at={insert_idx}"
2262
+ )
2263
+
2264
+ for i, expected_id in enumerate(group_ids):
2265
+ expected_name = group_func_names[i] if i < len(group_func_names) else ""
2266
+
2267
  if expected_id in collected_responses:
2268
+ # Direct ID match
2269
  group_responses.append(collected_responses.pop(expected_id))
2270
+ lib_logger.debug(
2271
+ f"[Grouping Recovery] Direct ID match for '{expected_id}'"
2272
+ )
2273
  elif collected_responses:
2274
+ # Try to find orphan with matching function name first
2275
+ matched_orphan_id = None
 
 
 
2276
 
2277
+ # First pass: match by function name
2278
+ for orphan_id, orphan_resp in collected_responses.items():
2279
+ orphan_name = orphan_resp.get("functionResponse", {}).get(
2280
+ "name", ""
2281
+ )
2282
+ # Match if names are equal, or if orphan has "unknown_function" (can be fixed)
2283
+ if orphan_name == expected_name:
2284
+ matched_orphan_id = orphan_id
2285
+ lib_logger.debug(
2286
+ f"[Grouping Recovery] Matched orphan '{orphan_id}' by name '{orphan_name}'"
2287
+ )
2288
+ break
2289
 
2290
+ # Second pass: if no name match, try "unknown_function" orphans
2291
+ if not matched_orphan_id:
2292
+ for orphan_id, orphan_resp in collected_responses.items():
2293
+ orphan_name = orphan_resp.get("functionResponse", {}).get(
2294
+ "name", ""
2295
+ )
2296
+ if orphan_name == "unknown_function":
2297
+ matched_orphan_id = orphan_id
2298
+ lib_logger.debug(
2299
+ f"[Grouping Recovery] Matched unknown_function orphan '{orphan_id}' "
2300
+ f"to expected '{expected_name}'"
2301
+ )
2302
+ break
2303
+
2304
+ # Third pass: if still no match, take first available (order-based)
2305
+ if not matched_orphan_id:
2306
+ matched_orphan_id = next(iter(collected_responses))
2307
+ lib_logger.debug(
2308
+ f"[Grouping Recovery] No name match, using first available orphan '{matched_orphan_id}'"
2309
+ )
2310
 
2311
+ if matched_orphan_id:
2312
+ orphan_resp = collected_responses.pop(matched_orphan_id)
2313
+
2314
+ # Fix the ID in the response to match the call
2315
+ old_id = orphan_resp["functionResponse"].get("id", "")
2316
+ orphan_resp["functionResponse"]["id"] = expected_id
2317
 
2318
+ # Fix the name if it was "unknown_function"
2319
+ if (
2320
+ orphan_resp["functionResponse"].get("name")
2321
+ == "unknown_function"
2322
+ and expected_name
2323
+ ):
2324
+ orphan_resp["functionResponse"]["name"] = expected_name
2325
+ lib_logger.info(
2326
+ f"[Grouping Recovery] Fixed function name from 'unknown_function' to '{expected_name}'"
2327
+ )
2328
+
2329
+ lib_logger.warning(
2330
+ f"[Grouping] Auto-repaired ID mismatch: mapped response '{old_id}' "
2331
+ f"to call '{expected_id}' (function: {expected_name})"
2332
+ )
2333
+ group_responses.append(orphan_resp)
2334
+ else:
2335
+ # No responses available - create placeholder
2336
+ placeholder_resp = {
2337
+ "functionResponse": {
2338
+ "name": expected_name or "unknown_function",
2339
+ "response": {
2340
+ "result": {
2341
+ "error": "Tool response was lost during context processing. "
2342
+ "This is a recovered placeholder.",
2343
+ "recovered": True,
2344
+ }
2345
+ },
2346
+ "id": expected_id,
2347
+ }
2348
+ }
2349
  lib_logger.warning(
2350
+ f"[Grouping Recovery] Created placeholder response for missing tool: "
2351
+ f"id='{expected_id}', name='{expected_name}'"
2352
  )
2353
+ group_responses.append(placeholder_resp)
2354
+
2355
+ if group_responses:
2356
+ # Insert at the correct position (right after the model message with the calls)
2357
+ new_contents.insert(
2358
+ insert_idx, {"parts": group_responses, "role": "user"}
2359
+ )
2360
+ lib_logger.info(
2361
+ f"[Grouping Recovery] Inserted {len(group_responses)} responses at position {insert_idx} "
2362
+ f"(expected {len(group_ids)})"
2363
+ )
2364
 
2365
  # Warn about unmatched responses
2366
  if collected_responses: